From a6aa050890789f689f3780cc5481f6e0340b5468 Mon Sep 17 00:00:00 2001 From: Eike Ziller Date: Wed, 8 Jan 2025 13:03:20 +0100 Subject: [PATCH] iOS: Support C++ debugging on devices with iOS 17+ Xcode 16 added special commands for that to lldb: - `device select ` switches the debugging to that device - `device process attach ` 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 Reviewed-by: Marcus Tillmanns --- share/qtcreator/debugger/lldbbridge.py | 33 ++++- share/qtcreator/debugger/utils.py | 4 +- src/plugins/debugger/debuggerconstants.h | 8 +- src/plugins/debugger/debuggerengine.h | 2 + src/plugins/debugger/debuggerruncontrol.cpp | 5 + src/plugins/debugger/debuggerruncontrol.h | 2 + src/plugins/debugger/lldb/lldbengine.cpp | 15 ++- src/plugins/ios/iosrunconfiguration.cpp | 9 +- src/plugins/ios/iosrunner.cpp | 141 ++++++++++++++------ 9 files changed, 161 insertions(+), 58 deletions(-) diff --git a/share/qtcreator/debugger/lldbbridge.py b/share/qtcreator/debugger/lldbbridge.py index 57275c1e9a9..e612f69e36a 100644 --- a/share/qtcreator/debugger/lldbbridge.py +++ b/share/qtcreator/debugger/lldbbridge.py @@ -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) diff --git a/share/qtcreator/debugger/utils.py b/share/qtcreator/debugger/utils.py index 8019d1e530a..9cd4a8822ec 100644 --- a/share/qtcreator/debugger/utils.py +++ b/share/qtcreator/debugger/utils.py @@ -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 diff --git a/src/plugins/debugger/debuggerconstants.h b/src/plugins/debugger/debuggerconstants.h index 48dfeb5b67f..8d92cc67412 100644 --- a/src/plugins/debugger/debuggerconstants.h +++ b/src/plugins/debugger/debuggerconstants.h @@ -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 diff --git a/src/plugins/debugger/debuggerengine.h b/src/plugins/debugger/debuggerengine.h index e35bdf27af9..9b907837651 100644 --- a/src/plugins/debugger/debuggerengine.h +++ b/src/plugins/debugger/debuggerengine.h @@ -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; diff --git a/src/plugins/debugger/debuggerruncontrol.cpp b/src/plugins/debugger/debuggerruncontrol.cpp index 1e87a30c7ab..9812c28f860 100644 --- a/src/plugins/debugger/debuggerruncontrol.cpp +++ b/src/plugins/debugger/debuggerruncontrol.cpp @@ -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; diff --git a/src/plugins/debugger/debuggerruncontrol.h b/src/plugins/debugger/debuggerruncontrol.h index 66b559393ec..2faacc846bf 100644 --- a/src/plugins/debugger/debuggerruncontrol.h +++ b/src/plugins/debugger/debuggerruncontrol.h @@ -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; diff --git a/src/plugins/debugger/lldb/lldbengine.cpp b/src/plugins/debugger/lldb/lldbengine.cpp index 0010000edbd..c1d208dcf8c 100644 --- a/src/plugins/debugger/lldb/lldbengine.cpp +++ b/src/plugins/debugger/lldb/lldbengine.cpp @@ -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; } } diff --git a/src/plugins/ios/iosrunconfiguration.cpp b/src/plugins/ios/iosrunconfiguration.cpp index 42c6fc455cd..008d6f6d631 100644 --- a/src/plugins/ios/iosrunconfiguration.cpp +++ b/src/plugins/ios/iosrunconfiguration.cpp @@ -103,7 +103,8 @@ bool IosRunConfiguration::isEnabled(Id runMode) const IosDevice::ConstPtr iosdevice = std::dynamic_pointer_cast(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(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); diff --git a/src/plugins/ios/iosrunner.cpp b/src/plugins/ios/iosrunner.cpp index 0a4c55d536d..348bbe35781 100644 --- a/src/plugins/ios/iosrunner.cpp +++ b/src/plugins/ios/iosrunner.cpp @@ -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); + GroupItem findProcess(Storage &appInfo); void reportStoppedImpl(); IosDevice::ConstPtr m_device; QStringList m_arguments; + qint64 m_processIdentifier = -1; private: - GroupItem findApp(const QString &bundleIdentifier, Storage appInfo); - GroupItem findProcess(Storage &appInfo); GroupItem killProcess(Storage &appInfo); virtual GroupItem launchTask(const QString &bundleIdentifier) = 0; @@ -135,7 +143,6 @@ private: std::unique_ptr m_stopTask; std::unique_ptr 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 m_deviceCtlOutput; + std::unique_ptr 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; + 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 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(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(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 deviceSdk = findDeviceSdk(dev); @@ -898,20 +944,39 @@ IosDebugSupport::IosDebugSupport(RunControl *runControl) void IosDebugSupport::start() { - if (!m_runner->isAppRunning()) { + const IosDeviceTypeAspect::Data *data = runControl()->aspectData(); + QTC_ASSERT(data, reportFailure("Broken IosDeviceTypeAspect setup."); return); + setRunControlName(data->applicationName); + setContinueAfterAttach(true); + + IosDevice::ConstPtr dev = std::dynamic_pointer_cast(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(); - 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();