Skip to content

fix(cli): wait for the task's graceful shutdown on ctrl-c#2065

Closed
forehalo wants to merge 9 commits into
voidzero-dev:mainfrom
forehalo:fix/issue-2036
Closed

fix(cli): wait for the task's graceful shutdown on ctrl-c#2065
forehalo wants to merge 9 commits into
voidzero-dev:mainfrom
forehalo:fix/issue-2036

Conversation

@forehalo

@forehalo forehalo commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Closes #2036

On Ctrl+C, vp (the PTY session leader) exited within milliseconds while the task tree (vite dev server, managed node) was still shutting down. The dying tree's late terminal writes and mode restores raced the next shell prompt, so fish's prompt probes (OSC 11, CSI 6n, DA1) were answered into a terminal nobody owned and the replies showed up as ^[]11;rgb:...^[[9;1R^[[?62;22;52c on the prompt.

vp now runs delegated commands behind a terminal guard: it saves and restores the terminal state, swallows SIGINT while the child runs (with a signal handler rather than SIG_IGN, so children without their own SIGINT handling stay interruptible), and lets the child's own exit decide when vp returns. A second Ctrl+C force-kills the child so a stuck task cannot make vp unstoppable, and children killed by a signal exit with the conventional 128 + signal code.

PTY snapshot tests pin both behaviors: waiting for the task's graceful shutdown, and the double Ctrl+C escalation. Verified with real fish 4.8 driven in a scripted PTY: the stray sequences no longer appear after killing vp run dev.

Copilot AI review requested due to automatic review settings July 6, 2026 09:12
@netlify

netlify Bot commented Jul 6, 2026

Copy link
Copy Markdown

Deploy Preview for viteplus-preview canceled.

Name Link
🔨 Latest commit 8a450a7
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-preview/deploys/6a4c5632284ece000814d2b0

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses stray/odd OSC terminal sequences after interrupting vp run with Ctrl+C by ensuring the parent process preserves/restores terminal state and temporarily ignores SIGINT while awaiting the child process. It applies this guard to the global CLI’s JS delegation path so Ctrl+C primarily affects the spawned JS CLI process instead of terminating the Rust parent mid-cleanup.

Changes:

  • Route JS CLI delegation through vite_command::execute_with_terminal_guard instead of Command::status().
  • Extend the Unix terminal-state guard to also ignore and then restore the process SIGINT disposition while the child runs.
  • Enable the nix signal feature for non-Windows builds.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
crates/vite_global_cli/src/js_executor.rs Uses execute_with_terminal_guard when delegating to JS entrypoints so terminal cleanup runs even after Ctrl+C.
crates/vite_command/src/lib.rs Updates terminal guard behavior to ignore/restore SIGINT on Unix while awaiting the child process.
crates/vite_command/Cargo.toml Adds nix signal feature to support sigaction usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/vite_command/src/lib.rs Outdated
@fengmk2

fengmk2 commented Jul 6, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60cb19524f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/vite_command/src/lib.rs Outdated
@fengmk2 fengmk2 added the preview-build Publish this PR's commits to the registry bridge as preview builds label Jul 6, 2026
@fengmk2

fengmk2 commented Jul 6, 2026

Copy link
Copy Markdown
Member

@forehalo I will make some improvements based on your fixes and add snapshot tests to ensure continuous verification.

fengmk2 added a commit that referenced this pull request Jul 6, 2026
…rl-c

Builds on the terminal-guard approach from #2065 with three adjustments:

Swallowing moves out of the guard's sigaction and into the wait loop as a
signal handler. SIG_IGN survives exec into the entire child tree, so any
task without its own SIGINT handler (a plain script, a shell) would become
uninterruptible; handler dispositions reset to default on exec and leave
children untouched. It also applies when stdin is not a terminal, where
the guard's SIG_IGN never installed at all.

A second Ctrl+C force-kills the child, so a task that ignores the first
interrupt cannot make vp unstoppable. Without it, killing a stuck task
requires an external signal.

Children killed by a signal now exit with the conventional 128 + signal
code (130 for SIGINT) instead of a generic failure.

Refs #2036
forehalo and others added 5 commits July 7, 2026 01:17
Killing a vp run task with Ctrl+C in fish leaves stray escape sequences on
the next prompt: fish probes the terminal (OSC 11, CSI 6n, DA1) at every
prompt draw, and vp run tears the task down within milliseconds of Ctrl+C
instead of waiting for its graceful shutdown, so the dying task tree's late
terminal writes and mode restores race the shell's probes and the replies
land as garbage input.

The new run_ctrlc_teardown fixture pins the vp-side defect: a
dev-server-like vpt payload whose shutdown takes 300 ms records, via a
watcher process in its own process group that survives vp's teardown,
whether that shutdown was allowed to finish. verdict.txt currently reads
"task was torn down before its graceful shutdown finished"; a fix should
flip it to "task completed its graceful shutdown".

Also adds vpt wait-file, a generic helper that polls until a file exists
and prints it, for asserting on files written by a step's surviving
process tree.

Refs voidzero-dev#2036
…rl-c

Builds on the terminal-guard approach from voidzero-dev#2065 with three adjustments:

Swallowing moves out of the guard's sigaction and into the wait loop as a
signal handler. SIG_IGN survives exec into the entire child tree, so any
task without its own SIGINT handler (a plain script, a shell) would become
uninterruptible; handler dispositions reset to default on exec and leave
children untouched. It also applies when stdin is not a terminal, where
the guard's SIG_IGN never installed at all.

A second Ctrl+C force-kills the child, so a task that ignores the first
interrupt cannot make vp unstoppable. Without it, killing a stuck task
requires an external signal.

Children killed by a signal now exit with the conventional 128 + signal
code (130 for SIGINT) instead of a generic failure.

Refs voidzero-dev#2036
…scalation

The run_ctrlc_teardown snapshot flips from "task was torn down before its
graceful shutdown finished" to "task completed its graceful shutdown" and
now guards the waiting behavior. A second case covers the escalation: a
task that ignores the interrupt (vpt report-orphan-on-ctrlc
--ignore-interrupt) keeps vp waiting after the first Ctrl+C and is
force-killed by the second, with vp exiting 137 and the surviving watcher
recording the unfinished shutdown. The "interrupted" milestone between the
two Ctrl+C presses keeps them from coalescing into a single signal.
@fengmk2 fengmk2 changed the title fix(cli): odd osc sequences after killing vp run with ctrl-c fix(cli): wait for the task's graceful shutdown on ctrl-c Jul 6, 2026
@fengmk2

fengmk2 commented Jul 6, 2026

Copy link
Copy Markdown
Member

@codex review

@fengmk2 fengmk2 self-assigned this Jul 6, 2026
@fengmk2 fengmk2 added preview-build Publish this PR's commits to the registry bridge as preview builds and removed preview-build Publish this PR's commits to the registry bridge as preview builds labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9425c4fda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/vite_command/src/lib.rs Outdated
Comment thread crates/vite_command/src/lib.rs Outdated
Comment thread crates/vite_command/src/lib.rs
fengmk2 added 3 commits July 7, 2026 02:31
Review follow-ups on the Ctrl+C guard:

Forward a targeted interrupt to the child when stdin is not a terminal:
a supervisor, timeout, or CI cancellation signals only this process, and
swallowing it left the task running with vp apparently hung. With a
terminal the foreground group already delivered it, so forwarding would
double it (a second SIGINT makes vite exit immediately instead of
gracefully).

Make the second Ctrl+C kill the whole process group when this process is
its own group leader (a shell job leader), dying with it, instead of
SIGKILLing only the intermediate node process and leaving the stuck task
tree alive; the terminal state is restored first. When sharing the
parent's group (a supervisor), only the direct child is killed, so the
supervisor is never taken down. The escalation also prints a warning so
the force quit is visible.

Re-raise SIGINT after a swallowed interrupt once the child has exited, so
calling shells see a died-of-SIGINT status and abort scripts and loops
(wait-and-cooperative-exit); previously vp exited with the child's plain
status and a Ctrl+C'd `vp run` inside a script let the script continue.

Restore the pre-guard SIGINT disposition after the child exits: tokio's
handler registration is process-global and permanent, which left the
process (including the node local CLI) immune to Ctrl+C for its remaining
lifetime. Register the signal stream before spawning so a registration
failure cannot leak an unwaited child.

Reuse shim::exec::exit_code_from_status (128 + signal) for vp run and vpr
exit codes instead of a duplicated mapping in main.rs and a plain
code().unwrap_or(1) in vpr; give the fixture's wait-file steps headroom
over the watcher's 10 s safety cap; share the ctrl-c bootstrap (including
the Windows CONSOLE_IGNORE_CTRL_C workaround) between the vpt payloads;
refresh the stale vpt header comment.

Refs voidzero-dev#2036
The local JS/NAPI path collapsed signal-death statuses to exit code 1
with status.code().unwrap_or(1) before returning across the NAPI
boundary, so interrupting a local vp dev/preview reported 1 instead of
130 (and force kills 1 instead of 137).

vite_command now exposes exit_code_from_status (128 + signal on unix)
as the single implementation; the binding's execution paths and the
global CLI's shim helper both delegate to it.

Refs voidzero-dev#2036
The field is regenerated by the packages build from the local vendored
vite clone; a stale clone in the build environment rewrote it to 8.1.2
and it was committed accidentally. The branch pin is 8.1.3.
@fengmk2

fengmk2 commented Jul 7, 2026

Copy link
Copy Markdown
Member

@forehalo Sorry, My changes are too extensive, I will close this, please help to resubmit.

@fengmk2 fengmk2 closed this Jul 7, 2026
forehalo pushed a commit to forehalo/vite-plus that referenced this pull request Jul 7, 2026
…rl-c

Builds on the terminal-guard approach from voidzero-dev#2065 with three adjustments:

Swallowing moves out of the guard's sigaction and into the wait loop as a
signal handler. SIG_IGN survives exec into the entire child tree, so any
task without its own SIGINT handler (a plain script, a shell) would become
uninterruptible; handler dispositions reset to default on exec and leave
children untouched. It also applies when stdin is not a terminal, where
the guard's SIG_IGN never installed at all.

A second Ctrl+C force-kills the child, so a task that ignores the first
interrupt cannot make vp unstoppable. Without it, killing a stuck task
requires an external signal.

Children killed by a signal now exit with the conventional 128 + signal
code (130 for SIGINT) instead of a generic failure.

Refs voidzero-dev#2036

(cherry picked from commit bb5239e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview-build Publish this PR's commits to the registry bridge as preview builds

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Odd characters after killing vp run with fish

3 participants