Refactoring iosAppRunnerHelper to use external package

This commit is contained in:
Jimmy Thomson 2016-03-17 09:59:30 -07:00
Родитель 609f476131
Коммит 40a6a0bece
5 изменённых файлов: 21 добавлений и 462 удалений

Просмотреть файл

@ -17,11 +17,13 @@ import Q = require ("q");
import util = require ("util");
import archiver = require ("archiver");
import iosAppRunner = require ("./iosAppRunnerHelper");
import ideviceAppLauncher = require ("idevice-app-launcher");
import resources = require ("../resources/resourceManager");
import sharedState = require ("./sharedState");
import utils = require ("taco-utils");
import iosAppRunner = ideviceAppLauncher.raw;
import BuildInfo = utils.BuildInfo;
import Logger = utils.Logger;
import ProcessLogger = utils.ProcessLogger;

Просмотреть файл

@ -1,281 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
/// <reference path="../../typings/node.d.ts" />
/// <reference path="../../typings/Q.d.ts" />
/// <reference path="../../typings/tacoUtils.d.ts" />
/// <reference path="../../typings/plist-with-patches.d.ts" />
"use strict";
import child_process = require ("child_process");
import fs = require ("fs");
import net = require ("net");
import pl = require ("plist-with-patches");
import Q = require ("q");
import utils = require ("taco-utils");
import sharedState = require("./sharedState");
import UtilHelper = utils.UtilHelper;
var promiseExec: (...args: any[]) => Q.Promise<any> = Q.denodeify(UtilHelper.loggedExec);
class IosAppRunnerHelper {
public static startDebugProxy(proxyPort: number): Q.Promise<child_process.ChildProcess> {
if (sharedState.nativeDebuggerProxyInstance) {
sharedState.nativeDebuggerProxyInstance.kill("SIGHUP"); // idevicedebugserver does not exit from SIGTERM
sharedState.nativeDebuggerProxyInstance = null;
}
return IosAppRunnerHelper.mountDeveloperImage().then(function (): Q.Promise<child_process.ChildProcess> {
var deferred = Q.defer<child_process.ChildProcess>();
sharedState.nativeDebuggerProxyInstance = child_process.spawn("idevicedebugserverproxy", [proxyPort.toString()]);
sharedState.nativeDebuggerProxyInstance.on("error", function (err: any): void {
deferred.reject(err);
});
// Allow 200ms for the spawn to error out, ~125ms isn't uncommon for some failures
Q.delay(200).then(() => deferred.resolve(sharedState.nativeDebuggerProxyInstance));
return deferred.promise;
});
}
// Attempt to start the app on the device, using the debug server proxy on a given port.
// Returns a socket speaking remote gdb protocol with the debug server proxy.
public static startApp(packageId: string, proxyPort: number, appLaunchStepTimeout: number): Q.Promise<net.Socket> {
// When a user has many apps installed on their device, the response from ideviceinstaller may be large (500k or more)
// This exceeds the maximum stdout size that exec allows, so we redirect to a temp file.
return promiseExec("ideviceinstaller -l -o xml > /tmp/$$.ideviceinstaller && echo /tmp/$$.ideviceinstaller")
.catch(function (err: any): any {
if (err.code === "ENOENT") {
throw new Error("IDeviceInstallerNotFound");
}
throw err;
}).spread<string>(function (stdout: string, stderr: string): string {
// First find the path of the app on the device
var filename: string = stdout.trim();
if (!/^\/tmp\/[0-9]+\.ideviceinstaller$/.test(filename)) {
throw new Error("WrongInstalledAppsFile");
}
var list: any[] = pl.parseFileSync(filename);
fs.unlink(filename);
for (var i: number = 0; i < list.length; ++i) {
if (list[i].CFBundleIdentifier === packageId) {
var path: string = list[i].Path;
return path;
}
}
throw new Error("PackageNotInstalled");
}).then(function (path: string): Q.Promise<net.Socket> { return IosAppRunnerHelper.startAppViaDebugger(proxyPort, path, appLaunchStepTimeout); });
}
public static startAppViaDebugger(portNumber: number, packagePath: string, appLaunchStepTimeout: number): Q.Promise<net.Socket> {
var encodedPath: string = IosAppRunnerHelper.encodePath(packagePath);
// We need to send 3 messages to the proxy, waiting for responses between each message:
// A(length of encoded path),0,(encoded path)
// Hc0
// c
// We expect a '+' for each message sent, followed by a $OK#9a to indicate that everything has worked.
// For more info, see http://www.opensource.apple.com/source/lldb/lldb-167.2/docs/lldb-gdb-remote.txt
var socket: net.Socket = new net.Socket();
var initState: number = 0;
var endStatus: number = null;
var endSignal: number = null;
var deferred1: Q.Deferred<net.Socket> = Q.defer<net.Socket>();
var deferred2: Q.Deferred<net.Socket> = Q.defer<net.Socket>();
var deferred3: Q.Deferred<net.Socket> = Q.defer<net.Socket>();
socket.on("data", function (data: any): void {
data = data.toString();
while (data[0] === "+") { data = data.substring(1); }
// Acknowledge any packets sent our way
if (data[0] === "$") {
socket.write("+");
if (data[1] === "W") {
// The app process has exited, with hex status given by data[2-3]
var status: number = parseInt(data.substring(2, 4), 16);
endStatus = status;
socket.end();
} else if (data[1] === "X") {
// The app rocess exited because of signal given by data[2-3]
var signal: number = parseInt(data.substring(2, 4), 16);
endSignal = signal;
socket.end();
} else if (data.substring(1, 3) === "OK") {
// last command was received OK;
if (initState === 1) {
deferred1.resolve(socket);
} else if (initState === 2) {
deferred2.resolve(socket);
}
} else if (data[1] === "O") {
// STDOUT was written to, and the rest of the input until reaching a '#' is a hex-encoded string of that output
if (initState === 3) {
deferred3.resolve(socket);
initState++;
}
} else if (data[1] === "E") {
// An error has occurred, with error code given by data[2-3]: parseInt(data.substring(2, 4), 16)
deferred1.reject("UnableToLaunchApp");
deferred2.reject("UnableToLaunchApp");
deferred3.reject("UnableToLaunchApp");
}
}
});
socket.on("end", function (): void {
deferred1.reject("UnableToLaunchApp");
deferred2.reject("UnableToLaunchApp");
deferred3.reject("UnableToLaunchApp");
});
socket.on("error", function (err: Error): void {
deferred1.reject(err);
deferred2.reject(err);
deferred3.reject(err);
});
socket.connect(portNumber, "localhost", function (): void {
// set argument 0 to the (encoded) path of the app
var cmd: string = IosAppRunnerHelper.makeGdbCommand("A" + encodedPath.length + ",0," + encodedPath);
initState++;
socket.write(cmd);
setTimeout(function (): void {
deferred1.reject("DeviceLaunchTimeout");
}, appLaunchStepTimeout);
});
return deferred1.promise.then(function (sock: net.Socket): Q.Promise<net.Socket> {
// Set the step and continue thread to any thread
var cmd: string = IosAppRunnerHelper.makeGdbCommand("Hc0");
initState++;
sock.write(cmd);
setTimeout(function (): void {
deferred2.reject("DeviceLaunchTimeout");
}, appLaunchStepTimeout);
return deferred2.promise;
}).then(function (sock: net.Socket): Q.Promise<net.Socket> {
// Continue execution; actually start the app running.
var cmd: string = IosAppRunnerHelper.makeGdbCommand("c");
initState++;
sock.write(cmd);
setTimeout(function (): void {
deferred3.reject("DeviceLaunchTimeout");
}, appLaunchStepTimeout);
return deferred3.promise;
});
}
public static encodePath(packagePath: string): string {
// Encode the path by converting each character value to hex
var encodedPath: string = "";
for (var i: number = 0; i < packagePath.length; ++i) {
var c: string = packagePath[i];
encodedPath += IosAppRunnerHelper.charToHex(c);
}
return encodedPath;
}
private static mountDeveloperImage(): Q.Promise<any> {
return IosAppRunnerHelper.getDiskImage()
.then(function (path: string): Q.Promise<any> {
var imagemounter: child_process.ChildProcess = child_process.spawn("ideviceimagemounter", [path]);
var deferred: Q.Deferred<any> = Q.defer();
var stdout: string = "";
imagemounter.stdout.on("data", function (data: any): void {
stdout += data.toString();
});
imagemounter.on("close", function (code: number): void {
if (code !== 0) {
if (stdout.indexOf("Error:") !== -1) {
deferred.resolve({}); // Technically failed, but likely caused by the image already being mounted.
} else if (stdout.indexOf("No device found, is it plugged in?") !== -1) {
deferred.reject("NoDeviceAttached");
}
deferred.reject("ErrorMountingDiskImage");
} else {
deferred.resolve({});
}
});
imagemounter.on("error", function(err: any): void {
deferred.reject(err);
});
return deferred.promise;
});
}
private static getDiskImage(): Q.Promise<string> {
// Attempt to find the OS version of the iDevice, e.g. 7.1
var versionInfo: Q.Promise<any> = promiseExec("ideviceinfo -s -k ProductVersion").spread<string>(function (stdout: string, stderr: string): string {
return stdout.trim().substring(0, 3); // Versions for DeveloperDiskImage seem to be X.Y, while some device versions are X.Y.Z
// NOTE: This will almost certainly be wrong in the next few years, once we hit version 10.0
}, function (): string {
throw new Error("FailedGetDeviceInfo");
});
// Attempt to find the path where developer resources exist.
var pathInfo: Q.Promise<any> = promiseExec("xcrun -sdk iphoneos --show-sdk-platform-path").spread<string>(function (stdout: string, stderr: string): string {
var sdkpath: string = stdout.trim();
return sdkpath;
});
// Attempt to find the developer disk image for the appropriate
return Q.all([versionInfo, pathInfo]).spread<string>(function (version: string, sdkpath: string): Q.Promise<string> {
var find: child_process.ChildProcess = child_process.spawn("find", [sdkpath, "-path", "*" + version + "*", "-name", "DeveloperDiskImage.dmg"]);
var deferred: Q.Deferred<string> = Q.defer<string>();
find.stdout.on("data", function (data: any): void {
var dataStr: string = data.toString();
var path: string = dataStr.split("\n")[0].trim();
if (!path) {
deferred.reject("FailedFindDeveloperDiskImage");
} else {
deferred.resolve(path);
}
});
find.on("close", function (code: number): void {
deferred.reject("FailedFindDeveloperDiskImage");
});
return deferred.promise;
});
}
private static charToHex(char: string): string {
var conversionTable: string = "0123456789ABCDEF";
var charCode: number = char.charCodeAt(0);
/* tslint:disable:no-bitwise */
// We do need some bitwise operations to convert the char to Hex
return conversionTable[(charCode & 0xF0) >> 4] + conversionTable[charCode & 0x0F];
/* tslint:enable:no-bitwise */
}
private static makeGdbCommand(command: string): string {
var commandString: string = "$" + command + "#";
var stringSum: number = 0;
for (var i: number = 0; i < command.length; i++) {
stringSum += command.charCodeAt(i);
}
/* tslint:disable:no-bitwise */
// We need some bitwise operations to calculate the checksum
stringSum = stringSum & 0xFF;
/* tslint:enable:no-bitwise */
var checksum: string = stringSum.toString(16).toUpperCase();
if (checksum.length < 2) {
checksum = "0" + checksum;
}
commandString += checksum;
return commandString;
}
}
export = IosAppRunnerHelper;

