diff --git a/CMakeLists.txt b/CMakeLists.txt index c83979b..dea9e62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.5) -project(smsub LANGUAGES CXX) +project(smsub LANGUAGES CXX VERSION 0.10.1) set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -18,6 +18,18 @@ else() find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) endif() find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network WebSockets REQUIRED) +set(SMSUB_LIBS Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::WebSockets) + +option(WITH_BOOST "Use Boost library" ON) +if(WITH_BOOST) + find_package(Boost COMPONENTS json REQUIRED) + list(APPEND SMSUB_LIBS + Boost::json + ) + list(APPEND SMSUB_DEFINES + BOOST_JSON + ) +endif() set(SMSUB_SOURCES main.cpp @@ -31,11 +43,7 @@ set(SMSUB_HEADERS SMSubServer.h ) -add_executable(smsub - ${SMSUB_HEADERS} - ${SMSUB_SOURCES} -) - -target_link_libraries(smsub PRIVATE Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::WebSockets) - +add_executable(smsub ${SMSUB_HEADERS} ${SMSUB_SOURCES}) +target_compile_definitions(smsub PRIVATE ${SMSUB_DEFINES}) +target_link_libraries(smsub PRIVATE ${SMSUB_LIBS}) install(TARGETS smsub DESTINATION bin) diff --git a/SMSubProcess.cpp b/SMSubProcess.cpp index 9a67656..9b19b51 100644 --- a/SMSubProcess.cpp +++ b/SMSubProcess.cpp @@ -1,6 +1,6 @@ /***************************************************************************** * smsub Server Manager Subprocess -* Copyright (C) 2020-2021 Syping +* Copyright (C) 2020-2024 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -18,55 +18,79 @@ #include <QCoreApplication> #include <QTextStream> +#include <QDateTime> #include "SMSubProcess.h" #include "smsub.h" -#include <QDebug> -SMSubProcess::SMSubProcess(const QString &executable, const QStringList &arguments, const QString &workingDirectory, const int &termTimeout) : - termTimeout(termTimeout) +SMSubProcess::SMSubProcess(const QString &executable, const QStringList &arguments, const QString &workingDirectory, const int termTimeout, const bool buffReads, const bool keepAlive) : + termTimeout(termTimeout), buffReads(buffReads), keepAlive(keepAlive) { // Set process executable, arguments and working directory process.setProgram(executable); process.setArguments(arguments); process.setWorkingDirectory(workingDirectory); - // stdout and stderr in same IO stream + // manage input channel + process.setInputChannelMode(QProcess::ManagedInputChannel); + + // stdout and stderr in same I/O stream process.setProcessChannelMode(QProcess::MergedChannels); // Connect process signal handlers QObject::connect(&process, &QProcess::readyRead, this, &SMSubProcess::readyRead); QObject::connect(&process, &QProcess::errorOccurred, this, &SMSubProcess::processError); - QObject::connect(&process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [=](int exitCode) { - // Exit with the same exit code as the process - emit processStopped(); - QCoreApplication::exit(exitCode); + QObject::connect(&process, &QProcess::started, this, [=]() { + QTextStream(stderr) << "Subprocess started!" << smsub_endl; + emit statusUpdated(true, QDateTime::currentDateTimeUtc().toSecsSinceEpoch()); + }); + QObject::connect(&process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [=](int exitCode) { + QTextStream(stderr) << "Subprocess exited!" << smsub_endl; + // Exit with the same exit code as the process + emit statusUpdated(false, QDateTime::currentDateTimeUtc().toSecsSinceEpoch()); + if (!keepAlive) + QCoreApplication::exit(exitCode); }); -} - -void SMSubProcess::start() -{ - process.start(QIODevice::ReadWrite); } void SMSubProcess::readyRead() { +#ifdef SMSUB_IODEBUG + QTextStream(stderr) << "Subprocess I/O RR!" << smsub_endl; +#endif // Read process output and emit event - while (process.canReadLine()) { - const QByteArray readData = process.readLine().trimmed(); - emit outputWritten(readData + '\n'); + if (buffReads) { + while (process.bytesAvailable()) { +#ifdef SMSUB_IODEBUG + QTextStream(stderr) << "Subprocess I/O W!" << smsub_endl; +#endif + const QByteArray readData = process.read(1024); + if (!readData.isEmpty()) + emit outputWritten(readData); + } + } + else { + while (process.canReadLine()) { +#ifdef SMSUB_IODEBUG + QTextStream(stderr) << "Subprocess I/O WL!" << smsub_endl; +#endif + const QByteArray readData = process.readLine().trimmed(); + emit outputWritten(readData + '\n'); + } } } void SMSubProcess::processError(QProcess::ProcessError error) { // Handle process errors - if (likely(error == QProcess::FailedToStart)) { + if (Q_LIKELY(error == QProcess::FailedToStart)) { QTextStream(stderr) << "Process failed to start!" << smsub_endl; - QCoreApplication::exit(1); + if (!keepAlive) + QCoreApplication::exit(1); } else if (error == QProcess::UnknownError) { QTextStream(stderr) << "Unknown error occurred!" << smsub_endl; - QCoreApplication::exit(1); + if (!keepAlive) + QCoreApplication::exit(1); } } @@ -81,24 +105,33 @@ void SMSubProcess::aboutToQuit() } } -void SMSubProcess::killProcess() +void SMSubProcess::startProcess() { - // Kill process as requested - if (process.state() == QProcess::Running) { - process.kill(); - } + // Start process as requested + if (process.state() == QProcess::NotRunning) + process.start(QIODevice::ReadWrite); } void SMSubProcess::stopProcess() { // Terminate process as requested - if (process.state() == QProcess::Running) { + if (process.state() == QProcess::Running) process.terminate(); - } +} + +void SMSubProcess::killProcess() +{ + // Kill process as requested + if (process.state() == QProcess::Running) + process.kill(); } void SMSubProcess::writeInput(const QByteArray &input) { +#ifdef SMSUB_IODEBUG + QTextStream(stderr) << "Subprocess I/O IN!" << smsub_endl; +#endif // Write input from Unix/IPC socket to process - process.write(input); + if (process.state() == QProcess::Running) + process.write(input); } diff --git a/SMSubProcess.h b/SMSubProcess.h index dedd81c..0402192 100644 --- a/SMSubProcess.h +++ b/SMSubProcess.h @@ -1,6 +1,6 @@ /***************************************************************************** * smsub Server Manager Subprocess -* Copyright (C) 2020-2021 Syping +* Copyright (C) 2020-2024 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -26,17 +26,19 @@ class SMSubProcess : public QObject { Q_OBJECT public: - SMSubProcess(const QString &executable, const QStringList &arguments, const QString& workingDirectory, const int &termTimeout = 60000); - void start(); + SMSubProcess(const QString &executable, const QStringList &arguments, const QString &workingDirectory, const int termTimeout = 60000, const bool buffReads = false, const bool keepAlive = false); private: QProcess process; int termTimeout; + bool buffReads; + bool keepAlive; public slots: void aboutToQuit(); - void killProcess(); + void startProcess(); void stopProcess(); + void killProcess(); void writeInput(const QByteArray &input); private slots: @@ -45,8 +47,7 @@ private slots: signals: void outputWritten(const QByteArray &output); - void processStopped(); - + void statusUpdated(const bool status, const qint64 time); }; #endif // SMSUBPROCESS_H diff --git a/SMSubServer.cpp b/SMSubServer.cpp index 962829a..6ea0b0f 100644 --- a/SMSubServer.cpp +++ b/SMSubServer.cpp @@ -1,6 +1,6 @@ /***************************************************************************** * smsub Server Manager Subprocess -* Copyright (C) 2020-2021 Syping +* Copyright (C) 2020-2024 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -21,6 +21,12 @@ #include <QUuid> #include "SMSubServer.h" #include "smsub.h" +#ifdef BOOST_JSON +#include <boost/json.hpp> +#else +#include <QJsonDocument> +#include <QJsonObject> +#endif SMSubServer::SMSubServer(SMSubServerSettings *serverSettings, const QString &socket) : serverSettings(serverSettings) { @@ -32,22 +38,31 @@ SMSubServer::SMSubServer(SMSubServerSettings *serverSettings, const QString &soc type = ServerType::Local; server = localServer; + status = false; + startTime = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); + stopTime = startTime; } SMSubServer::SMSubServer(SMSubServerSettings *serverSettings, const QString &serverName, const quint16 &port) : serverSettings(serverSettings) { QWebSocketServer *webSocketServer = new QWebSocketServer(serverName, QWebSocketServer::NonSecureMode, this); +#if QT_VERSION >= 0x060400 + webSocketServer->setSupportedSubprotocols(QStringList() << "smsub" << "smsub_json"); +#endif webSocketServer->listen(QHostAddress::LocalHost, port); QObject::connect(webSocketServer, &QWebSocketServer::newConnection, this, &SMSubServer::newConnection); type = ServerType::WebSocket; server = webSocketServer; + status = false; + startTime = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); + stopTime = startTime; } bool SMSubServer::isListening() { - if (likely(type == ServerType::Local)) { + if (type == ServerType::Local) { return static_cast<QLocalServer*>(server)->isListening(); } else if (type == ServerType::WebSocket) { @@ -59,12 +74,13 @@ bool SMSubServer::isListening() void SMSubServer::newConnection() { QObject *socket; - if (likely(type == ServerType::Local)) { + if (Q_LIKELY(type == ServerType::Local)) { QLocalSocket *localSocket = static_cast<QLocalServer*>(server)->nextPendingConnection(); QObject::connect(localSocket, &QLocalSocket::readyRead, this, &SMSubServer::lsReadyRead); QObject::connect(localSocket, &QLocalSocket::disconnected, this, &SMSubServer::deleteSocket); localSocket->write(QString("SMSub Version %1\n").arg(QCoreApplication::applicationVersion()).toUtf8()); socket = localSocket; + QTextStream(stderr) << "LocalSocket connected!" << smsub_endl; } else if (type == ServerType::WebSocket) { QWebSocket *webSocket = static_cast<QWebSocketServer*>(server)->nextPendingConnection(); @@ -72,6 +88,12 @@ void SMSubServer::newConnection() QObject::connect(webSocket, &QWebSocket::disconnected, this, &SMSubServer::deleteSocket); webSocket->sendBinaryMessage(QString("SMSub Version %1\n").arg(QCoreApplication::applicationVersion()).toUtf8()); socket = webSocket; + QTextStream(stderr) << QString("WebSocket %1:%2 connected!").arg(webSocket->peerAddress().toString(), QString::number(webSocket->peerPort())) << smsub_endl; + +#if QT_VERSION >= 0x060400 + if (webSocket->subprotocol() == "smsub_json") + socket->setProperty("ReceiveJson", true); +#endif } else { // Just for being sure @@ -91,57 +113,109 @@ void SMSubServer::newConnection() bool SMSubServer::messageReceived(QObject *socket, const QByteArray &message) { // Only allow commands being sent if authenticated - const bool isAuthenticated = socket->property("Authenticated").toBool(); - if (likely(isAuthenticated)) { - if (message.startsWith("+dbg")) { + const bool isAuthenticated = isVariantTrue(socket->property("Authenticated")); + if (isAuthenticated) { + if (message == "+dbg") { socket->setProperty("ReceiveDbgMsg", true); - sendMessage(socket, "Debug messages enabled!\n"); + debugOutput(socket, "Debug messages enabled!"); } - else if (message.startsWith("-dbg")) { + else if (message == "-dbg") { + debugOutput(socket, "Debug messages disabled!"); socket->setProperty("ReceiveDbgMsg", false); - sendMessage(socket, "Debug messages disabled!\n"); } - else if (message.startsWith("+log")) { + else if (message == "+json") { + socket->setProperty("ReceiveJson", true); + debugOutput(socket, "JSON output enabled!"); + } + else if (message == "-json") { + socket->setProperty("ReceiveJson", false); + debugOutput(socket, "JSON output disabled!"); + } + else if (message == "+log") { socket->setProperty("ReceiveLog", true); debugOutput(socket, "Log output enabled!"); } - else if (message.startsWith("-log")) { + else if (message == "-log") { socket->setProperty("ReceiveLog", false); debugOutput(socket, "Log output disabled!"); } - else if (message.startsWith("+reg")) { - if (likely(serverSettings->canRegister)) { - QByteArray authUuid = QUuid::createUuid().toByteArray(QUuid::Id128); + else if (message == "+status") { + socket->setProperty("ReceiveStatus", true); + debugOutput(socket, "Status updates enabled!"); + } + else if (message == "-status") { + socket->setProperty("ReceiveStatus", false); + debugOutput(socket, "Status updates disabled!"); + } + else if (message == "+reg") { + if (serverSettings->canRegister) { + QByteArray authUuid = QUuid::createUuid().toByteArray(QUuid::Id128) + + QUuid::createUuid().toByteArray(QUuid::Id128); authUuid = QByteArray::fromHex(authUuid).toBase64(QByteArray::OmitTrailingEquals); - emit tokenRegistered(QString::fromUtf8(authUuid)); + emit tokenRegistered(authUuid); sendMessage(socket, "Token: " + authUuid + '\n'); } else { sendMessage(socket, "Permission denied!\n"); } } - else if (message.startsWith("kill")) { - emit killRequested(); - debugOutput(socket, "Killing server!"); + else if (message == "status") { + if (isVariantTrue(socket->property("ReceiveJson"))) { +#ifdef BOOST_JSON + boost::json::object object; + object["type"] = "status"; + object["status"] = status; + object["start"] = startTime; + object["stop"] = stopTime; + const std::string json = boost::json::serialize(object) + '\n'; + sendMessage(socket, QByteArray::fromRawData(json.data(), json.size())); +#else + QJsonObject object; + object["type"] = "status"; + object["status"] = status; + object["start"] = startTime; + object["stop"] = stopTime; + sendMessage(socket, QJsonDocument(object).toJson(QJsonDocument::Compact) + '\n'); +#endif + } + else { + sendMessage(socket, QString("Status: %1\nLast Start: %2\nLast Stop: %3\n").arg(status ? "on" : "off", QString::number(startTime), QString::number(stopTime)).toUtf8()); + } } - else if (message.startsWith("stop")) { + else if (message == "start") { + emit startRequested(); + debugOutput(socket, "Starting server!"); + } + else if (message == "stop") { emit stopRequested(); debugOutput(socket, "Stopping server!"); } - else if (message.startsWith("wl")) { + else if (message == "kill") { + emit killRequested(); + debugOutput(socket, "Killing server!"); + } + else if (message == "quit") { + QTimer::singleShot(0, qApp, &QCoreApplication::quit); + debugOutput(socket, "Qutting smsub!"); + } + else if (message == "wl") { + emit inputWritten("\n"); + debugOutput(socket, "Writing line!"); + } + else if (message.startsWith("wl ")) { const QByteArray writeData = message.mid(3); emit inputWritten(writeData + '\n'); - debugOutput(socket, "Write line \"" + writeData + "\"!"); + debugOutput(socket, "Writing line: " + writeData); } - else if (message.startsWith("w")) { + else if (message.startsWith("w ")) { const QByteArray writeData = message.mid(2); emit inputWritten(writeData); - debugOutput(socket, "Write \"" + writeData + "\"!"); + debugOutput(socket, "Writing: " + writeData); } } else { // Authenticate when token is valid, otherwise disconnect - if (unlikely(tokens.contains(QString::fromUtf8(message)))) { + if (tokens.contains(message)) { // Set client as authenticated and add it to vector socket->setProperty("Authenticated", true); sendMessage(socket, "Login successful!\n"); @@ -149,7 +223,7 @@ bool SMSubServer::messageReceived(QObject *socket, const QByteArray &message) } else { // Stop receiving data and disconnect socket - if (likely(type == ServerType::Local)) { + if (type == ServerType::Local) { QLocalSocket *localSocket = static_cast<QLocalSocket*>(socket); QObject::disconnect(localSocket, &QLocalSocket::readyRead, this, &SMSubServer::lsReadyRead); localSocket->write("Incorrect token!\n"); @@ -168,6 +242,18 @@ bool SMSubServer::messageReceived(QObject *socket, const QByteArray &message) return true; } +bool SMSubServer::isVariantTrue(const QVariant &variant) +{ +#if QT_VERSION >= 0x060000 + if (variant.typeId() == QMetaType::Bool) { +#else + if (variant.type() == QVariant::Bool) { +#endif + return variant.toBool(); + } + return false; +} + void SMSubServer::wsMessageReceived(const QByteArray &message) { QWebSocket *socket = static_cast<QWebSocket*>(sender()); @@ -177,7 +263,13 @@ void SMSubServer::wsMessageReceived(const QByteArray &message) void SMSubServer::lsReadyRead() { QLocalSocket *socket = static_cast<QLocalSocket*>(sender()); +#ifdef SMSUB_IODEBUG + QTextStream(stderr) << "LocalSocket I/O RR!" << smsub_endl; +#endif while (socket->canReadLine()) { +#ifdef SMSUB_IODEBUG + QTextStream(stderr) << "LocalSocket I/O WL!" << smsub_endl; +#endif const QByteArray message = socket->readLine().trimmed(); if (!messageReceived(socket, message)) return; @@ -195,10 +287,22 @@ void SMSubServer::deleteSocket() void SMSubServer::debugOutput(QObject *socket, const QByteArray &message) { // Only send debug messages when the client opted-in - const QVariant variant = socket->property("ReceiveDbgMsg"); - if (unlikely(variant.type() == QVariant::Bool)) { - bool receiveDbgMsg = variant.toBool(); - if (likely(receiveDbgMsg)) { + if (isVariantTrue(socket->property("ReceiveDbgMsg"))) { + if (isVariantTrue(socket->property("ReceiveJson"))) { +#ifdef BOOST_JSON + boost::json::object object; + object["type"] = "debug"; + object["message"] = message.toBase64(QByteArray::OmitTrailingEquals).constData(); + const std::string json = boost::json::serialize(object) + '\n'; + sendMessage(socket, QByteArray::fromRawData(json.data(), json.size())); +#else + QJsonObject object; + object["type"] = "debug"; + object["message"] = QString::fromUtf8(message.toBase64(QByteArray::OmitTrailingEquals)); + sendMessage(socket, QJsonDocument(object).toJson(QJsonDocument::Compact) + '\n'); +#endif + } + else { sendMessage(socket, message + '\n'); } } @@ -207,11 +311,26 @@ void SMSubServer::debugOutput(QObject *socket, const QByteArray &message) void SMSubServer::writeOutput(const QByteArray &output) { // Read process output when client opted-in for log + QByteArray json_output; for (auto it = sockets.constBegin(); it != sockets.constEnd(); it++) { - const QVariant variant = (*it)->property("ReceiveLog"); - if (unlikely(variant.type() == QVariant::Bool)) { - bool receiveLog = variant.toBool(); - if (likely(receiveLog)) { + if (isVariantTrue((*it)->property("ReceiveLog"))) { + if (isVariantTrue((*it)->property("ReceiveJson"))) { + if (json_output.isEmpty()) { +#ifdef BOOST_JSON + boost::json::object object; + object["type"] = "log"; + object["message"] = output.toBase64(QByteArray::OmitTrailingEquals).constData(); + json_output = QByteArray::fromStdString(boost::json::serialize(object) + '\n'); +#else + QJsonObject object; + object["type"] = "log"; + object["message"] = QString::fromUtf8(output.toBase64(QByteArray::OmitTrailingEquals)); + json_output = QJsonDocument(object).toJson(QJsonDocument::Compact) + '\n'; +#endif + } + sendMessage(*it, json_output); + } + else { sendMessage(*it, output); } } @@ -220,7 +339,7 @@ void SMSubServer::writeOutput(const QByteArray &output) void SMSubServer::sendMessage(QObject *socket, const QByteArray &message) { - if (likely(type == ServerType::Local)) { + if (type == ServerType::Local) { QLocalSocket *localSocket = static_cast<QLocalSocket*>(socket); localSocket->write(message); } @@ -230,7 +349,7 @@ void SMSubServer::sendMessage(QObject *socket, const QByteArray &message) } } -void SMSubServer::registerToken(const QString &token) +void SMSubServer::registerToken(const QByteArray &token) { // Register temporary token for a secure remote connection tokens << token; @@ -238,3 +357,41 @@ void SMSubServer::registerToken(const QString &token) tokens.removeAll(token); }); } + +void SMSubServer::statusUpdated(const bool status_, const qint64 time) +{ + status = status_; + status ? (startTime = time) : (stopTime = time); + + QByteArray output, json_output; + for (auto it = sockets.constBegin(); it != sockets.constEnd(); it++) { + if (isVariantTrue((*it)->property("ReceiveStatus"))) { + if (isVariantTrue((*it)->property("ReceiveJson"))) { + if (json_output.isEmpty()) { +#ifdef BOOST_JSON + boost::json::object object; + object["type"] = "status"; + object["status"] = status; + object["start"] = startTime; + object["stop"] = stopTime; + json_output = QByteArray::fromStdString(boost::json::serialize(object) + '\n'); +#else + QJsonObject object; + object["type"] = "status"; + object["status"] = status; + object["start"] = startTime; + object["stop"] = stopTime; + json_output = QJsonDocument(object).toJson(QJsonDocument::Compact) + '\n'; +#endif + } + sendMessage(*it, json_output); + } + else { + if (output.isEmpty()) { + output = QString("Status: %1\nLast Start: %2\nLast Stop: %3\n").arg(status ? "on" : "off", QString::number(startTime), QString::number(stopTime)).toUtf8(); + } + sendMessage(*it, json_output); + } + } + } +} diff --git a/SMSubServer.h b/SMSubServer.h index f5f9936..d7f452a 100644 --- a/SMSubServer.h +++ b/SMSubServer.h @@ -1,6 +1,6 @@ /***************************************************************************** * smsub Server Manager Subprocess -* Copyright (C) 2020-2021 Syping +* Copyright (C) 2020-2024 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -44,7 +44,8 @@ public: public slots: void writeOutput(const QByteArray &output); - void registerToken(const QString &token); + void registerToken(const QByteArray &token); + void statusUpdated(const bool status, const qint64 time); private slots: void wsMessageReceived(const QByteArray &message); @@ -56,17 +57,22 @@ private: inline void debugOutput(QObject *socket, const QByteArray &message); inline void sendMessage(QObject *socket, const QByteArray &message); bool messageReceived(QObject *socket, const QByteArray &message); + inline bool isVariantTrue(const QVariant &variant); SMSubServerSettings *serverSettings; QVector<QObject*> sockets; - QVector<QString> tokens; + QVector<QByteArray> tokens; + qint64 startTime; + qint64 stopTime; ServerType type; QObject *server; + bool status; signals: - void tokenRegistered(const QString &password); + void tokenRegistered(const QByteArray &token); void inputWritten(const QByteArray &input); - void killRequested(); + void startRequested(); void stopRequested(); + void killRequested(); }; #endif // SMSUBSERVER_H diff --git a/main.cpp b/main.cpp index 1baf24b..a3d8eee 100644 --- a/main.cpp +++ b/main.cpp @@ -1,6 +1,6 @@ /***************************************************************************** * smsub Server Manager Subprocess -* Copyright (C) 2020-2021 Syping +* Copyright (C) 2020-2024 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -33,7 +33,6 @@ #ifdef Q_OS_UNIX #include <initializer_list> #include "signal.h" -#include "unistd.h" #endif #ifdef Q_OS_UNIX @@ -135,15 +134,18 @@ int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); a.setApplicationName("Server Manager Subprocess"); - a.setApplicationVersion("0.5"); + a.setApplicationVersion("0.10.1"); #ifdef Q_OS_UNIX catchUnixSignals({SIGINT, SIGHUP, SIGQUIT, SIGTERM}); #endif bool rportSet = false; + bool autoStart = true; + bool buffReads = false; + bool keepAlive = false; bool timeoutSet = false; - int termTimeout = 60000; + size_t termTimeout = 60000; quint16 rport; QString socket; QString rsocket; @@ -151,24 +153,28 @@ int main(int argc, char *argv[]) QString workingDirectory; QStringList argumentList; - QByteArray envEnvironmentMode = qgetenv("SMSUB_ENVIRONMENT_MODE"); - QByteArray envManifest = qgetenv("SMSUB_JSON"); - QByteArray envExecutable = qgetenv("SMSUB_EXEC"); - QByteArray envArguments = qgetenv("SMSUB_ARGS"); - QByteArray envSocket = qgetenv("SMSUB_SOCK"); - QByteArray envRemotePort = qgetenv("SMSUB_RPORT"); - QByteArray envRemoteSocket = qgetenv("SMSUB_RSOCK"); - QByteArray envTimeout = qgetenv("SMSUB_TIMEOUT"); - QByteArray envWorkDir = qgetenv("SMSUB_WORKDIR"); + const QByteArray envEnvironmentMode = qgetenv("SMSUB_ENVIRONMENT_MODE"); + const QByteArray envAutoStart = qgetenv("SMSUB_AUTOSTART"); + const QByteArray envBuffReads = qgetenv("SMSUB_BUFFERED_READS"); + const QByteArray envKeepAlive = qgetenv("SMSUB_KEEPALIVE"); + const QByteArray envManifest = qgetenv("SMSUB_JSON"); + const QByteArray envExecutable = qgetenv("SMSUB_EXEC"); + const QByteArray envArguments = qgetenv("SMSUB_ARGS"); + const QByteArray envSocket = qgetenv("SMSUB_SOCK"); + const QByteArray envRemotePort = qgetenv("SMSUB_RPORT"); + const QByteArray envRemoteSocket = qgetenv("SMSUB_RSOCK"); + const QByteArray envTimeout = qgetenv("SMSUB_TIMEOUT"); + const QByteArray envWorkDir = qgetenv("SMSUB_WORKDIR"); - if (unlikely(envEnvironmentMode == "1" || envEnvironmentMode.toLower() == "true")) { - if (likely(envExecutable.isEmpty() && envArguments.isEmpty())) { + if (envEnvironmentMode == "1" || envEnvironmentMode.toLower() == "true" || envEnvironmentMode.toLower() == "yes") { + if (envExecutable.isEmpty() && envArguments.isEmpty()) { QStringList arguments = a.arguments(); + arguments.removeFirst(); executable = arguments.takeFirst(); argumentList = arguments; } else { - if (likely(!envExecutable.isEmpty())) { + if (!envExecutable.isEmpty()) { executable = QString::fromUtf8(envExecutable); } else { @@ -176,7 +182,7 @@ int main(int argc, char *argv[]) return 1; } - if (likely(!envArguments.isEmpty())) { + if (!envArguments.isEmpty()) { argumentList = parseStringArguments(QString::fromUtf8(envArguments)); if (argumentList.empty()) { QTextStream(stderr) << "Arguments can't be parsed properly!" << smsub_endl; @@ -189,7 +195,7 @@ int main(int argc, char *argv[]) } } - if (unlikely(!envTimeout.isEmpty())) { + if (!envTimeout.isEmpty()) { bool ok; const int _termTimeout = envTimeout.toInt(&ok); if (ok) { @@ -202,14 +208,32 @@ int main(int argc, char *argv[]) } } - if (unlikely(!envWorkDir.isEmpty())) { + if (!envAutoStart.isEmpty()) { + if (envAutoStart == "0" || envAutoStart.toLower() == "false" || envAutoStart.toLower() == "no") { + autoStart = false; + } + } + + if (!envBuffReads.isEmpty()) { + if (envBuffReads == "1" || envBuffReads.toLower() == "true" || envBuffReads.toLower() == "yes") { + buffReads = true; + } + } + + if (!envKeepAlive.isEmpty()) { + if (envKeepAlive == "1" || envKeepAlive.toLower() == "true" || envKeepAlive.toLower() == "yes") { + keepAlive = true; + } + } + + if (!envWorkDir.isEmpty()) { workingDirectory = QString::fromUtf8(envWorkDir); } else { workingDirectory = QFileInfo(executable).absolutePath(); } - if (likely(!envSocket.isEmpty())) { + if (!envSocket.isEmpty()) { socket = QString::fromUtf8(envSocket); } else { @@ -221,19 +245,20 @@ int main(int argc, char *argv[]) return 1; } - if (unlikely(!envRemoteSocket.isEmpty())) { + if (!envRemoteSocket.isEmpty()) { rsocket = QString::fromUtf8(envRemoteSocket); } - if (unlikely(!envRemotePort.isEmpty())) { + if (!envRemotePort.isEmpty()) { bool ok; - rport = envRemotePort.toUShort(&ok); - if (!ok) { - QTextStream(stderr) << "WebSockets port is not valid in environment!" << smsub_endl; - return 1; + const quint16 _rport = envRemotePort.toUShort(&ok); + if (ok) { + rport = _rport; + rportSet = true; } else { - rportSet = true; + QTextStream(stderr) << "WebSockets port is not valid in environment!" << smsub_endl; + return 1; } } } @@ -268,31 +293,37 @@ int main(int argc, char *argv[]) #endif commandLineParser.addOption(subprocessRemoteSocket); + QCommandLineOption processAutoStart("autostart", "SMSub autostart mode.", "autostart"); + commandLineParser.addOption(processAutoStart); + + QCommandLineOption processKeepAlive("keepalive", "SMSub keepalive mode.", "keepalive"); + commandLineParser.addOption(processKeepAlive); + QCommandLineOption processTimeout("timeout", "SMSub termination timeout.", "timeout"); commandLineParser.addOption(processTimeout); commandLineParser.process(a); - if (unlikely(commandLineParser.isSet(processManifest) && commandLineParser.isSet(processExecutable) || + if (commandLineParser.isSet(processManifest) && commandLineParser.isSet(processExecutable) || !envManifest.isEmpty() && !envExecutable.isEmpty() || commandLineParser.isSet(processManifest) && !envExecutable.isEmpty() || - !envManifest.isEmpty() && commandLineParser.isSet(processExecutable))) { + !envManifest.isEmpty() && commandLineParser.isSet(processExecutable)) { QTextStream(stderr) << "You can't define a Process executable and a JSON process manifest at the same time!" << smsub_endl; return 1; } - if (unlikely(commandLineParser.isSet(processManifest) && commandLineParser.isSet(processArguments) || + if (commandLineParser.isSet(processManifest) && commandLineParser.isSet(processArguments) || !envManifest.isEmpty() && !envArguments.isEmpty() || commandLineParser.isSet(processManifest) && !envArguments.isEmpty() || - !envManifest.isEmpty() && commandLineParser.isSet(processArguments))) { - QTextStream(stderr) << "You can't define a Process arguments and a JSON process manifest at the same time!" << smsub_endl; + !envManifest.isEmpty() && commandLineParser.isSet(processArguments)) { + QTextStream(stderr) << "You can't define Process arguments and a JSON process manifest at the same time!" << smsub_endl; return 1; } - if (unlikely(commandLineParser.isSet(subprocessRemotePort) && commandLineParser.isSet(subprocessRemoteSocket) || + if (commandLineParser.isSet(subprocessRemotePort) && commandLineParser.isSet(subprocessRemoteSocket) || !envRemotePort.isEmpty() && !envRemoteSocket.isEmpty() || commandLineParser.isSet(subprocessRemotePort) && !envRemoteSocket.isEmpty() || - !envRemotePort.isEmpty() && commandLineParser.isSet(subprocessRemoteSocket))) { + !envRemotePort.isEmpty() && commandLineParser.isSet(subprocessRemoteSocket)) { #ifdef Q_OS_WIN QTextStream(stderr) << "You can't define a WebSockets port and a IPC socket at same time!" << smsub_endl; #else @@ -301,9 +332,9 @@ int main(int argc, char *argv[]) return 1; } - if (unlikely(commandLineParser.isSet(processTimeout))) { + if (commandLineParser.isSet(processTimeout)) { bool ok; - const int _termTimeout = commandLineParser.value(processTimeout).toInt(&ok); + const int _termTimeout = commandLineParser.value(processTimeout).toInt(&ok, 10); if (ok) { termTimeout = _termTimeout; timeoutSet = true; @@ -313,9 +344,9 @@ int main(int argc, char *argv[]) return 1; } } - else if (unlikely(!envTimeout.isEmpty())) { + else if (!envTimeout.isEmpty()) { bool ok; - const int _termTimeout = envTimeout.toInt(&ok); + const int _termTimeout = envTimeout.toInt(&ok, 10); if (ok) { termTimeout = _termTimeout; timeoutSet = true; @@ -326,14 +357,44 @@ int main(int argc, char *argv[]) } } - if (unlikely(commandLineParser.isSet(processExecutable))) { + if (commandLineParser.isSet(processExecutable)) { executable = commandLineParser.value(processExecutable); } - else if (unlikely(!envExecutable.isEmpty())) { + else if (!envExecutable.isEmpty()) { executable = QString::fromUtf8(envExecutable); } - if (unlikely(!envWorkDir.isEmpty())) { + if (commandLineParser.isSet(processAutoStart)) { + const QString claAutoStart = commandLineParser.value(processAutoStart); + if (claAutoStart == "0" || claAutoStart.toLower() == "false" || claAutoStart.toLower() == "no") { + autoStart = false; + } + } + else if (!envAutoStart.isEmpty()) { + if (envAutoStart == "0" || envAutoStart.toLower() == "false" || envAutoStart.toLower() == "no") { + autoStart = false; + } + } + + if (!envBuffReads.isEmpty()) { + if (envBuffReads == "1" || envBuffReads.toLower() == "true" || envBuffReads.toLower() == "yes") { + buffReads = true; + } + } + + if (commandLineParser.isSet(processKeepAlive)) { + const QString claKeepAlive = commandLineParser.value(processKeepAlive); + if (claKeepAlive == "1" || claKeepAlive.toLower() == "true" || claKeepAlive.toLower() == "yes") { + keepAlive = true; + } + } + else if (!envKeepAlive.isEmpty()) { + if (envKeepAlive == "1" || envKeepAlive.toLower() == "true" || envKeepAlive.toLower() == "yes") { + keepAlive = true; + } + } + + if (!envWorkDir.isEmpty()) { workingDirectory = QString::fromUtf8(envWorkDir); } else { @@ -341,23 +402,23 @@ int main(int argc, char *argv[]) } QString manifestPath; - if (likely(commandLineParser.isSet(processManifest))) { + if (commandLineParser.isSet(processManifest)) { manifestPath = commandLineParser.value(processManifest); } else if (!envManifest.isEmpty()) { manifestPath = QString::fromUtf8(envManifest); } - if (likely(!manifestPath.isEmpty())) { + if (!manifestPath.isEmpty()) { QFile manifestFile(manifestPath); - if (likely(manifestFile.open(QIODevice::ReadOnly))) { + if (manifestFile.open(QIODevice::ReadOnly)) { const QByteArray jsonData = manifestFile.readAll(); QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData); QJsonObject jsonObject = jsonDocument.object(); - if (likely(jsonObject.contains("Executable"))) { + if (jsonObject.contains("Executable")) { const QJsonValue jsonExecutable = jsonObject.value("Executable"); - if (unlikely(!jsonExecutable.isString())) { + if (!jsonExecutable.isString()) { QTextStream(stderr) << "Executable is not a string in manifest, aborting!" << smsub_endl; manifestFile.close(); return 1; @@ -370,9 +431,29 @@ int main(int argc, char *argv[]) return 1; } - if (likely(jsonObject.contains("WorkingDirectory"))) { + if (jsonObject.contains("AutoStart")) { + const QJsonValue jsonAutoStart = jsonObject.value("AutoStart"); + if (!jsonAutoStart.isBool()) { + QTextStream(stderr) << "AutoStart is not a bool in manifest, aborting!" << smsub_endl; + manifestFile.close(); + return 1; + } + autoStart = jsonAutoStart.toBool(); + } + + if (jsonObject.contains("KeepAlive")) { + const QJsonValue jsonKeepAlive = jsonObject.value("KeepAlive"); + if (!jsonKeepAlive.isBool()) { + QTextStream(stderr) << "KeepAlive is not a bool in manifest, aborting!" << smsub_endl; + manifestFile.close(); + return 1; + } + keepAlive = jsonKeepAlive.toBool(); + } + + if (jsonObject.contains("WorkingDirectory")) { const QJsonValue jsonWorkingDirectory = jsonObject.value("WorkingDirectory"); - if (unlikely(!jsonWorkingDirectory.isString())) { + if (!jsonWorkingDirectory.isString()) { QTextStream(stderr) << "Working Directory is not a string in manifest, aborting!" << smsub_endl; manifestFile.close(); return 1; @@ -380,9 +461,9 @@ int main(int argc, char *argv[]) workingDirectory = jsonWorkingDirectory.toString(); } - if (likely(jsonObject.contains("Arguments"))) { + if (jsonObject.contains("Arguments")) { const QJsonValue jsonArguments = jsonObject.value("Arguments"); - if (likely(jsonArguments.isArray())) { + if (jsonArguments.isArray()) { const QJsonArray jsonArray = jsonArguments.toArray(); for (auto it = jsonArray.constBegin(); it != jsonArray.constEnd(); it++) { argumentList << it->toString(); @@ -395,9 +476,9 @@ int main(int argc, char *argv[]) } } - if (unlikely(!timeoutSet && jsonObject.contains("TerminationTimeout"))) { + if (!timeoutSet && jsonObject.contains("TerminationTimeout")) { const QJsonValue jsonTimeout = jsonObject.value("TerminationTimeout"); - if (unlikely(!jsonTimeout.isDouble())) { + if (!jsonTimeout.isDouble()) { termTimeout = qRound(jsonTimeout.toDouble()); } else { @@ -409,14 +490,14 @@ int main(int argc, char *argv[]) manifestFile.close(); } } - else if (unlikely(commandLineParser.isSet(processArguments))) { + else if (commandLineParser.isSet(processArguments)) { argumentList = parseStringArguments(commandLineParser.value(processArguments)); if (argumentList.empty()) { QTextStream(stderr) << "Arguments can't be parsed properly!" << smsub_endl; return 1; } } - else if (unlikely(!envArguments.isEmpty())) { + else if (!envArguments.isEmpty()) { argumentList = parseStringArguments(QString::fromUtf8(envArguments)); if (argumentList.empty()) { QTextStream(stderr) << "Arguments can't be parsed properly!" << smsub_endl; @@ -424,7 +505,7 @@ int main(int argc, char *argv[]) } } - if (likely(commandLineParser.isSet(subprocessSocket))) { + if (commandLineParser.isSet(subprocessSocket)) { socket = commandLineParser.value(subprocessSocket); } else if (!envSocket.isEmpty()) { @@ -439,33 +520,35 @@ int main(int argc, char *argv[]) return 1; } - if (unlikely(commandLineParser.isSet(subprocessRemoteSocket))) { + if (commandLineParser.isSet(subprocessRemoteSocket)) { rsocket = commandLineParser.value(subprocessRemoteSocket); } - else if (unlikely(!envRemoteSocket.isEmpty())) { + else if (!envRemoteSocket.isEmpty()) { rsocket = QString::fromUtf8(envRemoteSocket); } - if (unlikely(commandLineParser.isSet(subprocessRemotePort))) { + if (commandLineParser.isSet(subprocessRemotePort)) { bool ok; - rport = commandLineParser.value(subprocessRemotePort).toUShort(&ok); - if (!ok) { - QTextStream(stderr) << "WebSockets port is not valid in arguments!" << smsub_endl; - return 1; + const quint16 _rport = commandLineParser.value(subprocessRemotePort).toUShort(&ok); + if (ok) { + rport = _rport; + rportSet = true; } else { - rportSet = true; + QTextStream(stderr) << "WebSockets port is not valid in arguments!" << smsub_endl; + return 1; } } else if (!envRemotePort.isEmpty()) { bool ok; - rport = envRemotePort.toUShort(&ok); - if (!ok) { - QTextStream(stderr) << "WebSockets port is not valid in environment!" << smsub_endl; - return 1; + const quint16 _rport = envRemotePort.toUShort(&ok); + if (ok) { + rport = _rport; + rportSet = true; } else { - rportSet = true; + QTextStream(stderr) << "WebSockets port is not valid in arguments!" << smsub_endl; + return 1; } } } @@ -474,7 +557,7 @@ int main(int argc, char *argv[]) localSettings.isLocal = true; SMSubServer subLocal(&localSettings, socket); - if (unlikely(!subLocal.isListening())) { + if (!subLocal.isListening()) { #ifdef Q_OS_WIN QTextStream(stderr) << "Failed to start local IPC socket!" << smsub_endl; #else @@ -483,15 +566,15 @@ int main(int argc, char *argv[]) return 1; } - SMSubProcess subProcess(executable, argumentList, workingDirectory, termTimeout); + SMSubProcess subProcess(executable, argumentList, workingDirectory, termTimeout, buffReads, keepAlive); SMSubServerSettings remoteSettings; remoteSettings.canRegister = false; remoteSettings.isLocal = false; - if (unlikely(!rsocket.isEmpty())) { + if (!rsocket.isEmpty()) { SMSubServer *subRemote = new SMSubServer(&remoteSettings, rsocket); - if (unlikely(!subRemote->isListening())) { + if (!subRemote->isListening()) { #ifdef Q_OS_WIN QTextStream(stderr) << "Failed to start remote IPC socket!" << smsub_endl; #else @@ -502,34 +585,43 @@ int main(int argc, char *argv[]) localSettings.canRegister = true; QObject::connect(&subLocal, &SMSubServer::tokenRegistered, subRemote, &SMSubServer::registerToken); QObject::connect(&subProcess, &SMSubProcess::outputWritten, subRemote, &SMSubServer::writeOutput); + QObject::connect(&subProcess, &SMSubProcess::statusUpdated, subRemote, &SMSubServer::statusUpdated); QObject::connect(subRemote, &SMSubServer::inputWritten, &subProcess, &SMSubProcess::writeInput); - QObject::connect(subRemote, &SMSubServer::killRequested, &subProcess, &SMSubProcess::killProcess); + QObject::connect(subRemote, &SMSubServer::startRequested, &subProcess, &SMSubProcess::startProcess); QObject::connect(subRemote, &SMSubServer::stopRequested, &subProcess, &SMSubProcess::stopProcess); + QObject::connect(subRemote, &SMSubServer::killRequested, &subProcess, &SMSubProcess::killProcess); } - else if (unlikely(rportSet)) { + else if (rportSet) { SMSubServer *subRemote = new SMSubServer(&remoteSettings, QString(), rport); - if (unlikely(!subRemote->isListening())) { + if (!subRemote->isListening()) { QTextStream(stderr) << "Failed to start remote WebSockets server!" << smsub_endl; return 1; } localSettings.canRegister = true; QObject::connect(&subLocal, &SMSubServer::tokenRegistered, subRemote, &SMSubServer::registerToken); QObject::connect(&subProcess, &SMSubProcess::outputWritten, subRemote, &SMSubServer::writeOutput); + QObject::connect(&subProcess, &SMSubProcess::statusUpdated, subRemote, &SMSubServer::statusUpdated); QObject::connect(subRemote, &SMSubServer::inputWritten, &subProcess, &SMSubProcess::writeInput); - QObject::connect(subRemote, &SMSubServer::killRequested, &subProcess, &SMSubProcess::killProcess); + QObject::connect(subRemote, &SMSubServer::startRequested, &subProcess, &SMSubProcess::startProcess); QObject::connect(subRemote, &SMSubServer::stopRequested, &subProcess, &SMSubProcess::stopProcess); + QObject::connect(subRemote, &SMSubServer::killRequested, &subProcess, &SMSubProcess::killProcess); } else { localSettings.canRegister = false; } QObject::connect(&subProcess, &SMSubProcess::outputWritten, &subLocal, &SMSubServer::writeOutput); + QObject::connect(&subProcess, &SMSubProcess::statusUpdated, &subLocal, &SMSubServer::statusUpdated); QObject::connect(&subLocal, &SMSubServer::inputWritten, &subProcess, &SMSubProcess::writeInput); - QObject::connect(&subLocal, &SMSubServer::killRequested, &subProcess, &SMSubProcess::killProcess); + QObject::connect(&subLocal, &SMSubServer::startRequested, &subProcess, &SMSubProcess::startProcess); QObject::connect(&subLocal, &SMSubServer::stopRequested, &subProcess, &SMSubProcess::stopProcess); + QObject::connect(&subLocal, &SMSubServer::killRequested, &subProcess, &SMSubProcess::killProcess); QObject::connect(&a, &QCoreApplication::aboutToQuit, &subProcess, &SMSubProcess::aboutToQuit); - subProcess.start(); + if (autoStart) + subProcess.startProcess(); + + QTextStream(stderr) << QString("SMSub Version %1 initialized!").arg(QCoreApplication::applicationVersion()) << smsub_endl; return a.exec(); } diff --git a/smsub.h b/smsub.h index d05e8bc..9b93a6b 100644 --- a/smsub.h +++ b/smsub.h @@ -1,6 +1,6 @@ /***************************************************************************** * smsub Server Manager Subprocess -* Copyright (C) 2020-2021 Syping +* Copyright (C) 2020-2023 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -20,23 +20,7 @@ #define SMSUB_H #include <QtGlobal> -#ifndef SMSUB_WITHOUT_EXPECT -#ifndef likely -#define likely(x) __builtin_expect((x),1) -#endif -#ifndef unlikely -#define unlikely(x) __builtin_expect((x),0) -#endif -#else -#ifndef likely -#define likely(x) (x) -#endif -#ifndef unlikely -#define unlikely(x) (x) -#endif -#endif - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) +#if QT_VERSION >= 0x050F00 #define smsub_endl Qt::endl #else #define smsub_endl endl