diff --git a/appServer.go b/appServer.go new file mode 100644 index 0000000..7051ce6 --- /dev/null +++ b/appServer.go @@ -0,0 +1,307 @@ +//go:build !wasm + +package rui + +import ( + "context" + _ "embed" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "os/exec" + "runtime" + "strconv" + "strings" + "time" +) + +//go:embed app_socket.js +var socketScripts string + +type application struct { + server *http.Server + params AppParams + createContentFunc func(Session) SessionContent + sessions map[int]Session +} + +func (app *application) getStartPage() string { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString("\n\n") + getStartPage(buffer, app.params, socketScripts) + buffer.WriteString("\n") + return buffer.String() +} + +func (app *application) Finish() { + for _, session := range app.sessions { + session.close() + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := app.server.Shutdown(ctx); err != nil { + log.Println(err.Error()) + } +} + +func (app *application) nextSessionID() int { + n := rand.Intn(0x7FFFFFFE) + 1 + _, ok := app.sessions[n] + for ok { + n = rand.Intn(0x7FFFFFFE) + 1 + _, ok = app.sessions[n] + } + return n +} + +func (app *application) removeSession(id int) { + delete(app.sessions, id) +} + +func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) { + + if ProtocolInDebugLog { + DebugLogF("%s %s", req.Method, req.URL.Path) + } + + switch req.Method { + case "GET": + switch req.URL.Path { + case "/": + w.WriteHeader(http.StatusOK) + io.WriteString(w, app.getStartPage()) + + case "/ws": + if brige := CreateSocketBrige(w, req); brige != nil { + go app.socketReader(brige) + } + + default: + filename := req.URL.Path[1:] + if size := len(filename); size > 0 && filename[size-1] == '/' { + filename = filename[:size-1] + } + + if !serveResourceFile(filename, w, req) && + !serveDownloadFile(filename, w, req) { + w.WriteHeader(http.StatusNotFound) + } + } + } +} + +func (app *application) socketReader(brige webBrige) { + var session Session + events := make(chan DataObject, 1024) + + for { + message, ok := brige.readMessage() + if !ok { + events <- NewDataObject("disconnect") + return + } + + if ProtocolInDebugLog { + DebugLog(message) + } + + if obj := ParseDataText(message); obj != nil { + command := obj.Tag() + switch command { + case "startSession": + answer := "" + if session, answer = app.startSession(obj, events, brige); session != nil { + if !brige.writeMessage(answer) { + return + } + session.onStart() + go sessionEventHandler(session, events, brige) + } + + case "reconnect": + if sessionText, ok := obj.PropertyValue("session"); ok { + if sessionID, err := strconv.Atoi(sessionText); err == nil { + if session = app.sessions[sessionID]; session != nil { + session.setBrige(events, brige) + answer := allocStringBuilder() + defer freeStringBuilder(answer) + + session.writeInitScript(answer) + if !brige.writeMessage(answer.String()) { + return + } + session.onReconnect() + go sessionEventHandler(session, events, brige) + return + } + DebugLogF("Session #%d not exists", sessionID) + } else { + ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error()) + } + } else { + ErrorLog(`"session" key not found`) + } + + answer := "" + if session, answer = app.startSession(obj, events, brige); session != nil { + if !brige.writeMessage(answer) { + return + } + session.onStart() + go sessionEventHandler(session, events, brige) + } + + case "answer": + session.handleAnswer(obj) + + case "imageLoaded": + session.imageManager().imageLoaded(obj, session) + + case "imageError": + session.imageManager().imageLoadError(obj, session) + + default: + events <- obj + } + } + } +} + +func sessionEventHandler(session Session, events chan DataObject, brige webBrige) { + for { + data := <-events + + switch command := data.Tag(); command { + case "disconnect": + session.onDisconnect() + return + + case "session-close": + session.onFinish() + session.App().removeSession(session.ID()) + brige.close() + + default: + session.handleEvent(command, data) + } + } +} + +func (app *application) startSession(params DataObject, events chan DataObject, brige webBrige) (Session, string) { + if app.createContentFunc == nil { + return nil, "" + } + + session := newSession(app, app.nextSessionID(), "", params) + session.setBrige(events, brige) + if !session.setContent(app.createContentFunc(session), session) { + return nil, "" + } + + app.sessions[session.ID()] = session + + answer := allocStringBuilder() + defer freeStringBuilder(answer) + + answer.WriteString("sessionID = '") + answer.WriteString(strconv.Itoa(session.ID())) + answer.WriteString("';\n") + session.writeInitScript(answer) + answerText := answer.String() + + if ProtocolInDebugLog { + DebugLog("Start session:") + DebugLog(answerText) + } + return session, answerText +} + +var apps = []*application{} + +// StartApp - create the new application and start it +func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) { + app := new(application) + app.params = params + app.sessions = map[int]Session{} + app.createContentFunc = createContentFunc + apps = append(apps, app) + + redirectAddr := "" + if index := strings.IndexRune(addr, ':'); index >= 0 { + redirectAddr = addr[:index] + ":80" + } else { + redirectAddr = addr + ":80" + if params.CertFile != "" && params.KeyFile != "" { + addr += ":443" + } else { + addr += ":80" + } + } + + app.server = &http.Server{Addr: addr} + http.Handle("/", app) + + serverRun := func(err error) { + if err != nil { + if err == http.ErrServerClosed { + log.Println(err) + } else { + log.Fatal(err) + } + } + } + + if params.CertFile != "" && params.KeyFile != "" { + if params.Redirect80 { + redirectTLS := func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "https://"+addr+r.RequestURI, http.StatusMovedPermanently) + } + + go func() { + serverRun(http.ListenAndServe(redirectAddr, http.HandlerFunc(redirectTLS))) + }() + } + serverRun(app.server.ListenAndServeTLS(params.CertFile, params.KeyFile)) + } else { + serverRun(app.server.ListenAndServe()) + } +} + +func FinishApp() { + for _, app := range apps { + app.Finish() + } + apps = []*application{} +} + +func OpenBrowser(url string) bool { + var err error + + switch runtime.GOOS { + case "linux": + for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} { + if _, err = exec.LookPath(provider); err == nil { + if exec.Command(provider, url).Start(); err == nil { + return true + } + } + } + + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + + case "darwin": + err = exec.Command("open", url).Start() + + default: + err = fmt.Errorf("unsupported platform") + } + + return err != nil +} diff --git a/appWasm.go b/appWasm.go new file mode 100644 index 0000000..8268dea --- /dev/null +++ b/appWasm.go @@ -0,0 +1,235 @@ +//go:build wasm + +package rui + +import ( + _ "embed" + "strings" + "syscall/js" +) + +//go:embed app_wasm.js +var wasmScripts string + +type wasmApp struct { + params AppParams + createContentFunc func(Session) SessionContent + session Session + brige webBrige +} + +func (app *wasmApp) Finish() { + app.session.close() +} + +/* +func (app *wasmApp) ServeHTTP(w http.ResponseWriter, req *http.Request) { + + if ProtocolInDebugLog { + DebugLogF("%s %s", req.Method, req.URL.Path) + } + + switch req.Method { + case "GET": + switch req.URL.Path { + case "/": + w.WriteHeader(http.StatusOK) + io.WriteString(w, app.getStartPage()) + + case "/ws": + if brige := CreateSocketBrige(w, req); brige != nil { + go app.socketReader(brige) + } + + default: + filename := req.URL.Path[1:] + if size := len(filename); size > 0 && filename[size-1] == '/' { + filename = filename[:size-1] + } + + if !serveResourceFile(filename, w, req) && + !serveDownloadFile(filename, w, req) { + w.WriteHeader(http.StatusNotFound) + } + } + } +} +*/ + +func (app *wasmApp) startSession(this js.Value, args []js.Value) interface{} { + if app.createContentFunc == nil || len(args) == 1 { + return nil + } + + params := ParseDataText(args[0].String()) + session := newSession(app, 0, "", params) + session.setBrige(make(chan DataObject), app.brige) + if !session.setContent(app.createContentFunc(session), session) { + return nil + } + + app.session = session + + answer := allocStringBuilder() + defer freeStringBuilder(answer) + + session.writeInitScript(answer) + answerText := answer.String() + + if ProtocolInDebugLog { + DebugLog("Start session:") + DebugLog(answerText) + } + return nil +} + +func (app *wasmApp) removeSession(id int) { +} + +// StartApp - create the new wasmApp and start it +func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) { + app := new(wasmApp) + app.params = params + app.createContentFunc = createContentFunc + + if createContentFunc == nil { + return + } + + app.brige = createWasmBrige() + js.Global().Set("startSession", js.FuncOf(app.startSession)) + + script := defaultScripts + wasmScripts + script = strings.ReplaceAll(script, "\\", `\\`) + script = strings.ReplaceAll(script, "\n", `\n`) + script = strings.ReplaceAll(script, "\t", `\t`) + script = strings.ReplaceAll(script, "\"", `\"`) + script = strings.ReplaceAll(script, "'", `\'`) + + //window := js.Global().Get("window") + //window.Call("execScript", `document.getElementById('ruiscript').text = "`+script+`"`) + js.Global().Call("execScript", `document.getElementById('ruiscript').text += "`+script+`"`) + + document := js.Global().Get("document") + body := document.Call("querySelector", "body") + body.Set("innerHTML", `
+ +`) + + js.Global().Call("execScript", "initSession()") + //window.Call("execScript", "initSession()") + + for true { + if message, ok := app.brige.readMessage(); ok && app.session != nil { + if ProtocolInDebugLog { + DebugLog(message) + } + + if obj := ParseDataText(message); obj != nil { + switch command := obj.Tag(); command { + /* + case "startSession": + answer := "" + if session, answer = app.startSession(obj, events, brige); session != nil { + if !brige.writeMessage(answer) { + return + } + session.onStart() + go sessionEventHandler(session, events, brige) + } + + case "reconnect": + if sessionText, ok := obj.PropertyValue("session"); ok { + if sessionID, err := strconv.Atoi(sessionText); err == nil { + if session = app.sessions[sessionID]; session != nil { + session.setBrige(events, brige) + answer := allocStringBuilder() + defer freeStringBuilder(answer) + + session.writeInitScript(answer) + if !brige.writeMessage(answer.String()) { + return + } + session.onReconnect() + go sessionEventHandler(session, events, brige) + return + } + DebugLogF("Session #%d not exists", sessionID) + } else { + ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error()) + } + } else { + ErrorLog(`"session" key not found`) + } + + answer := "" + if session, answer = app.startSession(obj, events, brige); session != nil { + if !brige.writeMessage(answer) { + return + } + session.onStart() + go sessionEventHandler(session, events, brige) + } + + case "disconnect": + session.onDisconnect() + return + + case "session-close": + session.onFinish() + session.App().removeSession(session.ID()) + brige.close() + + */ + case "answer": + app.session.handleAnswer(obj) + + case "imageLoaded": + app.session.imageManager().imageLoaded(obj, app.session) + + case "imageError": + app.session.imageManager().imageLoadError(obj, app.session) + + default: + app.session.handleEvent(command, obj) + } + } + } + } +} + +func FinishApp() { + //app.Finish() +} + +func OpenBrowser(url string) bool { + return false +} + +/* +func OpenBrowser(url string) bool { + var err error + + switch runtime.GOOS { + case "linux": + for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} { + if _, err = exec.LookPath(provider); err == nil { + if exec.Command(provider, url).Start(); err == nil { + return true + } + } + } + + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + + case "darwin": + err = exec.Command("open", url).Start() + + default: + err = fmt.Errorf("unsupported platform") + } + + return err != nil +} +*/ diff --git a/app_scripts.js b/app_scripts.js index 9894e88..b8466df 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -1,103 +1,6 @@ var sessionID = "0" -var socket -var socketUrl - -var images = new Map(); var windowFocus = true -function sendMessage(message) { - if (socket) { - socket.send(message) - } -} - -window.onload = function() { - socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://" - socketUrl += document.location.hostname - var port = document.location.port - if (port) { - socketUrl += ":" + port - } - socketUrl += window.location.pathname + "ws" - - socket = new WebSocket(socketUrl); - socket.onopen = socketOpen; - socket.onclose = socketClose; - socket.onerror = socketError; - socket.onmessage = function(event) { - window.execScript ? window.execScript(event.data) : window.eval(event.data); - }; -}; - -function socketOpen() { - - const touch_screen = (('ontouchstart' in document.documentElement) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) ? "1" : "0"; - var message = "startSession{touch=" + touch_screen - - const style = window.getComputedStyle(document.body); - if (style) { - var direction = style.getPropertyValue('direction'); - if (direction) { - message += ",direction=" + direction - } - } - - const lang = window.navigator.language; - if (lang) { - message += ",language=\"" + lang + "\""; - } - - const langs = window.navigator.languages; - if (langs) { - message += ",languages=\"" + langs + "\""; - } - - const userAgent = window.navigator.userAgent - if (userAgent) { - message += ",user-agent=\"" + userAgent + "\""; - } - - const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); - if (darkThemeMq.matches) { - message += ",dark=1"; - } - - const pixelRatio = window.devicePixelRatio; - if (pixelRatio) { - message += ",pixel-ratio=" + pixelRatio; - } - - sendMessage( message + "}" ); -} - -function socketReopen() { - sendMessage( "reconnect{session=" + sessionID + "}" ); -} - -function socketReconnect() { - if (!socket) { - socket = new WebSocket(socketUrl); - socket.onopen = socketReopen; - socket.onclose = socketClose; - socket.onerror = socketError; - socket.onmessage = function(event) { - window.execScript ? window.execScript(event.data) : window.eval(event.data); - }; - } -} - -function socketClose(event) { - console.log("socket closed") - socket = null; - if (!event.wasClean && windowFocus) { - window.setTimeout(socketReconnect, 10000); - } -} - -function socketError(error) { - console.log(error); -} - window.onresize = function() { scanElementsSize(); } @@ -111,15 +14,6 @@ window.onblur = function(event) { sendMessage( "session-pause{session=" + sessionID +"}" ); } -window.onfocus = function(event) { - windowFocus = true - if (!socket) { - socketReconnect() - } else { - sendMessage( "session-resume{session=" + sessionID +"}" ); - } -} - function getIntAttribute(element, tag) { let value = element.getAttribute(tag); if (value) { @@ -1188,6 +1082,8 @@ function stackTransitionEndEvent(stackId, propertyName, event) { event.stopPropagation(); } +var images = new Map(); + function loadImage(url) { var img = images.get(url); if (img != undefined) { diff --git a/app_socket.js b/app_socket.js new file mode 100644 index 0000000..6b280a5 --- /dev/null +++ b/app_socket.js @@ -0,0 +1,104 @@ +var socket +var socketUrl + +function sendMessage(message) { + if (socket) { + socket.send(message) + } +} + +window.onload = function() { + socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://" + socketUrl += document.location.hostname + var port = document.location.port + if (port) { + socketUrl += ":" + port + } + socketUrl += window.location.pathname + "ws" + + socket = new WebSocket(socketUrl); + socket.onopen = socketOpen; + socket.onclose = socketClose; + socket.onerror = socketError; + socket.onmessage = function(event) { + window.execScript ? window.execScript(event.data) : window.eval(event.data); + }; +}; + +function socketOpen() { + + const touch_screen = (('ontouchstart' in document.documentElement) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) ? "1" : "0"; + var message = "startSession{touch=" + touch_screen + + const style = window.getComputedStyle(document.body); + if (style) { + var direction = style.getPropertyValue('direction'); + if (direction) { + message += ",direction=" + direction + } + } + + const lang = window.navigator.language; + if (lang) { + message += ",language=\"" + lang + "\""; + } + + const langs = window.navigator.languages; + if (langs) { + message += ",languages=\"" + langs + "\""; + } + + const userAgent = window.navigator.userAgent + if (userAgent) { + message += ",user-agent=\"" + userAgent + "\""; + } + + const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); + if (darkThemeMq.matches) { + message += ",dark=1"; + } + + const pixelRatio = window.devicePixelRatio; + if (pixelRatio) { + message += ",pixel-ratio=" + pixelRatio; + } + + sendMessage( message + "}" ); +} + +function socketReopen() { + sendMessage( "reconnect{session=" + sessionID + "}" ); +} + +function socketReconnect() { + if (!socket) { + socket = new WebSocket(socketUrl); + socket.onopen = socketReopen; + socket.onclose = socketClose; + socket.onerror = socketError; + socket.onmessage = function(event) { + window.execScript ? window.execScript(event.data) : window.eval(event.data); + }; + } +} + +function socketClose(event) { + console.log("socket closed") + socket = null; + if (!event.wasClean && windowFocus) { + window.setTimeout(socketReconnect, 10000); + } +} + +function socketError(error) { + console.log(error); +} + +window.onfocus = function(event) { + windowFocus = true + if (!socket) { + socketReconnect() + } else { + sendMessage( "session-resume{session=" + sessionID +"}" ); + } +} diff --git a/app_wasm.js b/app_wasm.js new file mode 100644 index 0000000..8916025 --- /dev/null +++ b/app_wasm.js @@ -0,0 +1,48 @@ + + +function initSession() { + + const touch_screen = (('ontouchstart' in document.documentElement) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) ? "1" : "0"; + var message = "sessionInfo{touch=" + touch_screen + + const style = window.getComputedStyle(document.body); + if (style) { + var direction = style.getPropertyValue('direction'); + if (direction) { + message += ",direction=" + direction + } + } + + const lang = window.navigator.language; + if (lang) { + message += ",language=\"" + lang + "\""; + } + + const langs = window.navigator.languages; + if (langs) { + message += ",languages=\"" + langs + "\""; + } + + const userAgent = window.navigator.userAgent + if (userAgent) { + message += ",user-agent=\"" + userAgent + "\""; + } + + const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); + if (darkThemeMq.matches) { + message += ",dark=1"; + } + + const pixelRatio = window.devicePixelRatio; + if (pixelRatio) { + message += ",pixel-ratio=" + pixelRatio; + } + + startSession( message + "}" ); +} + + +window.onfocus = function(event) { + windowFocus = true + sendMessage( "session-resume{session=" + sessionID +"}" ); +} diff --git a/application.go b/application.go index dd75f12..7d2baf4 100644 --- a/application.go +++ b/application.go @@ -1,21 +1,8 @@ package rui import ( - "bytes" - "context" _ "embed" - "fmt" - "io" - "log" - "math/rand" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" "strings" - "time" ) //go:embed app_scripts.js @@ -30,17 +17,9 @@ var defaultThemeText string // Application - app interface type Application interface { Finish() - nextSessionID() int removeSession(id int) } -type application struct { - server *http.Server - params AppParams - createContentFunc func(Session) SessionContent - sessions map[int]Session -} - // AppParams defines parameters of the app type AppParams struct { // Title - title of the app window/tab @@ -61,28 +40,23 @@ type AppParams struct { Redirect80 bool } -func (app *application) getStartPage() string { - buffer := allocStringBuilder() - defer freeStringBuilder(buffer) - - buffer.WriteString(` - - +func getStartPage(buffer *strings.Builder, params AppParams, addScripts string) { + buffer.WriteString(`