Skip to content

Stop the dev server being interrupted at startup, and make interpreter selection work from .pyxl files - #13

Merged
shivamsn97 merged 2 commits into
mainfrom
fix/debug-interpreter-and-dev-server
Aug 6, 2026
Merged

Stop the dev server being interrupted at startup, and make interpreter selection work from .pyxl files#13
shivamsn97 merged 2 commits into
mainfrom
fix/debug-interpreter-and-dev-server

Conversation

@shivamsn97

Copy link
Copy Markdown
Member

The issue

Two problems, both reported from a real session and both rooted in how the extension talks to the Python environment.

1. The dev server was killed seconds after it started. Using "Debug Pyxle app (React browser)", the server came up and was then immediately interrupted:

pyxle dev
✅ Pyxle dev server ready in 1690 ms
^C⚠ Interrupt received; stopping dev server
pyenv shell 3.14.4

The extension started the server by opening a shell terminal and typing pyxle dev into it. A shell terminal is writable by anything, and the Python extension activates the selected environment in every newly created one. Modern activation goes through TerminalShellIntegration.executeCommand(), whose documented behaviour is to send ^C first to interrupt any running command — so VS Code interrupted the just-started server and typed the activation line (pyenv shell …, source .venv/bin/activate) in its place. Nothing the extension could time around: activation is asynchronous and lands whenever the interpreter resolves.

2. "Debugging needs pyxle-framework 0.8.0 or newer" with 0.8.0 installed. Two causes:

  • The pre-launch check ran against VS Code's selected interpreter, but the React-browser flow launched whatever pyxle was first on the shell PATH — routinely a different environment. The two configurations could run two different installs of two different versions.
  • The message offered only Copy upgrade command. When the working pyxle lives in another environment — the common case — upgrading the selected one is the wrong advice, and there was no way to switch environments from the dialog. There is also no interpreter indicator in a .pyxl editor (the Python extension scopes its own to .py files), so the interpreter driving the launch was invisible and unchangeable without opening an unrelated Python file.

The fix

The extension owns the dev-server process. A Pseudoterminal spawns it directly instead of typing into a shell. No shell means terminal.shellIntegration is never populated, so the ^C-sending API cannot be called on it, and a foreign Terminal.sendText() is delivered to our handleInput, where it is dropped — the injection is inert by construction rather than merely unlikely. The panel still shows the server's output, Ctrl-C still stops it, and shutdown is now exact: SIGINT to the process group (so Vite and the SSR workers go too), a SIGKILL backstop, taskkill /T /F on Windows.

Both configurations run the same environment. The React-browser flow now launches <selected interpreter> -m pyxle dev through the same argv helper and the same pre-launch check as the Python flow.

The pre-launch check is capability-based and no longer a dead end.

  • It asks what the interpreter can do (find_spec('pyxle.__main__')), never what its package metadata claims — so an editable install whose dist-info still reads 0.7.5 while the code is current launches normally. The reported version is shown for context only, labelled as package metadata.
  • A probe that cannot answer (interpreter crash, timeout, spawn failure) reports unknown and lets the launch proceed, so the debugger surfaces the real error instead of the check asserting "pyxle is not installed".
  • Both failure dialogs lead with Select Interpreter, always open the picker when clicked, and re-probe afterwards — so fixing the environment (or picking another one) continues the launch instead of costing another F5.

New: the interpreter is visible and switchable from .pyxl files — a status-bar item showing the selected environment (click to change), plus a Pyxle: Select Python Interpreter command.

Also in this change: owned dev servers are stopped when VS Code shuts down (they run in their own process group and would otherwise keep holding their ports), and command palette entries no longer read "Pyxle: Pyxle: …".

Verification

  • tsc --noEmit clean; npm test green (23 tests, 12 new covering the probe, version parsing and interpreter labelling).
  • Probed against three real environments: an editable install reporting 0.7.5 with current code → launches (previously refused), a 0.8.0 install → ok, a nonexistent interpreter → unknown rather than a false "not installed".
  • Packaged and installed the VSIX locally; the built bundle contains no sendText("pyxle dev").

The changelog entry is under Unreleased — this ships with the next extension release.

…tion

The "Debug Pyxle app (React browser)" configuration started the dev server by
opening a shell terminal and typing `pyxle dev` into it. A shell terminal is
writable by any extension, and the Python extension activates the selected
environment in every new one via TerminalShellIntegration.executeCommand(),
which is documented to send ^C first to interrupt whatever is running. The
server was killed seconds after starting and the activation line typed in its
place.

The extension now owns the process: a Pseudoterminal spawns the dev server
directly, so no shell exists to inject into (a foreign sendText is delivered to
handleInput, where it is dropped) and shutdown is exact — SIGINT to the process
group, SIGKILL backstop, taskkill on Windows.

