authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:14:02-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:14:02-07:00
loga76b7bbfb5ad1e87714d85516e68bda594f9f830
tree6a1ab1d9ee611931d91c579bcdb2b64db25fdc96
parenta51691a9b0a98e65f0e9b2b775b22a2382595e87
signature Signed by PGP key B5690EEEBB952194

Preserve PortScanner probe defects (#3282)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 94 insertions(+), 4 deletions(-)

apps/server/src/preview/PortScanner.test.ts+70-2
......@@ -3,14 +3,48 @@ import * as NodeNet from "node:net";
33import { it as effectIt } from "@effect/vitest";
44import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
55import * as Net from "@t3tools/shared/Net";
6import { Effect, Layer } from "effect";
6import * as Cause from "effect/Cause";
7import * as Effect from "effect/Effect";
8import * as Exit from "effect/Exit";
9import * as Layer from "effect/Layer";
10import * as PlatformError from "effect/PlatformError";
711import { expect } from "vite-plus/test";
812
913import * as ProcessRunner from "../processRunner.ts";
1014import * as PortScanner from "./PortScanner.ts";
1115const TestProcessRunner = Layer.succeed(ProcessRunner.ProcessRunner, {
12 run: () => Effect.die("ProcessRunner should not be used by Windows TCP probe tests"),
16 run: (input) =>
17 Effect.fail(
18 new ProcessRunner.ProcessSpawnError({
19 command: input.command,
20 argumentCount: input.args.length,
21 cwd: input.cwd,
22 cause: PlatformError.systemError({
23 _tag: "NotFound",
24 module: "ChildProcess",
25 method: "spawn",
26 description: "PowerShell is not installed in the test environment",
27 }),
28 }),
29 ),
1330});
31
32const makeProbeFailureLayer = (run: ProcessRunner.ProcessRunner["Service"]["run"]) =>
33 PortScanner.layer.pipe(
34 Layer.provide(
35 Layer.mergeAll(
36 Layer.succeed(ProcessRunner.ProcessRunner, { run }),
37 Layer.succeed(Net.NetService, {
38 canListenOnHost: () => Effect.succeed(true),
39 isPortAvailableOnLoopback: () => Effect.succeed(true),
40 reserveLoopbackPort: () => Effect.succeed(40_000),
41 findAvailablePort: (preferred) => Effect.succeed(preferred),
42 }),
43 Layer.succeed(HostProcessPlatform, "linux"),
44 ),
45 ),
46 );
47
1448const TestPortDiscoveryLive = PortScanner.layer.pipe(
1549 Layer.provide(
1650 Layer.mergeAll(TestProcessRunner, Net.layer, Layer.succeed(HostProcessPlatform, "win32")),
......@@ -87,3 +121,37 @@ effectIt.layer(TestPortDiscoveryLive)("PortDiscovery integration (TCP probe fall
87121 }),
88122 );
89123});
124
125effectIt("does not swallow process probe defects", () =>
126 Effect.gen(function* () {
127 const defect = new Error("unexpected process probe defect");
128 const layer = makeProbeFailureLayer(() => Effect.die(defect));
129
130 const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe(
131 Effect.provide(layer),
132 Effect.exit,
133 );
134
135 expect(Exit.isFailure(exit)).toBe(true);
136 if (Exit.isFailure(exit)) {
137 expect(Cause.hasDies(exit.cause)).toBe(true);
138 expect(Cause.squash(exit.cause)).toBe(defect);
139 }
140 }),
141);
142
143effectIt("does not swallow process probe interruption", () =>
144 Effect.gen(function* () {
145 const layer = makeProbeFailureLayer(() => Effect.interrupt);
146
147 const exit = yield* Effect.flatMap(PortScanner.PortDiscovery, (scanner) => scanner.scan()).pipe(
148 Effect.provide(layer),
149 Effect.exit,
150 );
151
152 expect(Exit.isFailure(exit)).toBe(true);
153 if (Exit.isFailure(exit)) {
154 expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true);
155 }
156 }),
157);
apps/server/src/preview/PortScanner.ts+24-2
......@@ -221,6 +221,14 @@ export const make = Effect.gen(function* PortDiscoveryMake() {
221221 }));
222222 });
223223
224 const recoverProcessProbeFailure =
225 (probe: "lsof" | "windows-listeners") => (error: ProcessRunner.ProcessRunError) =>
226 Effect.logDebug("preview port process probe failed; falling back to common-port probes", {
227 cause: error,
228 probe,
229 platform: hostPlatform,
230 }).pipe(Effect.as(null));
231
224232 const scanOnce = Effect.fn("PortDiscovery.scan")(function* () {
225233 const state = yield* Ref.get(stateRef);
226234 const terminalByProcessId = new Map<number, TerminalProcessOwner>();
......@@ -230,6 +238,7 @@ export const make = Effect.gen(function* PortDiscoveryMake() {
230238 }
231239 }
232240 if (hostPlatform === "win32") {
241 const recoverWindowsProbeFailure = recoverProcessProbeFailure("windows-listeners");
233242 const command =
234243 'Get-NetTCPConnection -State Listen -ErrorAction Stop | ForEach-Object { $processName = (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName; Write-Output "$($_.LocalAddress)|$($_.LocalPort)|$($_.OwningProcess)|$processName" }';
235244 const listeners = yield* processRunner
......@@ -242,11 +251,18 @@ export const make = Effect.gen(function* PortDiscoveryMake() {
242251 })
243252 .pipe(
244253 Effect.map((result) => parseWindowsListenerOutput(result.stdout, terminalByProcessId)),
245 Effect.catchCause(() => Effect.succeed(null)),
254 Effect.catchTags({
255 ProcessSpawnError: recoverWindowsProbeFailure,
256 ProcessStdinError: recoverWindowsProbeFailure,
257 ProcessOutputLimitError: recoverWindowsProbeFailure,
258 ProcessReadError: recoverWindowsProbeFailure,
259 ProcessTimeoutError: recoverWindowsProbeFailure,
260 }),
246261 );
247262 if (listeners !== null) return listeners;
248263 return yield* probeCommonPorts();
249264 }
265 const recoverLsofProbeFailure = recoverProcessProbeFailure("lsof");
250266 const lsofResult = yield* processRunner
251267 .run({
252268 command: "lsof",
......@@ -257,7 +273,13 @@ export const make = Effect.gen(function* PortDiscoveryMake() {
257273 })
258274 .pipe(
259275 Effect.map((result) => parseLsofOutput(result.stdout, terminalByProcessId)),
260 Effect.catchCause(() => Effect.succeed(null)),
276 Effect.catchTags({
277 ProcessSpawnError: recoverLsofProbeFailure,
278 ProcessStdinError: recoverLsofProbeFailure,
279 ProcessOutputLimitError: recoverLsofProbeFailure,
280 ProcessReadError: recoverLsofProbeFailure,
281 ProcessTimeoutError: recoverLsofProbeFailure,
282 }),
261283 );
262284 if (lsofResult !== null) return lsofResult;
263285 return yield* probeCommonPorts();