Skip to content

Commit 69e154b

Browse files
mweinbachclaude
andcommitted
fix(server): ignore partial Windows netstat snapshots on nonzero exit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 69c800f commit 69e154b

2 files changed

Lines changed: 53 additions & 2 deletions

File tree

‎apps/server/src/preview/PortScanner.test.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,3 +880,46 @@ effectIt.effect(
880880
}).pipe(Effect.provide(layer));
881881
},
882882
);
883+
884+
effectIt.effect("ignores a partial Windows netstat snapshot when one query exits nonzero", () => {
885+
const fetchFn = (() =>
886+
Promise.resolve(
887+
new Response("hello", { headers: { "content-type": "text/html" } }),
888+
)) as typeof globalThis.fetch;
889+
const layer = PortScanner.layer.pipe(
890+
Layer.provide(
891+
Layer.mergeAll(
892+
layerNoProc,
893+
Layer.succeed(ProcessRunner.ProcessRunner, {
894+
run: (input) => {
895+
const isIpv6 = input.args.includes("tcpv6");
896+
return Effect.succeed({
897+
stdout: isIpv6
898+
? ""
899+
: " TCP 127.0.0.1:5173 0.0.0.0:0 LISTENING 1234\r\n",
900+
stderr: "",
901+
code: isIpv6 ? ChildProcessSpawner.ExitCode(1) : null,
902+
timedOut: false,
903+
stdoutTruncated: false,
904+
stderrTruncated: false,
905+
stdoutInvalidUtf8: false,
906+
stderrInvalidUtf8: false,
907+
});
908+
},
909+
}),
910+
Layer.succeed(HostProcess.Platform, "win32"),
911+
FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn))),
912+
),
913+
),
914+
);
915+
916+
return Effect.gen(function* () {
917+
const scanner = yield* PortScanner.PortDiscovery;
918+
yield* scanner.registerTerminalProcesses({
919+
threadId: "scanner-thread",
920+
terminalId: "scanner-terminal",
921+
processIds: [1234],
922+
});
923+
expect(yield* scanner.scan()).toEqual([]);
924+
}).pipe(Effect.provide(layer));
925+
});

‎apps/server/src/preview/PortScanner.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,10 @@ const parseWindowsListenerLine = (
304304
};
305305
};
306306

307+
/** `ProcessRunner` reports a nonzero exit as a result, not a failure. */
308+
const exitedWithError = (result: ProcessRunner.ProcessRunOutput): boolean =>
309+
result.code !== null && result.code !== 0;
310+
307311
/** Reads the image name from `tasklist.exe /FO CSV /NH /FI "PID eq <pid>"` output. */
308312
const parseTasklistProcessName = (raw: string, pid: number): string | null => {
309313
for (const line of raw.split(/\r?\n/g)) {
@@ -694,7 +698,7 @@ export const make = Effect.gen(function* PortDiscoveryMake() {
694698
.pipe(
695699
// Only a completed lookup is cached; failures retry on the next scan.
696700
Effect.map((result) =>
697-
result.code !== null && result.code !== 0
701+
exitedWithError(result)
698702
? []
699703
: [
700704
[
@@ -752,8 +756,12 @@ export const make = Effect.gen(function* PortDiscoveryMake() {
752756
const listeners = yield* Effect.all([runNetstat("tcp"), runNetstat("tcpv6")], {
753757
concurrency: 2,
754758
}).pipe(
759+
// One failed query leaves a partial snapshot that could hide another process sharing a
760+
// port across IPv4/IPv6, so fall back to configured URLs instead.
755761
Effect.map(([tcp4, tcp6]) =>
756-
parseWindowsListenerOutput(`${tcp4.stdout}\n${tcp6.stdout}`, terminalByProcessId),
762+
exitedWithError(tcp4) || exitedWithError(tcp6)
763+
? null
764+
: parseWindowsListenerOutput(`${tcp4.stdout}\n${tcp6.stdout}`, terminalByProcessId),
757765
),
758766
Effect.catchTags({
759767
ProcessSpawnError: recoverWindowsProbeFailure,

0 commit comments

Comments
 (0)