diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index b3b2c77..c35c1ac 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -1,5 +1,6 @@ qt_add_library(quickshell-io STATIC datastream.cpp + process.cpp ) if (SOCKETS) diff --git a/src/io/module.md b/src/io/module.md index cbcd322..676cff7 100644 --- a/src/io/module.md +++ b/src/io/module.md @@ -3,5 +3,6 @@ description = "Io types" headers = [ "datastream.hpp", "socket.hpp", + "process.hpp", ] ----- diff --git a/src/io/process.cpp b/src/io/process.cpp new file mode 100644 index 0000000..dfa8051 --- /dev/null +++ b/src/io/process.cpp @@ -0,0 +1,159 @@ +#include "process.hpp" +#include // NOLINT +#include + +#include +#include +#include +#include +#include +#include + +#include "datastream.hpp" + +bool Process::isRunning() const { return this->process != nullptr; } + +void Process::setRunning(bool running) { + this->targetRunning = running; + if (running) this->startProcessIfReady(); + else if (this->isRunning()) this->process->terminate(); +} + +QVariant Process::pid() const { + if (this->process == nullptr) return QVariant(); + return QVariant::fromValue(this->process->processId()); +} + +QList Process::command() const { return this->mCommand; } + +void Process::setCommand(QList command) { + if (this->mCommand == command) return; + this->mCommand = std::move(command); + emit this->commandChanged(); + + this->startProcessIfReady(); +} + +DataStreamParser* Process::stdoutParser() const { return this->mStdoutParser; } + +void Process::setStdoutParser(DataStreamParser* parser) { + if (parser == this->mStdoutParser) return; + + if (this->mStdoutParser != nullptr) { + QObject::disconnect(this->mStdoutParser, nullptr, this, nullptr); + } + + this->mStdoutParser = parser; + + if (parser != nullptr) { + QObject::connect(parser, &QObject::destroyed, this, &Process::onStdoutParserDestroyed); + } + + emit this->stdoutParserChanged(); + + if (parser != nullptr && !this->stdoutBuffer.isEmpty()) { + parser->parseBytes(this->stdoutBuffer, this->stdoutBuffer); + } +} + +void Process::onStdoutParserDestroyed() { + this->mStdoutParser = nullptr; + emit this->stdoutParserChanged(); +} + +DataStreamParser* Process::stderrParser() const { return this->mStderrParser; } + +void Process::setStderrParser(DataStreamParser* parser) { + if (parser == this->mStderrParser) return; + + if (this->mStderrParser != nullptr) { + QObject::disconnect(this->mStderrParser, nullptr, this, nullptr); + } + + this->mStderrParser = parser; + + if (parser != nullptr) { + QObject::connect(parser, &QObject::destroyed, this, &Process::onStderrParserDestroyed); + } + + emit this->stderrParserChanged(); + + if (parser != nullptr && !this->stderrBuffer.isEmpty()) { + parser->parseBytes(this->stderrBuffer, this->stderrBuffer); + } +} + +void Process::onStderrParserDestroyed() { + this->mStderrParser = nullptr; + emit this->stderrParserChanged(); +} + +void Process::startProcessIfReady() { + if (this->process != nullptr || !this->targetRunning || this->mCommand.isEmpty()) return; + this->targetRunning = false; + + auto& cmd = this->mCommand.first(); + auto args = this->mCommand.sliced(1); + + this->process = new QProcess(this); + + // clang-format off + QObject::connect(this->process, &QProcess::started, this, &Process::onStarted); + QObject::connect(this->process, &QProcess::finished, this, &Process::onFinished); + QObject::connect(this->process, &QProcess::errorOccurred, this, &Process::onErrorOccurred); + QObject::connect(this->process, &QProcess::readyReadStandardOutput, this, &Process::onStdoutReadyRead); + QObject::connect(this->process, &QProcess::readyReadStandardError, this, &Process::onStderrReadyRead); + // clang-format on + + this->stdoutBuffer.clear(); + this->stderrBuffer.clear(); + + this->process->start(cmd, args); +} + +void Process::onStarted() { + emit this->started(); + emit this->runningChanged(); +} + +void Process::onFinished(qint32 exitCode, QProcess::ExitStatus exitStatus) { + this->process->deleteLater(); + this->process = nullptr; + this->stdoutBuffer.clear(); + this->stderrBuffer.clear(); + + emit this->exited(exitCode, exitStatus); + emit this->runningChanged(); +} + +void Process::onErrorOccurred(QProcess::ProcessError error) { + if (error == QProcess::FailedToStart) { // other cases should be covered by other events + qWarning() << "Process failed to start, likely because the binary could not be found. Command:" + << this->mCommand; + this->process->deleteLater(); + this->process = nullptr; + emit this->runningChanged(); + } +} + +void Process::onStdoutReadyRead() { + if (this->mStdoutParser == nullptr) return; + auto buf = this->process->readAllStandardOutput(); + this->mStdoutParser->parseBytes(buf, this->stdoutBuffer); +} + +void Process::onStderrReadyRead() { + if (this->mStderrParser == nullptr) return; + auto buf = this->process->readAllStandardError(); + this->mStderrParser->parseBytes(buf, this->stderrBuffer); +} + +void Process::signal(qint32 signal) { + if (this->process == nullptr) return; + kill(static_cast(this->process->processId()), signal); // NOLINT +} + +void Process::write(const QString& data) { + if (this->process == nullptr) return; + this->process->write(data.toUtf8()); +} diff --git a/src/io/process.hpp b/src/io/process.hpp new file mode 100644 index 0000000..9801931 --- /dev/null +++ b/src/io/process.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "datastream.hpp" + +///! Child process. +/// #### Example +/// ```qml +/// Process { +/// running: true +/// command: [ "some-command", "arg" ] +/// stdout: SplitParser { +/// onRead: data => console.log(`line read: ${data}`) +/// } +/// } +/// ``` +class Process: public QObject { + Q_OBJECT; + // clang-format off + /// If the process is currently running. Defaults to false. + /// + /// Setting this property to true will start the process if command has at least + /// one element. + /// Setting it to false will send SIGTERM. To immediately kill the process, + /// use [signal](#func.signal) with SIGKILL. The process will be killed when + /// quickshell dies. + /// + /// If you want to run the process in a loop, use the onRunningChanged signal handler + /// to restart the process. + /// ```qml + /// Process { + /// running: true + /// onRunningChanged: if (!running) running = true + /// } + /// ``` + Q_PROPERTY(bool running READ isRunning WRITE setRunning NOTIFY runningChanged); + /// The process ID of the running process or `null` if `running` is false. + Q_PROPERTY(QVariant pid READ pid NOTIFY pidChanged); + /// The command to execute. + /// + /// If the process is already running changing this property will affect the next + /// started process. If the property has been changed after starting a process it will + /// return the new value, not the one for the currently running process. + /// + /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with + /// > the system shell. + Q_PROPERTY(QList command READ command WRITE setCommand NOTIFY commandChanged); + /// The parser for STDOUT. If the parser is null no data will be read. + Q_PROPERTY(DataStreamParser* stdout READ stdoutParser WRITE setStdoutParser NOTIFY stdoutParserChanged); + /// The parser for STDERR. If the parser is null no data will be read. + Q_PROPERTY(DataStreamParser* stderr READ stderrParser WRITE setStderrParser NOTIFY stderrParserChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit Process(QObject* parent = nullptr): QObject(parent) {} + + /// Sends a signal to the process if `running` is true, otherwise does nothing. + Q_INVOKABLE void signal(qint32 signal); + + /// Writes to the process's STDIN. Does nothing if `running` is false. + Q_INVOKABLE void write(const QString& data); + + [[nodiscard]] bool isRunning() const; + void setRunning(bool running); + + [[nodiscard]] QVariant pid() const; + + [[nodiscard]] QList command() const; + void setCommand(QList command); + + [[nodiscard]] DataStreamParser* stdoutParser() const; + void setStdoutParser(DataStreamParser* parser); + + [[nodiscard]] DataStreamParser* stderrParser() const; + void setStderrParser(DataStreamParser* parser); + +signals: + void started(); + void exited(qint32 exitCode, QProcess::ExitStatus exitStatus); + + void runningChanged(); + void pidChanged(); + void commandChanged(); + void stdoutParserChanged(); + void stderrParserChanged(); + +private slots: + void onStarted(); + void onFinished(qint32 exitCode, QProcess::ExitStatus exitStatus); + void onErrorOccurred(QProcess::ProcessError error); + void onStdoutReadyRead(); + void onStderrReadyRead(); + void onStdoutParserDestroyed(); + void onStderrParserDestroyed(); + +private: + void startProcessIfReady(); + + QProcess* process = nullptr; + QList mCommand; + DataStreamParser* mStdoutParser = nullptr; + DataStreamParser* mStderrParser = nullptr; + QByteArray stdoutBuffer; + QByteArray stderrBuffer; + + bool targetRunning = false; +};