diff --git a/src/ui/WorkSpace/WorkSpaceDlg.cpp b/src/ui/WorkSpace/WorkSpaceDlg.cpp index 525d10ca..1460c7e2 100644 --- a/src/ui/WorkSpace/WorkSpaceDlg.cpp +++ b/src/ui/WorkSpace/WorkSpaceDlg.cpp @@ -95,6 +95,8 @@ void WorkSpaceDlg::OnSure() { WorkSpace* workSpace = WorkSpaceManager::Get().GetOrCreate(workspacePath, name); workSpace->SetDescribe(ui->etDescribe->toPlainText()); workSpace->SetCommondFilePath(commondPath_); + // Execute commands configured for onCreate right after workspace is set up + workSpace->ExecuteCommands(WorkSpace::CommandWhen::OnCreate); WorkSpaceManager::Get().SetCurrent(workSpace); accept(); @@ -115,33 +117,17 @@ void WorkSpaceDlg::OnSelectSavePath() { void WorkSpaceDlg::OnSelectCommondPath() { const QString workspacePath = Application::GetWorkSpacePath(); - const QString savePath = QFileDialog::getExistingDirectory(this, - tr("select commond file directory"), workspacePath, QFileDialog::DontResolveSymlinks); - if (savePath.isEmpty()) { - LOG_WARN("save commond file is empty"); + const QString xmlPath = QFileDialog::getOpenFileName( + this, + tr("select command xml file"), + workspacePath, + tr("XML files (*.xml);;All files (*.*)")); + if (xmlPath.isEmpty()) { + LOG_WARN("command xml file is empty"); return; } - commondPath_ = savePath; - ui->leCommondPath->setText(QString("%1").arg(commondPath_)); - LOG_INFO("select path: {}", commondPath_.toLocal8Bit().constData()); + commondPath_ = xmlPath; + ui->leCommondPath->setText(commondPath_); + LOG_INFO("select command xml: {}", commondPath_.toLocal8Bit().constData()); } -// -//void WorkSpaceDlg::InitFrame() { -// FrameTitleBar* titleBar = new FrameTitleBar(this); -// titleBar->SetMainWidget(this); -// -// titleBar->SetSysButton(FrameTitleBar::FTB_ICON | FrameTitleBar::FTB_CLOSE); -// -// QVBoxLayout* layout = new QVBoxLayout(this); -// layout->setContentsMargins(0, 0, 0, 0); -// layout->setSpacing(0); -// -// layout->setStretch(0, 0); -// layout->setStretch(1, 1); -// layout->setAlignment(Qt::AlignLeft | Qt::AlignTop); -// SetTitleBar(titleBar); -// -// QWidget* mainDilag_ = new QWidget(this); -// layout->addWidget(mainDilag_, 1); -// ui->setupUi(mainDilag_); -//} + diff --git a/src/workspace/CommandExecutor.cpp b/src/workspace/CommandExecutor.cpp new file mode 100644 index 00000000..d8d34db5 --- /dev/null +++ b/src/workspace/CommandExecutor.cpp @@ -0,0 +1,68 @@ +#include "workspace/CommandExecutor.h" + +#include +#include +#include + +#include "common/SpdLogger.h" + +void CommandExecutor::Execute(WorkSpace* ws, WorkSpace::CommandWhen when) { + if (!ws) return; + if (!cmd_.enabled) return; + + const QString whenStr = (when == WorkSpace::CommandWhen::OnCreate) ? QStringLiteral("oncreate") : QStringLiteral("onload"); + + // Build final arguments (already prepared by manager but honor rawArgs if provided) + QStringList argsList = cmd_.args; + auto pushArgs = [&argsList](const QString& s) { + if (!s.isEmpty()) { + for (const auto& part : s.split(' ', Qt::SkipEmptyParts)) { + argsList << part; + } + } + }; + if (!cmd_.rawArgs.isEmpty()) { + pushArgs(cmd_.rawArgs); + } + + const QString programLower = cmd_.program.toLower(); + if (!cmd_.path.isEmpty()) { + if (programLower.endsWith("cmd.exe")) { + argsList << "/c" << cmd_.path; + } else if (programLower.endsWith("powershell.exe")) { + argsList << "-NoProfile" << "-ExecutionPolicy" << "Bypass" << "-File" << cmd_.path; + } else { + argsList << cmd_.path; + } + } + + QProcess proc; + // Apply environment if provided + if (!cmd_.env.empty()) { + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + for (auto it = cmd_.env.begin(); it != cmd_.env.end(); ++it) { + env.insert(it.key(), it.value()); + } + proc.setProcessEnvironment(env); + } + + proc.setProgram(cmd_.program); + proc.setArguments(argsList); + proc.setWorkingDirectory(cmd_.workingDir.isEmpty() ? ws->GetDir() : cmd_.workingDir); + LOG_INFO("run command: name={} prog={} args={} cwd={} when={} desc={}", + cmd_.name.toLocal8Bit().constData(), + cmd_.program.toLocal8Bit().constData(), + argsList.join(' ').toLocal8Bit().constData(), + proc.workingDirectory().toLocal8Bit().constData(), + whenStr.toLocal8Bit().constData(), + cmd_.descript.toLocal8Bit().constData()); + proc.start(); + if (!proc.waitForStarted()) { + LOG_WARN("command failed to start: {}", cmd_.program.toLocal8Bit().constData()); + return; + } + proc.waitForFinished(cmd_.timeoutMs); + const QByteArray out = proc.readAllStandardOutput(); + const QByteArray err = proc.readAllStandardError(); + LOG_INFO("command '{}' exitCode={} stdout={} stderr={}", cmd_.name.toLocal8Bit().constData(), proc.exitCode(), out.constData(), err.constData()); +} \ No newline at end of file diff --git a/src/workspace/CommandExecutor.h b/src/workspace/CommandExecutor.h new file mode 100644 index 00000000..14a03bc9 --- /dev/null +++ b/src/workspace/CommandExecutor.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include "workspace/WorkSpace.h" + +// Merge Command model into this header to reduce files +struct Command { + QString name; + QString program; + QStringList args; // final argument list to pass to QProcess + QString rawArgs; // original args string from XML (optional) + QString path; // script or executable path + QString workingDir; // working directory + bool enabled{true}; + QMap env; // environment key/value pairs + QString descript; // description + int timeoutMs{30000}; // default 30s +}; + +class CommandExecutor { +public: + explicit CommandExecutor(const Command& cmd) : cmd_(cmd) {} + void Execute(WorkSpace* ws, WorkSpace::CommandWhen when); + + const Command& Get() const { return cmd_; } + +private: + Command cmd_; +}; \ No newline at end of file diff --git a/src/workspace/CommandManager.cpp b/src/workspace/CommandManager.cpp new file mode 100644 index 00000000..f7e11743 --- /dev/null +++ b/src/workspace/CommandManager.cpp @@ -0,0 +1,117 @@ +#include "workspace/CommandManager.h" + +#include + +#include "xml/tinyxml2.h" +#include "common/SpdLogger.h" + +static QMap parseEnvAttr(const QString& envAttr) { + QMap env; + if (envAttr.isEmpty()) return env; + const auto pairs = envAttr.split(';', Qt::SkipEmptyParts); + for (const auto& p : pairs) { + const auto kv = p.split('=', Qt::KeepEmptyParts); + if (kv.size() >= 2) env.insert(kv[0].trimmed(), kv[1].trimmed()); + } + return env; +} + +void CommandManager::Reload(WorkSpace* ws) { + onCreate_.clear(); + onLoad_.clear(); + if (!ws) return; + + const QString cmdPath = ws->GetCommondFilePath(); + if (cmdPath.isEmpty()) { + LOG_INFO("no command xml configured"); + return; + } + QFileInfo fi(cmdPath); + if (!fi.exists() || !fi.isFile()) { + LOG_WARN("command xml not found: {}", cmdPath.toLocal8Bit().constData()); + return; + } + + tinyxml2::XMLDocument doc; + auto rc = doc.LoadFile(cmdPath.toLocal8Bit().constData()); + if (rc != tinyxml2::XML_SUCCESS) { + LOG_WARN("load command xml failed: {} rc:{}", cmdPath.toLocal8Bit().constData(), rc); + return; + } + auto* root = doc.RootElement(); + if (!root) { + LOG_WARN("command xml has no root: {}", cmdPath.toLocal8Bit().constData()); + return; + } + + for (auto* node = root->FirstChildElement(); node; node = node->NextSiblingElement()) { + const char* tag = node->Name(); + if (!tag) continue; + QString tagQ = QString::fromUtf8(tag).toLower(); + if (tagQ != QLatin1String("commond") && tagQ != QLatin1String("command")) continue; + + Command cmd; + if (const char* nameAttr = node->Attribute("name")) cmd.name = QString::fromUtf8(nameAttr); + if (const char* exeAttr = node->Attribute("exe")) cmd.program = QString::fromUtf8(exeAttr); + if (cmd.program.isEmpty()) { + if (const char* programAttr = node->Attribute("program")) cmd.program = QString::fromUtf8(programAttr); + } + if (const char* pathAttr = node->Attribute("path")) cmd.path = QString::fromUtf8(pathAttr); + if (cmd.path.isEmpty()) { + if (const char* pathTypo = node->Attribute("paht")) cmd.path = QString::fromUtf8(pathTypo); + } + if (const char* argsAttr = node->Attribute("args")) cmd.rawArgs = QString::fromUtf8(argsAttr); + if (const char* cwdAttr = node->Attribute("workingDir")) cmd.workingDir = QString::fromUtf8(cwdAttr); + if (cmd.workingDir.isEmpty()) { + if (const char* cwdAttr2 = node->Attribute("cwd")) cmd.workingDir = QString::fromUtf8(cwdAttr2); + } + if (const char* enabledAttr = node->Attribute("enabled")) { + QString en = QString::fromUtf8(enabledAttr).toLower(); + cmd.enabled = !(en == QLatin1String("false") || en == QLatin1String("0")); + } + if (const char* descAttr = node->Attribute("descript")) cmd.descript = QString::fromUtf8(descAttr); + if (cmd.descript.isEmpty()) { + if (const char* desc2 = node->Attribute("description")) cmd.descript = QString::fromUtf8(desc2); + } + if (const char* timeoutAttr = node->Attribute("timeoutSec")) { + bool ok = false; int v = QString::fromUtf8(timeoutAttr).toInt(&ok); + if (ok && v > 0) cmd.timeoutMs = v * 1000; + } + // env: either attribute env="KEY=VAL;K2=V2" or child elements + if (const char* envAttr = node->Attribute("env")) { + cmd.env = parseEnvAttr(QString::fromUtf8(envAttr)); + } + for (auto* envNode = node->FirstChildElement("env"); envNode; envNode = envNode->NextSiblingElement("env")) { + const char* k = envNode->Attribute("key"); + const char* v = envNode->Attribute("value"); + if (k && v) cmd.env.insert(QString::fromUtf8(k), QString::fromUtf8(v)); + } + + // Pre-build args list from rawArgs (actual insertion of path happens in executor) + if (!cmd.rawArgs.isEmpty()) { + for (const auto& part : cmd.rawArgs.split(' ', Qt::SkipEmptyParts)) { + cmd.args << part; + } + } + + // when routing + WorkSpace::CommandWhen target = WorkSpace::CommandWhen::OnCreate; // default + if (const char* whenAttr = node->Attribute("when")) { + QString wa = QString::fromUtf8(whenAttr).toLower(); + if (wa == QLatin1String("onload")) target = WorkSpace::CommandWhen::OnLoad; + } + + auto exec = std::make_unique(cmd); + if (target == WorkSpace::CommandWhen::OnCreate) onCreate_.push_back(std::move(exec)); + else onLoad_.push_back(std::move(exec)); + } +} + +void CommandManager::Execute(WorkSpace* ws, WorkSpace::CommandWhen when) { + // Reload each time to reflect latest XML + Reload(ws); + auto& list = (when == WorkSpace::CommandWhen::OnCreate) ? onCreate_ : onLoad_; + for (auto& exec : list) { + exec->Execute(ws, when); + } +} \ No newline at end of file diff --git a/src/workspace/CommandManager.h b/src/workspace/CommandManager.h new file mode 100644 index 00000000..178f2fa3 --- /dev/null +++ b/src/workspace/CommandManager.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#include "workspace/WorkSpace.h" +#include "workspace/CommandExecutor.h" + +class CommandManager { +public: + void Reload(WorkSpace* ws); + void Execute(WorkSpace* ws, WorkSpace::CommandWhen when); + +private: + std::vector> onCreate_; + std::vector> onLoad_; +}; \ No newline at end of file diff --git a/src/workspace/WorkSpace.cpp b/src/workspace/WorkSpace.cpp index a0a40b01..b971b926 100644 --- a/src/workspace/WorkSpace.cpp +++ b/src/workspace/WorkSpace.cpp @@ -6,6 +6,7 @@ #include "workspace/WorkSpaceXMLParse.h" #include "workspace/WorkSpaceXMLWrite.h" +#include "workspace/CommandManager.h" #include "workspace/WorkSpaceItem.h" #include "workspace/Timestep.h" @@ -15,6 +16,7 @@ #include "common/SpdLogger.h" #include "entities/Entity.h" #include "utils/FileUtils.h" +#include //#include "workspace/WorkSpaceItemGroup.h" //#include "workspace/WorkSpaceRiverGroup.h" //#include "workspace/WorkSpaceRiverNetGroup.h" @@ -25,6 +27,7 @@ WorkSpace::WorkSpace(QObject* parent) noexcept : QObject(parent) { uuid_ = QUuid::createUuid().toString(); homeViewpoint_ = osgEarth::Viewpoint("home", 120.000000, 25.000000, 100.000000, -2.500000, -90.000000, 8200000.000000); + cmdMgr_ = std::make_unique(); } WorkSpace::WorkSpace(const QString& path, QObject* parent) @@ -32,6 +35,7 @@ WorkSpace::WorkSpace(const QString& path, QObject* parent) , path_(path){ uuid_ = QUuid::createUuid().toString(); homeViewpoint_ = osgEarth::Viewpoint("home", 120.000000, 25.000000, 100.000000, -2.500000, -90.000000, 8200000.000000); + cmdMgr_ = std::make_unique(); } const QString WorkSpace::GetDir() const { @@ -379,4 +383,13 @@ void WorkSpace::OnLoaded() { if (nullptr != timestep_) { emit TimestepChanged(timestep_); } + // Execute commands configured for onLoad + ExecuteCommands(CommandWhen::OnLoad); +} + +void WorkSpace::ExecuteCommands(CommandWhen when) { + if (!cmdMgr_) { + cmdMgr_ = std::make_unique(); + } + cmdMgr_->Execute(this, when); } diff --git a/src/workspace/WorkSpace.h b/src/workspace/WorkSpace.h index 80208e62..e649b2ba 100644 --- a/src/workspace/WorkSpace.h +++ b/src/workspace/WorkSpace.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -15,6 +16,7 @@ //#include "../ui/chartPlot/DYTChart.h" class WorkSpaceItem; +class CommandManager; class WorkSpace : public QObject { Q_OBJECT @@ -52,6 +54,10 @@ public: void SetCommondFilePath(const QString& path); const QString GetCommondFilePath() const; + // Execute command xml according to trigger + enum class CommandWhen { OnCreate, OnLoad }; + void ExecuteCommands(CommandWhen when); + void SetSimMatlab(const QString& path); const QString GetSimMatlab() const; @@ -172,6 +178,8 @@ private: std::map> files_; // Monotonic sequence for file entries changes, used to trigger UI refresh std::uint64_t filesSeq_{ 0 }; + // Executor for command XML actions + std::unique_ptr cmdMgr_; public: std::uint64_t GetFilesSeq() const { return filesSeq_; } friend class WorkSpaceXMLWrite;