diff options
-rw-r--r-- | src/plugins/ios/iostoolhandler.cpp | 327 | ||||
-rw-r--r-- | src/plugins/ios/simulatorcontrol.cpp | 509 | ||||
-rw-r--r-- | src/plugins/ios/simulatorcontrol.h | 47 |
3 files changed, 551 insertions, 332 deletions
diff --git a/src/plugins/ios/iostoolhandler.cpp b/src/plugins/ios/iostoolhandler.cpp index dcbb402e984..3af160caddc 100644 --- a/src/plugins/ios/iostoolhandler.cpp +++ b/src/plugins/ios/iostoolhandler.cpp @@ -33,6 +33,7 @@ #include <coreplugin/icore.h> #include <utils/qtcassert.h> #include <utils/fileutils.h> +#include "utils/runextensions.h" #include <QCoreApplication> #include <QFileInfo> @@ -160,10 +161,9 @@ public: protected: void killProcess(); - protected: IosToolHandler *q; - QProcess *process; + std::shared_ptr<QProcess> process; QTimer killTimer; QXmlStreamReader outputParser; QString deviceId; @@ -199,16 +199,63 @@ private: void processXml(); }; +/**************************************************************************** + * Flow to install an app on simulator:- + * +------------------+ + * | Transfer App | + * +--------+---------+ + * | + * v + * +---------+----------+ +--------------------------------+ + * | SimulatorRunning +---No------> +SimulatorControl::startSimulator| + * +---------+----------+ +--------+-----------------------+ + * Yes | + * | | + * v | + * +---------+--------------------+ | + * | SimulatorControl::installApp | <--------------+ + * +------------------------------+ + * + * + * + * Flow to launch an app on Simulator:- + * +---------+ + * | Run App | + * +----+----+ + * | + * v + * +-------------------+ +----------------------------- - --+ + * | SimulatorRunning? +---NO------> + SimulatorControl::startSimulator | + * +--------+----------+ +----------------+-----------------+ + * YES | + * | | + * v | + * +---------+-------------------------+ | + * | SimulatorControl::spawnAppProcess | <------------------+ + * +-----------------------------------+ + * | + * v + * +--------+-----------+ +-----------------------------+ + * | Debug Run ? +---YES------> + Wait for debugger to attach | + * +---------+----------+ +-----------+-----------------+ + * NO | + * | | + * v | + * +-----------------------------+ | + * | SimulatorControl::launchApp | <-------------------+ + * +-----------------------------+ + ***************************************************************************/ class IosSimulatorToolHandlerPrivate : public IosToolHandlerPrivate { public: explicit IosSimulatorToolHandlerPrivate(const IosDeviceType &devType, IosToolHandler *q); + ~IosSimulatorToolHandlerPrivate(); // IosToolHandlerPrivate overrides public: - void requestTransferApp(const QString &bundlePath, const QString &deviceIdentifier, + void requestTransferApp(const QString &appBundlePath, const QString &deviceIdentifier, int timeout = 1000) override; - void requestRunApp(const QString &bundlePath, const QStringList &extraArgs, + void requestRunApp(const QString &appBundlePath, const QStringList &extraArgs, IosToolHandler::RunKind runKind, const QString &deviceIdentifier, int timeout = 1000) override; void requestDeviceInfo(const QString &deviceId, int timeout = 1000) override; @@ -216,15 +263,23 @@ public: void debuggerStateChanged(Debugger::DebuggerState state) override; private: + void installAppOnSimulator(); + void spawnAppOnSimulator(const QStringList &extraArgs); + void launchAppOnSimulator(); + + bool isResponseValid(const SimulatorControl::ResponseData &responseData); + void onResponseAppSpawn(const SimulatorControl::ResponseData &response); + void simAppProcessError(QProcess::ProcessError error); void simAppProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void simAppProcessHasData(); void simAppProcessHasErrorOutput(); - void launchAppOnSimulator(); private: qint64 appPId = -1; bool appLaunched = false; + SimulatorControl *simCtl; + QList<QFuture<void>> futureList; }; IosToolHandlerPrivate::IosToolHandlerPrivate(const IosDeviceType &devType, @@ -242,12 +297,6 @@ IosToolHandlerPrivate::IosToolHandlerPrivate(const IosDeviceType &devType, IosToolHandlerPrivate::~IosToolHandlerPrivate() { - if (isRunning()) { - process->terminate(); - if (!process->waitForFinished(1000)) - process->kill(); - } - delete process; } bool IosToolHandlerPrivate::isRunning() @@ -559,7 +608,12 @@ IosDeviceToolHandlerPrivate::IosDeviceToolHandlerPrivate(const IosDeviceType &de IosToolHandler *q) : IosToolHandlerPrivate(devType, q) { - process = new QProcess; + auto deleter = [](QProcess *p) { + p->kill(); + p->waitForFinished(10000); + delete p; + }; + process = std::shared_ptr<QProcess>(new QProcess, deleter); // Prepare & set process Environment. QProcessEnvironment env(QProcessEnvironment::systemEnvironment()); @@ -583,13 +637,13 @@ IosDeviceToolHandlerPrivate::IosDeviceToolHandlerPrivate(const IosDeviceType &de qCDebug(toolHandlerLog) << "IosToolHandler runEnv:" << env.toStringList(); process->setProcessEnvironment(env); - QObject::connect(process, &QProcess::readyReadStandardOutput, + QObject::connect(process.get(), &QProcess::readyReadStandardOutput, std::bind(&IosDeviceToolHandlerPrivate::subprocessHasData,this)); - QObject::connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), + QObject::connect(process.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), std::bind(&IosDeviceToolHandlerPrivate::subprocessFinished,this, _1,_2)); - QObject::connect(process, &QProcess::errorOccurred, + QObject::connect(process.get(), &QProcess::errorOccurred, std::bind(&IosDeviceToolHandlerPrivate::subprocessError, this, _1)); QObject::connect(&killTimer, &QTimer::timeout, std::bind(&IosDeviceToolHandlerPrivate::killProcess, this)); @@ -685,45 +739,56 @@ void IosDeviceToolHandlerPrivate::stop(int errorCode) IosSimulatorToolHandlerPrivate::IosSimulatorToolHandlerPrivate(const IosDeviceType &devType, IosToolHandler *q) - : IosToolHandlerPrivate(devType, q) -{ } + : IosToolHandlerPrivate(devType, q), + simCtl(new SimulatorControl) +{ +} -void IosSimulatorToolHandlerPrivate::requestTransferApp(const QString &bundlePath, +IosSimulatorToolHandlerPrivate::~IosSimulatorToolHandlerPrivate() +{ + foreach (auto f, futureList) { + if (!f.isFinished()) + f.cancel(); + } + delete simCtl; +} +void IosSimulatorToolHandlerPrivate::requestTransferApp(const QString &appBundlePath, const QString &deviceIdentifier, int timeout) { Q_UNUSED(timeout); - this->bundlePath = bundlePath; - this->deviceId = deviceIdentifier; + bundlePath = appBundlePath; + deviceId = deviceIdentifier; isTransferringApp(bundlePath, deviceId, 0, 100, ""); - if (SimulatorControl::startSimulator(deviceId)) { - isTransferringApp(bundlePath, deviceId, 20, 100, ""); - QByteArray cmdOutput; - if (SimulatorControl::installApp(deviceId, Utils::FileName::fromString(bundlePath), cmdOutput)) { - isTransferringApp(bundlePath, deviceId, 100, 100, ""); - didTransferApp(bundlePath, deviceId, IosToolHandler::Success); + + auto onSimulatorStart = [this](const SimulatorControl::ResponseData &response) { + if (!isResponseValid(response)) + return; + + if (response.success) { + installAppOnSimulator(); } else { - errorMsg(IosToolHandler::tr("Application install on Simulator failed. %1").arg(QString::fromLocal8Bit(cmdOutput))); + errorMsg(IosToolHandler::tr("Application install on Simulator failed. Simulator not running.")); didTransferApp(bundlePath, deviceId, IosToolHandler::Failure); + emit q->finished(q); } - } else { - errorMsg(IosToolHandler::tr("Application install on Simulator failed. Simulator not running.")); - didTransferApp(bundlePath, deviceId, IosToolHandler::Failure); - } - emit q->finished(q); -} + }; + if (SimulatorControl::isSimulatorRunning(deviceId)) + installAppOnSimulator(); + else + futureList << Utils::onResultReady(simCtl->startSimulator(deviceId), onSimulatorStart); +} -void IosSimulatorToolHandlerPrivate::requestRunApp(const QString &bundlePath, +void IosSimulatorToolHandlerPrivate::requestRunApp(const QString &appBundlePath, const QStringList &extraArgs, IosToolHandler::RunKind runType, const QString &deviceIdentifier, int timeout) { Q_UNUSED(timeout); Q_UNUSED(deviceIdentifier); - this->bundlePath = bundlePath; - this->deviceId = devType.identifier; - this->runKind = runType; - op = OpAppRun; + bundlePath = appBundlePath; + deviceId = devType.identifier; + runKind = runType; Utils::FileName appBundle = Utils::FileName::fromString(bundlePath); if (!appBundle.exists()) { @@ -733,62 +798,22 @@ void IosSimulatorToolHandlerPrivate::requestRunApp(const QString &bundlePath, return; } - if (SimulatorControl::startSimulator(deviceId)) { - qint64 pId = -1; - bool debugRun = runType == IosToolHandler::DebugRun; - QProcess* controlProcess = SimulatorControl::spawnAppProcess(deviceId, appBundle, pId, debugRun, extraArgs); - if (controlProcess) { - Q_ASSERT(!process || !isRunning()); - if (process) { - delete process; - process = nullptr; - } - process = controlProcess; - QObject::connect(process, &QProcess::readyReadStandardOutput, - std::bind(&IosSimulatorToolHandlerPrivate::simAppProcessHasData,this)); - QObject::connect(process, &QProcess::readyReadStandardError, - std::bind(&IosSimulatorToolHandlerPrivate::simAppProcessHasErrorOutput,this)); - QObject::connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), - std::bind(&IosSimulatorToolHandlerPrivate::simAppProcessFinished,this, _1,_2)); - QObject::connect(process, &QProcess::errorOccurred, - std::bind(&IosSimulatorToolHandlerPrivate::simAppProcessError, this, _1)); - - appPId = pId; - gotInferiorPid(bundlePath,deviceId,pId); - - // For debug run, wait for the debugger to attach and then launch the app. - if (!debugRun) { - launchAppOnSimulator(); - } + auto onSimulatorStart = [this, extraArgs] (const SimulatorControl::ResponseData &response) { + if (isResponseValid(response)) + return; + if (response.success) { + spawnAppOnSimulator(extraArgs); } else { - errorMsg(IosToolHandler::tr("Spawning the Application process on Simulator failed.")); + errorMsg(IosToolHandler::tr("Application launch on Simulator failed. Simulator not running.") + .arg(bundlePath)); didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Failure); } - } else { - errorMsg(IosToolHandler::tr("Application launch on Simulator failed. Simulator not running.") - .arg(bundlePath)); - didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Failure); - } -} + }; -void IosSimulatorToolHandlerPrivate::launchAppOnSimulator() -{ - // Wait for the app to reach a state when we can launch it on the simulator. - if (appPId != -1 && SimulatorControl::waitForProcessSpawn(appPId)) { - QByteArray commandOutput; - Utils::FileName appBundle = Utils::FileName::fromString(bundlePath); - if (SimulatorControl::launchApp(deviceId, SimulatorControl::bundleIdentifier(appBundle), &commandOutput) != -1) { - appLaunched = true; - didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Success); - } else { - errorMsg(IosToolHandler::tr("Application launch on Simulator failed. %1") - .arg(QString::fromLocal8Bit(commandOutput))); - didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Failure); - } - } else { - errorMsg(IosToolHandler::tr("Spawning the Application process on Simulator failed. Spawning timed out.")); - didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Failure); - } + if (SimulatorControl::isSimulatorRunning(deviceId)) + spawnAppOnSimulator(extraArgs); + else + futureList << Utils::onResultReady(simCtl->startSimulator(deviceId), onSimulatorStart); } void IosSimulatorToolHandlerPrivate::requestDeviceInfo(const QString &deviceId, int timeout) @@ -799,19 +824,21 @@ void IosSimulatorToolHandlerPrivate::requestDeviceInfo(const QString &deviceId, void IosSimulatorToolHandlerPrivate::stop(int errorCode) { + if (process) { - if (isRunning()) { - process->terminate(); - if (!process->waitForFinished(1000)) - process->kill(); - } - process->deleteLater(); - process = nullptr; + QTC_ASSERT(process.unique(), process->kill(); qCDebug(toolHandlerLog)<<"App process is not unique."); + process.reset(); appPId = -1; appLaunched = false; } + foreach (auto f, futureList) { + if (!f.isFinished()) + f.cancel(); + } + toolExited(errorCode); + q->finished(q); } void IosSimulatorToolHandlerPrivate::debuggerStateChanged(Debugger::DebuggerState state) @@ -822,6 +849,112 @@ void IosSimulatorToolHandlerPrivate::debuggerStateChanged(Debugger::DebuggerStat } } +void IosSimulatorToolHandlerPrivate::installAppOnSimulator() +{ + auto onResponseAppInstall = [this](const SimulatorControl::ResponseData &response) { + if (!isResponseValid(response)) + return; + + if (response.success) { + isTransferringApp(bundlePath, deviceId, 100, 100, ""); + didTransferApp(bundlePath, deviceId, IosToolHandler::Success); + } else { + errorMsg(IosToolHandler::tr("Application install on Simulator failed. %1") + .arg(QString::fromLocal8Bit(response.commandOutput))); + didTransferApp(bundlePath, deviceId, IosToolHandler::Failure); + } + emit q->finished(q); + }; + + isTransferringApp(bundlePath, deviceId, 20, 100, ""); + futureList << Utils::onResultReady(simCtl->installApp(deviceId, Utils::FileName::fromString(bundlePath)), + onResponseAppInstall); +} + +void IosSimulatorToolHandlerPrivate::spawnAppOnSimulator(const QStringList &extraArgs) +{ + Utils::FileName appBundle = Utils::FileName::fromString(bundlePath); + bool debugRun = runKind == IosToolHandler::DebugRun; + futureList << Utils::onResultReady(simCtl->spawnAppProcess(deviceId, appBundle, debugRun, extraArgs), + std::bind(&IosSimulatorToolHandlerPrivate::onResponseAppSpawn, this, _1)); +} + +void IosSimulatorToolHandlerPrivate::launchAppOnSimulator() +{ + auto onResponseAppLaunch = [this](const SimulatorControl::ResponseData &response) { + if (!isResponseValid(response)) + return; + + if (response.pID != -1) { + appLaunched = true; + didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Success); + } else { + errorMsg(IosToolHandler::tr("Application launch on Simulator failed. %1") + .arg(QString::fromLocal8Bit(response.commandOutput))); + didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Failure); + stop(-1); + q->finished(q); + } + }; + + if (appPId != -1) { + Utils::FileName appBundle = Utils::FileName::fromString(bundlePath); + futureList << Utils::onResultReady(simCtl->launchApp(deviceId, + SimulatorControl::bundleIdentifier(appBundle), appPId), + onResponseAppLaunch); + } else { + errorMsg(IosToolHandler::tr("Spawning the Application process on Simulator failed. Spawning timed out.")); + didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Failure); + } +} + +bool IosSimulatorToolHandlerPrivate::isResponseValid(const SimulatorControl::ResponseData &responseData) +{ + if (responseData.simUdid.compare(deviceId) != 0) { + errorMsg(IosToolHandler::tr("Invalid simulator response. Device Id mismatch. " + "Device Id = %1 Response Id = %2") + .arg(responseData.simUdid) + .arg(deviceId)); + emit q->finished(q); + return false; + } + return true; +} + +void IosSimulatorToolHandlerPrivate::onResponseAppSpawn(const SimulatorControl::ResponseData &response) +{ + if (!isResponseValid(response)) + return; + + if (response.processInstance) { + QTC_ASSERT(!process || !isRunning(), + qCDebug(toolHandlerLog) << "Spwaning app while an app instance exits."); + process = response.processInstance; + QObject::connect(process.get(), &QProcess::readyReadStandardOutput, + std::bind(&IosSimulatorToolHandlerPrivate::simAppProcessHasData, this)); + QObject::connect(process.get(), &QProcess::readyReadStandardError, + std::bind(&IosSimulatorToolHandlerPrivate::simAppProcessHasErrorOutput, this)); + QObject::connect(process.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), + std::bind(&IosSimulatorToolHandlerPrivate::simAppProcessFinished, this, _1, _2)); + QObject::connect(process.get(), &QProcess::errorOccurred, + std::bind(&IosSimulatorToolHandlerPrivate::simAppProcessError, this, _1)); + + appPId = response.pID; + gotInferiorPid(bundlePath, deviceId, appPId); + + // For normal run. Launch app on Simulator. + // For debug run, wait for the debugger to attach and then launch the app. + if (runKind == IosToolHandler::NormalRun) + launchAppOnSimulator(); + } else { + errorMsg(IosToolHandler::tr("Spawning the Application process on Simulator failed. %1") + .arg(QString::fromLocal8Bit(response.commandOutput))); + didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Failure); + stop(-1); + q->finished(q); + } +} + void IosSimulatorToolHandlerPrivate::simAppProcessError(QProcess::ProcessError error) { errorMsg(IosToolHandler::tr("Simulator application process error %1").arg(error)); diff --git a/src/plugins/ios/simulatorcontrol.cpp b/src/plugins/ios/simulatorcontrol.cpp index 6746c27d789..4af05f00e3d 100644 --- a/src/plugins/ios/simulatorcontrol.cpp +++ b/src/plugins/ios/simulatorcontrol.cpp @@ -27,13 +27,16 @@ #include "iossimulator.h" #include "iosconfigurations.h" -#include <utils/runextensions.h> +#include "utils/runextensions.h" +#include "utils/qtcassert.h" +#include "utils/synchronousprocess.h" #ifdef Q_OS_MAC #include <CoreFoundation/CoreFoundation.h> #endif #include <chrono> +#include <memory> #include <QJsonArray> #include <QJsonDocument> @@ -47,6 +50,8 @@ #include <QUrl> #include <QWriteLocker> +using namespace std; + namespace { Q_LOGGING_CATEGORY(simulatorLog, "qtc.ios.simulator") } @@ -55,29 +60,61 @@ namespace Ios { namespace Internal { static int COMMAND_TIMEOUT = 10000; -static int SIMULATOR_TIMEOUT = 60000; +static int SIMULATOR_START_TIMEOUT = 60000; +static QString SIM_UDID_TAG = QStringLiteral("SimUdid"); -static bool checkForTimeout(const std::chrono::time_point< std::chrono::high_resolution_clock, std::chrono::nanoseconds> &start, int msecs = COMMAND_TIMEOUT) +static bool checkForTimeout(const chrono::time_point< chrono::high_resolution_clock, chrono::nanoseconds> &start, int msecs = COMMAND_TIMEOUT) { bool timedOut = false; - auto end = std::chrono::high_resolution_clock::now(); - if (std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count() > msecs) + auto end = chrono::high_resolution_clock::now(); + if (chrono::duration_cast<chrono::milliseconds>(end-start).count() > msecs) timedOut = true; return timedOut; } +static bool runCommand(QString command, const QStringList &args, QByteArray *output) +{ + Utils::SynchronousProcess p; + Utils::SynchronousProcessResponse resp = p.runBlocking(command, args); + if (output) + *output = resp.allRawOutput(); + return resp.result == Utils::SynchronousProcessResponse::Finished; +} + static QByteArray runSimCtlCommand(QStringList args) { - QProcess simCtlProcess; + QByteArray output; args.prepend(QStringLiteral("simctl")); - simCtlProcess.start(QStringLiteral("xcrun"), args, QProcess::ReadOnly); - if (!simCtlProcess.waitForFinished()) - qCDebug(simulatorLog) << "simctl command failed." << simCtlProcess.errorString(); - return simCtlProcess.readAll(); + runCommand(QStringLiteral("xcrun"), args, &output); + return output; +} + +static bool waitForProcessSpawn(qint64 processPId, QFutureInterface<SimulatorControl::ResponseData> &fi) +{ + bool success = false; + if (processPId != -1) { + // Wait for app to reach intruptible sleep state. + const QStringList args = {QStringLiteral("-p"), QString::number(processPId), + QStringLiteral("-o"), QStringLiteral("wq=")}; + int wqCount = -1; + QByteArray wqStr; + auto begin = chrono::high_resolution_clock::now(); + do { + if (fi.isCanceled() || !runCommand(QStringLiteral("ps"), args, &wqStr)) + break; + bool validInt = false; + wqCount = wqStr.trimmed().toInt(&validInt); + if (!validInt) + wqCount = -1; + } while (wqCount < 0 && !checkForTimeout(begin)); + success = wqCount >= 0; + } else { + qCDebug(simulatorLog) << "Wait for spawned failed. Invalid Process ID." << processPId; + } + return success; } -class SimulatorControlPrivate :QObject { - Q_OBJECT +class SimulatorControlPrivate { private: struct SimDeviceInfo { bool isBooted() const { return state.compare(QStringLiteral("Booted")) == 0; } @@ -89,27 +126,40 @@ private: QString sdk; }; - SimulatorControlPrivate(QObject *parent = nullptr); + SimulatorControlPrivate(); ~SimulatorControlPrivate(); - SimDeviceInfo deviceInfo(const QString &simUdid) const; - bool runCommand(QString command, const QStringList &args, QByteArray *output = nullptr); - QHash<QString, QProcess*> simulatorProcesses; - QReadWriteLock processDataLock; - QList<IosDeviceType> availableDevices; + static SimDeviceInfo deviceInfo(const QString &simUdid); + static QString bundleIdentifier(const Utils::FileName &bundlePath); + static QString bundleExecutable(const Utils::FileName &bundlePath); + + void startSimulator(QFutureInterface<SimulatorControl::ResponseData> &fi, const QString &simUdid); + void installApp(QFutureInterface<SimulatorControl::ResponseData> &fi, const QString &simUdid, + const Utils::FileName &bundlePath); + void spawnAppProcess(QFutureInterface<SimulatorControl::ResponseData> &fi, const QString &simUdid, + const Utils::FileName &bundlePath, bool waitForDebugger, QStringList extraArgs, + QThread *mainThread); + void launchApp(QFutureInterface<SimulatorControl::ResponseData> &fi, const QString &simUdid, + const QString &bundleIdentifier, qint64 spawnedPID); + + static QList<IosDeviceType> availableDevices; friend class SimulatorControl; }; -SimulatorControlPrivate *SimulatorControl::d = new SimulatorControlPrivate; - -SimulatorControl::SimulatorControl() +SimulatorControl::SimulatorControl(QObject *parent) : + QObject(parent), + d(new SimulatorControlPrivate) { +} +SimulatorControl::~SimulatorControl() +{ + delete d; } QList<Ios::Internal::IosDeviceType> SimulatorControl::availableSimulators() { - return d->availableDevices; + return SimulatorControlPrivate::availableDevices; } static QList<IosDeviceType> getAvailableSimulators() @@ -133,7 +183,7 @@ static QList<IosDeviceType> getAvailableSimulators() } } } - std::stable_sort(availableDevices.begin(), availableDevices.end()); + stable_sort(availableDevices.begin(), availableDevices.end()); } else { qCDebug(simulatorLog) << "Error parsing json output from simctl. Output:" << output; } @@ -142,92 +192,104 @@ static QList<IosDeviceType> getAvailableSimulators() void SimulatorControl::updateAvailableSimulators() { - QFuture<QList<IosDeviceType>> future = Utils::runAsync(getAvailableSimulators); - Utils::onResultReady(future, d, [](const QList<IosDeviceType> &devices) { - SimulatorControl::d->availableDevices = devices; + QFuture< QList<IosDeviceType> > future = Utils::runAsync(getAvailableSimulators); + Utils::onResultReady(future, [](const QList<IosDeviceType> &devices) { + SimulatorControlPrivate::availableDevices = devices; }); } -// Blocks until simulators reaches "Booted" state. -bool SimulatorControl::startSimulator(const QString &simUdid) +bool SimulatorControl::isSimulatorRunning(const QString &simUdid) { - QWriteLocker locker(&d->processDataLock); - bool simulatorRunning = isSimulatorRunning(simUdid); - if (!simulatorRunning && d->deviceInfo(simUdid).isAvailable()) { - // Simulator is not running but it's available. Start the simulator. - QProcess *p = new QProcess; - QObject::connect(p, static_cast<void(QProcess::*)(int)>(&QProcess::finished), [simUdid]() { - QWriteLocker locker(&d->processDataLock); - d->simulatorProcesses[simUdid]->deleteLater(); - d->simulatorProcesses.remove(simUdid); - }); + if (simUdid.isEmpty()) + return false; + return SimulatorControlPrivate::deviceInfo(simUdid).isBooted(); +} - const QString cmd = IosConfigurations::developerPath().appendPath(QStringLiteral("/Applications/Simulator.app")).toString(); - const QStringList args({QStringLiteral("--args"), QStringLiteral("-CurrentDeviceUDID"), simUdid}); - p->start(cmd, args); +QString SimulatorControl::bundleIdentifier(const Utils::FileName &bundlePath) +{ + return SimulatorControlPrivate::bundleIdentifier(bundlePath); +} - if (p->waitForStarted()) { - d->simulatorProcesses[simUdid] = p; - // At this point the sim device exists, available and was not running. - // So the simulator is started and we'll wait for it to reach to a state - // where we can interact with it. - auto start = std::chrono::high_resolution_clock::now(); - SimulatorControlPrivate::SimDeviceInfo info; - do { - info = d->deviceInfo(simUdid); - } while (!info.isBooted() - && p->state() == QProcess::Running - && !checkForTimeout(start, SIMULATOR_TIMEOUT)); - simulatorRunning = info.isBooted(); - } else { - qCDebug(simulatorLog) << "Error starting simulator." << p->errorString(); - delete p; - } - } - return simulatorRunning; +QString SimulatorControl::bundleExecutable(const Utils::FileName &bundlePath) +{ + return SimulatorControlPrivate::bundleExecutable(bundlePath); } -bool SimulatorControl::isSimulatorRunning(const QString &simUdid) +QFuture<SimulatorControl::ResponseData> SimulatorControl::startSimulator(const QString &simUdid) { - if (simUdid.isEmpty()) - return false; - return d->deviceInfo(simUdid).isBooted(); + return Utils::runAsync(&SimulatorControlPrivate::startSimulator, d, simUdid); } -bool SimulatorControl::installApp(const QString &simUdid, const Utils::FileName &bundlePath, QByteArray &commandOutput) +QFuture<SimulatorControl::ResponseData> +SimulatorControl::installApp(const QString &simUdid, const Utils::FileName &bundlePath) const { - bool installed = false; - if (isSimulatorRunning(simUdid)) { - commandOutput = runSimCtlCommand(QStringList() << QStringLiteral("install") << simUdid << bundlePath.toString()); - installed = commandOutput.isEmpty(); - } else { - commandOutput = "Simulator device not running."; - } - return installed; + return Utils::runAsync(&SimulatorControlPrivate::installApp, d, simUdid, bundlePath); +} + +QFuture<SimulatorControl::ResponseData> +SimulatorControl::spawnAppProcess(const QString &simUdid, const Utils::FileName &bundlePath, + bool waitForDebugger, const QStringList &extraArgs) const +{ + return Utils::runAsync(&SimulatorControlPrivate::spawnAppProcess, d, simUdid, bundlePath, + waitForDebugger, extraArgs, QThread::currentThread()); +} + +QFuture<SimulatorControl::ResponseData> +SimulatorControl::launchApp(const QString &simUdid, const QString &bundleIdentifier, + qint64 spawnedPID) const +{ + return Utils::runAsync(&SimulatorControlPrivate::launchApp, d, simUdid, + bundleIdentifier, spawnedPID); +} + +QList<IosDeviceType> SimulatorControlPrivate::availableDevices; + +SimulatorControlPrivate::SimulatorControlPrivate() +{ +} + +SimulatorControlPrivate::~SimulatorControlPrivate() +{ + } -qint64 SimulatorControl::launchApp(const QString &simUdid, const QString &bundleIdentifier, QByteArray* commandOutput) +SimulatorControlPrivate::SimDeviceInfo SimulatorControlPrivate::deviceInfo(const QString &simUdid) { - qint64 pId = -1; - pId = -1; - if (!bundleIdentifier.isEmpty() && isSimulatorRunning(simUdid)) { - const QStringList args({QStringLiteral("launch"), simUdid , bundleIdentifier}); - const QByteArray output = runSimCtlCommand(args); - const QByteArray pIdStr = output.trimmed().split(' ').last().trimmed(); - bool validInt = false; - pId = pIdStr.toLongLong(&validInt); - if (!validInt) { - // Launch Failed. - qCDebug(simulatorLog) << "Launch app failed. Process id returned is not valid. PID =" << pIdStr; - pId = -1; - if (commandOutput) - *commandOutput = output; + SimDeviceInfo info; + bool found = false; + if (!simUdid.isEmpty()) { + const QByteArray output = runSimCtlCommand({QLatin1String("list"), QLatin1String("-j"), QLatin1String("devices")}); + QJsonDocument doc = QJsonDocument::fromJson(output); + if (!doc.isNull()) { + const QJsonObject buildInfo = doc.object().value(QStringLiteral("devices")).toObject(); + foreach (const QString &buildVersion, buildInfo.keys()) { + QJsonArray devices = buildInfo.value(buildVersion).toArray(); + foreach (const QJsonValue device, devices) { + QJsonObject deviceInfo = device.toObject(); + QString deviceUdid = deviceInfo.value(QStringLiteral("udid")).toString(); + if (deviceUdid.compare(simUdid) == 0) { + found = true; + info.name = deviceInfo.value(QStringLiteral("name")).toString(); + info.udid = deviceUdid; + info.state = deviceInfo.value(QStringLiteral("state")).toString(); + info.sdk = buildVersion; + info.availability = deviceInfo.value(QStringLiteral("availability")).toString(); + break; + } + } + if (found) + break; + } + } else { + qCDebug(simulatorLog) << "Cannot find device info. Error parsing json output from simctl. Output:" << output; } + } else { + qCDebug(simulatorLog) << "Cannot find device info. Invalid UDID."; } - return pId; + return info; } -QString SimulatorControl::bundleIdentifier(const Utils::FileName &bundlePath) +QString SimulatorControlPrivate::bundleIdentifier(const Utils::FileName &bundlePath) { QString bundleID; #ifdef Q_OS_MAC @@ -247,7 +309,7 @@ QString SimulatorControl::bundleIdentifier(const Utils::FileName &bundlePath) return bundleID; } -QString SimulatorControl::bundleExecutable(const Utils::FileName &bundlePath) +QString SimulatorControlPrivate::bundleExecutable(const Utils::FileName &bundlePath) { QString executable; #ifdef Q_OS_MAC @@ -266,160 +328,169 @@ QString SimulatorControl::bundleExecutable(const Utils::FileName &bundlePath) return executable; } -SimulatorControlPrivate::SimulatorControlPrivate(QObject *parent): - QObject(parent), - processDataLock(QReadWriteLock::Recursive) +void SimulatorControlPrivate::startSimulator(QFutureInterface<SimulatorControl::ResponseData> &fi, + const QString &simUdid) { + SimulatorControl::ResponseData response(simUdid); + if (deviceInfo(simUdid).isAvailable()) { + // Simulator is available. + const QString cmd = IosConfigurations::developerPath() + .appendPath(QStringLiteral("/Applications/Simulator.app/Contents/MacOS/Simulator")) + .toString(); + const QStringList args({QStringLiteral("--args"), QStringLiteral("-CurrentDeviceUDID"), simUdid}); + + if (QProcess::startDetached(cmd, args)) { + if (fi.isCanceled()) + return; + // At this point the sim device exists, available and was not running. + // So the simulator is started and we'll wait for it to reach to a state + // where we can interact with it. + auto start = chrono::high_resolution_clock::now(); + SimulatorControlPrivate::SimDeviceInfo info; + do { + info = deviceInfo(simUdid); + if (fi.isCanceled()) + return; + } while (!info.isBooted() + && !checkForTimeout(start, SIMULATOR_START_TIMEOUT)); + if (info.isBooted()) { + response.success = true; + } + } else { + qCDebug(simulatorLog) << "Error starting simulator."; + } + } + + if (!fi.isCanceled()) { + fi.reportResult(response); + } } -SimulatorControlPrivate::~SimulatorControlPrivate() +void SimulatorControlPrivate::installApp(QFutureInterface<SimulatorControl::ResponseData> &fi, + const QString &simUdid, const Utils::FileName &bundlePath) { - + QTC_CHECK(bundlePath.exists()); + QByteArray output = runSimCtlCommand({QStringLiteral("install"), simUdid, bundlePath.toString()}); + SimulatorControl::ResponseData response(simUdid); + response.success = output.isEmpty(); + response.commandOutput = output; + + if (!fi.isCanceled()) { + fi.reportResult(response); + } } -// The simctl spawns the process and returns the pId but the application process might not have started, at least in a state where you can interrupt it. -// Use SimulatorControl::waitForProcessSpawn to be sure. -QProcess *SimulatorControl::spawnAppProcess(const QString &simUdid, const Utils::FileName &bundlePath, qint64 &pId, bool waitForDebugger, const QStringList &extraArgs) +void SimulatorControlPrivate::spawnAppProcess(QFutureInterface<SimulatorControl::ResponseData> &fi, + const QString &simUdid, const Utils::FileName &bundlePath, + bool waitForDebugger, QStringList extraArgs, QThread *mainThread) { - QProcess *simCtlProcess = nullptr; - if (isSimulatorRunning(simUdid)) { - QString bundleId = bundleIdentifier(bundlePath); - QString executableName = bundleExecutable(bundlePath); - QByteArray appPath = runSimCtlCommand(QStringList() << QStringLiteral("get_app_container") << simUdid << bundleId).trimmed(); - if (!appPath.isEmpty() && !executableName.isEmpty()) { - // Spawn the app. The spawned app is started in suspended mode. - appPath.append('/' + executableName.toLocal8Bit()); - simCtlProcess = new QProcess; - QStringList args; - args << QStringLiteral("simctl"); - args << QStringLiteral("spawn"); - if (waitForDebugger) - args << QStringLiteral("-w"); - args << simUdid; - args << QString::fromLocal8Bit(appPath); - args << extraArgs; - simCtlProcess->start(QStringLiteral("xcrun"), args); - if (!simCtlProcess->waitForStarted()){ - // Spawn command failed. - qCDebug(simulatorLog) << "Spawning the app failed." << simCtlProcess->errorString(); - delete simCtlProcess; - simCtlProcess = nullptr; + SimulatorControl::ResponseData response(simUdid); + + // Find the path of the installed app. + QString bundleId = bundleIdentifier(bundlePath); + QByteArray appContainer = runSimCtlCommand({QStringLiteral("get_app_container"), simUdid, bundleId}); + QString appPath = QString::fromLocal8Bit(appContainer.trimmed()); + + if (fi.isCanceled()) + return; + + QString executableName = bundleExecutable(bundlePath); + if (!appPath.isEmpty() && !executableName.isEmpty()) { + appPath.append('/' + executableName); + QStringList args = {QStringLiteral("simctl"), QStringLiteral("spawn"), simUdid, appPath}; + if (waitForDebugger) + args.insert(2, QStringLiteral("-w")); + args << extraArgs; + + // Spawn the app. The spawned app is started in suspended mode. + shared_ptr<QProcess> simCtlProcess(new QProcess, [](QProcess *p) { + if (p->state() != QProcess::NotRunning) { + p->kill(); + p->waitForFinished(COMMAND_TIMEOUT); } - - // Find the process id of the the app process. - if (simCtlProcess) { - qint64 simctlPId = simCtlProcess->processId(); - pId = -1; - QByteArray commandOutput; - QStringList pGrepArgs; - pGrepArgs << QStringLiteral("-f") << QString::fromLocal8Bit(appPath); - auto begin = std::chrono::high_resolution_clock::now(); - // Find the pid of the spawned app. - while (pId == -1 && d->runCommand(QStringLiteral("pgrep"), pGrepArgs, &commandOutput)) { - foreach (auto pidStr, commandOutput.trimmed().split('\n')) { - qint64 parsedPId = pidStr.toLongLong(); - if (parsedPId != simctlPId) - pId = parsedPId; - } - if (checkForTimeout(begin)) { - qCDebug(simulatorLog) << "Spawning the app failed. Process timed out"; - break; - } + delete p; + }); + simCtlProcess->start(QStringLiteral("xcrun"), args); + if (simCtlProcess->waitForStarted()) { + if (fi.isCanceled()) + return; + // Find the process id of the spawned app. + qint64 simctlPId = simCtlProcess->processId(); + QByteArray commandOutput; + const QStringList pGrepArgs = {QStringLiteral("-f"), appPath}; + auto begin = chrono::high_resolution_clock::now(); + int processID = -1; + while (processID == -1 && runCommand(QStringLiteral("pgrep"), pGrepArgs, &commandOutput)) { + if (fi.isCanceled()) { + qCDebug(simulatorLog) <<"Spawning the app failed. Future cancelled."; + return; + } + foreach (auto pidStr, commandOutput.trimmed().split('\n')) { + qint64 parsedPId = pidStr.toLongLong(); + if (parsedPId != simctlPId) + processID = parsedPId; + } + if (checkForTimeout(begin)) { + qCDebug(simulatorLog) << "Spawning the app failed. Process timed out"; + break; } } - if (pId == -1) { - // App process id can't be found. - qCDebug(simulatorLog) << "Spawning the app failed. PID not found."; - delete simCtlProcess; - simCtlProcess = nullptr; + if (processID == -1) { + qCDebug(simulatorLog) << "Spawning the app failed. App PID not found."; + simCtlProcess->waitForReadyRead(COMMAND_TIMEOUT); + response.commandOutput = simCtlProcess->readAllStandardError(); + } else { + response.processInstance = simCtlProcess; + response.processInstance->moveToThread(mainThread); + response.pID = processID; + response.success = true; } } else { - qCDebug(simulatorLog) << "Spawning the app failed. Check installed app." << appPath; + qCDebug(simulatorLog) << "Spawning the app failed." << simCtlProcess->errorString(); + response.commandOutput = simCtlProcess->errorString().toLatin1(); } } else { - qCDebug(simulatorLog) << "Spawning the app failed. Simulator not running." << simUdid; + qCDebug(simulatorLog) << "Spawning the app failed. Check installed app." << appPath; } - return simCtlProcess; -} -bool SimulatorControl::waitForProcessSpawn(qint64 processPId) -{ - bool success = true; - if (processPId != -1) { - // Wait for app to reach intruptible sleep state. - QByteArray wqStr; - QStringList args; - int wqCount = -1; - args << QStringLiteral("-p") << QString::number(processPId) << QStringLiteral("-o") << QStringLiteral("wq="); - auto begin = std::chrono::high_resolution_clock::now(); - do { - if (!d->runCommand(QStringLiteral("ps"), args, &wqStr)) { - success = false; - break; - } - bool validInt = false; - wqCount = wqStr.toInt(&validInt); - if (!validInt) { - wqCount = -1; - } - } while (wqCount < 0 && !checkForTimeout(begin)); - success = wqCount >= 0; - } else { - qCDebug(simulatorLog) << "Wait for spawned failed. Invalid Process ID." << processPId; + if (!fi.isCanceled()) { + fi.reportResult(response); } - return success; } -SimulatorControlPrivate::SimDeviceInfo SimulatorControlPrivate::deviceInfo(const QString &simUdid) const +void SimulatorControlPrivate::launchApp(QFutureInterface<SimulatorControl::ResponseData> &fi, + const QString &simUdid, const QString &bundleIdentifier, + qint64 spawnedPID) { - SimDeviceInfo info; - bool found = false; - if (!simUdid.isEmpty()) { - // It might happend that the simulator is not started by SimControl. - // Check of intances started externally. - const QByteArray output = runSimCtlCommand({QLatin1String("list"), QLatin1String("-j"), QLatin1String("devices")}); - QJsonDocument doc = QJsonDocument::fromJson(output); - if (!doc.isNull()) { - const QJsonObject buildInfo = doc.object().value(QStringLiteral("devices")).toObject(); - foreach (const QString &buildVersion, buildInfo.keys()) { - QJsonArray devices = buildInfo.value(buildVersion).toArray(); - foreach (const QJsonValue device, devices) { - QJsonObject deviceInfo = device.toObject(); - QString deviceUdid = deviceInfo.value(QStringLiteral("udid")).toString(); - if (deviceUdid.compare(simUdid) == 0) { - found = true; - info.name = deviceInfo.value(QStringLiteral("name")).toString(); - info.udid = deviceUdid; - info.state = deviceInfo.value(QStringLiteral("state")).toString(); - info.sdk = buildVersion; - info.availability = deviceInfo.value(QStringLiteral("availability")).toString(); - break; - } - } - if (found) - break; + SimulatorControl::ResponseData response(simUdid); + if (!bundleIdentifier.isEmpty()) { + bool processSpawned = true; + // Wait for the process to be spawned properly before launching app. + if (spawnedPID > -1) + processSpawned = waitForProcessSpawn(spawnedPID, fi); + + if (fi.isCanceled()) + return; + + if (processSpawned) { + const QStringList args({QStringLiteral("launch"), simUdid , bundleIdentifier}); + response.commandOutput = runSimCtlCommand(args); + const QByteArray pIdStr = response.commandOutput.trimmed().split(' ').last().trimmed(); + bool validInt = false; + response.pID = pIdStr.toLongLong(&validInt); + if (!validInt) { + // Launch Failed. + qCDebug(simulatorLog) << "Launch app failed. Process id returned is not valid. PID =" << pIdStr; + response.pID = -1; } - } else { - qCDebug(simulatorLog) << "Cannot find device info. Error parsing json output from simctl. Output:" << output; } - } else { - qCDebug(simulatorLog) << "Cannot find device info. Invalid UDID."; } - return info; -} -bool SimulatorControlPrivate::runCommand(QString command, const QStringList &args, QByteArray *output) -{ - bool success = false; - QProcess process; - process.start(command, args); - success = process.waitForFinished(); - if (output) - *output = process.readAll().trimmed(); - return success; + if (!fi.isCanceled()) { + fi.reportResult(response); + } } } // namespace Internal } // namespace Ios -#include "simulatorcontrol.moc" diff --git a/src/plugins/ios/simulatorcontrol.h b/src/plugins/ios/simulatorcontrol.h index 1a674d644eb..4680350c48b 100644 --- a/src/plugins/ios/simulatorcontrol.h +++ b/src/plugins/ios/simulatorcontrol.h @@ -22,10 +22,10 @@ ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ -#ifndef SIMULATORCONTROL_H -#define SIMULATORCONTROL_H +#pragma once -#include <QHash> +#include <QObject> +#include <QFuture> #include "utils/fileutils.h" QT_BEGIN_NAMESPACE @@ -38,29 +38,44 @@ namespace Internal { class IosDeviceType; class SimulatorControlPrivate; -class SimulatorControl +class SimulatorControl : public QObject { - explicit SimulatorControl(); + Q_OBJECT +public: + struct ResponseData { + ResponseData(const QString & udid): + simUdid(udid) { } + + QString simUdid; + bool success = false; + qint64 pID = -1; + QByteArray commandOutput = ""; + // For response type APP_SPAWN, the processInstance represents the control process of the spwaned app + // For other response types its null. + std::shared_ptr<QProcess> processInstance; + }; + +public: + explicit SimulatorControl(QObject* parent = nullptr); + ~SimulatorControl(); public: static QList<IosDeviceType> availableSimulators(); static void updateAvailableSimulators(); - - static bool startSimulator(const QString &simUdid); static bool isSimulatorRunning(const QString &simUdid); - - static bool installApp(const QString &simUdid, const Utils::FileName &bundlePath, QByteArray &commandOutput); - static QProcess* spawnAppProcess(const QString &simUdid, const Utils::FileName &bundlePath, qint64 &pId, - bool waitForDebugger, const QStringList &extraArgs); - - static qint64 launchApp(const QString &simUdid, const QString &bundleIdentifier, QByteArray *commandOutput = nullptr); static QString bundleIdentifier(const Utils::FileName &bundlePath); static QString bundleExecutable(const Utils::FileName &bundlePath); - static bool waitForProcessSpawn(qint64 processPId); + +public: + QFuture<ResponseData> startSimulator(const QString &simUdid); + QFuture<ResponseData> installApp(const QString &simUdid, const Utils::FileName &bundlePath) const; + QFuture<ResponseData> spawnAppProcess(const QString &simUdid, const Utils::FileName &bundlePath, + bool waitForDebugger, const QStringList &extraArgs) const; + QFuture<ResponseData> launchApp(const QString &simUdid, const QString &bundleIdentifier, + qint64 spawnedPID = -1) const; private: - static SimulatorControlPrivate *d; + SimulatorControlPrivate *d; }; } // namespace Internal } // namespace Ios -#endif // SIMULATORCONTROL_H |