[network/kdeconnect-kde] plugins/runcommand: Allow seeing the output of multiple commands at once

Albert Vaca Cintora <[email protected]> Tue, 4 Aug 2026 13:11:34 +0000 (UTC)
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit f5ed3ed843032f61c25d7c1b589cff97ffc2edaa by Albert Vaca Cintora.
Committed on 04/08/2026 at 13:11.
Pushed by albertvaka into branch 'master'.

Allow seeing the output of multiple commands at once

Associates each command that is ran with a sequential ID, and tags both the output and the exit code with that ID. If multiple commands run at the same time, it's now possible to distinguish for which one we are sending the output/status.

D  +0    -20   plugins/runcommand/runcommandoutput.h
M  +53   -26   plugins/runcommand/runcommandplugin.cpp
M  +5    -4    plugins/runcommand/runcommandplugin.h

https://invent.kde.org/network/kdeconnect-kde/-/commit/f5ed3ed843032f61c25d7c1b589cff97ffc2edaa

diff --git a/plugins/runcommand/runcommandoutput.h b/plugins/runcommand/runcommandoutput.h
deleted file mode 100644
index 028641f9e..000000000
--- a/plugins/runcommand/runcommandoutput.h
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * 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 1e2f986a1..bd9cf2fe3 100644
--- a/plugins/runcommand/runcommandplugin.cpp
+++ b/plugins/runcommand/runcommandplugin.cpp
@@ -24,9 +24,9 @@
 #include <core/openconfig.h>
 
 #include "plugin_runcommand_debug.h"
-#include "runcommandoutput.h"
 
 #define PACKET_TYPE_RUNCOMMAND QStringLiteral("kdeconnect.runcommand")
+#define PACKET_TYPE_RUNCOMMAND_OUTPUT QStringLiteral("kdeconnect.runcommand.output")
 
 #ifdef Q_OS_WIN
 #define COMMAND "cmd"
@@ -47,6 +47,11 @@ RunCommandPlugin::RunCommandPlugin(QObject *parent, const QVariantList &args)
     connect(config(), &KdeConnectPluginConfig::configChanged, this, &RunCommandPlugin::sendConfig);
 }
 
