diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 429c8d08680..0ad7eeb914d 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -125,6 +125,7 @@ "strip-ansi": "^7.1.2", "superjson": "^2.2.5", "tailwind-merge": "^3.4.0", + "tree-kill": "^1.2.2", "trpc-electron": "^0.1.2", "tw-animate-css": "^1.4.0", "zod": "^4.3.5", diff --git a/apps/desktop/src/main/terminal-host/pty-subprocess.ts b/apps/desktop/src/main/terminal-host/pty-subprocess.ts index e01c44a8d2b..2a9a8dd3fa3 100644 --- a/apps/desktop/src/main/terminal-host/pty-subprocess.ts +++ b/apps/desktop/src/main/terminal-host/pty-subprocess.ts @@ -11,6 +11,7 @@ import { write as fsWrite } from "node:fs"; import type { IPty } from "node-pty"; import * as pty from "node-pty"; +import treeKill from "tree-kill"; import { PtySubprocessFrameDecoder, PtySubprocessIpcType, @@ -367,23 +368,24 @@ function handleKill(payload: Buffer): void { const pid = ptyProcess.pid; - // Step 1: Send the requested signal (usually SIGTERM for graceful shutdown) - try { - ptyProcess.kill(signal); - } catch { - // Process may already be dead - } + // Step 1: Kill the process tree using tree-kill + // tree-kill traverses by PPID to find all descendants + treeKill(pid, signal, (err) => { + if (err) { + console.error("[pty-subprocess] Failed to kill process tree:", err); + } + }); // Step 2: Escalate to SIGKILL if still alive after 2 seconds // node-pty's onExit callback may not fire reliably after pty.kill() const escalationTimer = setTimeout(() => { if (!ptyProcess) return; // Already exited via onExit - try { - ptyProcess.kill("SIGKILL"); - } catch { - // Process may already be dead - } + treeKill(pid, "SIGKILL", (err) => { + if (err) { + console.error("[pty-subprocess] Failed to SIGKILL process tree:", err); + } + }); // Step 3: Force completion if onExit still hasn't fired after another 1 second // This ensures the subprocess exits even if node-pty never emits onExit @@ -440,11 +442,14 @@ function handleDispose(): void { ptyFd = null; if (ptyProcess) { - try { - ptyProcess.kill("SIGKILL"); - } catch { - // Ignore - } + const pid = ptyProcess.pid; + + // Kill the process tree - fire and forget since we're exiting + treeKill(pid, "SIGKILL", (err) => { + if (err) { + console.error("[pty-subprocess] Failed to kill process tree:", err); + } + }); ptyProcess = null; } diff --git a/bun.lock b/bun.lock index 8726b9ed627..3c3f7b83cb4 100644 --- a/bun.lock +++ b/bun.lock @@ -125,7 +125,7 @@ }, "apps/desktop": { "name": "@superset/desktop", - "version": "0.0.58", + "version": "0.0.59", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", @@ -219,6 +219,7 @@ "strip-ansi": "^7.1.2", "superjson": "^2.2.5", "tailwind-merge": "^3.4.0", + "tree-kill": "^1.2.2", "trpc-electron": "^0.1.2", "tw-animate-css": "^1.4.0", "zod": "^4.3.5", @@ -4409,6 +4410,8 @@ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trim-newlines": ["trim-newlines@4.1.1", "", {}, "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ=="],