Skip to content

WSL backend hangs at "Connecting to WSL…" (silent exit) when login-shell node is < 22.16 #3709

Description

@arturskruze

Summary

On Windows+WSL, the desktop backend hangs forever at "Connecting to WSL…" when the WSL login shell resolves a node that is present but below the engine floor (< 22.16). The server process starts, loads, and exits 0 silently without binding — so the desktop just respawns it in a loop with no diagnostic.

Two independent issues combine to make this a silent, hard-to-diagnose failure.

Environment

  • Windows 11 + WSL2 (Ubuntu), backend launched via wsl.exe -- node … --bootstrap-fd 0
  • nvm default node was v22.12.0; a satisfying v22.21.1 was also installed but not the default
  • Server engines.node: ^22.16 || ^23.11 || >=24.10

Root cause 1 — import.meta.main guard fails silently on Node < 22.16

apps/server/src/bin.ts gates all startup on import.meta.main:

if (import.meta.main) {
  Command.run(cli, { version: packageJson.version }).pipe(/* … */ NodeRuntime.runMain);
}

import.meta.main was added in Node 22.16.0. On 22.12.0 it is undefined, so the guard is falsy: the module loads its entire import graph and the process exits 0 with no output and no error. Verified:

$ node -v            # v22.12.0
$ node -e 'x' --input-type=module ...   # import.meta.main === undefined
# server run under 22.12.0 -> loads, exits 0, never creates a socket (strace: zero bind/listen)
# server run under 22.21.1 -> binds and serves /.well-known/t3/environment correctly

Because it exits 0 cleanly, the desktop's restart loop keeps respawning it and the UI shows "Connecting to WSL…" indefinitely. Suggestion: detect an unsupported/undefined import.meta.main Node and fail loudly (clear stderr message + non-zero exit) so the desktop can surface it, e.g. a robust entrypoint check plus an explicit engine assertion:

import { pathToFileURL } from "node:url";
const isEntrypoint =
  import.meta.main ??
  (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href);
if (isEntrypoint) { /* … */ }

Root cause 2 — ensure_remote_node_path won't upgrade away from an unsatisfying node

packages/ssh/src/tunnel.ts REMOTE_NODE_ENV_SCRIPT: the early-return correctly checks node present and satisfies engine, but every version-manager fallback (mise/fnm/nodenv/nvm, and the nvm version scan) is gated on ! command -v node — i.e. "no node at all", not "no satisfying node". So when a login shell already exposes a node that fails the engine range (e.g. nvm default 22.12.0, or a system /usr/bin/node), the fallbacks are skipped and ensure_remote_node_path execs the unsatisfying node anyway (… || true).

Minimal fix — introduce a remote_node_ready helper and use it for the gates:

remote_node_ready() {
  command -v node >/dev/null 2>&1 && remote_node_satisfies_engine >/dev/null 2>&1
}

ensure_remote_node_path() {
  if remote_node_ready; then
    return 0
  fi
  # …prepend_path_if_dir calls unchanged…
  if ! remote_node_ready && command -v mise   >/dev/null 2>&1; then; fi
  if ! remote_node_ready && command -v fnm    >/dev/null 2>&1; then; fi
  if ! remote_node_ready && command -v nodenv >/dev/null 2>&1; then; fi
  if [ -s "$NVM_DIR/nvm.sh" ]; then
    . "$NVM_DIR/nvm.sh"
    if ! remote_node_ready && command -v nvm >/dev/null 2>&1; then
      nvm use --silent default >/dev/null 2>&1 || nvm use --silent node >/dev/null 2>&1 || nvm use --silent --lts >/dev/null 2>&1 || true
    fi
  fi
  if ! remote_node_ready && [ -d "$NVM_DIR/versions/node" ]; then
    for T3_NODE_BIN in "$NVM_DIR"/versions/node/*/bin; do
      [ -x "$T3_NODE_BIN/node" ] && PATH="$T3_NODE_BIN:$PATH" && export PATH
    done
  fi
  remote_node_ready
}

(The nvm version scan could additionally prefer a satisfying version rather than the last glob match, but the gate change alone lets it fall through to nvm's default and the other managers.)

Impact / workaround

Workaround: make the login-shell default node satisfy the range (nvm alias default 22.21.1) — confirmed fixes it. But the failure mode is invisible: no log line, clean exit, infinite "Connecting to WSL…".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions