From 54677f498565d22df5ba0485cd1f05af3fb14b42 Mon Sep 17 00:00:00 2001 From: Vikas Pachdha Date: Wed, 21 Dec 2016 18:28:42 +0100 Subject: [PATCH] iOS: Capture console output of launched app on iOS simulator Task-number: QTCREATORBUG-17483 Change-Id: Id18c51e20cf8b396fc610918610f04d39ead28b0 Reviewed-by: Eike Ziller --- src/plugins/ios/iostoolhandler.cpp | 146 ++++++++++++++++++++------- src/plugins/ios/simulatorcontrol.cpp | 18 +++- src/plugins/ios/simulatorcontrol.h | 4 +- 3 files changed, 125 insertions(+), 43 deletions(-) diff --git a/src/plugins/ios/iostoolhandler.cpp b/src/plugins/ios/iostoolhandler.cpp index b12df9be16c..b1c1773cfc1 100644 --- a/src/plugins/ios/iostoolhandler.cpp +++ b/src/plugins/ios/iostoolhandler.cpp @@ -37,16 +37,20 @@ #include "utils/synchronousprocess.h" #include +#include #include +#include #include #include #include #include #include +#include #include #include #include #include +#include #include #include @@ -61,6 +65,68 @@ namespace Internal { using namespace std::placeholders; +// As per the currrent behavior, any absolute path given to simctl --stdout --stderr where the +// directory after the root also exists on the simulator's file system will map to +// simulator's file system i.e. simctl translates $TMPDIR/somwhere/out.txt to +// your_home_dir/Library/Developer/CoreSimulator/Devices/data/$TMP_DIR/somwhere/out.txt. +// Because /var also exists on simulator's file system. +// Though the log files located at CONSOLE_PATH_TEMPLATE are deleted on +// app exit any leftovers shall be removed on simulator restart. +static QString CONSOLE_PATH_TEMPLATE = QDir::homePath() + + "/Library/Developer/CoreSimulator/Devices/%1/data/tmp/%2"; + +class LogTailFiles : public QObject +{ + Q_OBJECT +public: + + void exec(QFutureInterface &fi, std::shared_ptr stdoutFile, + std::shared_ptr stderrFile) + { + if (fi.isCanceled()) + return; + + // The future is canceled when app on simulator is stoped. + QEventLoop loop; + QFutureWatcher watcher; + connect(&watcher, &QFutureWatcher::canceled, [&](){ + loop.quit(); + }); + watcher.setFuture(fi.future()); + + // Process to print the console output while app is running. + auto logProcess = [this, fi](QProcess *tailProcess, std::shared_ptr file) { + QObject::connect(tailProcess, &QProcess::readyReadStandardOutput, [=]() { + if (!fi.isCanceled()) + emit logMessage(QString::fromLocal8Bit(tailProcess->readAll())); + }); + tailProcess->start(QStringLiteral("tail"), QStringList() << "-f" << file->fileName()); + }; + + auto processDeleter = [](QProcess *process) { + if (process->state() != QProcess::NotRunning) { + process->terminate(); + process->waitForFinished(); + } + delete process; + }; + + std::unique_ptr tailStdout(new QProcess, processDeleter); + if (stdoutFile) + logProcess(tailStdout.get(), stdoutFile); + + std::unique_ptr tailStderr(new QProcess, processDeleter); + if (stderrFile) + logProcess(tailStderr.get(), stderrFile); + + // Blocks untill tool is deleted or toolexited is called. + loop.exec(); + } + +signals: + void logMessage(QString message); +}; + struct ParserState { enum Kind { Msg, @@ -256,14 +322,9 @@ private: void launchAppOnSimulator(const QStringList &extraArgs); bool isResponseValid(const SimulatorControl::ResponseData &responseData); - void simAppProcessError(QProcess::ProcessError error); - void simAppProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); - void simAppProcessHasData(); - void simAppProcessHasErrorOutput(); - private: - qint64 appPId = -1; SimulatorControl *simCtl; + LogTailFiles outputLogger; QList> futureList; }; @@ -727,6 +788,8 @@ IosSimulatorToolHandlerPrivate::IosSimulatorToolHandlerPrivate(const IosDeviceTy : IosToolHandlerPrivate(devType, q), simCtl(new SimulatorControl) { + QObject::connect(&outputLogger, &LogTailFiles::logMessage, + std::bind(&IosToolHandlerPrivate::appOutput, this, _1)); } IosSimulatorToolHandlerPrivate::~IosSimulatorToolHandlerPrivate() @@ -809,7 +872,6 @@ void IosSimulatorToolHandlerPrivate::requestDeviceInfo(const QString &deviceId, void IosSimulatorToolHandlerPrivate::stop(int errorCode) { - appPId = -1; foreach (auto f, futureList) { if (!f.isFinished()) f.cancel(); @@ -843,6 +905,31 @@ void IosSimulatorToolHandlerPrivate::installAppOnSimulator() void IosSimulatorToolHandlerPrivate::launchAppOnSimulator(const QStringList &extraArgs) { + const Utils::FileName appBundle = Utils::FileName::fromString(bundlePath); + const QString bundleId = SimulatorControl::bundleIdentifier(appBundle); + const bool debugRun = runKind == IosToolHandler::DebugRun; + bool captureConsole = IosConfigurations::xcodeVersion() >= QVersionNumber(8); + std::shared_ptr stdoutFile; + std::shared_ptr stderrFile; + + if (captureConsole) { + const QString fileTemplate = CONSOLE_PATH_TEMPLATE.arg(deviceId).arg(bundleId); + stdoutFile.reset(new QTemporaryFile); + stdoutFile->setFileTemplate(fileTemplate + QStringLiteral(".stdout")); + + stderrFile.reset(new QTemporaryFile); + stderrFile->setFileTemplate(fileTemplate + QStringLiteral(".stderr")); + + captureConsole = stdoutFile->open() && stderrFile->open(); + if (!captureConsole) + errorMsg(IosToolHandler::tr("Cannot capture console output from %1. " + "Error redirecting output to %2.*") + .arg(bundleId).arg(fileTemplate)); + } else { + errorMsg(IosToolHandler::tr("Cannot capture console output from %1. " + "Install Xcode 8 or later.").arg(bundleId)); + } + auto monitorPid = [this](QFutureInterface &fi, qint64 pid) { int exitCode = 0; const QStringList args({QStringLiteral("-0"), QString::number(pid)}); @@ -858,15 +945,17 @@ void IosSimulatorToolHandlerPrivate::launchAppOnSimulator(const QStringList &ext stop(0); }; - auto onResponseAppLaunch = [this, monitorPid](const SimulatorControl::ResponseData &response) { + auto onResponseAppLaunch = [=](const SimulatorControl::ResponseData &response) { if (!isResponseValid(response)) return; if (response.success) { - appPId = response.pID; - gotInferiorPid(bundlePath, deviceId, appPId); + gotInferiorPid(bundlePath, deviceId, response.pID); didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Success); // Start monitoring app's life signs. - futureList << Utils::runAsync(monitorPid, appPId); + futureList << Utils::runAsync(monitorPid, response.pID); + if (captureConsole) + futureList << Utils::runAsync(&LogTailFiles::exec, &outputLogger, stdoutFile, + stderrFile); } else { errorMsg(IosToolHandler::tr("Application launch on Simulator failed. %1") .arg(QString::fromLocal8Bit(response.commandOutput))); @@ -875,11 +964,12 @@ void IosSimulatorToolHandlerPrivate::launchAppOnSimulator(const QStringList &ext q->finished(q); } }; - Utils::FileName appBundle = Utils::FileName::fromString(bundlePath); - futureList << Utils::onResultReady(simCtl->launchApp(deviceId, - SimulatorControl::bundleIdentifier(appBundle), - runKind == IosToolHandler::DebugRun, - extraArgs), onResponseAppLaunch); + + futureList << Utils::onResultReady( + simCtl->launchApp(deviceId, bundleId, debugRun, extraArgs, + captureConsole ? stdoutFile->fileName() : QString(), + captureConsole ? stderrFile->fileName() : QString()), + onResponseAppLaunch); } bool IosSimulatorToolHandlerPrivate::isResponseValid(const SimulatorControl::ResponseData &responseData) @@ -895,28 +985,6 @@ bool IosSimulatorToolHandlerPrivate::isResponseValid(const SimulatorControl::Res return true; } -void IosSimulatorToolHandlerPrivate::simAppProcessError(QProcess::ProcessError error) -{ - errorMsg(IosToolHandler::tr("Simulator application process error %1").arg(error)); -} - -void IosSimulatorToolHandlerPrivate::simAppProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) -{ - stop((exitStatus == QProcess::NormalExit) ? exitCode : -1 ); - qCDebug(toolHandlerLog) << "IosToolHandler::finished(" << this << ")"; - q->finished(q); -} - -void IosSimulatorToolHandlerPrivate::simAppProcessHasData() -{ - appOutput(QString::fromLocal8Bit(process->readAllStandardOutput())); -} - -void IosSimulatorToolHandlerPrivate::simAppProcessHasErrorOutput() -{ - errorMsg(QString::fromLocal8Bit(process->readAllStandardError())); -} - void IosToolHandlerPrivate::killProcess() { if (isRunning()) @@ -973,3 +1041,5 @@ bool IosToolHandler::isRunning() } } // namespace Ios + +#include "iostoolhandler.moc" diff --git a/src/plugins/ios/simulatorcontrol.cpp b/src/plugins/ios/simulatorcontrol.cpp index 005ef882b59..232e3d01ceb 100644 --- a/src/plugins/ios/simulatorcontrol.cpp +++ b/src/plugins/ios/simulatorcontrol.cpp @@ -108,7 +108,8 @@ private: const Utils::FileName &bundlePath); void launchApp(QFutureInterface &fi, const QString &simUdid, const QString &bundleIdentifier, bool waitForDebugger, - const QStringList &extraArgs); + const QStringList &extraArgs, const QString &stdoutPath, + const QString &stderrPath); static QList availableDevices; friend class SimulatorControl; @@ -196,10 +197,11 @@ SimulatorControl::installApp(const QString &simUdid, const Utils::FileName &bund QFuture SimulatorControl::launchApp(const QString &simUdid, const QString &bundleIdentifier, - bool waitForDebugger, const QStringList &extraArgs) const + bool waitForDebugger, const QStringList &extraArgs, + const QString &stdoutPath, const QString &stderrPath) const { return Utils::runAsync(&SimulatorControlPrivate::launchApp, d, simUdid, bundleIdentifier, - waitForDebugger, extraArgs); + waitForDebugger, extraArgs, stdoutPath, stderrPath); } QList SimulatorControlPrivate::availableDevices; @@ -342,12 +344,20 @@ void SimulatorControlPrivate::installApp(QFutureInterface &fi, const QString &simUdid, const QString &bundleIdentifier, - bool waitForDebugger, const QStringList &extraArgs) + bool waitForDebugger, const QStringList &extraArgs, + const QString &stdoutPath, const QString &stderrPath) { SimulatorControl::ResponseData response(simUdid); if (!bundleIdentifier.isEmpty() && !fi.isCanceled()) { QStringList args({QStringLiteral("launch"), simUdid, bundleIdentifier}); + // simctl usage documentation : Note: Log output is often directed to stderr, not stdout. + if (!stdoutPath.isEmpty()) + args.insert(1, QStringLiteral("--stderr=%1").arg(stdoutPath)); + + if (!stderrPath.isEmpty()) + args.insert(1, QStringLiteral("--stdout=%1").arg(stderrPath)); + if (waitForDebugger) args.insert(1, QStringLiteral("-w")); diff --git a/src/plugins/ios/simulatorcontrol.h b/src/plugins/ios/simulatorcontrol.h index 5082a437dad..e82cc5afad1 100644 --- a/src/plugins/ios/simulatorcontrol.h +++ b/src/plugins/ios/simulatorcontrol.h @@ -69,7 +69,9 @@ public: QFuture startSimulator(const QString &simUdid) const; QFuture installApp(const QString &simUdid, const Utils::FileName &bundlePath) const; QFuture launchApp(const QString &simUdid, const QString &bundleIdentifier, - bool waitForDebugger, const QStringList &extraArgs) const; + bool waitForDebugger, const QStringList &extraArgs, + const QString& stdoutPath = QString(), + const QString& stderrPath = QString()) const; private: SimulatorControlPrivate *d;