Просмотреть файл

@ -28,7 +28,8 @@
"plist-with-patches": "0.5.1",
"taco-utils": "file:../taco-utils",
"semver": "^4.3.6",
"archiver": "^0.16.0"
"archiver": "^0.16.0",
"idevice-app-launcher": "^0.1.0"
},
"devDependencies": {
"mocha": "2.0.1",

Просмотреть файл

@ -1,179 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
/// <reference path="../../typings/mocha.d.ts" />
/// <reference path="../../typings/node.d.ts" />
/// <reference path="../../typings/should.d.ts" />
/// <reference path="../../typings/Q.d.ts" />
"use strict";
/* tslint:disable:no-var-requires */
// var require needed for should module to work correctly
// Note not import: We don't want to refer to shouldModule, but we need the require to occur since it modifies the prototype of Object.
var shouldModule: any = require("should");
/* tslint:enable:no-var-requires */
import net = require ("net");
import Q = require ("q");
import runner = require ("../ios/iosAppRunnerHelper");
import utils = require ("taco-utils");
import Logger = utils.Logger;
interface IMockDebuggerProxy extends net.Server {
protocolState?: number;
};
// Tests for lib/darwin/darwinAppRunner.js functionality
describe("Device functionality", function (): void {
// Check that when the debugger behaves nicely, we do as well
var port: number = 12345;
it("should complete the startup sequence when the debugger is well behaved", function (done: MochaDone): void {
var appPath: string = "/private/var/mobile/Applications/042F57CA-9717-4655-8349-532093FFCF44/BlankCordovaApp1.app";
var encodedAppPath: string = "2F707269766174652F7661722F6D6F62696C652F4170706C69636174696F6E732F30343246353743412D393731372D343635352D383334392D3533323039334646434634342F426C616E6B436F72646F7661417070312E617070";
encodedAppPath.should.equal(runner.encodePath(appPath));
var mockDebuggerProxy: IMockDebuggerProxy = net.createServer(function (client: net.Socket): void {
mockDebuggerProxy.close();
client.on("data", function (data: Buffer): void {
var dataString: string = data.toString();
if (mockDebuggerProxy.protocolState % 2 === 1) {
// Every second message should be an acknowledgement of a send of ours
dataString[0].should.equal("+");
mockDebuggerProxy.protocolState++;
dataString = dataString.substring(1);
if (dataString === "") {
return;
}
}
dataString[0].should.equal("$");
var expectedResponse: string = "";
switch (mockDebuggerProxy.protocolState) {
case 0:
expectedResponse = "A" + encodedAppPath.length + ",0," + encodedAppPath;
var checksum: number = 0;
for (var i: number = 0; i < expectedResponse.length; ++i) {
checksum += expectedResponse.charCodeAt(i);
};
/* tslint:disable:no-bitwise */
// Some bitwise operations needed to calculate the checksum here
checksum = checksum & 0xFF;
/* tslint:enable:no-bitwise */
var checkstring: string = checksum.toString(16).toUpperCase();
if (checkstring.length === 1) {
checkstring = "0" + checkstring;
}
expectedResponse = "$" + expectedResponse + "#" + checkstring;
dataString.should.equal(expectedResponse);
mockDebuggerProxy.protocolState++;
client.write("+");
client.write("$OK#9A");
break;
case 2:
expectedResponse = "$Hc0#DB";
dataString.should.equal(expectedResponse);
mockDebuggerProxy.protocolState++;
client.write("+");
client.write("$OK#9A");
break;
case 4:
expectedResponse = "$c#63";
dataString.should.equal(expectedResponse);
mockDebuggerProxy.protocolState++;
client.write("+");
// Respond with empty output
client.write("$O#4F");
client.end();
}
});
});
mockDebuggerProxy.protocolState = 0;
mockDebuggerProxy.on("error", done);
mockDebuggerProxy.listen(port, function (): void {
Logger.log("MockDebuggerProxy listening");
});
Q.timeout(runner.startAppViaDebugger(port, appPath, 5000), 1000).done(() => done(), done);
});
// Check that when the debugger reports an error, we notice it
it("should report an error if the debugger fails for some reason", function (done: MochaDone): void {
var appPath: string = "/private/var/mobile/Applications/042F57CA-9717-4655-8349-532093FFCF44/BlankCordovaApp1.app";
var encodedAppPath: string = "2F707269766174652F7661722F6D6F62696C652F4170706C69636174696F6E732F30343246353743412D393731372D343635352D383334392D3533323039334646434634342F426C616E6B436F72646F7661417070312E617070";
encodedAppPath.should.equal(runner.encodePath(appPath));
var mockDebuggerProxy: IMockDebuggerProxy = net.createServer(function (client: net.Socket): void {
mockDebuggerProxy.close();
client.on("data", function (data: Buffer): void {
var dataString: string = data.toString();
if (mockDebuggerProxy.protocolState % 2 === 1) {
// Every second message should be an acknowledgement of a send of ours
dataString[0].should.equal("+");
mockDebuggerProxy.protocolState++;
dataString = dataString.substring(1);
if (dataString === "") {
return;
}
}
dataString[0].should.equal("$");
var expectedResponse: string = "";
switch (mockDebuggerProxy.protocolState) {
case 0:
expectedResponse = "A" + encodedAppPath.length + ",0," + encodedAppPath;
var checksum: number = 0;
for (var i: number = 0; i < expectedResponse.length; ++i) {
checksum += expectedResponse.charCodeAt(i);
};
/* tslint:disable:no-bitwise */
// Some bit operations needed to calculate checksum
checksum = checksum & 0xFF;
/* tslint:enable:no-bitwise */
var checkstring: string = checksum.toString(16).toUpperCase();
if (checkstring.length === 1) {
checkstring = "0" + checkstring;
}
expectedResponse = "$" + expectedResponse + "#" + checkstring;
dataString.should.equal(expectedResponse);
mockDebuggerProxy.protocolState++;
client.write("+");
client.write("$OK#9A");
break;
case 2:
expectedResponse = "$Hc0#DB";
dataString.should.equal(expectedResponse);
mockDebuggerProxy.protocolState++;
client.write("+");
client.write("$OK#9A");
break;
case 4:
expectedResponse = "$c#63";
dataString.should.equal(expectedResponse);
mockDebuggerProxy.protocolState++;
client.write("+");
client.write("$E23#AA"); // Report an error
client.end();
}
});
});
mockDebuggerProxy.protocolState = 0;
mockDebuggerProxy.on("error", done);
mockDebuggerProxy.listen(port, function (): void {
Logger.log("MockDebuggerProxy listening");
});
Q.timeout(runner.startAppViaDebugger(port, appPath, 5000), 1000).then(function (): void {
throw new Error("Starting the app should have failed!");
}, function (err: any): void {
err.should.equal("UnableToLaunchApp");
}).done(() => done(), done);
});
});

16
src/typings/idevice-app-launcher.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
declare module "idevice-app-launcher" {
import * as child_process from "child_process";
import * as net from "net";
import * as Q from "q";
class IosAppRunnerHelper {
static startDebugProxy(proxyPort: number): Q.Promise<child_process.ChildProcess>;
static startApp(packageId: string, proxyPort: number, appLaunchStepTimeout: number): Q.Promise<net.Socket>;
static startAppViaDebugger(portNumber: number, packagePath: string, appLaunchStepTimeout: number): Q.Promise<net.Socket>;
static encodePath(packagePath: string): string;
}
export var raw: typeof IosAppRunnerHelper;
}