[network/kdeconnect-kde] plugins/runcommand: Support to transmit the output of commands to the connected device
Albert Vaca Cintora <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit eef2bf526753f1299450c3b1c144c24d37f0022f by Albert Vaca Cintora, on behalf of Johann Specht.
Committed on 30/07/2026 at 07:56.
Pushed by albertvaka into branch 'master'.
Support to transmit the output of commands to the connected device
Releated PR on network/kdeconnect-android [#632](https://invent.kde.org/network/kdeconnect-android/-/merge_requests/632)
When a connected device triggers a command run, the output of the command gets transmitted to the device that was used to send the command.
M +2 -1 plugins/runcommand/kdeconnect_runcommand.json
A +20 -0 plugins/runcommand/runcommandoutput.h [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
M +104 -10 plugins/runcommand/runcommandplugin.cpp
M +6 -0 plugins/runcommand/runcommandplugin.h
https://invent.kde.org/network/kdeconnect-kde/-/commit/eef2bf526753f1299450c3b1c144c24d37f0022f
diff --git a/plugins/runcommand/kdeconnect_runcommand.json b/plugins/runcommand/kdeconnect_runcommand.json
index 44cb34b6c..9b2c1fb10 100644
--- a/plugins/runcommand/kdeconnect_runcommand.json
+++ b/plugins/runcommand/kdeconnect_runcommand.json
@@ -176,7 +176,8 @@
"Name[zh_TW]": "執行遠端指令"
},
"X-KdeConnect-OutgoingPacketType": [
- "kdeconnect.runcommand"
+ "kdeconnect.runcommand",
+ "kdeconnect.runcommand.output"
],
"X-KdeConnect-SupportedPacketType": [
"kdeconnect.runcommand.request"
diff --git a/plugins/runcommand/runcommandoutput.h b/plugins/runcommand/runcommandoutput.h
new file mode 100644
index 000000000..028641f9e
--- /dev/null
+++ b/plugins/runcommand/runcommandoutput.h
@@ -0,0 +1,20 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Johann Specht <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+ */
+
+#pragma once
+
+#define PACKET_TYPE_RUNCOMMAND_OUTPUT QStringLiteral("kdeconnect.runcommand.output")
+
+/**
+ * Packet used to send the output of the commands that get executed through PACKET_TYPE_RUNCOMMAND
+ *
+ * The body should look like so:
+ * "stderr": List<String>, // List of type String that contains the output of the Error Channel
+ * "stdout": List<String>, // List of type String that contains the output of the Standard Channel
+ * "commandFinished": Boolean // Boolean that is true if this is the last output of the command
+ *
+ * Note: Often only stdout or stderr will contain Strings, not both. The other one will just have an empty list.
+ */
\ No newline at end of file
diff --git a/plugins/runcommand/runcommandplugin.cpp b/plugins/runcommand/runcommandplugin.cpp
index 982ab673f..30bc7477f 100644
--- a/plugins/runcommand/runcommandplugin.cpp
+++ b/plugins/runcommand/runcommandplugin.cpp
@@ -15,6 +15,8 @@
#include <QSettings>
#include <KShell>
+#include <QEventLoop>
+#include <QTimer>
#include <core/daemon.h>
#include <core/device.h>
@@ -22,6 +24,7 @@
#include <core/openconfig.h>
#include "plugin_runcommand_debug.h"
+#include "runcommandoutput.h"
#define PACKET_TYPE_RUNCOMMAND QStringLiteral("kdeconnect.runcommand")
@@ -35,6 +38,9 @@
K_PLUGIN_CLASS_WITH_JSON(RunCommandPlugin, "kdeconnect_runcommand.json")
+QMetaObject::Connection stderrConn;
+QMetaObject::Connection stdoutConn;
+
RunCommandPlugin::RunCommandPlugin(QObject *parent, const QVariantList &args)
: KdeConnectPlugin(parent, args)
{
@@ -49,20 +55,108 @@ void RunCommandPlugin::receivePacket(const NetworkPacket &np)
}
if (np.has(QStringLiteral("key"))) {
- QJsonDocument commandsDocument = QJsonDocument::fromJson(config()->getByteArray(QStringLiteral("commands"), "{}"));
- QJsonObject commands = commandsDocument.object();
- QString key = np.get<QString>(QStringLiteral("key"));
- QJsonValue value = commands[key];
- if (value == QJsonValue::Undefined) {
- qCWarning(KDECONNECT_PLUGIN_RUNCOMMAND) << key << "is not a configured command";
- }
- const QJsonObject commandJson = value.toObject();
- qCDebug(KDECONNECT_PLUGIN_RUNCOMMAND) << "Running:" << COMMAND << ARGS << commandJson[QStringLiteral("command")].toString();
- QProcess::startDetached(QStringLiteral(COMMAND), QStringList{QStringLiteral(ARGS), commandJson[QStringLiteral("command")].toString()});
+ startCommand(np);
} else if (np.has(QStringLiteral("setup"))) {
OpenConfig oc;
oc.openConfiguration(device()->id(), QStringLiteral("kdeconnect_runcommand"));
+ } else if (np.has(QStringLiteral("stop"))) {
+ if (currentProcess) {
+ currentProcess->terminate();
+ currentProcess = nullptr;
+ }
+ }
+}
+
+void RunCommandPlugin::startCommand(const NetworkPacket &np)
+{
+ if (currentProcess)
+ {
+ disconnect(stderrConn);
+ disconnect(stdoutConn);
+ currentProcess = nullptr;
+ }
+
+ QJsonDocument commandsDocument = QJsonDocument::fromJson(config()->getByteArray(QStringLiteral("commands"), "{}"));
+ QJsonObject commands = commandsDocument.object();
+ QString key = np.get<QString>(QStringLiteral("key"));
+ QJsonValue value = commands[key];
+ if (value == QJsonValue::Undefined) {
+ qCWarning(KDECONNECT_PLUGIN_RUNCOMMAND) << key << "is not a configured command";
}
+ const QJsonObject commandJson = value.toObject();
+
+ qCDebug(KDECONNECT_PLUGIN_RUNCOMMAND) << "Running:" << COMMAND << ARGS << commandJson[QStringLiteral("command")].toString();
+ auto *process = new QProcess(this);
+ process->setProcessChannelMode(QProcess::SeparateChannels);
+
+ stderrConn = connect(process, &QProcess::readyReadStandardError, this, [this] {onProcessReadyReadState(true);});
+ stderrConn = connect(process, &QProcess::readyReadStandardOutput, this, [this] {onProcessReadyReadState(false);});
+ connect(process, &QProcess::finished, this, &RunCommandPlugin::onProcessFinished);
+
+ connect(process, &QProcess::finished, process, &QObject::deleteLater);
+ currentProcess = process;
+
+ process->start(QStringLiteral(COMMAND), QStringList{QStringLiteral(ARGS), commandJson[QStringLiteral("command")].toString()});
+}
+
+void RunCommandPlugin::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
+{
+ qCDebug(KDECONNECT_PLUGIN_RUNCOMMAND) << "Finished with exit code: " << exitCode << " and status " << exitStatus;
+ NetworkPacket npOutput(PACKET_TYPE_RUNCOMMAND_OUTPUT, {{QStringLiteral("commandFinished"), exitCode != EXIT_FAILURE}});
+ sendPacket(npOutput);
+ currentProcess = nullptr;
+}
+
+
+void RunCommandPlugin::onProcessReadyReadState(const bool isErrorOutput)
+{
+ auto *process = qobject_cast<QProcess *>(sender());
+ if (!process) {
+ return;
+ }
+
+ if (isErrorOutput)
+ {
+ process->setReadChannel(QProcess::StandardError);
+ } else
+ {
+ process->setReadChannel(QProcess::StandardOutput);
+ }
+
+ QTextStream stream(process);
+ QList<QString> output;
+ QList<QString> empty;
+ while (!stream.atEnd()) {
+ output.append(stream.readLine());
+ if (output.size() == 5) {
+ if (isErrorOutput)
+ {
+ sendOutput(empty, output);
+ }
+ else
+ {
+ sendOutput(output, empty);
+ }
+ output.clear();
+ }
+ }
+ if (!output.isEmpty()) {
+ if (isErrorOutput)
+ {
+ sendOutput(empty, output);
+ }
+ else
+ {
+ sendOutput(output, empty);
+ }
+ }
+}
+
+void RunCommandPlugin::sendOutput(const QStringList &standard, const QStringList &error) const
+{
+ qCDebug(KDECONNECT_PLUGIN_RUNCOMMAND) << "Sending stdout: " << standard << " and stderr: " << error;
+ NetworkPacket npOutput(PACKET_TYPE_RUNCOMMAND_OUTPUT, {{QStringLiteral("stdout"), standard}, {QStringLiteral("stderr"), error}});
+ sendPacket(npOutput);
}
void RunCommandPlugin::connected()
diff --git a/plugins/runcommand/runcommandplugin.h b/plugins/runcommand/runcommandplugin.h
index 5f50f348f..9991170f6 100644
--- a/plugins/runcommand/runcommandplugin.h
+++ b/plugins/runcommand/runcommandplugin.h
@@ -12,6 +12,7 @@
#include <QFileSystemWatcher>
#include <QMap>
#include <QPair>
+#include <QProcess>
#include <QString>
#include <core/kdeconnectplugin.h>
@@ -26,5 +27,10 @@ public:
void connected() override;
private:
+ QProcess *currentProcess = nullptr;
+ void startCommand(const NetworkPacket &np);
void sendConfig();
+ void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
+ void onProcessReadyReadState(bool isErrorOutput);
+ void sendOutput(const QStringList &standard, const QStringList &error) const;
};