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:
@@ -1080,8 +1080,8 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
|
|||||||
return [...new Set(directories)];
|
return [...new Set(directories)];
|
||||||
},
|
},
|
||||||
// A managed restart can move OpenCode to a NEW port (the old one may stay
|
// 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
|
// occupied if killProcessOnPort/waitForPortRelease didn't free it in time,
|
||||||
// Windows). Rebind the message-stream upstream readers to the current port
|
// 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
|
// so the UI keeps receiving events instead of staying pinned to the old
|
||||||
// process (#2638). The runtime is created later by the startup pipeline;
|
// process (#2638). The runtime is created later by the startup pipeline;
|
||||||
// by the time any restart runs, it is assigned.
|
// by the time any restart runs, it is assigned.
|
||||||
|
|||||||
@@ -55,15 +55,25 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|||||||
|
|
||||||
const killProcessOnPortWin32 = (port) => {
|
const killProcessOnPortWin32 = (port) => {
|
||||||
try {
|
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 output = result.stdout || '';
|
||||||
const myPid = process.pid;
|
const myPid = process.pid;
|
||||||
const listeningPidPattern = /^\s*TCP\s+\S*:(\d+)\s+\S+\s+LISTENING\s+(\d+)\s*$/gim;
|
|
||||||
const pids = new Set();
|
const pids = new Set();
|
||||||
let match;
|
for (const line of output.split(/\r?\n/)) {
|
||||||
while ((match = listeningPidPattern.exec(output)) !== null) {
|
const pid = Number.parseInt(line.trim(), 10);
|
||||||
if (Number.parseInt(match[1], 10) !== port) continue;
|
|
||||||
const pid = Number.parseInt(match[2], 10);
|
|
||||||
if (pid && pid !== myPid) pids.add(pid);
|
if (pid && pid !== myPid) pids.add(pid);
|
||||||
}
|
}
|
||||||
for (const pid of pids) {
|
for (const pid of pids) {
|
||||||
|
|||||||
@@ -627,21 +627,12 @@ describe('killProcessOnPort on Windows', () => {
|
|||||||
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
|
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', () => {
|
it('force-kills the process listening on the target port via taskkill', () => {
|
||||||
setPlatform('win32');
|
setPlatform('win32');
|
||||||
const orphanPid = 54321;
|
const orphanPid = 54321;
|
||||||
spawnSyncMock.mockImplementation((cmd) => {
|
spawnSyncMock.mockImplementation((cmd) => {
|
||||||
if (cmd === 'netstat') {
|
if (cmd === 'powershell') {
|
||||||
return { stdout: netstatOutput(45678, orphanPid) };
|
return { stdout: `${orphanPid}\r\n` };
|
||||||
}
|
}
|
||||||
return { stdout: '' };
|
return { stdout: '' };
|
||||||
});
|
});
|
||||||
@@ -649,7 +640,11 @@ describe('killProcessOnPort on Windows', () => {
|
|||||||
const runtime = createRuntime();
|
const runtime = createRuntime();
|
||||||
runtime.killProcessOnPort(45678);
|
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(
|
expect(spawnSyncMock).toHaveBeenCalledWith(
|
||||||
'taskkill',
|
'taskkill',
|
||||||
['/PID', String(orphanPid), '/F'],
|
['/PID', String(orphanPid), '/F'],
|
||||||
@@ -660,8 +655,8 @@ describe('killProcessOnPort on Windows', () => {
|
|||||||
it('never force-kills its own process id', () => {
|
it('never force-kills its own process id', () => {
|
||||||
setPlatform('win32');
|
setPlatform('win32');
|
||||||
spawnSyncMock.mockImplementation((cmd) => {
|
spawnSyncMock.mockImplementation((cmd) => {
|
||||||
if (cmd === 'netstat') {
|
if (cmd === 'powershell') {
|
||||||
return { stdout: netstatOutput(45678, process.pid) };
|
return { stdout: `${process.pid}\r\n` };
|
||||||
}
|
}
|
||||||
return { stdout: '' };
|
return { stdout: '' };
|
||||||
});
|
});
|
||||||
@@ -675,8 +670,8 @@ describe('killProcessOnPort on Windows', () => {
|
|||||||
it('does nothing when no process is listening on the target port', () => {
|
it('does nothing when no process is listening on the target port', () => {
|
||||||
setPlatform('win32');
|
setPlatform('win32');
|
||||||
spawnSyncMock.mockImplementation((cmd) => {
|
spawnSyncMock.mockImplementation((cmd) => {
|
||||||
if (cmd === 'netstat') {
|
if (cmd === 'powershell') {
|
||||||
return { stdout: netstatOutput(9999, 54321) };
|
return { stdout: '' };
|
||||||
}
|
}
|
||||||
return { stdout: '' };
|
return { stdout: '' };
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user