Skip to content

fix(core): kill discrete task processes during SIGINT cleanup#35175

Draft
FrozenPandaz wants to merge 6 commits intomasterfrom
fix/sigint-discrete-task-cleanup
Draft

fix(core): kill discrete task processes during SIGINT cleanup#35175
FrozenPandaz wants to merge 6 commits intomasterfrom
fix/sigint-discrete-task-cleanup

Conversation

@FrozenPandaz
Copy link
Copy Markdown
Collaborator

Current Behavior

When Nx receives SIGINT (Ctrl+C), performCleanup() in the task orchestrator kills continuous tasks and run-commands tasks, but not discrete tasks (executor-based builds like @nx/js:tsc, nx:run-script, etc.).

In TUI mode, fork workers run in separate PTY process groups and don't receive SIGINT directly. Without explicit cleanup, performCleanup() hangs at await Promise.all(this.discreteTaskExitHandled.values()) waiting for tasks that will never exit.

Expected Behavior

All task types — discrete, continuous, and run-commands — should be explicitly killed during SIGINT cleanup so the process exits promptly.

Related Issue(s)

N/A — found via code inspection and confirmed with tests.

Notes

  • First commit adds tests only (no fix) — CI should show the TUI-mode e2e test failing
  • Second commit adds the fix — CI should pass

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 5, 2026

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit ae85d03
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/69d1f774f16eae00088845f5
😎 Deploy Preview https://deploy-preview-35175--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 5, 2026

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit ae85d03
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/69d1f7743b18e60008aaee17
😎 Deploy Preview https://deploy-preview-35175--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud bot commented Apr 5, 2026

View your CI Pipeline Execution ↗ for commit ae85d03

Command Status Duration Result
nx affected --targets=lint,test,build,e2e,e2e-c... ❌ Failed 28m 56s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 7s View ↗
nx-cloud record -- pnpm nx conformance:check ✅ Succeeded 7s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 2s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-05 06:52:41 UTC

BatchProcess.kill() was only sending the signal to the immediate child
process (the Node fork worker), leaving grandchild processes alive.
This is especially problematic for Gradle where the process tree is
deep (Node fork → Java JVM → Gradle Daemon → Gradle Workers).
nx-cloud[bot]

This comment was marked as outdated.

execSync blocks the Node event loop, preventing the batch worker from
responding to signals or IPC messages during SIGINT cleanup. Switch to
spawn so the process can be properly terminated.
Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud bot left a comment

Choose a reason for hiding this comment

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

Nx Cloud is proposing a fix for your failed CI:

We updated the SIGINT e2e test helpers to add detached: true and send signals via process.kill(-nxProcess.pid!, signal) targeting the entire process group. This ensures the signal reaches the nx process even when it runs as a grandchild of the shell, fixing the consistent timeout failures where exitedCleanly was always false.

Note

We are verifying this fix by re-running e2e-nx:e2e-ci--src/run.test.ts.

Suggested Fix changes
diff --git a/e2e/nx/src/run.test.ts b/e2e/nx/src/run.test.ts
index 970cf293f5..132b97438e 100644
--- a/e2e/nx/src/run.test.ts
+++ b/e2e/nx/src/run.test.ts
@@ -946,6 +946,9 @@ describe('Nx Running Tests', () => {
         },
         shell: true,
         stdio: 'pipe',
+        // detached creates a new process group so we can send signals to
+        // the entire group (shell + nx + its children) via -pid
+        detached: true,
       });
 
       // Wait for the child process to start.
@@ -987,14 +990,17 @@ describe('Nx Running Tests', () => {
       expect(childPid).toBeDefined();
       expect(isProcessAlive(childPid)).toBe(true);
 
-      // Send SIGINT
-      nxProcess.kill('SIGINT');
+      // Send SIGINT to the entire process group so the signal reaches
+      // the nx process even when it is a grandchild of the shell.
+      process.kill(-nxProcess.pid!, 'SIGINT');
 
       // Nx should exit promptly (cleanup should not hang).
       // Without the fix, cleanup hangs waiting for unkilled discrete tasks.
       const exitedCleanly = await new Promise<boolean>((resolve) => {
         const timeout = setTimeout(() => {
-          nxProcess.kill('SIGKILL');
+          try {
+            process.kill(-nxProcess.pid!, 'SIGKILL');
+          } catch {}
           resolve(false);
         }, 10_000);
         nxProcess.on('exit', () => {
@@ -1037,6 +1043,7 @@ describe('Nx Running Tests', () => {
         },
         shell: true,
         stdio: 'pipe',
+        detached: true,
       });
 
       // Poll for PID file (TUI swallows stdout so we can't parse it)
@@ -1051,20 +1058,25 @@ describe('Nx Running Tests', () => {
       }
 
       if (!childPid) {
-        nxProcess.kill('SIGKILL');
+        try {
+          process.kill(-nxProcess.pid!, 'SIGKILL');
+        } catch {}
         throw new Error('Timed out waiting for PID file from TUI task');
       }
 
       expect(isProcessAlive(childPid)).toBe(true);
 
-      // Send SIGINT
-      nxProcess.kill('SIGINT');
+      // Send SIGINT to the entire process group so the signal reaches
+      // the nx process even when it is a grandchild of the shell.
+      process.kill(-nxProcess.pid!, 'SIGINT');
 
       // Nx should exit promptly — if cleanup hangs because discrete tasks
       // aren't killed, this will time out.
       const exitedCleanly = await new Promise<boolean>((resolve) => {
         const timeout = setTimeout(() => {
-          nxProcess.kill('SIGKILL');
+          try {
+            process.kill(-nxProcess.pid!, 'SIGKILL');
+          } catch {}
           resolve(false);
         }, 10_000);
         nxProcess.on('exit', () => {

🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally cErl-2Ff9

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

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