Create a RunWorker for running apps on iOS 17 devices

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ø <tor.arne.vestbo@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
Eike Ziller
2024-01-16 12:19:29 +01:00
parent 0ecab7e0d8
commit 7e1b406229

View File

@@ -3,6 +3,7 @@
#include "iosrunner.h"
#include "devicectlutils.h"
#include "iosconfigurations.h"
#include "iosconstants.h"
#include "iosdevice.h"
@@ -27,29 +28,35 @@
#include <utils/fileutils.h>
#include <utils/process.h>
#include <utils/stringutils.h>
#include <utils/url.h>
#include <utils/utilsicons.h>
#include <solutions/tasking/tasktree.h>
#include <QDateTime>
#include <QDir>
#include <QJsonArray>
#include <QMessageBox>
#include <QRegularExpression>
#include <QSettings>
#include <QTcpServer>
#include <QTime>
#include <QTimer>
#include <memory>
#include <stdio.h>
#include <fcntl.h>
#ifdef Q_OS_UNIX
#include <unistd.h>
#else
#include <io.h>
#endif
#include <signal.h>
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> appInfo);
GroupItem findProcess(Storage<AppInfo> &appInfo);
GroupItem killProcess(Storage<AppInfo> &appInfo);
GroupItem launchTask(const QString &bundleIdentifier);
FilePath m_bundlePath;
QStringList m_arguments;
IosDevice::ConstPtr m_device;
std::unique_ptr<TaskTree> m_runTask;
std::unique_ptr<TaskTree> m_pollTask;
QTimer m_pollTimer;
qint64 m_processIdentifier = -1;
};
DeviceCtlRunner::DeviceCtlRunner(RunControl *runControl)
: RunWorker(runControl)
{
setId("IosDeviceCtlRunner");
const IosDeviceTypeAspect::Data *data = runControl->aspect<IosDeviceTypeAspect>();
QTC_ASSERT(data, return);
m_bundlePath = data->bundleDirectory;
m_arguments = ProcessArgs::splitArgs(runControl->commandLine().arguments(), OsTypeMac);
m_device = DeviceKitAspect::device(runControl->kit()).dynamicCast<const IosDevice>();
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> 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<QJsonValue> 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> &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<QJsonValue> 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> &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<QJsonValue> 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> 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<QJsonValue> 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<QJsonValue> 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<IosRunSupport>();
setProducer([](RunControl *control) -> RunWorker * {
IosDevice::ConstPtr iosdevice = control->device().dynamicCast<const IosDevice>();
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);
}