fix: properly cleanup Windows process tree on Obsidian exit

### Fixed
- Replaced unreliable taskkill /T with PowerShell Get-CimInstance for child process detection in WindowsProcess.ts
- Fixed orphaned node.exe processes when Obsidian closes by killing child processes before parent
- Added proper cleanup when shell: true creates cmd.exe -> node.exe process tree

### Added
- Static currentProcess field to track active process for cleanup during window close
- Static cleanupHandlerRegistered flag to prevent duplicate event handlers
- beforeunload event handler for synchronous cleanup when Obsidian window closes
- killProcessSync method for immediate process termination without async delays
- registerCleanupHandler method to set up window close event listener

### Changed
- Updated start method to store process reference and register cleanup handler
- Modified stop method to use PowerShell child lookup before killing parent process
- Enhanced error handling with try/catch blocks for PowerShell and taskkill operations
This commit is contained in:
Gerkinfeltser
2026-02-19 10:55:37 -06:00
committed by Mateusz Tymek
parent 2c17c9e7f6
commit 2a824b6d19

View File

@@ -2,33 +2,129 @@ import { ChildProcess, spawn, SpawnOptions } from "child_process";
import { OpenCodeProcess } from "./OpenCodeProcess";
export class WindowsProcess implements OpenCodeProcess {
// Static state to track the current process for cleanup
private static currentProcess: ChildProcess | null = null;
private static cleanupHandlerRegistered = false;
start(
command: string,
args: string[],
options: SpawnOptions
): ChildProcess {
return spawn(command, args, {
const process = spawn(command, args, {
...options,
shell: true,
windowsHide: true,
});
// Store process for cleanup
WindowsProcess.currentProcess = process;
WindowsProcess.registerCleanupHandler();
return process;
}
async stop(process: ChildProcess): Promise<void> {
const pid = process.pid;
if (!pid) {
WindowsProcess.currentProcess = null;
return;
}
console.log("[OpenCode] Stopping server process tree, PID:", pid);
// Use taskkill with /T flag to kill process tree
await this.execAsync(`taskkill /T /F /PID ${pid}`);
// Method 1: Find and kill child processes (actual node.exe) using PowerShell
// This is necessary because shell: true spawns cmd.exe -> node.exe, and
// killing cmd.exe leaves node.exe orphaned
try {
const { execSync } = require("child_process");
const output = execSync(
`powershell -Command "Get-CimInstance Win32_Process -Filter \\"ParentProcessId=${pid}\\" | Select-Object ProcessId"`,
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
);
const lines = output.split("\n").slice(3); // Skip headers
for (const line of lines) {
const childPid = line.trim();
if (childPid && !isNaN(parseInt(childPid))) {
try {
execSync(`taskkill /F /PID ${childPid}`, { stdio: "ignore" });
} catch {
// Child may already be gone
}
}
}
} catch {
// PowerShell lookup failed, continue to other methods
}
// Method 2: Kill the parent process (cmd.exe)
try {
await this.execAsync(`taskkill /F /PID ${pid}`);
} catch {
// Parent may already be gone
}
// Clear stored process
WindowsProcess.currentProcess = null;
// Wait for process to exit
await this.waitForExit(process, 5000);
}
private static registerCleanupHandler(): void {
if (WindowsProcess.cleanupHandlerRegistered) {
return;
}
// Register beforeunload handler for window close cleanup
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", () => {
if (WindowsProcess.currentProcess?.pid) {
WindowsProcess.killProcessSync(WindowsProcess.currentProcess.pid);
}
});
WindowsProcess.cleanupHandlerRegistered = true;
}
}
private static killProcessSync(pid: number): void {
try {
const { execSync } = require("child_process");
// Method 1: Kill child processes using PowerShell
try {
const output = execSync(
`powershell -Command "Get-CimInstance Win32_Process -Filter \\"ParentProcessId=${pid}\\" | Select-Object ProcessId"`,
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
);
const lines = output.split("\n").slice(3);
for (const line of lines) {
const childPid = line.trim();
if (childPid && !isNaN(parseInt(childPid))) {
try {
execSync(`taskkill /F /PID ${childPid}`, { stdio: "ignore" });
} catch {
// Child may already be gone
}
}
}
} catch {
// PowerShell lookup failed
}
// Method 2: Kill parent process
try {
execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore" });
} catch {
// Parent may already be gone
}
} catch {
// Process may already be gone
}
}
async verifyCommand(command: string): Promise<string | null> {
// Use 'where' command to check if executable exists in PATH
try {