diff --git a/declarations.d.ts b/declarations.d.ts index ab81dd29..44b5c4bb 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -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. */ @@ -1761,6 +1780,13 @@ interface INet { * @return {Promise} true if the port is available. */ isPortAvailable(port: number): Promise; + + /** + * 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; } interface IProcessService { @@ -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 { diff --git a/os-info.ts b/os-info.ts index ecb889d7..63fbcc67 100644 --- a/os-info.ts +++ b/os-info.ts @@ -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); diff --git a/services/net-service.ts b/services/net-service.ts index d95636e0..7fd98bda 100644 --- a/services/net-service.ts +++ b/services/net-service.ts @@ -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 { const server = net.createServer((sock: string) => { /* empty - noone will connect here */ }); @@ -71,6 +77,59 @@ export class Net implements INet { return startPort; } + + public async waitForPortToListen(waitForPortListenData: IWaitForPortListenData): Promise { + 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) { + 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); diff --git a/test/unit-tests/services/net-service.ts b/test/unit-tests/services/net-service.ts new file mode 100644 index 00000000..38b0ff43 --- /dev/null +++ b/test/unit-tests/services/net-service.ts @@ -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("childProcess"); + + childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise => { + const platformsData: IDictionary = { + 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(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(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(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("childProcess"); + const error = new Error("test error"); + childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise => { + execCalledCount++; + return Promise.reject(error); + }; + + const net = testInjector.resolve(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(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(Net); + await assert.isRejected(net.waitForPortToListen(null), "You must pass port and timeout for check."); + + }); + }); +});