From afd60048aa8bd12db5763194ff2abaef9268ac41 Mon Sep 17 00:00:00 2001
From: Cheng Shao <terrorjack@type.dance>
Date: Wed, 19 Mar 2025 17:17:13 +0000
Subject: [PATCH] wasm: support wasi console redirect for the ghci browser mode

This commit adds optional support for redirecting wasi console
stdout/stderr back to the host when running wasm ghci browser mode. By
default, the wasi console outputs are only available under F12
devtools console, but in case of testing against a mobile browser, the
devtools console may not be readily available, and it would be more
convenient to at least get wasi console output on the host side.

The redirection logic is simple, just adding another two WebSockets
connections that pump the line-buffered textual messages back to
host.

(cherry picked from commit ad7e271dee90e8bf17560baec942df90becfb846)
---
 utils/jsffi/dyld.mjs | 71 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 61 insertions(+), 10 deletions(-)

diff --git a/utils/jsffi/dyld.mjs b/utils/jsffi/dyld.mjs
index e84928c4031..e068f54239a 100755
--- a/utils/jsffi/dyld.mjs
+++ b/utils/jsffi/dyld.mjs
@@ -370,8 +370,11 @@ export class DyLDRPC {
   #origin;
   #wsPipe;
   #wsSig;
+  #redirectWasiConsole;
+  #wsStdout;
+  #wsStderr;
 
-  constructor({ origin }) {
+  constructor({ origin, redirectWasiConsole }) {
     this.#origin = origin;
 
     const ws_url = this.#origin.replace("http://", "ws://");
@@ -399,8 +402,17 @@ export class DyLDRPC {
     this.#wsSig = new WebSocket(ws_url, "sig");
     this.#wsSig.binaryType = "arraybuffer";
 
+    this.#redirectWasiConsole = redirectWasiConsole;
+    if (redirectWasiConsole) {
+      this.#wsStdout = new WebSocket(ws_url, "stdout");
+      this.#wsStderr = new WebSocket(ws_url, "stderr");
+    }
+
     this.opened = Promise.all(
-      [this.#wsPipe, this.#wsSig].map(
+      (redirectWasiConsole
+        ? [this.#wsPipe, this.#wsSig, this.#wsStdout, this.#wsStderr]
+        : [this.#wsPipe, this.#wsSig]
+      ).map(
         (ws) =>
           new Promise((res, rej) => {
             ws.addEventListener("open", res);
@@ -413,6 +425,10 @@ export class DyLDRPC {
   close() {
     this.#wsPipe.close();
     this.#wsSig.close();
+    if (this.#redirectWasiConsole) {
+      this.#wsStdout.close();
+      this.#wsStderr.close();
+    }
   }
 
   async #rpc(endpoint, ...args) {
@@ -444,6 +460,22 @@ export class DyLDRPC {
   async fetchWasm(p) {
     return fetch(`${this.#origin}/fs${p}`);
   }
+
+  stdout(msg) {
+    if (this.#redirectWasiConsole) {
+      this.#wsStdout.send(msg);
+    } else {
+      console.info(msg);
+    }
+  }
+
+  stderr(msg) {
+    if (this.#redirectWasiConsole) {
+      this.#wsStderr.send(msg);
+    } else {
+      console.warn(msg);
+    }
+  }
 }
 
 // Actual implementation of endpoints used by DyLDRPC
@@ -452,7 +484,15 @@ class DyLDRPCServer {
   #server;
   #wss;
 
-  constructor({ host, port, dyldPath, libdir, ghciSoPath, args }) {
+  constructor({
+    host,
+    port,
+    dyldPath,
+    libdir,
+    ghciSoPath,
+    args,
+    redirectWasiConsole,
+  }) {
     this.#server = http.createServer(async (req, res) => {
       const origin = originFromServerAddress(await this.listening);
 
@@ -487,7 +527,7 @@ class DyLDRPCServer {
           `
 import { DyLDRPC, main } from "./fs${dyldPath}";
 const args = ${JSON.stringify({ libdir, ghciSoPath, args })};
-args.rpc = new DyLDRPC({origin: "${origin}"});
+args.rpc = new DyLDRPC({origin: "${origin}", redirectWasiConsole: ${redirectWasiConsole}});
 args.rpc.opened.then(() => main(args));
 `
         );
@@ -578,6 +618,16 @@ args.rpc.opened.then(() => main(args));
         return;
       }
 
+      if (ws.protocol === "stdout") {
+        ws.addEventListener("message", (ev) => console.info(ev.data));
+        return;
+      }
+
+      if (ws.protocol === "stderr") {
+        ws.addEventListener("message", (ev) => console.warn(ev.data));
+        return;
+      }
+
       throw new Error(`unknown protocol ${ws.protocol}`);
     });
 
@@ -682,12 +732,8 @@ class DyLD {
           new wasi.OpenFile(
             new wasi.File(new Uint8Array(), { readonly: true })
           ),
-          wasi.ConsoleStdout.lineBuffered((msg) =>
-            console.log(`[WASI stdout] ${msg}`)
-          ),
-          wasi.ConsoleStdout.lineBuffered((msg) =>
-            console.warn(`[WASI stderr] ${msg}`)
-          ),
+          wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stdout(msg)),
+          wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stderr(msg)),
         ],
         { debug: false }
       );
@@ -1126,6 +1172,11 @@ export async function main({ rpc, libdir, ghciSoPath, args }) {
     libdir,
     ghciSoPath,
     args,
+    redirectWasiConsole:
+      process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS ||
+      process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE
+        ? false
+        : Boolean(process.env.GHCI_BROWSER_REDIRECT_WASI_CONSOLE),
   });
   const origin = originFromServerAddress(await server.listening);
 
-- 
GitLab