Skip to content
This repository has been archived by the owner on Feb 13, 2020. It is now read-only.

Commit

Permalink
Merge jeremy-windows-service-branch to the trunk.
Browse files Browse the repository at this point in the history
There are no tests for this code, but the branch was tested by using
it for releases of some Zope Corp. products.
  • Loading branch information
Jeremy Hylton committed Mar 29, 2004
1 parent b215a05 commit f0d0735
Showing 1 changed file with 113 additions and 41 deletions.
154 changes: 113 additions & 41 deletions service.py
Expand Up @@ -14,6 +14,7 @@

"""Windows Services installer/controller for Zope/ZEO/ZRS instance homes"""

import msvcrt
import win32api
import win32con
import win32event
Expand All @@ -37,8 +38,13 @@
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,
Expand All @@ -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

Expand All @@ -67,6 +80,7 @@ def __init__(self, args):
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)
Expand All @@ -76,8 +90,14 @@ def SvcStop(self):
# 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(
Expand Down Expand Up @@ -126,56 +146,108 @@ def SvcDoRun(self):
# 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()
Expand All @@ -198,10 +270,9 @@ def createProcessCaptureIO(self, cmd):
# 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)

Expand All @@ -217,8 +288,9 @@ def dup(self, pipe):
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)

0 comments on commit f0d0735

Please sign in to comment.