| 155 | * one vitest run) shares the block via disjoint offsets. |
| 156 | */ |
| 157 | export const claimPorts = async (claims: ReadonlyArray<PortClaim>): Promise<ClaimedPorts> => { |
| 158 | const ports: Record<string, number> = {}; |
| 159 | const unpinned = claims.filter((claim) => { |
| 160 | const pinned = process.env[claim.envVar]; |
| 161 | if (pinned) ports[claim.envVar] = Number(pinned); |
| 162 | return !pinned; |
| 163 | }); |
| 164 | if (unpinned.length === 0) return { ports, release: async () => {} }; |
| 165 | |
| 166 | for (let attempt = 0; attempt < BLOCK_COUNT; attempt++) { |
| 167 | const block = |
| 168 | BLOCK_BASE + ((portBlock - BLOCK_BASE + attempt * BLOCK_SIZE) % (BLOCK_COUNT * BLOCK_SIZE)); |
| 169 | // This process may already hold the block's lock (the other target's |
| 170 | // globalsetup in the same vitest run); reuse it instead of re-locking. |
| 171 | let lock = heldLocks.get(block); |
| 172 | if (!lock) { |
| 173 | lock = await tryLockBlock(block); |
| 174 | if (!lock) { |
| 175 | console.warn(`[e2e] port block ${block} is locked by another suite; trying next block`); |
| 176 | continue; |
| 177 | } |
| 178 | heldLocks.set(block, lock); |
| 179 | } |
| 180 | // BIND-probe every claimed port, holding all probe servers open until the |
| 181 | // whole block is proven free. A bind (unlike a connect-probe) also detects |
| 182 | // an ephemeral outbound socket squatting the port, and keeping the probes |
| 183 | // open means nothing can slip into these ports between the check and the |
| 184 | // moment we close them just before the services bind. |
| 185 | const probes = await Promise.all(unpinned.map((claim) => bindProbe(block + claim.offset))); |
| 186 | const held = probes.flatMap((probe) => probe ?? []); |
| 187 | if (probes.some((probe) => probe === undefined)) { |
| 188 | const taken = unpinned |
| 189 | .filter((_, index) => probes[index] === undefined) |
| 190 | .map((claim) => `${block + claim.offset} (${claim.label})`); |
| 191 | await Promise.all(held.map(closeServer)); // Free the ports we did grab. |
| 192 | console.warn( |
| 193 | `[e2e] port block ${block} has squatters — ${taken.join(", ")}; trying next block`, |
| 194 | ); |
| 195 | continue; // Keep the lock: a half-busy block is still ours, just unusable now. |
| 196 | } |
| 197 | for (const claim of unpinned) { |
| 198 | const port = block + claim.offset; |
| 199 | ports[claim.envVar] = port; |
| 200 | // Workers spawn after globalsetup, so they inherit these and agree. |
| 201 | process.env[claim.envVar] = String(port); |
| 202 | } |
| 203 | // Release the probe listeners the instant before we return: the caller boots |
| 204 | // its services immediately, so the ports go from probe-held to service-held |
| 205 | // with only a microsecond gap (vs. the seconds a pre-boot probe left open). |
| 206 | await Promise.all(held.map(closeServer)); |
| 207 | return { |
| 208 | ports, |
| 209 | release: async () => { |
| 210 | const held = heldLocks.get(block); |
| 211 | if (!held) return; |
| 212 | heldLocks.delete(block); |
| 213 | await new Promise<void>((done) => held.close(() => done())); |
| 214 | }, |