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

feat(net): Add method to check if port is in LISTEN state #1043

Merged
merged 1 commit into from
Jan 30, 2018
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
32 changes: 32 additions & 0 deletions declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,25 @@ interface IVersionData {
patch: string;
}

interface IWaitForPortListenData {
/**
* Port to be checked.
* @type {number}
*/
port: number;

/**
* Max amount of time in milliseconds to wait.
* @type {number}
*/
timeout: number;
/**
* @optional The amount of time between each check.
* @type {number}
*/
interval?: number;
}

/**
* Wrapper for net module of Node.js.
*/
Expand All @@ -1761,6 +1780,13 @@ interface INet {
* @return {Promise<boolean>} true if the port is available.
*/
isPortAvailable(port: number): Promise<boolean>;

/**
* Waits for port to be in LISTEN state.
* @param {IWaitForPortListenData} waitForPortListenData Data describing port, timeout and interval.
* @returns {boolean} true in case port is in LISTEN state, false otherwise.
*/
waitForPortToListen(waitForPortListenData: IWaitForPortListenData): Promise<boolean>;
}

interface IProcessService {
Expand Down Expand Up @@ -1950,6 +1976,12 @@ interface IOsInfo {
* @return {string} A string identifying the operating system bitness.
*/
arch(): string;

/**
* Returns a string identifying the operating system platform.
* @return {string} A string identifying the operating system platform.
*/
platform(): string;
}

interface IPromiseActions<T> {
Expand Down
4 changes: 4 additions & 0 deletions os-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export class OsInfo implements IOsInfo {
public arch(): string {
return os.arch();
}

public platform(): string {
return os.platform();
}
}

$injector.register("osInfo", OsInfo);
61 changes: 60 additions & 1 deletion services/net-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import * as net from "net";
import { sleep } from "../helpers";

export class Net implements INet {
constructor(private $errors: IErrors) { }
private static DEFAULT_INTERVAL = 1000;

constructor(private $errors: IErrors,
private $childProcess: IChildProcess,
private $logger: ILogger,
private $osInfo: IOsInfo) { }

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

return startPort;
}

public async waitForPortToListen(waitForPortListenData: IWaitForPortListenData): Promise<boolean> {
if (!waitForPortListenData) {
this.$errors.failWithoutHelp("You must pass port and timeout for check.");
}

const { timeout, port } = waitForPortListenData;
const interval = waitForPortListenData.interval || Net.DEFAULT_INTERVAL;

const endTime = new Date().getTime() + timeout;
const platformData: IDictionary<{ command: string, regex: RegExp }> = {
"darwin": {
command: "netstat -f inet -p tcp -anL",
regex: new RegExp(`\\.${port}\\b`, "g")
},
"linux": {
command: "netstat -tnl",
regex: new RegExp(`:${port}\\s`, "g")
},
"win32": {
command: "netstat -ant -p tcp",
regex: new RegExp(`TCP\\s+(\\d+\\.){3}\\d+:${port}.*?LISTEN`, "g")
}
};

const platform = this.$osInfo.platform();
const currentPlatformData = platformData[platform];
if (!currentPlatformData) {
this.$errors.failWithoutHelp(`Unable to check for free ports on ${platform}. Supported platforms are: ${_.keys(platformData).join(", ")}`);
}

while (true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of while (true) you can set the condition while (new Date().getTime() < endTime) and delete the break; code below.

IMO it'd be a bit clearer than looking at what seems to be an endless loop at a first glance

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The current flow executes at least one check, no matter of the passed timeout. Changing the while statement will change this behavior.
Also the current workflow checks if a timeout is passed and performs at least one check after that. Changing the while statement will change this behavior as well, as we have sleep at the end of the while. The sleep cannot be moved at the first line of the while as we may not have to wait for it.
So the desired workflow is:

  1. Check if the port is in LISTEN state.
  2. If yes - break. If not, check if timeout is passed and in case not - sleep for specified interval.
  3. Check if port is in LISTEN state.
  4. If yes - break. If not, check if timeout is passed and in case not - sleep for specified interval.
    ... Repeat until timeout is reached.

const { command, regex } = currentPlatformData;

try {
const result = await this.$childProcess.exec(command);
if (result && !!result.match(regex)) {
return true;
}
} catch (err) {
this.$logger.trace(`Error while calling '${command}': ${err}`);
}

const currentTime = new Date().getTime();
if (currentTime >= endTime) {
break;
}

await sleep(interval);
}

return false;
}
}

$injector.register("net", Net);
152 changes: 152 additions & 0 deletions test/unit-tests/services/net-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Net } from "../../../services/net-service";
import { assert } from "chai";
import { Yok } from "../../../yok";
import { ErrorsStub, CommonLoggerStub } from "../stubs";
import { EOL } from "os";