+RunCommandPlugin::~RunCommandPlugin()
+{
+    currentProcesses.clear();
+}
+
 void RunCommandPlugin::receivePacket(const NetworkPacket &np)
 {
     if (np.get<bool>(QStringLiteral("requestCommandList"), false)) {
@@ -60,20 +65,19 @@ void RunCommandPlugin::receivePacket(const NetworkPacket &np)
         OpenConfig oc;
         oc.openConfiguration(device()->id(), QStringLiteral("kdeconnect_runcommand"));
     } else if (np.has(QStringLiteral("stop"))) {
-        if (currentProcess) {
-            currentProcess->terminate();
-            currentProcess = nullptr;
+        for (QProcess *process : std::as_const(currentProcesses)) {
+            process->terminate(); // will trigger onProcessFinished
         }
     }
 }
 
 void RunCommandPlugin::startCommand(const NetworkPacket &np)
 {
-    if (currentProcess) {
-        disconnect(stderrConn);
-        disconnect(stdoutConn);
-        currentProcess = nullptr;
+    static unsigned id = 0;
+    if (id == std::numeric_limits<unsigned>::max()) {
+        id = 0;
     }
+    unsigned int currentId = id++;
 
     QJsonDocument commandsDocument = QJsonDocument::fromJson(config()->getByteArray(QStringLiteral("commands"), "{}"));
     QJsonObject commands = commandsDocument.object();
@@ -88,29 +92,46 @@ void RunCommandPlugin::startCommand(const NetworkPacket &np)
     auto *process = new QProcess(this);
     process->setProcessChannelMode(QProcess::SeparateChannels);
 
-    stderrConn = connect(process, &QProcess::readyReadStandardError, this, [this] {
-        onProcessReadyReadState(true);
+    stderrConn = connect(process, &QProcess::readyReadStandardError, this, [this, currentId] {
+        onProcessReadyReadState(currentId, true);
     });
-    stderrConn = connect(process, &QProcess::readyReadStandardOutput, this, [this] {
-        onProcessReadyReadState(false);
+    stderrConn = connect(process, &QProcess::readyReadStandardOutput, this, [this, currentId] {
+        onProcessReadyReadState(currentId, false);
     });
-    connect(process, &QProcess::finished, this, &RunCommandPlugin::onProcessFinished);
+    connect(process, &QProcess::finished, this, [this, currentId](int exitCode, QProcess::ExitStatus exitStatus) {
+        onProcessFinished(currentId, exitCode, exitStatus);
+    });
+
+    currentProcesses[currentId] = process;
 
-    connect(process, &QProcess::finished, process, &QObject::deleteLater);
-    currentProcess = process;
+    QString command = commandJson[QStringLiteral("command")].toString();
+    process->start(QStringLiteral(COMMAND), QStringList{QStringLiteral(ARGS), command});
 
-    process->start(QStringLiteral(COMMAND), QStringList{QStringLiteral(ARGS), commandJson[QStringLiteral("command")].toString()});
+    NetworkPacket npOutput(PACKET_TYPE_RUNCOMMAND_OUTPUT,
+                           {
+                               {QStringLiteral("commandStarted"), true},
+                               {QStringLiteral("command"), command},
+                               {QStringLiteral("id"), currentId},
+                           });
+    sendPacket(npOutput);
 }
 
-void RunCommandPlugin::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
+void RunCommandPlugin::onProcessFinished(unsigned int id, 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}});
+    NetworkPacket npOutput(PACKET_TYPE_RUNCOMMAND_OUTPUT,
+                           {
+                               {QStringLiteral("commandFinished"), true},
+                               {QStringLiteral("success"), exitCode != EXIT_FAILURE},
+                               {QStringLiteral("exitCode"), exitCode},
+                               {QStringLiteral("id"), id},
+                           });
     sendPacket(npOutput);
-    currentProcess = nullptr;
+    currentProcesses.remove(id);
+    sender()->deleteLater();
 }
 
-void RunCommandPlugin::onProcessReadyReadState(const bool isErrorOutput)
+void RunCommandPlugin::onProcessReadyReadState(unsigned int id, const bool isErrorOutput)
 {
     auto *process = qobject_cast<QProcess *>(sender());
     if (!process) {
@@ -130,26 +151,32 @@ void RunCommandPlugin::onProcessReadyReadState(const bool isErrorOutput)
         output.append(stream.readLine());
         if (output.size() == 5) {
             if (isErrorOutput) {
-                sendOutput(empty, output);
+                sendOutput(id, empty, output);
             } else {
-                sendOutput(output, empty);
+                sendOutput(id, output, empty);
             }
             output.clear();
         }
     }
     if (!output.isEmpty()) {
         if (isErrorOutput) {
-            sendOutput(empty, output);
+            sendOutput(id, empty, output);
         } else {
-            sendOutput(output, empty);
+            sendOutput(id, output, empty);
         }
     }
 }
 
-void RunCommandPlugin::sendOutput(const QStringList &standard, const QStringList &error) const
+void RunCommandPlugin::sendOutput(unsigned int id, 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}});
+    NetworkPacket npOutput(PACKET_TYPE_RUNCOMMAND_OUTPUT,
+                           {
+                               {QStringLiteral("commandOutput"), true},
+                               {QStringLiteral("stdout"), standard},
+                               {QStringLiteral("stderr"), error},
+                               {QStringLiteral("id"), id},
+                           });
     sendPacket(npOutput);
 }
 
diff --git a/plugins/runcommand/runcommandplugin.h b/plugins/runcommand/runcommandplugin.h
index 9991170f6..373c12939 100644
--- a/plugins/runcommand/runcommandplugin.h
+++ b/plugins/runcommand/runcommandplugin.h
@@ -22,15 +22,16 @@ class RunCommandPlugin : public KdeConnectPlugin
 
 public:
     explicit RunCommandPlugin(QObject *parent, const QVariantList &args);
+    ~RunCommandPlugin();
 
     void receivePacket(const NetworkPacket &np) override;
     void connected() override;
 
 private:
-    QProcess *currentProcess = nullptr;
+    QMap<unsigned int, QProcess *> currentProcesses;
     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;
+    void onProcessFinished(unsigned int id, int exitCode, QProcess::ExitStatus exitStatus);
+    void onProcessReadyReadState(unsigned int id, bool isErrorOutput);
+    void sendOutput(unsigned int id, const QStringList &standard, const QStringList &error) const;
 };