Skip to content

Commit 52f987f

Browse files
Fatme HavaluovaFatme Havaluova
Fatme Havaluova
authored and
Fatme Havaluova
committed
Refactor ios-debug-service and reuse the socket handshake and communication to iOS runtime from usb-livesync-service
1 parent af18e9b commit 52f987f

10 files changed

+459
-330
lines changed

lib/bootstrap.ts

+5
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,8 @@ $injector.require("androidToolsInfo", "./android-tools-info");
8484

8585
$injector.require("iosUsbLiveSyncServiceLocator", "./services/usb-livesync-service");
8686
$injector.require("androidUsbLiveSyncServiceLocator", "./services/usb-livesync-service");
87+
88+
$injector.require("iOSNotificationService", "./services/ios-notification-service");
89+
$injector.require("socketProxyFactory", "./device-sockets/ios/socket-proxy-factory");
90+
$injector.require("iOSNotification", "./device-sockets/ios/notification");
91+
$injector.require("iOSSocketRequestExecutor", "./device-sockets/ios/socket-request-executor");

lib/declarations.ts

+29
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,32 @@ interface IAndroidToolsInfoData {
147147
*/
148148
targetSdkVersion: number;
149149
}
150+
151+
interface ISocketProxyFactory {
152+
createSocketProxy(factory: () => any, socketFactoryAction: (backendSocket: any, frontendSocket: any) => void): IFuture<any>;
153+
}
154+
155+
interface IiOSNotification {
156+
waitForDebug: string;
157+
attachRequest: string;
158+
appLaunching: string;
159+
readyForAttach: string;
160+
attachAvailabilityQuery: string;
161+
alreadyConnected: string;
162+
attachAvailable: string;
163+
}
164+
165+
interface IiOSNotificationService {
166+
awaitNotification(npc: Mobile.INotificationProxyClient, notification: string, timeout: number): IFuture<string>;
167+
}
168+
169+
interface ISocketActions {
170+
socketFactoryAction: (backendSocket: any, frontendSocket: any) => void;
171+
afterSocketHandshakeAction: (socketProxy: any) => IFuture<void>;
172+
}
173+
174+
interface IiOSSocketRequestExecutor {
175+
executeLaunchRequest(socketActions: ISocketActions, timeout: number, readyForAttachTimeout: number, device: Mobile.IiOSDevice): IFuture<void>;
176+
executeAttachRequest(socketActions: ISocketActions, timeout: number, device: Mobile.IiOSDevice): IFuture<void>;
177+
executeRequest(factory: () => any, socketActions: ISocketActions): IFuture<void>;
178+
}
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
///<reference path="../../.d.ts"/>
2+
"use strict";
3+
4+
export class IOSNotification implements IiOSNotification {
5+
private static WAIT_FOR_DEBUG_NOTIFICATION_NAME = "WaitForDebugger";
6+
private static ATTACH_REQUEST_NOTIFICATION_NAME = "AttachRequest";
7+
private static APP_LAUNCHING_NOTIFICATION_NAME = "AppLaunching";
8+
private static READY_FOR_ATTACH_NOTIFICATION_NAME = "ReadyForAttach";
9+
private static ATTACH_AVAILABILITY_QUERY_NOTIFICATION_NAME = "AttachAvailabilityQuery";
10+
private static ALREADY_CONNECTED_NOTIFICATION_NAME = "AlreadyConnected";
11+
private static ATTACH_AVAILABLE_NOTIFICATION_NAME = "AttachAvailable";
12+
13+
constructor(private $projectData: IProjectData) { }
14+
15+
public get waitForDebug() {
16+
return this.formatNotification(IOSNotification.WAIT_FOR_DEBUG_NOTIFICATION_NAME);
17+
}
18+
19+
public get attachRequest(): string {
20+
return this.formatNotification(IOSNotification.ATTACH_REQUEST_NOTIFICATION_NAME);
21+
}
22+
23+
public get appLaunching(): string {
24+
return this.formatNotification(IOSNotification.APP_LAUNCHING_NOTIFICATION_NAME);
25+
}
26+
27+
public get readyForAttach(): string {
28+
return this.formatNotification(IOSNotification.READY_FOR_ATTACH_NOTIFICATION_NAME);
29+
}
30+
31+
public get attachAvailabilityQuery() {
32+
return this.formatNotification(IOSNotification.ATTACH_AVAILABILITY_QUERY_NOTIFICATION_NAME);
33+
}
34+
35+
public get alreadyConnected() {
36+
return this.formatNotification(IOSNotification.ALREADY_CONNECTED_NOTIFICATION_NAME);
37+
}
38+
39+
public get attachAvailable() {
40+
return this.formatNotification(IOSNotification.ATTACH_AVAILABLE_NOTIFICATION_NAME);
41+
}
42+
43+
private formatNotification(notification: string) {
44+
return `${this.$projectData.projectId}:NativeScript.Debug.${notification}`;
45+
}
46+
}
47+
$injector.register("iOSNotification", IOSNotification);
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
///<reference path="../../.d.ts"/>
2+
"use strict";
3+
4+
import * as stream from "stream";
5+
6+
export class PacketStream extends stream.Transform {
7+
private buffer: Buffer;
8+
private offset: number;
9+
10+
constructor(opts?: stream.TransformOptions) {
11+
super(opts);
12+
}
13+
14+
public _transform(packet: any, encoding: string, done: Function): void {
15+
while (packet.length > 0) {
16+
if (!this.buffer) {
17+
// read length
18+
let length = packet.readInt32BE(0);
19+
this.buffer = new Buffer(length);
20+
this.offset = 0;
21+
packet = packet.slice(4);
22+
}
23+
24+
packet.copy(this.buffer, this.offset);
25+
let copied = Math.min(this.buffer.length - this.offset, packet.length);
26+
this.offset += copied;
27+
packet = packet.slice(copied);
28+
29+
if (this.offset === this.buffer.length) {
30+
this.push(this.buffer);
31+
this.buffer = undefined;
32+
}
33+
}
34+
done();
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
///<reference path="../../.d.ts"/>
2+
"use strict";
3+
4+
import { PacketStream } from "./packet-stream";
5+
import * as net from "net";
6+
import semver = require("semver");
7+
import temp = require("temp");
8+
import ws = require("ws");
9+
10+
export class SocketProxyFactory implements ISocketProxyFactory {
11+
constructor(private $logger: ILogger,
12+
private $projectData: IProjectData,
13+
private $projectDataService: IProjectDataService) { }
14+
15+
public createSocketProxy(factory: () => net.Socket, socketFactoryAction: (backendSocket: net.Socket, frontendSocket: net.Socket) => void): IFuture<any> {
16+
return (() => {
17+
let socketFactory = (callback: (_socket: net.Socket) => void) => this.connectEventually(factory, callback);
18+
19+
this.$projectDataService.initialize(this.$projectData.projectDir);
20+
let frameworkVersion = this.$projectDataService.getValue("tns-ios").wait().version;
21+
let result: any;
22+
23+
if(semver.gte(frameworkVersion, "1.4.0")) {
24+
result = this.createTcpSocketProxy(socketFactory, socketFactoryAction);
25+
} else {
26+
result = this.createWebSocketProxy(socketFactory);
27+
}
28+
29+
return result;
30+
}).future<any>()();
31+
}
32+
33+
private connectEventually(factory: () => net.Socket, handler: (_socket: net.Socket) => void) { // TODO: Add socketProxyBase
34+
function tryConnect() {
35+
let tryConnectAfterTimeout = setTimeout.bind(undefined, tryConnect, 1000);
36+
37+
let socket = factory();
38+
socket.on("connect", () => {
39+
socket.removeListener("error", tryConnectAfterTimeout);
40+
handler(socket);
41+
});
42+
socket.on("error", tryConnectAfterTimeout);
43+
}
44+
45+
tryConnect();
46+
}
47+
48+
private createWebSocketProxy(socketFactory: (handler: (socket: net.Socket) => void) => void): ws.Server {
49+
// NOTE: We will try to provide command line options to select ports, at least on the localhost.
50+
let localPort = 8080;
51+
52+
this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n");
53+
54+
// NB: When the inspector frontend connects we might not have connected to the inspector backend yet.
55+
// That's why we use the verifyClient callback of the websocket server to stall the upgrade request until we connect.
56+
// We store the socket that connects us to the device in the upgrade request object itself and later on retrieve it
57+
// in the connection callback.
58+
59+
let server = ws.createServer(<any>{
60+
port: localPort,
61+
verifyClient: (info: any, callback: any) => {
62+
this.$logger.info("Frontend client connected.");
63+
socketFactory((_socket: any) => {
64+
this.$logger.info("Backend socket created.");
65+
info.req["__deviceSocket"] = _socket;
66+
callback(true);
67+
});
68+
}
69+
});
70+
server.on("connection", (webSocket) => {
71+
let deviceSocket: net.Socket = (<any>webSocket.upgradeReq)["__deviceSocket"];
72+
let packets = new PacketStream();
73+
deviceSocket.pipe(packets);
74+
75+
packets.on("data", (buffer: Buffer) => {
76+
webSocket.send(buffer.toString("utf16le"));
77+
});
78+
79+
webSocket.on("message", (message, flags) => {
80+
let length = Buffer.byteLength(message, "utf16le");
81+
let payload = new Buffer(length + 4);
82+
payload.writeInt32BE(length, 0);
83+
payload.write(message, 4, length, "utf16le");
84+
deviceSocket.write(payload);
85+
});
86+
87+
deviceSocket.on("end", () => {
88+
this.$logger.info("Backend socket closed!");
89+
process.exit(0);
90+
});
91+
92+
webSocket.on("close", () => {
93+
this.$logger.info('Frontend socket closed!');
94+
process.exit(0);
95+
});
96+
});
97+
98+
this.$logger.info("Opened localhost " + localPort);
99+
return server;
100+
}
101+
102+
private createTcpSocketProxy(socketFactory: (handler: (socket: net.Socket) => void) => void, socketFactoryAction: (backendSocket: net.Socket, frontendSocket: net.Socket) => void): string {
103+
this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n");
104+
105+
let server = net.createServer({
106+
allowHalfOpen: true
107+
});
108+
109+
server.on("connection", (frontendSocket: net.Socket) => {
110+
this.$logger.info("Frontend client connected.");
111+
112+
frontendSocket.on("end", () => {
113+
this.$logger.info('Frontend socket closed!');
114+
process.exit(0);
115+
});
116+
117+
socketFactory((backendSocket: net.Socket) => {
118+
this.$logger.info("Backend socket created.");
119+
120+
backendSocket.on("end", () => {
121+
this.$logger.info("Backend socket closed!");
122+
process.exit(0);
123+
});
124+
125+
socketFactoryAction(backendSocket, frontendSocket);
126+
});
127+
});
128+
129+
let socketFileLocation = temp.path({ suffix: ".sock" });
130+
server.listen(socketFileLocation);
131+
132+
return socketFileLocation;
133+
}
134+
}
135+
$injector.register("socketProxyFactory", SocketProxyFactory);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
///<reference path="../../.d.ts"/>
2+
"use strict";
3+
4+
import helpers = require("../../common/helpers");
5+
import * as net from "net";
6+
import * as iOSProxyServices from "../../common/mobile/ios/ios-proxy-services";
7+
8+
let InspectorBackendPort = 18181;
9+
10+
export class IOSSocketRequestExecutor implements IiOSSocketRequestExecutor {
11+
constructor(private $errors: IErrors,
12+
private $injector: IInjector,
13+
private $iOSNotification: IiOSNotification,
14+
private $iOSNotificationService: IiOSNotificationService,
15+
private $logger: ILogger,
16+
private $projectData: IProjectData,
17+
private $socketProxyFactory: ISocketProxyFactory) { }
18+
19+
public executeAttachRequest(socketActions: ISocketActions, timeout: number, device: Mobile.IiOSDevice): IFuture<void> {
20+
return (() => {
21+
let npc = new iOSProxyServices.NotificationProxyClient(device, this.$injector);
22+
23+
let [alreadyConnected, readyForAttach, attachAvailable] = [this.$iOSNotification.alreadyConnected, this.$iOSNotification.readyForAttach, this.$iOSNotification.attachAvailable]
24+
.map((notification) => this.$iOSNotificationService.awaitNotification(npc, notification, timeout));
25+
26+
npc.postNotificationAndAttachForData(this.$iOSNotification.attachAvailabilityQuery);
27+
28+
let receivedNotification: IFuture<string>;
29+
try {
30+
receivedNotification = helpers.whenAny<string>(alreadyConnected, readyForAttach, attachAvailable).wait();
31+
} catch (e) {
32+
this.$errors.failWithoutHelp(`The application ${this.$projectData.projectId} does not appear to be running on ${device.deviceInfo.displayName} or is not built with debugging enabled.`);
33+
}
34+
35+
switch (receivedNotification) {
36+
case alreadyConnected:
37+
this.$errors.failWithoutHelp("A client is already connected.");
38+
break;
39+
case attachAvailable:
40+
process.nextTick(() => npc.postNotificationAndAttachForData(this.$iOSNotification.attachRequest));
41+
try {
42+
this.$iOSNotificationService.awaitNotification(npc, this.$iOSNotification.readyForAttach, timeout).wait();
43+
} catch (e) {
44+
this.$errors.failWithoutHelp(`The application ${this.$projectData.projectId} timed out when performing the socket handshake.`);
45+
}
46+
this.executeRequestCore(socketActions, device).wait();
47+
break;
48+
case readyForAttach:
49+
this.executeRequestCore(socketActions, device).wait();
50+
break;
51+
}
52+
}).future<void>()();
53+
}
54+
55+
public executeLaunchRequest(socketActions: ISocketActions, timeout: number, readyForAttachTimeout: number, device: Mobile.IiOSDevice): IFuture<void> {
56+
return (() => {
57+
let npc = new iOSProxyServices.NotificationProxyClient(device, this.$injector);
58+
59+
try {
60+
this.$iOSNotificationService.awaitNotification(npc, this.$iOSNotification.appLaunching, timeout).wait();
61+
process.nextTick(() => {
62+
npc.postNotificationAndAttachForData(this.$iOSNotification.waitForDebug);
63+
npc.postNotificationAndAttachForData(this.$iOSNotification.attachRequest);
64+
});
65+
66+
this.$iOSNotificationService.awaitNotification(npc, this.$iOSNotification.readyForAttach, readyForAttachTimeout).wait();
67+
} catch(e) {
68+
this.$logger.trace(`Timeout error: ${e}`);
69+
this.$errors.failWithoutHelp("Timeout waiting for response from NativeScript runtime.");
70+
}
71+
72+
this.executeRequestCore(socketActions, device).wait();
73+
74+
}).future<void>()();
75+
}
76+
77+
public executeRequest(factory: () => net.Socket, socketActions: ISocketActions): IFuture<void> {
78+
return (() => {
79+
let socketProxy = this.$socketProxyFactory.createSocketProxy(factory, socketActions.socketFactoryAction).wait();
80+
socketActions.afterSocketHandshakeAction(socketProxy).wait();
81+
}).future<void>()();
82+
}
83+
84+
private executeRequestCore(socketActions: ISocketActions, device: Mobile.IiOSDevice): IFuture<void> {
85+
let factory = () => device.connectToPort(InspectorBackendPort);
86+
return this.executeRequest(factory, socketActions);
87+
}
88+
}
89+
$injector.register("iOSSocketRequestExecutor", IOSSocketRequestExecutor);

0 commit comments

Comments
 (0)