microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8a34309cfa5bb01e6bde1573e73e394cc4f4af15

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/ios/deviceRunner.ts

280lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import {ChildProcess} from "child_process";
5import * as net from "net";
6import * as Q from "q";
7
8import {Node} from "../../common/node/node";
9import {PlistBuddy} from "../../common/ios/plistBuddy";
10
11export class DeviceRunner {
12 private projectRoot: string;
13 private nativeDebuggerProxyInstance: ChildProcess;
14 private childProcess = new Node.ChildProcess();
15
16 constructor(projectRoot: string) {
17 this.projectRoot = projectRoot;
18 process.on("exit", () => this.cleanup());
19 }
20
21 public run(): Q.Promise<void> {
22 const proxyPort = 9999;
23 const appLaunchStepTimeout = 5000;
24 return new PlistBuddy().getBundleId(this.projectRoot, /*simulator=*/false)
25 .then((bundleId: string) => this.getPathOnDevice(bundleId))
26 .then((path: string) =>
27 this.startNativeDebugProxy(proxyPort).then(() =>
28 this.startAppViaDebugger(proxyPort, path, appLaunchStepTimeout)
29 )
30 )
31 .then(() => { });
32 }
33
34 // 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.
36 public startAppViaDebugger(portNumber: number, packagePath: string, appLaunchStepTimeout: number): Q.Promise<string> {
37 const 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
45 const socket: net.Socket = new net.Socket();
46 let initState: number = 0;
47 let endStatus: number = null;
48 let endSignal: number = null;
49
50 const deferred1: Q.Deferred<net.Socket> = Q.defer<net.Socket>();
51 const deferred2: Q.Deferred<net.Socket> = Q.defer<net.Socket>();
52 const deferred3: Q.Deferred<net.Socket> = Q.defer<net.Socket>();
53
54 socket.on("data", function(data: any): void {
55 data = data.toString();
56 while (data[0] === "+") { data = data.substring(1); }
57 // Acknowledge any packets sent our way
58 if (data[0] === "$") {
59 socket.write("+");
60 if (data[1] === "W") {
61 // The app process has exited, with hex status given by data[2-3]
62 let status: number = parseInt(data.substring(2, 4), 16);
63 endStatus = status;
64 socket.end();
65 } else if (data[1] === "X") {
66 // The app rocess exited because of signal given by data[2-3]
67 let signal: number = parseInt(data.substring(2, 4), 16);
68 endSignal = signal;
69 socket.end();
70 } else if (data.substring(1, 3) === "OK") {
71 // last command was received OK;
72 if (initState === 1) {
73 deferred1.resolve(socket);
74 } else if (initState === 2) {
75 deferred2.resolve(socket);
76 }
77 } else if (data[1] === "O") {
78 // STDOUT was written to, and the rest of the input until reaching a "#" is a hex-encoded string of that output
79 if (initState === 3) {
80 deferred3.resolve(socket);
81 initState++;
82 }
83 } else if (data[1] === "E") {
84 // An error has occurred, with error code given by data[2-3]: parseInt(data.substring(2, 4), 16)
85 const error = new Error("Unable to launch application.");
86 deferred1.reject(error);
87 deferred2.reject(error);
88 deferred3.reject(error);
89 }
90 }
91 });
92
93 socket.on("end", function(): void {
94 const error = new Error("Unable to launch application.");
95 deferred1.reject(error);
96 deferred2.reject(error);
97 deferred3.reject(error);
98 });
99
100 socket.on("error", function(err: Error): void {
101 deferred1.reject(err);
102 deferred2.reject(err);
103 deferred3.reject(err);
104 });
105
106 socket.connect(portNumber, "localhost", () => {
107 // set argument 0 to the (encoded) path of the app
108 const cmd: string = this.makeGdbCommand("A" + encodedPath.length + ",0," + encodedPath);
109 initState++;
110 socket.write(cmd);
111 setTimeout(function(): void {
112 deferred1.reject(new Error("Timeout launching application. Is the device locked?"));
113 }, appLaunchStepTimeout);
114 });
115
116 return deferred1.promise.then((sock: net.Socket): Q.Promise<net.Socket> => {
117 // Set the step and continue thread to any thread
118 const cmd: string = this.makeGdbCommand("Hc0");
119 initState++;
120 sock.write(cmd);
121 setTimeout(function(): void {
122 deferred2.reject(new Error("Timeout launching application. Is the device locked?"));
123 }, appLaunchStepTimeout);
124 return deferred2.promise;
125 }).then((sock: net.Socket): Q.Promise<net.Socket> => {
126 // Continue execution; actually start the app running.
127 const cmd: string = this.makeGdbCommand("c");
128 initState++;
129 sock.write(cmd);
130 setTimeout(function(): void {
131 deferred3.reject(new Error("Timeout launching application. Is the device locked?"));
132 }, appLaunchStepTimeout);
133 return deferred3.promise;
134 }).then(() => packagePath);
135 }
136
137 public encodePath(packagePath: string): string {
138 // Encode the path by converting each character value to hex
139 return packagePath.split("").map((c: string) => c.charCodeAt(0).toString(16)).join("").toUpperCase();
140 }
141
142 private cleanup(): void {
143 if (this.nativeDebuggerProxyInstance) {
144 this.nativeDebuggerProxyInstance.kill("SIGHUP");
145 this.nativeDebuggerProxyInstance = null;
146 }
147 }
148
149 private startNativeDebugProxy(proxyPort: number): Q.Promise<void> {
150 this.cleanup();
151
152 return this.mountDeveloperImage().then(function(): Q.Promise<any> {
153 let result = this.childProcess.spawn("idevicedebugserverproxy", [proxyPort.toString()]);
154 result.outcome.done(() => {}, () => {}); // Q prints a warning if we don't call .done(). We ignore all outcome errors
155 return result.startup.then(() => this.nativeDebuggerProxyInstance = result.spawnedProcess);
156 });
157 }
158
159 private mountDeveloperImage(): Q.Promise<void> {
160 return this.getDiskImage().then(function(path: string): Q.Promise<void> {
161 const imagemounter = this.childProcess.spawn("ideviceimagemounter", [path]).spawnedProcess;
162 const deferred = Q.defer<void>();
163 let stdout: string = "";
164 imagemounter.stdout.on("data", function(data: any): void {
165 stdout += data.toString();
166 });
167 imagemounter.on("exit", function(code: number): void {
168 if (code !== 0) {
169 if (stdout.indexOf("Error:") !== -1) {
170 deferred.resolve(void 0); // Technically failed, but likely caused by the image already being mounted.
171 } else if (stdout.indexOf("No device found, is it plugged in?") !== -1) {
172 deferred.reject(new Error("Unable to find device. Is the device plugged in?"));
173 }
174
175 deferred.reject(new Error("Unable to mount developer disk image."));
176 } else {
177 deferred.resolve(void 0);
178 }
179 });
180 imagemounter.on("error", function(err: any): void {
181 deferred.reject(err);
182 });
183 return deferred.promise;
184 });
185 }
186
187 private getDiskImage(): Q.Promise<string> {
188 const nodeChildProcess = this.childProcess;
189 // Attempt to find the OS version of the iDevice, e.g. 7.1
190 const versionInfo = nodeChildProcess.exec("ideviceinfo -s -k ProductVersion").outcome.then((stdout: Buffer) => {
191 return stdout.toString().trim().substring(0, 3); // Versions for DeveloperDiskImage seem to be X.Y, while some device versions are X.Y.Z
192 // NOTE: This will almost certainly be wrong in the next few years, once we hit version 10.0
193 }, function(): string {
194 throw new Error("Unable to get device OS version");
195 });
196
197 // Attempt to find the path where developer resources exist.
198 const pathInfo = nodeChildProcess.exec("xcrun -sdk iphoneos --show-sdk-platform-path").outcome.then((stdout: Buffer) => {
199 return stdout.toString().trim();
200 });
201
202 // Attempt to find the developer disk image for the appropriate
203 return Q.all([versionInfo, pathInfo]).spread<string>(function(version: string, sdkpath: string): Q.Promise<string> {
204 const find = nodeChildProcess.spawn("find", [sdkpath, "-path", "*" + version + "*", "-name", "DeveloperDiskImage.dmg"]).spawnedProcess;
205 const deferred = Q.defer<string>();
206
207 find.stdout.on("data", function(data: any): void {
208 const dataStr: string = data.toString();
209 const path: string = dataStr.split("\n")[0].trim();
210 if (!path) {
211 deferred.reject(new Error("Unable to find developer disk image"));
212 } else {
213 deferred.resolve(path);
214 }
215 });
216 find.on("exit", function(code: number): void {
217 deferred.reject(new Error("Unable to find developer disk image"));
218 });
219
220 return deferred.promise;
221 });
222 }
223
224 private getPathOnDevice(packageId: string): Q.Promise<string> {
225 const nodeChildProcess = this.childProcess;
226 const nodeFileSystem = new Node.FileSystem();
227 return nodeChildProcess.execToString("ideviceinstaller -l -o xml > /tmp/$$.ideviceinstaller && echo /tmp/$$.ideviceinstaller")
228 .catch(function(err: any): any {
229 if (err.code === "ENOENT") {
230 throw new Error("Unable to find ideviceinstaller.");
231 }
232 throw err;
233 }).then((stdout: string): Q.Promise<string> => {
234 // First find the path of the app on the device
235 let filename: string = stdout.trim();
236 if (!/^\/tmp\/[0-9]+\.ideviceinstaller$/.test(filename)) {
237 throw new Error("Unable to list installed applications on device");
238 }
239
240 const plistBuddy = new PlistBuddy();
241 // Search thrown the unknown-length array until we find the package
242 const findPackageEntry = (index: number): Q.Promise<string> => {
243 return plistBuddy.readPlistProperty(filename, `:${index}:CFBundleIdentifier`)
244 .then((bundleId: string) => {
245 if (bundleId === packageId) {
246 return plistBuddy.readPlistProperty(filename, `:${index}:Path`);
247 }
248 return findPackageEntry(index + 1);
249 });
250 };
251
252 return findPackageEntry(0)
253 .finally(() => {
254 nodeFileSystem.unlink(filename);
255 }).catch((): string => {
256 throw new Error("Application not installed on the device");
257 });
258 });
259 }
260
261 private makeGdbCommand(command: string): string {
262 let commandString: string = `$${command}#`;
263 let stringSum: number = 0;
264 for (let i: number = 0; i < command.length; i++) {
265 stringSum += command.charCodeAt(i);
266 }
267
268 /* tslint:disable:no-bitwise */
269 // We need some bitwise operations to calculate the checksum
270 stringSum = stringSum & 0xFF;
271 /* tslint:enable:no-bitwise */
272 let checksum: string = stringSum.toString(16).toUpperCase();
273 if (checksum.length < 2) {
274 checksum = "0" + checksum;
275 }
276
277 commandString += checksum;
278 return commandString;
279 }
280}