feat: implement process cleanup for OpenCode server and enhance internal function handling
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }> {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user