From 2a824b6d19318ecf00cf2b83e6be634cbfbec7f2 Mon Sep 17 00:00:00 2001 From: Gerkinfeltser Date: Thu, 19 Feb 2026 10:55:37 -0600 Subject: [PATCH] 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 --- src/server/process/WindowsProcess.ts | 102 ++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/src/server/process/WindowsProcess.ts b/src/server/process/WindowsProcess.ts index f905500..915bcb0 100644 --- a/src/server/process/WindowsProcess.ts +++ b/src/server/process/WindowsProcess.ts @@ -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 { 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 { // Use 'where' command to check if executable exists in PATH try {