describe("net", () => {
const createTestInjector = (platform: string): IInjector => {
const testInjector = new Yok();
testInjector.register("errors", ErrorsStub);
testInjector.register("childProcess", {});
testInjector.register("logger", CommonLoggerStub);
testInjector.register("osInfo", {
platform: () => platform
});

return testInjector;
};

describe("waitForPortToListen", () => {
let execCalledCount = 0;
beforeEach(() => {
execCalledCount = 0;
});

const createNetStatResult = (testInjector: IInjector, platform: string, port?: number, iteration?: number): void => {
const childProcess = testInjector.resolve<IChildProcess>("childProcess");

childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => {
const platformsData: IDictionary<any> = {
linux: {
data: `Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN
tcp 0 0 127.0.1.1:53 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN
tcp6 0 0 :::60433 :::* LISTEN
tcp6 0 0 ::1:631 :::* LISTEN`,
portData: `tcp6 0 0 :::${port} :::* LISTEN`
},

darwin: {
data: `Current listen queue sizes (qlen/incqlen/maxqlen)
Listen Local Address
0/0/1 127.0.0.1.9335
0/0/1 127.0.0.1.9334
0/0/1 127.0.0.1.9333
0/0/128 *.3283
0/0/128 *.88
0/0/128 *.22`,
portData: `0/0/128 *.${port}`
},
win32: {
data: `
Active Connections

Proto Local Address Foreign Address State
TCP 0.0.0.0:80 0.0.0.0:0 LISTENING
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING
TCP 0.0.0.0:60061 0.0.0.0:0 LISTENING
TCP 127.0.0.1:5037 127.0.0.1:54737 ESTABLISHED
TCP 127.0.0.1:5037 127.0.0.1:54741 ESTABLISHED
TCP 127.0.0.1:5354 0.0.0.0:0 LISTENING`,
portData: ` TCP 127.0.0.1:${port} 0.0.0.0:0 LISTENING`
}
};

execCalledCount++;

let data = platformsData[platform].data;

if (port) {
data += `${EOL}${platformsData[platform].portData}`;
}

if (iteration) {
return iteration === execCalledCount ? data : "";
}
return data;
};
};

_.each(["linux", "darwin", "win32"], platform => {
describe(`for ${platform}`, () => {
it("returns true when netstat returns port is listening", async () => {
const port = 18181;
const testInjector = createTestInjector(platform);
createNetStatResult(testInjector, platform, port);

const net = testInjector.resolve<INet>(Net);
const isPortListening = await net.waitForPortToListen({ port, timeout: 10, interval: 1 });
assert.isTrue(isPortListening);
assert.equal(execCalledCount, 1);
});

it("returns false when netstat does not return the port as not listening", async () => {
const testInjector = createTestInjector(platform);
createNetStatResult(testInjector, platform);

const net = testInjector.resolve<INet>(Net);
const isPortListening = await net.waitForPortToListen({ port: 18181, timeout: 5, interval: 1 });
assert.isFalse(isPortListening);
assert.isTrue(execCalledCount > 1);
});

it("returns true when netstat finds the port after some time", async () => {
const port = 18181;
const testInjector = createTestInjector(platform);
const iterations = 2;
createNetStatResult(testInjector, platform, port, iterations);

const net = testInjector.resolve<INet>(Net);
const isPortListening = await net.waitForPortToListen({ port, timeout: 10, interval: 1 });
assert.isTrue(isPortListening);
assert.equal(execCalledCount, iterations);
});

it("returns false when netstat command fails", async () => {
const testInjector = createTestInjector(platform);
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
const error = new Error("test error");
childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => {
execCalledCount++;
return Promise.reject(error);
};

const net = testInjector.resolve<INet>(Net);
const isPortListening = await net.waitForPortToListen({ port: 18181, timeout: 50, interval: 1 });
assert.isFalse(isPortListening);
assert.isTrue(execCalledCount > 1);
});
});
});

it("throws correct error when current operating system is not supported", async () => {
const invalidPlatform = "invalid_platform";
const testInjector = createTestInjector(invalidPlatform);

const net = testInjector.resolve<INet>(Net);
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`);
});

it("throws correct error when null is passed", async () => {
const invalidPlatform = "invalid_platform";
const testInjector = createTestInjector(invalidPlatform);

const net = testInjector.resolve<INet>(Net);
await assert.isRejected(net.waitForPortToListen(null), "You must pass port and timeout for check.");

});
});
});