iOS: Support C++ debugging on devices with iOS 17+

Xcode 16 added special commands for that to lldb:

- `device select <uuid>` switches the debugging to that device
- `device process attach <options>` attaches the debugger to a running
  app

So we start the application in "waiting state" with the --start-stopped
command line parameter, then start the debugger and use these commands
to attach.

Integration into the lldb bridge needs to directly run the new commands
and attach, to get access to the SBTarget.

Task-number: QTCREATORBUG-29895
Fixes: QTCREATORBUG-32106
Change-Id: I91abb35c689cbd4d2d9da53afb5a12ddc23e1e9a
Reviewed-by: hjk <hjk@qt.io>
Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
Eike Ziller
2025-01-08 13:03:20 +01:00
parent dd5d98dedf
commit a6aa050890
9 changed files with 161 additions and 58 deletions

View File

@@ -98,6 +98,7 @@ class Dumper(DumperBase):
self.startMode_ = None
self.processArgs_ = None
self.attachPid_ = None
self.deviceUuid_ = None
self.dyldImageSuffix = None
self.dyldLibraryPath = None
self.dyldFrameworkPath = None
@@ -918,6 +919,7 @@ class Dumper(DumperBase):
self.environment_ = args.get('environment', [])
self.environment_ = list(map(lambda x: self.hexdecode(x), self.environment_))
self.attachPid_ = args.get('attachpid', 0)
self.deviceUuid_ = args.get('deviceUuid', '')
self.sysRoot_ = args.get('sysroot', '')
self.remoteChannel_ = args.get('remotechannel', '')
self.platform_ = args.get('platform', '')
@@ -942,14 +944,26 @@ class Dumper(DumperBase):
if self.startMode_ == DebuggerStartMode.AttachExternal:
self.symbolFile_ = ''
self.target = self.debugger.CreateTarget(
self.symbolFile_, None, self.platform_, True, error)
if self.startMode_ == DebuggerStartMode.AttachToIosDevice:
# The script code depends on a target from now on,
# so we already need to attach with the special Apple lldb debugger commands
self.runDebuggerCommand('device select ' + self.deviceUuid_)
self.runDebuggerCommand('device process attach -p ' + str(self.attachPid_))
self.target = self.debugger.GetSelectedTarget()
else:
self.target = self.debugger.CreateTarget(
self.symbolFile_, None, self.platform_, True, error)
if not error.Success():
self.report(self.describeError(error))
self.reportState('enginerunfailed')
return
if not self.target:
self.report('Debugger failed to create target.')
self.reportState('enginerunfailed')
return
broadcaster = self.target.GetBroadcaster()
listener = self.debugger.GetListener()
broadcaster.AddListener(listener, lldb.SBProcess.eBroadcastBitStateChanged)
@@ -1090,6 +1104,11 @@ class Dumper(DumperBase):
self.reportState('enginerunokandinferiorunrunnable')
else:
self.reportState('enginerunfailed')
elif self.startMode_ == DebuggerStartMode.AttachToIosDevice:
# Already attached in setupInferior (to get a SBTarget),
# just get the process from it
self.process = self.target.GetProcess()
self.reportState('enginerunandinferiorrunok')
else:
launchInfo = lldb.SBLaunchInfo(self.processArgs_)
launchInfo.SetWorkingDirectory(self.workingDirectory_)
@@ -1920,16 +1939,20 @@ class Dumper(DumperBase):
self.debugger.GetCommandInterpreter().HandleCommand(command, result)
self.reportResult('fulltrace="%s"' % self.hexencode(result.GetOutput()), args)
def executeDebuggerCommand(self, args):
self.reportToken(args)
def runDebuggerCommand(self, command):
self.report('Running debugger command "{}"'.format(command))
result = lldb.SBCommandReturnObject()
command = args['command']
self.debugger.GetCommandInterpreter().HandleCommand(command, result)
success = result.Succeeded()
output = toCString(result.GetOutput())
error = toCString(str(result.GetError()))
self.report('success="%d",output="%s",error="%s"' % (success, output, error))
def executeDebuggerCommand(self, args):
self.reportToken(args)
command = args['command']
self.runDebuggerCommand(command)
def executeRoundtrip(self, args):
self.reportResult('', args)

View File

@@ -15,8 +15,10 @@ class DebuggerStartMode():
AttachCore,
AttachToRemoteServer,
AttachToRemoteProcess,
AttachToQmlServer,
StartRemoteProcess,
) = range(0, 9)
AttachToIosDevice
) = range(0, 11)
# Known special formats. Keep in sync with DisplayFormat in debuggerprotocol.h

View File

@@ -28,9 +28,8 @@ const char ANALYZERTASK_ID[] = "Analyzer.TaskId";
} // namespace Constants
// Keep in sync with dumper.py
enum DebuggerStartMode
{
// Keep in sync with debugger/utils.py
enum DebuggerStartMode {
NoStartMode,
StartInternal, // Start current start project's binary
StartExternal, // Start binary found in file system
@@ -40,7 +39,8 @@ enum DebuggerStartMode
AttachToRemoteServer, // Attach to a running gdbserver
AttachToRemoteProcess, // Attach to a running remote process
AttachToQmlServer, // Attach to a running QmlServer
StartRemoteProcess // Start and attach to a remote process
StartRemoteProcess, // Start and attach to a remote process
AttachToIosDevice // Attach to an application on a iOS 17+ device
};
enum DebuggerCloseMode

View File

@@ -116,6 +116,8 @@ public:
QString deviceSymbolsRoot;
bool continueAfterAttach = false;
Utils::FilePath sysRoot;
// iOS 17+
QString deviceUuid;
// Used by general core file debugging. Public access requested in QTCREATORBUG-17158.
Utils::FilePath coreFile;

View File

@@ -255,6 +255,11 @@ void DebuggerRunTool::setDeviceSymbolsRoot(const QString &deviceSymbolsRoot)
m_runParameters.deviceSymbolsRoot = deviceSymbolsRoot;
}
void DebuggerRunTool::setDeviceUuid(const QString &uuid)
{
m_runParameters.deviceUuid = uuid;
}
void DebuggerRunTool::setTestCase(int testCase)
{
m_runParameters.testCase = testCase;

View File

@@ -101,6 +101,8 @@ public:
void setIosPlatform(const QString &platform);
void setDeviceSymbolsRoot(const QString &deviceSymbolsRoot);
void setDeviceUuid(const QString &uuid);
void setAbi(const ProjectExplorer::Abi &abi);
DebuggerEngineType cppEngineType() const;

View File

@@ -270,6 +270,7 @@ void LldbEngine::handleLldbStarted()
cmd2.arg("startmode", rp.startMode);
cmd2.arg("nativemixed", isNativeMixedActive());
cmd2.arg("workingdirectory", inferior.workingDirectory.path());
cmd2.arg("deviceUuid", rp.deviceUuid);
Environment environment = inferior.environment;
// Prevent lldb from automatically setting OS_ACTIVITY_DT_MODE to mirror
// NSLog to stderr, as that will also mirror os_log, which we pick up in
@@ -303,18 +304,20 @@ void LldbEngine::handleLldbStarted()
if (rp.startMode != StartInternal) {
// it is better not to check the start mode on the python sid (as we would have to duplicate the
// enum values), and thus we assume that if the rp.attachPID is valid we really have to attach
QTC_CHECK(rp.attachPID.isValid() && (rp.startMode == AttachToRemoteProcess
|| rp.startMode == AttachToLocalProcess
|| rp.startMode == AttachToRemoteServer));
QTC_CHECK(
rp.attachPID.isValid()
&& (rp.startMode == AttachToRemoteProcess || rp.startMode == AttachToLocalProcess
|| rp.startMode == AttachToRemoteServer || rp.startMode == AttachToIosDevice));
cmd2.arg("attachpid", rp.attachPID.pid());
cmd2.arg("sysroot", rp.deviceSymbolsRoot.isEmpty() ? rp.sysRoot.toString()
: rp.deviceSymbolsRoot);
cmd2.arg("remotechannel", ((rp.startMode == AttachToRemoteProcess
|| rp.startMode == AttachToRemoteServer)
? rp.remoteChannel : QString()));
QTC_CHECK(!rp.continueAfterAttach || (rp.startMode == AttachToRemoteProcess
|| rp.startMode == AttachToLocalProcess
|| rp.startMode == AttachToRemoteServer));
QTC_CHECK(
!rp.continueAfterAttach
|| (rp.startMode == AttachToRemoteProcess || rp.startMode == AttachToLocalProcess
|| rp.startMode == AttachToRemoteServer || rp.startMode == AttachToIosDevice));
m_continueAtNextSpontaneousStop = false;
}
}

