Output von Befehl in Textbox zeigen

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Hallo,
ich arbeite momentan an einem in Python geschriebenen Minecraft Launcher. Dabei bin ich auch schon relativ weit gekommen.Allerdings habe ich momentan ein Problem: Der Output von Minecraft soll auch in meinem Laucnher in einem neuen Tab erscheinen. Das funktioniert bisher zwar, allerdings friert mein Launcher während des ausführens von Minecraft ein. Er soll aber benutzbar bleiben. Wie stelle ich das am besten an?

Da der Code meines Launchers ziemlich groß ist und über mehrere Dateien geht, habe ich das ganze mal auf das wesentliche gekürzt. Statt Minecraft wird mit dem Klick auf den Button ping ausgeführt:

Code: Alles auswählen

#!/usr/bin/env python3
from PyQt5.QtWidgets import *
import subprocess
import sys

class GameOutputTab(QPlainTextEdit):
    def executeCommand(self,command):
        for text in self.run(command):
            text = str(text)
            self.appendPlainText(text[2:-1])

    def run(self,command):
        process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
        while True:
            line = process.stdout.readline().rstrip()
            if not line:
                break
            yield line

class TabWindow(QTabWidget):
    def setup(self):
        widget = QWidget()
        button = QPushButton("Klick mich!",widget)
        button.clicked.connect(self.runGame)
        self.addTab(widget,"General")

        self.show()

    def runGame(self):
        output = GameOutputTab()
        tabid = self.addTab(output,"Output")
        self.setCurrentIndex(tabid)
        output.executeCommand("ping -c 5 example.com")

app = QApplication(sys.argv)
tabs = TabWindow()
tabs.setup()
sys.exit(app.exec_())    
Sirius3
User
Beiträge: 18251
Registriert: Sonntag 21. Oktober 2012, 17:20

Hallo JakobDev,

eine GUI funktioniert nur, wenn sie die meiste Zeit in ihrer Ereignisschleife auf Ereignisse wartet. Solange aber eine Funktion ausgeführt wird, blockiert die ganze GUI.

Langlaufende Funktionen müssen deshalb in einem Thread abgearbeitet werden, was nicht ganz einfach ist.

Statt ein Bytes-Objekt in seine String-Repräsentation zu verwandeln und die unnützen Anführungszeichen wegzuschneiden, solltest Du die Eingabe mit dem richtigen Encoding decodieren.

Hier ist noch ein Beispiel, wie man Dein Problem mit Qt-Mitteln lösen kann:
https://stackoverflow.com/questions/220 ... yqt-widget
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Danke!
Das von dir verlinkte Beispiel funktioniert! Die process.readAll() aus dem verlinkten Beispiel hat allerdings kein encode.

btw:
process.start nimmt die Argumente als Liste an. Wie bekomme ich die Argumente von einem String in die Liste? Wenn ich jetzt hingehe und über die Leerzeichen loope, habe ich Probleme mit Pfaden, die eventuell ein Leerzeichen enthalten.
Benutzeravatar
__blackjack__
User
Beiträge: 14000
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@JakobDev: Anmerkungen zum Quelltext:

Sternchen-Importe sind Böse™. Aus `QWidgets` importierst man auf diese Weise etwas mehr als 200(!) Namen von denen nur ein ganz kleiner Bruchteil verwendet wird.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Nach Kommas erhöht ein Leerzeichen die Lesbarkeit, genau wie in normalem Text.

Die `setup()`-Methode ist komisch bis falsch. Ein Objekt sollte nach der Initialisierung in aller Regel komplett sein und benutzbar sein. Das in das Erstellen des Objekts und eine separat aufrufbare öffentliche `setup()`-Methode aufzuteilen die man zudem noch zwingend immer nach dem erstellen aufrufen muss, macht keinen Sinn. Nur mehr Arbeit und bietet Angriffsfläche für Fehler.

Wobei der `show()`-Aufruf dann nicht mehr in die `__init__()` gehört, denn damit nimmt man dem Code der das Widget benutzt, die Möglichkeit zu entscheiden was er damit machen will. Kann ja beispielsweise sein, das man sich irgendwann einmal entscheidet das `TabWindow` nicht als Fenster zu verwenden, sondern da noch ein Hauptfenster drumherum basteln will/muss. Da ist der Name `TabWindow` dann auch nicht besonders hilfreich.

`GameOutputTab` ist auch als Name ein bisschen falsch, denn es ist ja nur ein Tab wenn man es als Tab einem `QTabWidget` hinzufügt. Das muss man aber nicht machen.

Die `run()`-Methode ist keine Methode sondern einfach nur eine Funktion. Als solche sollte sie vielleicht nicht in der Klasse stecken.

