microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.4.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/debugger/ios/deviceRunner.ts

269lines · modeblame

488f1908Jimmy Thomson10 years ago1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
bc96b26bJimmy Thomson10 years ago4import {ChildProcess} from "child_process";
5import * as net from "net";
6import * as Q from "q";
488f1908Jimmy Thomson10 years ago7
b0061ac6Meena Kunnathur Balakrishnan10 years ago8import {Node} from "../../common/node/node";
a9d96b7cdigeff10 years ago9import {PlistBuddy} from "../../common/ios/plistBuddy";
488f1908Jimmy Thomson10 years ago10
11export class DeviceRunner {
12private projectRoot: string;
bc96b26bJimmy Thomson10 years ago13private nativeDebuggerProxyInstance: ChildProcess;
0d8e6414digeff10 years ago14private childProcess = new Node.ChildProcess();
488f1908Jimmy Thomson10 years ago15
16constructor(projectRoot: string) {
17this.projectRoot = projectRoot;
bc96b26bJimmy Thomson10 years ago18process.on("exit", () => this.cleanup());
488f1908Jimmy Thomson10 years ago19}
20
21public run(): Q.Promise<void> {
bc96b26bJimmy Thomson10 years ago22const proxyPort = 9999;
e73ce045Jimmy Thomson10 years ago23const appLaunchStepTimeout = 10000;
bc96b26bJimmy Thomson10 years ago24return new PlistBuddy().getBundleId(this.projectRoot, /*simulator=*/false)
25.then((bundleId: string) => this.getPathOnDevice(bundleId))
26.then((path: string) =>
27this.startNativeDebugProxy(proxyPort).then(() =>
28this.startAppViaDebugger(proxyPort, path, appLaunchStepTimeout)
29)
30)
31.then(() => { });
32}
33
bdad2966Joshua Skelton10 years ago34// Attempt to start the app on the device, using the debug server proxy on a given port.
35// Returns a socket speaking remote gdb protocol with the debug server proxy.
36public startAppViaDebugger(portNumber: number, packagePath: string, appLaunchStepTimeout: number): Q.Promise<string> {
37const encodedPath: string = this.encodePath(packagePath);
38
39// We need to send 3 messages to the proxy, waiting for responses between each message:
40// A(length of encoded path),0,(encoded path)
41// Hc0
42// c
43// We expect a '+' for each message sent, followed by a $OK#9a to indicate that everything has worked.
44// For more info, see http://www.opensource.apple.com/source/lldb/lldb-167.2/docs/lldb-gdb-remote.txt
45const socket: net.Socket = new net.Socket();
46let initState: number = 0;
47let endStatus: number = null;
48let endSignal: number = null;
49
50const deferred1: Q.Deferred<net.Socket> = Q.defer<net.Socket>();
51const deferred2: Q.Deferred<net.Socket> = Q.defer<net.Socket>();
52
df86c7acJimmy Thomson10 years ago53socket.on("data", (data: any): void => {
bdad2966Joshua Skelton10 years ago54data = data.toString();
55while (data[0] === "+") { data = data.substring(1); }
56// Acknowledge any packets sent our way
57if (data[0] === "$") {
58socket.write("+");
59if (data[1] === "W") {
60// The app process has exited, with hex status given by data[2-3]
61let status: number = parseInt(data.substring(2, 4), 16);
62endStatus = status;
63socket.end();
64} else if (data[1] === "X") {
65// The app rocess exited because of signal given by data[2-3]
66let signal: number = parseInt(data.substring(2, 4), 16);
67endSignal = signal;
68socket.end();
69} else if (data.substring(1, 3) === "OK") {
70// last command was received OK;
71if (initState === 1) {
72deferred1.resolve(socket);
73} else if (initState === 2) {
74deferred2.resolve(socket);
75}
76} else if (data[1] === "O") {
77// STDOUT was written to, and the rest of the input until reaching a "#" is a hex-encoded string of that output
78} else if (data[1] === "E") {
79// An error has occurred, with error code given by data[2-3]: parseInt(data.substring(2, 4), 16)
80const error = new Error("Unable to launch application.");
81deferred1.reject(error);
82deferred2.reject(error);
83}
84}
85});
86
87socket.on("end", function(): void {
88const error = new Error("Unable to launch application.");
89deferred1.reject(error);
90deferred2.reject(error);
91});
92
93socket.on("error", function(err: Error): void {
94deferred1.reject(err);
95deferred2.reject(err);
96});
97
98socket.connect(portNumber, "localhost", () => {
99// set argument 0 to the (encoded) path of the app
100const cmd: string = this.makeGdbCommand("A" + encodedPath.length + ",0," + encodedPath);
101initState++;
102socket.write(cmd);
103setTimeout(function(): void {
104deferred1.reject(new Error("Timeout launching application. Is the device locked?"));
105}, appLaunchStepTimeout);
106});
107
108return deferred1.promise.then((sock: net.Socket): Q.Promise<net.Socket> => {
109// Set the step and continue thread to any thread
110const cmd: string = this.makeGdbCommand("Hc0");
111initState++;
112sock.write(cmd);
113setTimeout(function(): void {
114deferred2.reject(new Error("Timeout launching application. Is the device locked?"));
115}, appLaunchStepTimeout);
116return deferred2.promise;
e73ce045Jimmy Thomson10 years ago117}).then((sock: net.Socket): void => {
bdad2966Joshua Skelton10 years ago118// Continue execution; actually start the app running.
119const cmd: string = this.makeGdbCommand("c");
120initState++;
121sock.write(cmd);
e73ce045Jimmy Thomson10 years ago122return;
bdad2966Joshua Skelton10 years ago123}).then(() => packagePath);
124}
125
126public encodePath(packagePath: string): string {
127// Encode the path by converting each character value to hex
128return packagePath.split("").map((c: string) => c.charCodeAt(0).toString(16)).join("").toUpperCase();
129}
130
bc96b26bJimmy Thomson10 years ago131private cleanup(): void {
132if (this.nativeDebuggerProxyInstance) {
133this.nativeDebuggerProxyInstance.kill("SIGHUP");
134this.nativeDebuggerProxyInstance = null;
135}
136}
137
138private startNativeDebugProxy(proxyPort: number): Q.Promise<void> {
139this.cleanup();
140
df86c7acJimmy Thomson10 years ago141return this.mountDeveloperImage().then((): Q.Promise<any> => {
0d8e6414digeff10 years ago142let result = this.childProcess.spawn("idevicedebugserverproxy", [proxyPort.toString()]);
93c4b670digeff10 years ago143result.outcome.done(() => {}, () => {}); // Q prints a warning if we don't call .done(). We ignore all outcome errors
9596aa53digeff10 years ago144return result.startup.then(() => this.nativeDebuggerProxyInstance = result.spawnedProcess);
bc96b26bJimmy Thomson10 years ago145});
146}
147
148private mountDeveloperImage(): Q.Promise<void> {
df86c7acJimmy Thomson10 years ago149return this.getDiskImage().then((path: string): Q.Promise<void> => {
0d8e6414digeff10 years ago150const imagemounter = this.childProcess.spawn("ideviceimagemounter", [path]).spawnedProcess;
bc96b26bJimmy Thomson10 years ago151const deferred = Q.defer<void>();
152let stdout: string = "";
153imagemounter.stdout.on("data", function(data: any): void {
154stdout += data.toString();
155});
156imagemounter.on("exit", function(code: number): void {
157if (code !== 0) {
158if (stdout.indexOf("Error:") !== -1) {
159deferred.resolve(void 0); // Technically failed, but likely caused by the image already being mounted.
160} else if (stdout.indexOf("No device found, is it plugged in?") !== -1) {
cb6d0922digeff10 years ago161deferred.reject(new Error("Unable to find device. Is the device plugged in?"));
bc96b26bJimmy Thomson10 years ago162}
163
cb6d0922digeff10 years ago164deferred.reject(new Error("Unable to mount developer disk image."));
bc96b26bJimmy Thomson10 years ago165} else {
166deferred.resolve(void 0);
167}
168});
169imagemounter.on("error", function(err: any): void {
170deferred.reject(err);
171});
172return deferred.promise;
173});
174}
175
176private getDiskImage(): Q.Promise<string> {
0d8e6414digeff10 years ago177const nodeChildProcess = this.childProcess;
bc96b26bJimmy Thomson10 years ago178// Attempt to find the OS version of the iDevice, e.g. 7.1
81f88231Patricio Beltran9 years ago179const versionInfo = nodeChildProcess.exec("ideviceinfo -s -k ProductVersion").outcome.then((stdout: string) => {
bc96b26bJimmy Thomson10 years ago180return stdout.toString().trim().substring(0, 3); // Versions for DeveloperDiskImage seem to be X.Y, while some device versions are X.Y.Z
181// NOTE: This will almost certainly be wrong in the next few years, once we hit version 10.0
182}, function(): string {
183throw new Error("Unable to get device OS version");
184});
185
186// Attempt to find the path where developer resources exist.
81f88231Patricio Beltran9 years ago187const pathInfo = nodeChildProcess.exec("xcrun -sdk iphoneos --show-sdk-platform-path").outcome.then((stdout: string) => {
bc96b26bJimmy Thomson10 years ago188return stdout.toString().trim();
189});
190
191// Attempt to find the developer disk image for the appropriate
192return Q.all([versionInfo, pathInfo]).spread<string>(function(version: string, sdkpath: string): Q.Promise<string> {
9596aa53digeff10 years ago193const find = nodeChildProcess.spawn("find", [sdkpath, "-path", "*" + version + "*", "-name", "DeveloperDiskImage.dmg"]).spawnedProcess;
bc96b26bJimmy Thomson10 years ago194const deferred = Q.defer<string>();
195
196find.stdout.on("data", function(data: any): void {
197const dataStr: string = data.toString();
198const path: string = dataStr.split("\n")[0].trim();
199if (!path) {
cb6d0922digeff10 years ago200deferred.reject(new Error("Unable to find developer disk image"));
bc96b26bJimmy Thomson10 years ago201} else {
202deferred.resolve(path);
203}
204});
205find.on("exit", function(code: number): void {
cb6d0922digeff10 years ago206deferred.reject(new Error("Unable to find developer disk image"));
bc96b26bJimmy Thomson10 years ago207});
208
209return deferred.promise;
210});
211}
212
213private getPathOnDevice(packageId: string): Q.Promise<string> {
0d8e6414digeff10 years ago214const nodeChildProcess = this.childProcess;
bc96b26bJimmy Thomson10 years ago215const nodeFileSystem = new Node.FileSystem();
216return nodeChildProcess.execToString("ideviceinstaller -l -o xml > /tmp/$$.ideviceinstaller && echo /tmp/$$.ideviceinstaller")
217.catch(function(err: any): any {
218if (err.code === "ENOENT") {
219throw new Error("Unable to find ideviceinstaller.");
220}
221throw err;
222}).then((stdout: string): Q.Promise<string> => {
223// First find the path of the app on the device
224let filename: string = stdout.trim();
225if (!/^\/tmp\/[0-9]+\.ideviceinstaller$/.test(filename)) {
226throw new Error("Unable to list installed applications on device");
227}
228
229const plistBuddy = new PlistBuddy();
230// Search thrown the unknown-length array until we find the package
231const findPackageEntry = (index: number): Q.Promise<string> => {
232return plistBuddy.readPlistProperty(filename, `:${index}:CFBundleIdentifier`)
233.then((bundleId: string) => {
234if (bundleId === packageId) {
235return plistBuddy.readPlistProperty(filename, `:${index}:Path`);
236}
237return findPackageEntry(index + 1);
238});
239};
240
241return findPackageEntry(0)
242.finally(() => {
243nodeFileSystem.unlink(filename);
244}).catch((): string => {
245throw new Error("Application not installed on the device");
246});
247});
248}
249
250private makeGdbCommand(command: string): string {
251let commandString: string = `$${command}#`;
252let stringSum: number = 0;
253for (let i: number = 0; i < command.length; i++) {
254stringSum += command.charCodeAt(i);
255}
256
257/* tslint:disable:no-bitwise */
258// We need some bitwise operations to calculate the checksum
259stringSum = stringSum & 0xFF;
260/* tslint:enable:no-bitwise */
261let checksum: string = stringSum.toString(16).toUpperCase();
262if (checksum.length < 2) {
263checksum = "0" + checksum;
264}
265
266commandString += checksum;
267return commandString;
488f1908Jimmy Thomson10 years ago268}
269}