Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions src/server/process/PosixProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,30 @@ export class PosixProcess implements OpenCodeProcess {
}

async verifyCommand(command: string): Promise<string | null> {
// 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(
Expand Down
17 changes: 14 additions & 3 deletions src/server/process/WindowsProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,22 @@ export class WindowsProcess implements OpenCodeProcess {
}

async verifyCommand(command: string): Promise<string | null> {
// 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}'`;
}
}
Expand Down
6 changes: 3 additions & 3 deletions tests/ServerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
9 changes: 7 additions & 2 deletions tests/process/PosixProcess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand All @@ -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");
Expand Down
Loading