[Zope-Checkins] CVS: Zope/lib/python/nt_svcutils - service.py:1.5

Jeremy Hylton jeremy at zope.com
Mon Mar 29 11:07:48 EST 2004


Update of /cvs-repository/Zope/lib/python/nt_svcutils
In directory cvs.zope.org:/tmp/cvs-serv31778

Modified Files:
	service.py 
Log Message:
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.


=== Zope/lib/python/nt_svcutils/service.py 1.4 => 1.5 ===
--- Zope/lib/python/nt_svcutils/service.py:1.4	Tue Jan 13 17:37:02 2004
+++ Zope/lib/python/nt_svcutils/service.py	Mon Mar 29 11:07:48 2004
@@ -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_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 @@
         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 @@
     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 @@
         # 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 @@
         # 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
-
-        self.logmsg(servicemanager.PYS_SERVICE_STOPPED)
-
+                # 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
+
+    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 @@
         # 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 @@
         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)
+




More information about the Zope-Checkins mailing list