// Imported only by tests. Every port lease is held until the fixture closes. import { spawn } from 'node:child_process'; import { mkdirSync, realpathSync } from 'node:fs'; import { join } from 'node:path'; import { createServer } from 'node:net'; import { once } from 'node:events'; import { BANDS, within, SCRATCH, refuse } from './lane-state.mjs'; async function hold(file){ const child=spawn('flock',['-n','-E','73',file,'bash','-c','printf "held\\n"; IFS= read -r release'],{stdio:['pipe','pipe','pipe']}); let err='';child.stderr.on('data',c=>err+=c);const exited=once(child,'exit'); const ok=await new Promise((resolve,reject)=>{child.stdout.once('data',()=>resolve(true));child.once('exit',code=>code===73?resolve(false):reject(Error(`port lock holder exited ${code}: ${err}`)));child.once('error',reject);}); if(!ok)return null; return async()=>{child.stdin.end('\n');await exited;}; } // A free port the caller will listen on itself. Nothing holds the port itself, so this is for // callers that bind it next; the port is probed and closed. async function probe(port){const s=createServer();try{await new Promise((resolve,reject)=>{s.once('error',reject);s.listen(port,'127.0.0.1',resolve);});const found=s.address().port;await new Promise(r=>s.close(r));return found;}catch(e){if(s.listening)await new Promise(r=>s.close(r));if(e.code==='EADDRINUSE')return null;throw e;}} const probed=async port=>{const found=await probe(port);return found?{port:found}:null;}; // A port nothing answers on. A bound socket that never listens refuses every connect, like any // closed port, while bind() from another process and the kernel's ephemeral allocation both // skip it. Node cannot make one (its listen() always follows bind()), so a perl child holds the // socket and exits when its stdin closes, which also covers the fixture dying abruptly. const BOUND=String.raw`use strict;use Socket;$|=1; socket(my $s,PF_INET,SOCK_STREAM,0)or die"socket: $!\n"; bind($s,sockaddr_in($ARGV[0],inet_aton("127.0.0.1")))or do{my $busy=$!{EADDRINUSE};print STDERR"bind: $!\n";exit($busy?75:76)}; my($port)=sockaddr_in(getsockname($s));print"bound $port\n";;`; async function bound(port){ const child=spawn('perl',['-e',BOUND,String(port)],{stdio:['pipe','pipe','pipe']}); let err='',out='';child.stderr.on('data',c=>err+=c);child.stdin.on('error',()=>{}); const exited=once(child,'exit');exited.catch(()=>{}); const found=await new Promise((resolve,reject)=>{ child.stdout.on('data',c=>{out+=c;const m=out.match(/^bound (\d+)\n/);if(m)resolve(Number(m[1]));}); child.once('exit',code=>code===75?resolve(null):reject(Error(`port holder exited ${code}: ${err}`))); child.once('error',e=>reject(Error(`port holder did not start (perl is required): ${e.message}`))); }); if(found===null)return null; return {port:found,release:async()=>{child.stdin.end('\n');await exited;}}; } // A lane takes ports from its own band under a per-port lock; no lane takes the kernel's. async function lease(t,{lane=process.env.MOSAIC_GATE_LANE,stateRoot=process.env.MOSAIC_GATE_STATE}={},attempt){ if(!lane){ const got=await attempt(0);if(!got)refuse('no port available'); if(got.release)t.after(got.release); return got.port; } if(!BANDS[lane]||!stateRoot)refuse('invalid port lane state'); const state=realpathSync(stateRoot);if(!within(SCRATCH,state))refuse('port state outside scratch'); const dir=join(state,'ports',lane);mkdirSync(dir,{recursive:true,mode:0o700}); const [lo,hi]=BANDS[lane]; for(let port=lo;port<=hi;port++){ const release=await hold(join(dir,`${port}.lock`));if(!release)continue; let got;try{got=await attempt(port);}catch(e){await release();throw e;} if(!got){await release();continue;} t.after(async()=>{try{await got.release?.();}finally{await release();}});return got.port; } refuse('lane port band exhausted'); } export async function leasePort(t,{closed,...context}={}){ if(closed!==undefined)refuse('leasePort takes no closed option; leaseClosedPort holds a port nothing answers on'); return lease(t,context,probed); } export async function leaseClosedPort(t,context={}){return lease(t,context,bound);} export function fixtureCacheEnv(root){ mkdirSync(root,{recursive:true,mode:0o700});const dir=realpathSync(root);for(const n of ['tmp','jiti','cache'])mkdirSync(join(dir,n),{recursive:true,mode:0o700}); return {TMPDIR:join(dir,'tmp'),JITI_FS_CACHE:join(dir,'jiti'),XDG_CACHE_HOME:join(dir,'cache')}; }