зеркало из https://github.com/microsoft/TACO.git
Refactoring iosAppRunnerHelper to use external package
This commit is contained in:
Родитель
609f476131
Коммит
40a6a0bece
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
Загрузка…
Ссылка в новой задаче