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); }