diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index eb2eaed9ddc..7e7bb51ec5f 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -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 diff --git a/src/libs/utils/algorithm.h b/src/libs/utils/algorithm.h index b781d68102d..d1c3f37bff4 100644 --- a/src/libs/utils/algorithm.h +++ b/src/libs/utils/algorithm.h @@ -22,7 +22,9 @@ #include #include +#include #include +#include namespace Utils { @@ -1532,6 +1534,17 @@ void addToHash(QHash *result, const QHash &additionalContents) result->insert(additionalContents); } +template +static std::size_t tupleHashHelper(uint seed, const Tuple &tuple, std::index_sequence) +{ + return qHashMulti(seed, (std::get(tuple), ...)); +} + +template std::size_t qHash(const std::tuple &tuple, uint seed = 0) +{ + return tupleHashHelper(seed, tuple, std::make_index_sequence()); +} + // 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 [[nodiscard]] bool insert(QSet &s, const U &v) diff --git a/src/libs/utils/datafromprocess.h b/src/libs/utils/datafromprocess.h new file mode 100644 index 00000000000..909252ce505 --- /dev/null +++ b/src/libs/utils/datafromprocess.h @@ -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 +#include +#include +#include + +#include +#include +#include +#include + +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 class DataFromProcess +{ +public: + class Parameters + { + public: + using OutputParser = std::function(const QString &)>; + using ErrorHandler = std::function; + + 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 allowedResults{ProcessResult::FinishedWithSuccess}; + }; + + static std::optional getData(const Parameters ¶ms); + + // TODO: async variant. + +private: + using Key = std::tuple; + using Value = std::pair, QDateTime>; + static inline QHash m_cache; + static inline QMutex m_cacheMutex; +}; + +template +inline std::optional DataFromProcess::getData(const Parameters ¶ms) +{ + 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 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; + if (params.allowedResults.contains(outputRetriever.result())) + data = params.parser(outputRetriever.cleanedStdOut()); + else if (params.errorHandler) + params.errorHandler(outputRetriever); + QMutexLocker cacheLocker(&m_cacheMutex); + m_cache.insert(key, std::make_pair(data, exeTimestamp)); + return data; +} + +} // namespace Utils diff --git a/src/libs/utils/pathchooser.cpp b/src/libs/utils/pathchooser.cpp index 437b94bb5cd..b8704061cd5 100644 --- a/src/libs/utils/pathchooser.cpp +++ b/src/libs/utils/pathchooser.cpp @@ -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::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::getData(params)) + return *version; + return {}; } // Extends BinaryVersionToolTipEventFilter to prepend the existing pathchooser diff --git a/src/libs/utils/utils.qbs b/src/libs/utils/utils.qbs index 7bf55d8f247..9cc3aced5a3 100644 --- a/src/libs/utils/utils.qbs +++ b/src/libs/utils/utils.qbs @@ -77,6 +77,7 @@ QtcLibrary { "cpplanguage_details.h", "crumblepath.cpp", "crumblepath.h", + "datafromprocess.h", "delegates.cpp", "delegates.h", "detailsbutton.cpp", diff --git a/src/plugins/clangtools/clangtoolssettings.cpp b/src/plugins/clangtools/clangtoolssettings.cpp index 6699be10bdc..c8494c60ee0 100644 --- a/src/plugins/clangtools/clangtoolssettings.cpp +++ b/src/plugins/clangtools/clangtoolssettings.cpp @@ -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 diff --git a/src/plugins/clangtools/diagnosticconfigswidget.cpp b/src/plugins/clangtools/diagnosticconfigswidget.cpp index b787777f9d9..dcbcd70e13d 100644 --- a/src/plugins/clangtools/diagnosticconfigswidget.cpp +++ b/src/plugins/clangtools/diagnosticconfigswidget.cpp @@ -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 &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, diff --git a/src/plugins/clangtools/executableinfo.cpp b/src/plugins/clangtools/executableinfo.cpp index 22898b5734a..b1c5e2343d5 100644 --- a/src/plugins/clangtools/executableinfo.cpp +++ b/src/plugins/clangtools/executableinfo.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -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 { + 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; + }; - 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::Parameters params({executable, arguments}, parser); + params.environment.setupEnglishOutput(); + params.errorHandler = handleProcessError; + if (const auto checks = DataFromProcess::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,29 +85,41 @@ static ClazyChecks querySupportedClazyChecks(const FilePath &executablePath) // ... // ] // } + static const auto parser = [](const QString &jsonOutput) -> std::optional { + 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()) + continue; + info.level = checkObject["level"].toInt(); + for (const QJsonValue &item : checkObject["categories"].toArray()) + info.topics.append(item.toString().trimmed()); + infos << info; + } + return infos; + }; - ClazyChecks infos; - - const QJsonDocument document = QJsonDocument::fromJson(jsonOutput.toUtf8()); - if (document.isNull()) - return {}; - const QJsonArray checksArray = document.object()["checks"].toArray(); - - for (const QJsonValue &item: checksArray) { - const QJsonObject checkObject = item.toObject(); - - ClazyCheck info; - info.name = checkObject["name"].toString().trimmed(); - if (info.name.isEmpty()) - continue; - info.level = checkObject["level"].toInt(); - for (const QJsonValue &item : checkObject["categories"].toArray()) - info.topics.append(item.toString().trimmed()); - - infos << info; + static const QString queryFlag = "-supported-checks-json"; + DataFromProcess::Parameters params(CommandLine(executablePath, {queryFlag}), + parser); + params.environment.setupEnglishOutput(); + params.errorHandler = handleProcessError; + auto checks = DataFromProcess::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::getData(params); } - - return infos; + if (checks) + return *checks; + return {}; } ClangTidyInfo::ClangTidyInfo(const FilePath &executablePath) @@ -141,76 +127,80 @@ 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); - 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. - 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; + static const auto parser = [](const QString &stdOut) -> std::optional { + 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. + static const QStringList versionPrefixes{"clazy version ", "clazy version: "}; + const QString line = stream.readLine().simplified(); + for (const QString &prefix : versionPrefixes) { + if (line.startsWith(prefix)) + return QVersionNumber::fromString(line.mid(prefix.length())); } } - } + return {}; + }; + DataFromProcess::Parameters params({{executablePath, {"--version"}}, parser}); + params.environment.setupEnglishOutput(); + if (const auto v = DataFromProcess::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: // ... + const auto parser = [&clangToolPath](const QString &stdOut) -> std::optional { + QString output = stdOut; + QTextStream stream(&output); + const QString path = clangToolPath.parentDir().parentDir() + .pathAppended(stream.readLine()).toString(); + const auto filePath = FilePath::fromUserInput(QDir::cleanPath(path)); + if (filePath.exists()) + return filePath; + return {}; + }; - // Parse - QTextStream stream(&output); - const QString path = clangToolPath.parentDir().parentDir() - .pathAppended(stream.readLine()).toString(); - const auto filePath = FilePath::fromUserInput(QDir::cleanPath(path)); - if (filePath.exists()) - return filePath; + DataFromProcess::Parameters params({clangToolPath, + {"someFilePath", "--", "-print-resource-dir"}}, + parser); + params.environment.setupEnglishOutput(); + params.allowedResults << ProcessResult::FinishedWithError; + if (const auto filePath = DataFromProcess::getData(params)) + return *filePath; return {}; } QString queryVersion(const FilePath &clangToolPath, QueryFailMode failMode) { - QString output = runExecutable(CommandLine(clangToolPath, {"--version"}), failMode); - QTextStream stream(&output); - while (!stream.atEnd()) { - static const QStringList versionPrefixes{"LLVM version ", "clang version: "}; - const QString line = stream.readLine().simplified(); - for (const QString &prefix : versionPrefixes) { - auto idx = line.indexOf(prefix); - if (idx >= 0) - return line.mid(idx + prefix.length()); + static const auto parser = [](const QString &stdOut) -> std::optional { + QString output = stdOut; + QTextStream stream(&output); + while (!stream.atEnd()) { + static const QStringList versionPrefixes{"LLVM version ", "clang version: "}; + const QString line = stream.readLine().simplified(); + for (const QString &prefix : versionPrefixes) { + auto idx = line.indexOf(prefix); + if (idx >= 0) + return line.mid(idx + prefix.length()); + } } - } + return {}; + }; + DataFromProcess::Parameters params({clangToolPath, {"--version"}}, parser); + params.environment.setupEnglishOutput(); + if (failMode == QueryFailMode::Noisy) + params.errorHandler = handleProcessError; + if (const auto version = DataFromProcess::getData(params)) + return *version; return {}; } @@ -233,7 +223,5 @@ QPair getClangIncludeDirAndVersion(const FilePath &clangToolP return it.value(); } -QHash> ClazyStandaloneInfo::cache; - } // namespace Internal } // namespace ClangTools diff --git a/src/plugins/clangtools/executableinfo.h b/src/plugins/clangtools/executableinfo.h index cfa51b98b2c..5db586b5356 100644 --- a/src/plugins/clangtools/executableinfo.h +++ b/src/plugins/clangtools/executableinfo.h @@ -5,8 +5,6 @@ #include -#include -#include #include #include #include @@ -40,16 +38,11 @@ using ClazyChecks = QVector; 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> cache; }; } // namespace Internal diff --git a/src/plugins/clangtools/runsettingswidget.cpp b/src/plugins/clangtools/runsettingswidget.cpp index cd0dc6a1ee3..81a8b7c644a 100644 --- a/src/plugins/clangtools/runsettingswidget.cpp +++ b/src/plugins/clangtools/runsettingswidget.cpp @@ -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) diff --git a/src/plugins/projectexplorer/msvctoolchain.cpp b/src/plugins/projectexplorer/msvctoolchain.cpp index 67d3f327814..5c1ded6a84b 100644 --- a/src/plugins/projectexplorer/msvctoolchain.cpp +++ b/src/plugins/projectexplorer/msvctoolchain.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -42,8 +43,6 @@ #include #include -#include - 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> 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; + return info; + }; + const auto info = DataFromProcess::getData({{filePath, {"--version"}}, parser}); + return info ? *info : ClangClInfo(); } } // namespace ProjectExplorer::Internal