``shell=True`` sollte man nicht verwenden. Dein Code enthält nichts was das benötigen würde, dafür fängt man sich aber die Probleme ein die `os.system()` und die `os.popen*()`-Funktionen haben. Genau deswegen wurde das `subprocess`-Modul geschrieben, damit man da eben keine unnötige Shell zwischen dem eigenen Programm und dem externen Programm hat. Auch die `QProcess`-Klasse hat so eine Methode von der auch dort in der Dokumentation dringend abgeraten wird.

Die ``while True:``-Schleife ist ungewöhnlich – da würde man eher mit einer ``for``-Schleife über `process.stdout` iterieren. Die `readline()`-Methode braucht man eigentlich nie.

Und dann passiert in dieser Schleife etwas eher komisches von dem ich nicht weiss ob das so sein soll, oder ob Du da einen Fehler gemacht hast: Die Schleife bricht bei der ersten Leerzeile ab! Nicht wenn da nichts mehr vom externen Prozess kommt, sondern wenn da eine *Leerzeile* kommt!

Was soll denn da mit dem dem Prozess passieren? Um den sauber abzuräumen muss man am Ende ja den Rückgabecode abfragen, damit das kein Zombieprozess wird. Aber wenn eine Leerzeile kommt, dann muss der Prozess ja noch gar nicht fertig sein, man müsste also auf das Ende des Prozesses warten. Oder das Ende aktiv herbeiführen? Was soll denn da passieren in dem Fall?

Ausserdem sollte man an der Stelle wo man den Generator konsumiert dafür sorgen, dass er in jedem Fall geschlossen wird, damit Aufräumarbeiten im Generator auch auf jeden Fall ausgeführt werden.

Man kann das was da ausgeführt wird, also…

Code: Alles auswählen

        for line in process.stdout:
            line = line.rstrip()
            if not line:
                break
            yield line
… mit `itertools.takewhile()` auch wesentlich kompakter ausdrücken:

Code: Alles auswählen

        yield from takewhile(bool, (line.rstrip() for line in process.stdout))
Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
from contextlib import closing
from itertools import takewhile
import subprocess
import sys

from PyQt5.QtWidgets import (
    QApplication, QPlainTextEdit, QPushButton, QTabWidget, QWidget
)


def run(command):
    process = subprocess.Popen(
        command, stdout=subprocess.PIPE, universal_newlines=True
    )
    try:
        yield from takewhile(bool, (line.rstrip() for line in process.stdout))
    finally:
        #
        # TODO Nach der Leerzeile oder wenn ein Problem auftritt, wird
        #   der Prozess beendet und der Rückgabecode abgefragt.
        #   Sollte an dieser Stelle etwas anderes passieren…
        # 
        process.terminate()
        process.wait()


class GameOutput(QPlainTextEdit):
    
    def execute_command(self, command):
        with closing(run(command)) as lines:
            for line in lines:
                self.appendPlainText(line)


class Tabs(QTabWidget):
    
    def __init__(self, parent=None):
        QTabWidget.__init__(self, parent)
        widget = QWidget()
        button = QPushButton('Klick mich!', widget)
        button.clicked.connect(self.run_game)
        self.addTab(widget, 'General')

    def run_game(self):
        output = GameOutput()
        self.setCurrentIndex(self.addTab(output, 'Output'))
        output.execute_command(['ping', '-c', '5', 'example.com'])


def main():
    app = QApplication(sys.argv)
    tabs = Tabs()
    tabs.show()
    sys.exit(app.exec_())    


if __name__ == '__main__':
    main()
Der Code macht immer noch das was Deiner macht, hat also auch noch das Problem mit der blockierten GUI. Wenn man das mit `subprocess` lösen will, müsste man mit einem Thread arbeiten und von dem aus dann die einzelnen gelesenen Zeilen mit einem Signal an den GUI-Thread übermitteln.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
__blackjack__
User
Beiträge: 14000
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

JakobDev hat geschrieben: Mittwoch 17. Juli 2019, 18:47 process.start nimmt die Argumente als Liste an. Wie bekomme ich die Argumente von einem String in die Liste? Wenn ich jetzt hingehe und über die Leerzeichen loope, habe ich Probleme mit Pfaden, die eventuell ein Leerzeichen enthalten.
`subprocess.Popen()` nimmt die Argumente auch als Liste, das hätte von Anfang an so sein sollen. Nur wenn man unnötigerweise eine Shell dazwischen schaltet kann man das als *eine* Zeichenkette angeben, die dann aber auch etwas enthalten muss was den Regeln der Shell entspricht, die da ausgeführt wird. Schreib die Argumente einfach gleich als Liste hin.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Ich habe jetzt folgenden Code, der auch funktioniert:

