Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

125 lines · 4.1 KBRaw History
1// Ports for the servers end-to-end tests start (`wrangler dev` and its inspector, the sandbox pool, ...).
2//
3// Asking the kernel for a free port (listen on 0, close, hand the number on) races: until the server
4// binds it, the port is free again, and anything that binds port 0 meanwhile can be given the same
5// number: workerd's own sockets in another `wrangler dev` or `wrangler d1 execute`, a docs server, an
6// outgoing connection. Then wrangler dies with "bind(): Address already in use". So ports here come
7// from below the kernel's ephemeral range, which port 0 binds and outgoing connections never use, and
8// each is reserved with a lock file naming this process, so test files running side by side
9// (--test-concurrency) never pick the same one. A process keeps its ports until it exits; a lock left
10// by a process that's gone is taken over.
11import { linkSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
12import { createServer } from "node:net";
13import { tmpdir } from "node:os";
14import { join } from "node:path";
15
16const LOCKS = join(tmpdir(), "diffinery-e2e-ports");
17const [FIRST, LAST] = range();
18const held = new Set();
19let next = FIRST + Math.floor(Math.random() * (LAST - FIRST + 1));
20
21process.on("exit", () => {
22 for (const port of held) {
23 try {
24 unlinkSync(join(LOCKS, String(port)));
25 } catch {}
26 }
27});
28
29/** A port on 127.0.0.1 that's free now and that no other caller, in this process or another, will be given. */
30export async function freePort() {
31 mkdirSync(LOCKS, { recursive: true });
32 for (let tried = 0; tried <= LAST - FIRST; tried++) {
33 const port = next;
34 next = next === LAST ? FIRST : next + 1;
35 if (held.has(port) || !reserve(port)) continue;
36 if (await bindable(port)) {
37 held.add(port);
38 return port;
39 }
40 // Something outside the tests is listening there; leave the port to it.
41 unlinkSync(join(LOCKS, String(port)));
42 }
43 throw new Error(`no free port left in ${FIRST}-${LAST}`);
44}
45
46/** Ports from 20000 up to the kernel's ephemeral range (32768 to 60999 on Linux by default; macOS's starts at 49152). */
47function range() {
48 let [low, high] = [32768, 60999];
49 try {
50 [low, high] = readFileSync("/proc/sys/net/ipv4/ip_local_port_range", "utf8").trim().split(/\s+/).map(Number);
51 } catch {}
52 if (low >= 21000) return [20000, low - 1];
53 // An ephemeral range starting this low leaves too few ports under it; use the ports above it.
54 if (high <= 64000) return [high + 1, 65535];
55 throw new Error(`the ephemeral port range (${low}-${high}) leaves no room for test servers`);
56}
57
58/** Takes the lock file for `port`. It's written whole under a name of its own, then linked into place, so it never exists half written. */
59function reserve(port) {
60 const file = join(LOCKS, String(port));
61 const mine = `${file}.${process.pid}`;
62 writeFileSync(mine, String(process.pid));
63 try {
64 for (let attempt = 0; attempt < 2; attempt++) {
65 try {
66 linkSync(mine, file);
67 return true;
68 } catch (e) {
69 if (e.code !== "EEXIST") throw e;
70 }
71 if (!takeOver(file)) return false;
72 }
73 return false;
74 } finally {
75 unlinkSync(mine);
76 }
77}
78
79/**
80 * Moves a lock whose process is gone (killed before it could clean up) out of the way. It's renamed
81 * before it's read again, so of two processes taking over the same lock only one moves it, and a live
82 * lock moved by mistake goes back.
83 */
84function takeOver(file) {
85 if (alive(pidIn(file))) return false;
86 const moved = `${file}.stale-${process.pid}`;
87 try {
88 renameSync(file, moved);
89 } catch {
90 return false;
91 }
92 const dead = !alive(pidIn(moved));
93 if (!dead) {
94 try {
95 linkSync(moved, file);
96 } catch {}
97 }
98 unlinkSync(moved);
99 return dead;
100}
101
102function pidIn(file) {
103 try {
104 return Number(readFileSync(file, "utf8"));
105 } catch {
106 return 0;
107 }
108}
109
110function alive(pid) {
111 if (!pid) return false;
112 try {
113 process.kill(pid, 0);
114 return true;
115 } catch (e) {
116 return e.code === "EPERM";
117 }
118}
119
120const bindable = (port) =>
121 new Promise((res) => {
122 const s = createServer();
123 s.once("error", () => res(false));
124 s.listen(port, "127.0.0.1", () => s.close(() => res(true)));
125 });