[Zope-Checkins] SVN: Zope/trunk/lib/python/Signals/ * Implement a signal mechanism for Windows utilising Windows 'Named Events'.

Sidnei da Silva sidnei at awkly.org
Wed Apr 13 21:57:11 EDT 2005


Log message for revision 29973:
  
  * Implement a signal mechanism for Windows utilising Windows 'Named Events'.
    See comments in WinSignalHandler.py for details.
  * As Windows can not rename an open file, when the 'reopen' signal is
    received try and call a 'rotate' method rather than a reopen.  Rotation
    itself is implemented in zLOG.LogHandler
  

Changed:
  U   Zope/trunk/lib/python/Signals/Signals.py
  A   Zope/trunk/lib/python/Signals/WinSignalHandler.py

-=-
Modified: Zope/trunk/lib/python/Signals/Signals.py
===================================================================
--- Zope/trunk/lib/python/Signals/Signals.py	2005-04-14 01:22:47 UTC (rev 29972)
+++ Zope/trunk/lib/python/Signals/Signals.py	2005-04-14 01:57:11 UTC (rev 29973)
@@ -18,14 +18,23 @@
 __version__='$Revision: 1.3 $'[11:-2]
 
 import logging
-import sys
+import sys, os
 
 import Lifetime
 
-from SignalHandler import SignalHandler
-
 logger = logging.getLogger("Z2")
 
