Ausgabe von subprozess.run() in Textfenster umleiten

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Benutzeravatar
Dirki
User
Beiträge: 69
Registriert: Donnerstag 23. Juni 2016, 16:11

Moin zusammen!

Ich eröffne zu meinen aktuellen 2 Probleme 2 unterschiedliche Threads, ich hoffe, das ist richtig so.

Wenn ich einen Befehl mit subprozess.run() starte, bekomme ich die Ausgabe irgendwie in ein Textfeld?

Die Ui habe ich mit dem Qt-Creator erstellt, da muss es doch sicher eine Möglichkeit geben ein Fenster auszuwählen, und die Ausgabe "da rein zu bekommen". :roll:

Edit:
https://stackoverflow.com/questions/167 ... -qt-widget

Das klingt vielversprechend, aber ich bekomme das im Kopf nicht verknüpft.
Benutzeravatar
Dirki
User
Beiträge: 69
Registriert: Donnerstag 23. Juni 2016, 16:11

Ich habe die letzten Tage damit verbracht, Nicht zu komplizierten Beispielcode zu finden. Unter anderen auch den folgenden.
Nur funktioniert schon das Beispiel nicht. Ich setzte wie gesagt Python 3.7 und Qt5 ein.

Vielleicht findet einer von euch den Fehler, dann kann ich versuchen das Beispiel auf meinen Code zu übertragen.

Code: Alles auswählen

# ref to https://www.saltycrane.com/blog/2007/12/pyqt-example-how-to-run-command-and/
import os
import sys
import subprocess
from PyQt5 import QtWidgets


def main():
    app = QtWidgets.QApplication(sys.argv)
    w = MyWindow()
    w.show()
    sys.exit(app.exec_())


class MyWindow(QtWidgets.QWidget):
    def __init__(self, *args):
        QtWidgets.QWidget.__init__(self, *args)

        # create objects
        label = QtWidgets.QLabel(self.tr("Enter command and press Return"))
        self.le = QtWidgets.QLineEdit()
        self.te = QtWidgets.QTextEdit()

        # layout
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(label)
        layout.addWidget(self.le)
        layout.addWidget(self.te)
        self.setLayout(layout)

        # create connection
        self.le.returnPressed.connect(self.run_command)

    def run_command(self):
        cmd = str(self.le.text())
        stdouterr = subprocess.run(cmd)[1].read()
        self.te.setText(stdouterr)

if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13112
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dirki: Starte das doch mal aus einem Terminal heraus, dann wird der der Fehler angezeigt, und der ist ziemlich eindeutig:

Code: Alles auswählen

In [20]: subprocess.run('true')[1]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-20-e6614f14330b> in <module>()
----> 1 subprocess.run('true')[1]

TypeError: 'CompletedProcess' object does not support indexing
Wie bist Du denn auf die Idee gekommen das man bei `CompletedProcess`-Objekten per Index auf irgend etwas zugreifen kann?

Als nächstes hast Du soweit ich das sehe Glück das man `run()` *so* auch mit einer Zeichenkette aufrufen kann, denn eigentlich wird da eine Liste erwartet. Dann ist die Frage wovon Du eigentlich lesen möchtest – da gibt's nichts wo man `read()` aufrufen könnte. Die Ausgabe selbst könnte man vom `CompletedProcess` abfragen *wenn* man bei `run()` angegeben hätte, das man das haben möchte. Hast Du Dir die Dokumentation von `run()`/`subprocess()` denn überhaupt mal angeschaut?
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dirki
User
Beiträge: 69
Registriert: Donnerstag 23. Juni 2016, 16:11

Danke für deine Antwort, BlackJack!

Ich habe den Code im Internet bei meinen Recherchen gefunden und "geliehen" um zu verstehen was da passiert. Meine Hoffnung war, das wenn ich das verstanden habe, den Code an mein Programm anzupassen. ;)
Aktuall lese ich tatsächlich die Doku von subprocess. :)
Benutzeravatar
Dirki
User
Beiträge: 69
Registriert: Donnerstag 23. Juni 2016, 16:11