Code: Alles auswählen

#!/usr/bin/env python3
from PyQt5.QtWidgets import QPlainTextEdit, QTabWidget
from PyQt5.QtCore import QProcess
import subprocess
import sys

class GameOutputTab(QPlainTextEdit):
    def dataReady(self):
        cursor = self.textCursor()
        cursor.movePosition(cursor.End)
        cursor.insertText(str(self.process.readAll()).replace("\\n","\n")[2:-1])
        self.ensureCursorVisible()

    def executeCommand(self):
        self.process = QProcess(self)
        self.process.start("ping",["localhost"])
        self.process.readyRead.connect(self.dataReady)
        self.process.start()

class TabWindow(QTabWidget):
    def setup(self):
        widget = QWidget()
        button = QPushButton("Klick mich!",widget)
        button.clicked.connect(self.runGame)
        self.addTab(widget,"General")

        self.show()

    def runGame(self):
        output = GameOutputTab()
        tabid = self.addTab(output,"Output")
        self.setCurrentIndex(tabid)
        output.executeCommand()

app = QApplication(sys.argv)
tabs = TabWindow()
tabs.setup()
sys.exit(app.exec_())    
Mein Problem ist allerdings, dass ich den Befehl zum ausführen von Minecraft nur als String habe, da er von einer API kommt. Wie bekomme ich den jetzt in eine Liste? Ich habe mir folgendes überlegt:

Code: Alles auswählen

def getListl(command):
    words = command.split()
    commandlist = []
    for w in words:
        commandlist.append(w)
    return  commandlist
     
Dieser Code funktioniert allerdings nur solange sich kein Pfad mit Leerzeichen drin im Befehl befindet. Hat jemand eine Idee, wie man das Problem umgehen kann?

Ich werde mir auch mal einige Vorschläge von __blackjack__ bezüglich meines Codes anschauen.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und der Unterschied zwischen words und commandlist ist welcher genau?

Wenn da Leerzeichen im Pfad sind hast du auch ohne Liste schon ein Problem. Denn das funktioniert nur, weil jemand anderes die Liste erzeugt. Und dazu auch an Leerzeichen auftrennt. Aber ist es denn ein Programm + Argumente? Oder nur ein einziger Pfad zur EXE?
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Es geht beispielhaft um folgenden Command:

Code: Alles auswählen

