Skip to content

Commit 438134b

Browse files
committed
Resurrect iOS device debugging
1 parent f092ac3 commit 438134b

File tree

2 files changed

+112
-51
lines changed

2 files changed

+112
-51
lines changed

lib/services/ios-debug-service.ts

Lines changed: 111 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,39 @@ import ws = require("ws");
55
import stream = require("stream");
66
import path = require("path");
77
import http = require("http");
8+
import Future = require("fibers/future");
89

910
module notification {
11+
function formatNotification(bundleId: string, notification: string) {
12+
return `${bundleId}:NativeScript.Debug.${notification}`;
13+
}
14+
1015
export function waitForDebug(bundleId: string): string {
11-
return bundleId + ":NativeScript.Debug.WaitForDebugger";
16+
return formatNotification(bundleId, "WaitForDebugger");
1217
}
1318

1419
export function attachRequest(bundleId: string): string {
15-
return bundleId + ":NativeScript.Debug.AttachRequest";
20+
return formatNotification(bundleId, "AttachRequest");
1621
}
1722

1823
export function appLaunching(bundleId: string): string {
19-
return bundleId + ":NativeScript.Debug.AppLaunching";
24+
return formatNotification(bundleId, "AppLaunching");
2025
}
2126

2227
export function readyForAttach(bundleId: string): string {
23-
return bundleId + ":NativeScript.Debug.ReadyForAttach";
28+
return formatNotification(bundleId, "ReadyForAttach");
29+
}
30+
31+
export function attachAvailabilityQuery(bundleId: string) {
32+
return formatNotification(bundleId, "AttachAvailabilityQuery");
33+
}
34+
35+
export function alreadyConnected(bundleId: string) {
36+
return formatNotification(bundleId, "AlreadyConnected");
37+
}
38+
39+
export function attachAvailable(bundleId: string) {
40+
return formatNotification(bundleId, "AttachAvailable");
2441
}
2542
}
2643

