Utils: Add convenience class for parsing and caching process output

... and base the existing uses of this pattern on it.

Change-Id: I0eaf7535b68ff8c3e8e1c923ce08e63896cc83f7
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Christian Kandeler
2024-02-16 16:47:43 +01:00
parent 7c5a0e6bb2
commit 737bf48a0c
11 changed files with 238 additions and 160 deletions

View File

@@ -31,6 +31,7 @@ add_qtc_library(Utils
completingtextedit.cpp completingtextedit.h
cpplanguage_details.h
crumblepath.cpp crumblepath.h
datafromprocess.h
delegates.cpp delegates.h
detailsbutton.cpp detailsbutton.h
detailswidget.cpp detailswidget.h

View File

@@ -22,7 +22,9 @@
#include <memory>
#include <optional>
#include <tuple>
#include <type_traits>
#include <utility>
namespace Utils
{
@@ -1532,6 +1534,17 @@ void addToHash(QHash<Key, T> *result, const QHash<Key, T> &additionalContents)
result->insert(additionalContents);
}
template <typename Tuple, std::size_t... I>
static std::size_t tupleHashHelper(uint seed, const Tuple &tuple, std::index_sequence<I...>)
{
return qHashMulti(seed, (std::get<I>(tuple), ...));
}
template<typename... T> std::size_t qHash(const std::tuple<T...> &tuple, uint seed = 0)
{
return tupleHashHelper(seed, tuple, std::make_index_sequence<sizeof...(T)>());
}
// Workaround for missing information from QSet::insert()
// Return type could be a pair like for std::set, but we never use the iterator anyway.
template<typename T, typename U> [[nodiscard]] bool insert(QSet<T> &s, const U &v)

View File

@@ -0,0 +1,95 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include "algorithm.h"
#include "commandline.h"
#include "environment.h"
#include "filepath.h"
#include "process.h"
#include <QDateTime>
#include <QHash>
#include <QMutex>
#include <QMutexLocker>
#include <chrono>
#include <functional>
#include <optional>
#include <utility>
namespace Utils {
// Use this facility for cached retrieval of data from a tool that always returns the same
// output for the same parameters and is side effect free.
// A prime example is version info via a --version switch.
template<typename Data> class DataFromProcess
{
public:
class Parameters
{
public:
using OutputParser = std::function<std::optional<Data>(const QString &)>;
using ErrorHandler = std::function<void(const Process &)>;
Parameters(const CommandLine &cmdLine, const OutputParser &parser)
: commandLine(cmdLine)
, parser(parser)
{}
CommandLine commandLine;
Environment environment = Environment::systemEnvironment();
std::chrono::seconds timeout = std::chrono::seconds(10);
OutputParser parser;
ErrorHandler errorHandler;
QList<ProcessResult> allowedResults{ProcessResult::FinishedWithSuccess};
};
static std::optional<Data> getData(const Parameters &params);
// TODO: async variant.
private:
using Key = std::tuple<FilePath, QStringList, QString>;
using Value = std::pair<std::optional<Data>, QDateTime>;
static inline QHash<Key, Value> m_cache;
static inline QMutex m_cacheMutex;
};
template<typename Data>
inline std::optional<Data> DataFromProcess<Data>::getData(const Parameters &params)
{
if (params.commandLine.executable().isEmpty())
return {};
const auto key = std::make_tuple(params.commandLine.executable(),
params.environment.toStringList(),
params.commandLine.arguments());
const QDateTime exeTimestamp = params.commandLine.executable().lastModified();
{
QMutexLocker<QMutex> cacheLocker(&m_cacheMutex);
const auto it = m_cache.constFind(key);
if (it != m_cache.constEnd() && it.value().second == exeTimestamp)
return it.value().first;
}
Process outputRetriever;
outputRetriever.setCommand(params.commandLine);
outputRetriever.runBlocking(params.timeout);
// Do not store into cache: The next call might succeed.
if (outputRetriever.result() == ProcessResult::Canceled)
return {};
std::optional<Data> data;
if (params.allowedResults.contains(outputRetriever.result()))
data = params.parser(outputRetriever.cleanedStdOut());
else if (params.errorHandler)
params.errorHandler(outputRetriever);
QMutexLocker<QMutex> cacheLocker(&m_cacheMutex);
m_cache.insert(key, std::make_pair(data, exeTimestamp));
return data;
}
} // namespace Utils

View File

@@ -5,6 +5,7 @@
#include "async.h"
#include "commandline.h"
#include "datafromprocess.h"
#include "environment.h"
#include "fileutils.h"
#include "guard.h"
@@ -130,15 +131,12 @@ bool BinaryVersionToolTipEventFilter::eventFilter(QObject *o, QEvent *e)
QString BinaryVersionToolTipEventFilter::toolVersion(const CommandLine &cmd)
{
if (cmd.executable().isEmpty())
return QString();
Process proc;
proc.setCommand(cmd);
DataFromProcess<QString>::Parameters params(cmd, [](const QString &output) { return output; });
using namespace std::chrono_literals;
proc.runBlocking(1s);
if (proc.result() != ProcessResult::FinishedWithSuccess)
return QString();
return proc.allOutput();
params.timeout = 1s;
if (const auto version = DataFromProcess<QString>::getData(params))
return *version;
return {};
}
// Extends BinaryVersionToolTipEventFilter to prepend the existing pathchooser

View File

@@ -77,6 +77,7 @@ QtcLibrary {
"cpplanguage_details.h",
"crumblepath.cpp",
"crumblepath.h",
"datafromprocess.h",
"delegates.cpp",
"delegates.h",
"detailsbutton.cpp",

View File

@@ -230,7 +230,7 @@ VersionAndSuffix ClangToolsSettings::clangTidyVersion()
QVersionNumber ClangToolsSettings::clazyVersion()
{
return ClazyStandaloneInfo::getInfo(Internal::toolExecutable(ClangToolType::Clazy)).version;
return ClazyStandaloneInfo(Internal::toolExecutable(ClangToolType::Clazy)).version;
}
} // namespace Internal

View File

@@ -1263,7 +1263,7 @@ QString removeClangTidyCheck(const QString &checks, const QString &check)
QString removeClazyCheck(const QString &checks, const QString &check)
{
const ClazyStandaloneInfo clazyInfo = ClazyStandaloneInfo::getInfo(toolExecutable(ClangToolType::Clazy));
const ClazyStandaloneInfo clazyInfo = ClazyStandaloneInfo(toolExecutable(ClangToolType::Clazy));
ClazyChecksTreeModel model(clazyInfo.supportedChecks);
model.enableChecks(checks.split(',', Qt::SkipEmptyParts));
const QModelIndex index = model.indexForName(check.mid(QString("clazy-").length()));
@@ -1314,7 +1314,7 @@ void disableChecks(const QList<Diagnostic> &diagnostics)
if (config.clazyMode() == ClangDiagnosticConfig::ClazyMode::UseDefaultChecks) {
config.setClazyMode(ClangDiagnosticConfig::ClazyMode::UseCustomChecks);
const ClazyStandaloneInfo clazyInfo
= ClazyStandaloneInfo::getInfo(toolExecutable(ClangToolType::Clazy));
= ClazyStandaloneInfo(toolExecutable(ClangToolType::Clazy));
config.setChecks(ClangToolType::Clazy, clazyInfo.defaultChecks.join(','));
}
config.setChecks(ClangToolType::Clazy,

View File

@@ -6,6 +6,7 @@
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
#include <utils/datafromprocess.h>
#include <utils/environment.h>
#include <utils/process.h>
@@ -21,27 +22,10 @@ using namespace Utils;
namespace ClangTools {
namespace Internal {
static QString runExecutable(const Utils::CommandLine &commandLine, QueryFailMode queryFailMode)
static void handleProcessError(const Process &p)
{
if (commandLine.executable().isEmpty() || !commandLine.executable().toFileInfo().isExecutable())
return {};
Process cpp;
Environment env = Environment::systemEnvironment();
env.setupEnglishOutput();
cpp.setEnvironment(env);
cpp.setCommand(commandLine);
cpp.runBlocking();
if (cpp.result() != ProcessResult::FinishedWithSuccess
&& (queryFailMode == QueryFailMode::Noisy
|| cpp.result() != ProcessResult::FinishedWithError)) {
Core::MessageManager::writeFlashing(cpp.exitMessage());
Core::MessageManager::writeFlashing(QString::fromUtf8(cpp.allRawOutput()));
return {};
}
return cpp.cleanedStdOut();
Core::MessageManager::writeFlashing(p.exitMessage());
Core::MessageManager::writeFlashing(QString::fromUtf8(p.allRawOutput()));
}
static QStringList queryClangTidyChecks(const FilePath &executable,
@@ -51,47 +35,37 @@ static QStringList queryClangTidyChecks(const FilePath &executable,
if (!checksArgument.isEmpty())
arguments.prepend(checksArgument);
const CommandLine commandLine(executable, arguments);
QString output = runExecutable(commandLine, QueryFailMode::Noisy);
if (output.isEmpty())
return {};
// Expected output is (clang-tidy 8.0):
// Enabled checks:
// abseil-duration-comparison
// abseil-duration-division
// abseil-duration-factory-float
// ...
static const auto parser = [](const QString &stdOut) -> std::optional<QStringList> {
QString output = stdOut;
QTextStream stream(&output);
QString line = stream.readLine();
if (!line.startsWith("Enabled checks:"))
return {};
QStringList checks;
while (!stream.atEnd()) {
const QString candidate = stream.readLine().trimmed();
if (!candidate.isEmpty())
checks << candidate;
}
return checks;
};
DataFromProcess<QStringList>::Parameters params({executable, arguments}, parser);
params.environment.setupEnglishOutput();
params.errorHandler = handleProcessError;
if (const auto checks = DataFromProcess<QStringList>::getData(params))
return *checks;
return {};
}
static ClazyChecks querySupportedClazyChecks(const FilePath &executablePath)
{
static const QString queryFlag = "-supported-checks-json";
QString jsonOutput = runExecutable(CommandLine(executablePath, {queryFlag}),
QueryFailMode::Noisy);
// Some clazy 1.6.x versions have a bug where they expect an argument after the
// option.
if (jsonOutput.isEmpty())
jsonOutput = runExecutable(CommandLine(executablePath, {queryFlag, "dummy"}),
QueryFailMode::Noisy);
if (jsonOutput.isEmpty())
return {};
// Expected output is (clazy-standalone 1.6):
// {
// "available_categories" : ["readability", "qt4", "containers", ... ],
@@ -111,17 +85,14 @@ static ClazyChecks querySupportedClazyChecks(const FilePath &executablePath)
// ...
// ]
// }
ClazyChecks infos;
static const auto parser = [](const QString &jsonOutput) -> std::optional<ClazyChecks> {
const QJsonDocument document = QJsonDocument::fromJson(jsonOutput.toUtf8());
if (document.isNull())
return {};
const QJsonArray checksArray = document.object()["checks"].toArray();
ClazyChecks infos;
for (const QJsonValue &item: checksArray) {
const QJsonObject checkObject = item.toObject();
ClazyCheck info;
info.name = checkObject["name"].toString().trimmed();
if (info.name.isEmpty())
@@ -129,11 +100,26 @@ static ClazyChecks querySupportedClazyChecks(const FilePath &executablePath)
info.level = checkObject["level"].toInt();
for (const QJsonValue &item : checkObject["categories"].toArray())
info.topics.append(item.toString().trimmed());
infos << info;
}
return infos;
};
static const QString queryFlag = "-supported-checks-json";
DataFromProcess<ClazyChecks>::Parameters params(CommandLine(executablePath, {queryFlag}),
parser);
params.environment.setupEnglishOutput();
params.errorHandler = handleProcessError;
auto checks = DataFromProcess<ClazyChecks>::getData(params);
if (!checks) {
// Some clazy 1.6.x versions have a bug where they expect an argument after the
// option.
params.commandLine = CommandLine(executablePath, {queryFlag, "dummy"});
checks = DataFromProcess<ClazyChecks>::getData(params);
}
if (checks)
return *checks;
return {};
}
ClangTidyInfo::ClangTidyInfo(const FilePath &executablePath)
@@ -141,54 +127,39 @@ ClangTidyInfo::ClangTidyInfo(const FilePath &executablePath)
, supportedChecks(queryClangTidyChecks(executablePath, "-checks=*"))
{}
ClazyStandaloneInfo ClazyStandaloneInfo::getInfo(const FilePath &executablePath)
{
const QDateTime timeStamp = executablePath.lastModified();
const auto it = cache.find(executablePath);
if (it == cache.end()) {
const ClazyStandaloneInfo info(executablePath);
cache.insert(executablePath, {timeStamp, info});
return info;
}
if (it->first != timeStamp) {
it->first = timeStamp;
it->second = ClazyStandaloneInfo::getInfo(executablePath);
}
return it->second;
}
ClazyStandaloneInfo::ClazyStandaloneInfo(const FilePath &executablePath)
: defaultChecks(queryClangTidyChecks(executablePath, {})) // Yup, behaves as clang-tidy.
, supportedChecks(querySupportedClazyChecks(executablePath))
{
QString output = runExecutable({executablePath, {"--version"}}, QueryFailMode::Silent);
static const auto parser = [](const QString &stdOut) -> std::optional<QVersionNumber> {
QString output = stdOut;
QTextStream stream(&output);
while (!stream.atEnd()) {
// It's just "clazy version " right now, but let's be prepared for someone adding a colon
// later on.
// It's just "clazy version " right now, but let's be prepared for someone
// adding a colon later on.
static const QStringList versionPrefixes{"clazy version ", "clazy version: "};
const QString line = stream.readLine().simplified();
for (const QString &prefix : versionPrefixes) {
if (line.startsWith(prefix)) {
version = QVersionNumber::fromString(line.mid(prefix.length()));
break;
}
if (line.startsWith(prefix))
return QVersionNumber::fromString(line.mid(prefix.length()));
}
}
return {};
};
DataFromProcess<QVersionNumber>::Parameters params({{executablePath, {"--version"}}, parser});
params.environment.setupEnglishOutput();
if (const auto v = DataFromProcess<QVersionNumber>::getData(params))
version = *v;
}
static FilePath queryResourceDir(const FilePath &clangToolPath)
{
QString output = runExecutable(CommandLine(clangToolPath, {"someFilePath", "--",
"-print-resource-dir"}),
QueryFailMode::Silent);
// Expected output is (clang-tidy 10):
// lib/clang/10.0.1
// Error while trying to load a compilation database:
// ...
// Parse
const auto parser = [&clangToolPath](const QString &stdOut) -> std::optional<FilePath> {
QString output = stdOut;
QTextStream stream(&output);
const QString path = clangToolPath.parentDir().parentDir()
.pathAppended(stream.readLine()).toString();
@@ -196,11 +167,22 @@ static FilePath queryResourceDir(const FilePath &clangToolPath)
if (filePath.exists())
return filePath;
return {};
};
DataFromProcess<FilePath>::Parameters params({clangToolPath,
{"someFilePath", "--", "-print-resource-dir"}},
parser);
params.environment.setupEnglishOutput();
params.allowedResults << ProcessResult::FinishedWithError;
if (const auto filePath = DataFromProcess<FilePath>::getData(params))
return *filePath;
return {};
}
QString queryVersion(const FilePath &clangToolPath, QueryFailMode failMode)
{
QString output = runExecutable(CommandLine(clangToolPath, {"--version"}), failMode);
static const auto parser = [](const QString &stdOut) -> std::optional<QString> {
QString output = stdOut;
QTextStream stream(&output);
while (!stream.atEnd()) {
static const QStringList versionPrefixes{"LLVM version ", "clang version: "};
@@ -212,6 +194,14 @@ QString queryVersion(const FilePath &clangToolPath, QueryFailMode failMode)
}
}
return {};
};
DataFromProcess<QString>::Parameters params({clangToolPath, {"--version"}}, parser);
params.environment.setupEnglishOutput();
if (failMode == QueryFailMode::Noisy)
params.errorHandler = handleProcessError;
if (const auto version = DataFromProcess<QString>::getData(params))
return *version;
return {};
}
static QPair<FilePath, QString> clangIncludeDirAndVersion(const FilePath &clangToolPath)
@@ -233,7 +223,5 @@ QPair<FilePath, QString> getClangIncludeDirAndVersion(const FilePath &clangToolP
return it.value();
}
QHash<Utils::FilePath, QPair<QDateTime, ClazyStandaloneInfo>> ClazyStandaloneInfo::cache;
} // namespace Internal
} // namespace ClangTools

View File

@@ -5,8 +5,6 @@
#include <utils/filepath.h>
#include <QDateTime>
#include <QHash>
#include <QPair>
#include <QStringList>
#include <QVector>
@@ -40,16 +38,11 @@ using ClazyChecks = QVector<ClazyCheck>;
class ClazyStandaloneInfo
{
public:
static ClazyStandaloneInfo getInfo(const Utils::FilePath &executablePath);
ClazyStandaloneInfo(const Utils::FilePath &executablePath);
QVersionNumber version;
QStringList defaultChecks;
ClazyChecks supportedChecks;
private:
ClazyStandaloneInfo(const Utils::FilePath &executablePath);
static QHash<Utils::FilePath, QPair<QDateTime, ClazyStandaloneInfo>> cache;
};
} // namespace Internal

View File

@@ -85,7 +85,7 @@ static ClangDiagnosticConfigsWidget *createEditWidget(const ClangDiagnosticConfi
return new DiagnosticConfigsWidget(configs,
configToSelect,
ClangTidyInfo(clangTidyPath),
ClazyStandaloneInfo::getInfo(clazyStandalonePath));
ClazyStandaloneInfo(clazyStandalonePath));
}
void RunSettingsWidget::fromSettings(const RunSettings &s)

View File

@@ -17,6 +17,7 @@
#include <utils/algorithm.h>
#include <utils/async.h>
#include <utils/datafromprocess.h>
#include <utils/environment.h>
#include <utils/hostosinfo.h>
#include <utils/pathchooser.h>
@@ -42,8 +43,6 @@
#include <QFormLayout>
#include <QLabel>
#include <utility>
using namespace Utils;
using namespace std::chrono_literals;
@@ -1580,7 +1579,6 @@ private:
FilePath m_filePath;
QVersionNumber m_version;
Abi m_defaultAbi;
static inline QHash<FilePath, std::pair<ClangClInfo, QDateTime>> m_cache;
};
static const MsvcToolchain *selectMsvcToolChain(const QString &displayedVarsBat,
@@ -2280,18 +2278,8 @@ ClangClInfo ClangClInfo::getInfo(const FilePath &filePath)
{
QTC_ASSERT(!filePath.isEmpty(), return {});
auto &entry = m_cache[filePath];
ClangClInfo &info = entry.first;
const QDateTime lastModified = filePath.lastModified();
if (entry.second == lastModified)
return info;
entry.second = lastModified;
Process clangClProcess;
clangClProcess.setCommand({filePath, {"--version"}});
clangClProcess.runBlocking();
if (clangClProcess.result() == ProcessResult::FinishedWithSuccess) {
const QString stdOut = clangClProcess.cleanedStdOut();
static const auto parser = [](const QString &stdOut) {
ClangClInfo info;
const QRegularExpressionMatch versionMatch
= QRegularExpression("clang version (\\d+(\\.\\d+)+)").match(stdOut);
if (versionMatch.hasMatch())
@@ -2314,9 +2302,10 @@ ClangClInfo ClangClInfo::getInfo(const FilePath &filePath)
detectedAbi.wordWidth());
}
}
}
m_cache.insert(filePath, entry);
return info;
};
const auto info = DataFromProcess<ClangClInfo>::getData({{filePath, {"--version"}}, parser});
return info ? *info : ClangClInfo();
}
} // namespace ProjectExplorer::Internal