diff --git a/packages/integration-tests/.aegir.js b/packages/integration-tests/.aegir.js index bbd789eb5c..d7f3e201c7 100644 --- a/packages/integration-tests/.aegir.js +++ b/packages/integration-tests/.aegir.js @@ -1,4 +1,7 @@ import { execa } from 'execa' +import fs from 'node:fs' +import net from 'node:net' +import { randomUUID } from 'node:crypto' import pDefer from 'p-defer' /** @type {import('aegir').PartialOptions} */ @@ -169,11 +172,11 @@ async function createGoLibp2pRelay () { const { defaultLogger } = await import('@libp2p/logger') const log = defaultLogger().forComponent('go-libp2p') - const controlPort = Math.floor(Math.random() * (50000 - 10000 + 1)) + 10000 - const apiAddr = multiaddr(`/ip4/127.0.0.1/tcp/${controlPort}`) + const { controlSocketPath, apiAddr, daemonListenAddr } = await getControlEndpoint(multiaddr) const deferred = pDefer() + const proc = execa(p2pd(), [ - `-listen=${apiAddr.toString()}`, + `-listen=${daemonListenAddr}`, // listen on TCP, WebSockets and WebTransport '-hostAddrs=/ip4/127.0.0.1/tcp/0,/ip4/127.0.0.1/tcp/0/ws,/ip4/127.0.0.1/udp/0/quic-v1/webtransport', '-noise=true', @@ -185,8 +188,17 @@ async function createGoLibp2pRelay () { GOLOG_LOG_LEVEL: 'debug' } }) + proc.catch(() => { // go-libp2p daemon throws when killed + }).finally(() => { + if (controlSocketPath != null) { + fs.rmSync(controlSocketPath, { force: true }) + } + }) + + proc.once('exit', code => { + deferred.reject(new Error(`go-libp2p daemon exited before startup (code: ${code ?? 'unknown'})`)) }) proc.stdout?.on('data', (buf) => { @@ -202,6 +214,7 @@ async function createGoLibp2pRelay () { log(str) }) + await deferred.promise const daemonClient = createClient(apiAddr) @@ -214,3 +227,52 @@ async function createGoLibp2pRelay () { proc } } + +async function getControlEndpoint (multiaddr) { + if (process.platform === 'win32') { + const controlPort = await getAvailablePort() + const apiAddr = multiaddr(`/ip4/127.0.0.1/tcp/${controlPort}`) + + return { + controlPort, + apiAddr, + daemonListenAddr: apiAddr.toString() + } + } + + const controlSocketPath = `/tmp/p2pd-${randomUUID()}.sock` + const apiAddr = multiaddr(`/unix/${encodeURIComponent(controlSocketPath)}`) + + return { + controlSocketPath, + apiAddr, + daemonListenAddr: `/unix${controlSocketPath}` + } +} + +async function getAvailablePort () { + return new Promise((resolve, reject) => { + const server = net.createServer() + + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + const addr = server.address() + + if (addr == null || typeof addr === 'string') { + server.close(() => { + reject(new Error('could not allocate control port')) + }) + return + } + + server.close(err => { + if (err != null) { + reject(err) + return + } + + resolve(addr.port) + }) + }) + }) +} diff --git a/packages/integration-tests/test/interop.ts b/packages/integration-tests/test/interop.ts index 38873d2ac4..0b309e1812 100644 --- a/packages/integration-tests/test/interop.ts +++ b/packages/integration-tests/test/interop.ts @@ -1,4 +1,6 @@ import fs from 'fs' +import { randomUUID } from 'node:crypto' +import net from 'node:net' import { noise } from '@chainsafe/libp2p-noise' import { yamux } from '@chainsafe/libp2p-yamux' import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2' @@ -38,14 +40,20 @@ import type { Libp2pOptions, ServiceFactoryMap } from 'libp2p' * ``` */ +interface ControlEndpoint { + controlSocketPath?: string + controlPort?: number + apiAddr: ReturnType + daemonListenAddr: string +} + async function createGoPeer (options: SpawnOptions): Promise { - const controlPort = Math.floor(Math.random() * (50000 - 10000 + 1)) + 10000 - const apiAddr = multiaddr(`/ip4/127.0.0.1/tcp/${controlPort}`) + const { controlSocketPath, controlPort, apiAddr, daemonListenAddr } = await getControlEndpoint() - const log = logger(`go-libp2p:${controlPort}`) + const log = logger(`go-libp2p:${controlSocketPath ?? controlPort}`) const opts = [ - `-listen=${apiAddr.toString()}` + `-listen=${daemonListenAddr}` ] if (options.noListen === true) { @@ -92,13 +100,21 @@ async function createGoPeer (options: SpawnOptions): Promise { opts.push('-muxer=yamux') } - const deferred = pDefer() + const deferred = pDefer() const proc = execa(p2pd(), opts, { env: { GOLOG_LOG_LEVEL: 'debug' } }) + proc.catch(() => { + // go-libp2p daemon throws when killed + }) + + proc.once('exit', code => { + deferred.reject(new Error(`go-libp2p daemon exited before startup (code: ${code ?? 'unknown'})`)) + }) + proc.stdout?.on('data', (buf: Buffer) => { const str = buf.toString() log(str) @@ -119,8 +135,63 @@ async function createGoPeer (options: SpawnOptions): Promise { client: createClient(apiAddr), stop: async () => { proc.kill() + + if (controlSocketPath != null) { + fs.rmSync(controlSocketPath, { force: true }) + } + } + } +} + +async function getControlEndpoint (): Promise { + // Use tcp control ports on windows + if (process.platform === 'win32') { + const controlPort = await getAvailablePort() + const apiAddr = multiaddr(`/ip4/127.0.0.1/tcp/${controlPort}`) + + return { + controlPort, + apiAddr, + daemonListenAddr: apiAddr.toString() } } + + // Use unix domain sockets on non-windows - avoids port clashes + const controlSocketPath = `/tmp/p2pd-${randomUUID()}.sock` + const apiAddr = multiaddr(`/unix/${encodeURIComponent(controlSocketPath)}`) + + return { + controlSocketPath, + apiAddr, + daemonListenAddr: `/unix${controlSocketPath}` + } +} + +async function getAvailablePort (): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer() + + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + const addr = server.address() + + if (addr == null || typeof addr === 'string') { + server.close(() => { + reject(new Error('could not allocate control port')) + }) + return + } + + server.close(err => { + if (err != null) { + reject(err) + return + } + + resolve(addr.port) + }) + }) + }) } async function createJsPeer (options: SpawnOptions): Promise {