pingdotgg / pingdotgg/t3code

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

Open
#3,709 2 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

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…".

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with apps/server/src/bin.ts and packages/ssh/src/tunnel.ts, then reproduce the WSL launch with an unsupported login-shell Node version and a satisfying version installed. Trace the entrypoint check and REMOTE_NODE_ENV_SCRIPT fallback gates. Done means unsupported Node versions produce a visible non-zero failure and the remote setup can select a satisfying installed version instead of silently exiting.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, shell, typescript
Domain
backend, infrastructure
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.