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