feat: implement process cleanup for OpenCode server and enhance internal function handling

This commit is contained in:
Bohdan Triapitsyn
2026-01-16 01:22:43 +02:00
parent 1ae18d4793
commit cf9b7a6516
4 changed files with 100 additions and 11 deletions
@@ -131,7 +131,6 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
wrapLines = false, wrapLines = false,
layout = 'fill', layout = 'fill',
}) => { }) => {
const isInlineLayout = layout === 'inline';
const { isMobile } = useDeviceInfo(); const { isMobile } = useDeviceInfo();
const { inputBarOffset, isKeyboardOpen } = useUIStore(); const { inputBarOffset, isKeyboardOpen } = useUIStore();
+1
View File
@@ -1746,6 +1746,7 @@ export const useEventStream = () => {
messageCache.clear(); messageCache.clear();
// eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally accessing current ref value at cleanup time // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally accessing current ref value at cleanup time
notifiedMessagesRef.current.clear(); notifiedMessagesRef.current.clear();
// eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally accessing current ref value at cleanup time
notifiedQuestionsRef.current.clear(); notifiedQuestionsRef.current.clear();
pendingResumeRef.current = false; pendingResumeRef.current = false;
+74 -7
View File
@@ -1,5 +1,6 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as os from 'os'; import * as os from 'os';
import { execSync } from 'child_process';
import { createOpencodeServer } from '@opencode-ai/sdk/server'; import { createOpencodeServer } from '@opencode-ai/sdk/server';
const READY_CHECK_TIMEOUT_MS = 30000; const READY_CHECK_TIMEOUT_MS = 30000;
@@ -146,6 +147,8 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
let apiPrefixDetected = false; let apiPrefixDetected = false;
let cliMissing = false; let cliMissing = false;
let pendingOperation: Promise<void> | null = null;
const config = vscode.workspace.getConfiguration('openchamber'); const config = vscode.workspace.getConfiguration('openchamber');
const configuredApiUrl = config.get<string>('apiUrl') || ''; const configuredApiUrl = config.get<string>('apiUrl') || '';
const useConfiguredUrl = configuredApiUrl && configuredApiUrl.trim().length > 0; const useConfiguredUrl = configuredApiUrl && configuredApiUrl.trim().length > 0;
@@ -189,7 +192,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
return null; return null;
}; };
async function start(workdir?: string): Promise<void> { async function startInternal(workdir?: string): Promise<void> {
startCount += 1; startCount += 1;
lastStartAt = Date.now(); lastStartAt = Date.now();
@@ -203,8 +206,16 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
return; return;
} }
// If server already running, don't spawn another
if (server) {
if (status !== 'connected') {
setStatus('connected');
}
return;
}
setStatus('connecting'); setStatus('connecting');
cliMissing = false; // Reset assumption on retry cliMissing = false;
detectedPort = null; detectedPort = null;
apiPrefix = ''; apiPrefix = '';
@@ -219,7 +230,6 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
const originalCwd = process.cwd(); const originalCwd = process.cwd();
try { try {
process.chdir(workingDirectory); process.chdir(workingDirectory);
// Let the SDK/OS choose a random available port (port: 0)
server = await createOpencodeServer({ server = await createOpencodeServer({
hostname: '127.0.0.1', hostname: '127.0.0.1',
port: 0, port: 0,
@@ -276,7 +286,9 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
} }
} }
async function stop(): Promise<void> { async function stopInternal(): Promise<void> {
const portToKill = detectedPort;
if (server) { if (server) {
try { try {
server.close(); server.close();
@@ -286,17 +298,72 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
server = null; server = null;
} }
// SDK's proc.kill() only kills the Node wrapper, not the actual opencode binary.
// Kill any process listening on our port to clean up orphaned children.
if (portToKill) {
try {
execSync(`lsof -ti:${portToKill} | xargs kill -9 2>/dev/null || true`, {
stdio: 'ignore',
timeout: 5000
});
} catch {
// Ignore - process may already be dead
}
}
managedApiUrlOverride = null; managedApiUrlOverride = null;
detectedPort = null; detectedPort = null;
setStatus('disconnected'); setStatus('disconnected');
} }
async function restart(): Promise<void> { async function restartInternal(): Promise<void> {
restartCount += 1; restartCount += 1;
await stop(); await stopInternal();
await new Promise(r => setTimeout(r, 250)); await new Promise(r => setTimeout(r, 250));
await start(); await startInternal();
}
async function start(workdir?: string): Promise<void> {
if (pendingOperation) {
await pendingOperation;
if (server) {
return;
}
}
pendingOperation = startInternal(workdir);
try {
await pendingOperation;
} finally {
pendingOperation = null;
}
}
async function stop(): Promise<void> {
if (pendingOperation) {
await pendingOperation;
}
// Check if already stopped
if (!server) {
return;
}
pendingOperation = stopInternal();
try {
await pendingOperation;
} finally {
pendingOperation = null;
}
}
async function restart(): Promise<void> {
if (pendingOperation) {
await pendingOperation;
}
pendingOperation = restartInternal();
try {
await pendingOperation;
} finally {
pendingOperation = null;
}
} }
async function setWorkingDirectory(newPath: string): Promise<{ success: boolean; restarted: boolean; path: string }> { async function setWorkingDirectory(newPath: string): Promise<{ success: boolean; restarted: boolean; path: string }> {
+25 -3
View File
@@ -1429,6 +1429,20 @@ function parseArgs(argv = process.argv.slice(2)) {
return options; return options;
} }
function killProcessOnPort(port) {
if (!port) return;
try {
// SDK's proc.kill() only kills the Node wrapper, not the actual opencode binary.
// Kill any process listening on our port to clean up orphaned children.
spawnSync('sh', ['-c', `lsof -ti:${port} | xargs kill -9 2>/dev/null || true`], {
stdio: 'ignore',
timeout: 5000
});
} catch {
// Ignore - process may already be dead
}
}
async function startOpenCode() { async function startOpenCode() {
const desiredPort = ENV_CONFIGURED_OPENCODE_PORT ?? 0; const desiredPort = ENV_CONFIGURED_OPENCODE_PORT ?? 0;
console.log( console.log(
@@ -1496,6 +1510,8 @@ async function restartOpenCode() {
openCodeNotReadySince = Date.now(); openCodeNotReadySince = Date.now();
console.log('Restarting OpenCode process...'); console.log('Restarting OpenCode process...');
const portToKill = openCodePort;
if (openCodeProcess) { if (openCodeProcess) {
console.log('Stopping existing OpenCode process...'); console.log('Stopping existing OpenCode process...');
try { try {
@@ -1505,11 +1521,13 @@ async function restartOpenCode() {
} }
openCodeProcess = null; openCodeProcess = null;
syncToHmrState(); syncToHmrState();
// Brief delay to allow port release
await new Promise((resolve) => setTimeout(resolve, 250));
} }
killProcessOnPort(portToKill);
// Brief delay to allow port release
await new Promise((resolve) => setTimeout(resolve, 250));
if (ENV_CONFIGURED_OPENCODE_PORT) { if (ENV_CONFIGURED_OPENCODE_PORT) {
console.log(`Using OpenCode port from environment: ${ENV_CONFIGURED_OPENCODE_PORT}`); console.log(`Using OpenCode port from environment: ${ENV_CONFIGURED_OPENCODE_PORT}`);
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT); setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
@@ -1917,6 +1935,8 @@ async function gracefulShutdown(options = {}) {
clearInterval(healthCheckInterval); clearInterval(healthCheckInterval);
} }
const portToKill = openCodePort;
if (openCodeProcess) { if (openCodeProcess) {
console.log('Stopping OpenCode process...'); console.log('Stopping OpenCode process...');
try { try {
@@ -1927,6 +1947,8 @@ async function gracefulShutdown(options = {}) {
openCodeProcess = null; openCodeProcess = null;
} }
killProcessOnPort(portToKill);
if (server) { if (server) {
await Promise.race([ await Promise.race([
new Promise((resolve) => { new Promise((resolve) => {