GUI friert ein bei subprocess.call(self.args)

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
atarax
User
Beiträge: 44
Registriert: Samstag 14. Juni 2008, 22:49
Wohnort: Berlin
Kontaktdaten:

Ich bin dabei, ein CD-Ripper Frontend zu schreiben und bin jetzt an dem Punkt, wo der Konsolenbefehl an das Backend geschickt wird. Dies geschieht in einer Schleife über alle ausgewählten Tracks einer CD. Solange der Konsolenbefehl ausgeführt wird, ist das GUI aber unbenutzbar, was nicht sein soll. Änderungen, die zwischendurch an das GUI geschickt werden, werden erst nach dem Ende der Schleife ausgeführt. Wenn das GUI minimiert und dann wieder vergrößert wird, ist der Inhalt verschwunden, und wird erst aktualisiert, wenn die Schleife zu Ende ist. Ich habe versucht, das Problem zu lösen, indem ich die entsprechende Funktion in einen eigenen Thread gepackt habe, leider ohne Erfolg. Es sollen auch nicht alle Threads der Schleife parallel gestartet werden (sonst würde das Programm ja versuchen, mehrere Tracks gleichzeitig zu rippen), sondern erst, wenn der vorige Thread abgeschlossen ist. Es geht nur darum, dass das GUI normal bedienbar ist, während die Unterprozesse laufen.

Ich habe hier versucht, das Skript auf das Problem zu reduzieren:

Code: Alles auswählen

#!/usr/bin/python
#-*- coding: utf-8 -*-

import gtk, subprocess, threading, thread
 
class MainClass(object):
 
    def __init__(self):  # build gui
        """ Initialisiert das Fenster """
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.connect("delete_event", self.event_delete)
        self.window.connect("destroy", self.destroy)
        self.window.set_default_size(350, 100)
        self.window.show()
        
        self.box = gtk.HBox()
        self.box.show()
        self.window.add(self.box)
        
        self.widGoButton = gtk.Button("Start ripping")
        self.widGoButton.show()
        self.widGoButton.connect('clicked', self.actGoButton)
        self.box.add(self.widGoButton)
        
        self.text = gtk.Entry()
        self.text.show()
        self.box.add(self.text)
    
    def actGoButton(self, widget):
        for n in range(1, 3):
            self.text.set_text(str(n))  # wird erst nach ablauf der schleife sichtbar :-(
            self.args = ("cdparanoia", "-Z", "-B", "-d", "/dev/scd0", str(n), "/home/atarax/Desktop/Rip/wav")
            variante = 1
            if (variante == 0):
                # funktioniert, aber gui friert ein, solange die schleife läuft :-(
                self.actRip()
            elif (variante == 1):
                # funktioniert, aber gui friert ein, solange die schleife läuft :-(
                grabThread = threading.Thread(group=None, target=self.actRip(), name=None, args=self.args)
                grabThread.start()
            else:
                # gibt fehlermeldung: Unhandled exception in thread started by Error in sys.excepthook :-(
                thread.start_new_thread(self.actRip, ())

    def actRip(self):
        retcode = subprocess.call(self.args)
        
    def event_delete(self, widget, event, data=None):
        """ reagiert auf 'delete_event' """
        return False
 
    def destroy(self, data=None):
        """ reagiert auf 'destroy' """
        gtk.main_quit()
 
    def main(self):
        """ Nachrichtenschleife """
        gtk.main()
 
if __name__ == "__main__":
    MyApp = MainClass()
    MyApp.main()
http://www.decocode.de/
ms4py
User
Beiträge: 1178
Registriert: Montag 19. Januar 2009, 09:37

Da gibt es z.B. `gtk.main_iteration`.

Kleines Snippet für ein ProgressWindow (ist nur zusammen kopiert, eventuell fehlt etwas).

Code: Alles auswählen

from subprocess import Popen, PIPE

import gtk
import gobject

