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 @@ ...@@ -14,6 +14,7 @@
"""Windows Services installer/controller for Zope/ZEO/ZRS instance homes""" """Windows Services installer/controller for Zope/ZEO/ZRS instance homes"""
import msvcrt
import win32api import win32api
import win32con import win32con
import win32event import win32event
...@@ -37,8 +38,13 @@ BACKOFF_CLEAR_TIME = 30 ...@@ -37,8 +38,13 @@ BACKOFF_CLEAR_TIME = 30
BACKOFF_INITIAL_INTERVAL = 5 BACKOFF_INITIAL_INTERVAL = 5
class Service(win32serviceutil.ServiceFramework): class Service(win32serviceutil.ServiceFramework):
""" A class representing a Windows NT service that can manage an """Base class for a Windows Server to manage an external process.
instance-home-based Zope/ZEO/ZRS processes """
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 # The PythonService model requires that an actual on-disk class declaration
# represent a single service. Thus, the below definition of start_cmd, # represent a single service. Thus, the below definition of start_cmd,
...@@ -55,6 +61,13 @@ class Service(win32serviceutil.ServiceFramework): ...@@ -55,6 +61,13 @@ class Service(win32serviceutil.ServiceFramework):
r'-C "C:\Zope-Instance\etc\zope.conf"' 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 capture_io = False
log_file = None log_file = None
...@@ -67,6 +80,7 @@ class Service(win32serviceutil.ServiceFramework): ...@@ -67,6 +80,7 @@ class Service(win32serviceutil.ServiceFramework):
def SvcStop(self): def SvcStop(self):
# Before we do anything, tell the SCM we are starting the stop process. # Before we do anything, tell the SCM we are starting the stop process.
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
self.onStop()
# stop the process if necessary # stop the process if necessary
try: try:
win32process.TerminateProcess(self.hZope, 0) win32process.TerminateProcess(self.hZope, 0)
...@@ -76,8 +90,14 @@ class Service(win32serviceutil.ServiceFramework): ...@@ -76,8 +90,14 @@ class Service(win32serviceutil.ServiceFramework):
# And set my event. # And set my event.
win32event.SetEvent(self.hWaitStop) win32event.SetEvent(self.hWaitStop)
def onStop(self):
# A hook for subclasses to override
pass
def createProcess(self, cmd): def createProcess(self, cmd):
self.start_time = time.time()
if self.capture_io: if self.capture_io:
self.log = open(self.log_file, "ab")
return self.createProcessCaptureIO(cmd) return self.createProcessCaptureIO(cmd)
else: else:
return win32process.CreateProcess( return win32process.CreateProcess(
...@@ -126,55 +146,107 @@ class Service(win32serviceutil.ServiceFramework): ...@@ -126,55 +146,107 @@ class Service(win32serviceutil.ServiceFramework):
# BACKOFF_CLEAR_TIME seconds, the backoff stats are reset. # BACKOFF_CLEAR_TIME seconds, the backoff stats are reset.
# the initial number of seconds between process start attempts # 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 # the cumulative backoff seconds counter
backoff_cumulative = 0 self.backoff_cumulative = 0
import servicemanager import servicemanager
self.logmsg(servicemanager.PYS_SERVICE_STARTED) self.logmsg(servicemanager.PYS_SERVICE_STARTED)
while 1: while 1:
start_time = time.time()
info, handles = self.createProcess(self.start_cmd) info, handles = self.createProcess(self.start_cmd)
# XXX integrate handles into the wait and make a loop self.hZope = info[0] # process handle
# that reads data and writes it into a logfile # XXX why the test before the log message?
self.hZope = info[0] # the pid if self.backoff_interval > BACKOFF_INITIAL_INTERVAL:
if backoff_interval > BACKOFF_INITIAL_INTERVAL:
self.info("created process") self.info("created process")
rc = win32event.WaitForMultipleObjects( if not (self.run(handles) and self.checkRestart()):
(self.hWaitStop, self.hZope) + handles, 0, win32event.INFINITE) 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: if rc == win32event.WAIT_OBJECT_0:
# user sent a stop service request # user sent a stop service request
self.SvcStop() self.SvcStop()
keep_running = False
break break
else: elif rc == win32event.WAIT_OBJECT_0 + 1:
# user did not send a service stop request, but # user did not send a service stop request, but
# the process died; this may be an error condition # the process died; this may be an error condition
status = win32process.GetExitCodeProcess(self.hZope) status = win32process.GetExitCodeProcess(self.hZope)
if status == 0: # exit status 0 means the user caused a clean shutdown,
# the user shut the process down from the web # presumably via the web interface
# interface (or it otherwise exited cleanly) keep_running = status != 0
break break
else: 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
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. # this was an abormal shutdown.
if backoff_cumulative > BACKOFF_MAX: if self.backoff_cumulative > BACKOFF_MAX:
self.error("restarting too frequently; quit") self.error("restarting too frequently; quit")
self.SvcStop() self.SvcStop()
break return False
self.warning("sleep %s to avoid rapid restarts" self.warning("sleep %s to avoid rapid restarts"
% backoff_interval) % self.backoff_interval)
if time.time() - start_time > BACKOFF_CLEAR_TIME: if time.time() - self.start_time > BACKOFF_CLEAR_TIME:
backoff_interval = BACKOFF_INITIAL_INTERVAL self.backoff_interval = BACKOFF_INITIAL_INTERVAL
backoff_cumulative = 0 self.backoff_cumulative = 0
# XXX Since this is async code, it would be better # XXX Since this is async code, it would be better
# done by sending and catching a timed event (a # done by sending and catching a timed event (a
# service stop request will need to wait for us to # service stop request will need to wait for us to
# stop sleeping), but this works well enough for me. # stop sleeping), but this works well enough for me.
time.sleep(backoff_interval) time.sleep(self.backoff_interval)
backoff_cumulative += backoff_interval self.backoff_cumulative += self.backoff_interval
backoff_interval *= 2 self.backoff_interval *= 2
return True
self.logmsg(servicemanager.PYS_SERVICE_STOPPED)
def createProcessCaptureIO(self, cmd): def createProcessCaptureIO(self, cmd):
stdin = self.newPipe() stdin = self.newPipe()
...@@ -198,10 +270,9 @@ class Service(win32serviceutil.ServiceFramework): ...@@ -198,10 +270,9 @@ class Service(win32serviceutil.ServiceFramework):
# circumstances of a service process. # circumstances of a service process.
info = win32process.CreateProcess(None, cmd, None, None, True, 0, info = win32process.CreateProcess(None, cmd, None, None, True, 0,
None, None, si) None, None, si)
stdin[0].Close()
win32file.CloseHandle(stdin[0]) stdout[1].Close()
win32file.CloseHandle(stdout[1]) stderr[1].Close()
win32file.CloseHandle(stderr[1])
return info, (c_stdin, c_stdout, c_stderr) return info, (c_stdin, c_stdout, c_stderr)
...@@ -217,8 +288,9 @@ class Service(win32serviceutil.ServiceFramework): ...@@ -217,8 +288,9 @@ class Service(win32serviceutil.ServiceFramework):
pid = win32api.GetCurrentProcess() pid = win32api.GetCurrentProcess()
dup = win32api.DuplicateHandle(pid, pipe, pid, 0, 0, dup = win32api.DuplicateHandle(pid, pipe, pid, 0, 0,
win32con.DUPLICATE_SAME_ACCESS) win32con.DUPLICATE_SAME_ACCESS)
win32file.CloseHandle(pipe) pipe.Close()
return dup return dup
if __name__ == '__main__': if __name__ == '__main__':
win32serviceutil.HandleCommandLine(Service) 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