// 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 "qtcprocess.h" #include "settingsdatabase.h" #include #include #include #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< std::optional(const QString & /* stdOut */, const QString & /* stdErr */)>; using ErrorHandler = std::function; using Callback = 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; Callback callback; Callback cachedValueChangedCallback; bool persistValue = true; QList allowedResults{ProcessResult::FinishedWithSuccess}; bool disableUnixTerminal = false; }; // Use the first variant whenever possible. static void provideData(const Parameters ¶ms); static std::optional getData(const Parameters ¶ms); private: using Key = std::tuple; using Value = std::pair, QDateTime>; static std::optional getOrProvideData(const Parameters ¶ms); static std::optional handleProcessFinished(const Parameters ¶ms, const QDateTime &exeTimestamp, const Key &cacheKey, const std::shared_ptr &process); static inline QHash m_cache; static inline QMutex m_cacheMutex; }; template inline void DataFromProcess::provideData(const Parameters ¶ms) { QTC_ASSERT(params.callback, return); getOrProvideData(params); } template inline std::optional DataFromProcess::getData(const Parameters ¶ms) { QTC_ASSERT(!params.callback, return {}); return getOrProvideData(params); } template inline std::optional DataFromProcess::getOrProvideData(const Parameters ¶ms) { if (params.commandLine.executable().isEmpty()) { if (params.callback) params.callback({}); 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; } const auto outputRetriever = std::make_shared(); outputRetriever->setCommand(params.commandLine); outputRetriever->setEnvironment(params.environment); if (params.disableUnixTerminal) outputRetriever->setDisableUnixTerminal(); if (params.persistValue && !params.callback) { const QChar separator = params.commandLine.executable().pathListSeparator(); const QString stringKey = params.commandLine.executable().toUrlishString() + separator + params.commandLine.arguments() + separator + params.environment.toStringList().join(separator); if (const QByteArray json = SettingsDatabase::value(stringKey).toByteArray(); !json.isEmpty()) { if (const auto doc = QJsonDocument::fromJson(json); doc.isObject()) { const QJsonObject settingsObject = doc.object(); const QString out = settingsObject["stdout"].toString(); const QString err = settingsObject["stderr"].toString(); std::optional data = params.parser(out, err); QMutexLocker cacheLocker(&m_cacheMutex); m_cache.insert(key, std::make_pair(data, exeTimestamp)); QObject::connect( outputRetriever.get(), &Process::done, [params, exeTimestamp, key, outputRetriever] { handleProcessFinished(params, exeTimestamp, key, outputRetriever); }); outputRetriever->start(); return data; } } } if (params.callback) { QObject::connect(outputRetriever.get(), &Process::done, [params, exeTimestamp, key, outputRetriever] { handleProcessFinished(params, exeTimestamp, key, outputRetriever); }); outputRetriever->start(); return {}; } outputRetriever->runBlocking(params.timeout); return handleProcessFinished(params, exeTimestamp, key, outputRetriever); } template inline std::optional DataFromProcess::handleProcessFinished( const Parameters ¶ms, const QDateTime &exeTimestamp, const Key &cacheKey, const std::shared_ptr &process) { // Do not store into cache: The next call might succeed. if (process->result() == ProcessResult::Canceled) { if (params.callback) params.callback({}); return {}; } std::optional data; if (params.allowedResults.contains(process->result())) { if (params.persistValue) { const QChar separator = params.commandLine.executable().pathListSeparator(); const QString stringKey = params.commandLine.executable().toUrlishString() + separator + params.commandLine.arguments() + separator + params.environment.toStringList().join(separator); QJsonObject settingsObject; const QString out = process->cleanedStdOut(); const QString err = process->cleanedStdErr(); settingsObject["stdout"] = out; settingsObject["stderr"] = err; SettingsDatabase::setValue( stringKey, QString::fromUtf8(QJsonDocument(settingsObject).toJson(QJsonDocument::Compact))); } data = params.parser(process->cleanedStdOut(), process->cleanedStdErr()); } else if (params.errorHandler) { params.errorHandler(*process); } QMutexLocker cacheLocker(&m_cacheMutex); if (params.cachedValueChangedCallback) { const auto it = m_cache.constFind(cacheKey); if (it != m_cache.constEnd() && it.value().second == exeTimestamp) { if (it.value().first != data) params.cachedValueChangedCallback(data); } } m_cache.insert(cacheKey, std::make_pair(data, exeTimestamp)); if (params.callback) { params.callback(data); return {}; } return data; } } // namespace Utils