Also mit

Code: Alles auswählen

subprocess.run([ydl_path, "--output", target + "/%(playlist_index)s - %(title)s.%(ext)s", "-u", uname, "-p", pwd, url], capture_output=True, check=True)


Habe ich das so verstanden, das die Ausgabe zwischengespeichert wird, genau so eine Möglichkeit gibt es auch bei Popen:

Code: Alles auswählen

subprocess.Popen([ydl_path, "--output", target + "/%(playlist_index)s - %(title)s.%(ext)s", "-u", uname, "-p", pwd, url], stdout=subprocess.PIPE)
Ich finde nur nicht den dreher, wie ich an die daten wieder ran komme. Ich weiß, du/ihr meint es gut, das ihr uns Anfängern nicht die Lösung präsentiert, sondern uns den Weg zeigt, aber kannst du (ihr) evtl ein wenig konkreter werden? Ich komm einfach nicht dahinter.
__deets__
User
Beiträge: 14542
Registriert: Mittwoch 14. Oktober 2015, 14:29

run liefert dir ein Objekt. Darauf gibt es dann Attribute wie zb stdout und stderr. Du musst dir das also merken & dann damit weiter arbeiten.
Benutzeravatar
__blackjack__
User
Beiträge: 13112
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dirki: Gleich mal vorweg: Was Du willst ist *nicht einfach*! Denn ich nehme mal an Du willst die Ausgabe *während* das externe Programm läuft, und das externe Programm läuft nicht nur ganz kurz, sondern eine ganze Weile bis es fertig ist. Das bedeutet Du kannst das nicht einfach so in einem GUI-Rückruf machen, denn der darf nur sehr kurz irgendetwas tun, sonst blockiert die GUI, was im schlechtesten Fall dazu führen kann das der Benutzer vom System informiert wird, dass Dein Programm nicht mehr zu reagieren scheint, und ob es abgebrochen werden soll. Du brauchst also nebenläufige Programmierung um das asynchron zur GUI-Hauptschleife ausführen zu können. Dazu brauchst Du entweder einen Thread (oder zwei wenn `stderr` und `stdout` getrennt verarbeitet werden sollen) oder `QProcess`. Ich würde einen Thread verwenden, weil `QProcess` sehr unpraktisch wird wenn die Kodierung die der externe Prozess für seine Ausgaben verwendet nicht ein Byte für ein Zeichen verwendet.

Wie man an die Ausgabe(n) von `subprocess.run()` heran kommt steht doch in der Dokumentation: Die Funktion gibt etwas zurück. Das hat den Typ `CompletedProcess`. Und direkt nach der `subprocess.run()` ist Dokumentiert was für Attribute `CompletedProcess`-Objekte haben und was deren Wert jeweils ist. Falls die Ausführung des externen Programms nicht erfolgreich war, dann gibt die Funktion etwas vom Typ `CalledProcessError` zurück – das steht bei der Beschreibung vom `check`-Argument. Und was so ein Objekt für Attribute hat und was die Werte bedeuten, steht auch in der Dokumentation. Und ein Beispiel mit 'ls' ist auch auch in der Dokumentation. Man kann auch selbst mal eine interaktive Python-Shell starten, irgendein Programm ausführen was etwas ausgibt, und sich dann das Ergebnis anschauen was man da bekommt wenn man das mit ``capture_output=True`` aufruft. Dann sollte sehr schnell offensichtlich werden wo die Ausgabe landet und wie man da dann heran kommt. Als allerletzten Notnagel, wenn man mal wirklich eine totale Denkblockade hat, probiert man einfach alle vier Attribute vom `CompletedProcess` mal durch.