java -Xms512M -Xmx512M -Djava.library.path=/tmp/mc/versions/1.13/natives -cp /tmp/mc/libraries/com/mojang/patchy/1.1/patchy-1.1.jar:/tmp/mc/libraries/oshi-project/oshi-core/1.1/oshi-core-1.1.jar:/tmp/mc/libraries/net/java/dev/jna/jna/4.4.0/jna-4.4.0.jar:/tmp/mc/libraries/net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar:/tmp/mc/libraries/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar:/tmp/mc/libraries/net/sf/jopt-simple/jopt-simple/5.0.3/jopt-simple-5.0.3.jar:/tmp/mc/libraries/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar:/tmp/mc/libraries/com/paulscode/codecwav/20101023/codecwav-20101023.jar:/tmp/mc/libraries/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar:/tmp/mc/libraries/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar:/tmp/mc/libraries/io/netty/netty-all/4.1.25.Final/netty-all-4.1.25.Final.jar:/tmp/mc/libraries/com/google/guava/guava/21.0/guava-21.0.jar:/tmp/mc/libraries/org/apache/commons/commons-lang3/3.5/commons-lang3-3.5.jar:/tmp/mc/libraries/commons-io/commons-io/2.5/commons-io-2.5.jar:/tmp/mc/libraries/commons-codec/commons-codec/1.10/commons-codec-1.10.jar:/tmp/mc/libraries/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar:/tmp/mc/libraries/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar:/tmp/mc/libraries/com/mojang/brigadier/0.1.27/brigadier-0.1.27.jar:/tmp/mc/libraries/com/mojang/datafixerupper/1.0.16/datafixerupper-1.0.16.jar:/tmp/mc/libraries/com/google/code/gson/gson/2.8.0/gson-2.8.0.jar:/tmp/mc/libraries/com/mojang/authlib/1.5.25/authlib-1.5.25.jar:/tmp/mc/libraries/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar:/tmp/mc/libraries/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar:/tmp/mc/libraries/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar:/tmp/mc/libraries/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar:/tmp/mc/libraries/it/unimi/dsi/fastutil/7.1.0/fastutil-7.1.0.jar:/tmp/mc/libraries/org/apache/logging/log4j/log4j-api/2.8.1/log4j-api-2.8.1.jar:/tmp/mc/libraries/org/apache/logging/log4j/log4j-core/2.8.1/log4j-core-2.8.1.jar:/tmp/mc/libraries/org/lwjgl/lwjgl/3.1.6/lwjgl-3.1.6.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-jemalloc/3.1.6/lwjgl-jemalloc-3.1.6.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-openal/3.1.6/lwjgl-openal-3.1.6.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-opengl/3.1.6/lwjgl-opengl-3.1.6.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-glfw/3.1.6/lwjgl-glfw-3.1.6.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-stb/3.1.6/lwjgl-stb-3.1.6.jar:/tmp/mc/libraries/com/mojang/realms/1.13.4/realms-1.13.4.jar:/tmp/mc/libraries/org/lwjgl/lwjgl/3.1.6/lwjgl-3.1.6-natives-linux.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-jemalloc/3.1.6/lwjgl-jemalloc-3.1.6-natives-linux.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-openal/3.1.6/lwjgl-openal-3.1.6-natives-linux.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-opengl/3.1.6/lwjgl-opengl-3.1.6-natives-linux.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-glfw/3.1.6/lwjgl-glfw-3.1.6-natives-linux.jar:/tmp/mc/libraries/org/lwjgl/lwjgl-stb/3.1.6/lwjgl-stb-3.1.6-natives-linux.jar:/tmp/mc/libraries/com/mojang/text2speech/1.10.3/text2speech-1.10.3.jar:/tmp/mc/libraries/com/mojang/text2speech/1.10.3/text2speech-1.10.3-natives-linux.jar:/tmp/mc/versions/1.13/1.13.jar net.minecraft.client.main.Main --username JakobDev --version 1.13 --gameDir /tmp/mc --assetsDir /tmp/mc/assets --assetIndex 1.13 --uuid 9ae4333823cc42dcbd6b99f564a62d89 --accessToken geheim --userType mojang --versionType release
Der Pfad, in diesem Beispiel /tmp/mc sollte auch Leerzeichen enthalten können.
Benutzeravatar
__blackjack__
User
Beiträge: 14000
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@JakobDev: Es wurde ja schon mal gesagt: Bytes mit `str()` in eine Zeichenkette umwandeln und dann das b' und das ' an Anfang und Ende entfernen, und jetzt auch noch '\n' durch Zeilenendezeichen ersetzen ist *falsch*. Dekodieren das ordentlich und mach nicht so einen gruseligen Hack. Wenn das so funktioniert, dann scheint da nichts anderes als ASCII zu kommen, dann kann man das auch ganz einfach mit `decode()` dekodieren. Oder man gibt halt die korrekte Kodierung an, die man da erwartet vom externen Prozess.

Dann kann das bei Multibyte-Kodierungen mit dem `readAll()` allerdings problematisch werden.

Was den Befehl zum Ausführen von Minecraft als Zeichenkette angeht: Wie man das aufsplitten muss ist sehr davon abhängig für welche Shell diese Zeichenkette ist. Das ist einer der Gründe warum man sich mit so etwas eigentlich nicht herumschlagen will und die Argumente schon von Anfang an als Liste haben möchte.

Wenn es für etwas ”unixoides” ist, könnte das `shlex`-Modul nützlich sein. Für Windows gibt es AFAIK nichts in der Standardbibliothek.

Was heisst denn „sollte auch Leerzeichen enthalten können“? Wie sieht das denn *dann* aus?
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

readAll() scheint leider kein decode() zu haben.

Ich ahbe auch jetzt eine Lösung für den Command gefunden: Der Pfad wird mir replace() durch einen Platzhalter ersetzt. Beim umwandeln in eine Liste wird dann nach dem Platzhalter geschaut und dann statt dem Platzhalter der Pfad eingefügt.
Benutzeravatar
__blackjack__
User
Beiträge: 14000
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@JakobDev: Was soll das heissn? `readAll()` ist eine Methode, die liefert Bytes, und `bytes`-Objekte haben eine `decode()`-Methode.

Für `replace()` muss man den Pfad ja vorher kennen. Wenn das von der API wo Du das Kommando her bekommst so kaputt gelöst ist, musst Du halt doch eine Shell dazwischen schalten.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Bei decode kommt nur folgende Fehlermeldung:

Code: Alles auswählen

Traceback (most recent call last):
  File "./MCOutput.py", line 11, in dataReady
    cursor.insertText(self.process.readAll().decode())
AttributeError: 'QByteArray' object has no attribute 'decode'
Da mir der Pfad bekannt ist, konnte ich das problemlos auf die oben genannte Weise lösen.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

QByteArray sollte sich zu bytes konvertieren lassen. bytes(array)
Antworten