Android: Enclose the execution of logcat process in the recipe

Change-Id: I38d5ecb025206c801e630008f881555fe0380c8a
Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
This commit is contained in:
Jarek Kobus
2024-07-24 11:37:23 +02:00
parent 25e5ab9426
commit 9d06f04b7d
2 changed files with 175 additions and 203 deletions

View File

@@ -20,6 +20,7 @@
#include <qtsupport/baseqtversion.h>
#include <qtsupport/qtkitaspect.h>
#include <solutions/tasking/barrier.h>
#include <solutions/tasking/conditional.h>
#include <utils/hostosinfo.h>
@@ -309,95 +310,6 @@ void AndroidRunnerWorker::forceStop()
}
}
void AndroidRunnerWorker::logcatReadStandardError()
{
if (m_processPID != -1)
logcatProcess(m_adbLogcatProcess->readAllRawStandardError(), m_stderrBuffer, true);
}
void AndroidRunnerWorker::logcatReadStandardOutput()
{
if (m_processPID != -1)
logcatProcess(m_adbLogcatProcess->readAllRawStandardOutput(), m_stdoutBuffer, false);
}
void AndroidRunnerWorker::logcatProcess(const QByteArray &text, QByteArray &buffer, bool onlyError)
{
QList<QByteArray> lines = text.split('\n');
// lines always contains at least one item
lines[0].prepend(buffer);
if (!lines.last().endsWith('\n')) {
// incomplete line
buffer = lines.last();
lines.removeLast();
} else {
buffer.clear();
}
QString pidString = QString::number(m_processPID);
for (const QByteArray &msg : std::as_const(lines)) {
const QString line = QString::fromUtf8(msg).trimmed() + QLatin1Char('\n');
if (!line.contains(pidString))
continue;
if (m_useCppDebugger) {
switch (m_jdbState) {
case JDBState::Idle:
if (msg.trimmed().endsWith("Sending WAIT chunk")) {
m_jdbState = JDBState::Waiting;
handleJdbWaiting();
}
break;
case JDBState::Waiting:
if (msg.indexOf("debugger has settled") > 0) {
m_jdbState = JDBState::Settled;
handleJdbSettled();
}
break;
default:
break;
}
}
static const QRegularExpression regExpLogcat{"^[0-9\\-]*" // date
"\\s+"
"[0-9\\-:.]*"// time
"\\s*"
"(\\d*)" // pid 1. capture
"\\s+"
"\\d*" // unknown
"\\s+"
"(\\w)" // message type 2. capture
"\\s+"
"(.*): " // source 3. capture
"(.*)" // message 4. capture
"[\\n\\r]*$"};
const QRegularExpressionMatch match = regExpLogcat.match(line);
if (match.hasMatch()) {
// Android M
if (match.captured(1) == pidString) {
const QString messagetype = match.captured(2);
const QString output = line.mid(match.capturedStart(2));
if (onlyError
|| messagetype == QLatin1String("F")
|| messagetype == QLatin1String("E")
|| messagetype == QLatin1String("W"))
emit remoteErrorOutput(output);
else
emit remoteOutput(output);
}
} else {
if (onlyError || line.startsWith("F/")
|| line.startsWith("E/")
|| line.startsWith("W/"))
emit remoteErrorOutput(line);
else
emit remoteOutput(line);
}
}
}
void AndroidRunnerWorker::setAndroidDeviceInfo(const AndroidDeviceInfo &info)
{
m_deviceSerialNumber = info.serialNumber;
@@ -406,47 +318,6 @@ void AndroidRunnerWorker::setAndroidDeviceInfo(const AndroidDeviceInfo &info)
<< m_deviceSerialNumber << m_apiLevel;
}
void AndroidRunnerWorker::asyncStartLogcat()
{
// Its assumed that the device or avd returned by selector() is online.
// Start the logcat process before app starts.
QTC_CHECK(!m_adbLogcatProcess);
// Ideally AndroidManager::runAdbCommandDetached() should be used, but here
// we need to connect the readyRead signals from logcat otherwise we might
// lost some output between the process start and connecting those signals.
m_adbLogcatProcess.reset(new Process);
connect(m_adbLogcatProcess.get(), &Process::readyReadStandardOutput,
this, &AndroidRunnerWorker::logcatReadStandardOutput);
connect(m_adbLogcatProcess.get(), &Process::readyReadStandardError,
this, &AndroidRunnerWorker::logcatReadStandardError);
// Get target current time to fetch only recent logs
QString dateInSeconds;
QStringList timeArg;
if (runAdb({"shell", "date", "+%s"}, &dateInSeconds)) {
timeArg << "-T";
timeArg << QDateTime::fromSecsSinceEpoch(dateInSeconds.toInt())
.toString("MM-dd hh:mm:ss.mmm");
}
const QStringList logcatArgs = selector() << "logcat" << timeArg;
const FilePath adb = AndroidConfig::adbToolPath();
qCDebug(androidRunWorkerLog).noquote() << "Running logcat command (async):"
<< CommandLine(adb, logcatArgs).toUserOutput();
m_adbLogcatProcess->setCommand({adb, logcatArgs});
m_adbLogcatProcess->start();
if (m_adbLogcatProcess->waitForStarted(500ms) && m_adbLogcatProcess->state() == QProcess::Running)
m_adbLogcatProcess->setObjectName("AdbLogcatProcess");
}
void AndroidRunnerWorker::asyncStartHelper()
{
forceStop();
asyncStartLogcat();
}
void AndroidRunnerWorker::startNativeDebugging()
{
// run-as <package-name> pwd fails on API 22 so route the pwd through shell.
@@ -573,6 +444,166 @@ ExecutableItem AndroidRunnerWorker::removeForwardPortRecipe(
};
}
// The startBarrier is passed when logcat process received "Sending WAIT chunk" message.
// The settledBarrier is passed when logcat process received "debugger has settled" message.
ExecutableItem AndroidRunnerWorker::jdbRecipe(const SingleBarrier &startBarrier,
const SingleBarrier &settledBarrier)
{
const auto onSetup = [this] {
return m_useCppDebugger ? SetupResult::Continue : SetupResult::StopWithSuccess;
};
const auto onTaskTreeSetup = [this](TaskTree &taskTree) {
taskTree.setRecipe({
removeForwardPortRecipe("tcp:" + s_localJdbServerPort.toString(),
"jdwp:" + QString::number(m_processPID), "JDB")
});
};
const auto onJdbSetup = [this, settledBarrier](Process &process) {
const FilePath jdbPath = AndroidConfig::openJDKLocation().pathAppended("bin/jdb")
.withExecutableSuffix();
const QString portArg = QString("com.sun.jdi.SocketAttach:hostname=localhost,port=%1")
.arg(s_localJdbServerPort.toString());
process.setCommand({jdbPath, {"-connect", portArg}});
process.setProcessMode(ProcessMode::Writer);
process.setProcessChannelMode(QProcess::MergedChannels);
process.setReaperTimeout(s_jdbTimeout);
connect(settledBarrier->barrier(), &Barrier::done, &process, [processPtr = &process] {
processPtr->write("ignore uncaught java.lang.Throwable\n"
"threads\n"
"cont\n"
"exit\n");
});
};
const auto onJdbDone = [](const Process &process, DoneWith result) {
qCDebug(androidRunWorkerLog) << qPrintable(process.allOutput());
if (result == DoneWith::Cancel)
qCCritical(androidRunWorkerLog) << "Terminating JDB due to timeout";
};
return Group {
onGroupSetup(onSetup),
waitForBarrierTask(startBarrier),
TaskTreeTask(onTaskTreeSetup),
ProcessTask(onJdbSetup, onJdbDone).withTimeout(60s)
};
}
ExecutableItem AndroidRunnerWorker::logcatRecipe()
{
struct Buffer {
QStringList timeArgs;
QByteArray stdOutBuffer;
QByteArray stdErrBuffer;
};
const Storage<Buffer> storage;
const SingleBarrier startJdbBarrier; // When logcat received "Sending WAIT chunk".
const SingleBarrier settledJdbBarrier; // When logcat received "debugger has settled".
const auto onTimeSetup = [this](Process &process) {
process.setCommand({AndroidConfig::adbToolPath(), {selector(), "shell", "date", "+%s"}});
};
const auto onTimeDone = [storage](const Process &process) {
storage->timeArgs = {"-T", QDateTime::fromSecsSinceEpoch(
process.cleanedStdOut().trimmed().toInt()).toString("MM-dd hh:mm:ss.mmm")};
};
const auto onLogcatSetup = [this, storage, startJdbBarrier, settledJdbBarrier](Process &process) {
Buffer *bufferPtr = storage.activeStorage();
const auto parseLogcat = [this, bufferPtr, start = startJdbBarrier->barrier(),
settled = settledJdbBarrier->barrier(), processPtr = &process](
QProcess::ProcessChannel channel) {
if (m_processPID == -1)
return;
QByteArray &buffer = channel == QProcess::StandardOutput ? bufferPtr->stdOutBuffer
: bufferPtr->stdErrBuffer;
const QByteArray &text = channel == QProcess::StandardOutput
? processPtr->readAllRawStandardOutput()
: processPtr->readAllRawStandardError();
QList<QByteArray> lines = text.split('\n');
// lines always contains at least one item
lines[0].prepend(buffer);
if (lines.last().endsWith('\n'))
buffer.clear();
else
buffer = lines.takeLast(); // incomplete line
const QString pidString = QString::number(m_processPID);
for (const QByteArray &msg : std::as_const(lines)) {
const QString line = QString::fromUtf8(msg).trimmed() + QLatin1Char('\n');
if (!line.contains(pidString))
continue;
if (m_useCppDebugger) {
if (start->current() == 0 && msg.trimmed().endsWith("Sending WAIT chunk"))
start->advance();
else if (settled->current() == 0 && msg.indexOf("debugger has settled") > 0)
settled->advance();
}
static const QRegularExpression regExpLogcat{
"^[0-9\\-]*" // date
"\\s+"
"[0-9\\-:.]*"// time
"\\s*"
"(\\d*)" // pid 1. capture
"\\s+"
"\\d*" // unknown
"\\s+"
"(\\w)" // message type 2. capture
"\\s+"
"(.*): " // source 3. capture
"(.*)" // message 4. capture
"[\\n\\r]*$"
};
const bool onlyError = channel == QProcess::StandardError;
const QRegularExpressionMatch match = regExpLogcat.match(line);
if (match.hasMatch()) {
// Android M
if (match.captured(1) == pidString) {
const QString msgType = match.captured(2);
const QString output = line.mid(match.capturedStart(2));
if (onlyError || msgType == "F" || msgType == "E" || msgType == "W")
emit remoteErrorOutput(output);
else
emit remoteOutput(output);
}
} else {
if (onlyError || line.startsWith("F/") || line.startsWith("E/")
|| line.startsWith("W/")) {
emit remoteErrorOutput(line);
} else {
emit remoteOutput(line);
}
}
}
};
connect(&process, &Process::readyReadStandardOutput, this, [parseLogcat] {
parseLogcat(QProcess::StandardOutput);
});
connect(&process, &Process::readyReadStandardError, this, [parseLogcat] {
parseLogcat(QProcess::StandardError);
});
process.setCommand({AndroidConfig::adbToolPath(), {selector(), "logcat", storage->timeArgs}});
};
return Group {
parallel,
startJdbBarrier,
settledJdbBarrier,
Group {
storage,
ProcessTask(onTimeSetup, onTimeDone, CallDoneIf::Success) || successItem,
ProcessTask(onLogcatSetup)
},
jdbRecipe(startJdbBarrier, settledJdbBarrier)
};
}
ExecutableItem AndroidRunnerWorker::preStartRecipe()
{
const QString port = "tcp:" + QString::number(m_qmlServer.port());
@@ -712,11 +743,15 @@ ExecutableItem AndroidRunnerWorker::pidRecipe()
void AndroidRunnerWorker::asyncStart()
{
asyncStartHelper();
forceStop();
const Group recipe {
preStartRecipe(),
pidRecipe()
parallel,
logcatRecipe(),
Group {
preStartRecipe(),
pidRecipe()
}
};
m_taskTreeRunner.start(recipe);
@@ -728,57 +763,9 @@ void AndroidRunnerWorker::asyncStop()
if (m_processPID != -1)
forceStop();
m_jdbProcess.reset();
m_debugServerProcess.reset();
}
void AndroidRunnerWorker::handleJdbWaiting()
{
if (!removeForwardPort("tcp:" + s_localJdbServerPort.toString(),
"jdwp:" + QString::number(m_processPID), "JDB"))
return;
const FilePath jdbPath = AndroidConfig::openJDKLocation()
.pathAppended("bin/jdb").withExecutableSuffix();
QStringList jdbArgs("-connect");
jdbArgs << QString("com.sun.jdi.SocketAttach:hostname=localhost,port=%1")
.arg(s_localJdbServerPort.toString());
qCDebug(androidRunWorkerLog).noquote()
<< "Starting JDB:" << CommandLine(jdbPath, jdbArgs).toUserOutput();
m_jdbProcess.reset(new Process);
m_jdbProcess->setProcessChannelMode(QProcess::MergedChannels);
m_jdbProcess->setCommand({jdbPath, jdbArgs});
m_jdbProcess->setReaperTimeout(s_jdbTimeout);
m_jdbProcess->setProcessMode(ProcessMode::Writer);
m_jdbProcess->start();
if (!m_jdbProcess->waitForStarted()) {
emit remoteProcessFinished(Tr::tr("Failed to start JDB."));
m_jdbProcess.reset();
return;
}
m_jdbProcess->setObjectName("JdbProcess");
}
void AndroidRunnerWorker::handleJdbSettled()
{
qCDebug(androidRunWorkerLog) << "Handle JDB settled";
m_jdbProcess->write("ignore uncaught java.lang.Throwable\n"
"threads\n"
"cont\n"
"exit\n");
if (!m_jdbProcess->waitForFinished(s_jdbTimeout)) {
qCDebug(androidRunWorkerLog) << qPrintable(m_jdbProcess->allOutput());
qCCritical(androidRunWorkerLog) << "Terminating JDB due to timeout";
m_jdbProcess.reset();
} else if (m_jdbProcess->exitStatus() == QProcess::NormalExit && m_jdbProcess->exitCode() == 0) {
qCDebug(androidRunWorkerLog) << qPrintable(m_jdbProcess->allOutput());
qCDebug(androidRunWorkerLog) << "JDB settled";
return;
}
emit remoteProcessFinished(Tr::tr("Cannot attach JDB to the running application."));
}
bool AndroidRunnerWorker::removeForwardPort(const QString &port, const QString &adbArg,
const QString &portType)
{
@@ -803,9 +790,7 @@ void AndroidRunnerWorker::onProcessIdChanged(const PidUserPair &pidUser)
emit remoteProcessFinished(QLatin1String("\n\n") + Tr::tr("\"%1\" died.")
.arg(m_packageName));
// App died/killed. Reset log, monitor, jdb & gdbserver/lldb-server processes.
m_adbLogcatProcess.reset();
m_psIsAlive.reset();
m_jdbProcess.reset();
m_debugServerProcess.reset();
// Run adb commands after application quit.
@@ -817,7 +802,8 @@ void AndroidRunnerWorker::onProcessIdChanged(const PidUserPair &pidUser)
// In debugging cases this will be funneled to the engine to actually start
// and attach gdb. Afterwards this ends up in handleRemoteDebuggerRunning() below.
emit remoteProcessStarted(s_localDebugServerPort, m_qmlServer, m_processPID);
logcatReadStandardOutput();
// TODO: Add a pidBarrier and activate it -> it should start parsing the logcat output.
// logcatReadStandardOutput();
QTC_ASSERT(!m_psIsAlive, /**/);
QStringList isAliveArgs = selector() << "shell" << pidPollingScript.arg(m_processPID);
m_psIsAlive.reset(AndroidManager::startAdbProcess(isAliveArgs));

View File

@@ -6,6 +6,7 @@
#include <qmldebug/qmldebugcommandlinearguments.h>
#include <solutions/tasking/barrier.h>
#include <solutions/tasking/tasktreerunner.h>
#include <utils/environment.h>
@@ -41,32 +42,22 @@ private:
bool runAdb(const QStringList &args, QString *stdOut = nullptr, QString *stdErr = nullptr);
QStringList selector() const;
void forceStop();
void logcatReadStandardError();
void logcatReadStandardOutput();
void logcatProcess(const QByteArray &text, QByteArray &buffer, bool onlyError);
void handleJdbWaiting();
void handleJdbSettled();
bool removeForwardPort(const QString &port, const QString &adbArg, const QString &portType);
void asyncStartHelper();
void startNativeDebugging();
void startDebuggerServer(const QString &packageDir, const QString &debugServerFile);
bool deviceFileExists(const QString &filePath);
bool packageFileExists(const QString& filePath);
bool packageFileExists(const QString &filePath);
bool uploadDebugServer(const QString &debugServerFileName);
void asyncStartLogcat();
enum class JDBState {
Idle,
Waiting,
Settled
};
void onProcessIdChanged(const PidUserPair &pidUser);
bool isPreNougat() const { return m_apiLevel > 0 && m_apiLevel <= 23; }
Tasking::ExecutableItem removeForwardPortRecipe(const QString &port, const QString &adbArg,
const QString &portType);
Tasking::ExecutableItem jdbRecipe(const Tasking::SingleBarrier &startBarrier,
const Tasking::SingleBarrier &settledBarrier);
Tasking::ExecutableItem logcatRecipe();
Tasking::ExecutableItem preStartRecipe();
Tasking::ExecutableItem pidRecipe();
@@ -78,18 +69,13 @@ private:
QStringList m_amStartExtraArgs;
qint64 m_processPID = -1;
qint64 m_processUser = -1;
std::unique_ptr<Utils::Process> m_adbLogcatProcess;
std::unique_ptr<Utils::Process> m_psIsAlive;
QByteArray m_stdoutBuffer;
QByteArray m_stderrBuffer;
Tasking::TaskTreeRunner m_taskTreeRunner;
bool m_useCppDebugger = false;
bool m_useLldb = false; // FIXME: Un-implemented currently.
QmlDebug::QmlDebugServicesPreset m_qmlDebugServices;
QUrl m_qmlServer;
JDBState m_jdbState = JDBState::Idle;
std::unique_ptr<Utils::Process> m_debugServerProcess; // gdbserver or lldb-server
std::unique_ptr<Utils::Process> m_jdbProcess;
QString m_deviceSerialNumber;
int m_apiLevel = -1;
QString m_extraAppParams;