mirror of https://github.com/anoshenko/rui.git
				
				
				
			
		
			
				
	
	
		
			376 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			376 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
| package rui
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	_ "embed"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"math/rand"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"path/filepath"
 | |
| 	"runtime"
 | |
| 	"strconv"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| //go:embed app_scripts.js
 | |
| var defaultScripts string
 | |
| 
 | |
| //go:embed app_styles.css
 | |
| var appStyles string
 | |
| 
 | |
| //go:embed defaultTheme.rui
 | |
| var defaultThemeText string
 | |
| 
 | |
| // Application - app interface
 | |
| type Application interface {
 | |
| 	Finish()
 | |
| 	nextSessionID() int
 | |
| 	removeSession(id int)
 | |
| }
 | |
| 
 | |
| type application struct {
 | |
| 	params            AppParams
 | |
| 	createContentFunc func(Session) SessionContent
 | |
| 	sessions          map[int]Session
 | |
| }
 | |
| 
 | |
| // AppParams defines parameters of the app
 | |
| type AppParams struct {
 | |
| 	Title      string
 | |
| 	TitleColor Color
 | |
| 	Icon       string
 | |
| }
 | |
| 
 | |
| func (app *application) getStartPage() string {
 | |
| 	buffer := allocStringBuilder()
 | |
| 	defer freeStringBuilder(buffer)
 | |
| 
 | |
| 	buffer.WriteString(`<!DOCTYPE html>
 | |
| <html>
 | |
| 	<head>
 | |
| 		<meta charset="utf-8">
 | |
| 		<title>`)
 | |
| 	buffer.WriteString(app.params.Title)
 | |
| 	buffer.WriteString("</title>")
 | |
| 	if app.params.Icon != "" {
 | |
| 		buffer.WriteString(`
 | |
| 		<link rel="icon" href="`)
 | |
| 		buffer.WriteString(app.params.Icon)
 | |
| 		buffer.WriteString(`">`)
 | |
| 	}
 | |
| 
 | |
| 	if app.params.TitleColor != 0 {
 | |
| 		buffer.WriteString(`
 | |
| 		<meta name="theme-color" content="`)
 | |
| 		buffer.WriteString(app.params.TitleColor.cssString())
 | |
| 		buffer.WriteString(`">`)
 | |
| 	}
 | |
| 
 | |
| 	buffer.WriteString(`
 | |
| 		<base target="_blank" rel="noopener">
 | |
| 		<meta name="viewport" content="width=device-width">
 | |
| 		<style>`)
 | |
| 	buffer.WriteString(appStyles)
 | |
| 	buffer.WriteString(`</style>
 | |
| 		<script>`)
 | |
| 	buffer.WriteString(defaultScripts)
 | |
| 	buffer.WriteString(`</script>
 | |
| 	</head>
 | |
| 	<body>
 | |
| 		<div class="ruiRoot" id="ruiRootView"></div>
 | |
| 		<div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden;" onclick="clickOutsidePopup(event)"></div>
 | |
| 		<a id="ruiDownloader" download style="display: none;"></a>
 | |
| 	</body>
 | |
| </html>`)
 | |
| 
 | |
| 	return buffer.String()
 | |
| }
 | |
| 
 | |
| func (app *application) init(params AppParams) {
 | |
| 	app.params = params
 | |
| 	app.sessions = map[int]Session{}
 | |
| }
 | |
| 
 | |
| func (app *application) Start(addr string) {
 | |
| 	http.Handle("/", app)
 | |
| 	log.Fatal(http.ListenAndServe(addr, nil))
 | |
| }
 | |
| 
 | |
| func (app *application) Finish() {
 | |
| 	for _, session := range app.sessions {
 | |
| 		session.close()
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| 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 "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
 | |
| }
 | |
| 
 | |
| // NewApplication - create the new application and start it
 | |
| func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
 | |
| 	app := new(application)
 | |
| 	app.init(params)
 | |
| 	app.createContentFunc = createContentFunc
 | |
| 
 | |
| 	http.Handle("/", app)
 | |
| 	log.Fatal(http.ListenAndServe(addr, nil))
 | |
| }
 | |
| 
 | |
| func OpenBrowser(url string) bool {
 | |
| 	var err error
 | |
| 
 | |
| 	switch runtime.GOOS {
 | |
| 	case "linux":
 | |
| 		err = exec.Command("xdg-open", url).Start()
 | |
| 	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,
 | |
| 	})
 | |
| }
 |