diff --git a/src/Daggy/CConsoleDaggy.cpp b/src/Daggy/CConsoleDaggy.cpp index 84ead3da..99e87e35 100644 --- a/src/Daggy/CConsoleDaggy.cpp +++ b/src/Daggy/CConsoleDaggy.cpp @@ -87,6 +87,7 @@ void CConsoleDaggy::stop() { if (need_hard_stop_) { qWarning() << "HARD STOP"; + delete daggy_core_; qApp->exit(); } else { daggy_core_->stop(); diff --git a/src/DaggyCore/CMakeLists.txt b/src/DaggyCore/CMakeLists.txt index 30becc00..bd02ae24 100644 --- a/src/DaggyCore/CMakeLists.txt +++ b/src/DaggyCore/CMakeLists.txt @@ -15,8 +15,13 @@ target_sources(${TARGET} Errors.cpp providers/IProvider.cpp providers/CLocal.cpp + providers/CLocal.hpp providers/IFabric.cpp providers/CLocalFabric.cpp + providers/CSsh.cpp + providers/CSsh.hpp + providers/CSshFabric.cpp + providers/CSshFabric.hpp aggregators/IAggregator.cpp aggregators/CFile.cpp aggregators/CConsole.cpp diff --git a/src/DaggyCore/Core.cpp b/src/DaggyCore/Core.cpp index 8c288ea8..538bfcda 100644 --- a/src/DaggyCore/Core.cpp +++ b/src/DaggyCore/Core.cpp @@ -30,6 +30,9 @@ SOFTWARE. #include "providers/CLocalFabric.hpp" #include "providers/CLocal.hpp" +#include "providers/CSshFabric.hpp" +#include "providers/CSsh.hpp" + #ifdef SSH2_SUPPORT #include "providers/CSsh2Fabric.hpp" #include "providers/CSsh2.hpp" @@ -281,13 +284,17 @@ try { static thread_local providers::CLocalFabric local_fabric; fabrics_map[providers::CLocal::provider_type] = &local_fabric; + static thread_local providers::CSshFabric ssh_fabric; + fabrics_map[providers::CSsh::provider_type] = &ssh_fabric; + #ifdef SSH2_SUPPORT static thread_local providers::CSsh2Fabric ssh2_fabric; fabrics_map[providers::CSsh2::provider_type] = &ssh2_fabric; #endif - if (fabrics) + if (fabrics) { for (size_t index = 0; index < size; index++) fabrics_map[fabrics[index]->type()] = fabrics[index]; + } auto source = sources_.cbegin(); while(source != sources_.cend()) { diff --git a/src/DaggyCore/Precompiled.hpp b/src/DaggyCore/Precompiled.hpp index 9238512a..43a566c0 100644 --- a/src/DaggyCore/Precompiled.hpp +++ b/src/DaggyCore/Precompiled.hpp @@ -24,6 +24,7 @@ SOFTWARE. #pragma once #include +#include #include #include #include diff --git a/src/DaggyCore/Sources.cpp b/src/DaggyCore/Sources.cpp index 94685b65..b4f72dc2 100644 --- a/src/DaggyCore/Sources.cpp +++ b/src/DaggyCore/Sources.cpp @@ -184,6 +184,25 @@ struct convert } }; +template<> +struct convert +{ + static bool decode(const Node& node, QStringList& rhs) + { + if (!node.IsSequence()) + return false; + + rhs.clear(); + const_iterator it = node.begin(); + while (it != node.end()) + { + rhs.push_back(it->as()); + ++it; + } + return true; + } +}; + template struct convert> { @@ -252,6 +271,24 @@ bool daggy::sources::commands::Properties::operator==(const Properties& other) c restart == other.restart; } +QStringList daggy::sources::commands::Properties::getParameters() const +{ + QStringList result; + for (const auto& option : parameters.keys()) + { + result << option ; + const auto& value = parameters[option]; + const auto& list = value.toStringList(); + if (list.empty()) { + const auto& string_value = value.toString(); + if (!string_value.isEmpty()) + result << string_value; + } else + result << list; + } + return result; +} + bool daggy::sources::Properties::operator==(const Properties& other) const { diff --git a/src/DaggyCore/Sources.hpp b/src/DaggyCore/Sources.hpp index 5f74ecc5..ea07c0a3 100644 --- a/src/DaggyCore/Sources.hpp +++ b/src/DaggyCore/Sources.hpp @@ -60,6 +60,8 @@ struct DAGGYCORE_EXPORT Properties { bool restart = false; bool operator==(const Properties& other) const; + + QStringList getParameters() const; }; } diff --git a/src/DaggyCore/providers/CLocal.cpp b/src/DaggyCore/providers/CLocal.cpp index 199c795c..91bfe87f 100644 --- a/src/DaggyCore/providers/CLocal.cpp +++ b/src/DaggyCore/providers/CLocal.cpp @@ -75,7 +75,7 @@ std::error_code daggy::providers::CLocal::stop() noexcept case DaggyProviderStarting: case DaggyProviderStarted: case DaggyProviderFinishing: - terminate(); + terminateAll(); break; } @@ -197,7 +197,7 @@ void daggy::providers::CLocal::onProcessFinished(int exit_code, QProcess::ExitSt onProcessStop(process); } -void daggy::providers::CLocal::terminate() +void daggy::providers::CLocal::terminateAll() { if (state() != DaggyProviderStarted) return; @@ -207,11 +207,9 @@ void daggy::providers::CLocal::terminate() for (QProcess* process : processes()) { switch (process->state()) { case QProcess::Running: -#ifdef Q_OS_WIN - process->kill(); -#else - process->terminate(); -#endif + { + terminate(process); + } break; case QProcess::Starting: process->close(); @@ -226,25 +224,17 @@ void daggy::providers::CLocal::terminate() QProcess* daggy::providers::CLocal::startProcess(const sources::Command& command) { - QProcess* process = new QProcess(this); - process->setObjectName(command.first); - - connect(process, &QProcess::destroyed, this, &CLocal::onProcessDestroyed); - connect(process, &QProcess::started, this, &CLocal::onProcessStart); - connect(process, &QProcess::errorOccurred, this, &CLocal::onProcessError); - connect(process, &QProcess::readyReadStandardOutput, this, qOverload<>(&CLocal::onProcessReadyReadStandard)); - connect(process, &QProcess::readyReadStandardError, this, qOverload<>(&CLocal::onProcessReadyReadError)); - connect(process, &QProcess::finished, this, &CLocal::onProcessFinished); - const auto& properties = command.second; + return startProcess(command.first, properties.exec, properties.getParameters()); +} - auto parameters = properties.exec.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); - auto program = parameters.takeFirst(); - emit commandStateChanged(process->objectName(), - DaggyCommandStarting, - process->exitCode()); - process->start(program, parameters); - return process; +void daggy::providers::CLocal::terminate(QProcess* process) +{ +#ifdef Q_OS_WIN + process->kill(); +#else + process->terminate(); +#endif } bool daggy::providers::CLocal::onProcessStop(QProcess* process) @@ -258,6 +248,7 @@ bool daggy::providers::CLocal::onProcessStop(QProcess* process) startProcess({command_id, properties}); return true; } + return false; } @@ -293,15 +284,24 @@ void daggy::providers::CLocal::startCommands() } } -void daggy::providers::CLocal::startProcess(QProcess* process, const QString& command) +QProcess* daggy::providers::CLocal::startProcess(const QString& process_name, const QString& exec, const QStringList& arguments) { - const auto& id = process->objectName(); - auto parameters = command.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); - auto program = parameters.takeFirst(); - metaStream(id, DaggyStreamStandard, true); - metaStream(id, DaggyStreamError, true); - emit commandStateChanged(id, + QProcess* process = new QProcess(this); + process->setObjectName(process_name); + + connect(process, &QProcess::destroyed, this, &CLocal::onProcessDestroyed); + connect(process, &QProcess::started, this, &CLocal::onProcessStart); + connect(process, &QProcess::errorOccurred, this, &CLocal::onProcessError); + connect(process, &QProcess::readyReadStandardOutput, this, qOverload<>(&CLocal::onProcessReadyReadStandard)); + connect(process, &QProcess::readyReadStandardError, this, qOverload<>(&CLocal::onProcessReadyReadError)); + connect(process, &QProcess::finished, this, &CLocal::onProcessFinished); + + emit commandStateChanged(process->objectName(), DaggyCommandStarting, process->exitCode()); - process->start(program, parameters, QIODevice::ReadOnly); + if (arguments.empty()) + process->startCommand(exec); + else + process->start(exec, arguments); + return process; } diff --git a/src/DaggyCore/providers/CLocal.hpp b/src/DaggyCore/providers/CLocal.hpp index b144557c..c51728b3 100644 --- a/src/DaggyCore/providers/CLocal.hpp +++ b/src/DaggyCore/providers/CLocal.hpp @@ -54,17 +54,19 @@ private slots: void onProcessFinished(int exit_code, QProcess::ExitStatus); private: - void terminate(); - - QProcess* startProcess(const daggy::sources::Command& command); bool onProcessStop(QProcess* process); QList processes() const; int activeProcessesCount() const; +protected: + virtual QProcess* startProcess(const daggy::sources::Command& command); + virtual void terminate(QProcess* process); + void startCommands(); + void terminateAll(); - void startProcess(QProcess* process, const QString& command); + QProcess* startProcess(const QString& process_name, const QString& exec, const QStringList& arguments); }; } diff --git a/src/DaggyCore/providers/CSsh.cpp b/src/DaggyCore/providers/CSsh.cpp new file mode 100644 index 00000000..2a2303bc --- /dev/null +++ b/src/DaggyCore/providers/CSsh.cpp @@ -0,0 +1,162 @@ +#include "../Precompiled.hpp" +#include "CSsh.hpp" +#include "../Errors.hpp" + +namespace daggy { +namespace providers { + +const QString CSsh::provider_type = "ssh"; + +CSsh::CSsh(const QString& session, + QString host, + Settings settings, + sources::Commands commands, + QObject *parent) + : CLocal(session, std::move(commands), parent) + , host_(std::move(host)) + , settings_(std::move(settings)) + , ssh_master_(nullptr) +{ + +} + +CSsh::~CSsh() +{ + if (ssh_master_) + { + ssh_master_->kill(); + ssh_master_->waitForFinished(); + ssh_master_.reset(); + } +} + +std::error_code CSsh::start() noexcept +{ + startMaster(); + return CLocal::start(); +} + +std::error_code CSsh::stop() noexcept +{ + auto result = CLocal::stop(); + stopMaster(); + return result; +} + +const QString& CSsh::type() const noexcept +{ + return provider_type; +} + +const QString& CSsh::controlPath() const +{ + static const QString control_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.ssh/daggy/" + host_; + return settings_.control.isEmpty() ? control_path : settings_.control; +} + +QProcess* CSsh::startProcess(const sources::Command& command) +{ + const auto& process_name = command.first; + return CLocal::startProcess(process_name, "ssh", makeSlaveArguments(command)); +} + +void CSsh::terminate(QProcess* process) +{ + static char ctrlc = 0x003; + process->write(&ctrlc, 1); + process->waitForBytesWritten(); + process->kill(); +} + +void CSsh::onMasterProcessError(QProcess::ProcessError error) +{ + switch (error) { + case QProcess::FailedToStart: + case QProcess::Crashed: + case QProcess::Timedout: + case QProcess::ReadError: + case QProcess::WriteError: + case QProcess::UnknownError: + terminateAll(); + stopMaster(); + break; + } +} + +void CSsh::startMaster() +{ + if (!ssh_master_ && settings_.control.isEmpty()) + { + ssh_master_.reset(new QProcess()); + + ssh_master_->start("ssh", makeMasterArguments()); + ssh_master_->waitForStarted(); + ssh_master_->waitForReadyRead(); + } +} + +void CSsh::stopMaster() +{ + if (ssh_master_) { + QProcess::execute("ssh", {"-S", controlPath(), "-O", "exit", host_}); + ssh_master_->terminate(); + ssh_master_->waitForFinished(); + ssh_master_.reset(); + } +} + +QStringList CSsh::makeMasterArguments() const +{ + QStringList result; + if (!settings_.passphrase.isEmpty()) + result << "-p" << settings_.passphrase; + + result << "-tt" << controlArguments(true) << "-F" << settings_.config << "-M" << host_; + + return result; +} + +QStringList CSsh::makeSlaveArguments(const sources::Command& command) const +{ + QStringList result({"-tt", "-F", settings_.config}); + result << controlArguments(false) << host_; + + QString exec = command.second.exec; + const auto& parameters = command.second.getParameters(); + if (!parameters.isEmpty()) { + for (const auto& parameter : parameters) { + exec += " " + parameter; + } + } + result << exec; + return result; +} + +QStringList CSsh::controlArguments(bool master) const +{ + return (master ? + QStringList{ "-o", "ControlMaster=auto", "-o", QString("ControlPath=%1").arg(controlPath())} : + QStringList{ "-o", "ControlMaster=no", "-S", controlPath()}); +} + +const QString& CSsh::Settings::tempPath() +{ + static const QString temp_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.ssh/daggy"; + QDir temp_dir(temp_path); + if (!temp_dir.exists()) + temp_dir.mkpath("."); + return temp_path; +} + +QString CSsh::Settings::tempConfigPath(const QString& session) +{ + return tempPath() + "/ssh_config_" + session; +} + +QString CSsh::Settings::tempControlPath(const QString& session) +{ + return tempPath() + "/ssh_mux_" + session; +} + +} +} diff --git a/src/DaggyCore/providers/CSsh.hpp b/src/DaggyCore/providers/CSsh.hpp new file mode 100644 index 00000000..0cd84c9a --- /dev/null +++ b/src/DaggyCore/providers/CSsh.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "CLocal.hpp" +#include + +namespace daggy { +namespace providers { + +class CSsh : public CLocal +{ +public: + struct Settings + { + QString config; + QString passphrase; + QString control; + + static const QString& tempPath(); + static QString tempConfigPath(const QString& session); + static QString tempControlPath(const QString& session); + }; + + CSsh(const QString& session, + QString host, + Settings settings, + sources::Commands commands, + QObject *parent = nullptr); + ~CSsh(); + + std::error_code start() noexcept override; + std::error_code stop() noexcept override; + const QString& type() const noexcept override; + + const QString& controlPath() const; + + static const QString provider_type; + +protected: + QProcess* startProcess(const daggy::sources::Command& command) override; + void terminate(QProcess* process) override; + +private slots: + void onMasterProcessError(QProcess::ProcessError error); + +private: + void startMaster(); + void stopMaster(); + + QStringList makeMasterArguments() const; + QStringList makeSlaveArguments(const sources::Command& command) const; + + QStringList controlArguments(bool master) const; + + const QString host_; + const Settings settings_; + + QScopedPointer ssh_master_; +}; + +} +} + + diff --git a/src/DaggyCore/providers/CSshFabric.cpp b/src/DaggyCore/providers/CSshFabric.cpp new file mode 100644 index 00000000..50eab09d --- /dev/null +++ b/src/DaggyCore/providers/CSshFabric.cpp @@ -0,0 +1,90 @@ +#include "../Precompiled.hpp" +#include "CSshFabric.hpp" +#include "CSsh.hpp" + +namespace { +const QString g_type = "ssh"; + +const char* g_ssh_control_config = R"CONFIG( + +Host * + ControlMaster auto + ControlPath %1 +)CONFIG"; + +constexpr const char* g_configField = "config"; +constexpr const char* g_passphraseField = "passphrase"; +constexpr const char* g_controlField = "control"; + +constexpr const std::pair parameters_field[] = +{ + {g_configField, QMetaType::QString}, + {g_passphraseField, QMetaType::QString}, + {g_controlField, QMetaType::QString}, +}; + +daggy::Result convert(const QVariantMap& parameters) +{ + daggy::providers::CSsh::Settings result; + + for (const auto& field : parameters_field) { + if (parameters.contains(field.first) && parameters[field.first].metaType() != QMetaType(field.second)) + return { + daggy::errors::make_error_code(DaggyErrorSourceConvertion), + QString("Parameters field '%1' has invalid type").arg(field.first) + }; + } + + if (parameters.contains(g_configField)) + result.config = parameters[g_configField].toString(); + else + result.config = QDir::homePath() + "/.ssh/config"; + + if (parameters.contains(g_passphraseField)) + result.passphrase = parameters[g_passphraseField].toString(); + + if (parameters.contains(g_controlField)) + result.control = parameters[g_controlField].toString(); + + return result; +} + +} + +namespace daggy { +namespace providers { + +CSshFabric::CSshFabric() +{ + +} + +const QString& CSshFabric::type() const +{ + return g_type; +} + +daggy::Result CSshFabric::createProvider(const QString& session, const Source& source, QObject* parent) +{ + auto parameters = convert(source.second.parameters); + if (!parameters) { + return + { + parameters.error, parameters.message + }; + } + + const auto& properties = source.second; + + return new CSsh(session, + properties.host, + std::move(*parameters), + properties.commands, + parent); +} +} +} + + + + diff --git a/src/DaggyCore/providers/CSshFabric.hpp b/src/DaggyCore/providers/CSshFabric.hpp new file mode 100644 index 00000000..9619a5f5 --- /dev/null +++ b/src/DaggyCore/providers/CSshFabric.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "IFabric.hpp" + +namespace daggy +{ +namespace providers +{ + +class CSshFabric : public IFabric +{ +public: + CSshFabric(); + + const QString& type() const; + +protected: + daggy::Result createProvider(const QString& session, const Source& source, QObject* parent); + +}; + +} +} + + diff --git a/src/DaggyCore/providers/IProvider.cpp b/src/DaggyCore/providers/IProvider.cpp index 3bb02b60..977e0493 100644 --- a/src/DaggyCore/providers/IProvider.cpp +++ b/src/DaggyCore/providers/IProvider.cpp @@ -71,6 +71,11 @@ int daggy::providers::IProvider::restartCommandsCount() const noexcept return result; } +const QString& daggy::providers::IProvider::session() const +{ + return session_; +} + void daggy::providers::IProvider::setState(DaggyProviderStates state) { if (state_ == state) diff --git a/src/DaggyCore/providers/IProvider.hpp b/src/DaggyCore/providers/IProvider.hpp index 3ed90b50..3106db0c 100644 --- a/src/DaggyCore/providers/IProvider.hpp +++ b/src/DaggyCore/providers/IProvider.hpp @@ -54,6 +54,8 @@ class DAGGYCORE_EXPORT IProvider : public QObject int restartCommandsCount() const noexcept; + const QString& session() const; + signals: void stateChanged(DaggyProviderStates state); diff --git a/src/DaggyCore/tests/local/CMakeLists.txt b/src/DaggyCore/tests/local/CMakeLists.txt index 1308e985..d51d7a3b 100644 --- a/src/DaggyCore/tests/local/CMakeLists.txt +++ b/src/DaggyCore/tests/local/CMakeLists.txt @@ -17,7 +17,8 @@ set(SOURCES qt6_add_executable(${TARGET} ${SOURCES}) include(rpath_bin) add_test(NAME ${TARGET} COMMAND ${TARGET} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}) -set_tests_properties(${TARGET} PROPERTIES ENVIRONMENT "PATH=${PATH}:${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") +message("!!!!!!!!!!!!!!!!!111111111111111 PATH $ENV{PATH}:${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") +set_tests_properties(${TARGET} PROPERTIES ENVIRONMENT "PATH=$ENV{PATH}:${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") target_link_libraries(${TARGET} PRIVATE DaggyCore Qt6::Test) target_precompile_headers(${TARGET} PRIVATE Precompiled.h) diff --git a/src/DaggyCore/tests/local/DaggyCoreLocalTests.cpp b/src/DaggyCore/tests/local/DaggyCoreLocalTests.cpp index e66d5024..9ca64a90 100644 --- a/src/DaggyCore/tests/local/DaggyCoreLocalTests.cpp +++ b/src/DaggyCore/tests/local/DaggyCoreLocalTests.cpp @@ -158,6 +158,29 @@ thread_local const Sources once_process_data_sources = { }; +constexpr const char* yaml_test = R"YAML( +aliases: + - &my_commands + my_command: + exec: my_command 'with something' parameters + extension: log + parameters: + --exec: 'run something' + -v: True + -l: 10 + --option: option + --list: + - one + - two + - three + --flag: +sources: + localhost: + type: test + host: my_host + commands: *my_commands +)YAML"; + } @@ -167,12 +190,41 @@ DaggyCoreLocalTests::DaggyCoreLocalTests(QObject *parent) } -void DaggyCoreLocalTests::init() +void DaggyCoreLocalTests::initTestCase() { + auto path_env = QString(qgetenv("PATH")); + path_env = path_env.isEmpty() ? QCoreApplication::applicationDirPath() : QString("%1:%2").arg(path_env, QCoreApplication::applicationDirPath()); + auto path = path_env.toStdString(); + qputenv("PATH", path.c_str()); } -void DaggyCoreLocalTests::cleanup() +void DaggyCoreLocalTests::checkYamlParser() { +#ifdef YAML_SUPPORT + QString error; + Sources sources = std::move(*sources::convertors::yaml(yaml_test, error)); + QVERIFY(error.isEmpty()); + + const auto& localhost = sources["localhost"]; + QCOMPARE(localhost.type, "test"); + QCOMPARE(localhost.host, "my_host"); + + QCOMPARE(localhost.commands.isEmpty(), false); + + const auto& command = localhost.commands["my_command"]; + QCOMPARE(command.exec, "my_command 'with something' parameters"); + QCOMPARE(command.extension, "log"); + + const auto& parameters = command.parameters; + QCOMPARE(parameters.isEmpty(), false); + QCOMPARE(parameters["--exec"], "run something"); + QCOMPARE(parameters["-v"], true); + QCOMPARE(parameters["-l"], 10); + QCOMPARE(parameters["--option"], "option"); + QCOMPARE(parameters["--list"], QVariantList({"one", "two", "three"})); + + QCOMPARE(command.getParameters(), QStringList({"--exec", "run something", "--flag", "--list", "one", "two", "three", "--option", "option", "-l", "10", "-v", "true"})); +#endif } diff --git a/src/DaggyCore/tests/local/DaggyCoreLocalTests.h b/src/DaggyCore/tests/local/DaggyCoreLocalTests.h index 640d066d..ed1b6fcd 100644 --- a/src/DaggyCore/tests/local/DaggyCoreLocalTests.h +++ b/src/DaggyCore/tests/local/DaggyCoreLocalTests.h @@ -37,8 +37,9 @@ class DaggyCoreLocalTests : public QObject explicit DaggyCoreLocalTests(QObject *parent = nullptr); private slots: - void init(); - void cleanup(); + void initTestCase(); + + void checkYamlParser(); void startAndTerminateTest_data(); void startAndTerminateTest(); diff --git a/src/DaggyCore/tests/local/Precompiled.h b/src/DaggyCore/tests/local/Precompiled.h index ef862abd..90a46480 100644 --- a/src/DaggyCore/tests/local/Precompiled.h +++ b/src/DaggyCore/tests/local/Precompiled.h @@ -25,3 +25,4 @@ SOFTWARE. #pragma once #include +#include