Bei `Popen` ist das anders, da hat man nicht die Ausgabe des Programms nachdem es durchgelaufen ist, sondern man hat auf dem `Popen`-Objekt das oder die Dateiobjekte der Standard- und/oder Standardfehlerausgabe des externen Prozesses und ist dann selbst dafür verantwortlich die auszulesen und sinnvoll und sauber auf das Ende des Prozesses zu reagieren → `wait()` aufrufen damit kein Zombie-Prozess zurückbleibt. Und auch das man das auslesen so macht, dass keine Verklemmungen („deadlocks“) entstehen können. Wenn man `stdout` und `stderr` getrennt umleitet, dann muss man die auch nebenläufig verarbeiten, also zum Beispiel einen Thread pro Dateiobjekt. Oder man leitet nur eines von beiden um, oder man leitet `stderr` nach `stdout` um und braucht dann nur noch das verarbeiten – muss beide Kanäle dann aber gleich behandeln.

Bei dem Beispiel ist ``target + '/…`` falsch: Pfade setzt man nicht mit ``+`` zusammen sondern mit `os.path.join()` oder `pathlib`.

Falls das `ydl_path` das Python-Programm `youtube_dl` meint: Da würde ich schauen ob man das überhaupt als externes Programm starten muss, denn das ist ja in Python geschrieben und per ``pip`` installierbar. Eventuell ist es also sinnvoller das Modul zu importieren und programmatisch zu verwenden.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
OzanOs
User
Beiträge: 2
Registriert: Montag 22. Juni 2015, 17:10

Das gleiche Problem habe ich auch und deshalb bin ich mal so frei und klinke mich hier ein. Ich habe ebenfalls eine Gui mit dem Qt creator erstellt und bin gerade dabei den Rest fertig zu stellen.

Kurz zu meinem Programm:
Platform: Windows 7
Python 3.7
PyQT5

Das Programm soll die CMD-Line Version eines Import/Export Programms aufrufen und je nach Auswahl und Konfiguration in der Gui verschiedene Parameter übergeben. Es hat einen QTextEdit Bereich in dem die Ausgabe live angezeigt werden soll.

Ich habe dieses Problem wie @__blackjack__ beschrieben hat mit subprocess.Popen gelöst. Hierbei wird vom subprocess stdout und stderr "gepiped". Die leite ich dann per communicate() an stdout und stderr der Main weiter:

Code: Alles auswählen

def importhelper(self, befehl):
        with subprocess.Popen(befehl ,stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
                output, errors = p.communicate()
                lines = output.decode('iso8859-1')
                print(lines)
Für die weiterleitung von stdout an meinen QTextEdit habe ich eine Klasse erstellt und noch ein paar Funktionen in meiner QMainWindows hinzugefügt:

Code: Alles auswählen

class Stream(QtCore.QObject):
    newText = QtCore.pyqtSignal(str)

    def write(self, text):
        self.newText.emit(str(text))

Code: Alles auswählen

class ozi(QtWidgets.QMainWindow):
    def onUpdateText(self, text):
        cursor = self.process.textCursor()
        cursor.movePosition(QtGui.QTextCursor.End)
        cursor.insertText(text)
        self.process.setTextCursor(cursor)
        self.process.ensureCursorVisible()

    def __del__(self):
        sys.stdout = sys.__stdout__
    
    def __init__(self):
        super(ozi, self).__init__()
            
        #Lade Fenser aus gui Datei
        self.ui = gui.Ui_MainWindow()
        self.ui.setupUi(self)
        
        #stdout stream ausgabe
        sys.stdout = Stream(newText=self.onUpdateText)
        self.process = QtWidgets.QTextEdit(self.ui.tab11)
        self.process.setGeometry(QtCore.QRect(10, 390, 801, 121))
        self.process.setObjectName("textBrowser1")
        self.process.moveCursor(QtGui.QTextCursor.Start)
        self.process.ensureCursorVisible()
        self.process.setLineWrapColumnOrWidth(500)
        self.process.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth)
