diff --git a/declarations.d.ts b/declarations.d.ts index a6c76943..98d4c794 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -1575,6 +1575,38 @@ interface IProgressIndicator { * @return {Promise} */ showProgressIndicator(promise: Promise, timeout: number, options?: { surpressTrailingNewLine?: boolean }): Promise; + + /** + * Returns a spinner instance that will print a specified message when spinner is started and will repeat it until spinner is stopped. + * In case the terminal is not interactive, a mocked instance is returned, so the spinner will print the required message a single time - when it is started. + * @param {string} message The message to be printed. + * @returns {ISpinner} Instance of clui.Spinner in case terminal is interactive, mocked instance otherwise. + */ + getSpinner(message: string): ISpinner; +} + +/** + * Describes the clui spinner. + */ +interface ISpinner { + /** + * Sets the message that will be printed by spinner. + * @param {string} msg The new message. + * @returns {void} + */ + message(msg: string): void; + + /** + * Starts the spinner. + * @returns {void} + */ + start(): void; + + /** + * Stops the spinner. + * @returns {void} + */ + stop(): void; } /** @@ -1715,6 +1747,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. */ @@ -1740,6 +1791,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 { @@ -1929,6 +1987,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/definitions/mobile.d.ts b/definitions/mobile.d.ts index 92076be7..40e0103f 100644 --- a/definitions/mobile.d.ts +++ b/definitions/mobile.d.ts @@ -580,8 +580,34 @@ declare module Mobile { iOSSimPath: string; } + /** + * Describes the information when trying to connect to port. + */ + interface IConnectToPortData { + /** + * The port to connect. + * @type {number} + */ + port: number; + + /** + * Timeout in milliseconds. + * @type {number} + */ + timeout?: number; + } + interface IiOSSimulatorService extends IEmulatorPlatformServices { postDarwinNotification(notification: string): Promise; + + /** + * Tries to connect to specified port for speciefied amount of time. + * In case it succeeds, a socket is returned. + * In case it fails, undefined is returned. + * @param {IConnectToPortData} connectToPortData Data describing port and timeout to try to connect. + * @returns {net.Socket} Returns instance of net.Socket when connection is successful, otherwise undefined is returned. + */ + connectToPort(connectToPortData: IConnectToPortData): Promise; } interface IEmulatorSettingsService { diff --git a/helpers.ts b/helpers.ts index a3b5182c..e0b6d747 100644 --- a/helpers.ts +++ b/helpers.ts @@ -5,9 +5,53 @@ import { ReadStream } from "tty"; import { Configurations } from "./constants"; import { EventEmitter } from "events"; import * as crypto from "crypto"; +import progress = require("progress-stream"); +import filesize = require("filesize"); +import * as util from "util"; const Table = require("cli-table"); +export function trackDownloadProgress(destinationStream: NodeJS.WritableStream, url: string): NodeJS.ReadableStream { + // \r for carriage return doesn't work on windows in node for some reason so we have to use it's hex representation \x1B[0G + let lastMessageSize = 0; + const carriageReturn = "\x1B[0G"; + let timeElapsed = 0; + + const isInteractiveTerminal = isInteractive(); + const progressStream = progress({ time: 1000 }, (progress: any) => { + timeElapsed = progress.runtime; + + if (timeElapsed >= 1) { + if (isInteractiveTerminal) { + this.$logger.write("%s%s", carriageReturn, Array(lastMessageSize + 1).join(" ")); + + const message = util.format("%sDownload progress ... %s | %s | %s/s", + carriageReturn, + Math.floor(progress.percentage) + "%", + filesize(progress.transferred), + filesize(progress.speed)); + + this.$logger.write(message); + lastMessageSize = message.length; + } + } + }); + + progressStream.on("finish", () => { + if (timeElapsed >= 1) { + const msg = `Download of ${url} completed.`; + if (isInteractiveTerminal) { + this.$logger.out("%s%s%s%s", carriageReturn, Array(lastMessageSize + 1).join(" "), carriageReturn, msg); + } else { + this.$logger.out(msg); + } + } + }); + + progressStream.pipe(destinationStream); + return progressStream; +} + export async function executeActionByChunks(initialData: T[] | IDictionary, chunkSize: number, elementAction: (element: T, key?: string | number) => Promise): Promise { let arrayToChunk: (T | string)[]; let action: (key: string | T) => Promise; diff --git a/http-client.ts b/http-client.ts index 9b46d385..0aef27e0 100644 --- a/http-client.ts +++ b/http-client.ts @@ -3,8 +3,6 @@ import { EOL } from "os"; import * as helpers from "./helpers"; import * as zlib from "zlib"; import * as util from "util"; -import progress = require("progress-stream"); -import filesize = require("filesize"); import { HttpStatusCodes } from "./constants"; import * as request from "request"; @@ -133,8 +131,6 @@ export class HttpClient implements Server.IHttpClient { this.setResponseResult(promiseActions, timerId, { response }); }); - pipeTo = this.trackDownloadProgress(pipeTo); - responseStream.pipe(pipeTo); } else { const data: string[] = []; @@ -204,39 +200,6 @@ export class HttpClient implements Server.IHttpClient { } } - private trackDownloadProgress(pipeTo: NodeJS.WritableStream): NodeJS.ReadableStream { - // \r for carriage return doesn't work on windows in node for some reason so we have to use it's hex representation \x1B[0G - let lastMessageSize = 0; - const carriageReturn = "\x1B[0G"; - let timeElapsed = 0; - - const progressStream = progress({ time: 1000 }, (progress: any) => { - timeElapsed = progress.runtime; - - if (timeElapsed >= 1) { - this.$logger.write("%s%s", carriageReturn, Array(lastMessageSize + 1).join(" ")); - - const message = util.format("%sDownload progress ... %s | %s | %s/s", - carriageReturn, - Math.floor(progress.percentage) + "%", - filesize(progress.transferred), - filesize(progress.speed)); - - this.$logger.write(message); - lastMessageSize = message.length; - } - }); - - progressStream.on("finish", () => { - if (timeElapsed >= 1) { - this.$logger.out("%s%s%s%s", carriageReturn, Array(lastMessageSize + 1).join(" "), carriageReturn, "Download completed."); - } - }); - - progressStream.pipe(pipeTo); - return progressStream; - } - private getErrorMessage(statusCode: number, body: string): string { if (statusCode === HttpStatusCodes.PROXY_AUTHENTICATION_REQUIRED) { const clientNameLowerCase = this.$staticConfig.CLIENT_NAME.toLowerCase(); @@ -262,7 +225,8 @@ export class HttpClient implements Server.IHttpClient { return err.Message; } } catch (parsingFailed) { - return `The server returned unexpected response: ${parsingFailed.toString()}`; + this.$logger.trace("Failed to get error from http request: ", parsingFailed); + return `The server returned unexpected response: ${body}`; } return body; diff --git a/mobile/ios/simulator/ios-emulator-services.ts b/mobile/ios/simulator/ios-emulator-services.ts index c1539246..49ee31a8 100644 --- a/mobile/ios/simulator/ios-emulator-services.ts +++ b/mobile/ios/simulator/ios-emulator-services.ts @@ -1,4 +1,9 @@ +import * as net from "net"; +import { connectEventuallyUntilTimeout } from "../../../helpers"; + class IosEmulatorServices implements Mobile.IiOSSimulatorService { + private static DEFAULT_TIMEOUT = 10000; + constructor(private $logger: ILogger, private $emulatorSettingsService: Mobile.IEmulatorSettingsService, private $errors: IErrors, @@ -58,6 +63,15 @@ class IosEmulatorServices implements Mobile.IiOSSimulatorService { await this.$childProcess.spawnFromEvent(nodeCommandName, iosSimArgs, "close", { stdio: "inherit" }); } + public async connectToPort(data: Mobile.IConnectToPortData): Promise { + try { + const socket = await connectEventuallyUntilTimeout(() => net.connect(data.port), data.timeout || IosEmulatorServices.DEFAULT_TIMEOUT); + return socket; + } catch (e) { + this.$logger.debug(e); + } + } + private async runApplicationOnEmulatorCore(app: string, emulatorOptions?: Mobile.IEmulatorOptions): Promise { this.$logger.info("Starting iOS Simulator"); const iosSimPath = this.$iOSSimResolver.iOSSimPath; 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/progress-indicator.ts b/progress-indicator.ts index fddd9502..41f36b14 100644 --- a/progress-indicator.ts +++ b/progress-indicator.ts @@ -1,23 +1,29 @@ +import { isInteractive } from './helpers'; + +const clui = require("clui"); + export class ProgressIndicator implements IProgressIndicator { constructor(private $logger: ILogger) { } public async showProgressIndicator(promise: Promise, timeout: number, options?: { surpressTrailingNewLine?: boolean }): Promise { const surpressTrailingNewLine = options && options.surpressTrailingNewLine; - let isResolved = false; + let isFulfilled = false; const tempPromise = new Promise((resolve, reject) => { promise.then((res) => { - isResolved = true; + isFulfilled = true; resolve(res); }, (err) => { - isResolved = true; + isFulfilled = true; reject(err); }); }); - while (!isResolved) { - await this.$logger.printMsgWithTimeout(".", timeout); + if (!isInteractive()) { + while (!isFulfilled) { + await this.$logger.printMsgWithTimeout(".", timeout); + } } if (!surpressTrailingNewLine) { @@ -26,5 +32,18 @@ export class ProgressIndicator implements IProgressIndicator { return tempPromise; } + + public getSpinner(message: string): ISpinner { + if (isInteractive()) { + return new clui.Spinner(message); + } else { + let msg = message; + return { + start: () => this.$logger.info(msg), + message: (newMsg: string) => msg = newMsg, + stop: (): void => undefined + }; + } + } } $injector.register("progressIndicator", ProgressIndicator); 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."); + + }); + }); +});