Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Gio client #51

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions cmd/occamy-gui/README.md
Original file line number Diff line number Diff line change
@@ -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.
114 changes: 114 additions & 0 deletions cmd/occamy-gui/occamy.go
Original file line number Diff line number Diff line change
@@ -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 <vnc|rdp> 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)
}
51 changes: 3 additions & 48 deletions server/connection.go → cmd/occamyd/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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() {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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))
}
}
4 changes: 1 addition & 3 deletions occamy.go → cmd/occamyd/occamyd.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,4 @@

package main

import "changkun.de/x/occamy/server"

func main() { server.Run() }
func main() { Run() }
30 changes: 15 additions & 15 deletions server/routers.go → cmd/occamyd/routers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
26 changes: 12 additions & 14 deletions server/session.go → cmd/occamyd/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)
Expand All @@ -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)
}
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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
}
Expand Down
Loading