Skip to content
This repository was archived by the owner on Feb 2, 2021. It is now read-only.

Commit c3e7d93

Browse files
feat(net): Add method to check if port is in LISTEN state
Add method in net service to check if port is in LISTEN state. Add tests for it. The method has different implementation for macOS, Linux and Windows - added tests for each of the implementations.
1 parent 1ae842e commit c3e7d93

File tree

4 files changed

+248
-1
lines changed

4 files changed

+248
-1
lines changed

declarations.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,6 +1736,25 @@ interface IVersionData {
17361736
patch: string;
17371737
}
17381738

1739+
interface IWaitForPortListenData {
1740+
/**
1741+
* Port to be checked.
1742+
* @type {number}
1743+
*/
1744+
port: number;
1745+
1746+
/**
1747+
* Max amount of time in milliseconds to wait.
1748+
* @type {number}
1749+
*/
1750+
timeout: number;
1751+
/**
1752+
* @optional The amount of time between each check.
1753+
* @type {number}
1754+
*/
1755+
interval?: number;
1756+
}
1757+
17391758
/**
17401759
* Wrapper for net module of Node.js.
17411760
*/
@@ -1761,6 +1780,13 @@ interface INet {
17611780
* @return {Promise<boolean>} true if the port is available.
17621781
*/
17631782
isPortAvailable(port: number): Promise<boolean>;
1783+
1784+
/**
1785+
* Waits for port to be in LISTEN state.
1786+
* @param {IWaitForPortListenData} waitForPortListenData Data describing port, timeout and interval.
1787+
* @returns {boolean} true in case port is in LISTEN state, false otherwise.
1788+
*/
1789+
waitForPortToListen(waitForPortListenData: IWaitForPortListenData): Promise<boolean>;
17641790
}
17651791

17661792
interface IProcessService {
@@ -1950,6 +1976,12 @@ interface IOsInfo {
19501976
* @return {string} A string identifying the operating system bitness.
19511977
*/
19521978
arch(): string;
1979+
1980+
/**
1981+
* Returns a string identifying the operating system platform.
1982+
* @return {string} A string identifying the operating system platform.
1983+
*/
1984+
platform(): string;
19531985
}
19541986

19551987
interface IPromiseActions<T> {

os-info.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export class OsInfo implements IOsInfo {
1212
public arch(): string {
1313
return os.arch();
1414
}
15+
16+
public platform(): string {
17+
return os.platform();
18+
}
1519
}
1620

1721
$injector.register("osInfo", OsInfo);

services/net-service.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import * as net from "net";
2+
import { sleep } from "../helpers";
23

34
export class Net implements INet {
4-
constructor(private $errors: IErrors) { }
5+
private static DEFAULT_INTERVAL = 1000;
6+
7+
constructor(private $errors: IErrors,
8+
private $childProcess: IChildProcess,
9+
private $logger: ILogger,
10+
private $osInfo: IOsInfo) { }
511

612
public async getFreePort(): Promise<number> {
713
const server = net.createServer((sock: string) => { /* empty - noone will connect here */ });
@@ -71,6 +77,59 @@ export class Net implements INet {
7177

7278
return startPort;
7379
}
80+
81+
public async waitForPortToListen(waitForPortListenData: IWaitForPortListenData): Promise<boolean> {
82+
if (!waitForPortListenData) {
83+
this.$errors.failWithoutHelp("You must pass port and timeout for check.");
84+
}
85+
86+
const { timeout, port } = waitForPortListenData;
87+
const interval = waitForPortListenData.interval || Net.DEFAULT_INTERVAL;
88+
89+
const endTime = new Date().getTime() + timeout;
90+
const platformData: IDictionary<{ command: string, regex: RegExp }> = {
91+
"darwin": {
92+
command: "netstat -f inet -p tcp -anL",
93+
regex: new RegExp(`\\.${port}\\b`, "g")
94+
},
95+
"linux": {
96+
command: "netstat -tnl",
97+
regex: new RegExp(`:${port}\\s`, "g")
98+
},
99+
"win32": {
100+
command: "netstat -ant -p tcp",
101+
regex: new RegExp(`TCP\\s+(\\d+\\.){3}\\d+:${port}.*?LISTEN`, "g")
102+
}
103+
};
104+
105+
const platform = this.$osInfo.platform();
106+
const currentPlatformData = platformData[platform];
107+
if (!currentPlatformData) {
108+
this.$errors.failWithoutHelp(`Unable to check for free ports on ${platform}. Supported platforms are: ${_.keys(platformData).join(", ")}`);
109+
}
110+
111+
while (true) {
112+
const { command, regex } = currentPlatformData;
113+
114+
try {
115+
const result = await this.$childProcess.exec(command);
116+
if (result && !!result.match(regex)) {
117+
return true;
118+
}
119+
} catch (err) {
120+
this.$logger.trace(`Error while calling '${command}': ${err}`);
121+
}
122+
123+
const currentTime = new Date().getTime();
124+
if (currentTime >= endTime) {
125+
break;
126+
}
127+
128+
await sleep(interval);
129+
}
130+
131+
return false;
132+
}
74133
}
75134

76135
$injector.register("net", Net);
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { Net } from "../../../services/net-service";
2+
import { assert } from "chai";
3+
import { Yok } from "../../../yok";
4+
import { ErrorsStub, CommonLoggerStub } from "../stubs";
5+
import { EOL } from "os";
6+
7+
describe("net", () => {
8+
const createTestInjector = (platform: string): IInjector => {
9+
const testInjector = new Yok();
10+
testInjector.register("errors", ErrorsStub);
11+
testInjector.register("childProcess", {});
12+
testInjector.register("logger", CommonLoggerStub);
13+
testInjector.register("osInfo", {
14+
platform: () => platform
15+
});
16+
17+
return testInjector;
18+
};
19+
20+
describe("waitForPortToListen", () => {
21+
let execCalledCount = 0;
22+
beforeEach(() => {
23+
execCalledCount = 0;
24+
});
25+
26+
const createNetStatResult = (testInjector: IInjector, platform: string, port?: number, iteration?: number): void => {
27+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
28+
29+
childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => {
30+
const platformsData: IDictionary<any> = {
31+
linux: {
32+
data: `Active Internet connections (only servers)
33+
Proto Recv-Q Send-Q Local Address Foreign Address State
34+
tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN
35+
tcp 0 0 127.0.1.1:53 0.0.0.0:* LISTEN
36+
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN
37+
tcp6 0 0 :::60433 :::* LISTEN
38+
tcp6 0 0 ::1:631 :::* LISTEN`,
39+
portData: `tcp6 0 0 :::${port} :::* LISTEN`
40+
},
41+
42+
darwin: {
43+
data: `Current listen queue sizes (qlen/incqlen/maxqlen)
44+
Listen Local Address
45+
0/0/1 127.0.0.1.9335
46+
0/0/1 127.0.0.1.9334
47+
0/0/1 127.0.0.1.9333
48+
0/0/128 *.3283
49+
0/0/128 *.88
50+
0/0/128 *.22`,
51+
portData: `0/0/128 *.${port}`
52+
},
53+
win32: {
54+
data: `
55+
Active Connections
56+
57+
Proto Local Address Foreign Address State
58+
TCP 0.0.0.0:80 0.0.0.0:0 LISTENING
59+
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING
60+
TCP 0.0.0.0:60061 0.0.0.0:0 LISTENING
61+
TCP 127.0.0.1:5037 127.0.0.1:54737 ESTABLISHED
62+
TCP 127.0.0.1:5037 127.0.0.1:54741 ESTABLISHED
63+
TCP 127.0.0.1:5354 0.0.0.0:0 LISTENING`,
64+
portData: ` TCP 127.0.0.1:${port} 0.0.0.0:0 LISTENING`
65+
}
66+
};
67+
68+
execCalledCount++;
69+
70+
let data = platformsData[platform].data;
71+
72+
if (port) {
73+
data += `${EOL}${platformsData[platform].portData}`;
74+
}
75+
76+
if (iteration) {
77+
return iteration === execCalledCount ? data : "";
78+
}
79+
return data;
80+
};
81+
};
82+
83+
_.each(["linux", "darwin", "win32"], platform => {
84+
describe(`for ${platform}`, () => {
85+
it("returns true when netstat returns port is listening", async () => {
86+
const port = 18181;
87+
const testInjector = createTestInjector(platform);
88+
createNetStatResult(testInjector, platform, port);
89+
90+
const net = testInjector.resolve<INet>(Net);
91+
const isPortListening = await net.waitForPortToListen({ port, timeout: 10, interval: 1 });
92+
assert.isTrue(isPortListening);
93+
assert.equal(execCalledCount, 1);
94+
});
95+
96+
it("returns false when netstat does not return the port as not listening", async () => {
97+
const testInjector = createTestInjector(platform);
98+
createNetStatResult(testInjector, platform);
99+
100+
const net = testInjector.resolve<INet>(Net);
101+
const isPortListening = await net.waitForPortToListen({ port: 18181, timeout: 5, interval: 1 });
102+
assert.isFalse(isPortListening);
103+
assert.isTrue(execCalledCount > 1);
104+
});
105+
106+
it("returns true when netstat finds the port after some time", async () => {
107+
const port = 18181;
108+
const testInjector = createTestInjector(platform);
109+
const iterations = 2;
110+
createNetStatResult(testInjector, platform, port, iterations);
111+
112+
const net = testInjector.resolve<INet>(Net);
113+
const isPortListening = await net.waitForPortToListen({ port, timeout: 10, interval: 1 });
114+
assert.isTrue(isPortListening);
115+
assert.equal(execCalledCount, iterations);
116+
});
117+
118+
it("returns false when netstat command fails", async () => {
119+
const testInjector = createTestInjector(platform);
120+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
121+
const error = new Error("test error");
122+
childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => {
123+
execCalledCount++;
124+
return Promise.reject(error);
125+
};
126+
127+
const net = testInjector.resolve<INet>(Net);
128+
const isPortListening = await net.waitForPortToListen({ port: 18181, timeout: 50, interval: 1 });
129+
assert.isFalse(isPortListening);
130+
assert.isTrue(execCalledCount > 1);
131+
});
132+
});
133+
});
134+
135+
it("throws correct error when current operating system is not supported", async () => {
136+
const invalidPlatform = "invalid_platform";
137+
const testInjector = createTestInjector(invalidPlatform);
138+
139+
const net = testInjector.resolve<INet>(Net);
140+
await assert.isRejected(net.waitForPortToListen({ port: 18181, timeout: 50, interval: 1 }), `Unable to check for free ports on ${invalidPlatform}. Supported platforms are: darwin, linux, win32`);
141+
});
142+
143+
it("throws correct error when null is passed", async () => {
144+
const invalidPlatform = "invalid_platform";
145+
const testInjector = createTestInjector(invalidPlatform);
146+
147+
const net = testInjector.resolve<INet>(Net);
148+
await assert.isRejected(net.waitForPortToListen({ port: 18181, timeout: 50, interval: 1 }), "You must pass port and timeout for check.");
149+
150+
});
151+
});
152+
});

0 commit comments

Comments
 (0)