//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 func debugLog(text string) { log.Println("\033[34m" + text) } func errorLog(text string) { log.Println("\033[31m" + text) } 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 bridge := CreateSocketBridge(w, req); bridge != nil { go app.socketReader(bridge) } 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(bridge webBridge) { var session Session events := make(chan DataObject, 1024) for { message, ok := bridge.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, bridge); session != nil { if !bridge.writeMessage(answer) { return } session.onStart() go sessionEventHandler(session, events, bridge) } 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.setBridge(events, bridge) answer := allocStringBuilder() defer freeStringBuilder(answer) session.writeInitScript(answer) if !bridge.writeMessage(answer.String()) { return } session.onReconnect() go sessionEventHandler(session, events, bridge) return } DebugLogF("Session #%d not exists", sessionID) } else { ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error()) } } else { ErrorLog(`"session" key not found`) } bridge.writeMessage("restartSession();") 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, bridge webBridge) { for { data := <-events switch command := data.Tag(); command { case "disconnect": session.onDisconnect() return case "session-close": session.onFinish() session.App().removeSession(session.ID()) bridge.close() default: session.handleEvent(command, data) } } } func (app *application) startSession(params DataObject, events chan DataObject, bridge webBridge) (Session, string) { if app.createContentFunc == nil { return nil, "" } session := newSession(app, app.nextSessionID(), "", params) session.setBridge(events, bridge) if !session.setContent(app.createContentFunc(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 }