diff --git a/Makefile b/Makefile index 218e30d..3b772ae 100644 --- a/Makefile +++ b/Makefile @@ -10,23 +10,19 @@ HOME = changkun.de/x/occamy IMAGE = occamy compile: - go build -mod vendor -x -o occamyd + go build -x -o occamyd cmd/occamyd/* .PHONY: compile build: - docker build -t $(IMAGE):$(VERSION) -t $(IMAGE):latest -f docker/Dockerfile . + docker build --platform linux/x86_64 -t $(IMAGE):$(VERSION) -t $(IMAGE):latest -f docker/Dockerfile . .PHONY: occamy -run: +up: cd docker && docker-compose up -d -stop: +down: cd docker && docker-compose down -test: - go test -cover -coverprofile=cover.test -v ./... - go tool cover -html=cover.test -o cover.html - clean: docker images -f "dangling=true" -q | xargs docker rmi -f docker image prune -f diff --git a/cmd/occamy-gui/README.md b/cmd/occamy-gui/README.md new file mode 100644 index 0000000..376f70f --- /dev/null +++ b/cmd/occamy-gui/README.md @@ -0,0 +1,10 @@ +# Occamy GUI + +This is a minimum example of building an occamy client. + +It is based on the [bring](https://github.com/deluan/bring) implementation of the Guacamole protocol. + +Note that `bring` is a software renderer that does not utilize Gio's GPU backend. +Hence there is a lot of improvements for building this application. + +Later coding will revise the code into a gio accelerated rendering. \ No newline at end of file diff --git a/cmd/occamy-gui/occamy.go b/cmd/occamy-gui/occamy.go new file mode 100644 index 0000000..34eceb8 --- /dev/null +++ b/cmd/occamy-gui/occamy.go @@ -0,0 +1,114 @@ +// Copyright 2021 Changkun Ou. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "image" + "log" + "os" + + "changkun.de/x/occamy/internal/guac" + + "gioui.org/app" + "gioui.org/io/event" + "gioui.org/io/key" + "gioui.org/io/pointer" + "gioui.org/io/system" + "gioui.org/op" + "gioui.org/op/paint" + "gioui.org/unit" +) + +func main() { + if len(os.Args) < 3 { + log.Fatal("Usage: occamy-gui host:port") + return + } + + a, err := NewApp(os.Args[1], os.Args[2]) + if err != nil { + log.Fatalf("cannot create Occamy client: %v", err) + } + go a.Run() + app.Main() +} + +type App struct { + client *guac.Client + win *app.Window +} + +func NewApp(protocol, addr string) (a *App, err error) { + log.SetPrefix("occamy: ") + log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile) + + a = &App{} + a.win = app.NewWindow(app.Title("Occamy GUI Client")) + a.client, err = guac.NewClient("0.0.0.0:5636", map[string]string{ + "host": addr, + "protocol": protocol, + "username": "", + "password": "vncpassword", + }, a.win) + if err != nil { + return nil, err + } + w, h := 1280*2, 1024*2 + a.win.Option( + app.Size(unit.Px(float32(w)), unit.Px(float32(h))), + app.MaxSize(unit.Px(float32(w)), unit.Px(float32(h))), + app.MinSize(unit.Px(float32(w)), unit.Px(float32(h))), + ) + return a, nil +} + +func (a *App) Run() { + for e := range a.win.Events() { + switch e := e.(type) { + case system.DestroyEvent: + log.Println(e.Err) + os.Exit(0) + case system.FrameEvent: + ops := &op.Ops{} + a.updateScreen(ops, e.Queue) + e.Frame(ops) + case pointer.Event: + if err := a.client.SendMouse( + image.Point{X: int(e.Position.X), Y: int(e.Position.Y)}, + guac.MouseToGioButton[e.Buttons]); err != nil { + log.Println(err) + } + a.win.Invalidate() + case key.Event: + log.Printf("%+v, %+v", e.Name, e.Modifiers) + + // TODO: keyboard seems problematic, yet. + // See https://todo.sr.ht/~eliasnaur/gio/319 + // var keycode guac.KeyCode + // switch { + // case e.Modifiers.Contain(key.ModCtrl): + // keycode = guac.KeyCode(guac.KeyLeftControl) + // case e.Modifiers.Contain(key.ModCommand): + // keycode = guac.KeyCode(guac.KeyLeftControl) + // case e.Modifiers.Contain(key.ModShift): + // keycode = guac.KeyCode(guac.KeyLeftShift) + // case e.Modifiers.Contain(key.ModAlt): + // keycode = guac.KeyCode(guac.KeyLeftAlt) + // case e.Modifiers.Contain(key.ModSuper): + // keycode = guac.KeyCode(guac.KeySuper) + // } + // err := a.client.SendKey(keycode, e.State == key.Press) + // if err != nil { + // log.Println(err) + // } + } + } +} + +func (a *App) updateScreen(ops *op.Ops, q event.Queue) { + img, _ := a.client.Screen() + paint.NewImageOp(img).Add(ops) + paint.PaintOp{}.Add(ops) +} diff --git a/server/connection.go b/cmd/occamyd/connection.go similarity index 62% rename from server/connection.go rename to cmd/occamyd/connection.go index f660eb9..d50dae1 100644 --- a/server/connection.go +++ b/cmd/occamyd/connection.go @@ -2,13 +2,12 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package server +package main import ( "context" "log" "net/http" - "net/http/pprof" "os" "os/signal" "sync" @@ -28,7 +27,7 @@ func init() { // Run is an export method that serves occamy proxy func Run() { proxy := &proxy{ - sessions: make(map[string]*Session), + // sessions: make(map[string]*Session), upgrader: &websocket.Upgrader{ ReadBufferSize: protocol.MaxInstructionLength, WriteBufferSize: protocol.MaxInstructionLength, @@ -44,9 +43,7 @@ type proxy struct { jwtm *jwt.GinJWTMiddleware upgrader *websocket.Upgrader engine *gin.Engine - - mu sync.Mutex - sessions map[string]*Session + sess sync.Map // map[string]*Session } func (p *proxy) serve() { @@ -90,9 +87,6 @@ func (p *proxy) routers() *gin.Engine { auth := v1.Group("/connect") auth.Use(p.jwtm.MiddlewareFunc()) auth.GET("", p.serveWS) - if gin.Mode() == gin.DebugMode { - p.profile() - } return p.engine } @@ -129,42 +123,3 @@ func (p *proxy) initJWT() { } p.jwtm = jwtm } - -// profile the standard HandlerFuncs from the net/http/pprof package with -// the provided gin.Engine. prefixOptions is a optional. If not prefixOptions, -// the default path prefix is used, otherwise first prefixOptions will be path prefix. -// -// Basic Usage: -// -// - use the pprof tool to look at the heap profile: -// go tool pprof http://0.0.0.0:5636/debug/pprof/heap -// - look at a 30-second CPU profile: -// go tool pprof http://0.0.0.0:5636/debug/pprof/profile -// - look at the goroutine blocking profile, after calling runtime.SetBlockProfileRate: -// go tool pprof http://0.0.0.0:5636/debug/pprof/block -// - collect a 5-second execution trace: -// wget http://0.0.0.0:5636/debug/pprof/trace?seconds=5 -// -func (p *proxy) profile() { - pprofHandler := func(h http.HandlerFunc) gin.HandlerFunc { - handler := http.HandlerFunc(h) - return func(c *gin.Context) { - handler.ServeHTTP(c.Writer, c.Request) - } - } - r := p.engine.Group("/debug/pprof") - { - r.GET("/", pprofHandler(pprof.Index)) - r.GET("/cmdline", pprofHandler(pprof.Cmdline)) - r.GET("/profile", pprofHandler(pprof.Profile)) - r.POST("/symbol", pprofHandler(pprof.Symbol)) - r.GET("/symbol", pprofHandler(pprof.Symbol)) - r.GET("/trace", pprofHandler(pprof.Trace)) - r.GET("/allocs", pprofHandler(pprof.Handler("allocs").ServeHTTP)) - r.GET("/block", pprofHandler(pprof.Handler("block").ServeHTTP)) - r.GET("/goroutine", pprofHandler(pprof.Handler("goroutine").ServeHTTP)) - r.GET("/heap", pprofHandler(pprof.Handler("heap").ServeHTTP)) - r.GET("/mutex", pprofHandler(pprof.Handler("mutex").ServeHTTP)) - r.GET("/threadcreate", pprofHandler(pprof.Handler("threadcreate").ServeHTTP)) - } -} diff --git a/occamy.go b/cmd/occamyd/occamyd.go similarity index 71% rename from occamy.go rename to cmd/occamyd/occamyd.go index d8f2e7a..2bb61b6 100644 --- a/occamy.go +++ b/cmd/occamyd/occamyd.go @@ -4,6 +4,4 @@ package main -import "changkun.de/x/occamy/server" - -func main() { server.Run() } +func main() { Run() } diff --git a/server/routers.go b/cmd/occamyd/routers.go similarity index 74% rename from server/routers.go rename to cmd/occamyd/routers.go index f810022..f6b28b6 100644 --- a/server/routers.go +++ b/cmd/occamyd/routers.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package server +package main import ( "log" @@ -39,25 +39,25 @@ func (p *proxy) serveWS(c *gin.Context) { } func (p *proxy) routeConn(ws *websocket.Conn, jwt *config.JWT) (err error) { - p.mu.Lock() - s, ok := p.sessions[jwt.GenerateID()] - if ok { - err = s.Join(ws, jwt, false, func() { p.mu.Unlock() }) - return - } + jwtId := jwt.GenerateID() - s, err = NewSession(jwt.Protocol) + // Creating a new session because there was no session yet. + s, err := NewSession(jwt.Protocol) if err != nil { - p.mu.Unlock() return } - - p.sessions[jwt.GenerateID()] = s log.Printf("new session was created: %s", s.ID) - err = s.Join(ws, jwt, true, func() { p.mu.Unlock() }) // block here - p.mu.Lock() - delete(p.sessions, jwt.GenerateID()) - p.mu.Unlock() + // Check if there are already a session. If so, join. + ss, loaded := p.sess.LoadOrStore(jwtId, s) + if loaded { + s.Close() + s = ss.(*Session) + log.Printf("already had old session: %s", s.ID) + } + + err = s.Join(ws, jwt, true) // block here + p.sess.Delete(jwtId) + s.Close() return } diff --git a/server/session.go b/cmd/occamyd/session.go similarity index 90% rename from server/session.go rename to cmd/occamyd/session.go index 3370efd..89b7536 100644 --- a/server/session.go +++ b/cmd/occamyd/session.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package server +package main import ( "fmt" @@ -13,7 +13,7 @@ import ( "syscall" "changkun.de/x/occamy/internal/config" - "changkun.de/x/occamy/internal/lib" + "changkun.de/x/occamy/internal/guacd" "changkun.de/x/occamy/internal/protocol" "github.com/gorilla/websocket" ) @@ -24,14 +24,14 @@ type Session struct { ID string connectedUsers uint64 once sync.Once - client *lib.Client // shared client in a session + client *guacd.Client // shared client in a session } // NewSession creates a new occamy proxy session func NewSession(proto string) (*Session, error) { runtime.LockOSThread() // without unlock to exit the Go thread - cli, err := lib.NewClient() + cli, err := guacd.NewClient() if err != nil { return nil, fmt.Errorf("occamy-lib: new client error: %w", err) } @@ -40,7 +40,7 @@ func NewSession(proto string) (*Session, error) { s.client.InitLogLevel(config.Runtime.Mode) err = s.client.LoadProtocolPlugin(proto) if err != nil { - s.close() + s.Close() return nil, fmt.Errorf("occamy-lib: load protocol plugin failed: %w", err) } s.ID = s.client.ID @@ -51,26 +51,24 @@ func NewSession(proto string) (*Session, error) { // reading/writing from the socket via read/write threads. The given socket, // parser, and any associated resources will be freed unless the user is not // added successfully. -func (s *Session) Join(ws *websocket.Conn, jwt *config.JWT, owner bool, unlock func()) error { - defer s.close() - lib.ResetErrors() +func (s *Session) Join(ws *websocket.Conn, jwt *config.JWT, owner bool) error { + guacd.ResetErrors() // 1. prepare socket pair fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) if err != nil { - unlock() return fmt.Errorf("new socket pair error: %w", err) } // 2. create guac socket using fds[0] - sock, err := lib.NewSocket(fds[0]) + sock, err := guacd.NewSocket(fds[0]) if err != nil { return fmt.Errorf("occamy-lib: create guac socket error: %w", err) } defer sock.Close() // 3. create guac user using created guac socket - u, err := lib.NewUser(sock, s.client, owner, jwt) + u, err := guacd.NewUser(sock, s.client, owner, jwt) if err != nil { return fmt.Errorf("occamy-lib: create guac user error: %w", err) } @@ -83,10 +81,10 @@ func (s *Session) Join(ws *websocket.Conn, jwt *config.JWT, owner bool, unlock f // 5. preparing connection err = u.Prepare() if err != nil { - unlock() return fmt.Errorf("occamy-lib: handle user connection error: %w", err) } - unlock() + + log.Println("start to handle connections...") // 6. handle connection done := make(chan struct{}, 1) @@ -102,7 +100,7 @@ func (s *Session) Join(ws *websocket.Conn, jwt *config.JWT, owner bool, unlock f } // Close closes a session. -func (s *Session) close() { +func (s *Session) Close() { if atomic.LoadUint64(&s.connectedUsers) > 0 { return } diff --git a/docker/Dockerfile b/docker/Dockerfile index 40be35c..bd2201b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -31,7 +31,7 @@ ENV LC_ALL=en_US.UTF-8 \ libtool \ libvncserver-devel \ make" \ - GO_VERSION=1.15.7 + GO_VERSION=1.17.3 RUN yum -y update && \ yum -y install epel-release $RUNTIME_DEPENDENCIES $BUILD_DEPENDENCIES && \ # see: https://github.com/Zer0CoolX/guacamole-install-rhel/issues/78#issuecomment-534620524 @@ -49,7 +49,7 @@ RUN mkdir /golang && \ tar -xvf go${GO_VERSION}.linux-amd64.tar.gz ADD . . RUN ./guacamole/src/build-libguac.sh /occamy/guacamole -RUN /golang/go/bin/go build -mod vendor -x -o occamyd +RUN /golang/go/bin/go build -x -o occamyd cmd/occamyd/* EXPOSE 5636 CMD ["/occamy/occamyd"] \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 4f1bbdd..463733d 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -27,24 +27,24 @@ services: networks: occamy_network: ipv4_address: 172.16.239.11 - rdp: - # user: root, password: Docker - image: umis/xubuntu-office-xrdp-desktop:v1.0 - container_name: rdp - environment: - - "USERNAME:root" - - "PASSWORD:Docker" - networks: - occamy_network: - ipv4_address: 172.16.239.12 + # rdp: + # # user: root, password: Docker + # image: umis/xubuntu-office-xrdp-desktop:v1.0 + # container_name: rdp + # environment: + # - "USERNAME:root" + # - "PASSWORD:Docker" + # networks: + # occamy_network: + # ipv4_address: 172.16.239.12 - ssh: - # user: root, password: root - image: rastasheep/ubuntu-sshd:14.04 - container_name: ssh - networks: - occamy_network: - ipv4_address: 172.16.239.13 + # ssh: + # # user: root, password: root + # image: rastasheep/ubuntu-sshd:14.04 + # container_name: ssh + # networks: + # occamy_network: + # ipv4_address: 172.16.239.13 networks: occamy_network: diff --git a/go.mod b/go.mod index 10a0f25..f89587b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module changkun.de/x/occamy go 1.15 require ( - gioui.org v0.0.0-20211202105001-872b4ba41be0 + gioui.org v0.0.0-20211207114553-03016f0c69b7 github.com/appleboy/gin-jwt/v2 v2.6.2 github.com/gin-gonic/gin v1.4.0 github.com/gorilla/websocket v1.4.0 diff --git a/go.sum b/go.sum index ff094b6..3af306a 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -gioui.org v0.0.0-20211126213425-227c5a132be6 h1:I/VXI7iHRGYHVqkhLBOxw43zvDH+uzANxKhCj2DNbms= -gioui.org v0.0.0-20211126213425-227c5a132be6/go.mod h1:yoWOxPng6WkDpsud+NRmkoftmyWn3rkKsYGEcWHpjTI= -gioui.org v0.0.0-20211202105001-872b4ba41be0 h1:rXO+2zdXvX6G19M5oF1fs1U7kmUWb4uXCKWa+WHZSpA= -gioui.org v0.0.0-20211202105001-872b4ba41be0/go.mod h1:yoWOxPng6WkDpsud+NRmkoftmyWn3rkKsYGEcWHpjTI= +gioui.org v0.0.0-20211207114553-03016f0c69b7 h1:iJza27sQfbK41uk2Jlx8MpsrlWqMj7n5vCg0GGcC9DM= +gioui.org v0.0.0-20211207114553-03016f0c69b7/go.mod h1:yoWOxPng6WkDpsud+NRmkoftmyWn3rkKsYGEcWHpjTI= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc= gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= diff --git a/internal/guac/buttons.go b/internal/guac/buttons.go new file mode 100644 index 0000000..c42d03c --- /dev/null +++ b/internal/guac/buttons.go @@ -0,0 +1,269 @@ +// Copyright 2021 Changkun Ou. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// The following code is modified from +// https://github.com/deluan/bring +// Authored by Deluan Quintao released under MIT license. + +package guac + +import "gioui.org/io/pointer" + +// Mouse buttons recognized by guacd +type MouseButton int + +const ( + MouseLeft MouseButton = 1 << iota + MouseMiddle + MouseRight + MouseUp + MouseDown +) + +var MouseToGioButton = map[pointer.Buttons]MouseButton{ + pointer.ButtonPrimary: MouseLeft, + pointer.ButtonTertiary: MouseMiddle, + pointer.ButtonSecondary: MouseRight, +} + +// Keys recognized by guacd. ASCII symbols from 32 to 126 do not need mapping. +type KeyCode int32 + +const ( + KeyAgain KeyCode = 1024 + iota + KeyAllCandidates + KeyAlphanumeric + KeyLeftAlt + KeyRightAlt + KeyAttn + KeyAltGraph + KeyArrowDown + KeyArrowLeft + KeyArrowRight + KeyArrowUp + KeyBackspace + KeyCapsLock + KeyCancel + KeyClear + KeyConvert + KeyCopy + KeyCrsel + KeyCrSel + KeyCodeInput + KeyCompose + KeyLeftControl + KeyRightControl + KeyContextMenu + KeyDelete + KeyDown + KeyEnd + KeyEnter + KeyEraseEof + KeyEscape + KeyExecute + KeyExsel + KeyExSel + KeyF1 + KeyF2 + KeyF3 + KeyF4 + KeyF5 + KeyF6 + KeyF7 + KeyF8 + KeyF9 + KeyF10 + KeyF11 + KeyF12 + KeyF13 + KeyF14 + KeyF15 + KeyF16 + KeyF17 + KeyF18 + KeyF19 + KeyF20 + KeyF21 + KeyF22 + KeyF23 + KeyF24 + KeyFind + KeyGroupFirst + KeyGroupLast + KeyGroupNext + KeyGroupPrevious + KeyFullWidth + KeyHalfWidth + KeyHangulMode + KeyHankaku + KeyHanjaMode + KeyHelp + KeyHiragana + KeyHiraganaKatakana + KeyHome + KeyHyper + KeyInsert + KeyJapaneseHiragana + KeyJapaneseKatakana + KeyJapaneseRomaji + KeyJunjaMode + KeyKanaMode + KeyKanjiMode + KeyKatakana + KeyLeft + KeyMeta + KeyModeChange + KeyNumLock + KeyPageDown + KeyPageUp + KeyPause + KeyPlay + KeyPreviousCandidate + KeyPrintScreen + KeyRedo + KeyRight + KeyRomanCharacters + KeyScroll + KeySelect + KeySeparator + KeyLeftShift + KeyRightShift + KeySingleCandidate + KeySuper + KeyTab + KeyUIKeyInputDownArrow + KeyUIKeyInputEscape + KeyUIKeyInputLeftArrow + KeyUIKeyInputRightArrow + KeyUIKeyInputUpArrow + KeyUp + KeyUndo + KeyWin + KeyZenkaku + KeyZenkakuHankaku +) + +// KeyCodes mapped to X11 keysyms (used by guacd) +type keySym []int + +var keySyms map[KeyCode]keySym + +func init() { + keySyms = make(map[KeyCode]keySym) + keySyms[KeyAgain] = keySym{0xFF66} + keySyms[KeyAllCandidates] = keySym{0xFF3D} + keySyms[KeyAlphanumeric] = keySym{0xFF30} + keySyms[KeyLeftAlt] = keySym{0xFFE9} + keySyms[KeyRightAlt] = keySym{0xFFE9, 0xFE03} + keySyms[KeyAttn] = keySym{0xFD0E} + keySyms[KeyAltGraph] = keySym{0xFE03} + keySyms[KeyArrowDown] = keySym{0xFF54} + keySyms[KeyArrowLeft] = keySym{0xFF51} + keySyms[KeyArrowRight] = keySym{0xFF53} + keySyms[KeyArrowUp] = keySym{0xFF52} + keySyms[KeyBackspace] = keySym{0xFF08} + keySyms[KeyCapsLock] = keySym{0xFFE5} + keySyms[KeyCancel] = keySym{0xFF69} + keySyms[KeyClear] = keySym{0xFF0B} + keySyms[KeyConvert] = keySym{0xFF21} + keySyms[KeyCopy] = keySym{0xFD15} + keySyms[KeyCrsel] = keySym{0xFD1C} + keySyms[KeyCrSel] = keySym{0xFD1C} + keySyms[KeyCodeInput] = keySym{0xFF37} + keySyms[KeyCompose] = keySym{0xFF20} + keySyms[KeyLeftControl] = keySym{0xFFE3} + keySyms[KeyRightControl] = keySym{0xFFE3, 0xFFE4} + keySyms[KeyContextMenu] = keySym{0xFF67} + keySyms[KeyDelete] = keySym{0xFFFF} + keySyms[KeyDown] = keySym{0xFF54} + keySyms[KeyEnd] = keySym{0xFF57} + keySyms[KeyEnter] = keySym{0xFF0D} + keySyms[KeyEraseEof] = keySym{0xFD06} + keySyms[KeyEscape] = keySym{0xFF1B} + keySyms[KeyExecute] = keySym{0xFF62} + keySyms[KeyExsel] = keySym{0xFD1D} + keySyms[KeyExSel] = keySym{0xFD1D} + keySyms[KeyF1] = keySym{0xFFBE} + keySyms[KeyF2] = keySym{0xFFBF} + keySyms[KeyF3] = keySym{0xFFC0} + keySyms[KeyF4] = keySym{0xFFC1} + keySyms[KeyF5] = keySym{0xFFC2} + keySyms[KeyF6] = keySym{0xFFC3} + keySyms[KeyF7] = keySym{0xFFC4} + keySyms[KeyF8] = keySym{0xFFC5} + keySyms[KeyF9] = keySym{0xFFC6} + keySyms[KeyF10] = keySym{0xFFC7} + keySyms[KeyF11] = keySym{0xFFC8} + keySyms[KeyF12] = keySym{0xFFC9} + keySyms[KeyF13] = keySym{0xFFCA} + keySyms[KeyF14] = keySym{0xFFCB} + keySyms[KeyF15] = keySym{0xFFCC} + keySyms[KeyF16] = keySym{0xFFCD} + keySyms[KeyF17] = keySym{0xFFCE} + keySyms[KeyF18] = keySym{0xFFCF} + keySyms[KeyF19] = keySym{0xFFD0} + keySyms[KeyF20] = keySym{0xFFD1} + keySyms[KeyF21] = keySym{0xFFD2} + keySyms[KeyF22] = keySym{0xFFD3} + keySyms[KeyF23] = keySym{0xFFD4} + keySyms[KeyF24] = keySym{0xFFD5} + keySyms[KeyFind] = keySym{0xFF68} + keySyms[KeyGroupFirst] = keySym{0xFE0C} + keySyms[KeyGroupLast] = keySym{0xFE0E} + keySyms[KeyGroupNext] = keySym{0xFE08} + keySyms[KeyGroupPrevious] = keySym{0xFE0A} + keySyms[KeyFullWidth] = keySym(nil) + keySyms[KeyHalfWidth] = keySym(nil) + keySyms[KeyHangulMode] = keySym{0xFF31} + keySyms[KeyHankaku] = keySym{0xFF29} + keySyms[KeyHanjaMode] = keySym{0xFF34} + keySyms[KeyHelp] = keySym{0xFF6A} + keySyms[KeyHiragana] = keySym{0xFF25} + keySyms[KeyHiraganaKatakana] = keySym{0xFF27} + keySyms[KeyHome] = keySym{0xFF50} + keySyms[KeyHyper] = keySym{0xFFED, 0xFFED, 0xFFEE} + keySyms[KeyInsert] = keySym{0xFF63} + keySyms[KeyJapaneseHiragana] = keySym{0xFF25} + keySyms[KeyJapaneseKatakana] = keySym{0xFF26} + keySyms[KeyJapaneseRomaji] = keySym{0xFF24} + keySyms[KeyJunjaMode] = keySym{0xFF38} + keySyms[KeyKanaMode] = keySym{0xFF2D} + keySyms[KeyKanjiMode] = keySym{0xFF21} + keySyms[KeyKatakana] = keySym{0xFF26} + keySyms[KeyLeft] = keySym{0xFF51} + keySyms[KeyMeta] = keySym{0xFFE7, 0xFFE7, 0xFFE8} + keySyms[KeyModeChange] = keySym{0xFF7E} + keySyms[KeyNumLock] = keySym{0xFF7F} + keySyms[KeyPageDown] = keySym{0xFF56} + keySyms[KeyPageUp] = keySym{0xFF55} + keySyms[KeyPause] = keySym{0xFF13} + keySyms[KeyPlay] = keySym{0xFD16} + keySyms[KeyPreviousCandidate] = keySym{0xFF3E} + keySyms[KeyPrintScreen] = keySym{0xFF61} + keySyms[KeyRedo] = keySym{0xFF66} + keySyms[KeyRight] = keySym{0xFF53} + keySyms[KeyRomanCharacters] = keySym(nil) + keySyms[KeyScroll] = keySym{0xFF14} + keySyms[KeySelect] = keySym{0xFF60} + keySyms[KeySeparator] = keySym{0xFFAC} + keySyms[KeyLeftShift] = keySym{0xFFE1} + keySyms[KeyRightShift] = keySym{0xFFE1, 0xFFE2} + keySyms[KeySingleCandidate] = keySym{0xFF3C} + keySyms[KeySuper] = keySym{0xFFEB, 0xFFEB, 0xFFEC} + keySyms[KeyTab] = keySym{0xFF09} + keySyms[KeyUIKeyInputDownArrow] = keySym{0xFF54} + keySyms[KeyUIKeyInputEscape] = keySym{0xFF1B} + keySyms[KeyUIKeyInputLeftArrow] = keySym{0xFF51} + keySyms[KeyUIKeyInputRightArrow] = keySym{0xFF53} + keySyms[KeyUIKeyInputUpArrow] = keySym{0xFF52} + keySyms[KeyUp] = keySym{0xFF52} + keySyms[KeyUndo] = keySym{0xFF65} + keySyms[KeyWin] = keySym{0xFFEB} + keySyms[KeyZenkaku] = keySym{0xFF28} + keySyms[KeyZenkakuHankaku] = keySym{0xFF2} + + for ch := 32; ch < 127; ch++ { + keySyms[KeyCode(ch)] = keySym{ch} + } +} diff --git a/internal/guac/client.go b/internal/guac/client.go new file mode 100644 index 0000000..5816c10 --- /dev/null +++ b/internal/guac/client.go @@ -0,0 +1,181 @@ +// Copyright 2021 Changkun Ou. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// The following code is modified from +// https://github.com/deluan/bring +// Authored by Deluan Quintao released under MIT license. + +package guac + +import ( + "errors" + "image" + "log" + "strconv" + "time" + + "changkun.de/x/occamy/internal/protocol" + "gioui.org/app" +) + +var ErrInvalidKeyCode = errors.New("invalid key code") + +type OnSyncFunc = func(image image.Image, lastUpdate int64) + +// Guacamole protocol client. Automatically handles incoming and outgoing Guacamole instructions, +// updating its display using one or more graphic primitives. +type Client struct { + session *session + display *display + streams streams + onSync OnSyncFunc +} + +// NewClient creates a Client and connects it to the guacd server with the provided configuration. Logger is optional +func NewClient(addr string, config map[string]string, win *app.Window) (*Client, error) { + s, err := newSession(addr, config) + if err != nil { + return nil, err + } + + c := &Client{ + session: s, + display: newDisplay(), + streams: newStreams(), + } + go func() { + ping := time.NewTicker(pingFrequency) + defer ping.Stop() + for { + select { + case <-ping.C: + err := s.Send(protocol.NewInstruction("nop")) + if err != nil { + log.Printf("Failed ping the server: %s", err) + } + case <-s.done: + return + } + } + }() + + inschan := make(chan *protocol.Instruction, 100) + go func() { + for { + _, raw, err := s.tunnel.ReadMessage() + if err != nil { + log.Printf("Disconnecting from server. Reason: %v", err) + s.Terminate() + break + } + ins, err := protocol.ParseInstruction(raw) + if err != nil { + log.Printf("Failed to parse instruction: %v", err) + s.Terminate() + break + } + if ins.Opcode != "blob" { + log.Printf("S> %s", ins) + } + if ins.Opcode == "nop" { + continue + } + + inschan <- ins + } + }() + go func() { + log.Println("client instruction handler started!") + for ins := range inschan { + h, ok := handlers[ins.Opcode] + if !ok { + log.Printf("Instruction not implemented: %s", ins.Opcode) + continue + } + err = h(c, ins.Args) + if err != nil { + s.Terminate() + } + } + }() + return c, nil +} + +func (c *Client) OnSync(f OnSyncFunc) { + c.onSync = f +} + +// Returns a snapshot of the current screen, together with the last updated timestamp +func (c *Client) Screen() (image image.Image, lastUpdate int64) { + return c.display.getCanvas() +} + +// Returns the current session state +func (c *Client) State() SessionState { + return c.session.State +} + +// Send mouse events to the server. An event is composed by position of the +// cursor, and a list of any currently pressed MouseButtons +func (c *Client) SendMouse(p image.Point, pressedButtons ...MouseButton) error { + if c.session.State != SessionActive { + return ErrNotConnected + } + + buttonMask := 0 + for _, b := range pressedButtons { + buttonMask |= int(b) + } + c.display.moveCursor(p.X, p.Y) + err := c.session.Send(protocol.NewInstruction("mouse", strconv.Itoa(p.X), strconv.Itoa(p.Y), strconv.Itoa(buttonMask))) + if err != nil { + return err + } + return nil +} + +// Send the sequence of characters as they were typed. Only works with simple chars +// (no combination with control keys) +func (c *Client) SendText(sequence string) error { + if c.session.State != SessionActive { + return ErrNotConnected + } + + for _, ch := range sequence { + keycode := strconv.Itoa(int(ch)) + err := c.session.Send(protocol.NewInstruction("key", keycode, "1")) + if err != nil { + return nil + } + err = c.session.Send(protocol.NewInstruction("key", keycode, "0")) + if err != nil { + return nil + } + } + return nil +} + +// Send key presses and releases. +func (c *Client) SendKey(key KeyCode, pressed bool) error { + if c.session.State != SessionActive { + return ErrNotConnected + } + + p := "0" + if pressed { + p = "1" + } + keySym, ok := keySyms[key] + if !ok { + return ErrInvalidKeyCode + } + for _, k := range keySym { + keycode := strconv.Itoa(k) + err := c.session.Send(protocol.NewInstruction("key", keycode, p)) + if err != nil { + return nil + } + } + return nil +} diff --git a/internal/guac/display.go b/internal/guac/display.go new file mode 100644 index 0000000..93c1d91 --- /dev/null +++ b/internal/guac/display.go @@ -0,0 +1,196 @@ +// Copyright 2021 Changkun Ou. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// The following code is modified from +// https://github.com/deluan/bring +// Authored by Deluan Quintao released under MIT license. + +package guac + +import ( + "fmt" + "image" + "image/draw" + "log" + "time" + + "changkun.de/x/occamy/internal/uuid" +) + +var compositeOperations = map[byte]draw.Op{ + 0xC: draw.Src, + 0xE: draw.Over, +} + +type display struct { + cursor *layer + cursorHotspotX int + cursorHotspotY int + cursorX int + cursorY int + tasks []task + layers layers + defaultLayer *layer + canvas *image.RGBA + lastUpdate int64 +} + +func newDisplay() *display { + d := &display{ + cursor: newBuffer(), + layers: newLayers(), + canvas: image.NewRGBA(image.Rectangle{}), + } + d.defaultLayer = d.layers.getDefault() + return d +} + +type taskFunc func() error + +type task struct { + taskFunc taskFunc + name string + uuid string +} + +func (t *task) String() string { + return fmt.Sprintf("%s [%s]", t.name, t.uuid) +} + +func (d *display) scheduleTask(name string, t taskFunc) { + task := task{ + taskFunc: t, + name: name, + uuid: uuid.NewID(""), + } + log.Printf("Adding new task: %s. Total: %d", task.String(), len(d.tasks)+1) + d.tasks = append(d.tasks, task) +} + +func (d *display) processSingleTask(t task) { + log.Printf("Executing task %s", t.String()) + err := t.taskFunc() + if err != nil { + log.Printf("Skipping task %s due to error. This can lead to invalid screen state! Error: %s", t.String(), err) + return + } + if !d.defaultLayer.modified { + return + } + // TODO: Only update canvas after all tasks are applied? + mr := d.defaultLayer.modifiedRect + copyImage(d.canvas, mr.Min.X, mr.Min.Y, d.defaultLayer.image, mr, draw.Src) + d.lastUpdate = time.Now().UnixNano() + + d.defaultLayer.resetModified() +} + +func (d *display) flush() { + if len(d.tasks) == 0 { + return + } + log.Printf("Processing %d pending tasks", len(d.tasks)) + for _, t := range d.tasks { + d.processSingleTask(t) + } + log.Println("All pending tasks were completed") + d.tasks = nil +} + +func (d *display) getCanvas() (image.Image, int64) { + return d.canvas, d.lastUpdate +} + +func (d *display) dispose(layerIdx int) { + d.scheduleTask("dispose", func() error { + d.layers.delete(layerIdx) + return nil + }) +} + +func (d *display) copy(srcL, srcX, srcY, srcWidth, srcHeight, dstL, dstX, dstY int, compositeOperation byte) { + op := compositeOperations[compositeOperation] + d.scheduleTask("copy", func() error { + srcLayer := d.layers.get(srcL) + dstLayer := d.layers.get(dstL) + dstLayer.Copy(srcLayer, srcX, srcY, srcWidth, srcHeight, dstX, dstY, op) + return nil + }) +} + +func (d *display) draw(layerIdx, x, y int, compositeOperation byte, s *stream) { + op := compositeOperations[compositeOperation] + img, err := s.image() + + d.scheduleTask("draw", func() error { + if err != nil { + return err + } + layer := d.layers.get(layerIdx) + layer.Draw(x, y, img, op) + return nil + }) +} + +func (d *display) fill(layerIdx int, r, g, b, a, compositeOperation byte) { + op := compositeOperations[compositeOperation] + d.scheduleTask("fill", func() error { + layer := d.layers.get(layerIdx) + layer.Fill(r, g, b, a, op) + return nil + }) +} +func (d *display) rect(layerIdx int, x int, y int, width int, height int) { + d.scheduleTask("rect", func() error { + layer := d.layers.get(layerIdx) + layer.Rect(x, y, width, height) + return nil + }) +} + +func (d *display) resize(layerIdx, w, h int) { + d.scheduleTask("resize", func() error { + layer := d.layers.get(layerIdx) + layer.Resize(w, h) + if layerIdx == 0 { + d.canvas = image.NewRGBA(layer.image.Bounds()) + copyImage(d.canvas, 0, 0, layer.image, layer.image.Bounds(), draw.Src) + } + return nil + }) +} + +func (d *display) hideCursor() { + cr := image.Rect(d.cursorX, d.cursorY, d.cursorX+d.cursor.width, d.cursorY+d.cursor.height) + copyImage(d.canvas, d.cursorX, d.cursorY, d.defaultLayer.image, cr, draw.Src) +} + +func (d *display) moveCursor(x, y int) { + d.hideCursor() + + d.cursorX = x + d.cursorY = y + + copyImage(d.canvas, d.cursorX, d.cursorY, d.cursor.image, d.cursor.image.Bounds(), draw.Over) + d.lastUpdate = time.Now().UnixNano() +} + +func (d *display) setCursor(cursorHotspotX, cursorHotspotY, srcL, srcX, srcY, srcWidth, srcHeight int) { + d.scheduleTask("setCursor", func() error { + d.hideCursor() + + layer := d.layers.get(srcL) + d.cursor.Resize(srcWidth, srcHeight) + d.cursor.Copy(layer, srcX, srcY, srcWidth, srcHeight, 0, 0, draw.Src) + d.cursorHotspotX = cursorHotspotX + d.cursorHotspotY = cursorHotspotY + + // TODO: Calculate correct position based on cursorHotspot + //d.cursorX = cursorHotspotX + //d.cursorY = cursorHotspotY + + copyImage(d.canvas, d.cursorX, d.cursorY, d.cursor.image, d.cursor.image.Bounds(), draw.Over) + return nil + }) +} diff --git a/internal/guac/handlers.go b/internal/guac/handlers.go new file mode 100644 index 0000000..2486743 --- /dev/null +++ b/internal/guac/handlers.go @@ -0,0 +1,143 @@ +// Copyright 2021 Changkun Ou. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// The following code is modified from +// https://github.com/deluan/bring +// Authored by Deluan Quintao released under MIT license. + +package guac + +import ( + "log" + "strconv" + + "changkun.de/x/occamy/internal/protocol" +) + +// Handler func for Guacamole instructions +type handlerFunc = func(client *Client, args []string) error + +// Handlers for all instruction opcodes receivable by this Guacamole client. +var handlers = map[string]handlerFunc{ + "blob": func(c *Client, args []string) error { + idx := parseInt(args[0]) + return c.streams.append(idx, args[1]) + }, + + "copy": func(c *Client, args []string) error { + srcL := parseInt(args[0]) + srcX := parseInt(args[1]) + srcY := parseInt(args[2]) + srcWidth := parseInt(args[3]) + srcHeight := parseInt(args[4]) + mask := parseInt(args[5]) + dstL := parseInt(args[6]) + dstX := parseInt(args[7]) + dstY := parseInt(args[8]) + c.display.copy(srcL, srcX, srcY, srcWidth, srcHeight, + dstL, dstX, dstY, byte(mask)) + return nil + }, + + "cfill": func(c *Client, args []string) error { + mask := parseInt(args[0]) + layerIdx := parseInt(args[1]) + r := parseInt(args[2]) + g := parseInt(args[3]) + b := parseInt(args[4]) + a := parseInt(args[5]) + c.display.fill(layerIdx, byte(r), byte(g), byte(b), byte(a), byte(mask)) + return nil + }, + + "cursor": func(c *Client, args []string) error { + cursorHotspotX := parseInt(args[0]) + cursorHotspotY := parseInt(args[1]) + srcL := parseInt(args[2]) + srcX := parseInt(args[3]) + srcY := parseInt(args[4]) + srcWidth := parseInt(args[5]) + srcHeight := parseInt(args[6]) + c.display.setCursor(cursorHotspotX, cursorHotspotY, + srcL, srcX, srcY, srcWidth, srcHeight) + return nil + }, + + "disconnect": func(c *Client, args []string) error { + c.session.Terminate() + return nil + }, + + "dispose": func(c *Client, args []string) error { + layerIdx := parseInt(args[0]) + c.display.dispose(layerIdx) + return nil + }, + + "end": func(c *Client, args []string) error { + idx := parseInt(args[0]) + c.streams.end(idx) + c.streams.delete(idx) + return nil + }, + + "error": func(c *Client, args []string) error { + log.Printf("Received error from server: (%s) - %s", args[1], args[0]) + return nil + }, + + "img": func(c *Client, args []string) error { + s := c.streams.get(parseInt(args[0])) + op := byte(parseInt(args[1])) + layerIdx := parseInt(args[2]) + //mimetype := args[3] // Not used + x := parseInt(args[4]) + y := parseInt(args[5]) + s.onEnd = func(s *stream) { + c.display.draw(layerIdx, x, y, op, s) + } + return nil + }, + + "log": func(c *Client, args []string) error { + log.Printf("Log from server: %s", args[0]) + return nil + }, + + "rect": func(c *Client, args []string) error { + layerIdx := parseInt(args[0]) + x := parseInt(args[1]) + y := parseInt(args[2]) + w := parseInt(args[3]) + h := parseInt(args[4]) + c.display.rect(layerIdx, x, y, w, h) + return nil + }, + + "size": func(c *Client, args []string) error { + layerIdx := parseInt(args[0]) + w := parseInt(args[1]) + h := parseInt(args[2]) + c.display.resize(layerIdx, w, h) + return nil + }, + + "sync": func(c *Client, args []string) error { + c.display.flush() + if err := c.session.Send(protocol.NewInstruction("sync", args...)); err != nil { + log.Printf("Failed to send 'sync' back to server: %s", err) + return err + } + if c.onSync != nil { + img, ts := c.display.getCanvas() + c.onSync(img, ts) + } + return nil + }, +} + +func parseInt(s string) int { + n, _ := strconv.Atoi(s) + return n +} diff --git a/internal/guac/layer.go b/internal/guac/layer.go new file mode 100644 index 0000000..d19b929 --- /dev/null +++ b/internal/guac/layer.go @@ -0,0 +1,196 @@ +// Copyright 2021 Changkun Ou. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// The following code is modified from +// https://github.com/deluan/bring +// Authored by Deluan Quintao released under MIT license. + +package guac + +import ( + "image" + "image/draw" + + "github.com/tfriedel6/canvas" + "github.com/tfriedel6/canvas/backend/softwarebackend" +) + +type layer struct { + width int + height int + image *image.RGBA + gc *canvas.Canvas + visible bool + modified bool + modifiedRect image.Rectangle + pathOpen bool + pathRect image.Rectangle + autosize bool +} + +func (l *layer) updateModifiedRect(modArea image.Rectangle) { + before := l.modifiedRect + l.modifiedRect = l.modifiedRect.Union(modArea) + l.modified = l.modified || !before.Eq(l.modifiedRect) +} + +func (l *layer) resetModified() { + l.modifiedRect = image.Rectangle{} + l.modified = false +} + +func (l *layer) setupCanvas() { + be := softwarebackend.New(l.width, l.height) + be.Image = l.image + l.gc = canvas.New(be) +} + +func (l *layer) fitRect(x int, y int, w int, h int) { + rect := image.Rect(x, y, x+w, y+h) + final := l.image.Bounds().Union(rect) + l.Resize(final.Max.X, final.Max.Y) +} + +func copyImage(dest draw.Image, x, y int, src image.Image, sr image.Rectangle, op draw.Op) { + dp := image.Pt(x, y) + dr := image.Rectangle{Min: dp, Max: dp.Add(sr.Size())} + draw.Draw(dest, dr, src, sr.Min, op) +} + +func (l *layer) Copy(srcLayer *layer, srcx, srcy, srcw, srch, x, y int, op draw.Op) { + srcImg := srcLayer.image + srcDim := srcImg.Bounds() + + // If entire rectangle outside source canvas, stop + if srcx >= srcDim.Max.X || srcy >= srcDim.Max.Y { + return + } + + // Otherwise, clip rectangle to area + if srcx+srcw > srcDim.Max.X { + srcw = srcDim.Max.X - srcx + } + + if srcy+srch > srcDim.Max.Y { + srch = srcDim.Max.Y - srcy + } + + // Stop if nothing to draw. + if srcw == 0 || srch == 0 { + return + } + + if l.autosize { + l.fitRect(x, y, srcw, srch) + } + + srcCopyDim := image.Rect(srcx, srcy, srcx+srcw, srcy+srch) + copyImage(l.image, x, y, srcImg, srcCopyDim, op) + l.updateModifiedRect(image.Rect(x, y, x+srcw, y+srch)) +} + +func (l *layer) Draw(x, y int, src image.Image, op draw.Op) { + srcDim := src.Bounds() + if l.autosize { + l.fitRect(x, y, srcDim.Max.X, srcDim.Max.Y) + } + copyImage(l.image, x, y, src, srcDim, op) + l.updateModifiedRect(image.Rect(x, y, x+srcDim.Max.X, y+srcDim.Max.Y)) +} + +func (l *layer) Resize(w int, h int) { + original := l.image.Bounds() + if w == l.width && h == l.height { + return + } + newImage := image.NewRGBA(image.Rect(0, 0, w, h)) + draw.Draw(newImage, l.image.Bounds(), l.image, image.Pt(0, 0), draw.Src) + l.image = newImage + l.width = w + l.height = h + l.setupCanvas() + l.updateModifiedRect(original.Union(l.image.Bounds())) +} + +func (l *layer) appendToPath(rect image.Rectangle) { + if !l.pathOpen { + l.gc.BeginPath() + l.pathOpen = true + l.pathRect = image.Rectangle{} + } + l.pathRect = l.pathRect.Union(rect) +} + +func (l *layer) endPath() { + l.updateModifiedRect(l.pathRect) + l.pathOpen = false + l.pathRect = image.Rectangle{} +} + +func (l *layer) Rect(x int, y int, width int, height int) { + l.appendToPath(image.Rect(x, y, x+width, y+height)) + l.gc.Rect(float64(x), float64(y), float64(width), float64(height)) +} + +func (l *layer) Fill(r byte, g byte, b byte, a byte, op draw.Op) { + // Ignores op, as the canvas library does not support it :/ + l.gc.SetFillStyle(r, g, b, a) + l.gc.Fill() + l.endPath() +} + +type layers map[int]*layer + +func newLayers() layers { + ls := make(layers) + ls[0] = newBuffer() + ls[0].visible = true + return ls +} + +func newBuffer() *layer { + l := &layer{ + image: image.NewRGBA(image.Rect(0, 0, 0, 0)), + autosize: true, + } + l.setupCanvas() + return l +} + +func newVisibleLayer(l0 *layer) *layer { + l := &layer{ + width: l0.width, + height: l0.height, + image: image.NewRGBA(image.Rect(0, 0, l0.width, l0.height)), + visible: true, + } + l.setupCanvas() + return l +} + +func (ls layers) getDefault() *layer { + return ls[0] +} + +func (ls layers) get(id int) *layer { + if l, ok := ls[id]; ok { + return l + } + if id > 0 { + ls[id] = newVisibleLayer(ls[0]) + } else { + ls[id] = newBuffer() + } + return ls[id] +} + +func (ls layers) delete(id int) { + if id == 0 { + return + } + ls[0].updateModifiedRect(ls[id].image.Bounds()) + ls[id].image = nil + ls[id] = nil + delete(ls, id) +} diff --git a/internal/guac/session.go b/internal/guac/session.go new file mode 100644 index 0000000..9edc5d5 --- /dev/null +++ b/internal/guac/session.go @@ -0,0 +1,125 @@ +// Copyright 2021 Changkun Ou. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// The following code is modified from +// https://github.com/deluan/bring +// Authored by Deluan Quintao released under MIT license. + +package guac + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "log" + "net/http" + "strings" + "time" + + "changkun.de/x/occamy/internal/protocol" + "github.com/gorilla/websocket" +) + +type SessionState int + +const ( + SessionClosed SessionState = iota + SessionHandshake + SessionActive +) + +var ErrNotConnected = errors.New("not connected") + +const pingFrequency = 5 * time.Second + +// Session is used to create and keep a connection with a guacd server, +// and it is responsible for the initial handshake and to send and receive instructions. +// Instructions received are put in the In channel. Instructions are sent using the Send() function +type session struct { + State SessionState + Id string + + tunnel *websocket.Conn + ins chan *protocol.Instruction + done chan bool + config map[string]string +} + +// newSession creates a new connection with the guacd server, using the configuration provided +func newSession(addr string, config map[string]string) (*session, error) { + b, err := json.Marshal(config) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Post("http://"+addr+"/api/v1/login", "application/json", bytes.NewReader(b)) + if err != nil { + return nil, err + } + b, err = io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body.Close() + token := struct { + Token string `json:"token"` + }{} + err = json.Unmarshal(b, &token) + if err != nil { + return nil, err + } + + c, _, err := websocket.DefaultDialer.Dial("ws://"+addr+"/api/v1/connect?token="+token.Token, nil) + if err != nil { + return nil, err + } + + s := &session{ + State: SessionClosed, + done: make(chan bool), + tunnel: c, + ins: make(chan *protocol.Instruction, 100), + config: config, + } + go s.sender() + log.Printf("Initiating %s session with %s", strings.ToUpper(config["protocol"]), addr) + s.State = SessionActive + log.Printf("Handshake successful. Got connection ID %s", s.Id) + return s, nil +} + +// Terminate the current session, disconnecting from the server +func (s *session) Terminate() { + if s.State == SessionClosed { + return + } + close(s.done) +} + +// Send instructions to the server. Multiple instructions are sent in one single transaction +func (s *session) Send(ins ...*protocol.Instruction) (err error) { + // Serialize the sending instructions. + for _, i := range ins { + s.ins <- i + } + return +} + +func (s *session) sender() { + for { + select { + case ins := <-s.ins: + log.Printf("C> %s", ins) + err := s.tunnel.WriteMessage(websocket.TextMessage, []byte(ins.String())) + if err != nil { + return + } + case <-s.done: + _ = s.tunnel.WriteMessage(websocket.TextMessage, []byte(protocol.NewInstruction("disconnect").String())) + s.State = SessionClosed + s.tunnel.Close() + return + } + } +} diff --git a/internal/guac/stream.go b/internal/guac/stream.go new file mode 100644 index 0000000..6374225 --- /dev/null +++ b/internal/guac/stream.go @@ -0,0 +1,64 @@ +// Copyright 2021 Changkun Ou. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// The following code is modified from +// https://github.com/deluan/bring +// Authored by Deluan Quintao released under MIT license. + +package guac + +import ( + "bytes" + "encoding/base64" + "image" +) + +type onEndFunc func(s *stream) + +type stream struct { + buffer *bytes.Buffer + onEnd onEndFunc +} + +func (s *stream) image() (image.Image, error) { + dec := base64.NewDecoder(base64.StdEncoding, s.buffer) + img, _, err := image.Decode(dec) + return img, err +} + +type streams map[int]*stream + +func newStreams() streams { + return make(map[int]*stream) +} + +func (ss streams) get(id int) *stream { + if s, ok := ss[id]; ok { + return s + } + s := &stream{ + buffer: &bytes.Buffer{}, + } + ss[id] = s + return s +} + +func (ss streams) append(id int, data string) error { + s := ss.get(id) + _, err := s.buffer.WriteString(data) + return err +} + +func (ss streams) end(id int) { + s := ss.get(id) + if s.onEnd != nil { + s.onEnd(s) + } +} + +func (ss streams) delete(id int) { + ss[id].buffer = nil + ss[id] = nil + delete(ss, id) +} diff --git a/internal/lib/client.go b/internal/guacd/client.go similarity index 80% rename from internal/lib/client.go rename to internal/guacd/client.go index 426d269..073fda5 100644 --- a/internal/lib/client.go +++ b/internal/guacd/client.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package lib +package guacd /* #cgo LDFLAGS: -L/usr/local/lib -lguac @@ -30,25 +30,11 @@ void init_client_log(guac_client* client, int level) { */ import "C" import ( - "sync" - "time" "unsafe" "changkun.de/x/occamy/internal/uuid" ) -// ClientMouse ... -type ClientMouse int - -// ClientMouse constants -const ( - ClientMouseLeft ClientMouse = 0x01 - ClientMouseMiddle ClientMouse = 0x02 - ClientMouseRight ClientMouse = 0x04 - ClientMouseScrollUp ClientMouse = 0x08 - ClientMouseScrollDown ClientMouse = 0x10 -) - // clientLogLevel All supported log levels used by the logging subsystem of each Occamy // client. With the exception of GUAC_LOG_TRACE, these log levels correspond to // a subset of the log levels defined by RFC 5424. @@ -83,22 +69,10 @@ var clientLogLevelTable = map[string]clientLogLevel{ // Client is a guacamole client container type Client struct { + ID string + args []string + guacClient *C.struct_guac_client - ID string - running bool - data interface{} - lastSent time.Time - - mu sync.RWMutex - users *User // list of all connected users - owner *User - connectedUsers int64 - - handlerFree func() - handlerLog func() - handlerJoin func() - handlerLeave func() - args []string } // NewClient creates a new guacamole client @@ -114,21 +88,11 @@ func NewClient() (*Client, error) { return &Client{ guacClient: cli, - ID: id, - running: true, - lastSent: time.Now(), - args: []string{}, + ID: id, + args: []string{}, }, nil } -// isRunning checks if a client is still running -func (c *Client) isRunning() bool { - if c.guacClient.state == C.GUAC_CLIENT_RUNNING { - return true - } - return false -} - // Close closes the corresponding guacamole client func (c *Client) Close() { C.guac_client_stop(c.guacClient) diff --git a/internal/lib/client_test.go b/internal/guacd/client_test.go similarity index 86% rename from internal/lib/client_test.go rename to internal/guacd/client_test.go index 1d2a87c..76b77c7 100644 --- a/internal/lib/client_test.go +++ b/internal/guacd/client_test.go @@ -2,16 +2,16 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package lib_test +package guacd_test import ( "testing" - "changkun.de/x/occamy/internal/lib" + "changkun.de/x/occamy/internal/guacd" ) func TestNewClient(t *testing.T) { - cli, err := lib.NewClient() + cli, err := guacd.NewClient() if err != nil { t.Errorf("%v", err) t.FailNow() diff --git a/internal/lib/errdefs.go b/internal/guacd/errdefs.go similarity index 99% rename from internal/lib/errdefs.go rename to internal/guacd/errdefs.go index e30846b..0054ba0 100644 --- a/internal/lib/errdefs.go +++ b/internal/guacd/errdefs.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package lib +package guacd /* #cgo LDFLAGS: -L/usr/local/lib -lguac diff --git a/internal/lib/errdefs_test.go b/internal/guacd/errdefs_test.go similarity index 71% rename from internal/lib/errdefs_test.go rename to internal/guacd/errdefs_test.go index 123b50a..6d29907 100644 --- a/internal/lib/errdefs_test.go +++ b/internal/guacd/errdefs_test.go @@ -2,14 +2,14 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package lib_test +package guacd_test import ( "testing" - "changkun.de/x/occamy/internal/lib" + "changkun.de/x/occamy/internal/guacd" ) func TestResetErrors(t *testing.T) { - t.Log(lib.ResetErrors()) + t.Log(guacd.ResetErrors()) } diff --git a/internal/lib/socket.go b/internal/guacd/socket.go similarity index 93% rename from internal/lib/socket.go rename to internal/guacd/socket.go index cf82430..da12c9c 100644 --- a/internal/lib/socket.go +++ b/internal/guacd/socket.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package lib +package guacd /* #cgo LDFLAGS: -L/usr/local/lib -lguac @@ -18,16 +18,6 @@ import ( "changkun.de/x/occamy/internal/protocol" ) -// type ISocket interface { -// Read() -// Write() -// Flush() -// Lock() -// UnLock() -// Select() -// Free() -// } - // Socket is a wrapper of given open file descriptor type Socket struct { guacSocket *C.struct_guac_socket diff --git a/internal/lib/socket_test.go b/internal/guacd/socket_test.go similarity index 88% rename from internal/lib/socket_test.go rename to internal/guacd/socket_test.go index 167ab44..64a886d 100644 --- a/internal/lib/socket_test.go +++ b/internal/guacd/socket_test.go @@ -2,13 +2,13 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package lib_test +package guacd_test import ( "syscall" "testing" - "changkun.de/x/occamy/internal/lib" + "changkun.de/x/occamy/internal/guacd" ) func TestNewSocket(t *testing.T) { @@ -18,12 +18,12 @@ func TestNewSocket(t *testing.T) { t.FailNow() } - sock1, err := lib.NewSocket(fds[0]) + sock1, err := guacd.NewSocket(fds[0]) if err != nil { t.Error("create lib socket error: ", err) t.FailNow() } - sock2, err := lib.NewSocket(fds[1]) + sock2, err := guacd.NewSocket(fds[1]) if err != nil { t.Error("create lib socket error: ", err) t.FailNow() diff --git a/internal/lib/user.go b/internal/guacd/user.go similarity index 86% rename from internal/lib/user.go rename to internal/guacd/user.go index c3c78fe..fe2763c 100644 --- a/internal/lib/user.go +++ b/internal/guacd/user.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package lib +package guacd /* #cgo LDFLAGS: -L/usr/local/lib -lguac @@ -51,7 +51,6 @@ static int join_handler_bridge(guac_user* user, int argc, char** argv) { import "C" import ( "errors" - "fmt" "log" "net" "sync" @@ -66,7 +65,7 @@ import ( const UserMaxStreams = 64 // UserClosedStreamIndex is the maximum number of inbound or -// outbound streams supported by any one lib.User +// outbound streams supported by any one guacd.User const UserClosedStreamIndex = -1 // User is the representation of a physical connection within a larger logical connection @@ -76,14 +75,10 @@ type User struct { guacClient *C.struct_guac_client once sync.Once - ID string - owner bool - active bool - info connectInformation - client *Client - sock *Socket - prev, next *User // points to next connected user - data interface{} + ID string + owner bool + info connectInformation + client *Client } type connectInformation struct { @@ -127,9 +122,8 @@ func NewUser(s *Socket, c *Client, owner bool, jwt *config.JWT) (*User, error) { guacUser: user, guacClient: c.guacClient, - ID: id, - owner: owner, - active: true, + ID: id, + owner: owner, info: connectInformation{ Host: host, Port: port, @@ -147,14 +141,6 @@ func (u *User) Close() { }) } -// isActive checks if a user is still active -func (u *User) isActive() bool { - if u.guacUser.active != 0 { - return true - } - return false -} - const usecTimeout time.Duration = 15 * time.Millisecond func (u *User) Prepare() error { @@ -219,14 +205,3 @@ func (u *User) HandleConnection(done chan struct{}) { C.guac_socket_flush(u.guacUser.socket) close(done) } - -// Stop signals the given user that it must disconnect, or advises -// cooperating services that the given user is no longer connected. -func (u *User) Stop() { - u.active = false -} - -// Debug logs debug information -func (u *User) Debug(format string, args ...interface{}) { - log.Printf(fmt.Sprintf("[u:%s] %s", u.ID, format), args) -} diff --git a/internal/lib/user_test.go b/internal/guacd/user_test.go similarity index 81% rename from internal/lib/user_test.go rename to internal/guacd/user_test.go index 1612f49..741586d 100644 --- a/internal/lib/user_test.go +++ b/internal/guacd/user_test.go @@ -2,13 +2,13 @@ // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. -package lib_test +package guacd_test import ( "syscall" "testing" - "changkun.de/x/occamy/internal/lib" + "changkun.de/x/occamy/internal/guacd" ) func TestNewUser(t *testing.T) { @@ -18,32 +18,33 @@ func TestNewUser(t *testing.T) { t.FailNow() } - sock1, err := lib.NewSocket(fds[0]) + sock1, err := guacd.NewSocket(fds[0]) if err != nil { t.Error("create socket1 in NewUser error: ", err) t.FailNow() } - sock2, err := lib.NewSocket(fds[1]) + sock2, err := guacd.NewSocket(fds[1]) if err != nil { t.Error("create socket2 in NewUser error: ", err) t.FailNow() } - cli, err := lib.NewClient() + cli, err := guacd.NewClient() if err != nil { t.Error("create client in NewUser error: ", err) t.FailNow() } - user, err := lib.NewUser(sock1, cli, true) + u, err := guacd.NewUser(sock1, cli, true) if err != nil { t.Error("NewUser error: ", err) t.FailNow() } + defer u.Close() t.Run("handle-conn", func(t *testing.T) { done := make(chan bool, 2) go func() { - err := user.HandleConnection() + err := u.HandleConnection() if err != nil { t.Error("user handle connection error: ", err) t.FailNow() @@ -61,6 +62,4 @@ func TestNewUser(t *testing.T) { }() <-done }) - - user.Close() } diff --git a/internal/protocol/benchmark_test.go b/internal/protocol/benchmark_test.go deleted file mode 100644 index 0caa7ae..0000000 --- a/internal/protocol/benchmark_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2019 Changkun Ou. All rights reserved. -// Use of this source code is governed by a MIT -// license that can be found in the LICENSE file. - -package protocol_test - -import ( - "fmt" - "os" - "strconv" - "strings" - "testing" - "unicode/utf8" - - "changkun.de/x/occamy/internal/protocol" -) - -// ParseInstructionGuac parses an instruction: 1.a,2.bc,3.def,10.abcdefghij; -func ParseInstructionGuac(raw []byte) (ins *protocol.Instruction, err error) { - var ( - cursor int - elements []string - ) - - bytes := len(raw) - for cursor < bytes { - - // 1. parse digit - lengthEnd := -1 - for i := cursor; i < bytes; i++ { - if raw[i] == '.' { - lengthEnd = i - break - } - } - if lengthEnd == -1 { // cannot find '.' - return nil, protocol.ErrInstructionMissDot - } - length, err := strconv.Atoi(string(raw[cursor:lengthEnd])) - if err != nil { - return nil, protocol.ErrInstructionBadDigit - } - - // 2. parse rune - cursor = lengthEnd + 1 - element := new(strings.Builder) - for i := 1; i <= length; i++ { - r, n := utf8.DecodeRune(raw[cursor:]) - if r == utf8.RuneError { - return nil, protocol.ErrInstructionBadRune - } - cursor += n - element.WriteRune(r) - } - elements = append(elements, element.String()) - - // 3. done - if cursor == bytes-1 { - break - } - - // 4. parse next - if raw[cursor] != ',' { - return nil, protocol.ErrInstructionMissComma - } - - cursor++ - } - - return protocol.NewInstruction(elements), nil -} - -// ParseInstructionOccamy parses an instruction: a,bc,def,abcdefghij; -func ParseInstructionOccamy(raw []byte) (ins *protocol.Instruction, err error) { - if string(raw[len(raw)-1]) != ";" { - return nil, protocol.ErrInstructionMissSemi - } - - elements := strings.Split(string(raw)[:len(raw)-1], ",") - return protocol.NewInstruction(elements), nil -} - -func BenchmarkInstructionParser(b *testing.B) { - guacamoleProtocol := [][]byte{ - []byte("4.sync,11.10574782313;"), - []byte("4.blob,1.1,8064.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;"), - } - occamyProtocol := [][]byte{ - []byte("sync,10574782313;"), - []byte("blob,1,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;"), - } - - parser := ParseInstructionGuac - ins := guacamoleProtocol - proto := os.Getenv("PROTO") - if proto == "occamy" { - parser = ParseInstructionOccamy - ins = occamyProtocol - } - - for idx := range ins { - b.Run(fmt.Sprintf("occamy-%d", idx), func(b *testing.B) { - for i := 0; i < b.N; i++ { - parser(ins[idx]) - } - }) - } -} diff --git a/internal/protocol/instruction.go b/internal/protocol/instruction.go index c2568f7..8039d5c 100644 --- a/internal/protocol/instruction.go +++ b/internal/protocol/instruction.go @@ -6,7 +6,6 @@ package protocol import ( "bufio" - "bytes" "errors" "strconv" "strings" @@ -27,12 +26,13 @@ var ( // Instruction is a guacamole instruction type Instruction struct { - elements []string // [0] is the opcode of instruction + Opcode string + Args []string } // NewInstruction creates a instruction with given elements -func NewInstruction(elements []string) *Instruction { - return &Instruction{elements} +func NewInstruction(opcode string, args ...string) *Instruction { + return &Instruction{Opcode: opcode, Args: args} } // ParseInstruction parses an instruction: 1.a,2.bc,3.def,10.abcdefghij; @@ -44,7 +44,6 @@ func ParseInstruction(raw []byte) (ins *Instruction, err error) { bytes := len(raw) for cursor < bytes { - // 1. parse digit lengthEnd := -1 for i := cursor; i < bytes; i++ { @@ -87,46 +86,27 @@ func ParseInstruction(raw []byte) (ins *Instruction, err error) { cursor++ } - return NewInstruction(elements), nil + return NewInstruction(elements[0], elements[1:]...), nil } func (i Instruction) String() string { - buffer := new(bytes.Buffer) - buffer.WriteString(strconv.FormatInt(int64(utf8.RuneCountInString(i.elements[0])), 10)) - buffer.WriteString(".") - buffer.WriteString(i.elements[0]) - for index := 1; index < len(i.elements); index++ { - buffer.WriteString(",") - buffer.WriteString(strconv.FormatInt(int64(utf8.RuneCountInString(i.elements[index])), 10)) - buffer.WriteString(".") - buffer.WriteString(i.elements[index]) + b := strings.Builder{} + b.WriteString(strconv.Itoa(len(i.Opcode))) + b.WriteString(".") + b.WriteString(i.Opcode) + for _, a := range i.Args { + b.WriteString(",") + b.WriteString(strconv.FormatInt(int64(utf8.RuneCountInString(a)), 10)) + b.WriteString(".") + b.WriteString(a) } - buffer.WriteString(";") - return buffer.String() + b.WriteString(";") + return b.String() } // Expect op code func (i Instruction) Expect(op string) bool { - if len(i.elements) == 0 { - return false - } - return i.elements[0] == op -} - -// Opcode returns the opcode of an instruction -func (i Instruction) Opcode() string { - if len(i.elements) < 1 { - return "" - } - return i.elements[0] -} - -// Args returns the arguments of an instruction -func (i Instruction) Args() []string { - if len(i.elements) < 1 { - return []string{} - } - return i.elements[1:] + return i.Opcode == op } // InstructionIO implements io.Reader and io.Writer diff --git a/internal/protocol/instruction_test.go b/internal/protocol/instruction_test.go index 29f288f..33de1a6 100644 --- a/internal/protocol/instruction_test.go +++ b/internal/protocol/instruction_test.go @@ -12,7 +12,7 @@ import ( ) func TestNewInstruction(t *testing.T) { - ins := protocol.NewInstruction([]string{"hello", "世界"}) + ins := protocol.NewInstruction("hello", "世界") want := "5.hello,2.世界;" if want != ins.String() { t.Errorf("encode instruction error, got: %s", ins.String()) @@ -22,7 +22,7 @@ func TestNewInstruction(t *testing.T) { t.FailNow() } - ins = protocol.NewInstruction([]string{"fake", ""}) + ins = protocol.NewInstruction("fake", "") want = "4.fake,0.;" if want != ins.String() { t.Errorf("encode instruction error, got: %s", ins.String()) diff --git a/internal/protocol/parser.go b/internal/protocol/parser.go deleted file mode 100644 index eb9b7a9..0000000 --- a/internal/protocol/parser.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2019 Changkun Ou. All rights reserved. -// Use of this source code is governed by a MIT -// license that can be found in the LICENSE file. - -package protocol - -import ( - "strconv" - "strings" - "unicode/utf8" -) - -const ( - // InstructionMaxLength is the maximum number of characters per - // instruction. - InstructionMaxLength = 8192 - // InstructionMaxDigits is the maximum number of digits to allow per - // length prefix. - InstructionMaxDigits = 5 - // InstructionMaxElements is the maximum number of elements per - // instruction, including the opcode. - InstructionMaxElements = 128 -) - -// ParserState is the parsing state of a parser -type ParserState int - -// All possible states of the instruction parser. -const ( - ParserStateLength ParserState = iota - ParserStateContent - ParserStateComplete - ParserStateError -) - -// Parser parses an occamy instruction -type Parser struct { - state ParserState -} - -// NewParser creates an occamy protocol parser. -func NewParser() Parser { - return Parser{} -} - -// Parse parses raw inputs into a occamy instruction -func (p Parser) Parse(raw []byte, ins *Instruction) (err error) { - cursor := 0 - bytes := len(raw) - - for cursor < bytes { - - // 1. parse digit - lengthEnd := -1 - for i := cursor; i < bytes; i++ { - if raw[i]^'.' == 0 { - lengthEnd = i - break - } - } - if lengthEnd == -1 { // cannot find '.' - return ErrInstructionMissDot - } - length, err := strconv.Atoi(string(raw[cursor:lengthEnd])) - if err != nil { - return ErrInstructionBadDigit - } - - // 2. parse rune - cursor = lengthEnd + 1 - element := new(strings.Builder) - element.Grow(length) - for i := 1; i <= length; i++ { - r, n := utf8.DecodeRune(raw[cursor:]) - if r == utf8.RuneError { - return ErrInstructionBadRune - } - cursor += n - element.WriteRune(r) - } - ins.elements = append(ins.elements, element.String()) - - // 3. done - if cursor == bytes-1 { - break - } - - // 4. parse next - if raw[cursor]^',' != 0 { - return ErrInstructionMissComma - } - - cursor++ - } - - return nil -} diff --git a/internal/protocol/parser_test.go b/internal/protocol/parser_test.go deleted file mode 100644 index 742dc20..0000000 --- a/internal/protocol/parser_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2019 Changkun Ou. All rights reserved. -// Use of this source code is governed by a MIT -// license that can be found in the LICENSE file. - -package protocol_test - -import ( - "fmt" - "testing" - - "changkun.de/x/occamy/internal/protocol" -) - -var instructions = [][]byte{ - []byte("4.sync,11.10574782313;"), - []byte("4.blob,1.1,8064.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;"), -} - -func TestParser_Parse(t *testing.T) { - for idx := range instructions { - t.Run(fmt.Sprintf("occamy-ins-len-%d", len(instructions[idx])), func(t *testing.T) { - p := protocol.NewParser() - ins := protocol.Instruction{} - p.Parse(instructions[idx], &ins) - - if ins.String() != string(instructions[idx]) { - t.Fatalf("parse instruction not success") - } - }) - } -} - -func BenchmarkParser_Parse(b *testing.B) { - - for idx := range instructions { - b.Run(fmt.Sprintf("occamy-ins-len-%d", len(instructions[idx])), func(b *testing.B) { - b.ReportAllocs() - p := protocol.NewParser() - ins := protocol.Instruction{} - b.ResetTimer() - for i := 0; i < b.N; i++ { - p.Parse(instructions[idx], &ins) - } - }) - } -} diff --git a/vendor/gioui.org/internal/ops/ops.go b/vendor/gioui.org/internal/ops/ops.go index faf16a4..fece6d6 100644 --- a/vendor/gioui.org/internal/ops/ops.go +++ b/vendor/gioui.org/internal/ops/ops.go @@ -51,6 +51,9 @@ const ( TypePointerInput TypeClipboardRead TypeClipboardWrite + TypeSource + TypeTarget + TypeOffer TypeKeyInput TypeKeyFocus TypeKeySoftKeyboard @@ -129,6 +132,9 @@ const ( TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4 TypeClipboardReadLen = 1 TypeClipboardWriteLen = 1 + TypeSourceLen = 1 + TypeTargetLen = 1 + TypeOfferLen = 1 TypeKeyInputLen = 1 + 1 TypeKeyFocusLen = 1 + 1 TypeKeySoftKeyboardLen = 1 + 1 @@ -240,6 +246,12 @@ func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte { return o.data[len(o.data)-n:] } +func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte { + o.data = append(o.data, make([]byte, n)...) + o.refs = append(o.refs, ref1, ref2, ref3) + return o.data[len(o.data)-n:] +} + func PCFor(o *Ops) PC { return PC{data: len(o.data), refs: len(o.refs)} } @@ -353,6 +365,9 @@ func (t OpType) Size() int { TypePointerInputLen, TypeClipboardReadLen, TypeClipboardWriteLen, + TypeSourceLen, + TypeTargetLen, + TypeOfferLen, TypeKeyInputLen, TypeKeyFocusLen, TypeKeySoftKeyboardLen, @@ -377,8 +392,10 @@ func (t OpType) NumRefs() int { switch t { case TypeKeyInput, TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeCursor, TypeSemanticLabel, TypeSemanticDesc: return 1 - case TypeImage: + case TypeImage, TypeSource, TypeTarget: return 2 + case TypeOffer: + return 3 default: return 0 } @@ -418,6 +435,12 @@ func (t OpType) String() string { return "ClipboardRead" case TypeClipboardWrite: return "ClipboardWrite" + case TypeSource: + return "Source" + case TypeTarget: + return "Target" + case TypeOffer: + return "Offer" case TypeKeyInput: return "KeyInput" case TypeKeyFocus: diff --git a/vendor/gioui.org/io/router/pointer.go b/vendor/gioui.org/io/router/pointer.go index 2cef1cd..a4453aa 100644 --- a/vendor/gioui.org/io/router/pointer.go +++ b/vendor/gioui.org/io/router/pointer.go @@ -4,21 +4,24 @@ package router import ( "image" + "io" "gioui.org/f32" "gioui.org/internal/ops" "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/io/semantic" + "gioui.org/io/transfer" ) type pointerQueue struct { - hitTree []hitNode - areas []areaNode - cursors []cursorNode - cursor pointer.CursorName - handlers map[event.Tag]*pointerHandler - pointers []pointerInfo + hitTree []hitNode + areas []areaNode + cursors []cursorNode + cursor pointer.CursorName + handlers map[event.Tag]*pointerHandler + pointers []pointerInfo + transfers []io.ReadCloser // pending data transfers scratch []event.Tag @@ -56,6 +59,9 @@ type pointerInfo struct { // entered tracks the tags that contain the pointer. entered []event.Tag + + dataSource event.Tag // dragging source tag + dataTarget event.Tag // dragging target tag } type pointerHandler struct { @@ -65,6 +71,11 @@ type pointerHandler struct { types pointer.Type // min and max horizontal/vertical scroll scrollRange image.Rectangle + + sourceMimes []string + targetMimes []string + offeredMime string + data io.ReadCloser } type areaOp struct { @@ -182,7 +193,7 @@ func frect(r image.Rectangle) f32.Rectangle { } } -// fpt converts an point to a f32.Point. +// fpt converts a point to a f32.Point. func fpt(p image.Point) f32.Point { return f32.Point{ X: float32(p.X), Y: float32(p.Y), @@ -217,29 +228,36 @@ func (c *pointerCollector) addHitNode(n hitNode) { c.state.nodePlusOne = len(c.q.hitTree) - 1 + 1 } -func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) { +// newHandler returns the current handler or a new one for tag. +func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *pointerHandler { areaID := c.currentArea() - area := &c.q.areas[areaID] - area.semantic.content.tag = op.Tag - if op.Types&(pointer.Press|pointer.Release) != 0 { - area.semantic.content.gestures |= ClickGesture - } - area.semantic.valid = area.semantic.content.gestures != 0 c.addHitNode(hitNode{ area: areaID, - tag: op.Tag, + tag: tag, pass: c.state.pass > 0, }) - h, ok := c.q.handlers[op.Tag] + h, ok := c.q.handlers[tag] if !ok { h = new(pointerHandler) - c.q.handlers[op.Tag] = h + c.q.handlers[tag] = h // Cancel handlers on (each) first appearance, but don't // trigger redraw. - events.AddNoRedraw(op.Tag, pointer.Event{Type: pointer.Cancel}) + events.AddNoRedraw(tag, pointer.Event{Type: pointer.Cancel}) } h.active = true h.area = areaID + return h +} + +func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) { + areaID := c.currentArea() + area := &c.q.areas[areaID] + area.semantic.content.tag = op.Tag + if op.Types&(pointer.Press|pointer.Release) != 0 { + area.semantic.content.gestures |= ClickGesture + } + area.semantic.valid = area.semantic.content.gestures != 0 + h := c.newHandler(op.Tag, events) h.wantsGrab = h.wantsGrab || op.Grab h.types = h.types | op.Types h.scrollRange = op.ScrollBounds @@ -287,6 +305,22 @@ func (c *pointerCollector) cursor(name pointer.CursorName) { }) } +func (c *pointerCollector) sourceOp(op transfer.SourceOp, events *handlerEvents) { + h := c.newHandler(op.Tag, events) + h.sourceMimes = append(h.sourceMimes, op.Type) +} + +func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents) { + h := c.newHandler(op.Tag, events) + h.targetMimes = append(h.targetMimes, op.Type) +} + +func (c *pointerCollector) offerOp(op transfer.OfferOp, events *handlerEvents) { + h := c.newHandler(op.Tag, events) + h.offeredMime = op.Type + h.data = op.Data +} + func (c *pointerCollector) reset(q *pointerQueue) { q.reset() c.resetState() @@ -448,6 +482,8 @@ func (q *pointerQueue) reset() { h.active = false h.wantsGrab = false h.types = 0 + h.sourceMimes = h.sourceMimes[:0] + h.targetMimes = h.targetMimes[:0] } q.hitTree = q.hitTree[:0] q.areas = q.areas[:0] @@ -467,6 +503,12 @@ func (q *pointerQueue) reset() { delete(q.semantic.contentIDs, k) } } + for _, rc := range q.transfers { + if rc != nil { + rc.Close() + } + } + q.transfers = nil } func (q *pointerQueue) Frame(events *handlerEvents) { @@ -498,6 +540,7 @@ func (q *pointerQueue) Frame(events *handlerEvents) { for i := range q.pointers { p := &q.pointers[i] q.deliverEnterLeaveEvents(p, events, p.last) + q.deliverTransferDataEvent(p, events) } } @@ -554,10 +597,14 @@ func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) { } q.deliverEnterLeaveEvents(p, events, e) q.deliverEvent(p, events, e) + if p.pressed { + q.deliverDragEvent(p, events) + } case pointer.Release: q.deliverEvent(p, events, e) p.pressed = false q.deliverEnterLeaveEvents(p, events, e) + q.deliverDropEvent(p, events) case pointer.Scroll: q.deliverEnterLeaveEvents(p, events, e) q.deliverScrollEvent(p, events, e) @@ -671,6 +718,87 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv p.entered = append(p.entered[:0], hits...) } +func (q *pointerQueue) deliverDragEvent(p *pointerInfo, events *handlerEvents) { + if p.dataSource != nil { + return + } + // Identify the data source. + for _, k := range p.entered { + src := q.handlers[k] + if len(src.sourceMimes) == 0 { + continue + } + // One data source handler per pointer. + p.dataSource = k + // Notify all potential targets. + for k, tgt := range q.handlers { + if _, ok := firstMimeMatch(src, tgt); ok { + events.Add(k, transfer.InitiateEvent{}) + } + } + break + } +} + +func (q *pointerQueue) deliverDropEvent(p *pointerInfo, events *handlerEvents) { + if p.dataSource == nil { + return + } + // Request data from the source. + src := q.handlers[p.dataSource] + for _, k := range p.entered { + h := q.handlers[k] + if m, ok := firstMimeMatch(src, h); ok { + p.dataTarget = k + events.Add(p.dataSource, transfer.RequestEvent{Type: m}) + return + } + } + // No valid target found, abort. + q.deliverTransferCancelEvent(p, events) +} + +func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerEvents) { + if p.dataSource == nil { + return + } + src := q.handlers[p.dataSource] + if src.data == nil { + // Data not received yet. + return + } + if p.dataTarget == nil { + q.deliverTransferCancelEvent(p, events) + return + } + // Send the offered data to the target. + transferIdx := len(q.transfers) + events.Add(p.dataTarget, transfer.DataEvent{ + Type: src.offeredMime, + Open: func() io.ReadCloser { + q.transfers[transferIdx] = nil + return src.data + }, + }) + q.transfers = append(q.transfers, src.data) + p.dataTarget = nil +} + +func (q *pointerQueue) deliverTransferCancelEvent(p *pointerInfo, events *handlerEvents) { + events.Add(p.dataSource, transfer.CancelEvent{}) + // Cancel all potential targets. + src := q.handlers[p.dataSource] + for k, h := range q.handlers { + if _, ok := firstMimeMatch(src, h); ok { + events.Add(k, transfer.CancelEvent{}) + } + } + src.offeredMime = "" + src.data = nil + p.dataSource = nil + p.dataTarget = nil +} + func searchTag(tags []event.Tag, tag event.Tag) (int, bool) { for i, t := range tags { if t == tag { @@ -690,6 +818,18 @@ func addHandler(tags []event.Tag, tag event.Tag) []event.Tag { return append(tags, tag) } +// firstMimeMatch returns the first type match between src and tgt. +func firstMimeMatch(src, tgt *pointerHandler) (first string, matched bool) { + for _, m1 := range tgt.targetMimes { + for _, m2 := range src.sourceMimes { + if m1 == m2 { + return m1, true + } + } + } + return "", false +} + func (op *areaOp) Hit(pos f32.Point) bool { pos = pos.Sub(op.rect.Min) size := op.rect.Size() diff --git a/vendor/gioui.org/io/router/router.go b/vendor/gioui.org/io/router/router.go index e6906a4..747fbe7 100644 --- a/vendor/gioui.org/io/router/router.go +++ b/vendor/gioui.org/io/router/router.go @@ -13,6 +13,7 @@ package router import ( "encoding/binary" "image" + "io" "strings" "time" @@ -24,6 +25,7 @@ import ( "gioui.org/io/pointer" "gioui.org/io/profile" "gioui.org/io/semantic" + "gioui.org/io/transfer" "gioui.org/op" ) @@ -267,6 +269,25 @@ func (q *Router) collect() { case ops.TypeCursor: name := encOp.Refs[0].(pointer.CursorName) pc.cursor(name) + case ops.TypeSource: + op := transfer.SourceOp{ + Tag: encOp.Refs[0].(event.Tag), + Type: encOp.Refs[1].(string), + } + pc.sourceOp(op, &q.handlers) + case ops.TypeTarget: + op := transfer.TargetOp{ + Tag: encOp.Refs[0].(event.Tag), + Type: encOp.Refs[1].(string), + } + pc.targetOp(op, &q.handlers) + case ops.TypeOffer: + op := transfer.OfferOp{ + Tag: encOp.Refs[0].(event.Tag), + Type: encOp.Refs[1].(string), + Data: encOp.Refs[2].(io.ReadCloser), + } + pc.offerOp(op, &q.handlers) // Key ops. case ops.TypeKeyFocus: diff --git a/vendor/gioui.org/io/semantic/semantic.go b/vendor/gioui.org/io/semantic/semantic.go new file mode 100644 index 0000000..7499fc8 --- /dev/null +++ b/vendor/gioui.org/io/semantic/semantic.go @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package semantic provides operations for semantic descriptions of a user +// interface, to facilitate presentation and interaction in external software +// such as screen readers. +// +// Semantic descriptions are organized in a tree, with clip operations as +// nodes. Operations in this package are associated with the current semantic +// node, that is the most recent pushed clip operation. +package semantic + +import ( + "gioui.org/internal/ops" + "gioui.org/op" +) + +// LabelOp provides the content of a textual component. +type LabelOp string + +// DescriptionOp describes a component. +type DescriptionOp string + +// ClassOp provides the component class. +type ClassOp int + +const ( + Unknown ClassOp = iota + Button + CheckBox + Editor + RadioButton + Switch +) + +// SelectedOp describes the selected state for components that have +// boolean state. +type SelectedOp bool + +// DisabledOp describes the disabled state. +type DisabledOp bool + +func (l LabelOp) Add(o *op.Ops) { + s := string(l) + data := ops.Write1(&o.Internal, ops.TypeSemanticLabelLen, &s) + data[0] = byte(ops.TypeSemanticLabel) +} + +func (d DescriptionOp) Add(o *op.Ops) { + s := string(d) + data := ops.Write1(&o.Internal, ops.TypeSemanticDescLen, &s) + data[0] = byte(ops.TypeSemanticDesc) +} + +func (c ClassOp) Add(o *op.Ops) { + data := ops.Write(&o.Internal, ops.TypeSemanticClassLen) + data[0] = byte(ops.TypeSemanticClass) + data[1] = byte(c) +} + +func (s SelectedOp) Add(o *op.Ops) { + data := ops.Write(&o.Internal, ops.TypeSemanticSelectedLen) + data[0] = byte(ops.TypeSemanticSelected) + if s { + data[1] = 1 + } +} + +func (d DisabledOp) Add(o *op.Ops) { + data := ops.Write(&o.Internal, ops.TypeSemanticDisabledLen) + data[0] = byte(ops.TypeSemanticDisabled) + if d { + data[1] = 1 + } +} + +func (c ClassOp) String() string { + switch c { + case Unknown: + return "Unknown" + case Button: + return "Button" + case CheckBox: + return "CheckBox" + case Editor: + return "Editor" + case RadioButton: + return "RadioButton" + case Switch: + return "Switch" + default: + panic("invalid ClassOp") + } +} diff --git a/vendor/gioui.org/io/transfer/transfer.go b/vendor/gioui.org/io/transfer/transfer.go new file mode 100644 index 0000000..5870348 --- /dev/null +++ b/vendor/gioui.org/io/transfer/transfer.go @@ -0,0 +1,109 @@ +// Package transfer contains operations and events for brokering data transfers. +// +// The transfer protocol is as follows: +// +// - Data sources are registered with SourceOps, data targets with TargetOps. +// - A data source receives a RequestEvent when a transfer is initiated. +// It must respond with an OfferOp. +// - The target receives a DataEvent when transferring to it. It must close +// the event data after use. +// +// When a user initiates a pointer-guided drag and drop transfer, the +// source as well as all potential targets receive an InitiateEvent. +// Potential targets are targets with at least one MIME type in common +// with the source. When a drag gesture completes, a CancelEvent is sent +// to the source and all potential targets. +// +// Note that the RequestEvent is sent to the source upon drop. +package transfer + +import ( + "io" + + "gioui.org/internal/ops" + "gioui.org/io/event" + "gioui.org/op" +) + +// SourceOp registers a tag as a data source for a MIME type. +// Use multiple SourceOps if a tag supports multiple types. +type SourceOp struct { + Tag event.Tag + // Type is the MIME type supported by this source. + Type string +} + +// TargetOp registers a tag as a data target. +// Use multiple TargetOps if a tag supports multiple types. +type TargetOp struct { + Tag event.Tag + // Type is the MIME type accepted by this target. + Type string +} + +// OfferOp is used by data sources as a response to a RequestEvent. +type OfferOp struct { + Tag event.Tag + // Type is the MIME type of Data. + // It must be the Type from the corresponding RequestEvent. + Type string + // Data contains the offered data. It is closed when the + // transfer is complete or cancelled. + // Data must be kept valid until closed, and it may be used from + // a goroutine separate from the one processing the frame.. + Data io.ReadCloser +} + +func (op SourceOp) Add(o *op.Ops) { + data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type) + data[0] = byte(ops.TypeSource) +} + +func (op TargetOp) Add(o *op.Ops) { + data := ops.Write2(&o.Internal, ops.TypeTargetLen, op.Tag, op.Type) + data[0] = byte(ops.TypeTarget) +} + +// Add the offer to the list of operations. +// It panics if the Data field is not set. +func (op OfferOp) Add(o *op.Ops) { + if op.Data == nil { + panic("invalid nil data in OfferOp") + } + data := ops.Write3(&o.Internal, ops.TypeOfferLen, op.Tag, op.Type, op.Data) + data[0] = byte(ops.TypeOffer) +} + +// RequestEvent requests data from a data source. The source must +// respond with an OfferOp. +type RequestEvent struct { + // Type is the first matched type between the source and the target. + Type string +} + +func (RequestEvent) ImplementsEvent() {} + +// InitiateEvent is sent to a data source when a drag-and-drop +// transfer gesture is initiated. +// +// Potential data targets also receive the event. +type InitiateEvent struct{} + +func (InitiateEvent) ImplementsEvent() {} + +// CancelEvent is sent to data sources and targets to cancel the +// effects of an InitiateEvent. +type CancelEvent struct{} + +func (CancelEvent) ImplementsEvent() {} + +// DataEvent is sent to the target receiving the transfer. +type DataEvent struct { + // Type is the MIME type of Data. + Type string + // Open returns the transfer data. It is only valid to call Open in the frame + // the DataEvent is received. The caller must close the return value after use. + Open func() io.ReadCloser +} + +func (DataEvent) ImplementsEvent() {} diff --git a/vendor/gioui.org/layout/list.go b/vendor/gioui.org/layout/list.go index ea68c57..aa78f1b 100644 --- a/vendor/gioui.org/layout/list.go +++ b/vendor/gioui.org/layout/list.go @@ -293,6 +293,11 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { if pos > mainMax { pos = mainMax } + if crossMin, crossMax := l.Axis.crossConstraint(l.cs); maxCross < crossMin { + maxCross = crossMin + } else if maxCross > crossMax { + maxCross = crossMax + } dims := l.Axis.Convert(image.Pt(pos, maxCross)) call := macro.Stop() defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop() diff --git a/vendor/modules.txt b/vendor/modules.txt index e4c9160..7ac3b9b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# gioui.org v0.0.0-20211202105001-872b4ba41be0 +# gioui.org v0.0.0-20211207114553-03016f0c69b7 ## explicit gioui.org/app gioui.org/app/internal/log @@ -31,6 +31,7 @@ gioui.org/io/profile gioui.org/io/router gioui.org/io/semantic gioui.org/io/system +gioui.org/io/transfer gioui.org/layout gioui.org/op gioui.org/op/clip