Problem mit TKinter: subprocess realtime output

  • Ich benötige eine Methode um die Ausgabe über subprocess ausgeführte Konsolen Befehle sofort zu erhalten, nicht erst nachdem der Befehl sich beendet hat

    Habe bereits einige Zeit gegoogled ("subprocess realtime output" usw) und auch ein paar angeblich funktionierende Lösungen gefunden, allerdings klappt das nicht in Kombination mit Tkinter - also auf der Konsole kommt sofort der output aber in Tkinter erst nachdem der Befehl beendet wurde :(

    Mein aktuelles Script sieht so aus: http://codepad.org/7VEtLaCj

    Hauptmerkmal ist denk ich run_command, was ich auch schon wie folgt im Einsatz hatte aber das auch nicht zufriedenstellend funktionierte:
    [code=php]
    def run_command(command):
    process = Popen(shlex.split(command), close_fds=True, stdout=PIPE, bufsize=1)
    while True:
    output = process.stdout.readline()
    if output == '' and process.poll() is not None:
    break
    if output:
    Log.insert(tkinter.END, (output))
    print(output,)
    process.stdout.flush()
    process.stdout.close()
    process.wait()
    rc = process.poll()
    return rc
    [/php]


    Weiß jemand Rat?


    :danke_ATDE:


    PS: threading und multiprocessing möchte ich vermeiden.

  • Falls es jemanden interessiert, die derzeitige Version des Scripts sieht wie folgt aus: http://codepad.org/9i2LrELF

    [code=php]
    #!/usr/bin/python3
    # coding: utf-8
    #
    # Problem mit TKinter: subprocess realtime output
    #
    # 17.05.2016 Copyright (C) by meigrafd (meiraspi@gmail.com) published under the MIT License
    #
    import time
    import shlex
    import tkinter
    from subprocess import Popen, PIPE

    # selectable commands from dropdown menu
    choices = [
    'apt-get update',
    'apt-get upgrade -y',
    'apt-get install -y',
    'ls -la',
    'apt-get',
    ]

    def run_command():
    if running.get() == True:
    print("Error: only one running command at once allowed!")
    status_label.configure(text="Error: another command is active!", fg="red")
    return
    command = option_choice.get()
    if entry.get() is not None:
    command = command + " " + entry.get()
    print("\nExecuting command: %s" % command)
    running.set(True)
    status_label.configure(text="..running..", fg="green")
    returncode = execute_command(command)
    if (returncode is not None) and (returncode != 0):
    print("Returncode: ", end='')
    print(returncode)
    status_label.configure(text="Error!", fg="red")
    else:
    status_label.configure(text="Done: "+command, fg="green")
    running.set(False)

    def stop_command():
    print("Command ends ...")
    status_label.configure(text="Done", fg="green")
    running.set(False)

    def execute_command(command):
    entry.delete(0, tkinter.END)
    Log.delete('1.0', tkinter.END)
    process = Popen(shlex.split(command), stdout=PIPE, stderr=PIPE, bufsize=1)
    for output in iter(process.stdout.readline, b''):
    Log.insert(tkinter.END, (output))
    Log.see(tkinter.END)
    Log.update()
    print(output.strip())
    rc = process.poll()
    process.stdout.close()
    process.wait()
    print("\nDone")
    return rc

    class MyOptionMenu(tkinter.OptionMenu):
    def __init__(self, master, status, *options):
    self.var = tkinter.StringVar(master)
    self.var.set(status)
    tkinter.OptionMenu.__init__(self, master, self.var, *options)
    self.config(font=('calibri',(10)), bg='white', width=12)
    self['menu'].config(font=('calibri',(10)), bg='white')


    # TKinter Main Window
    window = tkinter.Tk()
    window.geometry("500x500+10+10")
    window.title("Command Executor")

    status_label = tkinter.Label(master=window)
    status_label.configure(text=" ", fg="red")
    status_label.grid(row=0, column=0)

    start_button = tkinter.Button(master=window, bg="#229", fg="white", text="Run", command=run_command)
    stop_button = tkinter.Button(master=window, bg="#229", fg="white", text="Stop", command=stop_command)
    exit_button = tkinter.Button(master=window, bg="#229", fg="white", text="X", command=window.destroy)

    proc_hdl = tkinter.IntVar()
    running = tkinter.BooleanVar()
    running.set(False)

    option_choice = tkinter.StringVar(window)
    option_choice.set(choices[0])
    options = tkinter.OptionMenu(window, option_choice, *choices)
    #options = MyOptionMenu(window, option_choice, choices)
    options.grid(row=1, column=0)
    entry = tkinter.Entry(master=window, width=20)
    entry.focus()
    entry.grid(row=1, column=1)

    start_button.grid(row=1, column=2)
    stop_button.grid(row=1, column=3)
    exit_button.grid(row=0, column=4)

    Log = tkinter.Text(master=window, height=31, width=68)
    Log.grid(row=2, column=0, columnspan=5)
    ScrollLog = tkinter.Scrollbar(window)
    ScrollLog.grid(row=2, column=5, sticky=tkinter.NE + tkinter.SE)
    ScrollLog.configure(command=Log.yview)
    Log.configure(yscrollcommand=ScrollLog.set)

    try:
    window.mainloop()
    except (KeyboardInterrupt, SystemExit):
    print("Schliesse Programm.")
    [/php]Es ist aber noch nicht fertig.

    Zu den Features gehören:
    - Mehrfachauswahl an auszuführenden Befehlen über ein Drop-Down Menü (OptionMenu).
    - Eingabefeld für zusätzliche Parameter des auszuführenden Befehls.
    - Echtzeitausgabe der ausführenden Befehle in einem "Log" Bereich.
    - Automatisches runter Scrollen des Logs.
    - Löschen des Logs sofern ein anderer Befehl ausgeführt wird.
    - Erkennung ob Befehl erfolgreich ausgeführt wurde sowie Status Anzeige.

    Was noch fehlt:
    - Returncode Behandlung bzw Zuweisung einer genaueren Fehlermeldung.
    - "Stop" funktionsfähig machen, da man derzeit einen laufenden Befehl nicht beenden kann.
    - Injections im Eingabefeld verhindern.

    Bekannte Probleme:
    - Obwohl Befehl erfolgreich beendet wurde kann man manchmal nicht das OptionsMenü bedienen. Dann hilft nur ein Klick auf "Stop".
    - Fenster-Elemente verschieben sich je nach Länge des Textes.

  • Ich hab den Command Executor noch mal etwas überarbeitet...

    Nun als Klasse aufgebaut wodurch sich die Übergabe der Objekte enorm vereinfacht haben (self).
    Auch gibt es nur noch einen Button dessen Beschriftung (text) sich je nach Zustand ändert: Run oder Stop
    Das Menü hab ich auch leicht verändert sowie den Status Text an die linke Seite (W)est sowie wie den Exit Button nach (E)east fixiert.
    Der erste Befehl in der "choices" Liste wird nun auch direkt vorausgewählt.
    Beim schließen des TK Fensters wird auch ein evtl. noch laufender Befehl beendet - also auch beim schließen nicht über den Exit Button.

    Aber ein Problem habe ich leider noch - bzw ein seltsames Phänomen:

    Wenn ein Befehl ausgeführt wird und beschäftigt ist scheint die GUI einzufrieren, was soweit auch nicht unbedingt verwunderlich ist... Klickt man dann auf den optisch nicht reagierenden "Stop" button wird der Befehl erst dann beendet wenn eine nächste Zeile im Log geschrieben wird - also eine neue Zeile in der Ausgabe erfolgt. Wenn man Beispielsweise beim Befehl "apt-get update" quasi direkt am Anfang auf "Stop" klickt, sieht man am Button selbst keine Reaktion und der Befehl läuft so lange weiter bis die nächste Zeile ausgegeben wird.

    Das wäre soweit zwar nicht ein allzu großes Problem - aber:
    Aus mir bisher unerklärlichen Gründen wird die "startstop_func" Funktion 2x ausgeführt obwohl man nur 1x auf "Stop" geklickt hat? :s
    Das hat zur Folge das im Status nicht "Killed: apt-get update" steht sondern "Error!" weil Returncode dann -1 ist...

    Also eigentlich wird "self.process" gesetzt weil "apt-get update" ausgeführt wird und der Button mit "Stop" beschriftet wird. Klickt man dann auf "Stop" wird die Funktion 2x ausgeführt - woher ich das weiß? Weil "Killing Process" auf der Konsole ausgegeben wird.
    Aber wieso wird die Funktion dann noch mal ausgeführt bzw wie verhinder ich das?

    :helpnew:

    => http://codepad.org/3e4x7b4g

  • Wenn ich Dein Skript über eine sehr langsame Verbindung aufrufe, sehe ich, daß der Status erst auf "killed: ..." und erst nach der Ausgabe des vollständigen Outputs auf "error: ..." gesetzt wird. Meine Vermutung: Nach dem killen des Prozesses arbeitet Dein execute_command noch die Pipe ab und überspringt anschließen den if-Zweig, weil process bereits None ist - damit wird dann Dein vorher gesetzter rc von -1 zurückgegeben.

  • Spontane Idee: Die einzige Möglichkeit, wie Prozess beim Erreichen der if-Anweisung None sein kann, ist doch, wenn Du es gekillt hast. In dem Fall speziellen Wert zurückgeben ("killed" oder so) und den entsprechend behandeln.

Jetzt mitmachen!

Du hast noch kein Benutzerkonto auf unserer Seite? Registriere dich kostenlos und nimm an unserer Community teil!