Skip to content

Commit

Permalink
Get debugger port from device logs
Browse files Browse the repository at this point in the history
  • Loading branch information
Fatme committed May 11, 2018
1 parent a51a706 commit 9e5390d
Show file tree
Hide file tree
Showing 16 changed files with 553 additions and 69 deletions.
3 changes: 3 additions & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,6 @@ $injector.require("nativeScriptCloudExtensionService", "./services/nativescript-
$injector.requireCommand("resources|generate|icons", "./commands/generate-assets");
$injector.requireCommand("resources|generate|splashes", "./commands/generate-assets");
$injector.requirePublic("assetsGenerationService", "./services/assets-generation/assets-generation-service");

$injector.require("iOSLogParserService", "./services/ios-log-parser-service");
$injector.require("iOSDebuggerPortService", "./services/ios-debugger-port-service");
4 changes: 4 additions & 0 deletions lib/definitions/debug.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ interface IDebugOptions {
* Defines if the iOS App Inspector should be used instead of providing URL to debug the application with Chrome DevTools
*/
inspector?: boolean;
/**
* Defines if should print all availableDevices
*/
availableDevices?: boolean;
}

/**
Expand Down
28 changes: 28 additions & 0 deletions lib/definitions/ios-debugger-port-service.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
interface IIOSDebuggerPortInputData {
deviceId: string;
appId: string;
}

interface IIOSDebuggerPortData extends IIOSDebuggerPortInputData {
port: number;
}

interface IIOSDebuggerPortStoredData {
port: number;
timer?: NodeJS.Timer;
}

interface IIOSDebuggerPortService {
/**
* Gets iOS debugger port for specified deviceId and appId
* @param {IIOSDebuggerPortInputData} data - Describes deviceId and appId
*/
getPort(data: IIOSDebuggerPortInputData): Promise<number>;
/**
* Attaches on DEBUGGER_PORT_FOUND event and STARTING_IOS_APPLICATION events
* In case when DEBUGGER_PORT_FOUND event is emitted, stores the port and clears the timeout if such.
* In case when STARTING_IOS_APPLICATION event is emiited, sets the port to null and add timeout for 5000 miliseconds which checks if port is null and prints a warning.
* @param {Mobile.IDevice} device - Describes the device which logs should be parsed.
*/
attachToDebuggerPortFoundEvent(device: Mobile.IDevice): void;
}
7 changes: 7 additions & 0 deletions lib/definitions/ios-log-parser-service.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface IIOSLogParserService extends NodeJS.EventEmitter {
/**
* Starts looking for debugger port. Attaches on device logs and processes them. In case when port is found, DEBUGGER_PORT_FOUND event is emitted.
* @param {Mobile.IDevice} device - Describes the device which logs will be processed.
*/
startLookingForDebuggerPort(device: Mobile.IDevice): void;
}
3 changes: 2 additions & 1 deletion lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ interface IProjectTemplatesService {

interface IPlatformProjectServiceBase {
getPluginPlatformsFolderPath(pluginData: IPluginData, platform: string): string;
getFrameworkVersion(projectData: IProjectData): string;
}

interface IBuildForDevice {
Expand Down Expand Up @@ -270,7 +271,7 @@ interface ILocalBuildService {

interface ICleanNativeAppData extends IProjectDir, IPlatform { }

interface IPlatformProjectService extends NodeJS.EventEmitter {
interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectServiceBase {
getPlatformData(projectData: IProjectData): IPlatformData;
validate(projectData: IProjectData): Promise<void>;
createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, config: ICreateProjectOptions): Promise<void>;
Expand Down
69 changes: 18 additions & 51 deletions lib/services/ios-debug-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,13 @@ import { ChildProcess } from "child_process";
import { DebugServiceBase } from "./debug-service-base";
import { CONNECTION_ERROR_EVENT_NAME, AWAIT_NOTIFICATION_TIMEOUT_SECONDS } from "../constants";
import { getPidFromiOSSimulatorLogs } from "../common/helpers";

import byline = require("byline");

const inspectorBackendPort = 18181;
const inspectorAppName = "NativeScript Inspector.app";
const inspectorNpmPackageName = "tns-ios-inspector";
const inspectorUiDir = "WebInspectorUI/";

export class IOSDebugService extends DebugServiceBase implements IPlatformDebugService {
private _lldbProcess: ChildProcess;
private _sockets: net.Socket[] = [];
private _childProcess: ChildProcess;
private _socketProxy: any;

constructor(protected device: Mobile.IDevice,
Expand All @@ -29,11 +24,11 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
private $logger: ILogger,
private $errors: IErrors,
private $npmInstallationManager: INpmInstallationManager,
private $iOSDebuggerPortService: IIOSDebuggerPortService,
private $iOSNotification: IiOSNotification,
private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor,
private $processService: IProcessService,
private $socketProxyFactory: ISocketProxyFactory,
private $net: INet,
private $projectDataService: IProjectDataService) {
super(device, $devicesService);
this.$processService.attachToProcessExitSignals(this, this.debugStop);
Expand Down Expand Up @@ -90,11 +85,6 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
await this.killProcess(this._lldbProcess);
this._lldbProcess = undefined;
}

if (this._childProcess) {
await this.killProcess(this._childProcess);
this._childProcess = undefined;
}
}

protected getChromeDebugUrl(debugOptions: IDebugOptions, port: number): string {
Expand All @@ -116,38 +106,21 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS

private async emulatorDebugBrk(debugData: IDebugData, debugOptions: IDebugOptions): Promise<string> {
const args = debugOptions.debugBrk ? "--nativescript-debug-brk" : "--nativescript-debug-start";
const child_process = await this.$iOSEmulatorServices.runApplicationOnEmulator(debugData.pathToAppPackage, {
const launchResult = await this.$iOSEmulatorServices.runApplicationOnEmulator(debugData.pathToAppPackage, {
waitForDebugger: true,
captureStdin: true,
args: args,
appId: debugData.applicationIdentifier,
skipInstall: true
});

const lineStream = byline(child_process.stdout);
this._childProcess = child_process;

lineStream.on('data', (line: NodeBuffer) => {
const lineText = line.toString();
if (lineText && _.startsWith(lineText, debugData.applicationIdentifier)) {
const pid = getPidFromiOSSimulatorLogs(debugData.applicationIdentifier, lineText);
if (!pid) {
this.$logger.trace(`Line ${lineText} does not contain PID of the application ${debugData.applicationIdentifier}.`);
return;
}

this._lldbProcess = this.$childProcess.spawn("lldb", ["-p", pid]);
if (log4js.levels.TRACE.isGreaterThanOrEqualTo(this.$logger.getLevel())) {
this._lldbProcess.stdout.pipe(process.stdout);
}
this._lldbProcess.stderr.pipe(process.stderr);
this._lldbProcess.stdin.write("process continue\n");
} else {
process.stdout.write(line + "\n");
}
});

await this.waitForBackendPortToBeOpened(debugData.deviceIdentifier);
const pid = getPidFromiOSSimulatorLogs(debugData.applicationIdentifier, launchResult);
this._lldbProcess = this.$childProcess.spawn("lldb", ["-p", pid]);
if (log4js.levels.TRACE.isGreaterThanOrEqualTo(this.$logger.getLevel())) {
this._lldbProcess.stdout.pipe(process.stdout);
}
this._lldbProcess.stderr.pipe(process.stderr);
this._lldbProcess.stdin.write("process continue\n");

return this.wireDebuggerClient(debugData, debugOptions);
}
Expand All @@ -158,20 +131,10 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
const attachRequestMessage = this.$iOSNotification.getAttachRequest(debugData.applicationIdentifier);

const iOSEmulatorService = <Mobile.IiOSSimulatorService>this.$iOSEmulatorServices;
await iOSEmulatorService.postDarwinNotification(attachRequestMessage);
await this.waitForBackendPortToBeOpened(debugData.deviceIdentifier);
await iOSEmulatorService.postDarwinNotification(attachRequestMessage, debugData.deviceIdentifier);
return result;
}

private async waitForBackendPortToBeOpened(deviceIdentifier: string): Promise<void> {
const portListens = await this.$net.waitForPortToListen({ port: inspectorBackendPort, timeout: 10000, interval: 200 });
if (!portListens) {
const error = <Mobile.IDeviceError>new Error("Unable to connect to application. Ensure application is running on simulator.");
error.deviceIdentifier = deviceIdentifier;
throw error;
}
}

private async deviceDebugBrk(debugData: IDebugData, debugOptions: IDebugOptions): Promise<string> {
await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier });
const projectData = this.$projectDataService.getProjectData(debugData.projectDir);
Expand Down Expand Up @@ -219,7 +182,7 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
// the VSCode Ext starts `tns debug ios --no-client` to start/attach to debug sessions
// check if --no-client is passed - default to opening a tcp socket (versus Chrome DevTools (websocket))
if ((debugOptions.inspector || !debugOptions.client) && this.$hostInfo.isDarwin) {
this._socketProxy = await this.$socketProxyFactory.createTCPSocketProxy(this.getSocketFactory(device));
this._socketProxy = await this.$socketProxyFactory.createTCPSocketProxy(this.getSocketFactory(debugData, device));
await this.openAppInspector(this._socketProxy.address(), debugData, debugOptions);
return null;
} else {
Expand All @@ -228,7 +191,7 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
}

const deviceIdentifier = device ? device.deviceInfo.identifier : debugData.deviceIdentifier;
this._socketProxy = await this.$socketProxyFactory.createWebSocketProxy(this.getSocketFactory(device), deviceIdentifier);
this._socketProxy = await this.$socketProxyFactory.createWebSocketProxy(this.getSocketFactory(debugData, device), deviceIdentifier);
return this.getChromeDebugUrl(debugOptions, this._socketProxy.options.port);
}
}
Expand All @@ -247,9 +210,13 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
}
}

private getSocketFactory(device?: Mobile.IiOSDevice): () => Promise<net.Socket> {
private getSocketFactory(debugData: IDebugData, device?: Mobile.IiOSDevice): () => Promise<net.Socket> {
const factory = async () => {
const socket = device ? await device.connectToPort(inspectorBackendPort) : net.connect(inspectorBackendPort);
const port = await this.$iOSDebuggerPortService.getPort({ deviceId: debugData.deviceIdentifier, appId: debugData.applicationIdentifier });
if (!port) {
this.$errors.fail("NativeScript debugger was not able to get inspector socket port.");
}
const socket = device ? await device.connectToPort(port) : net.connect(port);
this._sockets.push(socket);
return socket;
};
Expand Down
103 changes: 103 additions & 0 deletions lib/services/ios-debugger-port-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { DEBUGGER_PORT_FOUND_EVENT_NAME, STARTING_IOS_APPLICATION_EVENT_NAME } from "../common/constants";
import { cache } from "../common/decorators";
import * as semver from "semver";

export class IOSDebuggerPortService implements IIOSDebuggerPortService {
private mapDebuggerPortData: IDictionary<IIOSDebuggerPortStoredData> = {};
private static DEFAULT_PORT = 18181;
private static MIN_REQUIRED_FRAMEWORK_VERSION = "4.0.0";

constructor(private $iOSLogParserService: IIOSLogParserService,
private $iOSProjectService: IPlatformProjectService,
private $logger: ILogger,
private $projectData: IProjectData) { }

public getPort(data: IIOSDebuggerPortInputData): Promise<number> {
return new Promise((resolve, reject) => {
if (!this.canStartLookingForDebuggerPort()) {
return IOSDebuggerPortService.DEFAULT_PORT;
}

const key = `${data.deviceId}${data.appId}`;
let retryCount: number = 10;

const interval = setInterval(() => {
let port = this.getPortByKey(key);
if (port || retryCount === 0) {
clearInterval(interval);
resolve(port);
} else {
port = this.getPortByKey(key);
retryCount--;
}
}, 500);
});
}

public attachToDebuggerPortFoundEvent(device: Mobile.IDevice): void {
if (!this.canStartLookingForDebuggerPort()) {
return;
}

this.attachToDebuggerPortFoundEventCore();
this.attachToStartingApplicationEvent(device);

this.$iOSLogParserService.startLookingForDebuggerPort(device);
}

private canStartLookingForDebuggerPort(): boolean {
const frameworkVersion = this.$iOSProjectService.getFrameworkVersion(this.$projectData);
return semver.gte(frameworkVersion, IOSDebuggerPortService.MIN_REQUIRED_FRAMEWORK_VERSION);
}

@cache()
private attachToDebuggerPortFoundEventCore(): void {
this.$iOSLogParserService.on(DEBUGGER_PORT_FOUND_EVENT_NAME, (data: IIOSDebuggerPortData) => {
this.$logger.trace(DEBUGGER_PORT_FOUND_EVENT_NAME, data);
this.setData(data, { port: data.port });
this.clearTimeout(data);
});
}

@cache()
private attachToStartingApplicationEvent(device: Mobile.IDevice): void {
device.applicationManager.on(STARTING_IOS_APPLICATION_EVENT_NAME, (data: IIOSDebuggerPortData) => {
this.$logger.trace(STARTING_IOS_APPLICATION_EVENT_NAME, data);
const timer = setTimeout(() => {
this.clearTimeout(data);
if (!this.getPortByKey(`${data.deviceId}${data.appId}`)) {
this.$logger.warn("NativeScript debugger was not able to get inspector socket port.");
}
}, 5000);

this.setData(data, { port: null, timer });
});
}

private getPortByKey(key: string): number {
if (this.mapDebuggerPortData[key]) {
return this.mapDebuggerPortData[key].port;
}

return null;
}

private setData(data: IIOSDebuggerPortData, storedData: IIOSDebuggerPortStoredData): void {
const key = `${data.deviceId}${data.appId}`;

if (!this.mapDebuggerPortData[key]) {
this.mapDebuggerPortData[key] = <any>{};
}

this.mapDebuggerPortData[key].port = storedData.port;
this.mapDebuggerPortData[key].timer = storedData.timer;
}

private clearTimeout(data: IIOSDebuggerPortData): void {
const storedData = this.mapDebuggerPortData[`${data.deviceId}${data.appId}`];
if (storedData && storedData.timer) {
clearTimeout(storedData.timer);
}
}
}
$injector.register("iOSDebuggerPortService", IOSDebuggerPortService);
50 changes: 50 additions & 0 deletions lib/services/ios-log-parser-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { DEBUGGER_PORT_FOUND_EVENT_NAME, DEVICE_LOG_EVENT_NAME } from "../common/constants";
import { cache } from "../common/decorators";
import { EventEmitter } from "events";

export class IOSLogParserService extends EventEmitter implements IIOSLogParserService {
private static MESSAGE_REGEX = /NativeScript debugger has opened inspector socket on port (\d+?) for (.*)[.]/;

constructor(private $deviceLogProvider: Mobile.IDeviceLogProvider,
private $iosDeviceOperations: IIOSDeviceOperations,
private $iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider,
private $logger: ILogger,
private $projectData: IProjectData) {
super();
}

public startLookingForDebuggerPort(device: Mobile.IDevice): void {
this.$deviceLogProvider.setProjectNameForDevice(device.deviceInfo.identifier, this.$projectData.projectName);

this.startLookingForDebuggerPortCore(device);
this.startLogProcess(device);
}

@cache()
private startLookingForDebuggerPortCore(device: Mobile.IDevice): void {
const logProvider = device.isEmulator ? this.$iOSSimulatorLogProvider : this.$iosDeviceOperations;
logProvider.on(DEVICE_LOG_EVENT_NAME, (response: IOSDeviceLib.IDeviceLogData) => this.processDeviceLogResponse(response));
}

private processDeviceLogResponse(response: IOSDeviceLib.IDeviceLogData) {
const matches = IOSLogParserService.MESSAGE_REGEX.exec(response.message);
if (matches) {
const data = {
port: parseInt(matches[1]),
appId: matches[2],
deviceId: response.deviceId
};
this.$logger.trace(`Emitting ${DEBUGGER_PORT_FOUND_EVENT_NAME} event`, data);
this.emit(DEBUGGER_PORT_FOUND_EVENT_NAME, data);
}
}

private startLogProcess(device: Mobile.IDevice): void {
if (device.isEmulator) {
return this.$iOSSimulatorLogProvider.startNewMutedLogProcess(device.deviceInfo.identifier);
}

return this.$iosDeviceOperations.startDeviceLog(device.deviceInfo.identifier);
}
}
$injector.register("iOSLogParserService", IOSLogParserService);
4 changes: 2 additions & 2 deletions lib/services/ios-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
}

public getAppResourcesDestinationDirectoryPath(projectData: IProjectData): string {
const frameworkVersion = this.getFrameworkVersion(this.getPlatformData(projectData).frameworkPackageName, projectData.projectDir);
const frameworkVersion = this.getFrameworkVersion(projectData);

if (semver.lt(frameworkVersion, "1.3.0")) {
return path.join(this.getPlatformData(projectData).projectRoot, projectData.projectName, "Resources", "icons");
Expand Down Expand Up @@ -337,7 +337,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
];

// Starting from tns-ios 1.4 the xcconfig file is referenced in the project template
const frameworkVersion = this.getFrameworkVersion(this.getPlatformData(projectData).frameworkPackageName, projectData.projectDir);
const frameworkVersion = this.getFrameworkVersion(projectData);
if (semver.lt(frameworkVersion, "1.4.0")) {
basicArgs.push("-xcconfig", path.join(projectRoot, projectData.projectName, BUILD_XCCONFIG_FILE_NAME));
}
Expand Down
Loading

0 comments on commit 9e5390d

Please sign in to comment.