Skip to content

Commit c5852d0

Browse files
committed
Get debugger port from device logs
1 parent 6d88123 commit c5852d0

16 files changed

+550
-69
lines changed

lib/bootstrap.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,6 @@ $injector.require("nativeScriptCloudExtensionService", "./services/nativescript-
165165
$injector.requireCommand("resources|generate|icons", "./commands/generate-assets");
166166
$injector.requireCommand("resources|generate|splashes", "./commands/generate-assets");
167167
$injector.requirePublic("assetsGenerationService", "./services/assets-generation/assets-generation-service");
168+
169+
$injector.require("iOSLogParserService", "./services/ios-log-parser-service");
170+
$injector.require("iOSDebuggerPortService", "./services/ios-debugger-port-service");

lib/definitions/debug.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ interface IDebugOptions {
8787
* Defines if the iOS App Inspector should be used instead of providing URL to debug the application with Chrome DevTools
8888
*/
8989
inspector?: boolean;
90+
/**
91+
* Defines if should print all availableDevices
92+
*/
93+
availableDevices?: boolean;
9094
}
9195

9296
/**
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
interface IIOSDebuggerPortInputData {
2+
deviceId: string;
3+
appId: string;
4+
}
5+
6+
interface IIOSDebuggerPortData extends IIOSDebuggerPortInputData {
7+
port: number;
8+
}
9+
10+
interface IIOSDebuggerPortStoredData {
11+
port: number;
12+
timer?: NodeJS.Timer;
13+
}
14+
15+
interface IIOSDebuggerPortService {
16+
/**
17+
* Gets iOS debugger port for specified deviceId and appId
18+
* @param {IIOSDebuggerPortInputData} data - Describes deviceId and appId
19+
*/
20+
getPort(data: IIOSDebuggerPortInputData): Promise<number>;
21+
/**
22+
* Attaches on DEBUGGER_PORT_FOUND event and STARTING_IOS_APPLICATION events
23+
* In case when DEBUGGER_PORT_FOUND event is emitted, stores the port and clears the timeout if such.
24+
* 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.
25+
* @param {Mobile.IDevice} device - Describes the device which logs should be parsed.
26+
*/
27+
attachToDebuggerPortFoundEvent(device: Mobile.IDevice): void;
28+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
interface IIOSLogParserService extends NodeJS.EventEmitter {
2+
/**
3+
* Starts looking for debugger port. Attaches on device logs and processes them. In case when port is found, DEBUGGER_PORT_FOUND event is emitted.
4+
* @param {Mobile.IDevice} device - Describes the device which logs will be processed.
5+
*/
6+
startLookingForDebuggerPort(device: Mobile.IDevice): void;
7+
}

lib/definitions/project.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ interface IProjectTemplatesService {
218218

219219
interface IPlatformProjectServiceBase {
220220
getPluginPlatformsFolderPath(pluginData: IPluginData, platform: string): string;
221+
getFrameworkVersion(projectData: IProjectData): string;
221222
}
222223

223224
interface IBuildForDevice {
@@ -270,7 +271,7 @@ interface ILocalBuildService {
270271

271272
interface ICleanNativeAppData extends IProjectDir, IPlatform { }
272273

273-
interface IPlatformProjectService extends NodeJS.EventEmitter {
274+
interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectServiceBase {
274275
getPlatformData(projectData: IProjectData): IPlatformData;
275276
validate(projectData: IProjectData): Promise<void>;
276277
createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, config: ICreateProjectOptions): Promise<void>;

lib/services/ios-debug-service.ts

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,13 @@ import { ChildProcess } from "child_process";
66
import { DebugServiceBase } from "./debug-service-base";
77
import { CONNECTION_ERROR_EVENT_NAME, AWAIT_NOTIFICATION_TIMEOUT_SECONDS } from "../constants";
88
import { getPidFromiOSSimulatorLogs } from "../common/helpers";
9-
10-
import byline = require("byline");
11-
12-
const inspectorBackendPort = 18181;
139
const inspectorAppName = "NativeScript Inspector.app";
1410
const inspectorNpmPackageName = "tns-ios-inspector";
1511
const inspectorUiDir = "WebInspectorUI/";
1612

1713
export class IOSDebugService extends DebugServiceBase implements IPlatformDebugService {
1814
private _lldbProcess: ChildProcess;
1915
private _sockets: net.Socket[] = [];
20-
private _childProcess: ChildProcess;
2116
private _socketProxy: any;
2217

2318
constructor(protected device: Mobile.IDevice,
@@ -29,11 +24,11 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
2924
private $logger: ILogger,
3025
private $errors: IErrors,
3126
private $npmInstallationManager: INpmInstallationManager,
27+
private $iOSDebuggerPortService: IIOSDebuggerPortService,
3228
private $iOSNotification: IiOSNotification,
3329
private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor,
3430
private $processService: IProcessService,
3531
private $socketProxyFactory: ISocketProxyFactory,
36-
private $net: INet,
3732
private $projectDataService: IProjectDataService) {
3833
super(device, $devicesService);
3934
this.$processService.attachToProcessExitSignals(this, this.debugStop);
@@ -90,11 +85,6 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
9085
await this.killProcess(this._lldbProcess);
9186
this._lldbProcess = undefined;
9287
}
93-
94-
if (this._childProcess) {
95-
await this.killProcess(this._childProcess);
96-
this._childProcess = undefined;
97-
}
9888
}
9989

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

117107
private async emulatorDebugBrk(debugData: IDebugData, debugOptions: IDebugOptions): Promise<string> {
118108
const args = debugOptions.debugBrk ? "--nativescript-debug-brk" : "--nativescript-debug-start";
119-
const child_process = await this.$iOSEmulatorServices.runApplicationOnEmulator(debugData.pathToAppPackage, {
109+
const launchResult = await this.$iOSEmulatorServices.runApplicationOnEmulator(debugData.pathToAppPackage, {
120110
waitForDebugger: true,
121111
captureStdin: true,
122112
args: args,
123113
appId: debugData.applicationIdentifier,
124114
skipInstall: true
125115
});
126116

127-
const lineStream = byline(child_process.stdout);
128-
this._childProcess = child_process;
129-
130-
lineStream.on('data', (line: NodeBuffer) => {
131-
const lineText = line.toString();
132-
if (lineText && _.startsWith(lineText, debugData.applicationIdentifier)) {
133-
const pid = getPidFromiOSSimulatorLogs(debugData.applicationIdentifier, lineText);
134-
if (!pid) {
135-
this.$logger.trace(`Line ${lineText} does not contain PID of the application ${debugData.applicationIdentifier}.`);
136-
return;
137-
}
138-
139-
this._lldbProcess = this.$childProcess.spawn("lldb", ["-p", pid]);
140-
if (log4js.levels.TRACE.isGreaterThanOrEqualTo(this.$logger.getLevel())) {
141-
this._lldbProcess.stdout.pipe(process.stdout);
142-
}
143-
this._lldbProcess.stderr.pipe(process.stderr);
144-
this._lldbProcess.stdin.write("process continue\n");
145-
} else {
146-
process.stdout.write(line + "\n");
147-
}
148-
});
149-
150-
await this.waitForBackendPortToBeOpened(debugData.deviceIdentifier);
117+
const pid = getPidFromiOSSimulatorLogs(debugData.applicationIdentifier, launchResult);
118+
this._lldbProcess = this.$childProcess.spawn("lldb", ["-p", pid]);
119+
if (log4js.levels.TRACE.isGreaterThanOrEqualTo(this.$logger.getLevel())) {
120+
this._lldbProcess.stdout.pipe(process.stdout);
121+
}
122+
this._lldbProcess.stderr.pipe(process.stderr);
123+
this._lldbProcess.stdin.write("process continue\n");
151124

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

160133
const iOSEmulatorService = <Mobile.IiOSSimulatorService>this.$iOSEmulatorServices;
161-
await iOSEmulatorService.postDarwinNotification(attachRequestMessage);
162-
await this.waitForBackendPortToBeOpened(debugData.deviceIdentifier);
134+
await iOSEmulatorService.postDarwinNotification(attachRequestMessage, debugData.deviceIdentifier);
163135
return result;
164136
}
165137

166-
private async waitForBackendPortToBeOpened(deviceIdentifier: string): Promise<void> {
167-
const portListens = await this.$net.waitForPortToListen({ port: inspectorBackendPort, timeout: 10000, interval: 200 });
168-
if (!portListens) {
169-
const error = <Mobile.IDeviceError>new Error("Unable to connect to application. Ensure application is running on simulator.");
170-
error.deviceIdentifier = deviceIdentifier;
171-
throw error;
172-
}
173-
}
174-
175138
private async deviceDebugBrk(debugData: IDebugData, debugOptions: IDebugOptions): Promise<string> {
176139
await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier });
177140
const projectData = this.$projectDataService.getProjectData(debugData.projectDir);
@@ -219,7 +182,7 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
219182
// the VSCode Ext starts `tns debug ios --no-client` to start/attach to debug sessions
220183
// check if --no-client is passed - default to opening a tcp socket (versus Chrome DevTools (websocket))
221184
if ((debugOptions.inspector || !debugOptions.client) && this.$hostInfo.isDarwin) {
222-
this._socketProxy = await this.$socketProxyFactory.createTCPSocketProxy(this.getSocketFactory(device));
185+
this._socketProxy = await this.$socketProxyFactory.createTCPSocketProxy(this.getSocketFactory(debugData, device));
223186
await this.openAppInspector(this._socketProxy.address(), debugData, debugOptions);
224187
return null;
225188
} else {
@@ -228,7 +191,7 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
228191
}
229192

230193
const deviceIdentifier = device ? device.deviceInfo.identifier : debugData.deviceIdentifier;
231-
this._socketProxy = await this.$socketProxyFactory.createWebSocketProxy(this.getSocketFactory(device), deviceIdentifier);
194+
this._socketProxy = await this.$socketProxyFactory.createWebSocketProxy(this.getSocketFactory(debugData, device), deviceIdentifier);
232195
return this.getChromeDebugUrl(debugOptions, this._socketProxy.options.port);
233196
}
234197
}
@@ -247,9 +210,10 @@ export class IOSDebugService extends DebugServiceBase implements IPlatformDebugS
247210
}
248211
}
249212

250-
private getSocketFactory(device?: Mobile.IiOSDevice): () => Promise<net.Socket> {
213+
private getSocketFactory(debugData: IDebugData, device?: Mobile.IiOSDevice): () => Promise<net.Socket> {
251214
const factory = async () => {
252-
const socket = device ? await device.connectToPort(inspectorBackendPort) : net.connect(inspectorBackendPort);
215+
const port = await this.$iOSDebuggerPortService.getPort({ deviceId: debugData.deviceIdentifier, appId: debugData.applicationIdentifier });
216+
const socket = device ? await device.connectToPort(port) : net.connect(port);
253217
this._sockets.push(socket);
254218
return socket;
255219
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { DEBUGGER_PORT_FOUND_EVENT_NAME, STARTING_IOS_APPLICATION_EVENT_NAME } from "../common/constants";
2+
import { cache } from "../common/decorators";
3+
import * as semver from "semver";
4+
5+
export class IOSDebuggerPortService implements IIOSDebuggerPortService {
6+
private mapDebuggerPortData: IDictionary<IIOSDebuggerPortStoredData> = {};
7+
private static DEFAULT_PORT = 18181;
8+
private static MIN_REQUIRED_FRAMEWORK_VERSION = "4.0.0";
9+
10+
constructor(private $iOSLogParserService: IIOSLogParserService,
11+
private $iOSProjectService: IPlatformProjectService,
12+
private $logger: ILogger,
13+
private $projectData: IProjectData) { }
14+
15+
public getPort(data: IIOSDebuggerPortInputData): Promise<number> {
16+
return new Promise((resolve, reject) => {
17+
if (!this.canStartLookingForDebuggerPort()) {
18+
return IOSDebuggerPortService.DEFAULT_PORT;
19+
}
20+
21+
const key = `${data.deviceId}${data.appId}`;
22+
let retryCount: number = 10;
23+
24+
const interval = setInterval(() => {
25+
let port = this.getPortByKey(key);
26+
if (port || retryCount === 0) {
27+
clearInterval(interval);
28+
resolve(port);
29+
} else {
30+
port = this.getPortByKey(key);
31+
retryCount--;
32+
}
33+
}, 500);
34+
});
35+
}
36+
37+
public attachToDebuggerPortFoundEvent(device: Mobile.IDevice): void {
38+
if (!this.canStartLookingForDebuggerPort()) {
39+
return;
40+
}
41+
42+
this.attachToDebuggerPortFoundEventCore();
43+
this.attachToStartingApplicationEvent(device);
44+
45+
this.$iOSLogParserService.startLookingForDebuggerPort(device);
46+
}
47+
48+
private canStartLookingForDebuggerPort(): boolean {
49+
const frameworkVersion = this.$iOSProjectService.getFrameworkVersion(this.$projectData);
50+
return semver.gte(frameworkVersion, IOSDebuggerPortService.MIN_REQUIRED_FRAMEWORK_VERSION);
51+
}
52+
53+
@cache()
54+
private attachToDebuggerPortFoundEventCore(): void {
55+
this.$iOSLogParserService.on(DEBUGGER_PORT_FOUND_EVENT_NAME, (data: IIOSDebuggerPortData) => {
56+
this.$logger.trace(DEBUGGER_PORT_FOUND_EVENT_NAME, data);
57+
this.setData(data, { port: data.port });
58+
this.clearTimeout(data);
59+
});
60+
}
61+
62+
@cache()
63+
private attachToStartingApplicationEvent(device: Mobile.IDevice): void {
64+
device.applicationManager.on(STARTING_IOS_APPLICATION_EVENT_NAME, (data: IIOSDebuggerPortData) => {
65+
this.$logger.trace(STARTING_IOS_APPLICATION_EVENT_NAME, data);
66+
const timer = setTimeout(() => {
67+
this.clearTimeout(data);
68+
if (!this.getPortByKey(`${data.deviceId}${data.appId}`)) {
69+
this.$logger.warn("NativeScript debugger was not able to get inspector socket port.");
70+
}
71+
}, 5000);
72+
73+
this.setData(data, { port: null, timer });
74+
});
75+
}
76+
77+
private getPortByKey(key: string): number {
78+
if (this.mapDebuggerPortData[key]) {
79+
return this.mapDebuggerPortData[key].port;
80+
}
81+
82+
return null;
83+
}
84+
85+
private setData(data: IIOSDebuggerPortData, storedData: IIOSDebuggerPortStoredData): void {
86+
const key = `${data.deviceId}${data.appId}`;
87+
88+
if (!this.mapDebuggerPortData[key]) {
89+
this.mapDebuggerPortData[key] = <any>{};
90+
}
91+
92+
this.mapDebuggerPortData[key].port = storedData.port;
93+
this.mapDebuggerPortData[key].timer = storedData.timer;
94+
}
95+
96+
private clearTimeout(data: IIOSDebuggerPortData): void {
97+
const storedData = this.mapDebuggerPortData[`${data.deviceId}${data.appId}`];
98+
if (storedData && storedData.timer) {
99+
clearTimeout(storedData.timer);
100+
}
101+
}
102+
}
103+
$injector.register("iOSDebuggerPortService", IOSDebuggerPortService);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { DEBUGGER_PORT_FOUND_EVENT_NAME, DEVICE_LOG_EVENT_NAME } from "../common/constants";
2+
import { cache } from "../common/decorators";
3+
import { EventEmitter } from "events";
4+
5+
export class IOSLogParserService extends EventEmitter implements IIOSLogParserService {
6+
private static MESSAGE_REGEX = /NativeScript debugger has opened inspector socket on port (\d+?) for (.*)[.]/;
7+
8+
constructor(private $deviceLogProvider: Mobile.IDeviceLogProvider,
9+
private $iosDeviceOperations: IIOSDeviceOperations,
10+
private $iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider,
11+
private $logger: ILogger,
12+
private $projectData: IProjectData) {
13+
super();
14+
}
15+
16+
public startLookingForDebuggerPort(device: Mobile.IDevice): void {
17+
this.$deviceLogProvider.setProjectNameForDevice(device.deviceInfo.identifier, this.$projectData.projectName);
18+
19+
this.startLookingForDebuggerPortCore(device);
20+
this.startLogProcess(device);
21+
}
22+
23+
@cache()
24+
private startLookingForDebuggerPortCore(device: Mobile.IDevice): void {
25+
const logProvider = device.isEmulator ? this.$iOSSimulatorLogProvider : this.$iosDeviceOperations;
26+
logProvider.on(DEVICE_LOG_EVENT_NAME, (response: IOSDeviceLib.IDeviceLogData) => this.processDeviceLogResponse(response));
27+
}
28+
29+
private processDeviceLogResponse(response: IOSDeviceLib.IDeviceLogData) {
30+
const matches = IOSLogParserService.MESSAGE_REGEX.exec(response.message);
31+
if (matches) {
32+
const data = {
33+
port: parseInt(matches[1]),
34+
appId: matches[2],
35+
deviceId: response.deviceId
36+
};
37+
this.$logger.trace(`Emitting ${DEBUGGER_PORT_FOUND_EVENT_NAME} event`, data);
38+
this.emit(DEBUGGER_PORT_FOUND_EVENT_NAME, data);
39+
}
40+
}
41+
42+
private startLogProcess(device: Mobile.IDevice): void {
43+
if (device.isEmulator) {
44+
return this.$iOSSimulatorLogProvider.startNewMutedLogProcess(device.deviceInfo.identifier);
45+
}
46+
47+
return this.$iosDeviceOperations.startDeviceLog(device.deviceInfo.identifier);
48+
}
49+
}
50+
$injector.register("iOSLogParserService", IOSLogParserService);

lib/services/ios-project-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
125125
}
126126

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

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

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

0 commit comments

Comments
 (0)