fix: use Get-NetTCPConnection for locale-independent port lookup

The netstat-based parser matched the literal English "LISTENING"
state string, which is translated on non-English Windows (e.g.
"ABHÖREN", "ÉCOUTE", "ESCUTANDO"). On those systems the regex matched
zero lines, so killProcessOnPort silently did nothing -- fail-open,
not a regression, but ineffective for the exact users the fix targets.

Replace it with `Get-NetTCPConnection -State Listen -LocalPort <port>`,
which reads the same underlying WinNT API netstat's display layer
translates, so it's unaffected by OS display language. Verified
against a real listening port on this machine (matched the actual
owning PID).

Also fixed a stale duplicate of the "killProcessOnPort is a no-op on
Windows" comment left behind in server/index.js.
This commit is contained in:
Sérgio Pedro
2026-08-12 11:50:39 +01:00
parent d06329bca1
commit a1a1cfb93d
3 changed files with 29 additions and 24 deletions
+2 -2
View File
@@ -1080,8 +1080,8 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
return [...new Set(directories)];
},
// A managed restart can move OpenCode to a NEW port (the old one may stay
// occupied by an orphaned process, e.g. killProcessOnPort is a no-op on
// Windows). Rebind the message-stream upstream readers to the current port
// occupied if killProcessOnPort/waitForPortRelease didn't free it in time,
// on any platform). Rebind the message-stream upstream readers to the current port
// so the UI keeps receiving events instead of staying pinned to the old
// process (#2638). The runtime is created later by the startup pipeline;
// by the time any restart runs, it is assigned.
+16 -6
View File
@@ -55,15 +55,25 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
const killProcessOnPortWin32 = (port) => {
try {
const result = spawnSync('netstat', ['-ano'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
// Get-NetTCPConnection reads the same locale-independent WinNT API
// netstat's display layer translates (e.g. "LISTENING" renders as
// "ABHÖREN"/"ÉCOUTE"/"ESCUTANDO" on non-English Windows), so this
// works regardless of the OS display language.
const result = spawnSync(
'powershell',
[
'-NoProfile',
'-NonInteractive',
'-Command',
`Get-NetTCPConnection -State Listen -LocalPort ${Number.parseInt(port, 10)} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`,
],
{ encoding: 'utf8', timeout: 5000, windowsHide: true }
);
const output = result.stdout || '';
const myPid = process.pid;
const listeningPidPattern = /^\s*TCP\s+\S*:(\d+)\s+\S+\s+LISTENING\s+(\d+)\s*$/gim;
const pids = new Set();
let match;
while ((match = listeningPidPattern.exec(output)) !== null) {
if (Number.parseInt(match[1], 10) !== port) continue;
const pid = Number.parseInt(match[2], 10);
for (const line of output.split(/\r?\n/)) {
const pid = Number.parseInt(line.trim(), 10);
if (pid && pid !== myPid) pids.add(pid);
}
for (const pid of pids) {
@@ -627,21 +627,12 @@ describe('killProcessOnPort on Windows', () => {
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
};
const netstatOutput = (port, pid) => [
'',
'Active Connections',
'',
' Proto Local Address Foreign Address State PID',
` TCP 0.0.0.0:${port} 0.0.0.0:0 LISTENING ${pid}`,
'',
].join('\r\n');
it('force-kills the process listening on the target port via taskkill', () => {
setPlatform('win32');
const orphanPid = 54321;
spawnSyncMock.mockImplementation((cmd) => {
if (cmd === 'netstat') {
return { stdout: netstatOutput(45678, orphanPid) };
if (cmd === 'powershell') {
return { stdout: `${orphanPid}\r\n` };
}
return { stdout: '' };
});
@@ -649,7 +640,11 @@ describe('killProcessOnPort on Windows', () => {
const runtime = createRuntime();
runtime.killProcessOnPort(45678);
expect(spawnSyncMock).toHaveBeenCalledWith('netstat', ['-ano'], expect.objectContaining({ windowsHide: true }));
expect(spawnSyncMock).toHaveBeenCalledWith(
'powershell',
expect.arrayContaining([expect.stringContaining('-LocalPort 45678')]),
expect.objectContaining({ windowsHide: true })
);
expect(spawnSyncMock).toHaveBeenCalledWith(
'taskkill',
['/PID', String(orphanPid), '/F'],
@@ -660,8 +655,8 @@ describe('killProcessOnPort on Windows', () => {
it('never force-kills its own process id', () => {
setPlatform('win32');
spawnSyncMock.mockImplementation((cmd) => {
if (cmd === 'netstat') {
return { stdout: netstatOutput(45678, process.pid) };
if (cmd === 'powershell') {
return { stdout: `${process.pid}\r\n` };
}
return { stdout: '' };
});
@@ -675,8 +670,8 @@ describe('killProcessOnPort on Windows', () => {
it('does nothing when no process is listening on the target port', () => {
setPlatform('win32');
spawnSyncMock.mockImplementation((cmd) => {
if (cmd === 'netstat') {
return { stdout: netstatOutput(9999, 54321) };
if (cmd === 'powershell') {
return { stdout: '' };
}
return { stdout: '' };
});