From 4e7fa14ad2ae3a1abd470768991d396d3719a34c Mon Sep 17 00:00:00 2001 From: Dimitar Kerezov Date: Thu, 14 Dec 2017 12:57:31 +0200 Subject: [PATCH 1/8] Replace rename with write+delete On some systems with slower hard drives and rigorous antivirus software, on numerous occasions the newly created files in `platforms` directory are locked and cannot be renamed. The rename fails with `EPERM`. Switching to write+delete in favor of rename eliminates said issue. --- services/project-files-manager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/project-files-manager.ts b/services/project-files-manager.ts index 944d62ff..3576183b 100644 --- a/services/project-files-manager.ts +++ b/services/project-files-manager.ts @@ -75,7 +75,9 @@ export class ProjectFilesManager implements IProjectFilesManager { this.$fs.writeFile(filePath, fileContent); } // Rename the file - this.$fs.rename(filePath, onDeviceFilePath); + // this.$fs.rename is not called as it is error prone on some systems with slower hard drives and rigorous antivirus software + this.$fs.writeFile(onDeviceFilePath, this.$fs.readText(filePath)); + this.$fs.deleteFile(filePath); } } }); From e21316df734942cae8f703d414c2ca5cea615fbe Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Fri, 29 Dec 2017 09:45:13 +0200 Subject: [PATCH 2/8] Fix detection of Javac version The command `javac -version` prints result to stderr when JAVA 8 is used and to stdout when JAVA 9 is used. Current check in CLI uses the stderr output, so when JAVA 9 is installed it fails to detect the correct version. In order to support both JAVA 8 and JAVA 9, capture both stdout and stderr and get the version from there. Also remove unneeded check for Java version - we care about JAVA Compiler, which is included in JDK. --- declarations.d.ts | 5 ----- sys-info-base.ts | 17 +--------------- test/unit-tests/sys-info-base.ts | 34 +++++++++++++++++++++++--------- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/declarations.d.ts b/declarations.d.ts index 3cb7d64d..7a9987b0 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -1123,8 +1123,6 @@ interface ISysInfoData extends IPlatform { nodeGypVer: string; // dependencies - /** version of java, as returned by `java -version` */ - javaVer: string; /** Xcode version string as returned by `xcodebuild -version`. Valid only on Mac */ xcodeVer: string; /** Version string of adb, as returned by `adb version` */ @@ -1156,9 +1154,6 @@ interface ISysInfo { */ getSysInfo(pathToPackageJson: string, androidToolsInfo?: { pathToAdb: string }): Promise; - /** Returns Java version. **/ - getJavaVersion(): Promise; - /** Returns Java compiler version. **/ getJavaCompilerVersion(): Promise; diff --git a/sys-info-base.ts b/sys-info-base.ts index f8d2cc0e..ba302ba2 100644 --- a/sys-info-base.ts +++ b/sys-info-base.ts @@ -13,19 +13,6 @@ export class SysInfoBase implements ISysInfo { private monoVerRegExp = /version (\d+[.]\d+[.]\d+) /gm; private sysInfoCache: ISysInfoData = undefined; - private javaVerCache: string = null; - public async getJavaVersion(): Promise { - if (!this.javaVerCache) { - try { - // different java has different format for `java -version` command - const output = (await this.$childProcess.spawnFromEvent("java", ["-version"], "exit")).stderr; - this.javaVerCache = /(?:openjdk|java) version \"((?:\d+\.)+(?:\d+))/i.exec(output)[1]; - } catch (e) { - this.javaVerCache = null; - } - } - return this.javaVerCache; - } private npmVerCache: string = null; public async getNpmVersion(): Promise { @@ -47,7 +34,7 @@ export class SysInfoBase implements ISysInfo { const output = await this.exec(`"${pathToJavaCompilerExecutable}" -version`, { showStderr: true }); // for other versions of java javac version output is not on first line // thus can't use ^ for starts with in regex - this.javaCompilerVerCache = output ? /javac (.*)/i.exec(output.stderr)[1] : null; + this.javaCompilerVerCache = output ? /javac (.*)/i.exec(`${output.stderr}${os.EOL}${output.stdout}`)[1] : null; } catch (e) { this.$logger.trace(`Command "${pathToJavaCompilerExecutable} --version" failed: ${e}`); this.javaCompilerVerCache = null; @@ -154,8 +141,6 @@ export class SysInfoBase implements ISysInfo { res.npmVer = await this.getNpmVersion(); - res.javaVer = await this.getJavaVersion(); - res.nodeGypVer = await this.getNodeGypVersion(); res.xcodeVer = await this.getXCodeVersion(); res.xcodeprojGemLocation = await this.getXCodeProjGemLocation(); diff --git a/test/unit-tests/sys-info-base.ts b/test/unit-tests/sys-info-base.ts index 3452b302..b3213b47 100644 --- a/test/unit-tests/sys-info-base.ts +++ b/test/unit-tests/sys-info-base.ts @@ -19,7 +19,6 @@ interface IChildProcessResultDescription { interface IChildProcessResults { uname: IChildProcessResultDescription; npmV: IChildProcessResultDescription; - javaVersion: IChildProcessResultDescription; javacVersion: IChildProcessResultDescription; nodeGypVersion: IChildProcessResultDescription; xCodeVersion: IChildProcessResultDescription; @@ -32,7 +31,7 @@ interface IChildProcessResults { } function getResultFromChildProcess(childProcessResultDescription: IChildProcessResultDescription, spawnFromEventOpts?: { throwError: boolean }): any { - if (childProcessResultDescription.shouldThrowError) { + if (!childProcessResultDescription || childProcessResultDescription.shouldThrowError) { if (spawnFromEventOpts && !spawnFromEventOpts.throwError) { return { stderr: "This one throws error.", @@ -51,7 +50,6 @@ function createChildProcessResults(childProcessResult: IChildProcessResults): ID return { "uname -a": childProcessResult.uname, "npm -v": childProcessResult.npmV, - "java": childProcessResult.javaVersion, '"javac" -version': childProcessResult.javacVersion, "node-gyp -v": childProcessResult.nodeGypVersion, "xcodebuild -version": childProcessResult.xCodeVersion, @@ -65,7 +63,7 @@ function createChildProcessResults(childProcessResult: IChildProcessResults): ID }; } -function createTestInjector(childProcessResult: IChildProcessResults, hostInfoData: { isWindows: boolean, dotNetVersion: string, isDarwin: boolean }, itunesError: string): IInjector { +function createTestInjector(childProcessResult: IChildProcessResults, hostInfoData: { isWindows: boolean, dotNetVersion?: string, isDarwin: boolean }, itunesError: string): IInjector { const injector = new Yok(); const childProcessResultDictionary = createChildProcessResults(childProcessResult); injector.register("childProcess", { @@ -79,7 +77,7 @@ function createTestInjector(childProcessResult: IChildProcessResults, hostInfoDa }); injector.register("hostInfo", { - dotNetVersion: () => Promise.resolve(hostInfoData.dotNetVersion), + dotNetVersion: () => Promise.resolve(hostInfoData.dotNetVersion || "4.5.1"), isWindows: hostInfoData.isWindows, isDarwin: hostInfoData.isDarwin }); @@ -123,7 +121,6 @@ describe("sysInfoBase", () => { childProcessResult = { uname: { result: "name" }, npmV: { result: "2.14.1" }, - javaVersion: { result: { stderr: 'java version "1.8.0_60"' } }, javacVersion: { result: { stderr: 'javac 1.8.0_60' } }, nodeGypVersion: { result: "2.0.0" }, xCodeVersion: { result: "6.4.0" }, @@ -142,7 +139,6 @@ describe("sysInfoBase", () => { describe("returns correct results when everything is installed", () => { const assertCommonValues = (result: ISysInfoData) => { assert.deepEqual(result.npmVer, childProcessResult.npmV.result); - assert.deepEqual(result.javaVer, "1.8.0"); assert.deepEqual(result.javacVersion, "1.8.0_60"); assert.deepEqual(result.nodeGypVer, childProcessResult.nodeGypVersion.result); assert.deepEqual(result.adbVer, childProcessResult.adbVersion.result); @@ -214,12 +210,33 @@ describe("sysInfoBase", () => { }); }); + describe("getJavaCompilerVersion", () => { + const verifyJavaCompilerVersion = async (javaCompilerVersion: string, resultStream: string): Promise => { + const javaCompilerChildProcessResult: any = { + [resultStream]: `javac ${javaCompilerVersion}` + }; + + javaCompilerChildProcessResult.javacVersion = { result: javaCompilerChildProcessResult }; + testInjector = createTestInjector(javaCompilerChildProcessResult, { isWindows: false, isDarwin: true }, null); + sysInfoBase = testInjector.resolve("sysInfoBase"); + const actualJavaCompilerVersion = await sysInfoBase.getJavaCompilerVersion(); + assert.deepEqual(actualJavaCompilerVersion, javaCompilerVersion); + }; + + it("returns correct javac version when it is printed on stderr (Java 8)", () => { + return verifyJavaCompilerVersion("1.8.0_152", "stderr"); + }); + + it("returns correct javac version when it is printed on stdout (Java 9)", () => { + return verifyJavaCompilerVersion("9.0.1", "stdout"); + }); + }); + describe("returns correct results when exceptions are raised during sysInfo data collection", () => { beforeEach(() => { childProcessResult = { uname: { shouldThrowError: true }, npmV: { shouldThrowError: true }, - javaVersion: { shouldThrowError: true }, javacVersion: { shouldThrowError: true }, nodeGypVersion: { shouldThrowError: true }, xCodeVersion: { shouldThrowError: true }, @@ -253,7 +270,6 @@ describe("sysInfoBase", () => { sysInfoBase = testInjector.resolve("sysInfoBase"); const result = await sysInfoBase.getSysInfo(toolsPackageJson); assert.deepEqual(result.npmVer, null); - assert.deepEqual(result.javaVer, null); assert.deepEqual(result.javacVersion, null); assert.deepEqual(result.nodeGypVer, null); assert.deepEqual(result.xcodeVer, null); From 49e89bde97d4aa60e4cfe47c75fe0ff8c7efdf09 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 2 Jan 2018 23:30:16 +0200 Subject: [PATCH 3/8] Improve appendZeroesToVersion and add unit tests Improve `appendZeroesToVersion` method to return the passed value in case it is null, undefined or empty string and add tests for the method. --- helpers.ts | 14 +++++++--- test/unit-tests/helpers.ts | 55 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/helpers.ts b/helpers.ts index a0fe50ca..a3b5182c 100644 --- a/helpers.ts +++ b/helpers.ts @@ -290,10 +290,18 @@ export async function getFuturesResults(promises: Promise[], predica .value(); } +/** + * Appends zeroes to a version string until it reaches a specified length. + * @param {string} version The version on which to append zeroes. + * @param requiredVersionLength The required length of the version string. + * @returns {string} Appended version string. In case input is null, undefined or empty string, it is returned immediately without appending anything. + */ export function appendZeroesToVersion(version: string, requiredVersionLength: number): string { - const zeroesToAppend = requiredVersionLength - version.split(".").length; - for (let index = 0; index < zeroesToAppend; index++) { - version += ".0"; + if (version) { + const zeroesToAppend = requiredVersionLength - version.split(".").length; + for (let index = 0; index < zeroesToAppend; index++) { + version += ".0"; + } } return version; diff --git a/test/unit-tests/helpers.ts b/test/unit-tests/helpers.ts index 3b1f3e9e..4e8f0409 100644 --- a/test/unit-tests/helpers.ts +++ b/test/unit-tests/helpers.ts @@ -15,6 +15,61 @@ describe("helpers", () => { assert.deepEqual(actualResult, testData.expectedResult, `For input ${testData.input}, the expected result is: ${testData.expectedResult}, but actual result is: ${actualResult}.`); }; + describe("appendZeroesToVersion", () => { + interface IAppendZeroesToVersionTestData extends ITestData { + requiredVersionLength: number; + } + + const testData: IAppendZeroesToVersionTestData[] = [ + { + input: "3.0.0", + requiredVersionLength: 3, + expectedResult: "3.0.0" + }, + { + input: "3.0", + requiredVersionLength: 3, + expectedResult: "3.0.0" + }, + { + input: "3", + requiredVersionLength: 3, + expectedResult: "3.0.0" + }, + { + input: "1.8.0_152", + requiredVersionLength: 3, + expectedResult: "1.8.0_152" + }, + { + input: "", + requiredVersionLength: 3, + expectedResult: "" + }, + { + input: null, + requiredVersionLength: 3, + expectedResult: null + }, + { + input: undefined, + requiredVersionLength: 3, + expectedResult: undefined + }, + { + input: "1", + requiredVersionLength: 5, + expectedResult: "1.0.0.0.0" + }, + ]; + + it("appends correct number of zeroes", () => { + _.each(testData, testCase => { + assert.deepEqual(helpers.appendZeroesToVersion(testCase.input, testCase.requiredVersionLength), testCase.expectedResult); + }); + }); + }); + describe("executeActionByChunks", () => { const chunkSize = 2; From 62a5d6e1b373a353f7d80907ccc76753b95e411a Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Thu, 11 Jan 2018 02:28:04 +0200 Subject: [PATCH 4/8] Return correct error when response is not JSON In some cases, when any of the requests fail, the response is not JSON (the servers do not respect the Accept header of the request) and return html. In this case the JSON.parse operation that we execute in order to find the real error, is failing and we return error that we are unable to parse the result. This leads to confusion in many cases, so return the real response of the server instead. So when html is returned, it will be included in the error. This will help the users when they have some firewall issues and they are unable to connect to our servers. In this case they almost always receive html in which it is stated that this page is forbidded and they should contact their admins. --- http-client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/http-client.ts b/http-client.ts index 9b46d385..907b20fb 100644 --- a/http-client.ts +++ b/http-client.ts @@ -262,7 +262,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; From da7ba0fdac3d7765609aaf52c9da81649796234b Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Thu, 11 Jan 2018 01:50:08 +0200 Subject: [PATCH 5/8] Add getSpinner method to $progressIndicator Add `getSpinner` method to $progressIndicator - it will return a new instance of clui.Spinner in case terminal is interactive. In case it is not - a mocked instance will be returned. This way in CI builds the spinner will not print tons of repeated messages. --- declarations.d.ts | 32 ++++++++++++++++++++++++++++++++ progress-indicator.ts | 29 ++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/declarations.d.ts b/declarations.d.ts index 7a9987b0..ab81dd29 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -1564,6 +1564,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; } /** 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); From 93db157a7c1d124a76a93cf5facc4e0589b54391 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 23 Jan 2018 15:18:47 +0200 Subject: [PATCH 6/8] Remove progress indicator when piping http requests Currently all http request can be piped in httpClient, which is uses when we want to download some file. Each of these pipes is passed through progressStream, so we can print some indication on the same line that something happens. We do not want this in CI environment. Also it is strange to print message like "Download completed" in the middle of any operation. So remove the progress indicator (in fact the percentage part of it has been broken for a while). Extract the method in helpers in case we need it in the future. --- helpers.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ http-client.ts | 37 ------------------------------------- 2 files changed, 44 insertions(+), 37 deletions(-) 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 907b20fb..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(); From 9b0b11465b1cbd76e71ab72d9b473ae9d8273c2b Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 23 Jan 2018 17:57:28 +0200 Subject: [PATCH 7/8] Introduce connectToPort method to iOSEmulatorServices Introduce new method - connectToPort to iOSEmulatorServices. This method will try to connect to specified port on users machine (typically this port will be used by NativeScript applications runnin on iOS Simulator) for a specified amount of time. In case it succeeds, the socket of the connection is returned. In case it fails, undefined is returned. --- definitions/mobile.d.ts | 26 +++++++++++++++++++ mobile/ios/simulator/ios-emulator-services.ts | 14 ++++++++++ 2 files changed, 40 insertions(+) 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/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; From a3beed4e18892ca4a4a557d00e58e220859b0917 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Fri, 26 Jan 2018 14:18:08 +0200 Subject: [PATCH 8/8] 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. --- declarations.d.ts | 32 +++++ os-info.ts | 4 + services/net-service.ts | 61 +++++++++- test/unit-tests/services/net-service.ts | 152 ++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 test/unit-tests/services/net-service.ts 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."); + + }); + }); +});