Skip to content

Get debugger port from device logs #3540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 23, 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
2 changes: 2 additions & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,5 @@ $injector.requireCommand("resources|generate|splashes", "./commands/generate-ass
$injector.requirePublic("assetsGenerationService", "./services/assets-generation/assets-generation-service");

$injector.require("filesHashService", "./services/files-hash-service");
$injector.require("iOSLogParserService", "./services/ios-log-parser-service");
$injector.require("iOSDebuggerPortService", "./services/ios-debugger-port-service");
4 changes: 3 additions & 1 deletion lib/commands/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,15 @@ export class DebugIOSCommand implements ICommand {
private $injector: IInjector,
private $projectData: IProjectData,
private $platformsData: IPlatformsData,
$iosDeviceOperations: IIOSDeviceOperations) {
$iosDeviceOperations: IIOSDeviceOperations,
$iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider) {
this.$projectData.initializeProjectData();
// Do not dispose ios-device-lib, so the process will remain alive and the debug application (NativeScript Inspector or Chrome DevTools) will be able to connect to the socket.
// In case we dispose ios-device-lib, the socket will be closed and the code will fail when the debug application tries to read/send data to device socket.
// That's why the `$ tns debug ios --justlaunch` command will not release the terminal.
// In case we do not set it to false, the dispose will be called once the command finishes its execution, which will prevent the debugging.
$iosDeviceOperations.setShouldDispose(false);
$iOSSimulatorLogProvider.setShouldDispose(false);
}

public execute(args: string[]): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ interface ISocketProxyFactory extends NodeJS.EventEmitter {

interface IiOSNotification {
getWaitForDebug(projectId: string): string;
getAttachRequest(projectId: string): string;
getAttachRequest(projectId: string, deviceId: string): string;
getAppLaunching(projectId: string): string;
getReadyForAttach(projectId: string): string;
getAttachAvailabilityQuery(projectId: string): string;
Expand Down
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.
*/
startParsingLog(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
10 changes: 7 additions & 3 deletions lib/device-sockets/ios/notification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export class IOSNotification implements IiOSNotification {
import { EventEmitter } from "events";
import { ATTACH_REQUEST_EVENT_NAME } from "../../common/constants";

export class IOSNotification extends EventEmitter implements IiOSNotification {
private static WAIT_FOR_DEBUG_NOTIFICATION_NAME = "WaitForDebugger";
private static ATTACH_REQUEST_NOTIFICATION_NAME = "AttachRequest";
private static APP_LAUNCHING_NOTIFICATION_NAME = "AppLaunching";
Expand All @@ -11,8 +14,9 @@ export class IOSNotification implements IiOSNotification {
return this.formatNotification(IOSNotification.WAIT_FOR_DEBUG_NOTIFICATION_NAME, projectId);
}

public getAttachRequest(projectId: string): string {
return this.formatNotification(IOSNotification.ATTACH_REQUEST_NOTIFICATION_NAME, projectId);
public getAttachRequest(appId: string, deviceId: string): string {
this.emit(ATTACH_REQUEST_EVENT_NAME, { deviceId, appId });
return this.formatNotification(IOSNotification.ATTACH_REQUEST_NOTIFICATION_NAME, appId);
}

public getAppLaunching(projectId: string): string {
Expand Down
4 changes: 2 additions & 2 deletions lib/device-sockets/ios/socket-request-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class IOSSocketRequestExecutor implements IiOSSocketRequestExecutor {
const readyForAttachSocket = await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getReadyForAttach(projectId), constants.IOS_OBSERVE_NOTIFICATION_COMMAND_TYPE);
const readyForAttachPromise = this.$iOSNotificationService.awaitNotification(deviceIdentifier, +readyForAttachSocket, readyForAttachTimeout);

await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getAttachRequest(projectId));
await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getAttachRequest(projectId, deviceIdentifier));
await readyForAttachPromise;
} catch (e) {
this.$logger.trace("Launch request error:");
Expand All @@ -76,7 +76,7 @@ export class IOSSocketRequestExecutor implements IiOSSocketRequestExecutor {
// before we send the PostNotification.
const readyForAttachSocket = await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getReadyForAttach(projectId), constants.IOS_OBSERVE_NOTIFICATION_COMMAND_TYPE);
const readyForAttachPromise = this.$iOSNotificationService.awaitNotification(deviceIdentifier, +readyForAttachSocket, timeout);
await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getAttachRequest(projectId));
await this.$iOSNotificationService.postNotification(deviceIdentifier, this.$iOSNotification.getAttachRequest(projectId, deviceIdentifier));
await readyForAttachPromise;
} catch (e) {
this.$errors.failWithoutHelp(`The application ${projectId} timed out when performing the socket handshake.`);
Expand Down
4 changes: 3 additions & 1 deletion lib/helpers/livesync-command-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper {
private $platformsData: IPlatformsData,
private $analyticsService: IAnalyticsService,
private $bundleValidatorHelper: IBundleValidatorHelper,
private $errors: IErrors) {
private $errors: IErrors,
private $iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider) {
this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch);
}

Expand Down Expand Up @@ -53,6 +54,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper {
const shouldKeepProcessAlive = this.$options.watch || !this.$options.justlaunch;
if (workingWithiOSDevices && shouldKeepProcessAlive) {
this.$iosDeviceOperations.setShouldDispose(false);
this.$iOSSimulatorLogProvider.setShouldDispose(false);
}

if (this.$options.release) {
Expand Down
71 changes: 19 additions & 52 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,62 +106,35 @@ 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);
}

private async emulatorStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise<string> {
const result = await this.wireDebuggerClient(debugData, debugOptions);

const attachRequestMessage = this.$iOSNotification.getAttachRequest(debugData.applicationIdentifier);
const attachRequestMessage = this.$iOSNotification.getAttachRequest(debugData.applicationIdentifier, debugData.deviceIdentifier);

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 });
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thrown an error if port is null

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
Loading