diff --git a/src/plugins/axivion/CMakeLists.txt b/src/plugins/axivion/CMakeLists.txt index 7e51f8203eb..18b645c596f 100644 --- a/src/plugins/axivion/CMakeLists.txt +++ b/src/plugins/axivion/CMakeLists.txt @@ -1,7 +1,7 @@ add_qtc_plugin(Axivion PLUGIN_DEPENDS Core Debugger ProjectExplorer TextEditor - DEPENDS Qt::Network Qt::Widgets ExtensionSystem Utils + DEPENDS Qt::Network Qt::Sql Qt::Widgets ExtensionSystem Utils LONG_DESCRIPTION_MD AxivionDescription.md SOURCES axivionperspective.cpp axivionperspective.h @@ -14,6 +14,7 @@ add_qtc_plugin(Axivion dashboard/error.cpp dashboard/error.h dynamiclistmodel.cpp dynamiclistmodel.h issueheaderview.cpp issueheaderview.h + localbuild.cpp localbuild.h ) file(GLOB_RECURSE images RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} images/*) diff --git a/src/plugins/axivion/axivion.qbs b/src/plugins/axivion/axivion.qbs index 513d23aae57..916229c2386 100644 --- a/src/plugins/axivion/axivion.qbs +++ b/src/plugins/axivion/axivion.qbs @@ -9,8 +9,7 @@ QtcPlugin { Depends { name: "ProjectExplorer" } Depends { name: "TextEditor" } Depends { name: "Utils" } - Depends { name: "Qt.widgets" } - Depends { name: "Qt.network" } + Depends { name: "Qt"; submodules: ["network", "sql", "widgets"] } files: [ "axivionperspective.cpp", @@ -24,6 +23,8 @@ QtcPlugin { "dynamiclistmodel.h", "issueheaderview.cpp", "issueheaderview.h", + "localbuild.cpp", + "localbuild.h", ] cpp.includePaths: base.concat(["."]) // needed for the generated stuff below diff --git a/src/plugins/axivion/axivionperspective.cpp b/src/plugins/axivion/axivionperspective.cpp index 872fc195882..60884f40cc0 100644 --- a/src/plugins/axivion/axivionperspective.cpp +++ b/src/plugins/axivion/axivionperspective.cpp @@ -9,6 +9,7 @@ #include "dashboard/dto.h" #include "issueheaderview.h" #include "dynamiclistmodel.h" +#include "localbuild.h" #include #include @@ -212,6 +213,7 @@ private: void fetchTable(); void fetchIssues(const IssueListSearch &search); void onFetchRequested(int startRow, int limit); + void switchDashboard(bool local); void hideOverlays(); void openFilterHelp(); @@ -308,11 +310,11 @@ IssuesWidget::IssuesWidget(QWidget *parent) localLayout->addWidget(m_localDashBoard); connect(&settings(), &AxivionSettings::suitePathValidated, this, [this] { const auto info = settings().versionInfo(); - const bool enable = info && !info->versionNumber.isEmpty(); // for now + const bool enable = info && !info->versionNumber.isEmpty(); m_localBuild->setEnabled(enable); - m_localDashBoard->setEnabled(enable); + checkForLocalBuildResults(m_currentProject, [this] { m_localDashBoard->setEnabled(true); }); }); - + connect(m_localDashBoard, &QToolButton::clicked, this, &IssuesWidget::switchDashboard); m_typesButtonGroup = new QButtonGroup(this); m_typesButtonGroup->setExclusive(true); m_typesLayout = new QHBoxLayout; @@ -855,7 +857,7 @@ void IssuesWidget::updateBasicProjectInfo(const std::optional suiteVersionInfo = settings().versionInfo(); m_localBuild->setEnabled(!m_currentProject.isEmpty() && suiteVersionInfo && !suiteVersionInfo->versionNumber.isEmpty()); - m_localDashBoard->setEnabled(true); + checkForLocalBuildResults(m_currentProject, [this] { m_localDashBoard->setEnabled(true); }); } void IssuesWidget::updateAllFilters(const QVariant &namedFilter) @@ -1010,6 +1012,16 @@ void IssuesWidget::showErrorMessage(const QString &message) m_stack->setCurrentIndex(1); } +void IssuesWidget::switchDashboard(bool local) +{ + if (local) { + QTC_ASSERT(!m_currentProject.isEmpty(), return); + startLocalDashboard(m_currentProject, {}); + } else { + // TODO switch back + } +} + void IssuesWidget::hideOverlays() { if (m_overlay) diff --git a/src/plugins/axivion/axivionplugin.cpp b/src/plugins/axivion/axivionplugin.cpp index a442f94206a..e212a661c5d 100644 --- a/src/plugins/axivion/axivionplugin.cpp +++ b/src/plugins/axivion/axivionplugin.cpp @@ -8,6 +8,7 @@ #include "axiviontr.h" #include "dashboard/dto.h" #include "dashboard/error.h" +#include "localbuild.h" #include #include @@ -1227,6 +1228,14 @@ class AxivionPlugin final : public ExtensionSystem::IPlugin connect(EditorManager::instance(), &EditorManager::documentClosed, dd, &AxivionPluginPrivate::onDocumentClosed); } + + ShutdownFlag aboutToShutdown() final + { + if (shutdownAllLocalDashboards([this] { emit asynchronousShutdownFinished(); })) + return AsynchronousShutdown; + else + return SynchronousShutdown; + } }; void fetchIssueInfo(const QString &id) diff --git a/src/plugins/axivion/localbuild.cpp b/src/plugins/axivion/localbuild.cpp new file mode 100644 index 00000000000..7e193fbaf80 --- /dev/null +++ b/src/plugins/axivion/localbuild.cpp @@ -0,0 +1,296 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "localbuild.h" + +#include "axivionperspective.h" +#include "axivionsettings.h" +#include "axiviontr.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Tasking; +using namespace Utils; + +namespace Axivion::Internal { + +Q_LOGGING_CATEGORY(sqlLog, "qtc.axivion.sql", QtWarningMsg) +Q_LOGGING_CATEGORY(localDashLog, "qtc.axivion.localdashboard", QtWarningMsg) + +struct LocalDashboard +{ + QString id; + CommandLine startCommandLine; + CommandLine stopCommandLine; + Environment environment; + + // access data will be updated after successful start + QUrl localUrl; + QString localUser; + QString localProject; + QByteArray pass; +}; + +class LocalBuild +{ +public: + LocalBuild() {} + ~LocalBuild() + { + QTC_CHECK(m_startedDashboards.isEmpty()); // shutdownAll() must be done already + QTC_CHECK(m_startedDashboardTrees.empty()); + } + + void startDashboard(const QString &projectName, const LocalDashboard &dashboard, + const std::function &callback); + bool shutdownAll(const std::function &callback); + +private: + QHash m_startedDashboards; + std::unordered_map> m_startedDashboardTrees; +}; + +void LocalBuild::startDashboard(const QString &projectName, const LocalDashboard &dashboard, + const std::function &callback) +{ + if (ExtensionSystem::PluginManager::isShuttingDown()) + return; + + const Storage storage; + const auto onSetup = [dash = dashboard](Process &process) { + process.setCommand(dash.startCommandLine); + process.setEnvironment(dash.environment); + }; + + TaskTree *taskTree = new TaskTree; + m_startedDashboardTrees.insert_or_assign(projectName, std::unique_ptr(taskTree)); + const auto onDone = [this, callback, dash = dashboard, projectName] (const Process &process) { + const auto onFinish = qScopeGuard([this, projectName] { + auto it = m_startedDashboardTrees.find(projectName); + QTC_ASSERT(it != m_startedDashboardTrees.end(), return); + it->second.release()->deleteLater(); + m_startedDashboardTrees.erase(it); + }); + if (process.result() != ProcessResult::FinishedWithSuccess) { + qCDebug(localDashLog) << "Process failed...." << int(process.result()); + const QString errOutput = process.cleanedStdErr(); + if (errOutput.isEmpty()) + showErrorMessage(Tr::tr("Failed to start local dashboard.")); + else + showErrorMessage(errOutput); + return; + } + const QString output = process.cleanedStdOut(); + QJsonParseError error; + const QJsonDocument json = QJsonDocument::fromJson(output.toUtf8(), &error); + if (error.error != QJsonParseError::NoError) + return; + if (!json.isObject()) + return; + + LocalDashboard updated = dash; + const QJsonObject data = json.object(); + updated.localUrl = QUrl::fromUserInput(data.value("url").toString()); + updated.localProject = data.value("project").toString(); + updated.localUser = data.value("user").toString(); + updated.pass = data.value("password").toString().toUtf8(); + + m_startedDashboards.insert(updated.id, updated); + if (callback) + callback(); + }; + + m_startedDashboards.insert(dashboard.id, dashboard); + qCDebug(localDashLog) << "Dashboard [start]" << dashboard.startCommandLine.toUserOutput(); + taskTree->setRecipe({ProcessTask(onSetup, onDone)}); + taskTree->start(); +} + +bool LocalBuild::shutdownAll(const std::function &callback) +{ + for (auto it = m_startedDashboardTrees.begin(), end = m_startedDashboardTrees.end(); + it != end; ++it) { + if (it->second) + it->second->cancel(); + } + + if (m_startedDashboards.isEmpty()) + return false; + + const LoopList iterator(m_startedDashboards.values()); + + const auto onSetup = [iterator](Process &process) { + process.setCommand(iterator->stopCommandLine); + process.setEnvironment(iterator->environment); + qCDebug(localDashLog) << "Dashboard [stop]" << iterator->stopCommandLine.toUserOutput(); + }; + + const auto onDone = [this, iterator](const Process &) { + m_startedDashboards.remove(iterator->id); + }; + + const Group recipe = Group { + For (iterator) >> Do { + parallel, + continueOnError, + ProcessTask(onSetup, onDone) + } + }.withTimeout(std::chrono::seconds(5)); + + TaskTree *taskTree = new TaskTree(recipe); + QObject::connect(taskTree, &TaskTree::done, taskTree, [taskTree, callback] { + taskTree->deleteLater(); + if (callback) + callback(); + }); + taskTree->start(); + + return true; +} + +LocalBuild s_localBuildInstance; + +static QSqlDatabase localDashboardDB() +{ + static QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "localDashboardDB"); + return db; +} + +void checkForLocalBuildResults(const QString &projectName, const std::function &callback) +{ + const FilePath configDbl = FileUtils::homePath().pathAppended(".bauhaus/localbuild/config.dbl"); + if (!configDbl.exists()) + return; + + if (!QSqlDatabase::isDriverAvailable("QSQLITE")) + return; + + QSqlDatabase db = localDashboardDB(); + if (!db.isValid()) + return; + db.setDatabaseName(configDbl.path()); + if (!db.open()) { + qCDebug(sqlLog) << "open db failed" << db.lastError().text(); + return; + } + auto cleanup = qScopeGuard([&db] { db.close(); }); + + QSqlQuery query(db); + query.prepare("SELECT Data FROM axMetaData WHERE Name=\"version\""); + if (!query.exec() || !query.next()) + return; + if (!query.value("Data").toString().startsWith("1.")) + return; + + query.prepare("SELECT COUNT(*) FROM axLocalProjects WHERE Remote_Project_Name=(:projectName)"); + query.bindValue(":projectName", projectName); + + if (!query.exec() || !query.next()) + return; + bool ok = true; + const int count = query.value(0).toUInt(&ok); + if (!ok || count < 1) + return; + + if (callback) + callback(); +} + +static CommandLine parseCommandLine(const QString &jsonArrayCmd) +{ + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(jsonArrayCmd.toUtf8(), &error); + if (error.error != QJsonParseError::NoError) + return {}; + if (!doc.isArray()) + return {}; + const QJsonArray array = doc.array(); + QStringList fullCommand; + for (const auto &val : array) + fullCommand.append(val.toString()); + if (fullCommand.isEmpty()) + return {}; + const QString first = fullCommand.takeFirst(); + return CommandLine{FilePath::fromUserInput(first).withExecutableSuffix(), fullCommand}; +} + +void startLocalDashboard(const QString &projectName, const std::function &callback) +{ + QSqlDatabase db = localDashboardDB(); + QTC_ASSERT(db.isValid(), return); // we should be here only if we had some valid db before + if (!db.open()) { + qCDebug(sqlLog) << "open db failed" << db.lastError().text(); + return; + } + auto cleanup = qScopeGuard([&db]{ db.close(); }); + + QSqlQuery query(db); + query.prepare("SELECT ID, Dashboard_Start_Command_Line, Dashboard_Stop_Command_Line " + "FROM axLocalProjects WHERE Remote_Project_Name=(:projectName)"); + query.bindValue(":projectName", projectName); + if (!query.exec() || !query.next()) + return; + + const QString id = query.value("ID").toString(); + const QString startCmdLine = query.value("Dashboard_Start_Command_Line").toString(); + const QString stopCmdLine = query.value("Dashboard_Stop_Command_Line").toString(); + + query.prepare("SELECT Name, Value FROM axDashboardEnvironments WHERE LocalProject_ID=(:id)"); + query.bindValue(":id", id); + + if (!query.exec()) + return; + + const QString userAgent("Axivion" + QCoreApplication::applicationName() + + "Plugin/" + QCoreApplication::applicationVersion()); + EnvironmentItems envItems; + if (!settings().bauhausPython().isEmpty()) + envItems.append(EnvironmentItem("BAUHAUS_PYTHON", settings().bauhausPython().path())); + if (!settings().javaHome().isEmpty()) + envItems.append(EnvironmentItem("JAVA_HOME", settings().javaHome().path())); + envItems.append(EnvironmentItem("AXIVION_USER_AGENT", userAgent)); + while (query.next()) { + const QString name = query.value("Name").toString(); + const QString value = query.value("Value").toString(); + QTC_ASSERT(!name.isEmpty(), continue); + envItems.append(EnvironmentItem(name, value)); + } + + Environment env = Environment::systemEnvironment(); + env.modify(envItems); + const CommandLine start = parseCommandLine(startCmdLine); + const CommandLine stop = parseCommandLine(stopCmdLine); + + LocalDashboard localDashboard{id, start, stop, env, {}, {}, {}, {}}; + s_localBuildInstance.startDashboard(projectName, localDashboard, callback); +} + +bool shutdownAllLocalDashboards(const std::function &callback) +{ + return s_localBuildInstance.shutdownAll(callback); +} + +} // namespace Axivion::Internal diff --git a/src/plugins/axivion/localbuild.h b/src/plugins/axivion/localbuild.h new file mode 100644 index 00000000000..0874962e731 --- /dev/null +++ b/src/plugins/axivion/localbuild.h @@ -0,0 +1,14 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +namespace Axivion::Internal { + +void checkForLocalBuildResults(const QString &projectName, const std::function &callback); +void startLocalDashboard(const QString &projectName, const std::function &callback); +bool shutdownAllLocalDashboards(const std::function &callback); + +} // namespace Axivion::Internal