+if os.name == 'nt':
+    try:
+        from WinSignalHandler import SignalHandler
+    except ImportError:
+        msg = ('Can not install signal handlers.  Please install '
+               '(or upgrade) your pywin32 installation '
+               '(https://sf.net/projects/pywin32)')
+        logger.warning(msg)
+        SignalHandler = None
+else:
+    from SignalHandler import SignalHandler
 
 def shutdownFastHandler():
     """Shutdown cleanly on SIGTERM. This is registered first,
@@ -60,6 +69,24 @@
             log.reopen()
         logger.info("Log files reopened successfully")
 
+# On Windows, a 'reopen' is useless - the file can not be renamed
+# while open, so we perform a trivial 'rotate'.
+class LogfileRotateHandler:
+    """Rotate log files on SIGUSR2. Only called on Windows. This is 
+       registered first, so it should be called after all other SIGUSR2 
+       handlers."""
+    def __init__(self, loggers):
+        self.loggers = [log for log in loggers if log is not None]
+
+    def __call__(self):
+        logger.debug("Log files rotation starting...")
+        for log in self.loggers:
+            for f in log.handler_factories:
+                handler = f()
+                if hasattr(handler, 'rotate') and callable(handler.rotate):
+                    handler.rotate()
+        logger.info("Log files rotation complete")
+
 def packHandler():
     """ Packs the main database.  Not safe to call under a signal
     handler, because it blocks the main thread """
@@ -74,14 +101,32 @@
         
 
 def registerZopeSignals(loggers):
-    import signal
-    SignalHandler.registerHandler(signal.SIGTERM, shutdownFastHandler)
-    SignalHandler.registerHandler(signal.SIGINT, shutdownHandler)
-    SignalHandler.registerHandler(signal.SIGHUP, restartHandler)
-    SignalHandler.registerHandler(signal.SIGUSR2,
-                                  LogfileReopenHandler(loggers))
+    from signal import SIGTERM, SIGINT
+    try:
+        from signal import SIGHUP, SIGUSR1, SIGUSR2
+    except ImportError:
+        # Windows doesn't have these (but also doesn't care what the exact
+        # numbers are)
+        SIGHUP = 1
+        SIGUSR1 = 10
+        SIGUSR2 = 12
+
+    if not SignalHandler:
+        return
+    SignalHandler.registerHandler(SIGTERM, shutdownFastHandler)
+    SignalHandler.registerHandler(SIGINT, shutdownHandler)
+    if os.name != 'nt':
+        SignalHandler.registerHandler(SIGHUP, restartHandler)
+        SignalHandler.registerHandler(SIGUSR2, LogfileReopenHandler(loggers))
+    else:
+        # no restart handler on windows.
+        # Log files get 'rotated', not 'reopened'
+        SignalHandler.registerHandler(SIGUSR2, LogfileRotateHandler(loggers))
     # SIGUSR1 is nominally reserved for pack, but we dont have an
     # implementation that is stable yet because if the signal handler
     # fires it will be caught in the main thread and all network operations
     # will cease until it's finished.
-    #SignalHandler.registerHandler(signal.SIGUSR1, packHandler)
+    # (The above is *not* True for Windows - a different thread is used to
+    # catch the signals.  This probably could be switched on for Windows
+    # if anyone cares)
+    #SignalHandler.registerHandler(SIGUSR1, packHandler)

Added: Zope/trunk/lib/python/Signals/WinSignalHandler.py
===================================================================
--- Zope/trunk/lib/python/Signals/WinSignalHandler.py	2005-04-14 01:22:47 UTC (rev 29972)
+++ Zope/trunk/lib/python/Signals/WinSignalHandler.py	2005-04-14 01:57:11 UTC (rev 29973)
@@ -0,0 +1,270 @@
+##############################################################################
+#
+# Copyright (c) 2001 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
+#
+##############################################################################
+
+"""Signal handling dispatcher for Windows."""
+
+# This code "simulates" Unix signals via Windows events.  When a signal is
+# registered, we simply create a global named event for that signal.  The
+# signal can be set by any user with the correct permission opening and
+# setting the event.
+#
+# One event is used per signal, and the event name is based on both the
+# Zope process ID and the signal number.  For example, assuming a process
+# ID of 123, a SIGINT handler would create an event called "Zope-123-2" 
+# (as signal.SIGINT==2).  The logfile reopen handler uses an event named
+# "Zope-123-12" (as the logfile handler uses SIGUSR2, which == 12)
+
+# The following program will send such an event:
+#   import sys, win32event
+#   hev = win32event.OpenEvent(win32event.EVENT_MODIFY_STATE, 0, sys.argv[1])
+#   win32event.SetEvent(hev)
+# A good way to get the PID is to read the var/*.pid file for the app.
+
+# This code is only the generic signal mechanism for Windows.
+# The signal handlers are still external, just like for other platforms.
+
+# NOTE: There is one huge semantic difference between these "signals"
+# and signals on Unix.  On Windows, the signals are delivered asynchronously
+# to a thread inside this module.  This thread calls the event handler 
+# directly - there is no magic to switch the call back to the main thread.
+# If this is a problem (not currently, but likely later), one option may be 
+# to add yet another asyncore handler - the thread in this module could 
+# then "post" the request to the main thread via this asyncore handler.
+
+import sys, os
+import signal
+import threading
+import asyncore
+import atexit
+import Lifetime
+
+# SetConsoleCtrlHandler not in early pywin32 versions - Signals.py will
+# catch the import error.
+from win32api import SetConsoleCtrlHandler
+import win32con
+import win32event
+import pywintypes
+import ntsecuritycon
+
+import logging
+logger=logging.getLogger("WinSignalHandler")
+
+# We simulate signals via win32 named events.  This is the event name
+# prefix we use - the "signal number" is appended to this name.
+event_name_prefix = "Zope-%d-" % os.getpid()
+
+# For Windows 2000 and later, we prefix "Global\" to the name, so that
+# it works correctly in a Terminal Services environment.
+winver = sys.getwindowsversion()
+# sys.getwindowsversion() -> major, minor, build, platform_id, ver_string
+# for platform_id, 2==VER_PLATFORM_WIN32_NT
+if winver[0] >= 5 and winver[3] == 2:
+    event_name_prefix = "Global\\" + event_name_prefix
+
+def createEventSecurityObject():
+    # Create a security object giving World read/write access,
+    # but only "Owner" modify access.
+    sa = pywintypes.SECURITY_ATTRIBUTES()
+    sidEveryone = pywintypes.SID()
+    sidEveryone.Initialize(ntsecuritycon.SECURITY_WORLD_SID_AUTHORITY,1)
+    sidEveryone.SetSubAuthority(0, ntsecuritycon.SECURITY_WORLD_RID)
+    sidCreator = pywintypes.SID()
+    sidCreator.Initialize(ntsecuritycon.SECURITY_CREATOR_SID_AUTHORITY,1)
+    sidCreator.SetSubAuthority(0, ntsecuritycon.SECURITY_CREATOR_OWNER_RID)
+
+    acl = pywintypes.ACL()
+    acl.AddAccessAllowedAce(win32event.EVENT_MODIFY_STATE, sidEveryone)
+    acl.AddAccessAllowedAce(ntsecuritycon.FILE_ALL_ACCESS, sidCreator)
+
+    sa.SetSecurityDescriptorDacl(1, acl, 0)
+    return sa
+
+def wakeSelect():
+    """Interrupt a sleeping asyncore 'select' call"""
+    # What is the right thing to do here?  
+    # asyncore.close_all() works, but I fear that would
+    # prevent the poll based graceful cleanup code from working.
+    # This seems to work :)
+    for fd, obj in asyncore.socket_map.items():
+        if hasattr(obj, "pull_trigger"):
+            obj.pull_trigger()
+
+class SignalHandler:
+
+    def __init__(self):
+        self.registry = {}
+        self.event_handles = {}
+        self.admin_event_handle = win32event.CreateEvent(None, 0, 0, None)
+        self.shutdown_requested = False
+        # Register a "console control handler" for Ctrl+C/Break notification.
+        SetConsoleCtrlHandler(consoleCtrlHandler)
+        
+        # Start the thread that is watching for events.
+        thread = threading.Thread(target=self.signalCheckerThread)
+        # If something goes terribly wrong, don't wait for this thread!
+        thread.setDaemon(True)
+        thread.start()
+        self.signal_thread = thread
+
+    def shutdown(self):
+        # Shutdown our signal watcher thread.
+        logger.debug("signal handler shutdown starting.")
+        self.shutdown_requested = 1
+        win32event.SetEvent(self.admin_event_handle)
+        # sadly, this can deadlock at shutdown when Ctrl+C is used
+        # (although not then the event is used to trigger shutdown)
+        # at least in build 204.  Further updates as they come to hand...
+        # Remove the Windows control handler
+        #SetConsoleCtrlHandler(consoleCtrlHandler, 0)
+        self.signal_thread.join(5) # should never block for long!
+
+        self.registry = None
+        self.event_handles = None
+        self.admin_event_handle = None
+        logger.debug("signal handler shutdown complete.")
+
+    def consoleCtrlHandler(self, ctrlType):
+        """Called by Windows on a new thread whenever a console control 
+           event is raised."""
+        logger.debug("Windows control event %d" % ctrlType)
+        sig = None
+        if ctrlType == win32con.CTRL_C_EVENT:
+            # user pressed Ctrl+C or someone did GenerateConsoleCtrlEvent
+            sig = signal.SIGINT
+        elif ctrlType == win32con.CTRL_BREAK_EVENT:
+            sig = signal.SIGTERM
+        elif ctrlType == win32con.CTRL_CLOSE_EVENT:
+            # Console is about to die.
+            # CTRL_CLOSE_EVENT gives us 5 seconds before displaying
+            # the "End process" dialog - so treat as 'fast'
+            sig = signal.SIGTERM
+        elif ctrlType in (win32con.CTRL_LOGOFF_EVENT, 
+                          win32con.CTRL_SHUTDOWN_EVENT):
+            # MSDN says:
+            # "Note that this signal is received only by services. 
+            # Interactive applications are terminated at logoff, so 
+            # they are not present when the system sends this signal."
+            # We can therefore ignore it (our service framework 
+            # manages shutdown in this case)
+            pass
+        else:
+            logger.info("Unexpected windows control event %d" % ctrlType)
+        # Call the signal handler - we could also do it asynchronously
+        # by setting the relevant event, but we need it synchronous so
+        # that we don't wake the select loop until after the shutdown
+        # flags have been set.
+        result = 0
+        if sig is not None and self.registry.has_key(sig):
+            self.signalHandler(sig, None)
+            result = 1 # don't call other handlers.
+        return result
+
+    def signalCheckerThread(self):
+        while not self.shutdown_requested:
+            handles = [self.admin_event_handle]
+            signums = [None]
+            for signum, handle in self.event_handles.items():
+                signums.append(signum)
+                handles.append(handle)
+            rc = win32event.WaitForMultipleObjects(handles, False,
+                                                   win32event.INFINITE)
+            logger.debug("signalCheckerThread awake with %s" % rc)
+            signum = signums[rc - win32event.WAIT_OBJECT_0]
+            if signum is None:
+                # Admin event - either shutdown, or new event object created.
+                pass
+            else:
+                logger.debug("signalCheckerThread calling %s" % signum)
+                self.signalHandler(signum, None)
+                logger.debug("signalCheckerThread back")
+        logger.debug("signalCheckerThread stopped")
+
+    def registerHandler(self, signum, handler):
+        """Register a handler function that will be called when the process
+           recieves the signal signum. The signum argument must be a signal
+           constant such as SIGTERM. The handler argument must be a function
+           or method that takes no arguments."""
+        items = self.registry.get(signum)
+        if items is None:
+            items = self.registry[signum] = []
+            # Create an event for this signal.
+            event_name = event_name_prefix + str(signum)
+            sa = createEventSecurityObject()
+            hevent = win32event.CreateEvent(sa, 0, 0, event_name)
+            self.event_handles[signum] = hevent
+            # Let the worker thread know there is a new handle.
+            win32event.SetEvent(self.admin_event_handle)
+            signame = get_signal_name(signum)
+            logger.debug("Installed sighandler for %s (%s)" % (signame, event_name))
+        items.insert(0, handler)
+
+    def getRegisteredSignals(self):
+        """Return a list of the signals that have handlers registered. This
+           is used to pass the signals through to the ZDaemon code."""
+        return self.registry.keys()
+
+    def signalHandler(self, signum, frame):
+        """Meta signal handler that dispatches to registered handlers."""
+        signame = get_signal_name(signum)
+        logger.info("Caught signal %s" % signame)
+
+        for handler in self.registry.get(signum, []):
+            # Never let a bad handler prevent the standard signal
+            # handlers from running.
+            try: handler()
+            except SystemExit, rc:
+                # On Unix, signals are delivered to the main thread, so a 
+                # SystemExit does the right thing.  On Windows, we are on
+                # our own thread, so throwing SystemExit there isn't a great
+                # idea.  Just shutdown the main loop.
+                logger.debug("Trapped SystemExit(%s) - doing Lifetime shutdown" % (rc,))
+                Lifetime.shutdown(rc)
+            except:
+                logger.exception("A handler for %s failed!'" % signame)
+            wakeSelect() # trigger a walk around the Lifetime loop.
+
+_signals = None
+
+def get_signal_name(n):
+    """Return the symbolic name for signal n.
+
+    Returns 'signal n' if there is no SIG name bound to n in the signal
+    module.
+    """
+    global _signals
+    if _signals is None:
+        _signals = {}
+        for k, v in signal.__dict__.items():
+            startswith = getattr(k, 'startswith', None)
+            if startswith is None:
+                continue
+            if startswith('SIG') and not startswith('SIG_'):
+                _signals[v] = k
+        # extra ones that aren't (weren't?) in Windows.
+        for name, val in ("SIGHUP", 1), ("SIGUSR1", 10), ("SIGUSR2", 12):
+            if not _signals.has_key(name):
+                _signals[val] = name
+
+    return _signals.get(n, 'signal %d' % n)
+
+# The win32 ConsoleCtrlHandler
+def consoleCtrlHandler(ctrlType):
+    return SignalHandler.consoleCtrlHandler(ctrlType)
+
+# The SignalHandler is actually a singleton.
+SignalHandler = SignalHandler()
+
+# Need to be careful at shutdown - the 'signal watcher' thread which triggers 
+# the shutdown may still be running when the main thread terminates and 
+# Python starts cleaning up.
+atexit.register(SignalHandler.shutdown)


Property changes on: Zope/trunk/lib/python/Signals/WinSignalHandler.py
___________________________________________________________________
Name: svn:eol-style
   + native



More information about the Zope-Checkins mailing list