# 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 $