Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions apps/server/src/preview/PortScanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,21 @@ const TestPortDiscoveryLive = PortScanner.layer.pipe(
),
);

const LSOF_TEST_PORT = 43_123;
const LSOF_TEST_PORT = 5173;
const FOREIGN_LISTENER_PORT = 63_261;

const makeLsofScannerLayer = (input: {
readonly pid: () => number;
readonly fetch: typeof globalThis.fetch;
readonly port?: number;
}) =>
PortScanner.layer.pipe(
Layer.provide(
Layer.mergeAll(
Layer.succeed(ProcessRunner.ProcessRunner, {
run: () =>
Effect.succeed({
stdout: `p${input.pid()}\ncnode\nn*:${LSOF_TEST_PORT}\n`,
stdout: `p${input.pid()}\ncnode\nn*:${input.port ?? LSOF_TEST_PORT}\n`,
stderr: "",
code: null,
timedOut: false,
Expand Down Expand Up @@ -244,6 +246,54 @@ effectIt.layer(TestPortDiscoveryLive)("PortDiscovery integration (TCP probe fall
);
});

effectIt.effect(
"does not HTTP-probe a discovered listener that is not a curated port or a T3 terminal",
() => {
const requests: string[] = [];
const fetchFn = ((input: Parameters<typeof globalThis.fetch>[0]) => {
requests.push(String(input));
return Promise.resolve(new Response("hello", { headers: { "content-type": "text/html" } }));
}) as typeof globalThis.fetch;
const layer = makeLsofScannerLayer({
pid: () => 9999,
port: FOREIGN_LISTENER_PORT,
fetch: fetchFn,
});

return Effect.gen(function* () {
const scanner = yield* PortScanner.PortDiscovery;
expect(yield* scanner.scan()).toEqual([]);
expect(requests).toEqual([]);
}).pipe(Effect.provide(layer));
},
);

effectIt.effect("HTTP-probes a high-numbered listener owned by a registered T3 terminal", () => {
const requests: string[] = [];
const fetchFn = ((input: Parameters<typeof globalThis.fetch>[0]) => {
requests.push(String(input));
return Promise.resolve(new Response("hello", { headers: { "content-type": "text/html" } }));
}) as typeof globalThis.fetch;
const layer = makeLsofScannerLayer({
pid: () => 9999,
port: FOREIGN_LISTENER_PORT,
fetch: fetchFn,
});

return Effect.gen(function* () {
const scanner = yield* PortScanner.PortDiscovery;
yield* scanner.registerTerminalProcesses({
threadId: "thread-preview",
terminalId: "term-1",
processIds: [9999],
});
const servers = yield* scanner.scan();
expect(servers).toHaveLength(1);
expect(servers[0]?.port).toBe(FOREIGN_LISTENER_PORT);
expect(requests[0]).toBe(`http://localhost:${FOREIGN_LISTENER_PORT}/`);
}).pipe(Effect.provide(layer));
});

effectIt.effect("revalidates a successful HTML probe after its cache entry expires", () => {
let responds = true;
const requests: string[] = [];
Expand Down
9 changes: 8 additions & 1 deletion apps/server/src/preview/PortScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
* the shared Net service.
*
* Listening ports are published only after a bounded HTTP(S) probe finds a
* successful HTML document or a redirect to one.
* successful HTML document or a redirect to one. Discovered listeners are
* probed only when they sit on a curated dev port, belong to a registered T3
* terminal, or were named in a configured Preview URL. Other sockets are left
* alone so binary RPC listeners are not sent HTTP.
* Positive and negative results are cached briefly by candidate URL and listener identity,
* limiting repeated requests without leaving stale classifications around.
*
Expand Down Expand Up @@ -70,6 +73,9 @@ export const COMMON_DEV_PORTS: ReadonlyArray<number> = Object.freeze([
3000, 3001, 3333, 4173, 4200, 4321, 5000, 5173, 5174, 5175, 5500, 8000, 8080, 8081, 8888, 9000,
]);

const isEligibleDiscoveredWebProbe = (server: DiscoveredLocalServer): boolean =>
COMMON_DEV_PORTS.includes(server.port) || server.terminal !== null;

const POLL_INTERVAL = Duration.seconds(3);
const LSOF_TIMEOUT_MS = 5_000;
const WINDOWS_LISTENER_TIMEOUT_MS = 5_000;
Expand Down Expand Up @@ -380,6 +386,7 @@ export const make = Effect.gen(function* PortDiscoveryMake() {
}

for (const server of servers) {
if (!isEligibleDiscoveredWebProbe(server)) continue;
groups.push({
server,
urls: [`http://${server.host}:${server.port}`, `https://${server.host}:${server.port}`],
Expand Down
Loading