That path also ran whatever `pyxle` came first on PATH while the Python path ran
VS Code's selected interpreter — two different installs. Both now run the
selected interpreter as `python -m pyxle dev`, through one shared argv helper
and the same pre-launch check.

The pre-launch check is reworked:
- It tests what the interpreter can do (find_spec pyxle.__main__), never the
  version its metadata reports, so an editable install whose dist-info is stale
  is no longer refused. The reported version is shown for context only.
- A probe that cannot answer (crash, timeout, spawn failure) reports "unknown"
  and lets the launch proceed rather than asserting pyxle is absent.
- Both failure dialogs lead with Select Interpreter — the usual cause is that
  pyxle lives in another environment — always open the picker when clicked, and
  re-probe afterwards so a fixed environment continues the launch.

New: a status-bar item shows the selected interpreter while a .pyxl file is
open, with a Pyxle: Select Python Interpreter command, since the Python
extension scopes its own indicator to .py files.

Also stop owned dev servers when the extension shuts down (they run in their own
process group and would otherwise keep holding their ports), and drop the
duplicated "Pyxle: " prefix from command palette titles.
Review of the pty rework surfaced five defects, all in the new
process-lifecycle code:

The Windows teardown could throw and strand the tree. `cp.spawn` reports a
missing or blocked `taskkill` asynchronously as an "error" event, which the
surrounding try/catch cannot see and which throws when unhandled. It now
has a listener that falls back to killing the leader directly, instead of
waiting out the SIGKILL timer that only ever reaches that leader and leaves
Vite holding its port.

Exit settled on both "exit" and "close". "exit" fires before the piped
stdio drains, so a server that died on startup printed "The dev server
stopped" above its own traceback, and a deliberate stop closed the panel
on top of output still in flight. It settles on "close" alone now, which
also still covers the case the pair was guarding: a command that does not
exist emits "error" then "close" and never "exit" — checked against Node
directly.

`onDidExit` was declared, fired, and subscribed by nobody. The React
launch now uses it to cancel the readiness wait when a server it started
exits early, so a broken interpreter that the probe deliberately lets
through reports the real error instead of holding the in-flight guard for
the full two-minute timeout, silently swallowing every further F5.

The interpreter gate can block on a dialog and a quick pick for an
unbounded time, and discovery was not re-read afterwards — long enough for
a server to have appeared, and a second `pyxle dev` was spawned onto the
taken port. It re-checks and re-adopts instead.

The changelog claimed shutdown always stops the server; the process group
that makes one signal reach the whole tree also removes the SIGHUP a pty
close used to deliver, so a force-quit can still leave it running. Says so
now.
@shivamsn97

Copy link
Copy Markdown
Member Author

Reviewed and merging. The diagnosis is right and the pty approach is the correct fix rather than a timing workaround: VS Code routes Terminal.sendText() to Pseudoterminal.handleInput and never populates shellIntegration on an extension terminal, so the ^C-sending activation path is structurally unreachable. argv stays an array throughout, so interpreter paths with spaces are safe on both platforms, and the 0.3.0 invariants (refcounted ownership, discovery startedAt matching, provide-only Dynamic registration) are intact.

Five lifecycle gaps in the new code are fixed in a follow-up commit on this branch:

  1. Windows teardown could throw and strand the tree. cp.spawn reports a missing/blocked taskkill asynchronously as an error event — the surrounding try/catch cannot see it, and an unhandled error throws. Now has a listener that falls back to killing the leader, instead of relying on a SIGKILL timer that only reaches that leader and leaves Vite holding the port.
  2. Settled on both exit and close. exit fires before piped stdio drains, so a startup failure printed "The dev server stopped" above its own traceback, and a deliberate stop closed the panel over output still in flight. Settles on close alone now — verified against Node that a nonexistent command emits errorclose and never exit, so the case the pair was guarding is still covered.
  3. onDidExit was dead code. The React launch now subscribes to it, so a server that dies early cancels the readiness wait instead of holding browserLaunchInFlight for the full 120 s and silently swallowing every further F5 — which the new unknown probe verdict makes more reachable, by design.
  4. Discovery was not re-read after the interpreter gate, which can block on a dialog and a quick pick for an unbounded time. A second pyxle dev could be spawned onto a port that had since been taken; it re-checks and re-adopts now.
  5. Changelog accuracydetached gives one signal the whole tree but removes the SIGHUP a pty close used to deliver, so a force-quit can still leave the server running. The entry says so rather than claiming shutdown always stops it.

tsc --noEmit clean, 23/23 tests pass, packaged cleanly, and the bundle contains no sendText("pyxle dev").

@shivamsn97
shivamsn97 merged commit f61fb7f into main Aug 6, 2026
5 checks passed
@shivamsn97
shivamsn97 deleted the fix/debug-interpreter-and-dev-server branch August 6, 2026 13:59
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