Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimal example to get debugging to work with iOS 17 #25

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
58 changes: 40 additions & 18 deletions src/debugConfigProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { randomString } from './lib/utils';
import * as targetCommand from './targetCommand';
import { getTargetFromUDID, pickTarget, getOrPickTarget } from './targetPicker';
import * as simulatorFocus from './simulatorFocus';
import { _execFile } from './lib/utils';

let context: vscode.ExtensionContext;

Expand All @@ -32,7 +33,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
else if (typeof iosTarget === "string") {
return await getTargetFromUDID(iosTarget);
}

return undefined;
}

Expand Down Expand Up @@ -89,14 +90,14 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
let target: Target = dbgConfig.iosTarget;

if (target.type === "Simulator")
{
{
let pid: string|void;

// Check if we have enough permissions for the simulator focus monitor.
let enableSimulatorFocusMonitor = vscode.workspace.getConfiguration().get('ios-debug.focusSimulator') && await simulatorFocus.tryEnsurePermissions();

if (dbgConfig.iosRequest === "launch")
{
if (dbgConfig.iosRequest === "launch")
{
let outputBasename = getOutputBasename();
let stdout = `${outputBasename}-stdout`;
let stderr = `${outputBasename}-stderr`;
Expand Down Expand Up @@ -125,7 +126,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
dbgConfig.initCommands.push(`follow ${stdout}`);
dbgConfig.initCommands.push(`follow ${stderr}`);
}
else
else
{
pid = await targetCommand.simulatorGetPidFor({
target: target as Simulator,
Expand All @@ -145,11 +146,11 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
delete dbgConfig.env;
delete dbgConfig.args;
}
else if (target.type === "Device")
{
let platformPath: string|void;
if (dbgConfig.iosRequest === "launch")
else if (target.type === "Device")
{
let platformPath: string|void;
if (dbgConfig.iosRequest === "launch")
{
if (dbgConfig.iosInstallApp) {
platformPath = await targetCommand.deviceInstall({
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd also need to replace deviceInstall, right? Just noting, we can do this incrementally in a different PR as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Installing the app still works with ios-deploy. Only getting the pid of a running process and spawning the debug server seems to require DeveloperDiskImage.dmg and hence is retired since iOS 17.
Of course long-term it would be sort of cleaner to use xcrun devicectl instead as well, but this will only work with XCode >= 15 installed, so at the time being ios-deploy actually provides the highest compatibility.

target: target as Device,
Expand All @@ -162,7 +163,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
});
}
}
else
else
{
platformPath = await targetCommand.deviceAppPath({
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this still work? Doesn't this also use ios-deploy?

Copy link
Author

@yorickreum yorickreum Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, getting the platform path still works with ios-deploy.

One can also do it with devicectl of course, for running processes as the runningProcesses.executable of xcrun devicectl device info processes --json-output - and for all apps installed via apps.url of xcrun devicectl device info apps --json-output -.

target: target as Device,
Expand All @@ -181,16 +182,37 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv

if (!platformPath) { return null; }

let debugserverPort = await targetCommand.deviceDebugserver({
target: target as Device,
});
if (!debugserverPort) { return null; }

dbgConfig.iosDebugserverPort = debugserverPort;

dbgConfig.preRunCommands = (dbgConfig.preRunCommands instanceof Array) ? dbgConfig.preRunCommands : [];
dbgConfig.preRunCommands.push(`script lldb.target.module[0].SetPlatformFileSpec(lldb.SBFileSpec('${platformPath}'))`);
dbgConfig.preRunCommands.push(`process connect connect://127.0.0.1:${debugserverPort}`);

// iOS 17+ uses Apple LLDB directly to create and attach to the debugserver,
// below iOS 17 spawn debugserver with ios-deploy
let iosMajorVersion = dbgConfig.iosTarget.version.split('.')[0] as number;
if (iosMajorVersion < 17) {
let debugserverPort = await targetCommand.deviceDebugserver({
target: target as Device,
});
if (!debugserverPort) { return null; }

dbgConfig.iosDebugserverPort = debugserverPort;
dbgConfig.preRunCommands.push(`process connect connect://127.0.0.1:${debugserverPort}`);
} else {
// launch the app via devicectl
await _execFile(
'xcrun',
['devicectl', 'device', 'process', 'launch', '--device', target.udid, '--start-stopped', this.ensureBundleId(dbgConfig)],
);

// attach to the app via LLDB
dbgConfig.processCreateCommands = (dbgConfig.postRunCommands instanceof Array) ? dbgConfig.processCreateCommands : [];
// LLDB `device` command needs Apple's beta LLDB set as backend for CodeLLDB
// e.g. set vscode://settings/lldb.library to /Applications/Xcode-beta.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting requirement and will hamper normal usage. Maybe we should find a way to specify this always dynamically for a debug session. Again, not directly for this PR, but as a future improvement. Until now, I believe this was optional and the in-built lldb version in CodeLLDB also used to work well.

Copy link
Author

@yorickreum yorickreum Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it is an additional hurdle, but I can not think of an easy or clean way to programmatically set this, since we'd be messing with the settings of another extension, and the path to Apple's LLDB library might also vary?
Maybe the cleanest option is to just send a clear alert to the user explaining what to do (whenever they are using iOS 17 or when lldb complains about device not being a command).

dbgConfig.processCreateCommands.push(`script lldb.debugger.HandleCommand("device select ${target.udid}")`);
// using detour with `script` command, as direct evocation causes CodeLLDB to crash at the moment
// probably due to a bug in CodeLLDB / unusal exit results from the `device` command
let processName = path.basename(platformPath).split('.')[0];
dbgConfig.processCreateCommands.push(`script lldb.debugger.HandleCommand("device process attach --name '${processName}'")`);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How reliable attaching by name? Can the process have different name than binary? Should we try attaching to pid instead of name, if we can get pid from the process launch command above?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, pid might be cleaner. It gets returned as process.processIdentifier from xcrun devicectl device process launch --json-output -.

Cleanest option imo would be to use lldb also to start the process, but I couldn't figure out a way to do this with Apples lldb device.

}
}

logger.log("resolved debug configuration", dbgConfig);
Expand Down
48 changes: 46 additions & 2 deletions src/lib/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,44 @@ export async function getAppDevicePath(target: Device, appBundleId: string) {
return appDevicePath;
}

export async function getPidFor(target: Device, appBundleId: string): Promise<number>
export async function getPidForUsingDevicectl(target: Device, appBundleId: string): Promise<number>
{
logger.log(`Getting pid for app (bundle id: ${appBundleId}) on device (udid: ${target.udid})`);
logger.log(`Getting pid for app (bundle id: ${appBundleId}) on device (udid: ${target.udid}), using xcrun devicectl`);
let time = new Date().getTime();

let platformPath = await getAppDevicePath(target, appBundleId);

let p = _execFile(
'xcrun',
['devicectl', 'device', 'info', 'processes',
'--device', target.udid,
'--json-output', '-']);

let pid: number = await p.then(({stdout, stderr}): number => {
if (stderr) { logger.error(stderr); }

let processes = JSON.parse(stdout).result.runningProcesses;
// ToDo
// Use --filter option of devicectl to get the process directly,
// once it is actually working properly.
for (let process of processes) {
logger.log(`Checking process ${process.processIdentifier} with executable ${process.executable}`);
if (process.executable && platformPath && process.executable.includes(encodeURI(platformPath))) {
return process.processIdentifier;
}
}

throw new Error(`Could not find pid for ${appBundleId}. Is the app running?`);
});

logger.log(`Got pid "${pid}" in ${new Date().getTime() - time} ms`);

return pid;
}

export async function getPidForUsingIOSDeploy(target: Device, appBundleId: string): Promise<number>
{
logger.log(`Getting pid for app (bundle id: ${appBundleId}) on device (udid: ${target.udid}), using ios-deploy`);
let time = new Date().getTime();

let p = _execFile(
Expand Down Expand Up @@ -336,3 +371,12 @@ export async function getPidFor(target: Device, appBundleId: string): Promise<nu

return pid;
}

export async function getPidFor(target: Device, appBundleId: string): Promise<number>
{
if(target.version.split('.')[0] < '17') {
return await getPidForUsingIOSDeploy(target, appBundleId);
} else {
return await getPidForUsingDevicectl(target, appBundleId);
}
}