Compare commits
	
		
			2 commits
		
	
	
		
			d11ffc1475
			...
			ffa9d02d48
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ffa9d02d48 | |||
| 3f0bd20852 | 
					 4 changed files with 314 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
qt_add_library(quickshell-io STATIC
 | 
			
		||||
	datastream.cpp
 | 
			
		||||
	process.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if (SOCKETS)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,5 +3,6 @@ description = "Io types"
 | 
			
		|||
headers = [
 | 
			
		||||
	"datastream.hpp",
 | 
			
		||||
	"socket.hpp",
 | 
			
		||||
	"process.hpp",
 | 
			
		||||
]
 | 
			
		||||
-----
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										188
									
								
								src/io/process.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/io/process.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,188 @@
 | 
			
		|||
#include "process.hpp"
 | 
			
		||||
#include <csignal> // NOLINT
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include <qlist.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qprocess.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#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<QString> Process::command() const { return this->mCommand; }
 | 
			
		||||
 | 
			
		||||
void Process::setCommand(QList<QString> 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);
 | 
			
		||||
 | 
			
		||||
		if (this->process != nullptr) {
 | 
			
		||||
			this->process->closeReadChannel(QProcess::StandardOutput);
 | 
			
		||||
			this->process->readAllStandardOutput(); // discard
 | 
			
		||||
			this->stdoutBuffer.clear();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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);
 | 
			
		||||
 | 
			
		||||
		if (this->process != nullptr) {
 | 
			
		||||
			this->process->closeReadChannel(QProcess::StandardError);
 | 
			
		||||
			this->process->readAllStandardError(); // discard
 | 
			
		||||
			this->stderrBuffer.clear();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Process::stdinEnabled() const { return this->mStdinEnabled; }
 | 
			
		||||
 | 
			
		||||
void Process::setStdinEnabled(bool enabled) {
 | 
			
		||||
	if (enabled == this->mStdinEnabled) return;
 | 
			
		||||
	this->mStdinEnabled = enabled;
 | 
			
		||||
 | 
			
		||||
	if (!enabled && this->process != nullptr) {
 | 
			
		||||
		this->process->closeWriteChannel();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	emit this->stdinEnabledChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
 | 
			
		||||
	if (this->mStdoutParser == nullptr) this->process->closeReadChannel(QProcess::StandardOutput);
 | 
			
		||||
	if (this->mStderrParser == nullptr) this->process->closeReadChannel(QProcess::StandardError);
 | 
			
		||||
	if (!this->mStdinEnabled) this->process->closeWriteChannel();
 | 
			
		||||
 | 
			
		||||
	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<qint32>(this->process->processId()), signal); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Process::write(const QString& data) {
 | 
			
		||||
	if (this->process == nullptr) return;
 | 
			
		||||
	this->process->write(data.toUtf8());
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								src/io/process.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/io/process.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,124 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qprocess.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <qvariant.h>
 | 
			
		||||
 | 
			
		||||
#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", <your command>]` to execute your command with
 | 
			
		||||
	/// > the system shell.
 | 
			
		||||
	Q_PROPERTY(QList<QString> command READ command WRITE setCommand NOTIFY commandChanged);
 | 
			
		||||
	/// The parser for stdout. If the parser is null the process's stdout channel will be closed
 | 
			
		||||
	/// and no further data will be read, even if a new parser is attached.
 | 
			
		||||
	Q_PROPERTY(DataStreamParser* stdout READ stdoutParser WRITE setStdoutParser NOTIFY stdoutParserChanged);
 | 
			
		||||
	/// The parser for stderr. If the parser is null the process's stdout channel will be closed
 | 
			
		||||
	/// and no further data will be read, even if a new parser is attached.
 | 
			
		||||
	Q_PROPERTY(DataStreamParser* stderr READ stderrParser WRITE setStderrParser NOTIFY stderrParserChanged);
 | 
			
		||||
	/// If stdin is enabled. Defaults to true. If this property is set to false the process's stdin channel
 | 
			
		||||
	/// will be closed and [write](#func.write) will do nothing, even if set back to true.
 | 
			
		||||
	Q_PROPERTY(bool stdinEnabled READ stdinEnabled WRITE setStdinEnabled NOTIFY stdinEnabledChanged);
 | 
			
		||||
	// 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<QString> command() const;
 | 
			
		||||
	void setCommand(QList<QString> command);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] DataStreamParser* stdoutParser() const;
 | 
			
		||||
	void setStdoutParser(DataStreamParser* parser);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] DataStreamParser* stderrParser() const;
 | 
			
		||||
	void setStderrParser(DataStreamParser* parser);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool stdinEnabled() const;
 | 
			
		||||
	void setStdinEnabled(bool enabled);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void started();
 | 
			
		||||
	void exited(qint32 exitCode, QProcess::ExitStatus exitStatus);
 | 
			
		||||
 | 
			
		||||
	void runningChanged();
 | 
			
		||||
	void pidChanged();
 | 
			
		||||
	void commandChanged();
 | 
			
		||||
	void stdoutParserChanged();
 | 
			
		||||
	void stderrParserChanged();
 | 
			
		||||
	void stdinEnabledChanged();
 | 
			
		||||
 | 
			
		||||
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<QString> mCommand;
 | 
			
		||||
	DataStreamParser* mStdoutParser = nullptr;
 | 
			
		||||
	DataStreamParser* mStderrParser = nullptr;
 | 
			
		||||
	QByteArray stdoutBuffer;
 | 
			
		||||
	QByteArray stderrBuffer;
 | 
			
		||||
 | 
			
		||||
	bool targetRunning = false;
 | 
			
		||||
	bool mStdinEnabled = false;
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue