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.startMode_ = None
self.processArgs_ = None self.processArgs_ = None
self.attachPid_ = None self.attachPid_ = None
self.deviceUuid_ = None
self.dyldImageSuffix = None self.dyldImageSuffix = None
self.dyldLibraryPath = None self.dyldLibraryPath = None
self.dyldFrameworkPath = None self.dyldFrameworkPath = None
@@ -918,6 +919,7 @@ class Dumper(DumperBase):
self.environment_ = args.get('environment', []) self.environment_ = args.get('environment', [])
self.environment_ = list(map(lambda x: self.hexdecode(x), self.environment_)) self.environment_ = list(map(lambda x: self.hexdecode(x), self.environment_))
self.attachPid_ = args.get('attachpid', 0) self.attachPid_ = args.get('attachpid', 0)
self.deviceUuid_ = args.get('deviceUuid', '')
self.sysRoot_ = args.get('sysroot', '') self.sysRoot_ = args.get('sysroot', '')
self.remoteChannel_ = args.get('remotechannel', '') self.remoteChannel_ = args.get('remotechannel', '')
self.platform_ = args.get('platform', '') self.platform_ = args.get('platform', '')
@@ -942,14 +944,26 @@ class Dumper(DumperBase):
if self.startMode_ == DebuggerStartMode.AttachExternal: if self.startMode_ == DebuggerStartMode.AttachExternal:
self.symbolFile_ = '' self.symbolFile_ = ''
self.target = self.debugger.CreateTarget( if self.startMode_ == DebuggerStartMode.AttachToIosDevice:
self.symbolFile_, None, self.platform_, True, error) # 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(): if not error.Success():
self.report(self.describeError(error)) self.report(self.describeError(error))
self.reportState('enginerunfailed') self.reportState('enginerunfailed')
return return
if not self.target:
self.report('Debugger failed to create target.')
self.reportState('enginerunfailed')
return
broadcaster = self.target.GetBroadcaster() broadcaster = self.target.GetBroadcaster()
listener = self.debugger.GetListener() listener = self.debugger.GetListener()
broadcaster.AddListener(listener, lldb.SBProcess.eBroadcastBitStateChanged) broadcaster.AddListener(listener, lldb.SBProcess.eBroadcastBitStateChanged)
@@ -1090,6 +1104,11 @@ class Dumper(DumperBase):
self.reportState('enginerunokandinferiorunrunnable') self.reportState('enginerunokandinferiorunrunnable')
else: else:
self.reportState('enginerunfailed') 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: else:
launchInfo = lldb.SBLaunchInfo(self.processArgs_) launchInfo = lldb.SBLaunchInfo(self.processArgs_)
launchInfo.SetWorkingDirectory(self.workingDirectory_) launchInfo.SetWorkingDirectory(self.workingDirectory_)
@@ -1920,16 +1939,20 @@ class Dumper(DumperBase):
self.debugger.GetCommandInterpreter().HandleCommand(command, result) self.debugger.GetCommandInterpreter().HandleCommand(command, result)
self.reportResult('fulltrace="%s"' % self.hexencode(result.GetOutput()), args) self.reportResult('fulltrace="%s"' % self.hexencode(result.GetOutput()), args)
def executeDebuggerCommand(self, args): def runDebuggerCommand(self, command):
self.reportToken(args) self.report('Running debugger command "{}"'.format(command))
result = lldb.SBCommandReturnObject() result = lldb.SBCommandReturnObject()
command = args['command']
self.debugger.GetCommandInterpreter().HandleCommand(command, result) self.debugger.GetCommandInterpreter().HandleCommand(command, result)
success = result.Succeeded() success = result.Succeeded()
output = toCString(result.GetOutput()) output = toCString(result.GetOutput())
error = toCString(str(result.GetError())) error = toCString(str(result.GetError()))
self.report('success="%d",output="%s",error="%s"' % (success, output, error)) 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): def executeRoundtrip(self, args):
self.reportResult('', args) self.reportResult('', args)

View File

@@ -15,8 +15,10 @@ class DebuggerStartMode():
AttachCore, AttachCore,
AttachToRemoteServer, AttachToRemoteServer,
AttachToRemoteProcess, AttachToRemoteProcess,
AttachToQmlServer,
StartRemoteProcess, StartRemoteProcess,
) = range(0, 9) AttachToIosDevice
) = range(0, 11)
# Known special formats. Keep in sync with DisplayFormat in debuggerprotocol.h # 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 } // namespace Constants
// Keep in sync with dumper.py // Keep in sync with debugger/utils.py
enum DebuggerStartMode enum DebuggerStartMode {
{
NoStartMode, NoStartMode,
StartInternal, // Start current start project's binary StartInternal, // Start current start project's binary
StartExternal, // Start binary found in file system StartExternal, // Start binary found in file system
@@ -40,7 +39,8 @@ enum DebuggerStartMode
AttachToRemoteServer, // Attach to a running gdbserver AttachToRemoteServer, // Attach to a running gdbserver
AttachToRemoteProcess, // Attach to a running remote process AttachToRemoteProcess, // Attach to a running remote process
AttachToQmlServer, // Attach to a running QmlServer 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 enum DebuggerCloseMode

View File

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

View File

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

View File

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

View File

@@ -270,6 +270,7 @@ void LldbEngine::handleLldbStarted()
cmd2.arg("startmode", rp.startMode); cmd2.arg("startmode", rp.startMode);
cmd2.arg("nativemixed", isNativeMixedActive()); cmd2.arg("nativemixed", isNativeMixedActive());
cmd2.arg("workingdirectory", inferior.workingDirectory.path()); cmd2.arg("workingdirectory", inferior.workingDirectory.path());
cmd2.arg("deviceUuid", rp.deviceUuid);
Environment environment = inferior.environment; Environment environment = inferior.environment;
// Prevent lldb from automatically setting OS_ACTIVITY_DT_MODE to mirror // 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 // 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) { if (rp.startMode != StartInternal) {
// it is better not to check the start mode on the python sid (as we would have to duplicate the // 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 // 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 QTC_CHECK(
|| rp.startMode == AttachToLocalProcess rp.attachPID.isValid()
|| rp.startMode == AttachToRemoteServer)); && (rp.startMode == AttachToRemoteProcess || rp.startMode == AttachToLocalProcess
|| rp.startMode == AttachToRemoteServer || rp.startMode == AttachToIosDevice));
cmd2.arg("attachpid", rp.attachPID.pid()); cmd2.arg("attachpid", rp.attachPID.pid());
cmd2.arg("sysroot", rp.deviceSymbolsRoot.isEmpty() ? rp.sysRoot.toString() cmd2.arg("sysroot", rp.deviceSymbolsRoot.isEmpty() ? rp.sysRoot.toString()
: rp.deviceSymbolsRoot); : rp.deviceSymbolsRoot);
cmd2.arg("remotechannel", ((rp.startMode == AttachToRemoteProcess cmd2.arg("remotechannel", ((rp.startMode == AttachToRemoteProcess
|| rp.startMode == AttachToRemoteServer) || rp.startMode == AttachToRemoteServer)
? rp.remoteChannel : QString())); ? rp.remoteChannel : QString()));
QTC_CHECK(!rp.continueAfterAttach || (rp.startMode == AttachToRemoteProcess QTC_CHECK(
|| rp.startMode == AttachToLocalProcess !rp.continueAfterAttach
|| rp.startMode == AttachToRemoteServer)); || (rp.startMode == AttachToRemoteProcess || rp.startMode == AttachToLocalProcess
|| rp.startMode == AttachToRemoteServer || rp.startMode == AttachToIosDevice));
m_continueAtNextSpontaneousStop = false; 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); IosDevice::ConstPtr iosdevice = std::dynamic_pointer_cast<const IosDevice>(dev);
if (iosdevice && iosdevice->handler() == IosDevice::Handler::DeviceCtl if (iosdevice && iosdevice->handler() == IosDevice::Handler::DeviceCtl
&& runMode != ProjectExplorer::Constants::NORMAL_RUN_MODE) { && runMode != ProjectExplorer::Constants::NORMAL_RUN_MODE
&& !IosDeviceManager::isDeviceCtlDebugSupported()) {
return false; return false;
} }
@@ -280,9 +281,9 @@ QString IosRunConfiguration::disabledReason(Id runMode) const
} }
IosDevice::ConstPtr iosdevice = std::dynamic_pointer_cast<const IosDevice>(dev); IosDevice::ConstPtr iosdevice = std::dynamic_pointer_cast<const IosDevice>(dev);
if (iosdevice && iosdevice->handler() == IosDevice::Handler::DeviceCtl if (iosdevice && iosdevice->handler() == IosDevice::Handler::DeviceCtl
&& runMode != ProjectExplorer::Constants::NORMAL_RUN_MODE) { && runMode != ProjectExplorer::Constants::NORMAL_RUN_MODE
return Tr::tr("Debugging and profiling is currently not supported for devices with iOS " && !IosDeviceManager::isDeviceCtlDebugSupported()) {
"17 and later."); return Tr::tr("Debugging on devices with iOS 17 and later requires Xcode 16 or later.");
} }
} }
return RunConfiguration::disabledReason(runMode); return RunConfiguration::disabledReason(runMode);

View File

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