nhooyr / websocket

Minimal and idiomatic WebSocket library for Go
ISC License
3.62k stars 276 forks source link

all goroutines are dead asleep - deadlock! wasm docs #426

Open otaxhu opened 8 months ago

otaxhu commented 8 months ago

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.go

Structure 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
nhooyr commented 8 months ago

Unrelated to this library, see https://github.com/golang/go/issues/41310

otaxhu commented 8 months ago

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
}
nhooyr commented 8 months ago

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.

otaxhu commented 8 months ago

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

nhooyr commented 8 months ago

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.

otaxhu commented 8 months ago

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

otaxhu commented 8 months ago

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
}
nhooyr commented 5 months ago

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/