[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;
 };
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.