class ProgressWindow(gtk.Window):
    '''
    Fenster mit Progressbar.
    '''

    def __init__(self, parent):
        gtk.Window.__init__(self)     
        self.set_decorated(False)
        self.set_modal(True)
        self.set_transient_for(parent)
        self.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
        
        self.prog_bar = gtk.ProgressBar()
        self.prog_bar.show()
        self.add(self.prog_bar)
        self.show()        

    def pulse(self):
        '''
        gobject-Timeout
        '''
        self.prog_bar.pulse()
        return True

prog_win = ProgressWindow(self)
event = gobject.timeout_add(200, prog_win.pulse)
proc = Popen(cmd, stdout=PIPE)
while proc.poll() is None:
    gtk.main_iteration()  
errorcode = proc.poll()
gobject.source_remove(event)
prog_win.destroy()
„Lieber von den Richtigen kritisiert als von den Falschen gelobt werden.“
Gerhard Kocher

http://ms4py.org/
Benutzeravatar
atarax
User
Beiträge: 44
Registriert: Samstag 14. Juni 2008, 22:49
Wohnort: Berlin
Kontaktdaten:

OK, ich hab's jetzt so gemacht, und es klappt wie gewünscht:

Code: Alles auswählen

    def actGoButton(self, widget):
        for n in range(1, 3):
            self.text.set_text(str(n))  # wird erst nach ablauf der schleife sichtbar :-(
            self.args = ("cdparanoia", "-Z", "-B", "-d", "/dev/scd0", str(n), "/home/atarax/Desktop/Rip/wav")
            self.actRip()

    def actRip(self):
        proc = subprocess.Popen(self.args, stdout=subprocess.PIPE)
        while proc.poll() is None:
            gtk.main_iteration()
Im Terminal kann ich jetzt den Fortschritt des Prozesses anhand der Ausgabe verfolgen. Aber wie kann ich diese Informationen über mein Programm auslesen, um sie an das GUI zu übergeben? Ich denke, das hat jetzt was mit stdout=subprocess.PIPE zu tun, aber wie verwende ich die?
http://www.decocode.de/
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

``proc.stdout`` ist ein file-like-Objekt, aus dem du lesen kannst. Alternativ schaust du dir ``proc.communicate()`` an.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
BlackJack

`proc.communicate()` blockiert aber wieder. ``cdparanoia`` gibt die Statusinformationen nicht auf `stdout` aus, sondern auf `stderr`. Das ist auch besser geeignet, da das auch bei Pipes in der Regel einen Zeilen- statt eines Blockpuffers verwendet.
Benutzeravatar
atarax
User
Beiträge: 44
Registriert: Samstag 14. Juni 2008, 22:49
Wohnort: Berlin
Kontaktdaten:

So langsam taste ich mich ran. Ich hab's jetzt mal so versucht:

Code: Alles auswählen

    def actRip(self):  # rip and encode process main loop
        process = subprocess.Popen(self.args, stderr=subprocess.PIPE)
        while process.poll() is None:
            err = open(process.stderr)
            #out = err.readlines()
            #print out
            gtk.main_iteration()
Allerdings bekomme ich bei 'open(process.stderr)' folgenden Fehler:

TypeError: coercing to Unicode: need string or buffer, file found

Die auskommentierten Zeilen werden bislang scheinbar gar nicht ausgeführt (auch wenn sie nicht auskommentiert sind :wink: ). Allerdings stellen sie auch nur meinen Voodoo-Versuch dar, die Funktion von 'stderr' zu begreifen...
http://www.decocode.de/
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

du brauchst `open` nicht, btw. würde ich in der Schleife nicht von stderr lesen, weil es (falls keine Daten vorhanden sind), die Schleife blockieren würde.

Code: Alles auswählen

process.stderr.readline()
the more they change the more they stay the same
BlackJack

@atarax: Schau mal in die Dokumentation der `open()`-Funktion. Die möchte als erstes Argument eine Zeichenkette -- eben den Dateinamen. Und dann verrät Dir die Fehlermeldung auch welchen Typ Du stattdessen übergibst: `file`. Das Attribut `stderr` ist bereits eine offene Datei. Da kannst Du die Ausgabe einfach draus lesen. Entweder mit den entsprechenden Methoden oder darüber iterieren. Was Du nicht machen solltest, ist ein `readlines()`, denn das liesst alle Zeilen die da kommen in eine Liste ein -- und blockiert natürlich so lange bis auch wirklich alle Zeilen gelesen wurden, womit die GUI wieder nicht bedient werden kann. Da ``cdparanoia`` in der Regel beim Rippen recht kontinuierlich Ausgaben tätigt, kommt man da vielleicht mit einer Schleife über die Zeilen innerhalb derer `gtk.main_iteration()` aufgerufen wird aus, ohne eine allzulange einfrierende GUI zu bekommen.
Benutzeravatar
atarax
User
Beiträge: 44
Registriert: Samstag 14. Juni 2008, 22:49
Wohnort: Berlin
Kontaktdaten:

Gut, also ich habe es jetzt mal so versucht:

Code: Alles auswählen

    def actRip(self):
        process = subprocess.Popen(self.args, stderr=subprocess.PIPE)
        out = process.stderr.readline()
        while out:
            print "::", out
            out = process.stderr.readline()
            gtk.main_iteration()
Immerhin gibt es jetzt keine Fehlermeldung. Allerdings wird der Fortschritt jetzt nicht einmal mehr im Terminal angezeigt, und die Ausgabe von 'out' ist meistens ein leerer String, ansonsten nicht das, was ich brauche. Mit 'stdout=subprocess.PIPE' wird der Fortschritt im Terminal wieder angezeigt (die Informationen die ich eigentlich auslesen will), aber 'out' zeigt nun gar keine Zeilen mehr an. In beiden Fällen ist das GUI eingefroren, das war schon mal besser...
Ich muss sagen, dass es mir schwer fällt, intuitiv zu erfassen, was diese ganzen Funktionen wirklich bewirken. Wenn ich wüsste, wohin genau 'cdparanoia' seine Ausgabe schickt, wüsste ich vielleicht auch, wie ich diese Ausgabe ins Programm (statt ins Terminal) umleiten kann, aber wie's scheint, bin ich dazu noch zu sehr Noob.
http://www.decocode.de/
BlackJack

@atarax: Warum nimmst Du `readline()` und keine ``for``-Schleife über das Datei-Objekt?

Du benutzt hier `gtk.main_iteration()` auch falsch -- die Funktion *blockiert* wenn keine Ereignisse von der GUI vorliegen. Das übliche Muster sieht anders aus wie in der FAQ von PyGtk nachzulesen ist: How do I update a progress bar and do some work at the same time. Die Generatorlösung dort ist auch ganz nett.

``cdparanoia`` schickt seine Ausgaben an sein `stderr`, das zeigen Deine Erfahrungen mit den Umleitungen doch recht deutlich. Zum besseren Überblick könntest Du auch mal die `repr()` von `out` ausgeben und/oder die Ausgabe explizit "flushen" (sys.stdout.flush()`). Das Programm hat übrigens auch eine Option um die Ausgabe explizit für Wrapperskripte auf `stderr` auszugeben, wahrscheinlich inklusive einem "flush".
Benutzeravatar
atarax
User
Beiträge: 44
Registriert: Samstag 14. Juni 2008, 22:49
Wohnort: Berlin
Kontaktdaten:

Also, ich habe versucht, die Generatorlösung für mein Skript umzusetzen, und es scheint jetzt auch wie gewünscht zu funktionieren:

Code: Alles auswählen

    def actGoButton(self, widget):
        task = self.actRip(11)
        gobject.idle_add(task.next)
            
    def actRip(self, n):
        cmd = ("cdparanoia", "-e", "-Z", "-B", "-v", "-d", "/dev/scd0", str(n), "/home/atarax/Desktop/Rip/wav")
        process = subprocess.Popen(cmd, stderr=subprocess.PIPE)
        for line in process.stderr:
            if "[read]" in line:
                self.text.set_text(line)
            yield True
        yield False
Ich würde mich freuen, wenn jemand diese Lösung kommentieren könnte, da ich mir nicht sicher bin, ob es jetzt so richtig ist.
http://www.decocode.de/
Antworten