Skip to content

Commit c579ae8

Browse files
FatmeFatme
Fatme
authored and
Fatme
committed
Merge pull request #1107 from NativeScript/fatme/page-reload
Send page reload message to iOS runtime
2 parents 64e7c30 + c8df28b commit c579ae8

14 files changed

+534
-361
lines changed

lib/bootstrap.ts

+5
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,8 @@ $injector.require("androidToolsInfo", "./android-tools-info");
8585
$injector.require("iosUsbLiveSyncServiceLocator", "./services/usb-livesync-service");
8686
$injector.require("androidUsbLiveSyncServiceLocator", "./services/usb-livesync-service");
8787
$injector.require("sysInfo", "./sys-info");
88+
89+
$injector.require("iOSNotificationService", "./services/ios-notification-service");
90+
$injector.require("socketProxyFactory", "./device-sockets/ios/socket-proxy-factory");
91+
$injector.require("iOSNotification", "./device-sockets/ios/notification");
92+
$injector.require("iOSSocketRequestExecutor", "./device-sockets/ios/socket-request-executor");

lib/declarations.ts

+25-3
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,8 @@ interface IUsbLiveSyncService {
5858
liveSync(platform: string): IFuture<void>;
5959
}
6060

61-
interface IPlatformSpecificUsbLiveSyncService {
62-
restartApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths?: Mobile.ILocalToDevicePathData[]): IFuture<void>;
63-
beforeLiveSyncAction?(deviceAppData: Mobile.IDeviceAppData): IFuture<void>;
61+
interface IiOSUsbLiveSyncService extends IPlatformSpecificUsbLiveSyncService {
62+
sendPageReloadMessageToSimulator(): IFuture<void>;
6463
}
6564

