Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions lib/common/mobile/ios/device/ios-application-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { hook } from "../../../helpers";
import { ApplicationManagerBase } from "../../application-manager-base";
import { cache } from "../../../decorators";
import { IOptions } from "../../../../declarations";
import { ChildProcess } from "child_process";
import {
IChildProcess,
IHooksService,
IErrors,
IiOSNotificationService,
Expand All @@ -13,6 +15,7 @@ import {

export class IOSApplicationManager extends ApplicationManagerBase {
private applicationsLiveSyncInfos: Mobile.IApplicationInfo[];
private lldbProcesses: IDictionary<ChildProcess> = {};

constructor(
protected $logger: ILogger,
Expand All @@ -22,7 +25,8 @@ export class IOSApplicationManager extends ApplicationManagerBase {
private $iOSNotificationService: IiOSNotificationService,
private $iosDeviceOperations: IIOSDeviceOperations,
private $options: IOptions,
protected $deviceLogProvider: Mobile.IDeviceLogProvider
protected $deviceLogProvider: Mobile.IDeviceLogProvider,
private $childProcess: IChildProcess
) {
super($logger, $hooksService, $deviceLogProvider);
}
Expand Down Expand Up @@ -118,6 +122,7 @@ export class IOSApplicationManager extends ApplicationManagerBase {
const { appId } = appData;

await this.device.destroyDebugSocket(appId);
this.detachNativeDebugger(appId);

const action = () =>
this.$iosDeviceOperations.stop([
Expand Down Expand Up @@ -173,15 +178,61 @@ export class IOSApplicationManager extends ApplicationManagerBase {
private async runApplicationCore(
appData: Mobile.IStartApplicationData
): Promise<void> {
const deviceId = this.device.deviceInfo.identifier;
const waitForDebugger = (!!appData.waitForDebugger).toString();
await this.$iosDeviceOperations.start([
const result = await this.$iosDeviceOperations.start([
{
deviceId: this.device.deviceInfo.identifier,
deviceId,
appId: appData.appId,
ddi: this.$options.ddi,
waitForDebugger,
},
]);

const pid: number = (<any>result?.[deviceId]?.[0])?.pid;
if (appData.waitForDebugger && pid > 0) {
this.attachNativeDebugger(appData.appId, pid);
}
}

// iOS kills an app that takes about 20 seconds to launch, which is what waiting
// for or pausing in the JS debugger looks like, unless a native debugger is
// attached. lldb only knows about devices since Xcode 16; without it the wait
// is just limited to those 20 seconds.
private attachNativeDebugger(appId: string, pid: number): void {
this.detachNativeDebugger(appId);

const lldb = this.$childProcess.spawn("xcrun", ["lldb"]);
this.lldbProcesses[appId] = lldb;
lldb.on("error", (err: Error) => {
this.$logger.trace("Unable to attach lldb to the application", err);
});
// Attaching stops the app. It can only be continued once that has happened,
// and until then it can't answer the JS debugger's attach request.
let output = "";
lldb.stdout?.on("data", (data: Buffer) => {
this.$logger.trace(`${data}`);
if (output !== null) {
output += data;
if (/Process \d+ stopped/.test(output)) {
output = null;
lldb.stdin?.write("process continue\n");
}
}
});
lldb.stderr?.on("data", (data: Buffer) => this.$logger.trace(`${data}`));
lldb.stdin?.write(
`device select ${this.device.deviceInfo.identifier}\ndevice process attach -p ${pid}\n`
);
}

private detachNativeDebugger(appId: string): void {
const lldb = this.lldbProcesses[appId];
if (lldb) {
delete this.lldbProcesses[appId];
lldb.stdin?.write("process detach\nquit\n");
lldb.stdin?.end();
}
}

@cache()
Expand Down
94 changes: 94 additions & 0 deletions lib/common/test/unit-tests/mobile/ios-application-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { assert } from "chai";
import { EventEmitter } from "events";
import { IOSApplicationManager } from "../../../mobile/ios/device/ios-application-manager";

describe("IOSApplicationManager native debugger", () => {
const deviceId = "device";
const appData = { appId: "org.app", projectName: "app", projectDir: "/app" };

const setup = (pid?: number) => {
const lldbInput: string[] = [];
const lldbOutput = new EventEmitter();
const spawned: string[][] = [];
const manager = new (<any>IOSApplicationManager)(
{ trace: () => {}, info: () => {} },
{},
{
deviceInfo: { identifier: deviceId },
destroyDebugSocket: async () => {},
},
{},
{},
{
start: async () => ({ [deviceId]: [{ deviceId, pid }] }),
stop: async () => ({}),
apps: async () => ({}),
},
{ justlaunch: true },
new Proxy({}, { get: () => () => {} }),
{
spawn: (command: string, args: string[]) => {
spawned.push([command, ...args]);
return {
on: () => {},
stdout: lldbOutput,
stdin: {
write: (text: string) => lldbInput.push(text),
end: () => {},
},
};
},
},
);
return { manager, lldbInput, lldbOutput, spawned };
};

it("attaches lldb to an app that waits for the JS debugger", async () => {
const { manager, lldbInput, spawned } = setup(42);

await manager.restartApplication({ ...appData, waitForDebugger: true });

assert.deepEqual(spawned, [["xcrun", "lldb"]]);
assert.include(lldbInput[0], `device select ${deviceId}`);
assert.include(lldbInput[0], "device process attach -p 42");
});

it("continues the app once, after the attach stopped it", async () => {
const { manager, lldbInput, lldbOutput } = setup(42);

await manager.restartApplication({ ...appData, waitForDebugger: true });
assert.lengthOf(lldbInput, 1);

lldbOutput.emit("data", Buffer.from("Process 42 sto"));
assert.lengthOf(lldbInput, 1);
lldbOutput.emit("data", Buffer.from("pped\n"));
lldbOutput.emit("data", Buffer.from("Process 42 stopped\n"));

assert.deepEqual(lldbInput.slice(1), ["process continue\n"]);
});

it("doesn't attach to an app that doesn't wait", async () => {
const { manager, spawned } = setup(42);

await manager.restartApplication(appData);

assert.deepEqual(spawned, []);
});

it("doesn't attach without a process id", async () => {
const { manager, spawned } = setup(-1);

await manager.restartApplication({ ...appData, waitForDebugger: true });

assert.deepEqual(spawned, []);
});

it("detaches when the app stops", async () => {
const { manager, lldbInput } = setup(42);

await manager.restartApplication({ ...appData, waitForDebugger: true });
await manager.stopApplication(appData);

assert.include(lldbInput[lldbInput.length - 1], "process detach");
});
});
Loading