From 16fa91c2a194aed155a1c49641b5d2ae8734d49b Mon Sep 17 00:00:00 2001 From: WarmUpTill Date: Wed, 22 Feb 2023 17:15:49 +0100 Subject: [PATCH] Add "Run" condition Allows to use external programs as conditions --- data/locale/en-US.ini | 15 +- src/macro-core/macro-condition-run.cpp | 222 +++++++++++++++++++++++++ src/macro-core/macro-condition-run.hpp | 79 +++++++++ 3 files changed, 311 insertions(+), 5 deletions(-) create mode 100644 src/macro-core/macro-condition-run.cpp create mode 100644 src/macro-core/macro-condition-run.hpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 4b36f12b..9856a309 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -416,6 +416,9 @@ AdvSceneSwitcher.condition.variable.type.equalsVariable="equals variable" AdvSceneSwitcher.condition.variable.type.lessThanVariable="is less than variable" AdvSceneSwitcher.condition.variable.type.greaterThanVariable="is greater than variable" AdvSceneSwitcher.condition.variable.entry="{{variables}}{{conditions}}{{strValue}}{{numValue}}{{variables2}}" +AdvSceneSwitcher.condition.run="Run" +AdvSceneSwitcher.condition.run.entry="Process exits before timeout of{{timeout}} seconds" +AdvSceneSwitcher.condition.run.entry.exit="{{checkExitCode}}Check for exit code{{exitCode}}" ; Macro Actions AdvSceneSwitcher.action.switchScene="Switch scene" @@ -463,11 +466,6 @@ AdvSceneSwitcher.action.streaming.type.stop="Stop streaming" AdvSceneSwitcher.action.streaming.type.start="Start streaming" AdvSceneSwitcher.action.streaming.entry="{{actions}}" AdvSceneSwitcher.action.run="Run" -AdvSceneSwitcher.action.run.arguments="Arguments:" -AdvSceneSwitcher.action.run.addArgument="Add argument" -AdvSceneSwitcher.action.run.addArgumentDescription="Add new argument:" -AdvSceneSwitcher.action.run.entry="Run {{filePath}}" -AdvSceneSwitcher.action.run.entry.workingDirectory="Working directory:{{workingDirectory}}" AdvSceneSwitcher.action.sceneVisibility="Scene item visibility" AdvSceneSwitcher.action.sceneVisibility.type.show="Show" AdvSceneSwitcher.action.sceneVisibility.type.hide="Hide" @@ -921,6 +919,13 @@ AdvSceneSwitcher.regex.dotMatchNewline=". matches newlines" AdvSceneSwitcher.regex.multiLine="^ and $ match start/end of line" AdvSceneSwitcher.regex.extendedPattern="Enable Qt's ExtendedPatternSyntax" +AdvSceneSwitcher.process.showAdvanced="Show advanced settings" +AdvSceneSwitcher.process.arguments="Arguments:" +AdvSceneSwitcher.process.addArgument="Add argument" +AdvSceneSwitcher.process.addArgumentDescription="Add new argument:" +AdvSceneSwitcher.process.entry="Run{{filePath}}{{advancedSettings}}" +AdvSceneSwitcher.process.entry.workingDirectory="Working directory:{{workingDirectory}}" + AdvSceneSwitcher.selectScene="--select scene--" AdvSceneSwitcher.selectPreviousScene="Previous Scene" AdvSceneSwitcher.selectCurrentScene="Current Scene" diff --git a/src/macro-core/macro-condition-run.cpp b/src/macro-core/macro-condition-run.cpp new file mode 100644 index 00000000..a7e2bdde --- /dev/null +++ b/src/macro-core/macro-condition-run.cpp @@ -0,0 +1,222 @@ +#include "macro-condition-edit.hpp" +#include "macro-condition-run.hpp" +#include "utility.hpp" +#include "advanced-scene-switcher.hpp" + +#include +#include + +const std::string MacroConditionRun::id = "run"; + +bool MacroConditionRun::_registered = MacroConditionFactory::Register( + MacroConditionRun::id, + {MacroConditionRun::Create, MacroConditionRunEdit::Create, + "AdvSceneSwitcher.condition.run"}); + +MacroConditionRun::~MacroConditionRun() +{ + if (_thread.joinable()) { + _thread.join(); + } +} + +bool MacroConditionRun::CheckCondition() +{ + if (!_threadDone) { + return false; + } + + bool ret = false; + + switch (_procStatus) { + case MacroConditionRun::Status::FAILED_TO_START: + SetVariableValue("Failed to start process"); + ret = false; + break; + case MacroConditionRun::Status::TIMEOUT: + SetVariableValue("Timeout while running process"); + ret = false; + break; + case MacroConditionRun::Status::OK: + ret = _checkExitCode ? _exitCode == _procExitCode : true; + SetVariableValue(std::to_string(_procExitCode)); + break; + default: + break; + } + + if (_thread.joinable()) { + _thread.join(); + } + _threadDone = false; + _thread = std::thread(&MacroConditionRun::RunProcess, this); + + return ret; +} + +void MacroConditionRun::RunProcess() +{ + QProcess process; + process.setWorkingDirectory( + QString::fromStdString(_procConfig.WorkingDir())); + process.start(QString::fromStdString(_procConfig.Path()), + _procConfig.Args()); + int timeout = _timeout.seconds * 1000; + + vblog(LOG_INFO, "run \"%s\" with a timeout of %d ms", + _procConfig.Path().c_str(), timeout); + + bool procFinishedInTime = process.waitForFinished(timeout); + + if (!procFinishedInTime) { + if (process.error() == QProcess::FailedToStart) { + vblog(LOG_INFO, "failed to start \"%s\"!", + _procConfig.Path().c_str()); + _procStatus = Status::FAILED_TO_START; + } else { + vblog(LOG_INFO, + "timeout while running \"%s\"\nAttempting to kill process!", + _procConfig.Path().c_str()); + process.kill(); + process.waitForFinished(); + _procStatus = Status::TIMEOUT; + } + } + + bool validExitCode = process.exitStatus() == QProcess::NormalExit; + + if ((_checkExitCode && !validExitCode) || !procFinishedInTime) { + _threadDone = true; + return; + } + + _procExitCode = process.exitCode(); + _procStatus = Status::OK; + _threadDone = true; +} + +bool MacroConditionRun::Save(obs_data_t *obj) const +{ + MacroCondition::Save(obj); + _procConfig.Save(obj); + obs_data_set_bool(obj, "checkExitCode", _checkExitCode); + obs_data_set_int(obj, "exitCode", _exitCode); + _timeout.Save(obj, "timeout"); + return true; +} + +bool MacroConditionRun::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _procConfig.Load(obj); + _checkExitCode = obs_data_get_bool(obj, "checkExitCode"); + _exitCode = obs_data_get_int(obj, "exitCode"); + _timeout.Load(obj, "timeout"); + return true; +} + +std::string MacroConditionRun::GetShortDesc() const +{ + return _procConfig.Path(); +} + +MacroConditionRunEdit::MacroConditionRunEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _procConfig(new ProcessConfigEdit(this)), + _checkExitCode(new QCheckBox()), + _exitCode(new QSpinBox()), + _timeout(new DurationSelection(this, false, 0.1)) +{ + _exitCode->setMinimum(-99999); + _exitCode->setMaximum(999999); + + QWidget::connect(_procConfig, + SIGNAL(ConfigChanged(const ProcessConfig &)), this, + SLOT(ProcessConfigChanged(const ProcessConfig &))); + QWidget::connect(_timeout, SIGNAL(DurationChanged(double)), this, + SLOT(TimeoutChanged(double))); + QWidget::connect(_checkExitCode, SIGNAL(stateChanged(int)), this, + SLOT(CheckExitCodeChanged(int))); + QWidget::connect(_exitCode, SIGNAL(valueChanged(int)), this, + SLOT(ExitCodeChanged(int))); + + std::unordered_map widgetPlaceholders = { + {"{{checkExitCode}}", _checkExitCode}, + {"{{exitCode}}", _exitCode}, + {"{{timeout}}", _timeout}, + }; + + auto exitLayout = new QHBoxLayout(); + placeWidgets( + obs_module_text("AdvSceneSwitcher.condition.run.entry.exit"), + exitLayout, widgetPlaceholders); + auto timeoutLayout = new QHBoxLayout(); + placeWidgets(obs_module_text("AdvSceneSwitcher.condition.run.entry"), + timeoutLayout, widgetPlaceholders); + + auto *layout = new QVBoxLayout; + layout->addLayout(timeoutLayout); + layout->addWidget(_procConfig); + layout->addLayout(exitLayout); + setLayout(layout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionRunEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + _procConfig->SetProcessConfig(_entryData->_procConfig); + _timeout->SetDuration(_entryData->_timeout); + _checkExitCode->setChecked(_entryData->_checkExitCode); + _exitCode->setValue(_entryData->_exitCode); +} + +void MacroConditionRunEdit::TimeoutChanged(double seconds) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_timeout.seconds = seconds; +} + +void MacroConditionRunEdit::CheckExitCodeChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_checkExitCode = state; +} + +void MacroConditionRunEdit::ExitCodeChanged(int exitCode) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_exitCode = exitCode; +} + +void MacroConditionRunEdit::ProcessConfigChanged(const ProcessConfig &conf) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_procConfig = conf; + adjustSize(); + updateGeometry(); + emit HeaderInfoChanged( + QString::fromStdString(_entryData->GetShortDesc())); +} diff --git a/src/macro-core/macro-condition-run.hpp b/src/macro-core/macro-condition-run.hpp new file mode 100644 index 00000000..cccb1b08 --- /dev/null +++ b/src/macro-core/macro-condition-run.hpp @@ -0,0 +1,79 @@ +#pragma once +#include "macro.hpp" +#include "process-config.hpp" +#include "duration-control.hpp" + +#include +#include + +class MacroConditionRun : public MacroCondition { +public: + MacroConditionRun(Macro *m) : MacroCondition(m, true) {} + ~MacroConditionRun(); + bool CheckCondition(); + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + std::string GetShortDesc() const; + std::string GetId() const { return id; }; + static std::shared_ptr Create(Macro *m) + { + return std::make_shared(m); + } + + ProcessConfig _procConfig; + bool _checkExitCode = true; + int _exitCode = 0; + Duration _timeout = Duration(0.1); + +private: + enum class Status { + NONE, + FAILED_TO_START, + TIMEOUT, + OK, + }; + + void RunProcess(); + + std::thread _thread; + std::atomic_bool _threadDone{true}; + Status _procStatus = Status::NONE; + int _procExitCode = 0; + + static bool _registered; + static const std::string id; +}; + +class MacroConditionRunEdit : public QWidget { + Q_OBJECT + +public: + MacroConditionRunEdit(QWidget *parent, + std::shared_ptr cond = nullptr); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr cond) + { + return new MacroConditionRunEdit( + parent, + std::dynamic_pointer_cast(cond)); + } + +private slots: + void ProcessConfigChanged(const ProcessConfig &); + void TimeoutChanged(double); + void CheckExitCodeChanged(int); + void ExitCodeChanged(int); +signals: + void HeaderInfoChanged(const QString &); + +protected: + ProcessConfigEdit *_procConfig; + QCheckBox *_checkExitCode; + QSpinBox *_exitCode; + DurationSelection *_timeout; + std::shared_ptr _entryData; + +private: + bool _loading = true; +};