[Twisted-Python] Re: Windows NT Service Problem

David Bolen db3l.net at gmail.com
Fri May 18 16:26:41 EDT 2007


Lucas Taylor <lucas at lucaserve.com> writes:

> Berkstresser, Dan wrote:
>> Hello, I am trying to get my twisted project to run as a Windows
>> Service.  I have followed the instructions here:
>> http://gradstein.info/DeployingTwistedWindows
>> as well as here:
>> http://svn.twistedmatrix.com/cvs/sandbox/moonfallen/?rev=18080#dirlist
>
>
> This isn't really relevant to the references you've mentioned, but there
> is a simple way to create Windows services with a minor amount of code
> and it's not too onerous. You don't have to use py2exe, although it is
> certainly possible to do so once you've got the rest working. I'm not
> sure if this is the "best" way, but it has been successful for me in a
> couple of projects over the past 2 years.

This is extremely close to how I've also been doing it for years.  One
suggestion is to handle reactor shutdown right within SvcStop,
avoiding the need for a polling loop, e.g.:

    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        print '%s Service stopping' % self._svc_display_name_
        reactor.callFromThread(reactor.stop)
        self._stopped.wait(5)
        print '%s Service stopped' % self._svc_display_name_

You need the callFromThread since the service messages arrive on the
service thread.  Note that in my case, self._stopped is a
threading.Event that I set in SvcDoRun when reactor.run returns, for a
positive acknowledgement for the stop, although I choose not to wait
forever for it to happen in case something does go wrong.

It's not directly related to Twisted, but since I inevitably want
services built as executables for distribution, and often have a
handful of persistent config elements, here's a custom boot_service.py
module I use with py2exe.  In particular, it provides a single
implementation (rather than different pywin32 and py2exe) for command
line installation/removal, and adds updating support to py2exe's
version.  It also provides a debug mode where running the service
manually with "--debug" permits it to run from a normal console window
with I/O to the window, which I find handy.  The service when running
will be a subclass with a "debug" variable bound to True.

It takes service name information from the existing service class, but
is application specific in respects to command line parsing, so the
module would be adjusted by service - that could be factored out
easily enough.  Options are stored in the registry during installation
or update, and can be retrieved later by the main service module
(e.g., in SvcDoRun) with win32serviceutil.GetServiceCustomOption

          - - - - - - - - - - - - - - - - - - - - - - - - -

#
# boot_service.py
#
# This is a replacement for py2exe's own boot_service startup code that
# handles re-installation more gracefully, as well as supporting command
# line options during installation that get installed as parameters for
# the service in the registry.
#
# It is pulled in by the custom cmdclass in setup.py
#
#

import sys
import os
import getopt
import servicemanager
import winerror
import win32service
from win32serviceutil import InstallService, RemoveService, \
                             ChangeServiceConfig, SetServiceCustomOption

# Get access to our service class
from dataimportd import DataImportService


svc_display_name = getattr(DataImportService, "_svc_display_name_",
                           DataImportService._svc_name_)

#
# --------------------------------------------------------------------------
#

def usage():

    print svc_display_name, DataImportService.__version__
    print 'Usage: %s [ mode ] [ options ]' % os.path.basename(sys.argv[0])
    print 'mode (if not supplied, assumes Service Manager execution):'
    print '  --install       Install/Update the service'
    print '  --remove        Remove the service'
    print '  --debug         Run directly in current console for debugging'
    print '  --version       Display version and exit'
    print 'options (for installation/update):'
    print '  --config=name   Specify configuration file name (default is'
    print '                    data/dataimport.ini beneath installation)'
    print
    sys.exit(0)

#
# --------------------------------------------------------------------------
#

def update_service_options(options):
    for option, value in options.iteritems():
        SetServiceCustomOption(DataImportService._svc_name_, option, value)


#
# --------------------------------------------------------------------------
#

def main():

    try:
        opts, args = getopt.getopt(sys.argv[1:],'?',
                                   ['install','remove','debug',
                                    'help','version',
                                    'config='])
    except getopt.error, details:
        print details
        usage()

    opt_install = opt_remove = opt_debug = False
    service_options = {}

    for opt, optval in opts:
        if opt in ['-?', '--help']:
            usage()
        elif opt == '--install':
            opt_install = True
        elif opt == '--remove':
            opt_remove = True
        elif opt == '--debug':
            opt_debug = True
        elif opt == '--version':
            print DataImportService.__version__
            sys.exit(0)
        elif opt == '--config':
            service_options[opt[2:]] = optval

    if opt_install:

        try:
            InstallService(None,
                           serviceName = DataImportService._svc_name_,
                           displayName = svc_display_name,
                           exeName = sys.executable,
                           startType = win32service.SERVICE_AUTO_START,
                           serviceDeps = DataImportService._svc_deps_
                           )
            update_service_options(service_options)
            print svc_display_name, 'installed'

        except win32service.error, (hr, fn, msg):
            if hr != winerror.ERROR_SERVICE_EXISTS:
                raise

            ChangeServiceConfig(None,
                                serviceName = DataImportService._svc_name_,
                                displayName = svc_display_name,
                                exeName = sys.executable,
                                startType = win32service.SERVICE_AUTO_START,
                                serviceDeps = DataImportService._svc_deps_
                                )

            update_service_options(service_options)
            print svc_display_name, 'updated'

    elif opt_remove:
        RemoveService(DataImportService._svc_name_)
        print svc_display_name, 'removed'

    else:

        # Event source records come from servicemanager
        evtsrc_dll = os.path.abspath(servicemanager.__file__)

        servicemanager.Initialize(DataImportService._svc_name_, evtsrc_dll)
        servicemanager.PrepareToHostSingle(DataImportService)

        if opt_debug:
            print 'Debugging', svc_display_name
            # The normal registration that is done upon initialization by the
            # win32serviceutil.ServiceFramework class will fail, so we suppress
            # some of the __init__ processing.  This is fragile, but still
            # better than not being able to debug directly
            class DebugService(DataImportService):
                ssh = None   # Since this is dereferenced by the framework
                debug = True
                def __init__(self):
                    pass

            service = DebugService()
            service.initialize()
            service.SvcRun()

        else:
            try:
                servicemanager.StartServiceCtrlDispatcher()
            except:
                print 'Unable to start as service:'
                print sys.exc_info()[1]
                print
                usage()
    

# If we're running frozen, assume we should just start on import
if hasattr(sys, 'frozen'):
    main()


          - - - - - - - - - - - - - - - - - - - - - - - - -

Once you have this module, you can reuse it from the service
implementation module (for consistent command line parsing and debug
execution) instead of win32serviceutil with:

          - - - - - - - - - - - - - - - - - - - - - - - - -

if __name__ == "__main__":

    # Use the same boot code that we get when packaged
    import boot_service
    boot_service.main()

          - - - - - - - - - - - - - - - - - - - - - - - - -

And to wrap up the service with py2exe, subclass the default cmdclass
to return the right boot module (if you already have a subclass - for
example I use one which upx's binaries, just add the get_boot_script
method) and then reference it during the call to setup(), e.g.:

          - - - - - - - - - - - - - - - - - - - - - - - - -

from py2exe.build_exe import py2exe


class Py2exe(py2exe):

    # (... any other customizations ...)

    def get_boot_script(self, boot_type):
        if boot_type != 'service':
            return py2exe.get_boot_script(self, boot_type)
        else:
            return os.path.join(os.path.dirname(__file__),
                                "boot_" + boot_type + ".py")

# ...


setup( # (... normal options ...)
      cmdclass = {'py2exe': Py2exe})

          - - - - - - - - - - - - - - - - - - - - - - - - -


-- David





More information about the Twisted-Python mailing list