iOS: Capture console output of launched app on iOS simulator

Task-number: QTCREATORBUG-17483
Change-Id: Id18c51e20cf8b396fc610918610f04d39ead28b0
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Vikas Pachdha
2016-12-21 18:28:42 +01:00
parent fe0a091802
commit 54677f4985
3 changed files with 125 additions and 43 deletions

View File

@@ -37,16 +37,20 @@
#include "utils/synchronousprocess.h" #include "utils/synchronousprocess.h"
#include <QCoreApplication> #include <QCoreApplication>
#include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QFutureWatcher>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QList> #include <QList>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QPointer>
#include <QProcess> #include <QProcess>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QScopedArrayPointer> #include <QScopedArrayPointer>
#include <QSocketNotifier> #include <QSocketNotifier>
#include <QTemporaryFile>
#include <QTimer> #include <QTimer>
#include <QXmlStreamReader> #include <QXmlStreamReader>
@@ -61,6 +65,68 @@ namespace Internal {
using namespace std::placeholders; 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<void> &fi, std::shared_ptr<QTemporaryFile> stdoutFile,
std::shared_ptr<QTemporaryFile> stderrFile)
{
if (fi.isCanceled())
return;
// The future is canceled when app on simulator is stoped.
QEventLoop loop;
QFutureWatcher<void> watcher;
connect(&watcher, &QFutureWatcher<void>::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<QTemporaryFile> 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<QProcess, void(*)(QProcess *)> tailStdout(new QProcess, processDeleter);
if (stdoutFile)
logProcess(tailStdout.get(), stdoutFile);
std::unique_ptr<QProcess, void(*)(QProcess *)> 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 { struct ParserState {
enum Kind { enum Kind {
Msg, Msg,
@@ -256,14 +322,9 @@ private:
void launchAppOnSimulator(const QStringList &extraArgs); void launchAppOnSimulator(const QStringList &extraArgs);
bool isResponseValid(const SimulatorControl::ResponseData &responseData); bool isResponseValid(const SimulatorControl::ResponseData &responseData);
void simAppProcessError(QProcess::ProcessError error);
void simAppProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
void simAppProcessHasData();
void simAppProcessHasErrorOutput();
private: private:
qint64 appPId = -1;
SimulatorControl *simCtl; SimulatorControl *simCtl;
LogTailFiles outputLogger;
QList<QFuture<void>> futureList; QList<QFuture<void>> futureList;
}; };
@@ -727,6 +788,8 @@ IosSimulatorToolHandlerPrivate::IosSimulatorToolHandlerPrivate(const IosDeviceTy
: IosToolHandlerPrivate(devType, q), : IosToolHandlerPrivate(devType, q),
simCtl(new SimulatorControl) simCtl(new SimulatorControl)
{ {
QObject::connect(&outputLogger, &LogTailFiles::logMessage,
std::bind(&IosToolHandlerPrivate::appOutput, this, _1));
} }
IosSimulatorToolHandlerPrivate::~IosSimulatorToolHandlerPrivate() IosSimulatorToolHandlerPrivate::~IosSimulatorToolHandlerPrivate()
@@ -809,7 +872,6 @@ void IosSimulatorToolHandlerPrivate::requestDeviceInfo(const QString &deviceId,
void IosSimulatorToolHandlerPrivate::stop(int errorCode) void IosSimulatorToolHandlerPrivate::stop(int errorCode)
{ {
appPId = -1;
foreach (auto f, futureList) { foreach (auto f, futureList) {
if (!f.isFinished()) if (!f.isFinished())
f.cancel(); f.cancel();
@@ -843,6 +905,31 @@ void IosSimulatorToolHandlerPrivate::installAppOnSimulator()
void IosSimulatorToolHandlerPrivate::launchAppOnSimulator(const QStringList &extraArgs) 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<QTemporaryFile> stdoutFile;
std::shared_ptr<QTemporaryFile> 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<int> &fi, qint64 pid) { auto monitorPid = [this](QFutureInterface<int> &fi, qint64 pid) {
int exitCode = 0; int exitCode = 0;
const QStringList args({QStringLiteral("-0"), QString::number(pid)}); const QStringList args({QStringLiteral("-0"), QString::number(pid)});
@@ -858,15 +945,17 @@ void IosSimulatorToolHandlerPrivate::launchAppOnSimulator(const QStringList &ext
stop(0); stop(0);
}; };
auto onResponseAppLaunch = [this, monitorPid](const SimulatorControl::ResponseData &response) { auto onResponseAppLaunch = [=](const SimulatorControl::ResponseData &response) {
if (!isResponseValid(response)) if (!isResponseValid(response))
return; return;
if (response.success) { if (response.success) {
appPId = response.pID; gotInferiorPid(bundlePath, deviceId, response.pID);
gotInferiorPid(bundlePath, deviceId, appPId);
didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Success); didStartApp(bundlePath, deviceId, Ios::IosToolHandler::Success);
// Start monitoring app's life signs. // 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 { } else {
errorMsg(IosToolHandler::tr("Application launch on Simulator failed. %1") errorMsg(IosToolHandler::tr("Application launch on Simulator failed. %1")
.arg(QString::fromLocal8Bit(response.commandOutput))); .arg(QString::fromLocal8Bit(response.commandOutput)));
@@ -875,11 +964,12 @@ void IosSimulatorToolHandlerPrivate::launchAppOnSimulator(const QStringList &ext
q->finished(q); q->finished(q);
} }
}; };
Utils::FileName appBundle = Utils::FileName::fromString(bundlePath);
futureList << Utils::onResultReady(simCtl->launchApp(deviceId, futureList << Utils::onResultReady(
SimulatorControl::bundleIdentifier(appBundle), simCtl->launchApp(deviceId, bundleId, debugRun, extraArgs,
runKind == IosToolHandler::DebugRun, captureConsole ? stdoutFile->fileName() : QString(),
extraArgs), onResponseAppLaunch); captureConsole ? stderrFile->fileName() : QString()),
onResponseAppLaunch);
} }
bool IosSimulatorToolHandlerPrivate::isResponseValid(const SimulatorControl::ResponseData &responseData) bool IosSimulatorToolHandlerPrivate::isResponseValid(const SimulatorControl::ResponseData &responseData)
@@ -895,28 +985,6 @@ bool IosSimulatorToolHandlerPrivate::isResponseValid(const SimulatorControl::Res
return true; 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() void IosToolHandlerPrivate::killProcess()
{ {
if (isRunning()) if (isRunning())
@@ -973,3 +1041,5 @@ bool IosToolHandler::isRunning()
} }
} // namespace Ios } // namespace Ios
#include "iostoolhandler.moc"

View File

@@ -108,7 +108,8 @@ private:
const Utils::FileName &bundlePath); const Utils::FileName &bundlePath);
void launchApp(QFutureInterface<SimulatorControl::ResponseData> &fi, const QString &simUdid, void launchApp(QFutureInterface<SimulatorControl::ResponseData> &fi, const QString &simUdid,
const QString &bundleIdentifier, bool waitForDebugger, const QString &bundleIdentifier, bool waitForDebugger,
const QStringList &extraArgs); const QStringList &extraArgs, const QString &stdoutPath,
const QString &stderrPath);
static QList<IosDeviceType> availableDevices; static QList<IosDeviceType> availableDevices;
friend class SimulatorControl; friend class SimulatorControl;
@@ -196,10 +197,11 @@ SimulatorControl::installApp(const QString &simUdid, const Utils::FileName &bund
QFuture<SimulatorControl::ResponseData> QFuture<SimulatorControl::ResponseData>
SimulatorControl::launchApp(const QString &simUdid, const QString &bundleIdentifier, 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, return Utils::runAsync(&SimulatorControlPrivate::launchApp, d, simUdid, bundleIdentifier,
waitForDebugger, extraArgs); waitForDebugger, extraArgs, stdoutPath, stderrPath);
} }
QList<IosDeviceType> SimulatorControlPrivate::availableDevices; QList<IosDeviceType> SimulatorControlPrivate::availableDevices;
@@ -342,12 +344,20 @@ void SimulatorControlPrivate::installApp(QFutureInterface<SimulatorControl::Resp
void SimulatorControlPrivate::launchApp(QFutureInterface<SimulatorControl::ResponseData> &fi, void SimulatorControlPrivate::launchApp(QFutureInterface<SimulatorControl::ResponseData> &fi,
const QString &simUdid, const QString &bundleIdentifier, 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); SimulatorControl::ResponseData response(simUdid);
if (!bundleIdentifier.isEmpty() && !fi.isCanceled()) { if (!bundleIdentifier.isEmpty() && !fi.isCanceled()) {
QStringList args({QStringLiteral("launch"), simUdid, bundleIdentifier}); 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) if (waitForDebugger)
args.insert(1, QStringLiteral("-w")); args.insert(1, QStringLiteral("-w"));

View File

@@ -69,7 +69,9 @@ public:
QFuture<ResponseData> startSimulator(const QString &simUdid) const; QFuture<ResponseData> startSimulator(const QString &simUdid) const;
QFuture<ResponseData> installApp(const QString &simUdid, const Utils::FileName &bundlePath) const; QFuture<ResponseData> installApp(const QString &simUdid, const Utils::FileName &bundlePath) const;
QFuture<ResponseData> launchApp(const QString &simUdid, const QString &bundleIdentifier, QFuture<ResponseData> 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: private:
SimulatorControlPrivate *d; SimulatorControlPrivate *d;