forked from qt-creator/qt-creator
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:
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
|
Reference in New Issue
Block a user