Soweit so gut. Die Ausgabe wird an die GUI weitergeleitet und halbwegs lesbar (leider wegen decode ohne Umlaute) ausgegeben, ABER wie @__blackjack__ erwähnte friert dadurch die GUI ein und die Ausgabe erscheint erst, nachdem der Subprocess beendet wurde. Ich bin ein absoluter Python und PYQT Neuling und komme leider zum verrecken nicht weiter und verstehe ehrlich gesagt auch nicht, warum das Fenster erst aktualisiert wird, nachdem die Schleife in die Main zurück springt. Vielleicht können wir gemeinsam versuchen eine Lösung zu finden, oder jemand hilft uns etwas auf die Sprünge :P
__deets__
User
Beiträge: 14542
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du benutzt communicate. Das ist so programmiert, das es wartet bis der Prozess beendet ist. Du musst stattdessen (wie schon erwaehnt hier im Thread) run benutzen, und das zurueckgelieferte Objekt hat dann stdout/stderr Attribute. Die kannst du zB mit sowas wie dem QSocketNotifier https://doc.qt.io/qt-5/qsocketnotifier.html#details ueberwachen, wenn sich da was tut.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

@OzanOs: das Umleiten von sys.stdout ist eine ganz schlechte Idee, Zum einen, weil sie nicht funktioniert und zum anderen, weil damit ein globaler Zustand geändert wird, der vielleicht sonstwo ungewollte Auswirkungen hat. sys.stdout wird ja nur für die Python-print-Ausgabe verwendet und an der Stelle, wo Du es benutzt, weißt Du ja, dass Du etwas in ein Textfeld schreiben willst, also kannst Du es dort auch direkt machen.

Ein Kodierung zu raten ist auch schlecht, weil wie Du selbst bemerkst, dann keine Umlaute funktionieren. Die verwendete Codierung ergibt sich aus den Umgebungsvariablen für das aufgerufene Programm.
OzanOs
User
Beiträge: 2
Registriert: Montag 22. Juni 2015, 17:10

Danke erst mal für die Antworten. Ich hatte erst kürzlich wieder Gelegenheit an meinem Programm weiter zu basteln.
Mit dem QSocketNotifier habe ich es nicht zum laufen bekommen. Dann habe ich irgendwo mal gelesen, dass die Windows Kommandozeile sich nicht wie ein Socket verhält und habe die Idee verworfen.

Nichts desto trotz habe ich nach 3 Tagen verzweifeltem rumprobieren und Foren durchforsten auf anderem Wege eine Lösung gefunden und den Code etwas umgeschrieben:

Per threading verhindere ich das Blockieren der Main, dadurch friert das Fenster nicht mehr ein und die Anzeige kann sofort aktualisiert werden.

Code: Alles auswählen

     def start_task(self, test):
        self.thread = threading.Thread(target=self.start_function)
        self.thread.start()
Meine Importhelper Funktion habe ich wie folgt angepasst und die richtige codierung habe ich durch den Befehl "chcp" in der Komandozeile herausgefunden:

Code: Alles auswählen

def importhelper(self, befehl):
        p = subprocess.Popen(befehl, shell=False, stdout=subprocess.PIPE)
        while True:
            out = p.stdout.readline().decode("cp850", "ignore").rstrip('\r\n')
            if out == '' and p.poll() != None:
                break
            if out != '':
                print(out)
Auf dem Weg werden sämtliche Ausgaben direkt Live im QTextEdit angezeigt und man kann während der Subprozess zu Gange ist weiterhin das Programm bedienen.

Jetzt noch meine Frage:
Wenn ich das Programm beende meckert eclipse rum, dass meine Klasse EmittinStream keine flush funktion besitzt. Gibt es noch weitere dinge auf die ich für einen sauberen Exit beachten müsste? Oder habt ihr eventuell noch Ideen zum optimieren?
__deets__
User
Beiträge: 14542
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich sehe keine Klasse EmittinStream. Kann man also wenig zu sagen. Was kritisch ist: modifizierst du den Zustand des Widgets aus dem Hintergrundthread? Dann wird das frueher oder spaeter krachen. Dafuer muss man eigentlich einen QThread und eine QueuedConnection benutzen.
Antworten