# Perforce Defect Tracking Integration Project # # # SERVICE.PY -- NT SERVICE MANAGER FOR P4DTI # # Nick Levine, Ravenbrook Limited, 2001-11-05 # # # 1. INTRODUCTION # # This Python script manages the P4DTI replicator as a service on # Windows NT. See Chapter 18 ("Windows NT Services") of [Hammond & # Robinson, 2000]. # # We require the service to have the following properties: # # 1. it can be installed, uninstalled, started and stopped; # # 2. it handles startup failures gracefully - note that there is no # usable stdout on which to report errors; # # 3. it has the same functionality as run() in replicator.py - in # particular the same behaviour with respect to poll_period; # # 4. it can be started on system boot and halted on system shutdown, # can be started or halted from control panel or command line, and # these modes of interaction can be mixed; # # 5. it keeps running if the user who launched it logs off the system; # # 6. it does not prevent the system being run as a script; # # 7. it does not add to installation complexity; # # 8. it can be tested via the test suite (in particular it can work # with alternate configuration files by recognizing the environment # variable P4DTI_CONFIG). # # The intended readership of this document is project developers. # # This document is not confidential. # # # 1.1. Architecture # # The code in this module is used in three cases: # # 1. By the P4DTI administrator installing, starting, stopping or # removing the service. (See main(), section 3). # # 2. By the nt_service test case in test_p4dti.py, which installs, # starts, stops and uninstalls the service, just as the administrator # does. # # 3. By the Python service manager application, when the service is # started and stopped. See the p4dti_service class in section 2. import catalog import message import os import sys import win32serviceutil import win32service import win32event # 2. SERVICE FRAMEWORK # # Modelled after examples in [Hammond & Robinson 2000]. class p4dti_service(win32serviceutil.ServiceFramework): # Service name in the Windows registry. _svc_name_ = 'p4dti_service' # Pretty name in the control panel "Services" applet. _svc_display_name_ = 'P4DTI' # 2001-11-09 -- Regrettably, installation by another script (for # example the test suite) results in a relative path in for # PythonClass in the registry, and so the service cannot # start. We therefore generate the path by hand and pass it to the # win32serviceutil code. import service svcPath = (os.path.splitext(os.path.abspath(service.__file__))[0] + '.' + _svc_name_) def __init__(self, args): # Extract any configuration information from the argument # list; then load the configuration. args = self.process_arglist(args) # Initialize ServiceFramework. win32serviceutil.ServiceFramework.__init__(self, args) # Create an event which we will use to wait on. The "service # stop" event request will set this event. self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) def process_arglist(self, args): # We may want to load an alternate configuration file, or # specify alternate values for certain configuration # variables. We cannot control them in the usual way (through # an environment variable) because we are running in a system # environment, so we pass the values as command-line # arguments. evt_log = log_level = admin_address = None if len(args) > 1: import getopt try: opts, more = getopt.getopt(args[1:], None, ['p4dti-config=', 'p4dti-evtlog=', 'p4dti-loglevel=', 'p4dti-adminaddr=', 'p4dti-curdir=', ]) for opt, val in opts: if opt == '--p4dti-config': os.environ['P4DTI_CONFIG'] = val if opt == '--p4dti-evtlog': evt_log = 1 if opt == '--p4dti-loglevel': log_level = val if opt == '--p4dti-adminaddr': admin_address = val if opt == '--p4dti-curdir': os.chdir(val) args = [args[0]] + more except: pass # Now we can load the configuration... from config_loader import config # ... and reconfigure the configuration... if evt_log is not None: # Pass any value, to enable logging to NT Event Log config.use_windows_event_log = 1 if log_level is not None: config.log_level = int(log_level) if admin_address is not None: # Pass empty string, to register administrator_address None # (i.e. no mail will be sent). config.administrator_address = admin_address or None # When running as an NT service, stdout goes nowhere. config.use_stdout_log = 0 # ... and keep a handle on it. self.config = config # Return remaining args return args def SvcStop(self): # Before we do anything, tell the Service Manager that we # are intending to halt. self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) # Then set the event. win32event.SetEvent(self.hWaitStop) # Irrespective of logging configuration, all services ought to # notify the event log on startup and shutdown. def SvcDoRun(self): # "The P4DTI service has started." self.log(catalog.msg(1011)) try: self.run_logging_errors() finally: # "The P4DTI service has halted." self.log(catalog.msg(1012)) # 2001-11-13 NDL -- Renamed method as log() so that test suite # will believe that messages 1011 & 1012 are in use. def log(self, msg): assert isinstance(msg, message.message) # If the problem occurs before the logger has been created, # don't confuse matters further by trying to write to it. # Startup is a common time for errors (faulty configuration, for # example), so we do not expect to be able to go via the # replicator to get a handle on the logger. Instead, write # directly to the Windows Event Log. if hasattr(self.config, 'logger'): self.config.logger.log(msg) else: import servicemanager servicemanager.LogInfoMsg(str(msg)) def run_logging_errors(self): # Attempt to log fatal errors before raising them. It's worth # doing this because full tracebacks in the Windows Event log # can be fairly unreadable. try: self.run() except: type, value = sys.exc_info()[:2] self.log_fatal_error(type, value) raise def log_fatal_error(self, type, value): if value is not None: err = '%s: %s' % (type, value) else: err = str(type) # "Fatal error in P4DTI service: %s." self.log(catalog.msg(1010, err)) def run(self): # Create and initialize an instance of replicator.replicator. from init import r # Event loop analagous to that of run() in replicator.py. r.prepare_to_run() while 1: r.carefully_poll_databases() timeout = r.poll_period * 1000 # in milliseconds rc = win32event.WaitForSingleObject(self.hWaitStop, timeout) # Test return code to see whether our Event was signalled. if rc == win32event.WAIT_OBJECT_0: # We've been asked to halt. Bail out of loop: break # 3. RUN AS SCRIPT # # If this script is run with no arguments, default behaviour is to # install the service (and have it start up automatically on system # boot). We ensure that the Python Service Manager is registered first # (it does not appear to do any harm if this step is repeated). # # Note that when this script is used to start the service, it passes a # message to the NT Service Manager; the script then returns # immediately, and typically without any indication as to whether the # p4dti startup was successful or not. The service runs in a system # environment (as the "default user"), and the current directory is # something like c:\winnt\system32. We extract any values against # certain configuration environment variables at invocation time and # pass them into the service on its command line; the service can then # extract these values from its argument list and act on them # appropriately. # # It is a mistake to attempt to remove a service which is still running, # but it's difficult to do anything about this mistake after the fact, # short of a reboot. We would like to prevent this by preceding 'remove' # actions with a 'stop': we get an error if the service wasn't running # at the time but we can catch return code 1062 # (ERROR_SERVICE_NOT_ACTIVE - see [Microsoft 2001-07-06]) and only worry # about other non-zero codes. There is no immediately obvious clean way # to prevent an error message from the win32serviceutil code (it just # prints to stdout), but this is probably not going to be a problem. def action(argv): handler = win32serviceutil.HandleCommandLine return handler(p4dti_service, argv = argv, serviceClassString = p4dti_service.svcPath) def main(argv): # Things to do before an install. if not os.environ.has_key('P4DTI_CONFIG'): os.environ['P4DTI_CONFIG'] = os.path.join(os.getcwd(), 'config.py') os.environ['P4DTI_CURDIR'] = os.getcwd() if len(argv) <= 1: # "Installing service to start automatically..." print catalog.msg(1013) argv = argv + '--startup auto install'.split() if argv[-1] == 'install': service_exe = win32serviceutil.LocatePythonServiceExe() cmd = '"%s" /register' % service_exe os.system(cmd) # Things to do before a start. if argv[1] == 'start': controls = (('P4DTI_CONFIG', '--p4dti-config'), ('P4DTI_EVTLOG', '--p4dti-evtlog'), ('P4DTI_LOGLEVEL', '--p4dti-loglevel'), ('P4DTI_ADMINADDR', '--p4dti-adminaddr'), ('P4DTI_CURDIR', '--p4dti-curdir'), ) for key, arg in controls: if os.environ.has_key(key): argv = argv + [arg, os.environ[key]] # Things to do before a remove. if argv[1] == 'remove': # "Ensuring service is stopped first..." print catalog.msg(1014) rc = action([argv[0]] + ['stop']) if rc == 0: pass elif rc == 1062: # "OK (can ignore that error). Proceed with the remove..." print catalog.msg(1015) else: return rc # Now proceed with the action. rc = action(argv) sys.stdout.flush() return rc if __name__ == '__main__': main(sys.argv) # A. REFERENCES # # [Hammond & Robinson 2000] "Python Programming on Win32"; Mark # Hammond & Andy Robinson; O'Reilly & Associates, Inc.; 2000. # # [Microsoft 2001-07-06] "System Errors - Numerical Order"; Microsoft; # 2001-07-06; # . # # [NDL 2001-11-15] "very minor problem with nt services" (email # message); Nick Levine; Ravenbrook Ltd.; 2001-11-15; # . # # # B. DOCUMENT HISTORY # # 2000-11-05 NDL Created. # # 2001-11-09 NDL Added hooks etc. for test suite. # # 2001-11-13 GDR Set use_stdout_log to 0 to avoid logging to stdout. # Method and variable names use underscore to separate words. # # 2001-11-15 NDL Add link to email describing a minor problem with the # interaction of older (e.g. build 132) versions of win32all with this # code and the test suite. # # 2001-11-20 NDL Added catalog messages for output from main(). # # 2004-04-14 RHGC Set environment P4DTI_CONFIG if not already set. # # C. COPYRIGHT AND LICENSE # # This file is copyright (c) 2001 Perforce Software, Inc. All rights # reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. # # # $Id: //info.ravenbrook.com/project/p4dti/version/2.1/code/replicator/service.py#1 $