From 7e1b40622991aac74c8b23602f1f042e2bcfab5a Mon Sep 17 00:00:00 2001 From: Eike Ziller Date: Tue, 16 Jan 2024 12:19:29 +0100 Subject: [PATCH] Create a RunWorker for running apps on iOS 17 devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That uses the various devicectl commands for starting and stopping the app, and polling it's state for Qt Creator's stop button. Getting app output and debugging and profiling are not supported, since devicectl doesn't provide the necessary functionality. Fixes: QTCREATORBUG-29682 Change-Id: Ied63b280458e5c109446a140a7774c2909aad62f Reviewed-by: Tor Arne Vestbø Reviewed-by: Qt CI Bot --- src/plugins/ios/iosrunner.cpp | 333 +++++++++++++++++++++++++++++++++- 1 file changed, 330 insertions(+), 3 deletions(-) diff --git a/src/plugins/ios/iosrunner.cpp b/src/plugins/ios/iosrunner.cpp index b3468cad21d..c3209709d42 100644 --- a/src/plugins/ios/iosrunner.cpp +++ b/src/plugins/ios/iosrunner.cpp @@ -3,6 +3,7 @@ #include "iosrunner.h" +#include "devicectlutils.h" #include "iosconfigurations.h" #include "iosconstants.h" #include "iosdevice.h" @@ -27,29 +28,35 @@ #include #include +#include #include #include +#include + #include #include +#include #include #include #include #include #include +#include + +#include -#include #include #ifdef Q_OS_UNIX #include #else #include #endif -#include using namespace Debugger; using namespace ProjectExplorer; using namespace Utils; +using namespace Tasking; namespace Ios::Internal { @@ -71,6 +78,320 @@ static void stopRunningRunControl(RunControl *runControl) activeRunControls[devId] = runControl; } +struct AppInfo +{ + QUrl pathOnDevice; + qint64 processIdentifier = -1; +}; + +class DeviceCtlRunner : public RunWorker +{ +public: + DeviceCtlRunner(RunControl *runControl); + + void start() final; + void stop() final; + + void checkProcess(); + +private: + GroupItem findApp(const QString &bundleIdentifier, Storage appInfo); + GroupItem findProcess(Storage &appInfo); + GroupItem killProcess(Storage &appInfo); + GroupItem launchTask(const QString &bundleIdentifier); + + FilePath m_bundlePath; + QStringList m_arguments; + IosDevice::ConstPtr m_device; + std::unique_ptr m_runTask; + std::unique_ptr m_pollTask; + QTimer m_pollTimer; + qint64 m_processIdentifier = -1; +}; + +DeviceCtlRunner::DeviceCtlRunner(RunControl *runControl) + : RunWorker(runControl) +{ + setId("IosDeviceCtlRunner"); + const IosDeviceTypeAspect::Data *data = runControl->aspect(); + QTC_ASSERT(data, return); + m_bundlePath = data->bundleDirectory; + m_arguments = ProcessArgs::splitArgs(runControl->commandLine().arguments(), OsTypeMac); + m_device = DeviceKitAspect::device(runControl->kit()).dynamicCast(); + + using namespace std::chrono_literals; + m_pollTimer.setInterval(500ms); // not too often since running devicectl takes time + connect(&m_pollTimer, &QTimer::timeout, this, &DeviceCtlRunner::checkProcess); +} + +GroupItem DeviceCtlRunner::findApp(const QString &bundleIdentifier, Storage appInfo) +{ + const auto onSetup = [this](Process &process) { + if (!m_device) + return SetupResult::StopWithSuccess; // don't block the following tasks + process.setCommand({FilePath::fromString("/usr/bin/xcrun"), + {"devicectl", + "device", + "info", + "apps", + "--device", + m_device->uniqueInternalDeviceId(), + "--quiet", + "--json-output", + "-"}}); + return SetupResult::Continue; + }; + const auto onDone = [this, bundleIdentifier, appInfo](const Process &process) { + if (process.error() != QProcess::UnknownError) { + reportFailure(Tr::tr("Failed to run devicectl: %1.").arg(process.errorString())); + return DoneResult::Error; + } + const Utils::expected_str resultValue = parseDevicectlResult( + process.rawStdOut()); + if (resultValue) { + const QJsonArray apps = (*resultValue)["apps"].toArray(); + for (const QJsonValue &app : apps) { + if (app["bundleIdentifier"].toString() == bundleIdentifier) { + appInfo->pathOnDevice = QUrl(app["url"].toString()); + break; + } + } + return DoneResult::Success; + } + reportFailure(resultValue.error()); + return DoneResult::Error; + }; + return ProcessTask(onSetup, onDone); +} + +GroupItem DeviceCtlRunner::findProcess(Storage &appInfo) +{ + const auto onSetup = [this, appInfo](Process &process) { + if (!m_device || appInfo->pathOnDevice.isEmpty()) + return SetupResult::StopWithSuccess; // don't block the following tasks + process.setCommand( + {FilePath::fromString("/usr/bin/xcrun"), + {"devicectl", + "device", + "info", + "processes", + "--device", + m_device->uniqueInternalDeviceId(), + "--quiet", + "--json-output", + "-", + "--filter", + QLatin1String("executable.path BEGINSWITH '%1'").arg(appInfo->pathOnDevice.path())}}); + return SetupResult::Continue; + }; + const auto onDone = [this, appInfo](const Process &process) { + const Utils::expected_str resultValue = parseDevicectlResult( + process.rawStdOut()); + if (resultValue) { + const QJsonArray matchingProcesses = (*resultValue)["runningProcesses"].toArray(); + if (matchingProcesses.size() > 0) { + appInfo->processIdentifier + = matchingProcesses.first()["processIdentifier"].toInteger(-1); + } + return DoneResult::Success; + } + reportFailure(resultValue.error()); + return DoneResult::Error; + }; + return ProcessTask(onSetup, onDone); +} + +GroupItem DeviceCtlRunner::killProcess(Storage &appInfo) +{ + const auto onSetup = [this, appInfo](Process &process) { + if (!m_device || appInfo->processIdentifier < 0) + return SetupResult::StopWithSuccess; // don't block the following tasks + process.setCommand({FilePath::fromString("/usr/bin/xcrun"), + {"devicectl", + "device", + "process", + "signal", + "--device", + m_device->uniqueInternalDeviceId(), + "--quiet", + "--json-output", + "-", + "--signal", + "SIGKILL", + "--pid", + QString::number(appInfo->processIdentifier)}}); + return SetupResult::Continue; + }; + const auto onDone = [] { + // we tried our best and don't care at this point + return DoneResult::Success; + }; + return ProcessTask(onSetup, onDone); +} + +GroupItem DeviceCtlRunner::launchTask(const QString &bundleIdentifier) +{ + const auto onSetup = [this, bundleIdentifier](Process &process) { + if (!m_device) { + reportFailure(Tr::tr("Running failed. No iOS device found.")); + return SetupResult::StopWithError; + } + process.setCommand({FilePath::fromString("/usr/bin/xcrun"), + QStringList{"devicectl", + "device", + "process", + "launch", + "--device", + m_device->uniqueInternalDeviceId(), + "--quiet", + "--json-output", + "-", + bundleIdentifier} + + m_arguments}); + return SetupResult::Continue; + }; + const auto onDone = [this](const Process &process, DoneWith result) { + if (result == DoneWith::Cancel) { + reportFailure(Tr::tr("Running canceled.")); + return DoneResult::Error; + } + if (process.error() != QProcess::UnknownError) { + reportFailure(Tr::tr("Failed to run devicectl: %1.").arg(process.errorString())); + return DoneResult::Error; + } + const Utils::expected_str resultValue = parseDevicectlResult( + process.rawStdOut()); + if (resultValue) { + // success + m_processIdentifier = (*resultValue)["process"]["processIdentifier"].toInteger(-1); + if (m_processIdentifier < 0) { + // something unexpected happened ... + reportFailure(Tr::tr("devicectl returned unexpected output ... running failed.")); + return DoneResult::Error; + } + m_pollTimer.start(); + reportStarted(); + return DoneResult::Success; + } + // failure + reportFailure(resultValue.error()); + return DoneResult::Error; + }; + return ProcessTask(onSetup, onDone); +} + +void DeviceCtlRunner::start() +{ + QSettings settings(m_bundlePath.pathAppended("Info.plist").toString(), QSettings::NativeFormat); + const QString bundleIdentifier + = settings.value(QString::fromLatin1("CFBundleIdentifier")).toString(); + if (bundleIdentifier.isEmpty()) { + reportFailure(Tr::tr("Failed to determine bundle identifier.")); + return; + } + + // If the app is already running, we should first kill it, then launch again. + // Usually deployment already kills the running app, but we support running without + // deployment. Restarting is then e.g. needed if the app arguments changed. + // Unfortunately the information about running processes only includes the path + // on device and processIdentifier. + // So we find out if the app is installed, and its path on device. + // Check if a process is running for that path, and get its processIdentifier. + // Try to kill that. + // Then launch the app (again). + Storage appInfo; + m_runTask.reset(new TaskTree(Group{sequential, + appInfo, + findApp(bundleIdentifier, appInfo), + findProcess(appInfo), + killProcess(appInfo), + launchTask(bundleIdentifier)})); + m_runTask->start(); +} + +void DeviceCtlRunner::stop() +{ + // stop polling, we handle the reportStopped in the done handler + m_pollTimer.stop(); + if (m_pollTask) + m_pollTask.release()->deleteLater(); + const auto onSetup = [this](Process &process) { + if (!m_device) { + reportStopped(); + return SetupResult::StopWithError; + } + process.setCommand({FilePath::fromString("/usr/bin/xcrun"), + {"devicectl", + "device", + "process", + "signal", + "--device", + m_device->uniqueInternalDeviceId(), + "--quiet", + "--json-output", + "-", + "--signal", + "SIGKILL", + "--pid", + QString::number(m_processIdentifier)}}); + return SetupResult::Continue; + }; + const auto onDone = [this](const Process &process) { + if (process.error() != QProcess::UnknownError) { + reportFailure(Tr::tr("Failed to run devicectl: %1.").arg(process.errorString())); + return DoneResult::Error; + } + const Utils::expected_str resultValue = parseDevicectlResult( + process.rawStdOut()); + if (!resultValue) { + reportFailure(resultValue.error()); + return DoneResult::Error; + } + reportStopped(); + return DoneResult::Success; + }; + m_runTask.reset(new TaskTree(Group{ProcessTask(onSetup, onDone)})); + m_runTask->start(); +} + +void DeviceCtlRunner::checkProcess() +{ + if (m_pollTask) + return; + const auto onSetup = [this](Process &process) { + if (!m_device) + return SetupResult::StopWithError; + process.setCommand( + {FilePath::fromString("/usr/bin/xcrun"), + {"devicectl", + "device", + "info", + "processes", + "--device", + m_device->uniqueInternalDeviceId(), + "--quiet", + "--json-output", + "-", + "--filter", + QLatin1String("processIdentifier == %1").arg(QString::number(m_processIdentifier))}}); + return SetupResult::Continue; + }; + const auto onDone = [this](const Process &process) { + const Utils::expected_str resultValue = parseDevicectlResult( + process.rawStdOut()); + if (!resultValue || (*resultValue)["runningProcesses"].toArray().size() < 1) { + // no process with processIdentifier found, or some error occurred, device disconnected + // or such, assume "stopped" + m_pollTimer.stop(); + reportStopped(); + } + m_pollTask.release()->deleteLater(); + return DoneResult::Success; + }; + m_pollTask.reset(new TaskTree(Group{ProcessTask(onSetup, onDone)})); + m_pollTask->start(); +} + class IosRunner : public RunWorker { public: @@ -562,7 +883,13 @@ void IosDebugSupport::start() IosRunWorkerFactory::IosRunWorkerFactory() { - setProduct(); + setProducer([](RunControl *control) -> RunWorker * { + IosDevice::ConstPtr iosdevice = control->device().dynamicCast(); + if (iosdevice && iosdevice->handler() == IosDevice::Handler::DeviceCtl) { + return new DeviceCtlRunner(control); + } + return new IosRunSupport(control); + }); addSupportedRunMode(ProjectExplorer::Constants::NORMAL_RUN_MODE); addSupportedRunConfig(Constants::IOS_RUNCONFIG_ID); }