Commit 273bc8b4 authored by Jeremy Hylton's avatar Jeremy Hylton

Merge jeremy-windows-service-branch to the trunk.

There are no tests for this code, but the branch was tested by using
it for releases of some Zope Corp. products.
parent 5fcc3282
......@@ -14,6 +14,7 @@
"""Windows Services installer/controller for Zope/ZEO/ZRS instance homes"""
import msvcrt
import win32api
import win32con
import win32event
......@@ -37,8 +38,13 @@ BACKOFF_CLEAR_TIME = 30
BACKOFF_INITIAL_INTERVAL = 5
class Service(win32serviceutil.ServiceFramework):
""" A class representing a Windows NT service that can manage an
instance-home-based Zope/ZEO/ZRS processes """
"""Base class for a Windows Server to manage an external process.
Subclasses can be used to managed an instance home-based Zope or
ZEO process. The win32 Python service module registers a specific
file and class for a service. To manage an instance, a subclass
should be created in the instance home.
"""
# The PythonService model requires that an actual on-disk class declaration
# represent a single service. Thus, the below definition of start_cmd,
......@@ -54,7 +60,14 @@ class Service(win32serviceutil.ServiceFramework):
r'"C:\Program Files\Zope-2.7.0-a1\lib\python\Zope\Startup\run.py" '
r'-C "C:\Zope-Instance\etc\zope.conf"'
)
# If capture_io is True, then log_file must be the path of a file
# that the controlled process's stdout and stderr will be written to.
# The I/O capture is immature. It does not handle buffering in the
# controlled process or sensible interleaving of output between
# stdout and stderr. It is intended primarily as a stopgap when
# the controlled process produces critical output that can't be
# written to a log file using mechanism inside that process.
capture_io = False
log_file = None
......@@ -67,6 +80,7 @@ class Service(win32serviceutil.ServiceFramework):
def SvcStop(self):
# Before we do anything, tell the SCM we are starting the stop process.
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
self.onStop()
# stop the process if necessary
try:
win32process.TerminateProcess(self.hZope, 0)
......@@ -76,8 +90,14 @@ class Service(win32serviceutil.ServiceFramework):
# And set my event.
win32event.SetEvent(self.hWaitStop)
def onStop(self):
# A hook for subclasses to override
pass
def createProcess(self, cmd):
self.start_time = time.time()
if self.capture_io:
self.log = open(self.log_file, "ab")
return self.createProcessCaptureIO(cmd)
else:
return win32process.CreateProcess(
......@@ -126,56 +146,108 @@ class Service(win32serviceutil.ServiceFramework):
# BACKOFF_CLEAR_TIME seconds, the backoff stats are reset.
# the initial number of seconds between process start attempts
backoff_interval = BACKOFF_INITIAL_INTERVAL
self.backoff_interval = BACKOFF_INITIAL_INTERVAL
# the cumulative backoff seconds counter
backoff_cumulative = 0
self.backoff_cumulative = 0
import servicemanager
self.logmsg(servicemanager.PYS_SERVICE_STARTED)
while 1:
start_time = time.time()
info, handles = self.createProcess(self.start_cmd)
# XXX integrate handles into the wait and make a loop
# that reads data and writes it into a logfile
self.hZope = info[0] # the pid
if backoff_interval > BACKOFF_INITIAL_INTERVAL:
self.hZope = info[0] # process handle
# XXX why the test before the log message?
if self.backoff_interval > BACKOFF_INITIAL_INTERVAL:
self.info("created process")
rc = win32event.WaitForMultipleObjects(
(self.hWaitStop, self.hZope) + handles, 0, win32event.INFINITE)
if not (self.run(handles) and self.checkRestart()):
break
self.logmsg(servicemanager.PYS_SERVICE_STOPPED)
def run(self, handles):
"""Monitor the daemon process.
Returns True if the service should continue running and
False if the service process should exit. On True return,
the process exited unexpectedly and the caller should restart
it.
"""
keep_running = True
# Assume that the controlled program isn't expecting anything
# on stdin.
if handles:
handles[0].Close()
if handles:
waitfor = [self.hWaitStop, self.hZope, handles[1], handles[2]]
else:
waitfor = [self.hWaitStop, self.hZope]
while 1:
rc = win32event.WaitForMultipleObjects(waitfor, 0,
win32event.INFINITE)
if rc == win32event.WAIT_OBJECT_0:
# user sent a stop service request
self.SvcStop()
keep_running = False
break
else:
elif rc == win32event.WAIT_OBJECT_0 + 1:
# 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:
# the user shut the process down from the web
# interface (or it otherwise exited cleanly)
break
else:
# this was an abormal shutdown.
if backoff_cumulative > BACKOFF_MAX:
self.error("restarting too frequently; quit")
self.SvcStop()
break
self.warning("sleep %s to avoid rapid restarts"
% backoff_interval)
if time.time() - start_time > BACKOFF_CLEAR_TIME:
backoff_interval = BACKOFF_INITIAL_INTERVAL
backoff_cumulative = 0
# XXX 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)
backoff_cumulative += backoff_interval
backoff_interval *= 2
# exit status 0 means the user caused a clean shutdown,
# presumably via the web interface
keep_running = status != 0
break
else:
i = rc - win32event.WAIT_OBJECT_0
if not self.redirect(waitfor[i]):
del waitfor[i]
if handles:
handles[1].Close()
handles[2].Close()
return keep_running
self.logmsg(servicemanager.PYS_SERVICE_STOPPED)
def redirect(self, handle):
# This call will block until 80 bytes of output are ready.
# If the controlled program is buffering its I/O, it's
# possible for this to take a long time. Don't know if
# there is a better solution.
try:
ec, data = win32file.ReadFile(handle, 80)
except pywintypes.error, err:
# 109 means that the pipe was closed by the controlled
# process. Other errors might have similarly inocuous
# explanations, but we haven't run into them yet.
if err[0] != 109:
self.warning("Error reading output from process: %s" % err)
return False
# In the absence of overlapped I/O, the Python win32api
# turns all error codes into exceptions.
assert ec == 0
self.log.write(data)
self.log.flush()
return True
def checkRestart(self):
# this was an abormal shutdown.
if self.backoff_cumulative > BACKOFF_MAX:
self.error("restarting too frequently; quit")
self.SvcStop()
return False
self.warning("sleep %s to avoid rapid restarts"
% self.backoff_interval)
if time.time() - self.start_time > BACKOFF_CLEAR_TIME:
self.backoff_interval = BACKOFF_INITIAL_INTERVAL
self.backoff_cumulative = 0
# XXX 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(self.backoff_interval)
self.backoff_cumulative += self.backoff_interval
self.backoff_interval *= 2
return True
def createProcessCaptureIO(self, cmd):
stdin = self.newPipe()
stdout = self.newPipe()
......@@ -198,10 +270,9 @@ class Service(win32serviceutil.ServiceFramework):
# circumstances of a service process.
info = win32process.CreateProcess(None, cmd, None, None, True, 0,
None, None, si)
win32file.CloseHandle(stdin[0])
win32file.CloseHandle(stdout[1])
win32file.CloseHandle(stderr[1])
stdin[0].Close()
stdout[1].Close()
stderr[1].Close()
return info, (c_stdin, c_stdout, c_stderr)
......@@ -217,8 +288,9 @@ class Service(win32serviceutil.ServiceFramework):
pid = win32api.GetCurrentProcess()
dup = win32api.DuplicateHandle(pid, pipe, pid, 0, 0,
win32con.DUPLICATE_SAME_ACCESS)
win32file.CloseHandle(pipe)
pipe.Close()
return dup
if __name__ == '__main__':
win32serviceutil.HandleCommandLine(Service)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment