Skip to content

Commit 5ee9cfb

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 19743c5 commit 5ee9cfb

11 files changed

+479
-321
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

+25
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ interface IUsbLiveSyncService {
6161
interface IPlatformSpecificUsbLiveSyncService {
6262
restartApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths?: Mobile.ILocalToDevicePathData[]): IFuture<void>;
6363
beforeLiveSyncAction?(deviceAppData: Mobile.IDeviceAppData): IFuture<void>;
64+
sendReloadMessageToDevice(): IFuture<void>;
65+
sendReloadMessageToSimulator(): IFuture<void>;
6466
}
6567

6668
interface IOptions extends ICommonOptions {
@@ -147,3 +149,26 @@ interface IAndroidToolsInfoData {
147149
*/
148150
targetSdkVersion: number;
149151
}
152+
153+
interface ISocketProxyFactory {
154+
createSocketProxy(factory: () => any): IFuture<any>;
155+
}
156+
157+
interface IiOSNotification {
158+
waitForDebug: string;
159+
attachRequest: string;
160+
appLaunching: string;
161+
readyForAttach: string;
162+
attachAvailabilityQuery: string;
163+
alreadyConnected: string;
164+
attachAvailable: string;
165+
}
166+
167+
interface IiOSNotificationService {
168+
awaitNotification(npc: Mobile.INotificationProxyClient, notification: string, timeout: number): IFuture<string>;
169+
}
170+
171+
interface IiOSSocketRequestExecutor {
172+
executeLaunchRequest(device: Mobile.IiOSDevice, timeout: number, readyForAttachTimeout: number): IFuture<void>;
173+
executeAttachRequest(device: Mobile.IiOSDevice, timeout: number): IFuture<void>;
174+
}
+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,140 @@
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): 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);
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 encoding = "utf16le";
72+
73+
let deviceSocket: net.Socket = (<any>webSocket.upgradeReq)["__deviceSocket"];
74+
let packets = new PacketStream();
75+
deviceSocket.pipe(packets);
76+
77+
packets.on("data", (buffer: Buffer) => {
78+
webSocket.send(buffer.toString(encoding));
79+
});
80+
81+
webSocket.on("message", (message, flags) => {
82+
let length = Buffer.byteLength(message, encoding);
83+
let payload = new Buffer(length + 4);
84+
payload.writeInt32BE(length, 0);
85+
payload.write(message, 4, length, encoding);
86+
deviceSocket.write(payload);
87+
});
88+
89+
deviceSocket.on("end", () => {
90+
this.$logger.info("Backend socket closed!");
91+
process.exit(0);
92+
});
93+
94+
webSocket.on("close", () => {
95+
this.$logger.info('Frontend socket closed!');
96+
process.exit(0);
97+
});
98+
99+
});
100+
101+
this.$logger.info("Opened localhost " + localPort);
102+
return server;
103+
}
104+
105+
private createTcpSocketProxy(socketFactory: (handler: (socket: net.Socket) => void) => void): string {
106+
this.$logger.info("\nSetting up proxy...\nPress Ctrl + C to terminate, or disconnect.\n");
107+
108+
let server = net.createServer({
109+
allowHalfOpen: true
110+
});
111+
112+
server.on("connection", (frontendSocket: net.Socket) => {
113+
this.$logger.info("Frontend client connected.");
114+
115+
frontendSocket.on("end", () => {
116+
this.$logger.info('Frontend socket closed!');
117+
process.exit(0);
118+
});
119+
120+
socketFactory((backendSocket: net.Socket) => {
121+
this.$logger.info("Backend socket created.");
122+
123+
backendSocket.on("end", () => {
124+
this.$logger.info("Backend socket closed!");
125+
process.exit(0);
126+
});
127+
128+
backendSocket.pipe(frontendSocket);
129+
frontendSocket.pipe(backendSocket);
130+
frontendSocket.resume();
131+
});
132+
});
133+
134+
let socketFileLocation = temp.path({ suffix: ".sock" });
135+
server.listen(socketFileLocation);
136+
137+
return socketFileLocation;
138+
}
139+
}
140+
$injector.register("socketProxyFactory", SocketProxyFactory);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
///<reference path="../../.d.ts"/>
2+
"use strict";
3+
4+
import * as helpers from "../../common/helpers";
5+
import * as iOSProxyServices from "../../common/mobile/ios/ios-proxy-services";
6+
7+
export class IOSSocketRequestExecutor implements IiOSSocketRequestExecutor {
8+
constructor(private $errors: IErrors,
9+
private $injector: IInjector,
10+
private $iOSNotification: IiOSNotification,
11+
private $iOSNotificationService: IiOSNotificationService,
12+
private $logger: ILogger,
13+
private $projectData: IProjectData,
14+
private $socketProxyFactory: ISocketProxyFactory) { }
15+
16+
public executeAttachRequest(device: Mobile.IiOSDevice, timeout: number): IFuture<void> {
17+
return (() => {
18+
let npc = new iOSProxyServices.NotificationProxyClient(device, this.$injector);
19+
20+
let [alreadyConnected, readyForAttach, attachAvailable] = [this.$iOSNotification.alreadyConnected, this.$iOSNotification.readyForAttach, this.$iOSNotification.attachAvailable]
21+
.map((notification) => this.$iOSNotificationService.awaitNotification(npc, notification, timeout));
22+
23+
npc.postNotificationAndAttachForData(this.$iOSNotification.attachAvailabilityQuery);
24+
25+
let receivedNotification: IFuture<string>;
26+
try {
27+
receivedNotification = helpers.whenAny(alreadyConnected, readyForAttach, attachAvailable).wait();
28+
} catch (e) {
29+
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.`);
30+
}
31+
32+
switch (receivedNotification) {
33+
case alreadyConnected:
34+
this.$errors.failWithoutHelp("A client is already connected.");
35+
break;
36+
case attachAvailable:
37+
this.executeAttachAvailable(npc, timeout).wait();
38+
break;
39+
case readyForAttach:
40+
break;
41+
}
42+
}).future<void>()();
43+
}
44+
45+
public executeLaunchRequest(device: Mobile.IiOSDevice, timeout: number, readyForAttachTimeout: number): IFuture<void> {
46+
return (() => {
47+
let npc = new iOSProxyServices.NotificationProxyClient(device, this.$injector);
48+
49+
try {
50+
this.$iOSNotificationService.awaitNotification(npc, this.$iOSNotification.appLaunching, timeout).wait();
51+
process.nextTick(() => {
52+
npc.postNotificationAndAttachForData(this.$iOSNotification.waitForDebug );
53+
npc.postNotificationAndAttachForData(this.$iOSNotification.attachRequest);
54+
});
55+
56+
this.$iOSNotificationService.awaitNotification(npc, this.$iOSNotification.readyForAttach, readyForAttachTimeout).wait();
57+
} catch(e) {
58+
this.$logger.trace(`Timeout error: ${e}`);
59+
this.$errors.failWithoutHelp("Timeout waiting for response from NativeScript runtime.");
60+
}
61+
}).future<void>()();
62+
}
63+
64+
private executeAttachAvailable(npc: Mobile.INotificationProxyClient, timeout: number): IFuture<void> {
65+
return (() => {
66+
process.nextTick(() => npc.postNotificationAndAttachForData(this.$iOSNotification.attachRequest));
67+
try {
68+
this.$iOSNotificationService.awaitNotification(npc, this.$iOSNotification.readyForAttach, timeout).wait();
69+
} catch (e) {
70+
this.$errors.failWithoutHelp(`The application ${this.$projectData.projectId} timed out when performing the socket handshake.`);
71+
}
72+
}).future<void>()();
73+
}
74+
}
75+
$injector.register("iOSSocketRequestExecutor", IOSSocketRequestExecutor);

0 commit comments

Comments
 (0)