Commit d4ca232d authored by BogDan Vatra's avatar BogDan Vatra
Browse files

Android: Improve application output window by adding filters



 - allow the user to choose the visible log levels
 - allow the user to choose which activities/service(s) logs are visible
 - wakeup the device (API 20+)
 - use only the most recent logs (API 21+ add "-T 0" to logcat params)
 - use logcat -v time format, which is the same on all Android versions

In the future we can even allow the user to choose which parts of the
log line are visible, e.g. time, log level, TAG, PID, Message

Task-number: QTCREATORBUG-16887
Change-Id: I07ce00aff59a479660f5ac6da75eef973ba3f627
Reviewed-by: default avatarhjk <hjk@qt.io>
Reviewed-by: Vikas Pachdha's avatarVikas Pachdha <vikas.pachdha@qt.io>
parent 8dc98995
......@@ -186,6 +186,16 @@ AndroidDebugSupport::AndroidDebugSupport(AndroidRunConfiguration *runConfig,
QTC_ASSERT(m_runControl, return);
m_runControl->showMessage(output, AppOutput);
});
QTC_ASSERT(runControl, return);
auto formatter = qobject_cast<AndroidOutputFormatter*>(runControl->outputFormatter());
QTC_ASSERT(formatter, return);
connect(m_runner, &AndroidRunner::pidFound, formatter, &AndroidOutputFormatter::appendPid);
connect(m_runner, &AndroidRunner::pidLost, formatter, &AndroidOutputFormatter::removePid);
connect(m_runner, &AndroidRunner::remoteProcessFinished, formatter,
[formatter] {
formatter->removePid(-1);
});
}
void AndroidDebugSupport::handleRemoteProcessStarted(Utils::Port gdbServerPort, Utils::Port qmlPort)
......
......@@ -29,16 +29,205 @@
#include "androidmanager.h"
#include <projectexplorer/kitinformation.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectexplorersettings.h>
#include <projectexplorer/target.h>
#include <qtsupport/qtoutputformatter.h>
#include <qtsupport/qtkitinformation.h>
#include <QPlainTextEdit>
#include <QRegularExpression>
#include <QToolButton>
#include <utils/qtcassert.h>
#include <utils/utilsicons.h>
using namespace ProjectExplorer;
namespace Android {
static QRegularExpression logCatRegExp("([0-9\\-]*\\s+[0-9\\-:.]*)" // 1. time
"\\s*"
"([DEIVWF])" // 2. log level
"\\/"
"(.*)" // 3. TAG
"\\(\\s*"
"(\\d+)" // 4. PID
"\\)\\:\\s"
"(.*)"); // 5. Message
AndroidOutputFormatter::AndroidOutputFormatter(Project *project)
: QtSupport::QtOutputFormatter(project)
, m_filtersButton(new QToolButton)
{
auto filtersMenu = new QMenu(m_filtersButton.data());
m_filtersButton->setToolTip(tr("Filters"));
m_filtersButton->setIcon(Utils::Icons::FILTER.icon());
m_filtersButton->setProperty("noArrow", true);
m_filtersButton->setAutoRaise(true);
m_filtersButton->setPopupMode(QToolButton::InstantPopup);
m_filtersButton->setMenu(filtersMenu);
auto logsMenu = filtersMenu->addMenu(tr("Log Level"));
addLogAction(All, logsMenu, tr("All"));
addLogAction(Verbose, logsMenu, tr("Verbose"));
addLogAction(Info, logsMenu, tr("Info"));
addLogAction(Debug, logsMenu, tr("Debug"));
addLogAction(Warning, logsMenu, tr("Warning"));
addLogAction(Error, logsMenu, tr("Error"));
addLogAction(Fatal, logsMenu, tr("Fatal"));
updateLogMenu();
m_appsMenu = filtersMenu->addMenu(tr("Applications"));
appendPid(-1, tr("All"));
}
AndroidOutputFormatter::~AndroidOutputFormatter()
{}
QList<QWidget *> AndroidOutputFormatter::toolbarWidgets() const
{
return QList<QWidget *>{m_filtersButton.data()};
}
void AndroidOutputFormatter::appendMessage(const QString &text, Utils::OutputFormat format)
{
if (text.isEmpty())
return;
CachedLine line;
line.content = text;
if (format < Utils::StdOutFormat) {
line.level = SkipFiltering;
line.pid = -1;
} else {
QRegularExpressionMatch match = logCatRegExp.match(text);
if (!match.hasMatch())
return;
line.level = None;
switch (match.captured(2).toLatin1()[0]) {
case 'D': line.level = Debug; break;
case 'I': line.level = Info; break;
case 'V': line.level = Verbose; break;
case 'W': line.level = Warning; break;
case 'E': line.level = Error; break;
case 'F': line.level = Fatal; break;
default: return;
}
line.pid = match.captured(4).toLongLong();
}
m_cachedLines.append(line);
if (m_cachedLines.size() > ProjectExplorerPlugin::projectExplorerSettings().maxAppOutputLines)
m_cachedLines.pop_front();
filterMessage(line);
}
void AndroidOutputFormatter::clear()
{
m_cachedLines.clear();
}
void AndroidOutputFormatter::appendPid(qint64 pid, const QString &name)
{
if (m_pids.contains(pid))
return;
auto action = m_appsMenu->addAction(name);
m_pids[pid] = action;
action->setCheckable(true);
action->setChecked(pid != -1);
connect(action, &QAction::triggered, this, &AndroidOutputFormatter::applyFilter);
applyFilter();
}
void AndroidOutputFormatter::removePid(qint64 pid)
{
if (pid == -1) {
for (auto action : m_pids)
m_appsMenu->removeAction(action);
m_pids.clear();
} else {
m_appsMenu->removeAction(m_pids[pid]);
m_pids.remove(pid);
}
}
void AndroidOutputFormatter::updateLogMenu(LogLevel set, LogLevel reset)
{
m_logLevelFlags |= set;
m_logLevelFlags &= ~reset;
for (const auto & pair : m_logLevels)
pair.second->setChecked((m_logLevelFlags & pair.first) == pair.first);
applyFilter();
}
void AndroidOutputFormatter::filterMessage(const CachedLine &line)
{
if (line.level == SkipFiltering || m_pids[-1]->isChecked()) {
QtOutputFormatter::appendMessage(line.content, Utils::NormalMessageFormat);
} else {
// Filter Log Level
if (!(m_logLevelFlags & line.level))
return;
// Filter PIDs
if (!m_pids[-1]->isChecked()) {
auto it = m_pids.find(line.pid);
if (it == m_pids.end() || !(*it)->isChecked())
return;
}
Utils::OutputFormat format = Utils::NormalMessageFormat;
switch (line.level) {
case Debug:
format = Utils::DebugFormat;
break;
case Info:
case Verbose:
format = Utils::StdOutFormat;
break;
case Warning:
case Error:
case Fatal:
format = Utils::StdErrFormat;
break;
default:
return;
}
Utils::OutputFormatter::appendMessage(line.content, format);
}
}
void AndroidOutputFormatter::applyFilter()
{
if (!plainTextEdit())
return;
plainTextEdit()->clear();
if (!m_pids[-1]->isChecked()) {
bool allOn = true;
for (auto action : m_pids) {
if (!action->isChecked()) {
allOn = false;
break;
}
}
m_pids[-1]->setChecked(allOn);
} else {
for (auto action : m_pids)
action->setChecked(true);
}
for (const auto &line : m_cachedLines)
filterMessage(line);
}
AndroidRunConfiguration::AndroidRunConfiguration(Target *parent, Core::Id id)
: RunConfiguration(parent, id)
{
......@@ -56,7 +245,7 @@ QWidget *AndroidRunConfiguration::createConfigurationWidget()
Utils::OutputFormatter *AndroidRunConfiguration::createOutputFormatter() const
{
return new QtSupport::QtOutputFormatter(target()->project());
return new AndroidOutputFormatter(target()->project());
}
const QString AndroidRunConfiguration::remoteChannel() const
......
......@@ -28,9 +28,72 @@
#include "android_global.h"
#include <projectexplorer/runconfiguration.h>
#include <qtsupport/qtoutputformatter.h>
#include <QMenu>
class QToolButton;
namespace Android {
class AndroidOutputFormatter : public QtSupport::QtOutputFormatter
{
Q_OBJECT
public:
enum LogLevel {
None = 0,
Verbose = 1,
Info = 1 << 1,
Debug = 1 << 2,
Warning = 1 << 3,
Error = 1 << 4,
Fatal = 1 << 5,
All = Verbose | Info | Debug | Warning | Error | Fatal,
SkipFiltering = ~All
};
public:
explicit AndroidOutputFormatter(ProjectExplorer::Project *project);
~AndroidOutputFormatter();
// OutputFormatter interface
QList<QWidget*> toolbarWidgets() const override;
void appendMessage(const QString &text, Utils::OutputFormat format) override;
void clear() override;
public slots:
void appendPid(qint64 pid, const QString &name);
void removePid(qint64 pid);
private:
struct CachedLine {
qint64 pid;
LogLevel level;
QString content;
};
private:
void updateLogMenu(LogLevel set = None, LogLevel reset = None);
void filterMessage(const CachedLine &line);
void applyFilter();
void addLogAction(LogLevel level, QMenu *logsMenu, const QString &name) {
auto action = logsMenu->addAction(name);
m_logLevels.push_back(qMakePair(level, action));
action->setCheckable(true);
connect(action, &QAction::triggered, this, [level, this](bool checked) {
updateLogMenu(checked ? level : None , checked ? None : level);
});
}
private:
int m_logLevelFlags = All;
QVector<QPair<LogLevel, QAction*>> m_logLevels;
QHash<qint64, QAction*> m_pids;
QScopedPointer<QToolButton> m_filtersButton;
QMenu *m_appsMenu;
QList<CachedLine> m_cachedLines;
};
class ANDROID_EXPORT AndroidRunConfiguration : public ProjectExplorer::RunConfiguration
{
Q_OBJECT
......
......@@ -62,6 +62,13 @@ void AndroidRunControl::start()
this, &AndroidRunControl::handleRemoteOutput);
connect(m_runner, &AndroidRunner::remoteProcessFinished,
this, &AndroidRunControl::handleRemoteProcessFinished);
auto formatter = static_cast<AndroidOutputFormatter *>(outputFormatter());
connect(m_runner, &AndroidRunner::pidFound,
formatter, &AndroidOutputFormatter::appendPid);
connect(m_runner, &AndroidRunner::pidLost,
formatter, &AndroidOutputFormatter::removePid);
appendMessage(tr("Starting remote process."), Utils::NormalMessageFormat);
m_runner->setRunnable(runnable().as<AndroidRunnable>());
m_runner->start();
......
......@@ -52,6 +52,7 @@
#include <QTime>
#include <QTcpServer>
#include <QTcpSocket>
#include <QRegularExpression>
using namespace std;
using namespace std::placeholders;
......@@ -126,10 +127,10 @@ namespace Internal {
const int MIN_SOCKET_HANDSHAKE_PORT = 20001;
const int MAX_SOCKET_HANDSHAKE_PORT = 20999;
static const QString pidScript = QStringLiteral("for p in /proc/[0-9]*; "
"do cat <$p/cmdline && echo :${p##*/}; done");
static const QString pidPollingScript = QStringLiteral("while true; do sleep 1; "
"cat /proc/%1/cmdline > /dev/null; done");
static const QString pidScript = QStringLiteral("input keyevent KEYCODE_WAKEUP; "
"while true; do sleep 1; echo \"=\"; "
"for p in /proc/[0-9]*; "
"do cat <$p/cmdline && echo :${p##*/}; done; done");
static const QString regExpLogcat = QStringLiteral("[0-9\\-]*" // date
"\\s+"
......@@ -147,55 +148,26 @@ static const QString regExpLogcat = QStringLiteral("[0-9\\-]*" // date
);
static int APP_START_TIMEOUT = 45000;
static bool isTimedOut(const chrono::high_resolution_clock::time_point &start,
int msecs = APP_START_TIMEOUT)
{
bool timedOut = false;
auto end = chrono::high_resolution_clock::now();
if (chrono::duration_cast<chrono::milliseconds>(end-start).count() > msecs)
timedOut = true;
return timedOut;
}
static qint64 extractPID(const QByteArray &output, const QString &packageName)
{
qint64 pid = -1;
foreach (auto tuple, output.split('\n')) {
tuple = tuple.simplified();
if (!tuple.isEmpty()) {
auto parts = tuple.split(':');
QString commandName = QString::fromLocal8Bit(parts.first());
if (parts.length() == 2 && commandName == packageName) {
pid = parts.last().toLongLong();
break;
}
}
}
return pid;
}
enum class PidStatus {
Found,
Lost
};
void findProcessPID(QFutureInterface<qint64> &fi, const QString &adbPath,
QStringList selector, const QString &packageName)
struct PidInfo
{
if (packageName.isEmpty())
return;
qint64 processPID = -1;
chrono::high_resolution_clock::time_point start = chrono::high_resolution_clock::now();
do {
QThread::msleep(200);
const QByteArray out = Utils::SynchronousProcess()
.runBlocking(adbPath, selector << QStringLiteral("shell") << pidScript)
.allRawOutput();
processPID = extractPID(out, packageName);
} while (processPID == -1 && !isTimedOut(start) && !fi.isCanceled());
if (!fi.isCanceled())
fi.reportResult(processPID);
}
PidInfo(qint64 pid = -1, PidStatus status = PidStatus::Lost, QString name = {})
: pid(pid)
, status(status)
, name(name)
{}
qint64 pid;
PidStatus status;
QString name;
};
static void deleter(QProcess *p)
{
p->disconnect();
p->kill();
p->waitForFinished();
// Might get deleted from its own signal handler.
......@@ -229,29 +201,31 @@ signals:
void remoteOutput(const QString &output);
void remoteErrorOutput(const QString &output);
void pidFound(qint64, const QString &name);
void pidLost(qint64);
private:
void onProcessIdChanged(qint64 pid);
void findProcessPids();
void onProcessIdChanged(PidInfo pidInfo);
void logcatReadStandardError();
void logcatReadStandardOutput();
void adbKill(qint64 pid);
QStringList selector() const { return m_selector; }
void forceStop();
void findPs();
void logcatProcess(const QByteArray &text, QByteArray &buffer, bool onlyError);
bool adbShellAmNeedsQuotes();
bool runAdb(const QStringList &args, QString *exitMessage = nullptr, int timeoutS = 10);
int deviceSdkVersion();
// Create the processes and timer in the worker thread, for correct thread affinity
std::unique_ptr<QProcess, decltype(&deleter)> m_adbLogcatProcess;
std::unique_ptr<QProcess, decltype(&deleter)> m_psIsAlive;
std::unique_ptr<QProcess, decltype(&deleter)> m_pidsFinderProcess;
QScopedPointer<QTcpSocket> m_socket;
QByteArray m_stdoutBuffer;
QByteArray m_stderrBuffer;
QFuture<qint64> m_pidFinder;
qint64 m_processPID = -1;
QSet<qint64> m_processPids;
bool m_useCppDebugger = false;
QmlDebug::QmlDebugServicesPreset m_qmlDebugServices;
Utils::Port m_localGdbServerPort; // Local end of forwarded debug socket.
......@@ -262,20 +236,20 @@ private:
QString m_gdbserverSocket;
QString m_adb;
QStringList m_selector;
QRegExp m_logCatRegExp;
DebugHandShakeType m_handShakeMethod = SocketHandShake;
bool m_customPort = false;
QString m_packageName;
int m_socketHandShakePort = MIN_SOCKET_HANDSHAKE_PORT;
QByteArray m_pidsBuffer;
QScopedPointer<QTimer> m_timeoutTimer;
};
AndroidRunnerWorker::AndroidRunnerWorker(AndroidRunConfiguration *runConfig, Core::Id runMode,
const QString &packageName, const QStringList &selector)
: m_adbLogcatProcess(nullptr, deleter)
, m_psIsAlive(nullptr, deleter)
, m_pidsFinderProcess(nullptr, deleter)
, m_selector(selector)
, m_logCatRegExp(regExpLogcat)
, m_packageName(packageName)
{
Debugger::DebuggerRunConfigurationAspect *aspect
......@@ -339,23 +313,18 @@ AndroidRunnerWorker::AndroidRunnerWorker(AndroidRunConfiguration *runConfig, Cor
AndroidRunnerWorker::~AndroidRunnerWorker()
{
if (!m_pidFinder.isFinished())
m_pidFinder.cancel();
}
void AndroidRunnerWorker::forceStop()
{
runAdb(selector() << "shell" << "am" << "force-stop" << m_packageName, nullptr, 30);
// try killing it via kill -9
const QByteArray out = Utils::SynchronousProcess()
.runBlocking(m_adb, selector() << QStringLiteral("shell") << pidScript)
.allRawOutput();
qint64 pid = extractPID(out.simplified(), m_packageName);
if (pid != -1) {
adbKill(pid);
for (auto it = m_processPids.constBegin(); it != m_processPids.constEnd(); ++it) {
emit pidLost(*it);
adbKill(*it);
}
m_processPids.clear();
m_pidsBuffer.clear();
}
void AndroidRunnerWorker::asyncStart(const QString &intentName,
......@@ -369,8 +338,12 @@ void AndroidRunnerWorker::asyncStart(const QString &intentName,
this, &AndroidRunnerWorker::logcatReadStandardOutput);
connect(logcatProcess.get(), &QProcess::readyReadStandardError,
this, &AndroidRunnerWorker::logcatReadStandardError);
// Its assumed that the device or avd returned by selector() is online.
logcatProcess->start(m_adb, selector() << "logcat");
QStringList logcatArgs = selector() << "logcat" << "-v" << "time";
if (deviceSdkVersion() > 20)
logcatArgs << "-T" << "0";
logcatProcess->start(m_adb, logcatArgs);
QString errorMessage;
......@@ -508,9 +481,20 @@ void AndroidRunnerWorker::asyncStart(const QString &intentName,
QTC_ASSERT(!m_adbLogcatProcess, /**/);
m_adbLogcatProcess = std::move(logcatProcess);
m_pidFinder = Utils::onResultReady(Utils::runAsync(&findProcessPID, m_adb, selector(),
m_packageName),
bind(&AndroidRunnerWorker::onProcessIdChanged, this, _1));
m_timeoutTimer.reset(new QTimer);
m_timeoutTimer->setSingleShot(true);
connect(m_timeoutTimer.data(), &QTimer::timeout,
this,[this] { onProcessIdChanged(PidInfo{}); });
m_timeoutTimer->start(APP_START_TIMEOUT);
m_pidsFinderProcess.reset(new QProcess);
m_pidsFinderProcess->setProcessChannelMode(QProcess::MergedChannels);
connect(m_pidsFinderProcess.get(), &QProcess::readyRead, this, &AndroidRunnerWorker::findProcessPids);
connect(m_pidsFinderProcess.get(),
static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
this, [this] { onProcessIdChanged(PidInfo{}); });
m_pidsFinderProcess->start(m_adb, selector() << "shell" << pidScript);
}
bool AndroidRunnerWorker::adbShellAmNeedsQuotes()
......@@ -546,6 +530,19 @@ bool AndroidRunnerWorker::runAdb(const QStringList &args, QString *exitMessage,
return response.result == Utils::SynchronousProcessResponse::Finished;
}
int AndroidRunnerWorker::deviceSdkVersion()
{
Utils::SynchronousProcess adb;
adb.setTimeoutS(10);
Utils::SynchronousProcessResponse response
= adb.run(m_adb, selector() << "shell" << "getprop" << "ro.build.version.sdk");
if (response.result == Utils::SynchronousProcessResponse::StartFailed
|| response.result != Utils::SynchronousProcessResponse::Finished)
return -1;
return response.allOutput().trimmed().toInt();
}
void AndroidRunnerWorker::handleRemoteDebuggerRunning()
{
if (m_useCppDebugger) {
......@@ -559,21 +556,79 @@ void AndroidRunnerWorker::handleRemoteDebuggerRunning()
runAdb(selector() << "push" << tmp.fileName() << m_pongFile);
}
QTC_CHECK(m_processPID != -1);
QTC_CHECK(!m_processPids.isEmpty());
}
emit remoteProcessStarted(m_localGdbServerPort, m_qmlPort);
}
void AndroidRunnerWorker::asyncStop(const QVector<QStringList> &adbCommands)
void AndroidRunnerWorker::findProcessPids()
{
if (!m_pidFinder.isFinished())
m_pidFinder.cancel();
static QMap<qint64, QByteArray> extractedPids;
static auto oldPids = m_processPids;
if (m_processPID != -1) {
forceStop();
m_pidsBuffer += m_pidsFinderProcess->readAll();
while (!m_pidsBuffer.isEmpty()) {
const int to = m_pidsBuffer.indexOf('\n');
if (to < 0)
break;
if (to == 0) {
m_pidsBuffer = m_pidsBuffer.mid(1);
continue;
}
// = is used to delimit ps outputs
// is needed to know when an existins PID is killed
if (m_pidsBuffer[0] != '=') {
QByteArray tuple = m_pidsBuffer.left(to + 1).simplified();
QList<QByteArray> parts = tuple.split(':');
QByteArray commandName = parts.takeFirst();
if <