@@ -111,12 +128,27 @@ class IOSDebugService implements IDebugService {
111128
return (() => {
112129
this.$devicesServices.initialize({ platform: this.platform, deviceId: this.$options.device }).wait();
113130
this.$devicesServices.execute(device => (() => {
114-
this.$platformService.deployOnDevice(this.platform).wait();
115-
116-
var iosDevice = <iOSDevice.IOSDevice>device;
117-
iosDevice.runApplication(this.$projectData.projectId /* , ["--nativescript-debug-brk"] */).wait();
131+
// we intentionally do not wait on this here, because if we did, we'd miss the AppLaunching notification
132+
let deploy = this.$platformService.deployOnDevice(this.platform);
133+
134+
let iosDevice = <iOSDevice.IOSDevice>device;
135+
let projectId = this.$projectData.projectId;
136+
let npc = new iOSProxyServices.NotificationProxyClient(iosDevice, this.$injector);
137+
138+
try {
139+
awaitNotification(npc, notification.appLaunching(projectId), 60000).wait();
140+
process.nextTick(() => {
141+
npc.postNotificationAndAttachForData(notification.waitForDebug(projectId));
142+
npc.postNotificationAndAttachForData(notification.attachRequest(projectId));
143+
});
144+
awaitNotification(npc, notification.readyForAttach(projectId), 5000).wait();
145+
} catch(e) {
146+
this.$errors.failWithoutHelp("Timeout waiting for NativeScript debugger.");
147+
}
148+
118149
createWebSocketProxy(this.$logger, (callback) => connectEventually(() => iosDevice.connectToPort(InspectorBackendPort), callback));
119150
this.executeOpenDebuggerClient().wait();
151+
deploy.wait();
120152
}).future<void>()()).wait();
121153
}).future<void>()();
122154
}
@@ -125,9 +157,38 @@ class IOSDebugService implements IDebugService {
125157
return (() => {
126158
this.$devicesServices.initialize({ platform: this.platform, deviceId: this.$options.device }).wait();
127159
this.$devicesServices.execute(device => (() => {
128-
var iosDevice = <iOSDevice.IOSDevice>device;
129-
createWebSocketProxy(this.$logger, (callback) => connectEventually(() => iosDevice.connectToPort(InspectorBackendPort), callback));
130-
this.executeOpenDebuggerClient().wait();
160+
let iosDevice = <iOSDevice.IOSDevice>device;
161+
let projectId = this.$projectData.projectId;
162+
let npc = new iOSProxyServices.NotificationProxyClient(iosDevice, this.$injector);
163+
164+
let [alreadyConnected, readyForAttach, attachAvailable] = [
165+
notification.alreadyConnected(projectId),
166+
notification.readyForAttach(projectId),
167+
notification.attachAvailable(projectId)
168+
].map((notification) => awaitNotification(npc, notification, 2000));
169+
170+
npc.postNotificationAndAttachForData(notification.attachAvailabilityQuery(projectId));
171+
172+
let receivedNotification: IFuture<string>;
173+
try {
174+
receivedNotification = whenAny(alreadyConnected, readyForAttach, attachAvailable).wait();
175+
} catch (e) {
176+
this.$errors.failWithoutHelp(`The application ${projectId} does not appear to be running on ${device.getDisplayName()} or is not built with debugging enabled.`);
177+
}
178+
179+
switch (receivedNotification) {
180+
case alreadyConnected:
181+
this.$errors.failWithoutHelp("A debugger is already connected.");
182+
case attachAvailable:
183+
process.nextTick(() => npc.postNotificationAndAttachForData(notification.attachRequest(projectId)));
184+
try { awaitNotification(npc, notification.readyForAttach(projectId), 2000).wait(); }
185+
catch (e) {
186+
this.$errors.failWithoutHelp(`The application ${projectId} timed out when performing the NativeScript debugger handshake.`);
187+
}
188+
case readyForAttach:
189+
createWebSocketProxy(this.$logger, (callback) => connectEventually(() => iosDevice.connectToPort(InspectorBackendPort), callback));
190+
this.executeOpenDebuggerClient().wait();
191+
}
131192
}).future<void>()()).wait();
132193
}).future<void>()();
133194
}
@@ -183,18 +244,16 @@ function createWebSocketProxy($logger: ILogger, socketFactory: (handler: (socket
183244
var server = ws.createServer(<any>{
184245
port: localPort,
185246
verifyClient: (info: any, callback: any) => {
247+
$logger.info("Frontend client connected.");
186248
socketFactory((socket) => {
249+
$logger.info("Backend socket created.");
187250
info.req["__deviceSocket"] = socket;
188251
callback(true);
189252
});
190253
}
191254
});
192255
server.on("connection", (webSocket) => {
193-
$logger.info("Frontend client connected.");
194-
195256
var deviceSocket: net.Socket = (<any>webSocket.upgradeReq)["__deviceSocket"];
196-
197-
$logger.info("Backend socket created.");
198257
var packets = new PacketStream();
199258
deviceSocket.pipe(packets);
200259

@@ -225,48 +284,50 @@ function createWebSocketProxy($logger: ILogger, socketFactory: (handler: (socket
225284
return server;
226285
}
227286

228-
class IOSDeviceDebugging {
229-
private $notificationProxyClient: iOSProxyServices.NotificationProxyClient;
230-
231-
constructor(private bundleId: string,
232-
private $iOSDevice: iOSDevice.IOSDevice,
233-
private $logger: ILogger,
234-
private $injector: IInjector) {
235-
236-
this.$notificationProxyClient = this.$injector.resolve(iOSProxyServices.NotificationProxyClient, { device: this.$iOSDevice })
237-
}
238-
239-
public debugApplicationOnStart() {
240-
var appLaunchMessage = notification.appLaunching(this.bundleId);
241-
this.$notificationProxyClient.addObserver(appLaunchMessage, () => {
242-
this.$logger.info("Got AppLaunching");
243-
this.proxyDebuggingTraffic();
244-
var waitForDebuggerMessage = notification.waitForDebug(this.bundleId);
245-
this.$notificationProxyClient.postNotificationAndAttachForData(waitForDebuggerMessage);
246-
});
287+
function awaitNotification(npc: iOSProxyServices.NotificationProxyClient, notification: string, timeout: number): IFuture<string> {
288+
let future = new Future<string>();
289+
290+
let timeoutObject = setTimeout(() => {
291+
detachObserver();
292+
future.throw(new Error("Timeout receiving notification."));
293+
}, timeout);
294+
295+
function notificationObserver(notification: string) {
296+
clearTimeout(timeoutObject);
297+
detachObserver();
298+
future.return(notification);
247299
}
248-
249-
public debugRunningApplication() {
250-
this.proxyDebuggingTraffic();
251-
var attachRequestMessage = notification.attachRequest(this.bundleId);
252-
this.$notificationProxyClient.postNotificationAndAttachForData(attachRequestMessage);
300+
301+
function detachObserver() {
302+
process.nextTick(() => npc.removeObserver(notification, notificationObserver));
253303
}
304+
305+
npc.addObserver(notification, notificationObserver);
306+
307+
return future;
308+
}
254309

255-
private proxyDebuggingTraffic(): void {
256-
var identifier = this.$iOSDevice.getIdentifier();
257-
this.$logger.info("Device Identifier: " + identifier);
310+
function whenAny<T>(...futures: IFuture<T>[]): IFuture<IFuture<T>> {
311+
let resultFuture = new Future<IFuture<T>>();
312+
let futuresLeft = futures.length;
258313

259-
var readyForAttachMessage = notification.readyForAttach(this.bundleId);
260-
this.$notificationProxyClient.addObserver(readyForAttachMessage, () => {
261-
createWebSocketProxy(this.$logger, (callback) => callback(this.$iOSDevice.connectToPort(InspectorBackendPort)));
314+
for (let future of futures) {
315+
var futureLocal = future;
316+
future.resolve((error, result?) => {
317+
futuresLeft--;
318+
319+
if (!resultFuture.isResolved()) {
320+
if (typeof error === "undefined") {
321+
resultFuture.return(futureLocal);
322+
} else if (futuresLeft == 0) {
323+
resultFuture.throw(new Error("None of the futures succeeded."));
324+
}
325+
}
262326
});
263327
}
264-
265-
private printHowToTerminate() {
266-
this.$logger.info("\nSetting up debugger proxy...\n\nPress Ctrl + C to terminate, or disconnect.\n");
267-
}
328+
329+
return resultFuture;
268330
}
269-
$injector.register("iosDeviceDebugging", IOSDeviceDebugging);
270331

271332
class PacketStream extends stream.Transform {
272333
private buffer: Buffer;

0 commit comments

Comments
 (0)