Skip to content

fix(cli): bound Tailscale Serve invocation and surface stdout in diagnostic - #10

Draft
andromarces wants to merge 1 commit into
rjx18:mainfrom
andromarces:fix/tailscale-serve-hang
Draft

fix(cli): bound Tailscale Serve invocation and surface stdout in diagnostic#10
andromarces wants to merge 1 commit into
rjx18:mainfrom
andromarces:fix/tailscale-serve-hang

Conversation

@andromarces

Copy link
Copy Markdown
Contributor

Problem

codor install, after choosing "On this computer and remotely", can hang indefinitely at "configuring Tailscale" with no prompt, no error, no timeout, and no exit — the only recovery is Ctrl-C.

This happens when the tailnet does not have HTTPS certificates enabled. In that case tailscale serve --bg does not fail; it enters Tailscale's interactive feature-enablement flow, printing a consent URL to stdout and blocking until a human completes it in a browser. Two things in configureTailscaleServe (setup.ts) turn that into a silent freeze:

  1. stdout is piped, not inherited, so the consent URL is captured into a buffer nobody sees;
  2. no timeout is passed to execFileSync, so the wait is unbounded.

Fix

  • Bound only the serve --bg invocation with a 20s timeout. serve status (the next call, which never blocks this way) stays unbounded, and every other defaultExec caller is unaffected since the new options parameter is optional — verified by adding it to the shared exec type rather than special-casing one call site.
  • Surface stdout alongside stderr in the thrown diagnostic, so a bounded failure explains itself instead of surfacing as a bare timeout.

Both changes are scoped to configureTailscaleServe and the shared exec signature; defaultExec's behavior is unchanged for every other call site (plutil, launchctl, wslinfo, icacls, systemctl, loginctl, schtasks, where/which).

Not in this PR

Preflighting the actual precondition (checking CertDomains via tailscale status --json before ever calling Serve, to turn the 20s wait into an immediate message) is a larger, separately-reviewable change that adds a dependency on a Tailscale JSON field. Proposing it as a follow-up once this lands.

Testing

pnpm install --frozen-lockfile
pnpm -r build
cd packages/cli
pnpm exec vitest run

Result: same 5 pre-existing failures as the unmodified baseline at this commit (runtime-install.spec.ts:116, setup-tailscale.spec.tsresolveTailscale, two POSIX-path/JSON-escaping failures and a timeout in index.spec.ts, one unrelated artifact.spec.ts assertion) — all pnpm-11/Windows-path portability issues unrelated to this change. 177 passed (was 175), the 2 new tests in setup-tailscale.spec.ts both pass, no new failures.

pnpm exec vitest run is used directly rather than pnpm --filter @codor/cli test / pnpm -r test, which currently cannot reach or complete on Windows for unrelated reasons at this commit.

Not independently reproduced: the hang itself was not triggered by actually executing tailscale serve --bg — doing so publishes an endpoint to a tailnet and issues a publicly-logged HTTPS certificate, which is an irreversible disclosure. The blocking-consent mechanism is documented Tailscale behavior, confirmed by a second reviewer against tailscale v1.98.9 (ipn/ipnlocal/serve_legacy.go, enableFeatureInteractive). The preconditions codor checks (CertDomains: null, BackendState: Running, Health: [], serve --help exits 0) were all verified on the affected machine (Windows 11, Tailscale 1.98.9). Only tested on Windows 11; the macOS/Linux paths through this function are unchanged but not independently re-verified here.

Harn

Touches the setup-resolves-and-capability-probes-tailscale-serve assumption (setup.ts, "Setup resolves Tailscale by path and app location and probes Serve support"). The assumption's substance is unchanged — Tailscale is still resolved and probed the same way; this only bounds and better-diagnoses the subsequent Serve call.

…nostic

`codor install` hangs indefinitely at "configuring Tailscale" when the
tailnet has no HTTPS certificates enabled: `tailscale serve --bg` does
not fail in that case, it blocks on an interactive browser-consent flow
and prints the consent URL to stdout, which `execFileSync` captures into
a buffer nobody ever sees, with no timeout to bound the wait.

Bound the `serve --bg` invocation to 20s (only that call; `serve status`
stays unbounded, and every other `defaultExec` caller is unaffected
since the new options parameter is optional), and surface stdout
alongside stderr in the thrown diagnostic so a bounded failure is
actionable instead of a bare timeout.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant