[PySide] Daemon aus graf. Oberfläche starten und stoppen

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Hi,
ich möchte aus einer grafischen Oberfläche ein Programm starten, dass im Hintergrund weiterläuft, auch wenn die Oberfläche geschlossen wurde, also als Daemon.
Der Daemon soll nichts weiter machen, als auf ein DBus-Signal zu warten und dann eine entsprechende Anzeige zu setzen.
Der Daemon soll auch wieder gestoppt werden können, was ebenfalls über die grafische Oberfläche geschehen soll.
Da dies meine erstes Programm in dieser Beziehung ist, habe ich ein wenig in den Weiten des Netzes gesucht und bin im Wesentlichen auf folgende zwei Möglichkeiten gestoßen:
Gibt es Vor/Nachteile bei der Verwendung der einen oder anderen Möglichkeit?

Was ich noch nicht ganz verstanden habe, ist die Kommunikation zwischen GUI und Daemon. Wenn der Daemon läuft und ich das GUI nochmal starte, ist doch die Kommunikation bei beiden Möglichkeiten verschieden, oder? Bei QProgress muss ich quasi meinen Daemon so einrichten, dass er auf Nachrichten reagieren bzw. selbst welche schicken kann, also quasi Client-Server-Prinzip. Bei der zweiten Methode kann ich nur den Daemon starten und stoppen, was mir erstmal prinzipiell reichen würde. Habe ich das so richtig verstanden?

Was mir auch noch nicht ganz klar ist, wie nehme ich bei QProcess wieder "Kontakt" mit dem Daemon auf, wenn ich die grafische Oberfläche wieder starte, um den Daemon zu beenden. Bei der zweiten Methode über das pid-File und bei QProcess?

Gruß EmaNymton
BlackJack

@EmaNymton: Wenn der Deamon wtwas mit DBus zu tun hat, wäre es vielleicht günstig auch die Kommunikation mit ihm darüber abzuwickeln.
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Danke für deinen Hinweis!
Also Starten des Daemons über die GUI und sobald er gestartet ist empfängt er die Signale nur noch über DBus?

Das hört sich prinzipiell gut an, jedoch habe ich ein wenig Bauchschmerzen damit und wollte DBus eigentlich vermeiden, da es bei der verwendeten Plattform (Meego Harmattan) im Moment noch Probleme gibt eigene DBus-Signale abzusetzen bzw. man sein Programm vorher signieren muss, um das zu tun. Signale von DBus abzugreifen ist nicht das Problem, aber senden...

Ich würde es gerne mit PySide/Python-Boardmitteln machen, wenn es geht!
deets

Wenn es moeglich ist, dass die GUI beendet wird (absichtlich oder nicht), aber der Daemon noch lebt, dann kann zur Interprozesskommunikation nur etwas externes herhalten - die Moeglichkeit, Pipes zwischen GUI und Daemon zu spannen ist dir naemlich dann verbaut.

Also musst du deinen Daemon so schreiben, dass er sein PID in ein von der GUI zu lesendes File legt (oft sinnvollerweise gleich ein Lockfile), und dann kannst du zb mit os.kill den Prozess abschiessen.

Wenn du echte RPC machen willst, dann muss der Daemon das anbieten. ZB ueber Sockets, oder - wie BJ schon erwaehnte - halt DBUS.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hmm, dolle Wurst, Du schreibst ein Programm für ein IPC-System und kannst das System selbst nicht für Deine Belange nutzen. Ich geb Dir einfach mal ein Paar Stichwörter, vllt. ist da was Passendes für Dein System dabei:

Qt selbst kennt:
- QSharedMemory
- QLocalServer/QLocalSocket

Mit Python-Mitteln könntest Du den Dämon um XML-RPC erweitern (geht auch mit Qt, mußt Du dann halt selber schreiben), und last but not least gibts natürlich noch die low level Varianten, wo das "Protokoll" Dir überlassen ist - UNIX-Socket und fifo.

Was meego hiervon unterstützt, weiss ich nicht. Falls es ein POSIX-System ist, dürften XML-RPC, fifo und die Sockets funktionieren. Die shared memory-Sache müßtest Du ausprobieren.
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Danke für die Stichwörter, damit kann ich mich erstmal beschäftigen.
Wenn ich dann noch Fragen habe, komme ich nochmal wieder ;)
Hmm, dolle Wurst, Du schreibst ein Programm für ein IPC-System und kannst das System selbst nicht für Deine Belange nutzen.
Jepp, die Entwickler sind darüber auch "not amused", zumal Nokia mit dem N900 gezeigt hat, dass es auch anders geht.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hätte vllt Deinen Ausgangspost genauer lesen sollen :oops:
EmaNymton hat geschrieben:Gibt es Vor/Nachteile bei der Verwendung der einen oder anderen Möglichkeit?
Kommt drauf an, was Du haben willst. QProcess macht mit startDetached() in etwa das, was im 2. Bsp. in der Methode demonize() abläuft (double fork). D.h. Dir würde das ganze Drumherum mit PID-File und start/stop-Logik fehlen und müßtest das selbst in die Hand nehmen. Schneller bist Du mit dem 2. Bsp. am Ziel.
EmaNymton hat geschrieben:Was ich noch nicht ganz verstanden habe, ist die Kommunikation zwischen GUI und Daemon. Wenn der Daemon läuft und ich das GUI nochmal starte, ist doch die Kommunikation bei beiden Möglichkeiten verschieden, oder?
Nein. In beiden Fällen hast Du getrennte Prozesse, die weder einer Session- noch einer Prozessgruppe angehören. Möglichkeiten zur Kommunikation siehe anderen Post von mir.
EmaNymton hat geschrieben: Bei QProgress muss ich quasi meinen Daemon so einrichten, dass er auf Nachrichten reagieren bzw. selbst welche schicken kann, also quasi Client-Server-Prinzip. Bei der zweiten Methode kann ich nur den Daemon starten und stoppen, was mir erstmal prinzipiell reichen würde. Habe ich das so richtig verstanden?
Das Server-Client-Modell wäre ein mögliches Szenario der IPC (über sockets). Da es Dir nur ums "Abschiessen" des Demons geht, brauchst Du keine aufwendige IPC, denn hierfür gibts signals, wovon das 2. Bsp auch Gebrauch macht. Für die QProcess-Variante wäre das auch der einfachere Weg.
EmaNymton hat geschrieben:Wahrscheinlich würde ich dann mit dem python-process-Modul den Daemon starten.
Für das 2. Bsp. brauchst Du nicht das subprocess-Modul bemühen, sondern kannst demon.start/stop aus Deinem GUI-Programm heraus ausführen. Die Klasse Demon kümmert sich um alles andere.
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Danke für die ausführliche Rückmeldung, das läuft wohl auf Variante 2 hinaus.

Mir kam noch ein anderer Gedanke, zu dem ich mal eben eure Meinung hätte: Die Oberfläche ist im Prinzip nur dazu da, Einstellungen vorzunehmen, die der Daemon dann verarbeiten soll, d.h. es werden in einer Datei eh bestimmte Dinge gespeichert. Ist es nicht das einfachste, wenn ich in der cfg-Datei auch die pid vom daemon-Prozess speichere, wenn er läuft. Dann könnte die UI doch auf diese pid zugreifen und den Prozess darüber killen. Ist das so sauber umsetzbar?
deets

Ich halte das nicht fuer sauber. Denn es fuehrt dazu, dass *zwei* Prozesse dieselbe Datei beschreiben wollen - damit ist Aerger vorherbestimmt.

Leg die PID an einen bekannten Ort, so wie andere Daemons das auch machen - uU benutzt du die Datei gleich noch als Lock-Datei, um zu verhindern, dass mehrerer Daemons gleichzeitig gestartet werden.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Wie deets schon sagte, ist das keine gute Idee bzw. widerspricht den UNIX-Dämon-Erwartungen. Die PID-Datei "gehört" dem Dämon-Prozess und sollte auch so behandelt werden (Dämon kümmert sich selbst um Erzeugung und Entfernung dergleichen). Eine PID-Datei enthält klassischerweise nichts weiter als die PID. Wenn Du Bsp. 2 umsetzt ist das "Locking" des Dämon-Prozesses gewährt, für QProcess müsstest Du das halt wie gesagt zusätzlich implementieren.

Wenn Du jetzt doch zusätzliche Informationen neben SIGTERM/SIGKILL mit dem Dämon austauschen willst, läufts halt auf eine IPC hinaus. Der simpelste Fall wäre die Nutzung einer Datei mit Konfigurationseinstellungen. Klassischerweise würde man hier davon ausgehen, das config-Dateien zum Start des Dämons ausgelesen werden bzw. über ein benutzerdefiniertes Signal ein "reload" der Konfigurationsdateien den Dämon zur Übernahme der geänderten Einstellungen bringen. Letzteres erfordert allerdings ein erweitertes Signalhandling im Dämon-Prozess, was alles andere als trivial ist.

Um das Ganze abzukürzen - vergiss Signalhandling - Du kannst eine config-Datei verwenden, wie Du selbst gesagt hast, bitte allerdings verschieden von der PID-Datei. Diese config-Datei wird nun vom GUI-Prozess beschrieben und vom Dämon in regelmäßigem Abstand geprüft bzw. gelesen. Die Prüfung seitens des Dämon-Prozesses könnte z.B. die mtime der Datei sein.
Die Einfachheit dieses Ansatzes hat ein paar Nachteile:
- Dämon muß config-Dateien pollen
- config-Datei muß gelockt werden von beiden Seiten im Falle des Zugriffes
- Responsivität des Dämons ist max. Polling-Frequenz der config-Datei

Sollten diese Nachteile ein no go für Dich sein, mußt Du entweder ein echtes IPC vorhalten oder mit Signalhandling innerhalb des Dämons arbeiten. Beides ist allerdings sehr aufwändig, kA ob in Deinem Falle gerechtfertigt.

Zu Deiner konkreten Frage:
EmaNymton hat geschrieben:Ist es nicht das einfachste, wenn ich in der cfg-Datei auch die pid vom daemon-Prozess speichere, wenn er läuft. Dann könnte die UI doch auf diese pid zugreifen und den Prozess darüber killen. Ist das so sauber umsetzbar?
Das geht, sauber ist es nicht (siehe oben). Mir scheint, Dir ist die Funktionsweise von Bsp. 2 nicht ganz klar. Worüber Du Dir im Klaren sein musst, sind die verschiedenen Prozesse, die in Bsp. 2 gestartet werden. Wichtig hierfür sind die Aufrufe 'os.fork()' und die Beendigung der Prozesse über 'sys.exit()'. 'os.fork()' macht nichts weiter als einen neuen Prozess vom gegenwärtigen zu klonen (fork halt). Übertragen auf Bsp. 2 heisst das, dass Du plötzlich 2 Python-Prozesse mit demselben "Ausführungsstand" im Pythoncode hast, nämlich genau an der Stelle des 'os.fork()'-Aufrufs. Einziger Unterschied auf Prozessebene ist die PID, da der erste Child-Prozess PID 0 erhält während der Parent-Prozess eine PID von 0 verschieden hat. Mit if 'pid > 0: sys.exit(0)' wird der Parent-Prozess beendet, während der Child-Prozess weiterlebt (das Ganze passiert 2mal, um das Terminal und die "Elternschaft" des alten Prozesses loszuwerden, aber egal). Wichtig für Dich ist jetzt, was der Child-Child-Prozess macht, er ist nämlich der eigentliche Dämonprozess:
- STD-Dateihandler umleiten (wichtig für Terminaldetachment)
- PID-Datei schreiben (ja - ich bin ein aktiver Dämon)
- atexit-Regel: lösche PID-Datei, wenn ich beendet werde
- rufe run-Methode auf (der eigentliche Dämoncode)
Wie Du siehst, kümmert sich der Dämon selbst um die PID-Belange.

Der Code von Bsp. 2 ist eine ziemliche gute Übertragung des POSIX/UNIX-Standards für Dämons in Python. Diese Vorgehensweise solltest Du nicht leichtfertig ändern. Desweiteren liefert es mit der zweiten Datei eine Art start-/stop-Skript-Vorlage, welche allerdings für eine echte Systemintegration an die Systemgegebenheiten angepasst werde müsste (runlevel Skript). Genau hier wäre auch Dein Anknüpfungspunkt an den Dämonprozess - Du kannst die start/stop-API nutzen innerhalb Deines GUI-Programmes, entweder direkt (falls in Python) oder aber über das 2. Skript.
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Danke für die ausführliche Erklärung.
Ich habe jetzt mal mit einem Minimalbeispiel, basierend auf der Daemon-Klasse angefangen, um erstmal ein Grundgerüst zum Laufen zu kriegen, aber irgendwie bin ich zu doof :/

Hier mal das Minimalbeispiel

Code: Alles auswählen

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

import os, sys, time
from PySide import QtGui, QtCore
from daemon import Daemon

class MyDaemon(Daemon):
    def run(self):
        while True:
            time.sleep(2)

class Minimalbeispiel(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)  
        self.label = QtGui.QLabel()
        self.button = QtGui.QPushButton("Start/Stop")
        self.layout = QtGui.QVBoxLayout()
        self.layout.addWidget(self.label)
        self.layout.addWidget(self.button)
        self.setLayout(self.layout)

        self.pid = '/tmp/daemon-example.pid'
        self.daemon = MyDaemon(self.pid)
        if os.path.isfile(self.pid):
            self.label.setText(u"daemon läuft")
        else:
            self.label.setText(u"daemon läuft nicht")

        self.button.clicked.connect(self.button_pressed)

    def button_pressed(self):
        if os.path.isfile(self.pid):
            self.daemon.stop()
        else:
            self.daemon.start()

if __name__=="__main__":
    app=QtGui.QApplication(sys.argv)
    widget = Minimalbeispiel()
    widget.show()
    sys.exit(app.exec_())
Folgende Probleme:

Punkt 1 ist, dass sich das gesamte Programm schließt, wenn man den Daemon startet. Ich führe das darauf zurück, dass ja innerhalb der Daemonklasse der Elternprozess gekillt wird, ist der aber nicht unabhängig von meinem Qt-Event-Loop? Das würde aber doch darauf hinauslaufen, dass ich diese Daemon-Klasse als Vorlage gar nicht verwenden kann, wenn ich den Daemon aus einer GUI starten/stoppen will?

Punkt 2 ist, dass zwar der pid-File angelegt wird, der Prozess aber nicht wirklich startet oder zumindest nicht weiter läuft, da ich beim Starten zwar ein pid-File vorfinde, den Prozess aber nicht beenden kann.
Beim Starten kommen zum Teil unterschiedliche Fehlermeldungen:

Code: Alles auswählen

<unknown>: Fatal IO error 11 (Die Ressource ist zur Zeit nicht verfügbar) on X server :0.0.
oder

Code: Alles auswählen

python: ../../src/xcb_io.c:249: process_responses: Zusicherung »(((long) (dpy->last_request_read) - (long) (dpy->request)) <= 0)« nicht erfüllt.
<unknown>: Fatal IO error 11 (Die Ressource ist zur Zeit nicht verfügbar) on X server :0.0.
und beim Versuch den Daemon zu beenden:

Code: Alles auswählen

[Errno 3] Kein passender Prozess gefunden
Was ja auch klar ist, wenn der Prozess gar nicht läuft. ;)

Anscheinend fehlen mir da noch ein paar Grundlagen und ich hab mittlerweile mehrere Bretter vor dem Kopf, da ich jetzt so konkret keine Idee habe, wie ich die zwei Probleme lösen kann. Könnt ihr mir ein wenig auf die Sprünge helfen?
Danke für eure Geduld!
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Um das aus der GUI-Anwendung heraus nutzen zu können, müßtest Du die fork-Logik etwas umbauen, damit der Großelternprozess weiterlebt (das GUI-Fenster) und die Resourcenkonflikte nicht auftreten.

Wahrscheinlich ist es einfacher, wenn Du den Dämon doch über das 2.Skript mittels subprocess steuerst und so die Konflikte umgehst. So ist z.B. sichergestellt, dass der Dämonprozess nicht noch irgendwelche X Resourcen mitschleppt. (subprocess selbst macht ein fork-exec, was so ziemlich den gesamten Prozess "runderneuert" und solche Abhängigkeiten rausschmeisst.)
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hab grad gesehen, dass das Dämon-Bsp. einen Hack für das Löschen der PID-Datei benutzt (da atexit nicht ausgeführt wird bei SIGTERM). Falls Du Aufräumarbeiten beim Beenden des Dämons zu erledigen hast, wirst Du wohl nicht ums Signalhandling kommen.

Hier mal ein simples Bsp, welches auf SIGUSR1 (Neuladen der Konfiguration) und SIGTERM reagiert:

Code: Alles auswählen

from signal import SIGUSR1, SIGTERM, signal
import time, os

class MyDaemon(object):
    def __init__(self):
        print 'PID:', os.getpid()
        self.count = 0
        self.running = True
        self.reread_conf = False
        signal(SIGUSR1, self.sigusr1)
        signal(SIGTERM, self.sigterm)
    def cleanup(self):
        print 'cleanup'
    def sigterm(self, signum, frame):
        self.running = False
    def sigusr1(self, signum, frame):
        self.reread_conf = True
    def run(self):
        while self.running:
            if self.reread_conf:
                print 'Re-Reading configuration...'
                self.reread_conf = False
            self.count += 1
            print self.count
            time.sleep(1)
        self.cleanup()

if __name__ == '__main__':
    daemon = MyDaemon()
    daemon.run()
Der Prozess reagiert auf:

Code: Alles auswählen

#> kill -USR1 <PID> # neuladen
#> kill -TERM <PID> # cleanup und beenden
ABER:
Signale laufen prinzipiell asynchron ein, d.h. sie können an jeder beliebigen Stelle der run-Methode auftreten. Python verarbeitet Signale zwar mit einer gewissen Atomarität, richtig problematisch werden aber I/O-Operationen, welche je nach System unterschiedlich behandelt werden können (siehe signal-Modul Beschreibung und http://www.gnu.org/s/hello/manual/libc/ ... tives.html ). Die Frage, wie dann mit der angefragten I/O-Ressource umzugehen ist (Versuch wiederholen? Abbrechen?), weisst nur Du als Programmierer. Trifft das Signal während der Abarbeitung von Drittcode innerhalb eines fremden Modules auf, wirds dann richtig hässlich, da Dir mitunter nicht mehr klar ist, welchen Zweck die Ressource verfolgt und Du reagieren solltest. Das fängt schon bei 'print "blabla"' an, hier wird "blabla" auf STDOUT geschrieben. Ich kenne die Pythoninterna zu wenig, um sagen zu können, wie Python mit einem Signal an dieser Stelle umgeht (Evtl. müßte 'print' in einen Exceptionblock).
Unterm Strich führt Signalhandling in Deinem Code dazu, dass Du Dir Gedanken über die Atomarität machen musst und diese notfalls künstlich "erzwingst". So kannst Du z.B. komplexe Zustandsänderungen seitens eines Drittmodules kapseln und übernehmen, falls alles ordnungsgemäß durchläuft (innerhalb eines Exceptionblockes).

Das Bsp hier bezieht sich nicht auf die Dämon-Implementation aus Bsp. 2. bzw. müßte etwas umgearbeitet werden, um damit zu funktionieren.
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Vielen vielen Dank für deine Mühe, hab wieder was gelernt und hab es jetzt auch mit subprocess hinbekommen.

Ganz so kompliziert ist es nun nicht, da der Daemon nur eine Konfigurationsdatei ausliest und auf dbus-Signale reagiert.
Das Programm selbst zeigt auf dem N9/N950 Kalendereinträge in einem Event-Screen an.
Der Benutzer kann über die UI im wesentlichen die Konfiguration der Einstellung vornehmen, also maximal Anzahl der angezeigten Termine, Zeitraum, Auswahl der Kalender, ...

Der Daemon liest die Konfigurationsdatei aus und reagiert auf das DBus-Signal wenn die Kalender-DB verändert wurde bzw. setzt sich einen Timer, der zum nächsten Termin ändert.
Dabei wird dann immer die aktuelle Konfiguration gelesen und die Kalendereinträge abgerufen und dargestellt.

Trotzdem vielen Dank für deine Ausführlichkeit und deine Geduld!
Antworten