Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor with async/await instead of callback #59

Merged
merged 3 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/bin/detect-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
171 changes: 91 additions & 80 deletions src/detect-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>;
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<number>;
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') {
Expand All @@ -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<number> {
// 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);
Comment on lines +68 to +117
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor recursive calls to iterative loops

The functions tryListen and handleError use recursion to retry listening on incremented ports, which could lead to a stack overflow if many ports are unavailable. Consider refactoring this logic to use iterative loops to enhance reliability and prevent potential stack issues.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 71-71: src/detect-port.ts#L71
Added line #L71 was not covered by tests


[warning] 83-84: src/detect-port.ts#L83-L84
Added lines #L83 - L84 were not covered by tests


[warning] 92-93: src/detect-port.ts#L92-L93
Added lines #L92 - L93 were not covered by tests


[warning] 99-100: src/detect-port.ts#L99-L100
Added lines #L99 - L100 were not covered by tests


[warning] 106-111: src/detect-port.ts#L106-L111
Added lines #L106 - L111 were not covered by tests


[warning] 117-117: src/detect-port.ts#L117
Added line #L117 was not covered by tests

}
}

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<number>((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);
});
});
}
7 changes: 5 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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
fengmk2 marked this conversation as resolved.
Show resolved Hide resolved
export const detect = detectPort;

export * from './wait-port.js';
2 changes: 1 addition & 1 deletion src/wait-port.ts
Original file line number Diff line number Diff line change
@@ -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');

Expand Down
25 changes: 23 additions & 2 deletions test/detect-port.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Comment on lines +40 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Inconsistent error handling patterns found across server setups

The verification reveals inconsistency in error handling:

  • server and server4 have error handlers
  • server2 and server3 lack error handlers, which could lead to unhandled errors
🔗 Analysis chain

LGTM! Server setup addresses the reported issue

The addition of server4 listening on 127.0.0.1:25000 directly addresses the port detection issue reported in #57. The error handling is consistent with existing patterns.

Let's verify that all server setups follow consistent patterns:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify consistency in server setup patterns
# Expected: All server setups should follow similar error handling patterns

rg -A 5 "new net.Server\(\)" test/detect-port.test.ts

Length of output: 918


for (let port = 27000; port < 27010; port++) {
const server = new net.Server();
if (port % 3 === 0) {
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down
Loading