Ein Python-Programm als Windows-Dienst

Code-Stücke können hier veröffentlicht werden.
Gesperrt
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

Samstag 23. Dezember 2006, 00:29

Hi!

Man braucht es ab und zu. Ein Python-Programm das als Windows-Dienst (=Service) gestartet werden kann. Es ist auch gar nicht so schwer, wie ich mir am Anfang gedacht habe.

Installieren als Dienst war kein Problem -- aber das Starten des Dienstes hat mich ein paar gesunde Haare und Nerven gekostet. Es hat mich so um die fünf Stunden aufgehalten, bis ich raus gefunden hatte, dass ein Python-Modul, das als Dienst gestartet werden soll, NICHT in einem Netzlaufwerk liegen darf.
Da ich meine komplette Entwicklungsumgebung in einem Netzlaufwerk habe, trifft mich dieser Umstand doppelt.
Ein Python-Modul, das als Dienst gestartet wird, arbeitet in einer unkonfigurierten Umgebung. Das heißt, dass keine Umgebungsvariablen gesetzt sind und auch keine Netzlaufwerke gemappt sind. Wenn man Umgebungsvariablen braucht, dann muss man sich diese im Python-Modul selbst erstellen.

Aber jetzt zum Wesentlichen. Zuerst braucht man pywin32 von Marc Hammond. Ohne pywin32 wäre die Sache ein Pfusch.
Man könnte z.B. mit Cygwin und dessen Programm ``cygrunsrv`` einen Dienst erstellen und starten, aber man könnte im Programm nicht einfach auf eine STOP-Anweisung reagieren. Das ist also keine gute Option wenn man Aufräumarbeiten durchführen muss. Außerdem wird dazu ein installiertes Cygwin benötigt. Die Installation von Cygwin kann man auch nicht jedem Kunden zumuten...

Man könnte einen Dienst auch mit den Programmen ``instsrv.exe`` und ``srvany.exe`` zum Laufen bringen. Diese Programme sind im "Win2K Resource Kit" mit dabei. Das ist sicher eine Option wenn es sich nur um einen einfachen Dienst handeln soll, der keine Aufräumarbeiten braucht und bei dem es auch egal ist, wenn er mal länger als erwartet läuft. Außerdem erspart man sich dadurch die Installation von Cygwin. (Obwohl kein Windows ohne Cygwin sein sollte. :P )
Siehe auch: http://www.mischiefbox.com/blog/?p=242

Aber wie schon geschrieben -- mit pywin32 hat man mehr Kontrolle und so schwer ist es auch nicht, einen Dienst zu schreiben.

So sieht Mark Hammond den kleinsten Windows-Dienst:

Code: Alles auswählen

# SmallestService.py
#
# A sample demonstrating the smallest possible service written in Python.

import win32serviceutil
import win32service
import win32event

class SmallestPythonService(win32serviceutil.ServiceFramework):
    _svc_name_ = "SmallestPythonService"
    _svc_display_name_ = "The smallest possible Python Service"
    def __init__(self, args):
        win32serviceutil.ServiceFramework.__init__(self, args)
        # Create an event which we will use to wait on.
        # The "service stop" request will set this event.
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)

    def SvcStop(self):
        # Before we do anything, tell the SCM we are starting the stop process.
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        # And set my event.
        win32event.SetEvent(self.hWaitStop)

    def SvcDoRun(self):
        # We do nothing other than wait to be stopped!
        win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)

if __name__=='__main__':
    win32serviceutil.HandleCommandLine(SmallestPythonService)
Gibt man an der Konsole ``SmallestService.py`` ein, dann bekommt man eine Übersicht, was man so alles mit dem Programm anfangen kann:

Code: Alles auswählen

Usage: 'SimpleXmlrpcService.py [options] install|update|remove|start [...]|stop|restart [...]|debug [...]'
Options for 'install' and 'update' commands only:
 --username domain\username : The Username the service is to run under
 --password password : The password for the username
 --startup [manual|auto|disabled] : How the service starts, default = manual
 --interactive : Allow the service to interact with the desktop.
 --perfmonini file: .ini file to use for registering performance monitor data
 --perfmondll file: .dll file to use when querying the service for
   performance data, default = perfmondata.dll
Options for 'start' and 'stop' commands only:
 --wait seconds: Wait for the service to actually start or stop.
                 If you specify --wait with the 'stop' option, the service
                 and all dependent services will be stopped, each waiting
                 the specified period.
Installieren des neuen Diestes:

Code: Alles auswählen

SmallestService.py install
Löschen des neuen Diestes:

Code: Alles auswählen

SmallestService.py remove
Man kann zwar mit ``SmallestService.py start`` den neu erstellten Dienst starten, aber leider bekommt man damit keine Meldung zurück, ob der Dienst korrekt gestartet wurde, oder nicht. Deshalb empfehle ich, den neuen Dienst, zum Testen, über die Windows-Diensteverwaltung zu starten. Es ist auch wichtig, die Windows-Ereignisanzeige nach Meldungen zu durchsuchen.

Und jetzt zu einem Beispiel aus der Praxis --> Ein XMLRPC-Server, der als Dienst gestartet wird und auf Anfragen eines XMLRPC-Clients wartet. Der XMLRPC-Server wird von der Dienst-Klasse aus als eigenständiger Thread gestartet. So wird der XMLRPC-Server nicht blockiert und verrichtet seinen Dienst ziemlich unabhängig.
Wird der Windows-Dienst beendet, dann wird von der Dienst-Klasse aus der XMLRPC-Server beendet.

Und hier der Code (``SimpleXmlrpcService.py``):

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
"""
Windows-Dienst, der einen XMLRPC-Server auf Port 53479 startet und
auf Anfragen wartet.

Filename:      SimpleXmlrpcService.py
Created:       2006-12-23 by Gerold Penz - gerold.penz(at)tirol.utanet.at
Requirements:  Python: http://www.python.org/
               pywin32: http://sourceforge.net/projects/pywin32/
"""

import win32serviceutil
import win32service
import servicemanager
import threading
from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
import xmlrpclib
import time

PORT = 53479


class MyXmlrpcHandler(object):
    """
    In dieser Klasse stehen alle Funktionen, die per XMLRPC-Server zur Verfügung
    gestellt werden.
    """
    
    def get_quit_dummy(self):
        """
        Gibt den String "Quit" zurück.
        """
        
        # Ins Eventlog schreiben
        servicemanager.LogInfoMsg("Der String 'Quit' wird gesendet...")
        
        return "Quit"
    
    
    def get_helloworld(self):
        """
        Gibt den String "Hallo Welt" zurück.
        """
        
        # Ins Eventlog schreiben
        servicemanager.LogInfoMsg("Der String 'Hallo Welt' wird gesendet...")
        
        return "Hallo Welt"


class MyXmlrpcServer(SimpleXMLRPCServer, threading.Thread):
    """
    Diese Klasse stellt den XMLRPC-Server dar. Der Server läuft als 
    eigenständiger Thread.
    """
    
    def __init__(
        self, addr, requestHandler = SimpleXMLRPCRequestHandler, logRequests = True
    ):
        """
        Initialisiert den XMLRPCServer und den Thread.
        """
        
        # Instanz initialisieren
        SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests)
        threading.Thread.__init__(self)
        # Event zum Stoppen des Threads
        self.stopevent = threading.Event()
        # Handler-Klassen an den XMLRPC-Server binden
        self.register_instance(MyXmlrpcHandler())
    
    
    def run(self):
        """
        Hier läuft der Thread und wartet entweder auf das Abbruchsignal oder
        auf einen neuen XMLRPC-Request.
        """
        
        while True:
            # Prüfen ob der Server beendet werden soll
            if self.stopevent.isSet():
                # Ins Eventlog schreiben
                servicemanager.LogInfoMsg("Der XMLRPC-Server wird gestoppt...")
                # Schleife abbrechen
                break
            # Auf eine Anfrage warten
            self.handle_request()
    
    
    def stop(self):
        """
        Stoppt den Thread und somit den XMLRPCServer.
        """
        
        # Stopevent auslösen
        self.stopevent.set()
        
        # Eine Sekunde warten, damit dem Server genug Zeit gelassen wird, einen
        # evt. noch offenen Request zu beenden.
        time.sleep(1)
        
        # Zum Server verbinden und einen Request senden, damit der Server
        # nicht mehr blockiert.
        server = xmlrpclib.ServerProxy("http://localhost:" + str(PORT))
        server.get_quit_dummy()
        
        # Noch eine Sekunde warten, damit auch wirklich genug Zeit zum Beenden
        # gelassen wird.
        time.sleep(1)


class SimpleXmlrpcService(win32serviceutil.ServiceFramework):
    """
    Ich bin der Dienst...
    """
    
    _svc_name_ = "simplexmlrpcservice"
    _svc_display_name_ = "Simple XMLRPC Service"
    _svc_description_ = "Einfacher XMLRPC-Server-Dienst"


    def __init__(self, args):
        """
        Dienst initialisieren und Stopevent erstellen.
        """
        
        win32serviceutil.ServiceFramework.__init__(self, args)
        self.stopevent = threading.Event()
    

    def SvcStop(self):
        """
        Wird von Windows ausgeführt wenn der Dienst beendet werden soll.
        """
        # Ins Eventlog schreiben
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        # Das Stopevent auslösen. Dadurch wird der Dienst nicht mehr 
        # blockiert und kann sich beenden. Info: Der Code blockiert in dieser
        # Klasse bei ``self.stopevent.wait()`` in der Methode ``SvcDoRun``.
        self.stopevent.set()

    
    def SvcDoRun(self):
        """
        Wird von Windows ausgeführt wenn der Dienst gestartet werden soll.
        Startet den XMLRPC-Server und wartet.
        """
        
        # Ins Eventlog schreiben
        self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
        
        # XMLRPCServer starten
        server = MyXmlrpcServer(("localhost", PORT), logRequests = False)
        server.start()
        
        # Ins Eventlog schreiben
        self.ReportServiceStatus(win32service.SERVICE_RUNNING)
        
        # Hier wird gewartet, bis der Dienst beendet wird
        self.stopevent.wait()
        
        # XMLRPCServer beenden
        server.stop()
        
        # Ins Eventlog schreiben
        self.ReportServiceStatus(win32service.SERVICE_STOPPED)


def main():
    win32serviceutil.HandleCommandLine(SimpleXmlrpcService)
    
    
if __name__=='__main__':
    main()
Wenn es Fragen zum Threma Threading gibt, dann sollte man sich http://www.python-forum.de/topic-3869.html *komplett* durchlesen.

Und wie im Allgemeinen ein XMLRPC-Server und der dazugehörige Client erstellt wird, sieht man hier: http://www.python-forum.de/topic-5478.html

lg
Gerold
:-)

Stichworte: Pythonprogramm, Windows-Service, Service, Dienst, Windows-Dienst
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
dolle
User
Beiträge: 3
Registriert: Donnerstag 1. März 2007, 14:34
Wohnort: Südtirol
Kontaktdaten:

Montag 5. März 2007, 14:03

Hallo gerold,

dein Windows-Dienst funktioniert einwandfrei. Ich möchte in dazu verwenden um DB-Abfragen auszuführen und mir nur die Resultate an den Client schicken lassen. So weit so gut.

Da meine Clients im Wlan arbeiten und nicht immer Empfang haben, müsste ich eine realtive niedrige Timeout-Zeit (5 - 10 sec.) am Client setzten. Leider habe ich keinen Weg gefunden wie ich das realisieren kann.

bye
Robert
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

Montag 5. März 2007, 15:52

dolle hat geschrieben:Timeout-Zeit (5 - 10 sec.) am Client setzten.
Hallo Robert!

Das geht so:

Code: Alles auswählen

import socket
socket.setdefaulttimeout(5)
import xmlrpclib
mfg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
chris17
User
Beiträge: 2
Registriert: Mittwoch 5. Dezember 2007, 14:34

Mittwoch 5. Dezember 2007, 16:18

Hallo Gerold,

wir haben hier auch ein größeres Python Programm als Dienst mit SRVANY laufen. Leider beendet windows das Programm bei der Benztzerabmeldung! SRVANY läuft aber weiter und merkt es noch nicht einmal.

Ist Dir das auch untergekommen?

Gruß Christian
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

Mittwoch 5. Dezember 2007, 16:35

Hallo Christian!

Willkommen im Python-Forum!
chris17 hat geschrieben:Ist Dir das auch untergekommen?
Nein? :D

mfg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
chris17
User
Beiträge: 2
Registriert: Mittwoch 5. Dezember 2007, 14:34

Dienstag 11. Dezember 2007, 09:06

Habe das Problem gelöst bzw. im Netz eine Anleitung gefunden warum der Dienst beendet wird beim User-Logoff!

Gerald, das Problem tritt auch bei deinem Beispiel auf und zwar dann wenn in der Hauptschleife (nur der Hauptthread) ein time.sleep(x) verwendet wird! Windows sendet an eine Console Anwendung die als Dienst läuft einen CTRL_LOGOFF_EVENT wenn sich ein User abmeldet. Wird dieser nicht abgefangen so beendet sich die Anwendung (im sleep!).

Hier ein Beispiel:

Vorher (Dienst beendet sich selbst bei User-Logoff):

Code: Alles auswählen

import time

while 1:
    # mache etwas, hier läuft das Programm
    run()
    time.sleep(0.1)
Nachher (Hier wird der Event abgefangen):

Code: Alles auswählen

import time

while 1:
    # mache etwas, hier läuft das Programm
    run()
    try:
        time.sleep(0.1)
    except IOError:
        # Exception CTRL_LOGOFF_EVENT (oder auch andere!)
        print "Exception IOError: Vermute CTRL_LOGOFF_EVENT")
Der Code mag zwar nicht ganz sauber sein, jedoch läuft das Programm so über einen Logoff-Event hinweg. Evtl. kann der spezielle CTRL_LOGOFF_EVENT über die Exception bestimmt und abgefangen werden. Das habe ich allerdings noch nicht probiert.

Vielleicht hat noch jemand einen Verbesserungsvorschlag?

Gruß
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

Montag 31. März 2008, 12:29

Auf Grund technischer Schwierigkeiten, kann man in diesem Thread nicht mehr antworten. Bitte erstelle einen neuen Thread, falls zu diesem Threma Fragen auftauchen.
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
Gesperrt