Skip to content

wasip1: LSP over node:wasi hangs after initialize — signal watcher starves the scheduler (fix included) #63862

Description

Split out from #63858. tsgo --lsp --stdio built for wasip1 and run under Node's node:wasi answers initialize, then ignores everything after it, spinning at 100% CPU. CLI invocations (tsgo -p) are unaffected — output is identical to the native binary on node >= 23.

Cause: cmd/tsgo calls signal.NotifyContext unconditionally. On wasip1 signals are never delivered, and Go's signal-watcher goroutine busy-spins (a sched_yield loop — we measured ~190k calls in 8s of idle). Wasm is single-threaded, so that always-runnable goroutine keeps the scheduler from ever reaching poll_oneoff — and the stdin reader, parked waiting for input, never wakes.

Fix: skip the signal watcher on wasip1 — since no signal can arrive, context.WithCancel is equivalent:

func notifyContext(parent context.Context, sigs ...os.Signal) (context.Context, context.CancelFunc) {
	if runtime.GOOS == "wasip1" {
		return context.WithCancel(parent)
	}
	return signal.NotifyContext(parent, sigs...)
}

Patch applies at typescript/v7.0.2. With it, a full LSP session under node:wasi works — initialize, publishDiagnostics, hover, shutdown — with exit behavior matching native. It also lets the runtime idle in poll_oneoff instead of burning a core.

One note: this makes LSP-over-wasm node-only for now — wasmtime 46 refuses the non-blocking stdio call (fd_fdstat_set_flags) the LSP path needs, so it still freezes there even with the fix.

Minimal repro driver:

import { readFile } from 'node:fs/promises';
import { WASI } from 'node:wasi';
const wasi = new WASI({ version: 'preview1', args: ['tsgo', '--lsp', '--stdio'],
  env: { PWD: '/' }, preopens: { '/': process.cwd() }, returnOnExit: true });
const wasm = await WebAssembly.compile(await readFile('tsgo.wasm'));
const inst = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.start(inst);

Spawn it with piped stdio, send a framed initialize (answered), then anything else: the stock binary never reads it; the patched one proceeds.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions