Experimental support for running Senpai on the web, with GOOS=js GOARCH=wasm + xterm.js Give it a try! You'll need a binary IRCv3 websocket that accepts connections from your origin. You can put it in the hash of the URL to pass it to Senpai on startup. e.g. `python -m http.server` -> http://localhost:8000/#wss://example.net/some/socket The binary websocket limitation makes it tricky to use soju, which only offers text. The app doesn't seem to receive messages when the socket is used in text mode.. A demo is currently running at https://cyberspace.baby/senpai. If you give me a bell in #senpai I'll pass you a config block to use a real soju session on cyberspace.baby.. currently going through a binary websocket proxy. mooff (2): Make history prefetch limit configurable Add support for running on the web, connecting to websockets Makefile | 5 + app.go | 22 +- cmd/senpai/main.go | 7 +- cmd/senpai/signals_js.go | 5 + cmd/senpai/signals_x.go | 21 ++ config.go | 17 ++ go.mod | 7 +- go.sum | 67 ++++- web/.gitignore | 3 + web/index.html | 261 ++++++++++++++++ web/package-lock.json | 39 +++ web/package.json | 6 + web/wasm_exec.js | 636 +++++++++++++++++++++++++++++++++++++++ 13 files changed, 1072 insertions(+), 24 deletions(-) create mode 100644 cmd/senpai/signals_js.go create mode 100644 cmd/senpai/signals_x.go create mode 100644 web/.gitignore create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/wasm_exec.js -- 2.32.0
Time to deprecate gamja i guess? Jokes aside, nice project, this looks cool (and it works? it seems so) I think you should set this up in a new repo with a Makefile and the HTML/JS boilerplate. I don't have a lot of ideas about the text vs binary websocket issue.. What happens when you don't negotiate any sub-protocols? It might also be possible for soju to accept the binary sub-protocol.
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~taiite/public-inbox/patches/26926/mbox | git am -3Learn more about email & git
From: mooff <mooff@awful.cooking> history { prefetch-limit 500 } --- app.go | 2 +- config.go | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index f50f8bd..52397b1 100644 --- a/app.go +++ b/app.go @@ -615,7 +615,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) { s.Join(channel, "") } s.NewHistoryRequest(""). - WithLimit(1000). + WithLimit(app.cfg.HistoryPrefetch). Targets(app.lastCloseTime, msg.TimeOrNow()) body := "Connected to the server" if s.Nick() != app.cfg.Nick { diff --git a/config.go b/config.go index d232cf4..a0d2d2f 100644 --- a/config.go +++ b/config.go @@ -70,6 +70,8 @@ type Config struct { Colors ConfigColors + HistoryPrefetch int + Debug bool } @@ -100,6 +102,7 @@ func Defaults() (cfg Config, err error) { Colors: ConfigColors{ Prompt: Color(tcell.ColorDefault), }, + HistoryPrefetch: 1000, Debug: false, } @@ -191,6 +194,20 @@ func unmarshal(filename string, cfg *Config) (err error) { if err := d.ParseParams(&cfg.OnHighlightPath); err != nil { return err } + case "history": + for _, child := range d.Children { + switch child.Name { + case "prefetch-limit": + var prefetch string + if err := child.ParseParams(&prefetch); err != nil { + return err + } + + if cfg.HistoryPrefetch, err = strconv.Atoi(prefetch); err != nil { + return err + } + } + } case "pane-widths": for _, child := range d.Children { switch child.Name { -- 2.32.0
From: mooff <mooff@awful.cooking> --- Makefile | 5 + app.go | 20 +- cmd/senpai/main.go | 7 +- cmd/senpai/signals_js.go | 5 + cmd/senpai/signals_x.go | 21 ++ go.mod | 7 +- go.sum | 67 ++++- web/.gitignore | 3 + web/index.html | 261 ++++++++++++++++ web/package-lock.json | 39 +++ web/package.json | 6 + web/wasm_exec.js | 636 +++++++++++++++++++++++++++++++++++++++ 12 files changed, 1054 insertions(+), 23 deletions(-) create mode 100644 cmd/senpai/signals_js.go create mode 100644 cmd/senpai/signals_x.go create mode 100644 web/.gitignore create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/wasm_exec.js diff --git a/Makefile b/Makefile index da150ec..0b97630 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,11 @@ doc/senpai.1: doc/senpai.1.scd doc/senpai.5: doc/senpai.5.scd $(SCDOC) < doc/senpai.5.scd > doc/senpai.5 +web: js + cd web && npm install --silent +js: + GOOS=js GOARCH=wasm $(GO) build -o web/senpai.wasm $(GOFLAGS) ./cmd/senpai + clean: $(RM) -rf senpai doc/senpai.1 doc/senpai.5 install: all diff --git a/app.go b/app.go index 52397b1..c7777bb 100644 --- a/app.go +++ b/app.go @@ -1,6 +1,7 @@ package senpai import ( + "context" "crypto/tls" "errors" "fmt" @@ -14,6 +15,7 @@ import ( "git.sr.ht/~taiite/senpai/irc" "git.sr.ht/~taiite/senpai/ui" "github.com/gdamore/tcell/v2" + "nhooyr.io/websocket" ) const eventChanSize = 64 @@ -311,8 +313,17 @@ func (app *App) connect(netID string) net.Conn { } } -func (app *App) tryConnect() (conn net.Conn, err error) { +func (app *App) tryConnect() (net.Conn, error) { addr := app.cfg.Addr + if strings.HasPrefix(addr, "wss://") || strings.HasPrefix(addr, "ws://") { + return app.tryConnectWebSocket() + } + return app.tryConnectTCP() +} + +func (app *App) tryConnectTCP() (conn net.Conn, err error) { + addr := app.cfg.Addr + colonIdx := strings.LastIndexByte(addr, ':') bracketIdx := strings.LastIndexByte(addr, ']') if colonIdx <= bracketIdx { @@ -346,6 +357,13 @@ func (app *App) tryConnect() (conn net.Conn, err error) { return } +func (app *App) tryConnectWebSocket() (net.Conn, error) { + conn, _, err := websocket.Dial(context.TODO(), app.cfg.Addr, &websocket.DialOptions{ + Subprotocols: []string{"binary.ircv3.net", "binary"},
what happens if you remove this line (thus don't negotiate any sub-protocol)?
+ }) + return websocket.NetConn(context.TODO(), conn, websocket.MessageBinary), err
MessageBinary should also be changed to MessageText then
+} + func (app *App) debugOutputMessages(netID string, out chan<- irc.Message) chan<- irc.Message { debugOut := make(chan irc.Message, cap(out)) go func() { diff --git a/cmd/senpai/main.go b/cmd/senpai/main.go index 665cacc..92d4c86 100644 --- a/cmd/senpai/main.go +++ b/cmd/senpai/main.go @@ -6,10 +6,8 @@ import ( "io/ioutil" "math/rand" "os" - "os/signal" "path" "strings" - "syscall" "time" "git.sr.ht/~taiite/senpai" @@ -54,11 +52,8 @@ func main() { app.SwitchToBuffer(lastNetID, lastBuffer) app.SetLastClose(getLastStamp()) - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - go func() { - <-sigCh + <-closeSignal() app.Close() }() diff --git a/cmd/senpai/signals_js.go b/cmd/senpai/signals_js.go new file mode 100644 index 0000000..509b20f --- /dev/null +++ b/cmd/senpai/signals_js.go @@ -0,0 +1,5 @@ +package main + +func closeSignal() chan struct{} { + return make(chan struct{}) // never fires +} diff --git a/cmd/senpai/signals_x.go b/cmd/senpai/signals_x.go new file mode 100644 index 0000000..3b88aff --- /dev/null +++ b/cmd/senpai/signals_x.go @@ -0,0 +1,21 @@ +// +build !js + +package main + +import ( + "os" + "os/signal" + "syscall" +) + +func closeSignal() chan struct{} { + osClose := make(chan os.Signal, 1) + signal.Notify(osClose, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + + genericClose := make(chan struct{}, 1) + go func() { + <-osClose + genericClose <- struct{}{} + }()
pro-tip: have closeSignal() return a chan interface{} instead, so this extra goroutine is not needed.
+ return genericClose +} diff --git a/go.mod b/go.mod index d3c9b55..d0d120a 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,13 @@ go 1.16 require ( git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc github.com/gdamore/tcell/v2 v2.3.11 - github.com/mattn/go-runewidth v0.0.10 + github.com/mattn/go-runewidth v0.0.13 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 mvdan.cc/xurls/v2 v2.3.0 + nhooyr.io/websocket v1.8.7 ) -replace github.com/gdamore/tcell/v2 => github.com/hhirtz/tcell/v2 v2.3.12-0.20210807133752-5d743c3ab0c9 +replace github.com/gdamore/tcell/v2 => github.com/awfulcooking/tcell/v2 v2.4.1-0.20211128170204-5ebcb5571e5d + +replace golang.org/x/term => github.com/awfulcooking/term v0.0.0-20211128155416-2652f7c0d88b diff --git a/go.sum b/go.sum index 856f865..bce6b18 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,72 @@ git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc h1:51BD67xFX+bozd3ZRuOUfalrhx4/nQSh6A9lI08rYOk= git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc/go.mod h1:t+Ww6SR24yYnXzEWiNlOY0AFo5E9B73X++10lrSpp4U= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/awfulcooking/tcell/v2 v2.4.1-0.20211128170204-5ebcb5571e5d h1:GGONl5jDMT0wtMWEEu4sH67JkaMCYqTcyW2Xha9uy/o= +github.com/awfulcooking/tcell/v2 v2.4.1-0.20211128170204-5ebcb5571e5d/go.mod h1:X4k/PfBso5eCwoulBKD17GlepBi4ecMbxPM1O1EG2is= +github.com/awfulcooking/term v0.0.0-20211128155416-2652f7c0d88b h1:xzEg9LjSS8aku4qx07hjNn8YxdPrKjOwaxouhImrOlc= +github.com/awfulcooking/term v0.0.0-20211128155416-2652f7c0d88b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/hhirtz/tcell/v2 v2.3.12-0.20210807133752-5d743c3ab0c9 h1:YE0ZsDHfDGR0MeB6YLSGW8tjoxOXZKX3XbB0ytGDX4M= -github.com/hhirtz/tcell/v2 v2.3.12-0.20210807133752-5d743c3ab0c9/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= -github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= -golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 h1:7NCfEGl0sfUojmX78nK9pBJuUlSZWEJA/TwASvfiPLo= +golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs= golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= mvdan.cc/xurls/v2 v2.3.0 h1:59Olnbt67UKpxF1EwVBopJvkSUBmgtb468E4GVWIZ1I= mvdan.cc/xurls/v2 v2.3.0/go.mod h1:AjuTy7gEiUArFMjgBBDU4SMxlfUYsRokpJQgNWOt3e4= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..1800848 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,3 @@ +*.wasm +node_modules/ +upload.sh diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..9663b6e --- /dev/null +++ b/web/index.html @@ -0,0 +1,261 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Senpai</title> + + <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" /> + <script src="node_modules/xterm/lib/xterm.js"></script> + <script src="node_modules/xterm-addon-fit/lib/xterm-addon-fit.js"></script> +</head> + +<body> + <style> + * { box-sizing: border-box; padding: 0px; margin: 0px; } + + :root, body, #terminal { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + overflow: hidden; + + background: #000; + color: #eeeeee; + } + </style> + + <div id="terminal"></div> + + <script src="wasm_exec.js"></script> + <script> + const fallbackName = randUsername(); + const fallbackSocket = location.hash.slice(1) || `wss://${location.host}/bounce`; + + const senpaiConfig = sessionStorage['senpaiConfig'] || localStorage['senpaiConfig'] || ` + address ${fallbackSocket} + nickname ${fallbackName} + pane-widths { + channels 17 + } + history { + prefetch-limit 30 + } + `; + + function randUsername() { + const names = [ + "alice", "bob", "jane", "visual", "studio", "code", "jaffa", + "web", "cyber", "develop", "tools", "mozilla", "firefox", + "happy", "juice", "cola", "shake", "tree", "sun", "radiant", + "tmux", "git", "sunshine", "duck", "cat", "dog", "fish", "penguin", + ]; + return names[Math.floor(Math.random()*names.length)]; + } + </script> + <script> + const goStubFS = globalThis.fs; // defined in wasm_exec.js + + const configFD = 42; // used for /config/senpai/senpai.scfg + const termFD = 69; // used for /dev/tty + + const fds = {}; + + let nextFD = 100; + + const files = { + "/config.scfg": senpaiConfig, + }; + + const senpaiFS = { + open(path, flags, mode, callback) { + console.info('fs open', path, flags, mode, callback); + if (path == '/dev/tty') + return callback(null, termFD); + else if (!(path in files)) + return callback({code: "ENOENT"}); + + const fd = nextFD++; + fds[fd] = {path, flags, mode, position: 0}; + + callback(null, fd); + }, + async read(fd, buffer, offset, length, position, callback) { + let numBytes = 0; + + if (fd === termFD) { // read any input from xterm.js + await waitReadAvailable(); + + numBytes = Math.min(readBuffer.length, position+length); + buffer.set(readBuffer.slice(position, numBytes), offset); + + resetReadBuffer(); + return callback(null, numBytes, buffer); + } + + const desc = fds[fd]; + if (position != null) + desc.position = position; + + const data = new TextEncoder().encode(files[desc.path]); + + numBytes = length; + if (desc.position + numBytes > data.length) + numBytes = Math.max(0, data.length - desc.position); + + buffer.set(data.slice(desc.position, numBytes), offset); + desc.position += numBytes; + + callback(null, numBytes, buffer); + }, + writeSync(fd, buf) { + if (fd == 1 || fd == 2) { // stdout, stderr + return goStubFS.writeSync(fd, buf); + } else if (fd == termFD) { // for xterm.js tty + term.write(buf); + } else { + console.error("Write to fd", "|", fd, buf); + } + return buf.length; + }, + mkdir: (path, perm, callback) => callback(null), + }; + + globalThis.fs = Object.assign({}, goStubFS, senpaiFS); + </script> + + <script> + globalThis["golang.org/x/term"] = { + isTerminal: (fd) => true, + getSize: (fd) => ({width: term.cols, height: term.rows}), + makeRaw: (fd) => false, + getState: (fd) => {}, + restore: (fd, state) => null, + }; + + // look at numSizeRequests in console to see how often it happens + let numSizeRequests = 0; + globalThis["golang.org/x/term"].getSize = () => { + numSizeRequests++; + return {width: term.cols, height: term.rows}; + } + </script> + + <script> + const baseTheme = { + foreground: '#F8F8F8', + background: '#2D2E2C', + selection: '#5DA5D533', + black: '#1E1E1D', + brightBlack: '#262625', + red: '#CE5C5C', + brightRed: '#FF7272', + green: '#5BCC5B', + brightGreen: '#72FF72', + yellow: '#CCCC5B', + brightYellow: '#FFFF72', + blue: '#5D5DD3', + brightBlue: '#7279FF', + magenta: '#BC5ED1', + brightMagenta: '#E572FF', + cyan: '#5DA5D5', + brightCyan: '#72F0FF', + white: '#F8F8F8', + brightWhite: '#FFFFFF' + }; + + const snazzyTheme = { + foreground: '#eff0eb', + background: '#282a36', + selection: '#97979b33', + black: '#282a36', + brightBlack: '#686868', + red: '#ff5c57', + brightRed: '#ff5c57', + green: '#5af78e', + brightGreen: '#5af78e', + yellow: '#f3f99d', + brightYellow: '#f3f99d', + blue: '#57c7ff', + brightBlue: '#57c7ff', + magenta: '#ff6ac1', + brightMagenta: '#ff6ac1', + cyan: '#9aedfe', + brightCyan: '#9aedfe', + white: '#f1f1f0', + brightWhite: '#eff0eb' + }; + + const theme = null; + </script> + + <script> + const term = new Terminal({ theme, scrollback: 0 }); + const fitAddon = new FitAddon.FitAddon(); + + term.loadAddon(fitAddon); + + function fit() { + fitAddon.fit(); + // todo: notify senpai, make tcell ask terminal less often + } + + window.addEventListener('resize', fit); + setTimeout(fit, 0); + + term.open(document.querySelector('#terminal')); + term.focus(); + + let readBuffer = new Uint8Array; + let _readAvailable; + + function waitReadAvailable() { + if (readBuffer.length) + return Promise.resolve(); + + return new Promise(resolve => _readAvailable = resolve); + } + + term.onData(data => { + const len = data.length; + const bytes = new Uint8Array(len); + + for (let i=0; i<len; ++i) + bytes[i] = data.charCodeAt(i) & 255; + + readBuffer = new Uint8Array([...readBuffer, ...bytes]); + _readAvailable?.(); + }); + + term.onBinary(bytes => { + readBuffer = new Uint8Array([...readBuffer, ...bytes]); + _readAvailable?.(); + }); + + function resetReadBuffer() { + readBuffer = new Uint8Array; + } + </script> + + <script> + (async () => { + const blob = await fetch('senpai.wasm'); + + globalThis.go = new Go(); + go.argv = ["senpai", "-config", "/config.scfg"]; + go.env.TERM = "xterm-256color"; + go.env.XDG_CACHE_HOME = "/cache"; + go.env.LC_ALL = "UTF-8"; + + const {module, instance} = await WebAssembly.instantiateStreaming(blob, go.importObject); + + globalThis.senpai = module; + globalThis.instance = instance; + + go.run(instance); + })(); + </script> +</body> + +</html> diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..bc87443 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,39 @@ +{ + "name": "web", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "xterm": "^4.15.0", + "xterm-addon-fit": "^0.5.0" + } + }, + "node_modules/xterm": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.15.0.tgz", + "integrity": "sha512-Ik1GoSq1yqKZQ2LF37RPS01kX9t4TP8gpamUYblD09yvWX5mEYuMK4CcqH6+plgiNEZduhTz/UrcaWs97gOlOw==" + }, + "node_modules/xterm-addon-fit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", + "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==", + "peerDependencies": { + "xterm": "^4.0.0" + } + } + }, + "dependencies": { + "xterm": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.15.0.tgz", + "integrity": "sha512-Ik1GoSq1yqKZQ2LF37RPS01kX9t4TP8gpamUYblD09yvWX5mEYuMK4CcqH6+plgiNEZduhTz/UrcaWs97gOlOw==" + }, + "xterm-addon-fit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", + "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==", + "requires": {} + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..d4b845e --- /dev/null +++ b/web/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "xterm": "^4.15.0", + "xterm-addon-fit": "^0.5.0" + } +} diff --git a/web/wasm_exec.js b/web/wasm_exec.js new file mode 100644 index 0000000..5651141 --- /dev/null +++ b/web/wasm_exec.js @@ -0,0 +1,636 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +(() => { + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + // - Webpack + + if (typeof global !== "undefined") { + // global already exists + } else if (typeof window !== "undefined") { + window.global = window; + } else if (typeof self !== "undefined") { + self.global = self; + } else { + throw new Error("cannot export Go (neither global, window nor self is defined)"); + } + + if (!global.require && typeof require !== "undefined") { + global.require = require; + } + + if (!global.fs && global.require) { + const fs = require("fs"); + if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) { + global.fs = fs; + } + } + + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!global.fs) { + let outputBuf = ""; + global.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substr(0, nl)); + outputBuf = outputBuf.substr(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!global.process) { + global.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!global.crypto && global.require) { + const nodeCrypto = require("crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + } + if (!global.crypto) { + throw new Error("global.crypto is not available, polyfill required (getRandomValues only)"); + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + } + + if (!global.TextEncoder && global.require) { + global.TextEncoder = require("util").TextEncoder; + } + if (!global.TextEncoder) { + throw new Error("global.TextEncoder is not available, polyfill required"); + } + + if (!global.TextDecoder && global.require) { + global.TextDecoder = require("util").TextDecoder; + } + if (!global.TextDecoder) { + throw new Error("global.TextDecoder is not available, polyfill required"); + } + + // End of polyfills for common API. + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + global.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + go: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + global, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [global, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } + + if ( + typeof module !== "undefined" && + global.require && + global.require.main === module && + global.process && + global.process.versions && + !global.process.versions.electron + ) { + if (process.argv.length < 3) { + console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); + process.exit(1); + } + + const go = new Go(); + go.argv = process.argv.slice(2); + go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); + go.exit = process.exit; + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { + process.on("exit", (code) => { // Node.js exits if no event handler is pending + if (code === 0 && !go.exited) { + // deadlock, make Go print error and stack traces + go._pendingEvent = { id: 0 }; + go._resume(); + } + }); + return go.run(result.instance); + }).catch((err) => { + console.error(err); + process.exit(1); + }); + } +})(); -- 2.32.0
Time to deprecate gamja i guess? Jokes aside, nice project, this looks cool (and it works? it seems so) I think you should set this up in a new repo with a Makefile and the HTML/JS boilerplate. I don't have a lot of ideas about the text vs binary websocket issue.. What happens when you don't negotiate any sub-protocols? It might also be possible for soju to accept the binary sub-protocol.