From 598f19bab2ec090547a646db4360ef197e5ebf25 Mon Sep 17 00:00:00 2001 From: yangyangyanghub <56469744+yangyangyanghub@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:07:42 +0800 Subject: [PATCH] Fix OpenCode process cleanup on shutdown (#1) Use synchronous shutdown cleanup and terminate Windows process trees so plugin-started opencode serve processes do not remain after Obsidian exits. Co-authored-by: Admin Co-authored-by: Claude Opus 4.8 --- src/main.ts | 16 +++--- src/server/ServerManager.ts | 14 +++++ src/server/process/OpenCodeProcess.ts | 3 ++ src/server/process/PosixProcess.ts | 13 +++++ src/server/process/WindowsProcess.ts | 74 +++++---------------------- 5 files changed, 53 insertions(+), 67 deletions(-) diff --git a/src/main.ts b/src/main.ts index c925699..c8de6b7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,7 +42,7 @@ export default class OpenCodePlugin extends Plugin { await this.saveData(this.settings); this.refreshClientState(); if (this.getServerState() === "running") { - await this.stopServer(); + await this.stopServer(false); await this.startServer(); } }); @@ -117,7 +117,7 @@ export default class OpenCodePlugin extends Plugin { id: "start-opencode-server", name: "Start OpenCode server", callback: () => { - this.startServer(); + void this.startServer(); }, }); @@ -125,7 +125,7 @@ export default class OpenCodePlugin extends Plugin { id: "stop-opencode-server", name: "Stop OpenCode server", callback: () => { - this.stopServer(); + void this.stopServer(); }, }); @@ -149,7 +149,7 @@ export default class OpenCodePlugin extends Plugin { async onunload(): Promise { this.contextManager.destroy(); - await this.stopServer(); + await this.stopServer(false); this.app.workspace.detachLeavesOfType(OPENCODE_VIEW_TYPE); } @@ -210,9 +210,11 @@ export default class OpenCodePlugin extends Plugin { return success; } - async stopServer(): Promise { + async stopServer(showNotice = true): Promise { await this.processManager.stop(); - new Notice("OpenCode server stopped"); + if (showNotice) { + new Notice("OpenCode server stopped"); + } } getServerState(): ServerState { @@ -294,7 +296,7 @@ export default class OpenCodePlugin extends Plugin { this.registerEvent( this.app.workspace.on("quit", () => { console.log("[OpenCode] Obsidian quitting - performing sync cleanup"); - this.stopServer(); + this.processManager.stopSync(); }) ); } diff --git a/src/server/ServerManager.ts b/src/server/ServerManager.ts index aac8c92..9d6eb43 100644 --- a/src/server/ServerManager.ts +++ b/src/server/ServerManager.ts @@ -210,6 +210,20 @@ export class ServerManager extends EventEmitter { await this.processImpl.stop(proc); } + stopSync(): void { + if (!this.process) { + this.setState("stopped"); + return; + } + + const proc = this.process; + + this.setState("stopped"); + this.process = null; + + this.processImpl.stopSync(proc); + } + private setState(state: ServerState): void { this.state = state; this.emit("stateChange", state); diff --git a/src/server/process/OpenCodeProcess.ts b/src/server/process/OpenCodeProcess.ts index 60c3642..3e10692 100644 --- a/src/server/process/OpenCodeProcess.ts +++ b/src/server/process/OpenCodeProcess.ts @@ -13,6 +13,9 @@ export interface OpenCodeProcess { * Handles all PID/process tree logic internally. */ stop(process: ChildProcess): Promise; + /** Stop the process synchronously during application shutdown. */ + stopSync(process: ChildProcess): void; + /** Verify that command exists and is executable. Returns error message or null if OK. */ verifyCommand(command: string): Promise; } diff --git a/src/server/process/PosixProcess.ts b/src/server/process/PosixProcess.ts index 56c308f..d2709ca 100644 --- a/src/server/process/PosixProcess.ts +++ b/src/server/process/PosixProcess.ts @@ -44,6 +44,19 @@ export class PosixProcess implements OpenCodeProcess { } } + stopSync(proc: ChildProcess): void { + const pid = proc.pid; + if (!pid) { + return; + } + + try { + process.kill(-pid, "SIGKILL"); + } catch { + // 进程可能已经退出 + } + } + async verifyCommand(command: string): Promise { // Check if command is absolute path - verify it exists and is executable if (command.startsWith('/') || command.startsWith('./')) { diff --git a/src/server/process/WindowsProcess.ts b/src/server/process/WindowsProcess.ts index 02563d7..de86df4 100644 --- a/src/server/process/WindowsProcess.ts +++ b/src/server/process/WindowsProcess.ts @@ -33,43 +33,25 @@ export class WindowsProcess implements OpenCodeProcess { console.log("[OpenCode] Stopping server process tree, 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 - } - } - } + await this.execAsync(`taskkill /F /T /PID ${pid}`); } 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 + WindowsProcess.currentProcess = null; + await this.waitForExit(process, 250); + } + + stopSync(process: ChildProcess): void { + const pid = process.pid; + if (!pid) { + WindowsProcess.currentProcess = null; + return; } - // Clear stored process + WindowsProcess.killProcessSync(pid); WindowsProcess.currentProcess = null; - - // Wait for process to exit - await this.waitForExit(process, 5000); } private static registerCleanupHandler(): void { @@ -92,37 +74,9 @@ export class WindowsProcess implements OpenCodeProcess { 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 - } + execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore" }); } catch { - // Process may already be gone + // 进程可能已经退出 } }