# 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. import catalog # 2001-11-08 -- section commented out -- see notes on _svc_deps_ below. # # # Delay loading config, other than for install. In particular, when # # starting the p4dti we may want to load an alternate configuration # # but will not be able to control it through the environment. See # # __init__() method on p4dtiService below. # if __name__ == '__main__' and (len(argv) <= 1 or argv[-1] == 'install'): # import config_loader # import config import os import sys import win32serviceutil import win32service import win32event # Correspondance between two names for defect trackers: those known to # our configuration and those known by NT Service Manager. dt_service_names = {'TeamTrack': 'TeamTrack Broker Service', } # 2. SERVICE FRAMEWORK # # Modelled after examples in [Hammond & Robinson, 2000]. class p4dtiService(win32serviceutil.ServiceFramework): # Service name in the Windows registry. _svc_name_ = 'p4dtiService' # Pretty name in the control panel "Services" applet. _svc_display_name_ = 'P4DTI' # 2001-11-08 -- Section commented out. This feature works but is # typically not wanted. There is no expectation that the p4dti # must run on the same machine as either of the other services. # # # Services on which we "depend": p4dti won't start without # # them; they won't stop without halting p4dti. # _svc_deps_ = ('Perforce', # dt_service_names[config.dt_name] # ) # 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 an alternate logfile and logging level. We cannot # control them in the usual way (through an environment # variable) because we are running in a system environment, so # we pass the value as a command-line argument. evt_log = log_file = log_level = None if len(args) > 1: import getopt try: opts, more = getopt.getopt(args[1:], None, ['p4dti-config=', 'p4dti-evtlog=', 'p4dti-loglevel=']) 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 args = [args[0]] + more except: pass # Now we can load the configuration... import config_loader import config # ... and reconfigure the configuration... if evt_log: config.use_windows_event_log = 1 if log_file: config.log_file = log_file if log_level: config.log_level = int(log_level) # ... 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.write_to_event_log(1011) try: self.run_logging_errors() finally: # "The P4DTI service has halted." self.write_to_event_log(1012) def write_to_event_log(self, message): import servicemanager servicemanager.LogInfoMsg(unicode(catalog.msg(message))) 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 StandardError, err: self.log_fatal_error(err, None) raise except: type, value = sys.exc_info()[:2] self.log_fatal_error(type, value) raise def log_fatal_error(self, type, value): # 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. config = self.config if hasattr(config,'logger'): if value is not None: type = '%s: %s' % (type, value) # "Fatal error in P4DTI service: %s." config.logger.log(catalog.msg(1010, str(type))) 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(p4dtiService, argv = argv, serviceClassString = p4dtiService.svcPath) def main(argv): # Things to do before an install. if len(argv) <= 1: print 'Installing service to start automatically...' 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'), ) 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': print 'Ensuring service is stopped first...' rc = action([argv[0]] + ['stop']) if rc == 0: pass elif rc == 1062: print 'OK (can ignore that error). Proceed with the remove...' 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. # # # B. Document History # # 2000-11-05 NDL Created. # # 2001-11-09 NDL Added hooks etc. for test suite. # # # C. COPYRIGHT AND LICENCE # # 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/branch/2001-11-05/nt-service/code/replicator/service.py#2 $