microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
3b6023b2c4497b5b8cf54373639977efc9e6e612

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/ios/deviceRunner.ts

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