6665
interface IOptions extends ICommonOptions {
@@ -168,3 +167,26 @@ interface IAndroidToolsInfoData {
168167
*/
169168
targetSdkVersion: number;
170169
}
170+
171+
interface ISocketProxyFactory {
172+
createSocketProxy(factory: () => any): IFuture<any>;
173+
}
174+
175+
interface IiOSNotification {
176+
waitForDebug: string;
177+
attachRequest: string;
178+
appLaunching: string;
179+
readyForAttach: string;
180+
attachAvailabilityQuery: string;
181+
alreadyConnected: string;
182+
attachAvailable: string;
183+
}
184+
185+
interface IiOSNotificationService {
186+
awaitNotification(npc: Mobile.INotificationProxyClient, notification: string, timeout: number): IFuture<string>;
187+
}
188+
189+
interface IiOSSocketRequestExecutor {
190+
executeLaunchRequest(device: Mobile.IiOSDevice, timeout: number, readyForAttachTimeout: number): IFuture<void>;
191+
executeAttachRequest(device: Mobile.IiOSDevice, timeout: number): IFuture<void>;
192+
}

lib/definitions/platform.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface IPlatformService {
1818
getLatestApplicationPackageForDevice(platformData: IPlatformData): IFuture<IApplicationPackage>;
1919
getLatestApplicationPackageForEmulator(platformData: IPlatformData): IFuture<IApplicationPackage>;
2020
copyLastOutput(platform: string, targetPath: string, settings: {isForDevice: boolean}): IFuture<void>;
21+
ensurePlatformInstalled(platform: string): IFuture<void>;
2122
}
2223

2324
interface IPlatformData {
+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,126 @@
1+
///<reference path="../../.d.ts"/>
2+
"use strict";
3+
4+
import { PacketStream } from "./packet-stream";
5+
import * as net from "net";
6+
import * as semver from "semver";
7+
import * as ws from "ws";
8+
import temp = require("temp");
9+
import * as helpers from "../../common/helpers";
10+
11+
export class SocketProxyFactory implements ISocketProxyFactory {
12+
constructor(private $logger: ILogger,
13+
private $projectData: IProjectData,
14+
private $projectDataService: IProjectDataService) { }
15+
16+
public createSocketProxy(factory: () => net.Socket): IFuture<any> {
17+
return (() => {
18+
let socketFactory = (callback: (_socket: net.Socket) => void) => helpers.connectEventually(factory, callback);
19+
20+
this.$projectDataService.initialize(this.$projectData.projectDir);
21+
let frameworkVersion = this.$projectDataService.getValue("tns-ios").wait().version;
22+
let result: any;
23+
24+
if(semver.gte(frameworkVersion, "1.4.0")) {
25+
result = this.createTcpSocketProxy(socketFactory);
26+
} else {
27+
result = this.createWebSocketProxy(socketFactory);
28+
}
29+
30+
return result;
31+
}).future<any>()();
32+
}
33+
34+
private createWebSocketProxy(socketFactory: (handler: (socket: net.Socket) => void) => void): ws.Server {
35+
// NOTE: We will try to provide command line options to select ports, at least on the localhost.
36+
let localPort = 8080;
37+
38+
this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n");
39+
40+
// NB: When the inspector frontend connects we might not have connected to the inspector backend yet.
41+
// That's why we use the verifyClient callback of the websocket server to stall the upgrade request until we connect.
42+
// We store the socket that connects us to the device in the upgrade request object itself and later on retrieve it
43+
// in the connection callback.
44+
45+
let server = ws.createServer(<any>{
46+
port: localPort,
47+
verifyClient: (info: any, callback: Function) => {
48+
this.$logger.info("Frontend client connected.");
49+
socketFactory((_socket: any) => {
50+
this.$logger.info("Backend socket created.");
51+
info.req["__deviceSocket"] = _socket;
52+
callback(true);
53+
});
54+
}
55+
});
56+
server.on("connection", (webSocket) => {
57+
let encoding = "utf16le";
58+
59+
let deviceSocket: net.Socket = (<any>webSocket.upgradeReq)["__deviceSocket"];
60+
let packets = new PacketStream();
61+
deviceSocket.pipe(packets);
62+
63+
packets.on("data", (buffer: Buffer) => {
64+
webSocket.send(buffer.toString(encoding));
65+
});
66+
67+
webSocket.on("message", (message, flags) => {
68+
let length = Buffer.byteLength(message, encoding);
69+
let payload = new Buffer(length + 4);
70+
payload.writeInt32BE(length, 0);
71+
payload.write(message, 4, length, encoding);
72+
deviceSocket.write(payload);
73+
});
74+
75+
deviceSocket.on("end", () => {
76+
this.$logger.info("Backend socket closed!");
77+
process.exit(0);
78+
});
79+
80+
webSocket.on("close", () => {
81+
this.$logger.info('Frontend socket closed!');
82+
process.exit(0);
83+
});
84+
85+
});
86+
87+
this.$logger.info("Opened localhost " + localPort);
88+
return server;
89+
}
90+
91+
private createTcpSocketProxy(socketFactory: (handler: (socket: net.Socket) => void) => void): string {
92+
this.$logger.info("\nSetting up proxy...\nPress Ctrl + C to terminate, or disconnect.\n");
93+
94+
let server = net.createServer({
95+
allowHalfOpen: true
96+
});
97+
98+
server.on("connection", (frontendSocket: net.Socket) => {
99+
this.$logger.info("Frontend client connected.");
100+
101+
frontendSocket.on("end", () => {
102+
this.$logger.info('Frontend socket closed!');
103+
process.exit(0);
104+
});
105+
106+
socketFactory((backendSocket: net.Socket) => {
107+
this.$logger.info("Backend socket created.");
108+
109+
backendSocket.on("end", () => {
110+
this.$logger.info("Backend socket closed!");
111+
process.exit(0);
112+
});
113+
114+
backendSocket.pipe(frontendSocket);
115+
frontendSocket.pipe(backendSocket);
116+
frontendSocket.resume();
117+
});
118+
});
119+
120+
let socketFileLocation = temp.path({ suffix: ".sock" });
121+
server.listen(socketFileLocation);
122+
123+
return socketFileLocation;
124+
}
125+
}
126+
$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)