diff --git a/README.md b/README.md index 65620b4..7e7c8df 100644 --- a/README.md +++ b/README.md @@ -42,24 +42,40 @@ npm i detect-port ``` +CommonJS + ```javascript -const detect = require('detect-port'); -/** - * use as a promise - */ +const { detect } = require('detect-port'); detect(port) - .then(_port => { - if (port == _port) { + .then(realPort => { + if (port == realPort) { console.log(`port: ${port} was not occupied`); } else { - console.log(`port: ${port} was occupied, try port: ${_port}`); + console.log(`port: ${port} was occupied, try port: ${realPort}`); } }) .catch(err => { console.log(err); }); +``` + +ESM and TypeScript +```ts +import { detect } from 'detect-port'; + +detect(port) + .then(realPort => { + if (port == realPort) { + console.log(`port: ${port} was not occupied`); + } else { + console.log(`port: ${port} was occupied, try port: ${realPort}`); + } + }) + .catch(err => { + console.log(err); + }); ``` ## Command Line Tool diff --git a/src/bin/detect-port.ts b/src/bin/detect-port.ts index 96c9df8..6c98ee6 100755 --- a/src/bin/detect-port.ts +++ b/src/bin/detect-port.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { readFileSync } from 'node:fs'; -import detectPort from '../detect-port.js'; +import { detectPort } from '../detect-port.js'; const pkgFile = path.join(__dirname, '../../../package.json'); const pkg = JSON.parse(readFileSync(pkgFile, 'utf-8')); diff --git a/src/detect-port.ts b/src/detect-port.ts index 1a30168..77e2f13 100644 --- a/src/detect-port.ts +++ b/src/detect-port.ts @@ -4,18 +4,26 @@ import { ip } from 'address'; const debug = debuglog('detect-port'); -type DetectPortCallback = (err: Error | null, port?: number) => void; +export type DetectPortCallback = (err: Error | null, port?: number) => void; -interface PortConfig { +export interface PortConfig { port?: number | string; hostname?: string | undefined; callback?: DetectPortCallback; } -export default function detectPort(port?: number | PortConfig | string): Promise; -export default function detectPort(callback: DetectPortCallback): void; -export default function detectPort(port: number | PortConfig | string | undefined, callback: DetectPortCallback): void; -export default function detectPort(port?: number | string | PortConfig | DetectPortCallback, callback?: DetectPortCallback) { +export class IPAddressNotAvailableError extends Error { + constructor(options?: ErrorOptions) { + super('The IP address is not available on this machine', options); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} + +export function detectPort(port?: number | PortConfig | string): Promise; +export function detectPort(callback: DetectPortCallback): void; +export function detectPort(port: number | PortConfig | string | undefined, callback: DetectPortCallback): void; +export function detectPort(port?: number | string | PortConfig | DetectPortCallback, callback?: DetectPortCallback) { let hostname: string | undefined = ''; if (port && typeof port === 'object') { @@ -36,99 +44,102 @@ export default function detectPort(port?: number | string | PortConfig | DetectP } debug('detect free port between [%s, %s)', port, maxPort); if (typeof callback === 'function') { - return tryListen(port, maxPort, hostname, callback); + return tryListen(port, maxPort, hostname) + .then(port => callback(null, port)) + .catch(callback); } // promise - return new Promise(resolve => { - tryListen(port as number, maxPort, hostname, (_, realPort) => { - resolve(realPort); - }); - }); + return tryListen(port as number, maxPort, hostname); } -function tryListen(port: number, maxPort: number, hostname: string | undefined, callback: DetectPortCallback) { - function handleError() { - port++; - if (port >= maxPort) { - debug('port: %s >= maxPort: %s, give up and use random port', port, maxPort); - port = 0; - maxPort = 0; - } - tryListen(port, maxPort, hostname, callback); +async function handleError(port: number, maxPort: number, hostname?: string) { + if (port >= maxPort) { + debug('port: %s >= maxPort: %s, give up and use random port', port, maxPort); + port = 0; + maxPort = 0; } + return await tryListen(port, maxPort, hostname); +} +async function tryListen(port: number, maxPort: number, hostname?: string): Promise { // use user hostname if (hostname) { - listen(port, hostname, (err, realPort) => { - if (err) { - if ((err as any).code === 'EADDRNOTAVAIL') { - return callback(new Error('The IP address is not available on this machine')); - } - return handleError(); + try { + return await listen(port, hostname); + } catch (err: any) { + if (err.code === 'EADDRNOTAVAIL') { + throw new IPAddressNotAvailableError({ cause: err }); } + return await handleError(++port, maxPort, hostname); + } + } - callback(null, realPort); - }); - } else { - // 1. check null - listen(port, void 0, (err, realPort) => { - // ignore random listening - if (port === 0) { - return callback(err, realPort); - } + // 1. check null / undefined + try { + await listen(port); + } catch (err) { + // ignore random listening + if (port === 0) { + throw err; + } + return await handleError(++port, maxPort, hostname); + } - if (err) { - return handleError(); - } + // 2. check 0.0.0.0 + try { + await listen(port, '0.0.0.0'); + } catch (err) { + return await handleError(++port, maxPort, hostname); + } - // 2. check 0.0.0.0 - listen(port, '0.0.0.0', err => { - if (err) { - return handleError(); - } - - // 3. check localhost - listen(port, 'localhost', err => { - // if localhost refer to the ip that is not unkonwn on the machine, you will see the error EADDRNOTAVAIL - // https://stackoverflow.com/questions/10809740/listen-eaddrnotavail-error-in-node-js - if (err && (err as any).code !== 'EADDRNOTAVAIL') { - return handleError(); - } - - // 4. check current ip - listen(port, ip(), (err, realPort) => { - if (err) { - return handleError(); - } - - callback(null, realPort); - }); - }); - }); - }); + // 3. check 127.0.0.1 + try { + await listen(port, '127.0.0.1'); + } catch (err) { + return await handleError(++port, maxPort, hostname); + } + + // 4. check localhost + try { + await listen(port, 'localhost'); + } catch (err: any) { + // if localhost refer to the ip that is not unknown on the machine, you will see the error EADDRNOTAVAIL + // https://stackoverflow.com/questions/10809740/listen-eaddrnotavail-error-in-node-js + if (err.code !== 'EADDRNOTAVAIL') { + return await handleError(++port, maxPort, hostname); + } + } + + // 5. check current ip + try { + return await listen(port, ip()); + } catch (err) { + return await handleError(++port, maxPort, hostname); } } -function listen(port: number, hostname: string | undefined, callback: DetectPortCallback) { +function listen(port: number, hostname?: string) { const server = createServer(); - server.once('error', err => { - debug('listen %s:%s error: %s', hostname, port, err); - server.close(); + return new Promise((resolve, reject) => { + server.once('error', err => { + debug('listen %s:%s error: %s', hostname, port, err); + server.close(); - if ((err as any).code === 'ENOTFOUND') { - debug('ignore dns ENOTFOUND error, get free %s:%s', hostname, port); - return callback(null, port); - } + if ((err as any).code === 'ENOTFOUND') { + debug('ignore dns ENOTFOUND error, get free %s:%s', hostname, port); + return resolve(port); + } - return callback(err); - }); + return reject(err); + }); - debug('try listen %d on %s', port, hostname); - server.listen(port, hostname, () => { - port = (server.address() as AddressInfo).port; - debug('get free %s:%s', hostname, port); - server.close(); - return callback(null, port); + debug('try listen %d on %s', port, hostname); + server.listen(port, hostname, () => { + port = (server.address() as AddressInfo).port; + debug('get free %s:%s', hostname, port); + server.close(); + return resolve(port); + }); }); } diff --git a/src/index.ts b/src/index.ts index ebad509..b29cb85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,9 @@ -import detectPort from './detect-port.js'; +import { detectPort } from './detect-port.js'; export default detectPort; -export { detectPort }; +export * from './detect-port.js'; +// keep alias detectPort to detect +export const detect = detectPort; + export * from './wait-port.js'; diff --git a/src/wait-port.ts b/src/wait-port.ts index 093b31a..aabea15 100644 --- a/src/wait-port.ts +++ b/src/wait-port.ts @@ -1,5 +1,5 @@ import { debuglog } from 'node:util'; -import detectPort from './detect-port.js'; +import { detectPort } from './detect-port.js'; const debug = debuglog('detect-port:wait-port'); diff --git a/test/detect-port.test.ts b/test/detect-port.test.ts index 1cd2a5a..5611e10 100644 --- a/test/detect-port.test.ts +++ b/test/detect-port.test.ts @@ -3,8 +3,8 @@ import net from 'node:net'; import { strict as assert } from 'node:assert'; import { ip } from 'address'; import mm from 'mm'; - -import detectPort from '../src/detect-port.js'; +import detect from '../src/index.js'; +import { detect as detect2, detectPort } from '../src/index.js'; describe('test/detect-port.test.ts', () => { afterEach(mm.restore); @@ -37,6 +37,13 @@ describe('test/detect-port.test.ts', () => { server3.listen(28080, '0.0.0.0', cb); servers.push(server3); + const server4 = new net.Server(); + server4.listen(25000, '127.0.0.1', cb); + server4.on('error', err => { + console.error('listen 127.0.0.1 error:', err); + }); + servers.push(server4); + for (let port = 27000; port < 27010; port++) { const server = new net.Server(); if (port % 3 === 0) { @@ -68,6 +75,13 @@ describe('test/detect-port.test.ts', () => { assert(port >= 1024 && port < 65535); }); + it('should detect work', async () => { + let port = await detect(); + assert(port >= 1024 && port < 65535); + port = await detect2(); + assert(port >= 1024 && port < 65535); + }); + it('with occupied port, like "listen EACCES: permission denied"', async () => { const port = 80; const realPort = await detectPort(port); @@ -81,6 +95,13 @@ describe('test/detect-port.test.ts', () => { assert.equal(realPort, 23001); }); + it('work with listening next port 25001 because 25000 was listened to 127.0.0.1', async () => { + const port = 25000; + const realPort = await detectPort(port); + assert(realPort); + assert.equal(realPort, 25001); + }); + it('should listen next port 24001 when localhost is not binding', async () => { mm(dns, 'lookup', (...args: any[]) => { mm.restore();