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)];
|
||||
},
|
||||
// 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.
|
||||
|
||||
@@ -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: '' };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user