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(` `) - buffer.WriteString(app.params.Title) + buffer.WriteString(params.Title) buffer.WriteString("") - if app.params.Icon != "" { + if params.Icon != "" { buffer.WriteString(` `) } - if app.params.TitleColor != 0 { + if params.TitleColor != 0 { buffer.WriteString(` `) } @@ -92,356 +66,15 @@ func (app *application) getStartPage() string { -
- -`) - - 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() - - case "session-pause": - session.onPause() - - case "session-resume": - session.onResume() - - case "root-size": - session.handleRootSize(data) - - case "resize": - session.handleResize(data) - - default: - session.handleViewEvent(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 -} - -type downloadFile struct { - filename string - path string - data []byte -} - -var currentDownloadId = int(rand.Int31()) -var downloadFiles = map[string]downloadFile{} - -func (session *sessionData) startDownload(file downloadFile) { - currentDownloadId++ - id := strconv.Itoa(currentDownloadId) - downloadFiles[id] = file - session.runScript(fmt.Sprintf(`startDowndload("%s", "%s")`, id, file.filename)) -} - -func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool { - if file, ok := downloadFiles[id]; ok { - delete(downloadFiles, id) - if file.data != nil { - http.ServeContent(w, r, file.filename, time.Now(), bytes.NewReader(file.data)) - return true - } else if _, err := os.Stat(file.path); err == nil { - http.ServeFile(w, r, file.path) - return true - } - } - return false -} - -// DownloadFile starts downloading the file on the client side. -func (session *sessionData) DownloadFile(path string) { - if _, err := os.Stat(path); err != nil { - ErrorLog(err.Error()) - return - } - - _, filename := filepath.Split(path) - session.startDownload(downloadFile{ - filename: filename, - path: path, - data: nil, - }) -} - -// DownloadFileData starts downloading the file on the client side. Arguments specify the name of the downloaded file and its contents -func (session *sessionData) DownloadFileData(filename string, data []byte) { - if data == nil { - ErrorLog("Invalid download data. Must be not nil.") - return - } - - session.startDownload(downloadFile{ - filename: filename, - path: "", - data: data, - }) + `) } diff --git a/downloadFile.go b/downloadFile.go new file mode 100644 index 0000000..44c146a --- /dev/null +++ b/downloadFile.go @@ -0,0 +1,71 @@ +package rui + +import ( + "bytes" + "fmt" + "math/rand" + "net/http" + "os" + "path/filepath" + "strconv" + "time" +) + +type downloadFile struct { + filename string + path string + data []byte +} + +var currentDownloadId = int(rand.Int31()) +var downloadFiles = map[string]downloadFile{} + +func (session *sessionData) startDownload(file downloadFile) { + currentDownloadId++ + id := strconv.Itoa(currentDownloadId) + downloadFiles[id] = file + session.runScript(fmt.Sprintf(`startDowndload("%s", "%s")`, id, file.filename)) +} + +func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool { + if file, ok := downloadFiles[id]; ok { + delete(downloadFiles, id) + if file.data != nil { + http.ServeContent(w, r, file.filename, time.Now(), bytes.NewReader(file.data)) + return true + } else if _, err := os.Stat(file.path); err == nil { + http.ServeFile(w, r, file.path) + return true + } + } + return false +} + +// DownloadFile starts downloading the file on the client side. +func (session *sessionData) DownloadFile(path string) { + if _, err := os.Stat(path); err != nil { + ErrorLog(err.Error()) + return + } + + _, filename := filepath.Split(path) + session.startDownload(downloadFile{ + filename: filename, + path: path, + data: nil, + }) +} + +// DownloadFileData starts downloading the file on the client side. Arguments specify the name of the downloaded file and its contents +func (session *sessionData) DownloadFileData(filename string, data []byte) { + if data == nil { + ErrorLog("Invalid download data. Must be not nil.") + return + } + + session.startDownload(downloadFile{ + filename: filename, + path: "", + data: data, + }) +} diff --git a/session.go b/session.go index 38fe98f..29d10d6 100644 --- a/session.go +++ b/session.go @@ -7,6 +7,15 @@ import ( "strings" ) +type webBrige interface { + readMessage() (string, bool) + writeMessage(text string) bool + runGetterScript(script string) DataObject + answerReceived(answer DataObject) + close() + remoteAddr() string +} + // SessionContent is the interface of a session content type SessionContent interface { CreateRootView(session Session) View @@ -85,14 +94,14 @@ type Session interface { nextViewID() string styleProperty(styleTag, property string) any - setBrige(events chan DataObject, brige WebBrige) + setBrige(events chan DataObject, brige webBrige) writeInitScript(writer *strings.Builder) runScript(script string) runGetterScript(script string) DataObject //, answer chan DataObject) handleAnswer(data DataObject) handleRootSize(data DataObject) handleResize(data DataObject) - handleViewEvent(command string, data DataObject) + handleEvent(command string, data DataObject) close() onStart() @@ -137,7 +146,7 @@ type sessionData struct { ignoreUpdates bool popups *popupManager images *imageManager - brige WebBrige + brige webBrige events chan DataObject animationCounter int animationCSS string @@ -166,38 +175,8 @@ func newSession(app Application, id int, customTheme string, params DataObject) } } - if value, ok := params.PropertyValue("touch"); ok { - session.touchScreen = (value == "1" || value == "true") - } - - if value, ok := params.PropertyValue("user-agent"); ok { - session.userAgent = value - } - - if value, ok := params.PropertyValue("direction"); ok { - if value == "rtl" { - session.textDirection = RightToLeftDirection - } - } - - if value, ok := params.PropertyValue("language"); ok { - session.language = value - } - - if value, ok := params.PropertyValue("languages"); ok { - session.languages = strings.Split(value, ",") - } - - if value, ok := params.PropertyValue("dark"); ok { - session.darkTheme = (value == "1" || value == "true") - } - - if value, ok := params.PropertyValue("pixel-ratio"); ok { - if f, err := strconv.ParseFloat(value, 64); err != nil { - ErrorLog(err.Error()) - } else { - session.pixelRatio = f - } + if params != nil { + session.handleSessionInfo(params) } return session @@ -211,7 +190,7 @@ func (session *sessionData) ID() int { return session.sessionID } -func (session *sessionData) setBrige(events chan DataObject, brige WebBrige) { +func (session *sessionData) setBrige(events chan DataObject, brige webBrige) { session.events = events session.brige = brige } @@ -344,7 +323,7 @@ func (session *sessionData) imageManager() *imageManager { func (session *sessionData) runScript(script string) { if session.brige != nil { - session.brige.WriteMessage(script) + session.brige.writeMessage(script) } else { ErrorLog("No connection") } @@ -352,7 +331,7 @@ func (session *sessionData) runScript(script string) { func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) { if session.brige != nil { - return session.brige.RunGetterScript(script) + return session.brige.runGetterScript(script) } ErrorLog("No connection") @@ -362,7 +341,7 @@ func (session *sessionData) runGetterScript(script string) DataObject { //}, ans } func (session *sessionData) handleAnswer(data DataObject) { - session.brige.AnswerReceived(data) + session.brige.answerReceived(data) } func (session *sessionData) handleRootSize(data DataObject) { @@ -429,13 +408,67 @@ func (session *sessionData) handleResize(data DataObject) { } } -func (session *sessionData) handleViewEvent(command string, data DataObject) { - if viewID, ok := data.PropertyValue("id"); ok { - if view := session.viewByHTMLID(viewID); view != nil { - view.handleCommand(view, command, data) +func (session *sessionData) handleSessionInfo(params DataObject) { + if value, ok := params.PropertyValue("touch"); ok { + session.touchScreen = (value == "1" || value == "true") + } + + if value, ok := params.PropertyValue("user-agent"); ok { + session.userAgent = value + } + + if value, ok := params.PropertyValue("direction"); ok { + if value == "rtl" { + session.textDirection = RightToLeftDirection + } + } + + if value, ok := params.PropertyValue("language"); ok { + session.language = value + } + + if value, ok := params.PropertyValue("languages"); ok { + session.languages = strings.Split(value, ",") + } + + if value, ok := params.PropertyValue("dark"); ok { + session.darkTheme = (value == "1" || value == "true") + } + + if value, ok := params.PropertyValue("pixel-ratio"); ok { + if f, err := strconv.ParseFloat(value, 64); err != nil { + ErrorLog(err.Error()) + } else { + session.pixelRatio = f + } + } +} + +func (session *sessionData) handleEvent(command string, data DataObject) { + switch command { + case "session-pause": + session.onPause() + + case "session-resume": + session.onResume() + + case "root-size": + session.handleRootSize(data) + + case "resize": + session.handleResize(data) + + case "sessionInfo": + session.handleSessionInfo(data) + + default: + if viewID, ok := data.PropertyValue("id"); ok { + if view := session.viewByHTMLID(viewID); view != nil { + view.handleCommand(view, command, data) + } + } else if command != "clickOutsidePopup" { + ErrorLog(`"id" property not found. Event: ` + command) } - } else if command != "clickOutsidePopup" { - ErrorLog(`"id" property not found. Event: ` + command) } } diff --git a/wasmBrige.go b/wasmBrige.go new file mode 100644 index 0000000..b2bc788 --- /dev/null +++ b/wasmBrige.go @@ -0,0 +1,104 @@ +//go:build wasm + +package rui + +import ( + "strconv" + "sync" + "syscall/js" + + "github.com/gorilla/websocket" +) + +type wasmBrige struct { + queue chan string + answer map[int]chan DataObject + answerID int + answerMutex sync.Mutex + closed bool +} + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 8096, +} + +func createWasmBrige() webBrige { + brige := new(wasmBrige) + brige.queue = make(chan string, 1000) + brige.answerID = 1 + brige.answer = make(map[int]chan DataObject) + brige.closed = false + + js.Global().Set("sendMessage", js.FuncOf(brige.sendMessage)) + + return brige +} + +func (brige *wasmBrige) sendMessage(this js.Value, args []js.Value) interface{} { + if len(args) > 0 { + brige.queue <- args[0].String() + } + return nil +} + +func (brige *wasmBrige) close() { +} + +func (brige *wasmBrige) readMessage() (string, bool) { + return <-brige.queue, true +} + +func (brige *wasmBrige) writeMessage(script string) bool { + if ProtocolInDebugLog { + DebugLog("Run script:") + DebugLog(script) + } + + window := js.Global().Get("window") + window.Call("execScript", script) + + return true +} + +func (brige *wasmBrige) runGetterScript(script string) DataObject { + brige.answerMutex.Lock() + answerID := brige.answerID + brige.answerID++ + brige.answerMutex.Unlock() + + answer := make(chan DataObject) + brige.answer[answerID] = answer + errorText := "" + + js.Global().Set("answerID", strconv.Itoa(answerID)) + + window := js.Global().Get("window") + window.Call("execScript", script) + + result := NewDataObject("error") + result.SetPropertyValue("text", errorText) + delete(brige.answer, answerID) + return result +} + +func (brige *wasmBrige) answerReceived(answer DataObject) { + if text, ok := answer.PropertyValue("answerID"); ok { + if id, err := strconv.Atoi(text); err == nil { + if chanel, ok := brige.answer[id]; ok { + chanel <- answer + delete(brige.answer, id) + } else { + ErrorLog("Bad answerID = " + text + " (chan not found)") + } + } else { + ErrorLog("Invalid answerID = " + text) + } + } else { + ErrorLog("answerID not found") + } +} + +func (brige *wasmBrige) remoteAddr() string { + return "localhost" +} diff --git a/webBrige.go b/webBrige.go index bcfa471..873c95d 100644 --- a/webBrige.go +++ b/webBrige.go @@ -1,3 +1,5 @@ +//go:build !wasm + package rui import ( @@ -8,15 +10,6 @@ import ( "github.com/gorilla/websocket" ) -type WebBrige interface { - ReadMessage() (string, bool) - WriteMessage(text string) bool - RunGetterScript(script string) DataObject - AnswerReceived(answer DataObject) - Close() - remoteAddr() string -} - type wsBrige struct { conn *websocket.Conn answer map[int]chan DataObject @@ -30,7 +23,7 @@ var upgrader = websocket.Upgrader{ WriteBufferSize: 8096, } -func CreateSocketBrige(w http.ResponseWriter, req *http.Request) WebBrige { +func CreateSocketBrige(w http.ResponseWriter, req *http.Request) webBrige { conn, err := upgrader.Upgrade(w, req, nil) if err != nil { ErrorLog(err.Error()) @@ -45,13 +38,12 @@ func CreateSocketBrige(w http.ResponseWriter, req *http.Request) WebBrige { return brige } -func (brige *wsBrige) Close() { +func (brige *wsBrige) close() { brige.closed = true brige.conn.Close() } -func (brige *wsBrige) ReadMessage() (string, bool) { - //messageType, p, err := brige.conn.ReadMessage() +func (brige *wsBrige) readMessage() (string, bool) { _, p, err := brige.conn.ReadMessage() if err != nil { if !brige.closed { @@ -63,7 +55,7 @@ func (brige *wsBrige) ReadMessage() (string, bool) { return string(p), true } -func (brige *wsBrige) WriteMessage(script string) bool { +func (brige *wsBrige) writeMessage(script string) bool { if ProtocolInDebugLog { DebugLog("Run script:") DebugLog(script) @@ -75,7 +67,7 @@ func (brige *wsBrige) WriteMessage(script string) bool { return true } -func (brige *wsBrige) RunGetterScript(script string) DataObject { +func (brige *wsBrige) runGetterScript(script string) DataObject { brige.answerMutex.Lock() answerID := brige.answerID brige.answerID++ @@ -107,7 +99,7 @@ func (brige *wsBrige) RunGetterScript(script string) DataObject { return result } -func (brige *wsBrige) AnswerReceived(answer DataObject) { +func (brige *wsBrige) answerReceived(answer DataObject) { if text, ok := answer.PropertyValue("answerID"); ok { if id, err := strconv.Atoi(text); err == nil { if chanel, ok := brige.answer[id]; ok {