all goroutines are dead asleep - deadlock! wasm docs
otaxhu opened this issue · 8 comments
I'm trying to do the next:
Client Code:
//go:build js && wasm
package main
import (
"context"
"log"
"syscall/js"
"nhooyr.io/websocket"
)
func main() {
ctx := context.Background()
window := js.Global()
window.Set("goWS", js.FuncOf(func(_ js.Value, args []js.Value) any {
conn, _, err := websocket.Dial(ctx, args[0].String(), nil)
if err != nil {
log.Fatal(err)
}
goWSObject := window.Get("goWS")
goWSObject.Set("send", js.FuncOf(func(_ js.Value, args []js.Value) any {
if err := conn.Write(ctx, websocket.MessageText, []byte(args[0].String())); err != nil {
log.Fatal(err)
}
return nil
}))
goWSObject.Set("waitMessage", js.FuncOf(func(_ js.Value, _ []js.Value) any {
_, bb, err := conn.Read(ctx)
if err != nil {
log.Fatal(err)
}
return bb
}))
return nil
}))
waitCh := make(chan struct{})
<-waitCh
}index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>goWS</title>
<script src="/assets/wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("/assets/app.wasm"), go.importObject)
.then(res => go.run(res.instance))
</script>
</head>
<body>
</body>
</html>Server code:
package main
import (
"context"
"net/http"
"nhooyr.io/websocket"
)
func main() {
ctx := context.Background()
http.HandleFunc("/ws-chat", func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, bb, err := conn.Read(ctx)
if err != nil {
conn.Close(websocket.StatusInternalError, err.Error())
return
}
wr, err := conn.Writer(ctx, websocket.MessageText)
if err != nil {
conn.Close(websocket.StatusInternalError, err.Error())
return
}
wr.Write(bb)
wr.Close()
conn.CloseNow()
})
http.ListenAndServe(":8080", http.DefaultServeMux)
}Also I'm going to attach my Makefile so you can see the commands when I'm compiling:
build_client:
GOOS=js GOARCH=wasm go build -o dist/assets/app.wasm cmd/go-ws-client/main.go
cp $(go env GOROOT)/misc/wasm/wasm_exec.js dist/assets/
build_server:
go build -o dist/bin/app-server cmd/go-ws-server/main.goStructure of dist folder:
dist/
- assets/
- app.wasm
- wasm_exec.js
- bin/
- app-server
- index.html
But i'm getting the following errors after calling window.goWS("ws://localhost:8080/ws-chat") from the firefox browser's console:
Console:
> window.goWS("ws://localhost:8080/ws-chat")
fatal error: all goroutines are asleep - deadlock! wasm_exec.js:22:14
<empty string>
wasm_exec.js:22:14
goroutine 1 [chan receive]: wasm_exec.js:22:14
main.main() wasm_exec.js:22:14
/home/oscar/Escritorio/proyecto-react/cmd/go-ws-client/main.go:47 +0xc wasm_exec.js:22:14
<empty string> wasm_exec.js:22:14
goroutine 7 [select]: wasm_exec.js:22:14
nhooyr.io/websocket.dial({0x5aa80, 0x1430050}, {0x1466020, 0x1b}, 0x0) wasm_exec.js:22:14
/home/oscar/go/pkg/mod/nhooyr.io/websocket@v1.8.10/ws_js.go:320 +0x1c wasm_exec.js:22:14
nhooyr.io/websocket.Dial({0x5aa80, 0x1430050}, {0x1466020, 0x1b}, 0x0) wasm_exec.js:22:14
/home/oscar/go/pkg/mod/nhooyr.io/websocket@v1.8.10/ws_js.go:289 +0x2 wasm_exec.js:22:14
main.main.func1.1({{}, 0x0, 0x0}, {0x140e0f0, 0x1, 0x1}) wasm_exec.js:22:14
/home/oscar/Escritorio/proyecto-react/cmd/go-ws-client/main.go:19 +0x4 wasm_exec.js:22:14
syscall/js.handleEvent() wasm_exec.js:22:14
/usr/local/go/src/syscall/js/func.go:100 +0x26 wasm_exec.js:22:14
exit code: 2 wasm_exec.js:101:14
undefined
Unrelated to this library, see golang/go#41310
I don't know if you are too busy af but I found the answer and I don't know if you have the time to create an example of a ping pong websocket client compiled in WASM.
So what I did was to require a callback, passing the message as an argument, the reason is because of async nature of (*Conn) Read() method, minimal reproduction following:
//go:build js && wasm
package main
import (
"context"
"syscall/js"
"nhooyr.io/websocket"
)
func main() {
ctx := context.Background()
conn, _, err := websocket.Dial(ctx, "ws://localhost:8080/ping-pong", nil)
if err != nil {
panic(err)
}
wr, err := conn.Writer(ctx, websocket.MessageText)
if err != nil {
panic(err)
}
js.Global().Set("sendMessageGo", js.FuncOf(func(_ js.Value, args []js.Value) any {
// args[0] is the string that want to sent to the server
wr.Write([]byte(args[0].String()))
wr.Close()
// spawning a goroutine because of conn.Read() is async, and js functions cannot wait
go func() {
_, bb, err := conn.Read(ctx)
if err != nil {
panic(err)
}
// args[1] is the callback, has the shape (bytes) => {/* any operation with the bytes */}
// passing to the callback the bytes in string format
args[1].Invoke(string(bb))
}()
// the js functions must return inmediatly, cannot wait for async operations
return nil
}))
waitCh := make(chan struct{})
<-waitCh
}Hmm weird. I have this test here which runs in WASM and requires no extra goroutines. https://github.com/nhooyr/websocket/blob/master/ws_js_test.go
I'm def a little busy to look into exactly what's going on in your example. I'll open this up again and look into it later.
don't worry, I saw in the tests that you are not testing the (*Conn) .Read() method, it's only dialing to a ws server, no reading
No it is, see this line https://github.com/nhooyr/websocket/blob/e3a2d32f704fb06c439e56d2a85334de04b50d32/ws_js_test.go#L32
It writes a message and then confirms the same message is read back.
forget it, you are in fact reading in the wstest.Echo, but I think it has something to do when you are binding the Go function with the JS function
I tried this other example and also worked, with Promise object instead of callback for compatibility with async and await js keywords:
//go:build js && wasm
package main
import (
"context"
"syscall/js"
"nhooyr.io/websocket"
)
func main() {
ctx := context.Background()
conn, _, err := websocket.Dial(ctx, "ws://localhost:8080/ws-chat", nil)
if err != nil {
panic(err)
}
js.Global().Set("sendMessageGo", js.FuncOf(func(_ js.Value, args []js.Value) any {
wr, err := conn.Writer(ctx, websocket.MessageText)
if err != nil {
panic(err)
}
wr.Write([]byte(args[0].String()))
wr.Close()
// returning a Promise
return js.Global().Get("Promise").New(js.FuncOf(func(_ js.Value, args []js.Value) any {
// args[0] is resolve callback
// args[1] is reject callback
// spawining a goroutine because of conn.Read() blocking nature
go func() {
_, bb, err := conn.Read(ctx)
if err != nil {
// if there is an error, reject the promise, calling the reject callback
args[1].Invoke(err.Error())
return
}
// if there is no error then pass to resolve the bytes that comes from the server
args[0].Invoke(string(bb))
}()
// also the constructor must return inmediatly, cannot wait for async operations
return nil
}))
}))
waitCh := make(chan struct{})
<-waitCh
}Ah yes I see, you probably can't block in a JS callback so you have to return a promise. That makes sense. We can document it for sure. See also https://www.reddit.com/r/WebAssembly/comments/nm69e8/blocking_calls_in_wasm/