1 | 'use strict';
|
2 | const net = require('net');
|
3 |
|
4 | class Locked extends Error {
|
5 | constructor(port) {
|
6 | super(`${port} is locked`);
|
7 | }
|
8 | }
|
9 |
|
10 | const lockedPorts = {
|
11 | old: new Set(),
|
12 | young: new Set()
|
13 | };
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | const releaseOldLockedPortsIntervalMs = 1000 * 15;
|
19 |
|
20 |
|
21 | let interval;
|
22 |
|
23 | const getAvailablePort = options => new Promise((resolve, reject) => {
|
24 | const server = net.createServer();
|
25 | server.unref();
|
26 | server.on('error', reject);
|
27 | server.listen(options, () => {
|
28 | const {port} = server.address();
|
29 | server.close(() => {
|
30 | resolve(port);
|
31 | });
|
32 | });
|
33 | });
|
34 |
|
35 | const portCheckSequence = function * (ports) {
|
36 | if (ports) {
|
37 | yield * ports;
|
38 | }
|
39 |
|
40 | yield 0;
|
41 | };
|
42 |
|
43 | module.exports = async options => {
|
44 | let ports;
|
45 |
|
46 | if (options) {
|
47 | ports = typeof options.port === 'number' ? [options.port] : options.port;
|
48 | }
|
49 |
|
50 | if (interval === undefined) {
|
51 | interval = setInterval(() => {
|
52 | lockedPorts.old = lockedPorts.young;
|
53 | lockedPorts.young = new Set();
|
54 | }, releaseOldLockedPortsIntervalMs);
|
55 |
|
56 | interval.unref();
|
57 | }
|
58 |
|
59 | for (const port of portCheckSequence(ports)) {
|
60 | try {
|
61 | let availablePort = await getAvailablePort({...options, port});
|
62 | while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) {
|
63 | if (port !== 0) {
|
64 | throw new Locked(port);
|
65 | }
|
66 |
|
67 | availablePort = await getAvailablePort({...options, port});
|
68 | }
|
69 |
|
70 | lockedPorts.young.add(availablePort);
|
71 | return availablePort;
|
72 | } catch (error) {
|
73 | if (!['EADDRINUSE', 'EACCES'].includes(error.code) && !(error instanceof Locked)) {
|
74 | throw error;
|
75 | }
|
76 | }
|
77 | }
|
78 |
|
79 | throw new Error('No available ports found');
|
80 | };
|
81 |
|
82 | module.exports.makeRange = (from, to) => {
|
83 | if (!Number.isInteger(from) || !Number.isInteger(to)) {
|
84 | throw new TypeError('`from` and `to` must be integer numbers');
|
85 | }
|
86 |
|
87 | if (from < 1024 || from > 65535) {
|
88 | throw new RangeError('`from` must be between 1024 and 65535');
|
89 | }
|
90 |
|
91 | if (to < 1024 || to > 65536) {
|
92 | throw new RangeError('`to` must be between 1024 and 65536');
|
93 | }
|
94 |
|
95 | if (to < from) {
|
96 | throw new RangeError('`to` must be greater than or equal to `from`');
|
97 | }
|
98 |
|
99 | const generator = function * (from, to) {
|
100 | for (let port = from; port <= to; port++) {
|
101 | yield port;
|
102 | }
|
103 | };
|
104 |
|
105 | return generator(from, to);
|
106 | };
|