Axivion: Check for local dashboard results

Enable the local dashboard button depending on the existence
of available results.
Start local dashboards on request and shut them down at end.

Task-number: QTCREATORBUG-32385
Change-Id: I31083669f0c042195570da51d24fd6c95449e1ea
Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io>
This commit is contained in:
Christian Stenger
2025-03-25 13:53:08 +01:00
parent 09f5911142
commit 12d421c6c3
6 changed files with 340 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
add_qtc_plugin(Axivion add_qtc_plugin(Axivion
PLUGIN_DEPENDS PLUGIN_DEPENDS
Core Debugger ProjectExplorer TextEditor 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 LONG_DESCRIPTION_MD AxivionDescription.md
SOURCES SOURCES
axivionperspective.cpp axivionperspective.h axivionperspective.cpp axivionperspective.h
@@ -14,6 +14,7 @@ add_qtc_plugin(Axivion
dashboard/error.cpp dashboard/error.h dashboard/error.cpp dashboard/error.h
dynamiclistmodel.cpp dynamiclistmodel.h dynamiclistmodel.cpp dynamiclistmodel.h
issueheaderview.cpp issueheaderview.h issueheaderview.cpp issueheaderview.h
localbuild.cpp localbuild.h
) )
file(GLOB_RECURSE images RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} images/*) file(GLOB_RECURSE images RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} images/*)

View File

@@ -9,8 +9,7 @@ QtcPlugin {
Depends { name: "ProjectExplorer" } Depends { name: "ProjectExplorer" }
Depends { name: "TextEditor" } Depends { name: "TextEditor" }
Depends { name: "Utils" } Depends { name: "Utils" }
Depends { name: "Qt.widgets" } Depends { name: "Qt"; submodules: ["network", "sql", "widgets"] }
Depends { name: "Qt.network" }
files: [ files: [
"axivionperspective.cpp", "axivionperspective.cpp",
@@ -24,6 +23,8 @@ QtcPlugin {
"dynamiclistmodel.h", "dynamiclistmodel.h",
"issueheaderview.cpp", "issueheaderview.cpp",
"issueheaderview.h", "issueheaderview.h",
"localbuild.cpp",
"localbuild.h",
] ]
cpp.includePaths: base.concat(["."]) // needed for the generated stuff below cpp.includePaths: base.concat(["."]) // needed for the generated stuff below

View File

@@ -9,6 +9,7 @@
#include "dashboard/dto.h" #include "dashboard/dto.h"
#include "issueheaderview.h" #include "issueheaderview.h"
#include "dynamiclistmodel.h" #include "dynamiclistmodel.h"
#include "localbuild.h"
#include <coreplugin/actionmanager/actionmanager.h> #include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
@@ -212,6 +213,7 @@ private:
void fetchTable(); void fetchTable();
void fetchIssues(const IssueListSearch &search); void fetchIssues(const IssueListSearch &search);
void onFetchRequested(int startRow, int limit); void onFetchRequested(int startRow, int limit);
void switchDashboard(bool local);
void hideOverlays(); void hideOverlays();
void openFilterHelp(); void openFilterHelp();
@@ -308,11 +310,11 @@ IssuesWidget::IssuesWidget(QWidget *parent)
localLayout->addWidget(m_localDashBoard); localLayout->addWidget(m_localDashBoard);
connect(&settings(), &AxivionSettings::suitePathValidated, this, [this] { connect(&settings(), &AxivionSettings::suitePathValidated, this, [this] {
const auto info = settings().versionInfo(); 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_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 = new QButtonGroup(this);
m_typesButtonGroup->setExclusive(true); m_typesButtonGroup->setExclusive(true);
m_typesLayout = new QHBoxLayout; m_typesLayout = new QHBoxLayout;
@@ -855,7 +857,7 @@ void IssuesWidget::updateBasicProjectInfo(const std::optional<Dto::ProjectInfoDt
std::optional<AxivionVersionInfo> suiteVersionInfo = settings().versionInfo(); std::optional<AxivionVersionInfo> suiteVersionInfo = settings().versionInfo();
m_localBuild->setEnabled(!m_currentProject.isEmpty() m_localBuild->setEnabled(!m_currentProject.isEmpty()
&& suiteVersionInfo && !suiteVersionInfo->versionNumber.isEmpty()); && suiteVersionInfo && !suiteVersionInfo->versionNumber.isEmpty());
m_localDashBoard->setEnabled(true); checkForLocalBuildResults(m_currentProject, [this] { m_localDashBoard->setEnabled(true); });
} }
void IssuesWidget::updateAllFilters(const QVariant &namedFilter) void IssuesWidget::updateAllFilters(const QVariant &namedFilter)
@@ -1010,6 +1012,16 @@ void IssuesWidget::showErrorMessage(const QString &message)
m_stack->setCurrentIndex(1); 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() void IssuesWidget::hideOverlays()
{ {
if (m_overlay) if (m_overlay)

View File

@@ -8,6 +8,7 @@
#include "axiviontr.h" #include "axiviontr.h"
#include "dashboard/dto.h" #include "dashboard/dto.h"
#include "dashboard/error.h" #include "dashboard/error.h"
#include "localbuild.h"
#include <coreplugin/credentialquery.h> #include <coreplugin/credentialquery.h>
#include <coreplugin/dialogs/ioptionspage.h> #include <coreplugin/dialogs/ioptionspage.h>
@@ -1227,6 +1228,14 @@ class AxivionPlugin final : public ExtensionSystem::IPlugin
connect(EditorManager::instance(), &EditorManager::documentClosed, connect(EditorManager::instance(), &EditorManager::documentClosed,
dd, &AxivionPluginPrivate::onDocumentClosed); dd, &AxivionPluginPrivate::onDocumentClosed);
} }
ShutdownFlag aboutToShutdown() final
{
if (shutdownAllLocalDashboards([this] { emit asynchronousShutdownFinished(); }))
return AsynchronousShutdown;
else
return SynchronousShutdown;
}
}; };
void fetchIssueInfo(const QString &id) void fetchIssueInfo(const QString &id)

View File

@@ -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 <extensionsystem/pluginmanager.h>
#include <solutions/tasking/tasktree.h>
#include <utils/algorithm.h>
#include <utils/commandline.h>
#include <utils/environment.h>
#include <utils/fileutils.h>
#include <utils/qtcprocess.h>
#include <utils/qtcassert.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QLoggingCategory>
#include <QSqlDatabase>
#include <QSqlDriver>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlResult>
#include <QTimer>
#include <unordered_map>
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<void()> &callback);
bool shutdownAll(const std::function<void()> &callback);
private:
QHash<QString, LocalDashboard> m_startedDashboards;
std::unordered_map<QString, std::unique_ptr<TaskTree>> m_startedDashboardTrees;
};
void LocalBuild::startDashboard(const QString &projectName, const LocalDashboard &dashboard,
const std::function<void()> &callback)
{
if (ExtensionSystem::PluginManager::isShuttingDown())
return;
const Storage<LocalDashboard> 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>(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<void()> &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<void()> &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<void ()> &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<void()> &callback)
{
return s_localBuildInstance.shutdownAll(callback);
}
} // namespace Axivion::Internal

View File

@@ -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 <QString>
namespace Axivion::Internal {
void checkForLocalBuildResults(const QString &projectName, const std::function<void()> &callback);
void startLocalDashboard(const QString &projectName, const std::function<void()> &callback);
bool shutdownAllLocalDashboards(const std::function<void()> &callback);
} // namespace Axivion::Internal