View File

@@ -103,7 +103,8 @@ bool IosRunConfiguration::isEnabled(Id runMode) const
IosDevice::ConstPtr iosdevice = std::dynamic_pointer_cast<const IosDevice>(dev);
if (iosdevice && iosdevice->handler() == IosDevice::Handler::DeviceCtl
&& runMode != ProjectExplorer::Constants::NORMAL_RUN_MODE) {
&& runMode != ProjectExplorer::Constants::NORMAL_RUN_MODE
&& !IosDeviceManager::isDeviceCtlDebugSupported()) {
return false;
}
@@ -280,9 +281,9 @@ QString IosRunConfiguration::disabledReason(Id runMode) const
}
IosDevice::ConstPtr iosdevice = std::dynamic_pointer_cast<const IosDevice>(dev);
if (iosdevice && iosdevice->handler() == IosDevice::Handler::DeviceCtl
&& runMode != ProjectExplorer::Constants::NORMAL_RUN_MODE) {
return Tr::tr("Debugging and profiling is currently not supported for devices with iOS "
"17 and later.");
&& runMode != ProjectExplorer::Constants::NORMAL_RUN_MODE
&& !IosDeviceManager::isDeviceCtlDebugSupported()) {
return Tr::tr("Debugging on devices with iOS 17 and later requires Xcode 16 or later.");
}
}
return RunConfiguration::disabledReason(runMode);

View File

