Skip to content
Open
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
28 changes: 25 additions & 3 deletions service/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,23 @@ async function sendCommand(serverUrl, sessionId, directory, parsedCommand, optio
*/
async function getOpencodePorts() {
try {
// Use full path to lsof since /usr/sbin may not be in PATH in all contexts
const lsofPaths = [
'/usr/sbin/lsof',
'/usr/bin/lsof',
'/bin/lsof',
'/sbin/lsof'
];
let lsofBin = 'lsof';
for (const p of lsofPaths) {
if (existsSync(p)) {
lsofBin = p;
break;
}
}
Comment on lines +196 to +202

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Check that the selected lsof path is executable.
existsSync() can pick a directory or non-executable file, and execSync() will then fail without trying the remaining candidates or the PATH fallback. Use accessSync(path, X_OK) or keep iterating until one candidate can actually run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/actions.js` around lines 196 - 202, Update the lsof candidate
selection loop to validate each path with executable permissions, using
accessSync and X_OK alongside existsSync or an equivalent executable check. Only
assign and break on a runnable candidate; otherwise continue through lsofPaths
so the existing default lsof PATH fallback remains available.


// Use full path to lsof since standard paths may not be in PATH in all contexts
// (e.g., when running as a service or from certain shell environments)
const output = execSync('/usr/sbin/lsof -i -P 2>/dev/null | grep -E "opencode.*LISTEN" || true', {
const output = execSync(`${lsofBin} -i -P 2>/dev/null | grep -E "opencode.*LISTEN" || true`, {
encoding: 'utf-8',
timeout: 30000
});
Expand Down Expand Up @@ -290,7 +304,15 @@ export async function discoverOpencodeServer(targetDir, options = {}) {
const fetchFn = options.fetch || fetch;
const preferredPort = options.preferredPort ?? getServerPort();

const ports = await getPorts();
let ports = await getPorts();

// If a preferredPort is configured but not already detected via lsof
// (e.g. because lsof couldn't find/see it, or lsof isn't available),
// append it to the ports list so that we still attempt to discover it.
if (preferredPort && !ports.includes(preferredPort)) {
ports = [...ports, preferredPort];
}

if (ports.length === 0) {
debug('discoverOpencodeServer: no servers found');
return null;
Expand Down
23 changes: 23 additions & 0 deletions test/unit/actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,29 @@ Check for bugs and security issues.`;

assert.strictEqual(result, null, 'Should return null when server returns unhealthy project data');
});

test('checks configured preferredPort even when getPorts returns no ports', async () => {
const { discoverOpencodeServer } = await import('../../service/actions.js');

const mockPorts = async () => [];
const mockFetch = async (url) => {
if (url === 'http://localhost:4096/project/current') {
return {
ok: true,
json: async () => ({ id: 'preferred-proj', worktree: '/Users/test/project', sandboxes: [], time: { created: 1 } })
};
}
return { ok: false };
};

const result = await discoverOpencodeServer('/Users/test/project', {
getPorts: mockPorts,
fetch: mockFetch,
preferredPort: 4096
});

assert.strictEqual(result, 'http://localhost:4096');
});
});

describe('executeAction', () => {
Expand Down
Loading