Skip to content

Commit 9254c18

Browse files
authored
feat: refactor with async/await instead of callback (#59)
listen 127.0.0.1 and check occupied closes #57 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced error handling with a new specific error type for unavailable IP addresses. - Support for multiple parameter combinations in the port detection function. - **Bug Fixes** - Improved handling of occupied ports in the test suite. - **Documentation** - Updated import and export statements for clarity and consistency across files. - **Tests** - Added new test cases to validate port detection functionality under various scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 9a90713 commit 9254c18

File tree

6 files changed

+144
-93
lines changed

6 files changed

+144
-93
lines changed

README.md

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,40 @@
4242
npm i detect-port
4343
```
4444

45+
CommonJS
46+
4547
```javascript
46-
const detect = require('detect-port');
47-
/**
48-
* use as a promise
49-
*/
48+
const { detect } = require('detect-port');
5049

5150
detect(port)
52-
.then(_port => {
53-
if (port == _port) {
51+
.then(realPort => {
52+
if (port == realPort) {
5453
console.log(`port: ${port} was not occupied`);
5554
} else {
56-
console.log(`port: ${port} was occupied, try port: ${_port}`);
55+
console.log(`port: ${port} was occupied, try port: ${realPort}`);
5756
}
5857
})
5958
.catch(err => {
6059
console.log(err);
6160
});
61+
```
62+
63+
ESM and TypeScript
6264

65+
```ts
66+
import { detect } from 'detect-port';
67+
68+
detect(port)
69+
.then(realPort => {
70+
if (port == realPort) {
71+
console.log(`port: ${port} was not occupied`);
72+
} else {
73+
console.log(`port: ${port} was occupied, try port: ${realPort}`);
74+
}
75+
})
76+
.catch(err => {
77+
console.log(err);
78+
});
6379
```
6480

6581
## Command Line Tool

src/bin/detect-port.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import path from 'node:path';
44
import { readFileSync } from 'node:fs';
5-
import detectPort from '../detect-port.js';
5+
import { detectPort } from '../detect-port.js';
66

77
const pkgFile = path.join(__dirname, '../../../package.json');
88
const pkg = JSON.parse(readFileSync(pkgFile, 'utf-8'));

src/detect-port.ts

Lines changed: 91 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,26 @@ import { ip } from 'address';
44

55
const debug = debuglog('detect-port');
66

7-
type DetectPortCallback = (err: Error | null, port?: number) => void;
7+
export type DetectPortCallback = (err: Error | null, port?: number) => void;
88

9-
interface PortConfig {
9+
export interface PortConfig {
1010
port?: number | string;
1111
hostname?: string | undefined;
1212
callback?: DetectPortCallback;
1313
}
1414

15-
export default function detectPort(port?: number | PortConfig | string): Promise<number>;
16-
export default function detectPort(callback: DetectPortCallback): void;
17-
export default function detectPort(port: number | PortConfig | string | undefined, callback: DetectPortCallback): void;
18-
export default function detectPort(port?: number | string | PortConfig | DetectPortCallback, callback?: DetectPortCallback) {
15+
export class IPAddressNotAvailableError extends Error {
16+
constructor(options?: ErrorOptions) {
17+
super('The IP address is not available on this machine', options);
18+
this.name = this.constructor.name;
19+
Error.captureStackTrace(this, this.constructor);
20+
}
21+
}
22+
23+
export function detectPort(port?: number | PortConfig | string): Promise<number>;
24+
export function detectPort(callback: DetectPortCallback): void;
25+
export function detectPort(port: number | PortConfig | string | undefined, callback: DetectPortCallback): void;
26+
export function detectPort(port?: number | string | PortConfig | DetectPortCallback, callback?: DetectPortCallback) {
1927
let hostname: string | undefined = '';
2028

2129
if (port && typeof port === 'object') {
@@ -36,99 +44,102 @@ export default function detectPort(port?: number | string | PortConfig | DetectP
3644
}
3745
debug('detect free port between [%s, %s)', port, maxPort);
3846
if (typeof callback === 'function') {
39-
return tryListen(port, maxPort, hostname, callback);
47+
return tryListen(port, maxPort, hostname)
48+
.then(port => callback(null, port))
49+
.catch(callback);
4050
}
4151
// promise
42-
return new Promise(resolve => {
43-
tryListen(port as number, maxPort, hostname, (_, realPort) => {
44-
resolve(realPort);
45-
});
46-
});
52+
return tryListen(port as number, maxPort, hostname);
4753
}
4854

49-
function tryListen(port: number, maxPort: number, hostname: string | undefined, callback: DetectPortCallback) {
50-
function handleError() {
51-
port++;
52-
if (port >= maxPort) {
53-
debug('port: %s >= maxPort: %s, give up and use random port', port, maxPort);
54-
port = 0;
55-
maxPort = 0;
56-
}
57-
tryListen(port, maxPort, hostname, callback);
55+
async function handleError(port: number, maxPort: number, hostname?: string) {
56+
if (port >= maxPort) {
57+
debug('port: %s >= maxPort: %s, give up and use random port', port, maxPort);
58+
port = 0;
59+
maxPort = 0;
5860
}
61+
return await tryListen(port, maxPort, hostname);
62+
}
5963

64+
async function tryListen(port: number, maxPort: number, hostname?: string): Promise<number> {
6065
// use user hostname
6166
if (hostname) {
62-
listen(port, hostname, (err, realPort) => {
63-
if (err) {
64-
if ((err as any).code === 'EADDRNOTAVAIL') {
65-
return callback(new Error('The IP address is not available on this machine'));
66-
}
67-
return handleError();
67+
try {
68+
return await listen(port, hostname);
69+
} catch (err: any) {
70+
if (err.code === 'EADDRNOTAVAIL') {
71+
throw new IPAddressNotAvailableError({ cause: err });
6872
}
73+
return await handleError(++port, maxPort, hostname);
74+
}
75+
}
6976

70-
callback(null, realPort);
71-
});
72-
} else {
73-
// 1. check null
74-
listen(port, void 0, (err, realPort) => {
75-
// ignore random listening
76-
if (port === 0) {
77-
return callback(err, realPort);
78-
}
77+
// 1. check null / undefined
78+
try {
79+
await listen(port);
80+
} catch (err) {
81+
// ignore random listening
82+
if (port === 0) {
83+
throw err;
84+
}
85+
return await handleError(++port, maxPort, hostname);
86+
}
7987

80-
if (err) {
81-
return handleError();
82-
}
88+
// 2. check 0.0.0.0
89+
try {
90+
await listen(port, '0.0.0.0');
91+
} catch (err) {
92+
return await handleError(++port, maxPort, hostname);
93+
}
8394

84-
// 2. check 0.0.0.0
85-
listen(port, '0.0.0.0', err => {
86-
if (err) {
87-
return handleError();
88-
}
89-
90-
// 3. check localhost
91-
listen(port, 'localhost', err => {
92-
// if localhost refer to the ip that is not unkonwn on the machine, you will see the error EADDRNOTAVAIL
93-
// https://stackoverflow.com/questions/10809740/listen-eaddrnotavail-error-in-node-js
94-
if (err && (err as any).code !== 'EADDRNOTAVAIL') {
95-
return handleError();
96-
}
97-
98-
// 4. check current ip
99-
listen(port, ip(), (err, realPort) => {
100-
if (err) {
101-
return handleError();
102-
}
103-
104-
callback(null, realPort);
105-
});
106-
});
107-
});
108-
});
95+
// 3. check 127.0.0.1
96+
try {
97+
await listen(port, '127.0.0.1');
98+
} catch (err) {
99+
return await handleError(++port, maxPort, hostname);
100+
}
101+
102+
// 4. check localhost
103+
try {
104+
await listen(port, 'localhost');
105+
} catch (err: any) {
106+
// if localhost refer to the ip that is not unknown on the machine, you will see the error EADDRNOTAVAIL
107+
// https://stackoverflow.com/questions/10809740/listen-eaddrnotavail-error-in-node-js
108+
if (err.code !== 'EADDRNOTAVAIL') {
109+
return await handleError(++port, maxPort, hostname);
110+
}
111+
}
112+
113+
// 5. check current ip
114+
try {
115+
return await listen(port, ip());
116+
} catch (err) {
117+
return await handleError(++port, maxPort, hostname);
109118
}
110119
}
111120

112-
function listen(port: number, hostname: string | undefined, callback: DetectPortCallback) {
121+
function listen(port: number, hostname?: string) {
113122
const server = createServer();
114123

115-
server.once('error', err => {
116-
debug('listen %s:%s error: %s', hostname, port, err);
117-
server.close();
124+
return new Promise<number>((resolve, reject) => {
125+
server.once('error', err => {
126+
debug('listen %s:%s error: %s', hostname, port, err);
127+
server.close();
118128

119-
if ((err as any).code === 'ENOTFOUND') {
120-
debug('ignore dns ENOTFOUND error, get free %s:%s', hostname, port);
121-
return callback(null, port);
122-
}
129+
if ((err as any).code === 'ENOTFOUND') {
130+
debug('ignore dns ENOTFOUND error, get free %s:%s', hostname, port);
131+
return resolve(port);
132+
}
123133

124-
return callback(err);
125-
});
134+
return reject(err);
135+
});
126136

127-
debug('try listen %d on %s', port, hostname);
128-
server.listen(port, hostname, () => {
129-
port = (server.address() as AddressInfo).port;
130-
debug('get free %s:%s', hostname, port);
131-
server.close();
132-
return callback(null, port);
137+
debug('try listen %d on %s', port, hostname);
138+
server.listen(port, hostname, () => {
139+
port = (server.address() as AddressInfo).port;
140+
debug('get free %s:%s', hostname, port);
141+
server.close();
142+
return resolve(port);
143+
});
133144
});
134145
}

src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import detectPort from './detect-port.js';
1+
import { detectPort } from './detect-port.js';
22

33
export default detectPort;
44

5-
export { detectPort };
5+
export * from './detect-port.js';
6+
// keep alias detectPort to detect
7+
export const detect = detectPort;
8+
69
export * from './wait-port.js';

src/wait-port.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { debuglog } from 'node:util';
2-
import detectPort from './detect-port.js';
2+
import { detectPort } from './detect-port.js';
33

44
const debug = debuglog('detect-port:wait-port');
55

test/detect-port.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import net from 'node:net';
33
import { strict as assert } from 'node:assert';
44
import { ip } from 'address';
55
import mm from 'mm';
6-
7-
import detectPort from '../src/detect-port.js';
6+
import detect from '../src/index.js';
7+
import { detect as detect2, detectPort } from '../src/index.js';
88

99
describe('test/detect-port.test.ts', () => {
1010
afterEach(mm.restore);
@@ -37,6 +37,13 @@ describe('test/detect-port.test.ts', () => {
3737
server3.listen(28080, '0.0.0.0', cb);
3838
servers.push(server3);
3939

40+
const server4 = new net.Server();
41+
server4.listen(25000, '127.0.0.1', cb);
42+
server4.on('error', err => {
43+
console.error('listen 127.0.0.1 error:', err);
44+
});
45+
servers.push(server4);
46+
4047
for (let port = 27000; port < 27010; port++) {
4148
const server = new net.Server();
4249
if (port % 3 === 0) {
@@ -68,6 +75,13 @@ describe('test/detect-port.test.ts', () => {
6875
assert(port >= 1024 && port < 65535);
6976
});
7077

78+
it('should detect work', async () => {
79+
let port = await detect();
80+
assert(port >= 1024 && port < 65535);
81+
port = await detect2();
82+
assert(port >= 1024 && port < 65535);
83+
});
84+
7185
it('with occupied port, like "listen EACCES: permission denied"', async () => {
7286
const port = 80;
7387
const realPort = await detectPort(port);
@@ -81,6 +95,13 @@ describe('test/detect-port.test.ts', () => {
8195
assert.equal(realPort, 23001);
8296
});
8397

98+
it('work with listening next port 25001 because 25000 was listened to 127.0.0.1', async () => {
99+
const port = 25000;
100+
const realPort = await detectPort(port);
101+
assert(realPort);
102+
assert.equal(realPort, 25001);
103+
});
104+
84105
it('should listen next port 24001 when localhost is not binding', async () => {
85106
mm(dns, 'lookup', (...args: any[]) => {
86107
mm.restore();

0 commit comments

Comments
 (0)