@@ -92,6 +92,12 @@ static void stopRunningRunControl(RunControl *runControl)
activeRunControls[devId] = runControl;
}
static QString getBundleIdentifier(const FilePath &bundlePath)
{
QSettings settings(bundlePath.pathAppended("Info.plist").toString(), QSettings::NativeFormat);
return settings.value(QString::fromLatin1("CFBundleIdentifier")).toString();
}
struct AppInfo
{
QUrl pathOnDevice;
@@ -104,16 +110,18 @@ public:
DeviceCtlRunnerBase(RunControl *runControl);
void start();
qint64 processIdentifier() const { return m_processIdentifier; }
protected:
GroupItem findApp(const QString &bundleIdentifier, Storage<AppInfo> appInfo);
GroupItem findProcess(Storage<AppInfo> &appInfo);
void reportStoppedImpl();
IosDevice::ConstPtr m_device;
QStringList m_arguments;
qint64 m_processIdentifier = -1;
private:
GroupItem findApp(const QString &bundleIdentifier, Storage<AppInfo> appInfo);
GroupItem findProcess(Storage<AppInfo> &appInfo);
GroupItem killProcess(Storage<AppInfo> &appInfo);
virtual GroupItem launchTask(const QString &bundleIdentifier) = 0;
@@ -135,7 +143,6 @@ private:
std::unique_ptr<TaskTree> m_stopTask;
std::unique_ptr<TaskTree> m_pollTask;
QTimer m_pollTimer;
qint64 m_processIdentifier = -1;
};
class DeviceCtlRunner final : public DeviceCtlRunnerBase
@@ -145,11 +152,15 @@ public:
void stop();
void setStartStopped(bool startStopped) { m_startStopped = startStopped; }
private:
GroupItem launchTask(const QString &bundleIdentifier);
Process m_process;
std::unique_ptr<TemporaryFile> m_deviceCtlOutput;
std::unique_ptr<TaskTree> m_processIdTask;
bool m_startStopped = false;
};
DeviceCtlRunnerBase::DeviceCtlRunnerBase(RunControl *runControl)
@@ -305,9 +316,7 @@ void DeviceCtlRunnerBase::reportStoppedImpl()
void DeviceCtlRunnerBase::start()
{
QSettings settings(m_bundlePath.pathAppended("Info.plist").toString(), QSettings::NativeFormat);
const QString bundleIdentifier
= settings.value(QString::fromLatin1("CFBundleIdentifier")).toString();
const QString bundleIdentifier = getBundleIdentifier(m_bundlePath);
if (bundleIdentifier.isEmpty()) {
reportFailure(Tr::tr("Failed to determine bundle identifier."));
return;
@@ -450,25 +459,47 @@ GroupItem DeviceCtlRunner::launchTask(const QString &bundleIdentifier)
reportFailure(Tr::tr("Running failed. Failed to create the temporary output file."));
return false;
}
m_process.setCommand(
{FilePath::fromString("/usr/bin/xcrun"),
{"devicectl",
"device",
"process",
"launch",
"--device",
m_device->uniqueInternalDeviceId(),
"--quiet",
"--json-output",
m_deviceCtlOutput->fileName(),
"--console",
bundleIdentifier,
m_arguments}});
connect(&m_process, &Process::started, this, [this] { reportStarted(); });
const QStringList startStoppedArg = m_startStopped ? QStringList("--start-stopped")
: QStringList();
const QStringList arguments = QStringList(
{"devicectl",
"device",
"process",
"launch",
"--device",
m_device->uniqueInternalDeviceId(),
"--quiet",
"--json-output",
m_deviceCtlOutput->fileName()})
+ startStoppedArg
+ QStringList({"--console", bundleIdentifier}) + m_arguments;
m_process.setCommand({FilePath::fromString("/usr/bin/xcrun"), arguments});
connect(&m_process, &Process::started, this, [this, bundleIdentifier] {
// devicectl does report the process ID in its json output, but that is broken
// for --console. When that is used, the json output is only written after the process
// finished, which is not helpful.
// Manually find the process ID for the bundle identifier.
Storage<AppInfo> appInfo;
m_processIdTask.reset(new TaskTree(Group{
sequential,
appInfo,
findApp(bundleIdentifier, appInfo),
findProcess(appInfo),
onGroupDone([this, appInfo](DoneWith doneWith) {
if (doneWith == DoneWith::Success) {
m_processIdentifier = appInfo->processIdentifier;
reportStarted();
} else {
reportFailure(Tr::tr("Failed to retrieve process ID."));
}
})}));
m_processIdTask->start();
});
connect(&m_process, &Process::done, this, [this] {
if (m_process.error() != QProcess::UnknownError)
reportFailure(Tr::tr("Failed to run devicectl: %1.").arg(m_process.errorString()));
m_deviceCtlOutput->reset();
m_processIdTask.reset();
reportStoppedImpl();
});
connect(&m_process, &Process::readyReadStandardError, this, [this] {
@@ -841,7 +872,8 @@ public:
private:
void start() override;
IosRunner *m_runner;
IosRunner *m_iosRunner = nullptr;
DeviceCtlRunner *m_deviceCtlRunner = nullptr;
};
static expected_str<FilePath> findDeviceSdk(IosDevice::ConstPtr dev)
@@ -874,15 +906,29 @@ IosDebugSupport::IosDebugSupport(RunControl *runControl)
{
setId("IosDebugSupport");
m_runner = new IosRunner(runControl);
m_runner->setCppDebugging(isCppDebugging());
m_runner->setQmlDebugging(isQmlDebugging() ? QmlDebuggerServices : NoQmlDebugServices);
IosDevice::ConstPtr dev = std::dynamic_pointer_cast<const IosDevice>(device());
addStartDependency(m_runner);
if (dev->type() == Ios::Constants::IOS_SIMULATOR_TYPE
|| dev->handler() == IosDevice::Handler::IosTool) {
m_iosRunner = new IosRunner(runControl);
m_iosRunner->setCppDebugging(isCppDebugging());
m_iosRunner->setQmlDebugging(isQmlDebugging() ? QmlDebuggerServices : NoQmlDebugServices);
addStartDependency(m_iosRunner);
} else {
QTC_CHECK(isCppDebugging());
m_deviceCtlRunner = new DeviceCtlRunner(runControl);
m_deviceCtlRunner->setStartStopped(true);
addStartDependency(m_deviceCtlRunner);
}
if (device()->type() == Ios::Constants::IOS_DEVICE_TYPE) {
IosDevice::ConstPtr dev = std::dynamic_pointer_cast<const IosDevice>(device());
setStartMode(AttachToRemoteProcess);
if (dev->handler() == IosDevice::Handler::DeviceCtl) {
QTC_CHECK(IosDeviceManager::isDeviceCtlDebugSupported());
setStartMode(AttachToIosDevice);
setDeviceUuid(dev->uniqueInternalDeviceId());
} else {
setStartMode(AttachToRemoteProcess);
}
setIosPlatform("remote-ios");
const expected_str<FilePath> deviceSdk = findDeviceSdk(dev);
@@ -898,20 +944,39 @@ IosDebugSupport::IosDebugSupport(RunControl *runControl)
void IosDebugSupport::start()
{
if (!m_runner->isAppRunning()) {
const IosDeviceTypeAspect::Data *data = runControl()->aspectData<IosDeviceTypeAspect>();
QTC_ASSERT(data, reportFailure("Broken IosDeviceTypeAspect setup."); return);
setRunControlName(data->applicationName);
setContinueAfterAttach(true);
IosDevice::ConstPtr dev = std::dynamic_pointer_cast<const IosDevice>(device());
if (dev->type() == Ios::Constants::IOS_DEVICE_TYPE
&& dev->handler() == IosDevice::Handler::DeviceCtl) {
const auto msgOnlyCppDebuggingSupported = [] {
return Tr::tr("Only C++ debugging is supported for devices with iOS 17 and later.");
};
if (!isCppDebugging()) {
reportFailure(msgOnlyCppDebuggingSupported());
return;
}
if (isQmlDebugging()) {
runParameters().isQmlDebugging = false;
appendMessage(msgOnlyCppDebuggingSupported(), OutputFormat::LogMessageFormat, true);
}
setAttachPid(m_deviceCtlRunner->processIdentifier());
setInferiorExecutable(data->localExecutable);
DebuggerRunTool::start();
return;
}
if (!m_iosRunner->isAppRunning()) {
reportFailure(Tr::tr("Application not running."));
return;
}
const IosDeviceTypeAspect::Data *data = runControl()->aspectData<IosDeviceTypeAspect>();
QTC_ASSERT(data, reportFailure("Broken IosDeviceTypeAspect setup."); return);
setRunControlName(data->applicationName);
setContinueAfterAttach(true);
Port gdbServerPort = m_runner->gdbServerPort();
Port qmlServerPort = m_runner->qmlServerPort();
setAttachPid(ProcessHandle(m_runner->pid()));
Port gdbServerPort = m_iosRunner->gdbServerPort();
Port qmlServerPort = m_iosRunner->qmlServerPort();
setAttachPid(ProcessHandle(m_iosRunner->pid()));
const bool cppDebug = isCppDebugging();
const bool qmlDebug = isQmlDebugging();