rajt
Version:
A serverless bundler layer, fully typed for AWS Lambda (Node.js and LLRT) and Cloudflare Workers.
119 lines (103 loc) • 3.4 kB
text/typescript
import net from 'node:net'
import { exec } from 'node:child_process'
import { error, warn } from './log'
export function withPort(desiredPort: number, cb: (port: number) => void, maxAttempts = 10) {
getAvailablePort(desiredPort)
.then(([port, pid]) => {
if (port != desiredPort)
warn(
`Port ${desiredPort} is in use by ${pid ? 'process '+ pid : 'an unknown process'}, using available port ${port} instead`
)
cb(Number(port))
}).catch(e => error('Error finding available port:', e))
}
export async function isPortInUse(port: number) {
return new Promise(resolve => {
const server = net.createServer()
.once('error', () => resolve(true))
.once('listening', () => {
server.close(() => resolve(false))
})
.listen(port)
})
}
export async function getAvailablePort(startPort: number, maxAttempts = 10) {
let port = startPort
let attempts = 0
while (attempts < maxAttempts) {
const inUse = await isPortInUse(port)
if (!inUse && attempts) {
const pid = await getProcessIdUsingPort(startPort)
return [port, pid]
}
if (!inUse)
return [port, null]
port++
attempts++
}
throw new Error(`No available ports found after ${maxAttempts} attempts`)
}
export async function getProcessIdUsingPort(port: number): Promise<string | null> {
const timeoutMs = 250
const processLookupController = new AbortController()
const pidPromise = new Promise<string | null>((resolve) => {
const handleError = (e: Error) => {
error('Failed to get process ID for port', port, e)
resolve(null)
}
try { // Use lsof on Unix-like systems (macOS, Linux)
if (process.platform !== 'win32') {
exec(
`lsof -ti:${port} -sTCP:LISTEN`,
{ signal: processLookupController.signal },
(error, stdout) => {
if (error) {
handleError(error)
return
}
// `-sTCP` will ensure there's only one port, clean up output
const pid = stdout.trim()
resolve(pid || null)
}
)
} else { // Use netstat on Windows
exec(
`netstat -ano | findstr /C:":${port} " | findstr LISTENING`,
{ signal: processLookupController.signal },
(error, stdout) => {
if (error) {
handleError(error)
return
}
// Clean up output and extract PID
const cleanOutput = stdout.replace(/\s+/g, ' ').trim()
if (cleanOutput) {
const lines = cleanOutput.split('\n')
const firstLine = lines[0].trim()
if (firstLine) {
const parts = firstLine.split(' ')
const pid = parts[parts.length - 1]
resolve(pid || null)
} else {
resolve(null)
}
} else {
resolve(null)
}
}
)
}
} catch (cause) {
handleError(
new Error('Unexpected error during process lookup', { cause })
)
}
})
const timeoutId = setTimeout(() => {
processLookupController.abort(
`PID detection timed out after ${timeoutMs}ms for port ${port}.`
)
}, timeoutMs)
pidPromise.finally(() => clearTimeout(timeoutId))
return pidPromise
}