[Zope-Checkins] CVS: Zope/lib/python/Zope/Startup/nt - NTService.py:1.4.8.1
Chris McDonough
chrism@zope.com
Sun, 6 Jul 2003 02:35:48 -0400
Update of /cvs-repository/Zope/lib/python/Zope/Startup/nt
In directory cvs.zope.org:/tmp/cvs-serv1525
Modified Files:
Tag: Zope-2_7-branch
NTService.py
Log Message:
New NT service implementation merged from trunk.
=== Zope/lib/python/Zope/Startup/nt/NTService.py 1.4 => 1.4.8.1 ===
--- Zope/lib/python/Zope/Startup/nt/NTService.py:1.4 Tue Mar 18 16:49:06 2003
+++ Zope/lib/python/Zope/Startup/nt/NTService.py Sun Jul 6 02:35:43 2003
@@ -1,241 +1,170 @@
##############################################################################
#
-# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
+# FOR A PARTICULAR PURPOSE.
#
##############################################################################
-"""
-ZServer as a NT service.
-The serice starts up and monitors a ZServer process.
+""" Zope Windows NT/2K service installer/controller for 2.7+ instance homes """
-Features:
-
- * When you start the service it starts ZServer
- * When you stop the serivice it stops ZServer
- * It monitors ZServer and restarts it if it exits abnormally
- * If ZServer is shutdown from the web, the service stops.
- * If ZServer cannot be restarted, the service stops.
-
-Usage:
-
- Installation
-
- The ZServer service should be installed by the Zope Windows
- installer. You can manually install, uninstall the service from
- the commandline.
-
- ZService.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
-
- Commands
-
- install : Installs the service
-
- update : Updates the service, use this when you change
- ZServer.py
-
- remove : Removes the service
-
- start : Starts the service, this can also be done from the
- services control panel
-
- stop : Stops the service, this can also be done from the
- services control panel
-
- restart : Restarts the service
-
- debug : Runs the service in debug mode
-
- You can view the usage options by running ZServer.py without any
- arguments.
-
- Note: you may have to register the Python service program first,
-
- win32\pythonservice.exe /register
-
- Starting Zope
-
- Start Zope by clicking the 'start' button in the services control
- panel. You can set Zope to automatically start at boot time by
- choosing 'Auto' startup by clicking the 'statup' button.
-
- Stopping Zope
-
- Stop Zope by clicking the 'stop' button in the services control
- panel. You can also stop Zope through the web by going to the
- Zope control panel and by clicking 'Shutdown'.
-
- Event logging
-
- Zope events are logged to the NT application event log. Use the
- event viewer to keep track of Zope events.
-
- Registry Settings
-
- You can change how the service starts ZServer by editing a registry
- key.
-
- HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\
- <Service Name>\Parameters\start
-
- The value of this key is the command which the service uses to
- start ZServer. For example:
-
- "C:\Program Files\Zope\bin\python.exe"
- "C:\Program Files\Zope\z2.py" -w 8888
-
-
-TODO:
-
- * Integrate it into the Windows installer.
- * Add ZLOG logging in addition to event log logging.
- * Make it easier to run multiple Zope services with one Zope install
-
-This script does for NT the same sort of thing zdaemon.py does for UNIX.
-Requires Python win32api extensions.
-"""
-__version__ = '$Revision$'[11:-2]
-import sys, os, time, imp, getopt
-import win32api
-def magic_import(modulename, filename):
- # by Mark Hammond
- try:
- # See if it does import first!
- return __import__(modulename)
- except ImportError:
- pass
- # win32 can find the DLL name.
- h = win32api.LoadLibrary(filename)
- found = win32api.GetModuleFileName(h)
- # Python can load the module
- mod = imp.load_module(modulename, None, found, ('.dll', 'rb',
- imp.C_EXTENSION))
- # inject it into the global module list.
- sys.modules[modulename] = mod
- # And finally inject it into the namespace.
- globals()[modulename] = mod
- win32api.FreeLibrary(h)
-
-magic_import('pywintypes','pywintypes21.dll')
-
-import win32serviceutil, win32service, win32event, win32process
-# servicemanager comes as a builtin if we're running via PythonService.exe,
-# but it's not available outside
-try:
- import servicemanager
-except:
- pass
-
-class NTService(win32serviceutil.ServiceFramework):
-
- # Some trickery to determine the service name. The WISE
- # installer will write an svcname.txt to the ZServer dir
- # that we can use to figure out our service name.
-
- restart_min_time=5 # if ZServer restarts before this many
- # seconds then we have a problem, and
- # need to stop the service.
-
- _svc_name_= 'Zope'
- _svc_display_name_ = _svc_name_
+import win32serviceutil
+import win32service
+import win32event
+import win32process
+import pywintypes
+import time
+
+# the max seconds we're allowed to spend backing off
+BACKOFF_MAX = 300
+# if the process runs successfully for more than BACKOFF_CLEAR_TIME
+# seconds, we reset the backoff stats to their initial values
+BACKOFF_CLEAR_TIME = 30
+# the initial backoff interval (the amount of time we wait to restart
+# a dead process)
+BACKOFF_INITIAL_INTERVAL = 5
+
+class ZopeService(win32serviceutil.ServiceFramework):
+ """ A class representing a Windows NT service that can manage an
+ Zope 2.7+ instance-home-based Zope process """
+
+ # The PythonService model requires that an actual on-disk class declaration
+ # represent a single service. Thus, the below definition of start_cmd,
+ # must be overridden in a subclass in a file within the instance home for
+ # each Zope instance. The below-defined start_cmd is just an example.
+
+ _svc_name_ = r'Zope-Instance'
+ _svc_display_name_ = r'Zope instance at C:\Zope-Instance'
+
+ start_cmd = (
+ r'"C:\Program Files\Zope-2.7.0-a1\bin\pythonw.exe" '
+ '"C:\Program Files\Zope-2.7.0-a1\lib\python\Zope\Startup\run.py" '
+ '-C "C:\Zope-Instance\etc\zope.conf"'
+ )
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 SvcDoRun(self):
- self.start_zserver()
- while 1:
- rc=win32event.WaitForMultipleObjects(
- (self.hWaitStop, self.hZServer), 0, win32event.INFINITE)
- if rc - win32event.WAIT_OBJECT_0 == 0:
- break
- else:
- self.restart_zserver()
- self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING, 5000)
-
def SvcStop(self):
- servicemanager.LogInfoMsg('Stopping Zope.')
+ # Before we do anything, tell the SCM we are starting the stop process.
+ self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+ # stop the process if necessary
try:
- self.stop_zserver()
- except:
+ win32process.TerminateProcess(self.hZope, 0)
+ except pywintypes.error:
+ # the process may already have been terminated
pass
- self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+ # And set my event.
win32event.SetEvent(self.hWaitStop)
- def restart_zserver(self):
- if time.time() - self.last_start_time < self.restart_min_time:
- servicemanager.LogErrorMsg('Zope died and could not be restarted.')
- self.SvcStop()
- code=win32process.GetExitCodeProcess(self.hZServer)
- if code == 0:
- # Exited with a normal status code,
- # assume that shutdown is intentional.
- self.SvcStop()
- else:
- servicemanager.LogWarningMsg('Restarting Zope.')
- self.start_zserver()
-
- def start_zserver(self):
- sc=self.get_start_command()
- result=win32process.CreateProcess(None, sc,
- None, None, 0, 0, None, None, win32process.STARTUPINFO())
- self.hZServer=result[0]
- self.last_start_time=time.time()
- servicemanager.LogInfoMsg('Starting Zope.')
+ def createProcess(self, cmd):
+ return win32process.CreateProcess(
+ None, cmd, None, None, 0, 0, None, None,
+ win32process.STARTUPINFO())
- def stop_zserver(self):
- try:
- win32process.TerminateProcess(self.hZServer,0)
- except:
- pass
- result=win32process.CreateProcess(None, self.get_stop_command(),
- None, None, 0, 0, None, None, win32process.STARTUPINFO())
- return result
-
- def get_start_command(self):
- return win32serviceutil.GetServiceCustomOption(self,'start', None)
-
- def get_stop_command(self):
- cmd = win32serviceutil.GetServiceCustomOption(self,'stop', None)
-
-def set_start_command(value):
- "sets the ZServer start command if the start command is not already set"
- current=win32serviceutil.GetServiceCustomOption(NTService,
- 'start', None)
- if current is None:
- win32serviceutil.SetServiceCustomOption(NTService,'start',value)
-
-def set_stop_command(value):
- "sets the ZServer start command if the start command is not already set"
- current=win32serviceutil.GetServiceCustomOption(NTService,
- 'stop', None)
- if current is None:
- win32serviceutil.SetServiceCustomOption(NTService,'stop',value)
+ def SvcDoRun(self):
+ # daemon behavior: we want to to restart the process if it
+ # dies, but if it dies too many times, we need to give up.
+
+ # we use a simple backoff algorithm to determine whether
+ # we should try to restart a dead process: for each
+ # time the process dies unexpectedly, we wait some number of
+ # seconds to restart it, as determined by the backoff interval,
+ # which doubles each time the process dies. if we exceed
+ # BACKOFF_MAX seconds in cumulative backoff time, we give up.
+ # at any time if we successfully run the process for more thab
+ # BACKOFF_CLEAR_TIME seconds, the backoff stats are reset.
+
+ # the initial number of seconds between process start attempts
+ backoff_interval = BACKOFF_INITIAL_INTERVAL
+ # the cumulative backoff seconds counter
+ backoff_cumulative = 0
+
+ import servicemanager
+
+ # log a service started message
+ servicemanager.LogMsg(
+ servicemanager.EVENTLOG_INFORMATION_TYPE,
+ servicemanager.PYS_SERVICE_STARTED,
+ (self._svc_name_, ' (%s)' % self._svc_display_name_))
+
+
+ while 1:
+ start_time = time.time()
+ info = self.createProcess(self.start_cmd)
+ self.hZope = info[0] # the pid
+ if backoff_interval > BACKOFF_INITIAL_INTERVAL:
+ # if we're in a backoff state, log a message about
+ # starting a new process
+ servicemanager.LogInfoMsg(
+ '%s (%s): recovering from died process, new process '
+ 'started' % (self._svc_name_, self._svc_display_name_)
+ )
+ rc = win32event.WaitForMultipleObjects(
+ (self.hWaitStop, self.hZope), 0, win32event.INFINITE)
+ if rc == win32event.WAIT_OBJECT_0:
+ # user sent a stop service request
+ self.SvcStop()
+ break
+ else:
+ # user did not send a service stop request, but
+ # the process died; this may be an error condition
+ status = win32process.GetExitCodeProcess(self.hZope)
+ if status != 0:
+ # this was an abormal shutdown. if we can, we want to
+ # restart the process but if it seems hopeless,
+ # don't restart an infinite number of times.
+ if backoff_cumulative > BACKOFF_MAX:
+ # it's hopeless
+ servicemanager.LogErrorMsg(
+ '%s (%s): process could not be restarted due to max '
+ 'restart attempts exceeded' % (
+ self._svc_display_name_, self._svc_name_
+ ))
+ self.SvcStop()
+ break
+ servicemanager.LogWarningMsg(
+ '%s (%s): process died unexpectedly. Will attempt '
+ 'restart after %s seconds.' % (
+ self._svc_name_, self._svc_display_name_,
+ backoff_interval
+ )
+ )
+ # if BACKOFF_CLEAR_TIME seconds have elapsed since we last
+ # started the process, reset the backoff interval
+ # and the cumulative backoff time to their original
+ # states
+ if time.time() - start_time > BACKOFF_CLEAR_TIME:
+ backoff_interval = BACKOFF_INITIAL_INTERVAL
+ backoff_cumulative = 0
+ # we sleep for the backoff interval. since this is async
+ # code, it would be better done by sending and
+ # catching a timed event (a service
+ # stop request will need to wait for us to stop sleeping),
+ # but this works well enough for me.
+ time.sleep(backoff_interval)
+ # update backoff_cumulative with the time we spent
+ # backing off.
+ backoff_cumulative = backoff_cumulative + backoff_interval
+ # bump the backoff interval up by 2* the last interval
+ backoff_interval = backoff_interval * 2
+
+ # loop and try to restart the process
+
+ # log a service stopped message
+ servicemanager.LogMsg(
+ servicemanager.EVENTLOG_INFORMATION_TYPE,
+ servicemanager.PYS_SERVICE_STOPPED,
+ (self._svc_name_, ' (%s) ' % self._svc_display_name_))
if __name__=='__main__':
- dn = os.path.dirname
- zope_home = dn(dn(dn(dn(sys.argv[0]))))
- win32serviceutil.HandleCommandLine(ZServerService)
- if 'install' in args:
- command='"%s" "%s"' % (sys.executable,
- os.path.join(zope_home, 'bin', 'zope.py'))
- set_start_command(command)
- print "Setting Zope start command to:", command
+ win32serviceutil.HandleCommandLine(ZopeInstanceService)