From 0cb1b28acc90ad7b7184eab11a33ce3f8d908cdb Mon Sep 17 00:00:00 2001 From: Mateusz Tymek Date: Sat, 14 Feb 2026 16:02:00 +0100 Subject: [PATCH] Process management improvements --- src/server/process/PosixProcess.ts | 24 ++++++++++++++++++------ src/server/process/WindowsProcess.ts | 17 ++++++++++++++--- tests/ServerManager.test.ts | 6 +++--- tests/process/PosixProcess.test.ts | 9 +++++++-- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/server/process/PosixProcess.ts b/src/server/process/PosixProcess.ts index 0a6c4f5..34e0c35 100644 --- a/src/server/process/PosixProcess.ts +++ b/src/server/process/PosixProcess.ts @@ -44,18 +44,30 @@ export class PosixProcess implements OpenCodeProcess { } async verifyCommand(command: string): Promise { - // Check if command is absolute path - verify it exists and is executable - if (command.startsWith('/') || command.startsWith('./')) { + // For absolute paths or relative paths, check file exists first + if (command.includes('/') || command.startsWith('.')) { + const fs = require('fs'); + if (!fs.existsSync(command)) { + return `Executable not found at '${command}'`; + } + // Also check it's executable try { - const fs = require('fs'); fs.accessSync(command, fs.constants.X_OK); - return null; } catch { return `Executable not found at '${command}'`; } + return null; + } + + // For PATH lookups, use 'command -v' which is POSIX standard + try { + const { execSync } = require('child_process'); + execSync(`command -v "${command}"`, { stdio: 'ignore', timeout: 5000 }); + return null; + } catch (error: any) { + // If we can't find it, report as not found + return `Executable not found at '${command}'`; } - // For non-absolute paths, let spawn handle it (will fire ENOENT if not found) - return null; } private async killProcessGroup( diff --git a/src/server/process/WindowsProcess.ts b/src/server/process/WindowsProcess.ts index 6eefddd..e78f6d9 100644 --- a/src/server/process/WindowsProcess.ts +++ b/src/server/process/WindowsProcess.ts @@ -30,11 +30,22 @@ export class WindowsProcess implements OpenCodeProcess { } async verifyCommand(command: string): Promise { - // Use 'where' command to check if executable exists in PATH + // For absolute paths or relative paths, check file exists first + if (command.includes('\\') || command.includes('/') || command.startsWith('.')) { + const fs = require('fs'); + if (!fs.existsSync(command)) { + return `Executable not found at '${command}'`; + } + return null; + } + + // For PATH lookups, use 'where' command try { - await this.execAsync(`where "${command}"`); + const { execSync } = require('child_process'); + execSync(`where "${command}"`, { stdio: 'ignore', timeout: 5000 }); return null; - } catch { + } catch (error: any) { + // If we can't find it, report as not found return `Executable not found at '${command}'`; } } diff --git a/tests/ServerManager.test.ts b/tests/ServerManager.test.ts index 3b8d6c5..effdcef 100644 --- a/tests/ServerManager.test.ts +++ b/tests/ServerManager.test.ts @@ -146,14 +146,14 @@ describe("ServerManager", () => { await currentManager.stop(); expect(currentManager.getState()).toBe("stopped"); - // Wait for process to fully terminate - await new Promise((resolve) => setTimeout(resolve, 1000)); + // Wait for process to fully terminate and port to be released + await new Promise((resolve) => setTimeout(resolve, 2000)); // Restart const secondStart = await currentManager.start(); expect(secondStart).toBe(true); expect(currentManager.getState()).toBe("running"); - }); + }, 10000); test("returns true immediately if already running", async () => { const port = getNextPort(); diff --git a/tests/process/PosixProcess.test.ts b/tests/process/PosixProcess.test.ts index 0ebb15f..9c92935 100644 --- a/tests/process/PosixProcess.test.ts +++ b/tests/process/PosixProcess.test.ts @@ -5,8 +5,8 @@ describe.skipIf(process.platform === "win32")("PosixProcess", () => { const processImpl = new PosixProcess(); describe("verifyCommand", () => { - test("returns null for non-absolute commands", async () => { - // Non-absolute paths should return null (let spawn handle it) + test("returns null for existing commands in PATH", async () => { + // 'ls' should exist on all POSIX systems const result = await processImpl.verifyCommand("ls"); expect(result).toBeNull(); }); @@ -24,6 +24,11 @@ describe.skipIf(process.platform === "win32")("PosixProcess", () => { expect(result).toContain(nonExistentPath); }); + test("returns error for non-existent command in PATH", async () => { + const result = await processImpl.verifyCommand("definitely-not-a-real-command-12345"); + expect(result).toContain("Executable not found"); + }); + test("returns error for non-executable file", async () => { // Test with a regular file that's not executable const result = await processImpl.verifyCommand("/etc/passwd");