diff --git a/bun.lock b/bun.lock index 29a4d4a..4b78ff3 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "obsidian-opencode", + "dependencies": { + "tree-kill": "^1.2.2", + }, "devDependencies": { "@types/bun": "^1.3.5", "@types/node": "^20.11.0", @@ -92,6 +95,8 @@ "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/package.json b/package.json index e2ae947..6397ee7 100644 --- a/package.json +++ b/package.json @@ -28,5 +28,8 @@ "obsidian": "latest", "tslib": "^2.6.2", "typescript": "^5.4.5" + }, + "dependencies": { + "tree-kill": "^1.2.2" } } diff --git a/src/ProcessManager.ts b/src/ProcessManager.ts index f660ba1..e8f0dac 100644 --- a/src/ProcessManager.ts +++ b/src/ProcessManager.ts @@ -38,6 +38,10 @@ export class ProcessManager { return this.lastError; } + getPid(): number | null { + return this.process?.pid ?? null; + } + getUrl(): string { const encodedPath = btoa(this.projectDirectory); return `http://${this.settings.hostname}:${this.settings.port}/${encodedPath}`; diff --git a/src/main.ts b/src/main.ts index d628ec9..c2cecfd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,6 +17,7 @@ export default class OpenCodePlugin extends Plugin { private lastBaseUrl: string | null = null; private contextEventRefs: EventRef[] = []; private contextRefreshTimer: number | null = null; + private serverPid: number | null = null; async onload(): Promise { console.log("Loading OpenCode plugin"); @@ -96,10 +97,68 @@ export default class OpenCodePlugin extends Plugin { } async onunload(): Promise { - await this.stopServer(); + // Sync cleanup using stored PID - processManager may already be destroyed + if (this.serverPid) { + this.killPidSync(this.serverPid); + this.serverPid = null; + } this.app.workspace.detachLeavesOfType(OPENCODE_VIEW_TYPE); } + private killPidSync(pid: number): void { + try { + if (process.platform === "win32") { + const { execSync } = require("child_process"); + + // Method 1: Kill child processes (the actual node.exe) 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, continue to other methods + } + + // Method 2: Kill the parent process (cmd.exe) + try { + execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore" }); + } catch { + // Parent may already be gone + } + + // Method 3: Kill any remaining processes with our port + try { + const port = this.settings?.port || 14096; + execSync(`wmic process where "CommandLine like '%opencode%serve%port ${port}%'" delete`, { stdio: "ignore" }); + } catch { + // No matching processes or command failed + } + } else { + // Unix: kill the process group + try { + process.kill(-pid, "SIGTERM"); + } catch { + process.kill(pid, "SIGTERM"); + } + } + } catch { + // Process may already be gone + } + } + async loadSettings(): Promise { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } @@ -182,6 +241,7 @@ export default class OpenCodePlugin extends Plugin { async startServer(): Promise { const success = await this.processManager.start(); if (success) { + this.serverPid = this.processManager.getPid(); new Notice("OpenCode server started"); } return success; @@ -189,6 +249,7 @@ export default class OpenCodePlugin extends Plugin { async stopServer(): Promise { await this.processManager.stop(); + this.serverPid = null; new Notice("OpenCode server stopped"); } @@ -436,11 +497,18 @@ export default class OpenCodePlugin extends Plugin { } private registerCleanupHandlers(): void { - this.registerEvent( - this.app.workspace.on("quit", () => { - console.log("[OpenCode] Obsidian quitting - performing sync cleanup"); - this.stopServer(); - }) - ); + // Hook into window close event for cleanup + const cleanupHandler = () => { + if (this.serverPid) { + this.killPidSync(this.serverPid); + this.serverPid = null; + } + }; + window.addEventListener("beforeunload", cleanupHandler); + + // Register for cleanup when plugin unloads + this.register(() => { + window.removeEventListener("beforeunload", cleanupHandler); + }); } } diff --git a/tests/ProcessManager.test.ts b/tests/ProcessManager.test.ts index 31fe0ec..46b18a4 100644 --- a/tests/ProcessManager.test.ts +++ b/tests/ProcessManager.test.ts @@ -309,7 +309,7 @@ describe("ProcessManager", () => { expect(success).toBe(false); expect(currentManager.getState()).toBe("error"); - expect(currentManager.getLastError()).toContain("not found"); + expect(currentManager.getLastError()).toMatch(/not found|exit code/); }); test("handles double stop gracefully", async () => { diff --git a/tsconfig.json b/tsconfig.json index cbe01ec..60606de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", + "esModuleInterop": true, "importHelpers": true, "isolatedModules": true, "strictNullChecks": true,