From 74dc0c871bb311eaac6f6a791ad13d9b6c2b1951 Mon Sep 17 00:00:00 2001 From: Przemek Pawlas <3606072+Destroy666x@users.noreply.github.com> Date: Sat, 23 Mar 2024 02:51:51 +0100 Subject: [PATCH] Add Run action/condition temp vars --- data/locale/en-US.ini | 12 +++++- plugins/base/macro-action-run.cpp | 61 +++++++++++++++++++++++++-- plugins/base/macro-action-run.hpp | 4 ++ plugins/base/macro-condition-run.cpp | 42 ++++++++++++++++++ plugins/base/macro-condition-run.hpp | 4 ++ plugins/base/utils/process-config.cpp | 36 +++++++++++++++- plugins/base/utils/process-config.hpp | 25 +++++++++-- 7 files changed, 175 insertions(+), 9 deletions(-) diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index e6808427..6863a4d0 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -691,7 +691,8 @@ AdvSceneSwitcher.action.streaming.type.username="Set username" AdvSceneSwitcher.action.streaming.type.password="Set password" AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}" AdvSceneSwitcher.action.run="Run" -AdvSceneSwitcher.action.run.entry.wait="{{wait}}Wait for process exit or at most {{timeout}}" +AdvSceneSwitcher.action.run.wait.entry="{{wait}}Wait for process exit or at most {{timeout}}{{waitHelp}}" +AdvSceneSwitcher.action.run.wait.help.tooltip="Note that macro properties won't work if you leave this unticked, as the process spawns detached from the rest of logic and there's no control over it." AdvSceneSwitcher.action.sceneVisibility="Scene item visibility" AdvSceneSwitcher.action.sceneVisibility.type.show="Show" AdvSceneSwitcher.action.sceneVisibility.type.hide="Hide" @@ -1619,6 +1620,15 @@ AdvSceneSwitcher.tempVar.macro.matchedCount.description="The number of macros of AdvSceneSwitcher.tempVar.process.name="Process name" +AdvSceneSwitcher.tempVar.run.process.id="Process ID" +AdvSceneSwitcher.tempVar.run.process.id.description="PID of the process assigned by the system." +AdvSceneSwitcher.tempVar.run.process.exitCode="Process exit code" +AdvSceneSwitcher.tempVar.run.process.exitCode.description="Numeric code with which the process exited. Usually 0 is positive and other values are error codes." +AdvSceneSwitcher.tempVar.run.process.stream.output="Process standard output stream" +AdvSceneSwitcher.tempVar.run.process.stream.output.description="Full standard output stream, usually numbered 1 and what you see in CLI after running the command." +AdvSceneSwitcher.tempVar.run.process.stream.error="Process standard error stream" +AdvSceneSwitcher.tempVar.run.process.stream.error.description="Full standard error stream, usually numbered 2." + AdvSceneSwitcher.tempVar.recording.durationSeconds="Recording duration" AdvSceneSwitcher.tempVar.recording.durationSeconds.description="Recording duration in seconds.\nThis value does not change while the recording is paused and will be reset to zero if the recording is stopped." diff --git a/plugins/base/macro-action-run.cpp b/plugins/base/macro-action-run.cpp index e5f73e93..2948a7b8 100644 --- a/plugins/base/macro-action-run.cpp +++ b/plugins/base/macro-action-run.cpp @@ -1,5 +1,6 @@ #include "macro-action-run.hpp" #include "layout-helpers.hpp" +#include "ui-helpers.hpp" #include #include @@ -16,6 +17,8 @@ bool MacroActionRun::PerformAction() { if (_wait) { _procConfig.StartProcessAndWait(_timeout.Milliseconds()); + SetTempVarValues(); + return true; } @@ -28,6 +31,7 @@ bool MacroActionRun::PerformAction() QDesktopServices::openUrl(QUrl::fromLocalFile( QString::fromStdString(_procConfig.Path()))); } + return true; } @@ -36,6 +40,45 @@ void MacroActionRun::LogAction() const ablog(LOG_INFO, "run \"%s\"", _procConfig.UnresolvedPath().c_str()); } +void MacroActionRun::SetupTempVars() +{ + MacroAction::SetupTempVars(); + + AddTempvar( + "process.id", + obs_module_text("AdvSceneSwitcher.tempVar.run.process.id"), + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.id.description")); + AddTempvar( + "process.exitCode", + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.exitCode"), + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.exitCode.description")); + AddTempvar( + "process.stream.output", + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.stream.output"), + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.stream.output.description")); + AddTempvar( + "process.stream.error", + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.stream.error"), + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.stream.error.description")); +} + +void MacroActionRun::SetTempVarValues() +{ + SetTempVarValue("process.id", _procConfig.GetProcessId()); + SetTempVarValue("process.exitCode", _procConfig.GetProcessExitCode()); + SetTempVarValue("process.stream.output", + _procConfig.GetProcessOutputStream()); + SetTempVarValue("process.stream.error", + _procConfig.GetProcessErrorStream()); +} + bool MacroActionRun::Save(obs_data_t *obj) const { MacroAction::Save(obj); @@ -86,8 +129,18 @@ MacroActionRunEdit::MacroActionRunEdit( _procConfig(new ProcessConfigEdit(this)), _waitLayout(new QHBoxLayout()), _wait(new QCheckBox()), - _timeout(new DurationSelection(this, true, 0.1)) + _timeout(new DurationSelection(this, true, 0.1)), + _waitHelp(new QLabel()) { + QString helpIconPath = GetThemeTypeName() == "Light" + ? ":/res/images/help.svg" + : ":/res/images/help_light.svg"; + QIcon helpIcon(helpIconPath); + _waitHelp->setPixmap(helpIcon.pixmap(QSize(16, 16))); + _waitHelp->hide(); + _waitHelp->setToolTip(obs_module_text( + "AdvSceneSwitcher.action.run.wait.help.tooltip")); + QWidget::connect(_procConfig, SIGNAL(ConfigChanged(const ProcessConfig &)), this, SLOT(ProcessConfigChanged(const ProcessConfig &))); @@ -98,9 +151,11 @@ MacroActionRunEdit::MacroActionRunEdit( QWidget::connect(_timeout, SIGNAL(DurationChanged(const Duration &)), this, SLOT(TimeoutChanged(const Duration &))); - PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.run.entry.wait"), + PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.run.wait.entry"), _waitLayout, - {{"{{wait}}", _wait}, {"{{timeout}}", _timeout}}); + {{"{{wait}}", _wait}, + {"{{timeout}}", _timeout}, + {"{{waitHelp}}", _waitHelp}}); SetLayoutVisible(_waitLayout, false); auto layout = new QVBoxLayout; diff --git a/plugins/base/macro-action-run.hpp b/plugins/base/macro-action-run.hpp index b1f1f5a0..6160d770 100644 --- a/plugins/base/macro-action-run.hpp +++ b/plugins/base/macro-action-run.hpp @@ -25,6 +25,9 @@ public: Duration _timeout = 1; private: + void SetupTempVars(); + void SetTempVarValues(); + static bool _registered; static const std::string id; }; @@ -57,6 +60,7 @@ private: QHBoxLayout *_waitLayout; QCheckBox *_wait; DurationSelection *_timeout; + QLabel *_waitHelp; std::shared_ptr _entryData; bool _loading = true; diff --git a/plugins/base/macro-condition-run.cpp b/plugins/base/macro-condition-run.cpp index c635a7c0..17c2d29a 100644 --- a/plugins/base/macro-condition-run.cpp +++ b/plugins/base/macro-condition-run.cpp @@ -48,6 +48,7 @@ bool MacroConditionRun::CheckCondition() if (_thread.joinable()) { _thread.join(); } + _threadDone = false; _thread = std::thread(&MacroConditionRun::RunProcess, this); @@ -64,7 +65,48 @@ void MacroConditionRun::RunProcess() _error = ProcessConfig::ProcStartError::NONE; _procExitCode = std::get(result); } + _threadDone = true; + SetTempVarValues(); +} + +void MacroConditionRun::SetupTempVars() +{ + MacroCondition::SetupTempVars(); + + AddTempvar( + "process.id", + obs_module_text("AdvSceneSwitcher.tempVar.run.process.id"), + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.id.description")); + AddTempvar( + "process.exitCode", + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.exitCode"), + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.exitCode.description")); + AddTempvar( + "process.stream.output", + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.stream.output"), + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.stream.output.description")); + AddTempvar( + "process.stream.error", + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.stream.error"), + obs_module_text( + "AdvSceneSwitcher.tempVar.run.process.stream.error.description")); +} + +void MacroConditionRun::SetTempVarValues() +{ + SetTempVarValue("process.id", _procConfig.GetProcessId()); + SetTempVarValue("process.exitCode", _procConfig.GetProcessExitCode()); + SetTempVarValue("process.stream.output", + _procConfig.GetProcessOutputStream()); + SetTempVarValue("process.stream.error", + _procConfig.GetProcessErrorStream()); } bool MacroConditionRun::Save(obs_data_t *obj) const diff --git a/plugins/base/macro-condition-run.hpp b/plugins/base/macro-condition-run.hpp index 751d318b..5133df24 100644 --- a/plugins/base/macro-condition-run.hpp +++ b/plugins/base/macro-condition-run.hpp @@ -30,6 +30,10 @@ public: private: void RunProcess(); + + void SetupTempVars(); + void SetTempVarValues(); + std::thread _thread; std::atomic_bool _threadDone{true}; ProcessConfig::ProcStartError _error = diff --git a/plugins/base/utils/process-config.cpp b/plugins/base/utils/process-config.cpp index 8351a719..27195d9b 100644 --- a/plugins/base/utils/process-config.cpp +++ b/plugins/base/utils/process-config.cpp @@ -4,7 +4,6 @@ #include "name-dialog.hpp" #include -#include namespace advss { @@ -16,6 +15,7 @@ bool ProcessConfig::Save(obs_data_t *obj) const _args.Save(data, "args", "arg"); obs_data_set_obj(obj, "processConfig", data); obs_data_release(data); + return true; } @@ -27,6 +27,7 @@ bool ProcessConfig::Load(obs_data_t *obj) _workingDirectory = obs_data_get_string(obj, "workingDirectory"); _args.Load(obj, "args", "arg"); + return true; } @@ -35,6 +36,7 @@ bool ProcessConfig::Load(obs_data_t *obj) _workingDirectory.Load(data, "workingDirectory"); _args.Load(data, "args", "arg"); obs_data_release(data); + return true; } @@ -44,6 +46,7 @@ QStringList ProcessConfig::Args() const for (auto &arg : _args) { result << QString::fromStdString(arg); } + return result; } @@ -61,11 +64,14 @@ void ProcessConfig::ResolveVariables() } std::variant -ProcessConfig::StartProcessAndWait(int timeout) const +ProcessConfig::StartProcessAndWait(int timeout) { + ResetFinishedProcessData(); + QProcess process; process.setWorkingDirectory(QString::fromStdString(WorkingDir())); process.start(QString::fromStdString(Path()), Args()); + SetProcessId(QString::number(process.processId()).toStdString()); vblog(LOG_INFO, "run \"%s\" with a timeout of %d ms", Path().c_str(), timeout); @@ -75,21 +81,47 @@ ProcessConfig::StartProcessAndWait(int timeout) const Path().c_str()); return ProcStartError::FAILED_TO_START; } + + SetFinishedProcessData(process); vblog(LOG_INFO, "timeout while running \"%s\"\nAttempting to kill process!", Path().c_str()); process.kill(); process.waitForFinished(); + return ProcStartError::TIMEOUT; } + SetFinishedProcessData(process); + if (process.exitStatus() == QProcess::NormalExit) { return process.exitCode(); } + vblog(LOG_INFO, "process \"%s\" crashed!", Path().c_str()); return ProcStartError::CRASH; } +void ProcessConfig::SetFinishedProcessData(QProcess &process) +{ + static const QRegularExpression regex("(\\r\\n|\\r|\\n)$"); + _processExitCode = QString::number(process.exitCode()).toStdString(); + // Qt reads extra newline, at least on Windows, hence the workaround + _processOutputStream = QString(process.readAllStandardOutput()) + .remove(regex) + .toStdString(); + _processErrorStream = QString(process.readAllStandardError()) + .remove(regex) + .toStdString(); +} + +void ProcessConfig::ResetFinishedProcessData() +{ + _processExitCode = ""; + _processOutputStream = ""; + _processErrorStream = ""; +} + ProcessConfigEdit::ProcessConfigEdit(QWidget *parent) : QWidget(parent), _filePath(new FileSelection()), diff --git a/plugins/base/utils/process-config.hpp b/plugins/base/utils/process-config.hpp index e70856cc..43922011 100644 --- a/plugins/base/utils/process-config.hpp +++ b/plugins/base/utils/process-config.hpp @@ -5,8 +5,9 @@ #include #include -#include #include +#include +#include #include #include #include @@ -23,6 +24,20 @@ public: std::string WorkingDir() const { return _workingDirectory; } QStringList Args() const; // Resolves variables + void SetProcessId(std::string processId) { _processId = processId; } + std::string GetProcessId() const { return _processId; } + void SetFinishedProcessData(QProcess &process); + void ResetFinishedProcessData(); + std::string GetProcessExitCode() const { return _processExitCode; } + std::string GetProcessOutputStream() const + { + return _processOutputStream; + } + std::string GetProcessErrorStream() const + { + return _processErrorStream; + } + enum class ProcStartError { NONE, FAILED_TO_START, @@ -30,8 +45,7 @@ public: CRASH, }; - std::variant - StartProcessAndWait(int timeoutInMs) const; + std::variant StartProcessAndWait(int timeoutInMs); bool StartProcessDetached() const; void ResolveVariables(); @@ -41,6 +55,11 @@ private: StringVariable _workingDirectory = ""; StringList _args; + std::string _processId; + std::string _processExitCode; + std::string _processOutputStream; + std::string _processErrorStream; + friend class ProcessConfigEdit; };