[CalendarServer-changes] [5390] CalendarServer/branches/users/glyph/sendfdport

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 24 08:44:13 PDT 2010


Revision: 5390
          http://trac.macosforge.org/projects/calendarserver/changeset/5390
Author:   glyph at apple.com
Date:     2010-03-24 08:44:13 -0700 (Wed, 24 Mar 2010)
Log Message:
-----------
refactor existing file-descriptor inheritance and add some test coverage (plus stub for new mode)

Modified Paths:
--------------
    CalendarServer/branches/users/glyph/sendfdport/calendarserver/tap/caldav.py
    CalendarServer/branches/users/glyph/sendfdport/calendarserver/tap/test/test_caldav.py
    CalendarServer/branches/users/glyph/sendfdport/twistedcaldav/stdconfig.py

Modified: CalendarServer/branches/users/glyph/sendfdport/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/glyph/sendfdport/calendarserver/tap/caldav.py	2010-03-24 15:40:14 UTC (rev 5389)
+++ CalendarServer/branches/users/glyph/sendfdport/calendarserver/tap/caldav.py	2010-03-24 15:44:13 UTC (rev 5390)
@@ -40,7 +40,7 @@
 from twisted.python.usage import Options, UsageError
 from twisted.python.reflect import namedClass
 from twisted.plugin import IPlugin
-from twisted.internet.reactor import callLater, spawnProcess, addSystemEventTrigger
+from twisted.internet.reactor import callLater, addSystemEventTrigger
 from twisted.internet.process import ProcessExitedAlready
 from twisted.internet.protocol import Protocol, Factory
 from twisted.application.internet import TCPServer, UNIXServer
@@ -516,6 +516,20 @@
             return service
 
 
+    def createContextFactory(self):
+        """
+        Create an SSL context factory for use with any SSL socket talking to
+        this server.
+        """
+        return ChainingOpenSSLContextFactory(
+            config.SSLPrivateKey,
+            config.SSLCertificate,
+            certificateChainFile=config.SSLAuthorityChain,
+            passwdCallback=getSSLPassphrase,
+            sslmethod=getattr(OpenSSL.SSL, config.SSLMethod),
+        )
+
+
     def makeService_Slave(self, options):
         #
         # Change default log level to "info" as its useful to have
@@ -585,19 +599,14 @@
 
         config.addPostUpdateHooks((updateFactory,))
 
-        if config.InheritFDs or config.InheritSSLFDs:
+        if config.InheritSSLFDs or config.InheritFDs:
+            # Inherit sockets to call accept() on them individually.
 
             for fd in config.InheritSSLFDs:
                 fd = int(fd)
 
                 try:
-                    contextFactory = ChainingOpenSSLContextFactory(
-                        config.SSLPrivateKey,
-                        config.SSLCertificate,
-                        certificateChainFile=config.SSLAuthorityChain,
-                        passwdCallback=getSSLPassphrase,
-                        sslmethod=getattr(OpenSSL.SSL, config.SSLMethod),
-                    )
+                    contextFactory = self.createContextFactory()
                 except SSLError, e:
                     log.error("Unable to set up SSL context factory: %s" % (e,))
                 else:
@@ -623,7 +632,11 @@
                     inherit=True
                 ).setServiceParent(service)
 
+        elif config.MetaFD:
+            fd = int(config.MetaFD)
 
+            # XXX sendmsg()-FD case
+
         else: # Not inheriting, therefore we open our own:
 
             if not config.BindAddresses:
@@ -651,13 +664,7 @@
                                   % (bindAddress, port))
 
                     try:
-                        contextFactory = ChainingOpenSSLContextFactory(
-                            config.SSLPrivateKey,
-                            config.SSLCertificate,
-                            certificateChainFile=config.SSLAuthorityChain,
-                            passwdCallback=getSSLPassphrase,
-                            sslmethod=getattr(OpenSSL.SSL, config.SSLMethod),
-                        )
+                        contextFactory = self.createContextFactory()
                     except SSLError, e:
                         self.log_error("Unable to set up SSL context factory: %s"
                                        % (e,))
@@ -957,10 +964,23 @@
 
 
 class TwistdSlaveProcess(object):
+    """
+    A L{TwistdSlaveProcess} is information about how to start a slave process
+    running a C{twistd} plugin, to be used by
+    L{DelayedStartupProcessMonitor.addProcessObject}.
+
+    @ivar inheritFDs: File descriptors to be inherited for calling accept() on
+        in the subprocess.
+    @type inheritFDs: C{list} of C{int}
+
+    @ivar inheritSSLFDs: File descriptors to be inherited for calling accept()
+        on in the subprocess, and speaking TLS on the resulting sockets.
+    @type inheritSSLFDs: C{list} of C{int}
+    """
     prefix = "caldav"
 
     def __init__(self, twistd, tapname, configFile, id, interfaces,
-            inheritFDs=None, inheritSSLFDs=None):
+                 inheritFDs=None, inheritSSLFDs=None, metaFD=None):
 
         self.twistd = twistd
 
@@ -969,16 +989,42 @@
         self.configFile = configFile
 
         self.id = id
+        def emptyIfNone(x):
+            if x is None:
+                return []
+            else:
+                return x
+        self.inheritFDs = emptyIfNone(inheritFDs)
+        self.inheritSSLFDs = emptyIfNone(inheritSSLFDs)
+        self.metaFD = metaFD
 
-        self.inheritFDs = inheritFDs
-        self.inheritSSLFDs = inheritSSLFDs
-
         self.interfaces = interfaces
 
     def getName(self):
         return '%s-%s' % (self.prefix, self.id)
 
+
+    def getFileDescriptors(self):
+        """
+        @return: a mapping of file descriptor numbers for the new (child)
+            process to file descriptor numbers in the current (master) process.
+        """
+        fds = {}
+        maybeMetaFD = []
+        if self.metaFD:
+            maybeMetaFD.append(self.metaFD)
+        for fd in self.inheritSSLFDs + self.inheritFDs + maybeMetaFD:
+            fds[fd] = fd
+        return fds
+
+
     def getCommandLine(self):
+        """
+        @return: a list of command-line arguments, including the executable to
+            be used to start this subprocess.
+
+        @rtype: C{list} of C{str}
+        """
         args = [sys.executable, self.twistd]
 
         if config.UserName:
@@ -1020,6 +1066,11 @@
                 "-o", "InheritSSLFDs=%s" % (",".join(map(str, self.inheritSSLFDs)),)
             ])
 
+        if self.metaFD:
+            args.extend([
+                    "-o", "MetaFD=%s" % (self.metaFD,)
+                ])
+
         return args
 
 
@@ -1034,17 +1085,48 @@
         config.ControlPort = self._port.getHost().port
 
 class DelayedStartupProcessMonitor(procmon.ProcessMonitor):
+    """
+    A L{DelayedStartupProcessMonitor} is a L{procmon.ProcessMonitor} that
+    defers building its command lines until the service is actually ready to
+    start.  It also specializes process-starting to allow for process objects
+    to
 
+    @ivar processObjects: a list of L{TwistdSlaveProcess} to add using
+        C{self.addProcess} when this service starts up.
+
+    @ivar _extraFDs: a mapping from process names to extra file-descriptor
+        maps.  (By default, all processes will have the standard stdio mapping,
+        so all file descriptors here should be >2.)  This is updated during
+        L{DelayedStartupProcessMonitor.startService}, by inspecting
+        L{TwistdSlaveProcess.getFileDescriptors}.
+
+    @ivar reactor: an L{IReactorProcess} for spawning processes, defaulting to
+        the global reactor.
+    """
+
     def __init__(self, *args, **kwargs):
         procmon.ProcessMonitor.__init__(self, *args, **kwargs)
 
         # processObjects stores TwistdSlaveProcesses which need to have their
         # command-lines determined just in time
         self.processObjects = []
+        self._extraFDs = {}
+        from twisted.internet import reactor
+        self.reactor = reactor
 
+
     def addProcessObject(self, process, env):
+        """
+        Add a process object to be run when this service is started.
+
+        @param env: a dictionary of environment variables.
+
+        @param process: a L{TwistdSlaveProcesses} object to be started upon
+            service startup.
+        """
         self.processObjects.append((process, env))
 
+
     def startService(self):
         Service.startService(self)
 
@@ -1057,19 +1139,21 @@
                 processObject.getCommandLine(),
                 env=env
             )
+            self._extraFDs[
+                processObject.getName()] = processObject.getFileDescriptors()
 
         self.active = 1
         delay = 0
 
         if config.MultiProcess.StaggeredStartup.Enabled:
-            delay_interval = config.MultiProcess.StaggeredStartup.Interval
+            interval = config.MultiProcess.StaggeredStartup.Interval
         else:
-            delay_interval = 0
+            interval = 0
 
         for name in self.processes.keys():
             if name.startswith("caldav"):
                 when = delay
-                delay += delay_interval
+                delay += interval
             else:
                 when = 0
             callLater(when, self.startProcess, name)
@@ -1122,16 +1206,12 @@
 
         childFDs = { 0 : "w", 1 : "r", 2 : "r" }
 
-        # Examine args for -o InheritFDs= and -o InheritSSLFDs=
-        # Add any file descriptors listed in those args to the childFDs
-        # dictionary so those don't get closed across the spawn.
-        for i in xrange(len(args)-1):
-            if args[i] == "-o" and args[i+1].startswith("Inherit"):
-                for fd in map(int, args[i+1].split("=")[1].split(",")):
-                    childFDs[fd] = fd
+        childFDs.update(self._extraFDs.get(name, {}))
 
-        spawnProcess(p, args[0], args, uid=uid, gid=gid, env=env,
-            childFDs=childFDs)
+        self.reactor.spawnProcess(
+            p, args[0], args, uid=uid, gid=gid, env=env,
+            childFDs=childFDs
+        )
 
 
 class DelayedStartupLineLogger(object):

Modified: CalendarServer/branches/users/glyph/sendfdport/calendarserver/tap/test/test_caldav.py
===================================================================
--- CalendarServer/branches/users/glyph/sendfdport/calendarserver/tap/test/test_caldav.py	2010-03-24 15:40:14 UTC (rev 5389)
+++ CalendarServer/branches/users/glyph/sendfdport/calendarserver/tap/test/test_caldav.py	2010-03-24 15:44:13 UTC (rev 5390)
@@ -21,6 +21,8 @@
 
 from os.path import dirname, abspath
 
+from zope.interface import implements
+
 from twisted.trial.unittest import TestCase as BaseTestCase
 
 from twisted.python.threadable import isInIOThread
@@ -30,7 +32,8 @@
 from twisted.python import log
 
 from twisted.internet.protocol import ServerFactory
-from twisted.internet.defer import Deferred
+from twisted.internet.defer import Deferred, inlineCallbacks
+from twisted.internet.interfaces import IProcessTransport, IReactorProcess
 
 from twisted.application.service import IService
 from twisted.application import internet
@@ -50,16 +53,76 @@
 from twistedcaldav.directory.directory import UnknownRecordTypeError
 from twistedcaldav.test.util import TestCase
 
-from calendarserver.tap.caldav import (CalDAVOptions, CalDAVServiceMaker,
-                                       CalDAVService, GroupOwnedUNIXServer,
-                                       DelayedStartupProcessMonitor,
-                                       DelayedStartupLineLogger)
+from calendarserver.tap.caldav import (
+    CalDAVOptions, CalDAVServiceMaker, CalDAVService, GroupOwnedUNIXServer,
+    DelayedStartupProcessMonitor, DelayedStartupLineLogger, TwistdSlaveProcess
+)
 
 
 # Points to top of source tree.
 sourceRoot = dirname(dirname(dirname(dirname(abspath(__file__)))))
 
 
+class NotAProcessTransport(object):
+    """
+    Simple L{IProcessTransport} stub.
+    """
+    implements(IProcessTransport)
+
+    def __init__(self, processProtocol, executable, args, env, path,
+                 uid, gid, usePTY, childFDs):
+        """
+        Hold on to all the attributes passed to spawnProcess.
+        """
+        self.processProtocol = processProtocol
+        self.executable = executable
+        self.args = args
+        self.env = env
+        self.path = path
+        self.uid = uid
+        self.gid = gid
+        self.usePTY = usePTY
+        self.childFDs = childFDs
+
+
+class InMemoryProcessSpawner(object):
+    """
+    Stub out L{IReactorProcess.spawnProcess} so that we can examine the
+    interaction of L{DelayedStartupProcessMonitor} and the reactor.
+    """
+    implements(IReactorProcess)
+
+    def __init__(self):
+        """
+        Create some storage to hold on to all the fake processes spawned.
+        """
+        self.processTransports = []
+        self.waiting = []
+
+    def waitForOneProcess(self):
+        """
+        Return a L{Deferred} which will fire when spawnProcess has been
+        invoked, with the L{IProcessTransport}.
+        """
+        d = Deferred()
+        self.waiting.append(d)
+        return d
+
+    def spawnProcess(self, processProtocol, executable, args=(), env={},
+                     path=None, uid=None, gid=None, usePTY=0,
+                     childFDs=None):
+
+        transport = NotAProcessTransport(
+            processProtocol, executable, args, env, path, uid, gid, usePTY,
+            childFDs
+        )
+        self.processTransports.append(transport)
+        if self.waiting:
+            self.waiting.pop(0).callback(transport)
+        return transport
+        
+
+
 class TestCalDAVOptions (CalDAVOptions):
     """
     A fake implementation of CalDAVOptions that provides
@@ -798,6 +861,8 @@
 class DummyProcessObject(object):
     """
     Simple stub for the Process Object API that will run a test script.
+
+    This is a stand in for L{TwistdSlaveProcess}.
     """
 
     def __init__(self, scriptname, *args):
@@ -809,9 +874,17 @@
         """
         Get the command line to invoke this script.
         """
-        return [sys.executable, FilePath(__file__).sibling(self.scriptname).path] + self.args
+        return [sys.executable,
+                FilePath(__file__).sibling(self.scriptname).path] + self.args
 
 
+    def getFileDescriptors(self):
+        """
+        Return a dummy, empty mapping of file descriptors.
+        """
+        return {}
+
+
     def getName(self):
         """
         Get a dummy name.
@@ -873,3 +946,68 @@
         return d
 
 
+    @inlineCallbacks
+    def test_acceptDescriptorInheritance(self):
+        """
+        If a L{TwistdSlaveProcess} specifies some file descriptors to be
+        inherited, they should be inherited by the subprocess.
+        """
+        dspm         = DelayedStartupProcessMonitor()
+        dspm.reactor = InMemoryProcessSpawner()
+
+        # Most arguments here will be ignored, so these are bogus values.
+        slave = TwistdSlaveProcess(
+            twistd        = "bleh",
+            tapname       = "caldav",
+            configFile    = "/does/not/exist",
+            id            = 10,
+            interfaces    = '127.0.0.1',
+            inheritFDs    = [3, 7],
+            inheritSSLFDs = [19, 25],
+        )
+
+        dspm.addProcessObject(slave, {})
+        dspm.startService()
+        self.addCleanup(dspm.consistency.cancel)
+        # We can easily stub out spawnProcess, because caldav calls it, but a
+        # bunch of callLater calls are buried in procmon itself, so we need to
+        # use the real clock.
+        oneProcessTransport = yield dspm.reactor.waitForOneProcess()
+        self.assertEquals(oneProcessTransport.childFDs,
+                          {0: 'w', 1: 'r', 2: 'r',
+                           3: 3, 7: 7,
+                           19: 19, 25: 25})
+    @inlineCallbacks
+    def test_metaDescriptorInheritance(self):
+        """
+        If a L{TwistdSlaveProcess} specifies a meta-file-descriptor to be
+        inherited, it should be inherited by the subprocess, and a
+        configuration argument should be passed that indicates to the
+        subprocess.
+        """
+        dspm         = DelayedStartupProcessMonitor()
+        dspm.reactor = InMemoryProcessSpawner()
+
+        # Most arguments here will be ignored, so these are bogus values.
+        slave = TwistdSlaveProcess(
+            twistd        = "bleh",
+            tapname       = "caldav",
+            configFile    = "/does/not/exist",
+            id            = 10,
+            interfaces    = '127.0.0.1',
+            metaFD        = 4
+        )
+
+        dspm.addProcessObject(slave, {})
+        dspm.startService()
+        self.addCleanup(dspm.consistency.cancel)
+        oneProcessTransport = yield dspm.reactor.waitForOneProcess()
+        self.assertEquals(oneProcessTransport.childFDs,
+                          {0: 'w', 1: 'r', 2: 'r',
+                           4: 4})
+        self.assertIn("MetaFD=4", oneProcessTransport.args)
+        self.assertEquals(
+            oneProcessTransport.args[oneProcessTransport.args.index("MetaFD=4")-1],
+            '-o',
+            "MetaFD argument was not passed as an option"
+        )

Modified: CalendarServer/branches/users/glyph/sendfdport/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/glyph/sendfdport/twistedcaldav/stdconfig.py	2010-03-24 15:40:14 UTC (rev 5389)
+++ CalendarServer/branches/users/glyph/sendfdport/twistedcaldav/stdconfig.py	2010-03-24 15:44:13 UTC (rev 5390)
@@ -141,6 +141,7 @@
     "BindSSLPorts" : [],   # List of port numbers to bind to for SSL [empty = same as "SSLPort"]
     "InheritFDs"   : [],   # File descriptors to inherit for HTTP requests (empty = don't inherit)
     "InheritSSLFDs": [],   # File descriptors to inherit for HTTPS requests (empty = don't inherit)
+    "MetaFD": 0,        # Inherited file descriptor to call recvmsg() on to recive sockets (none = don't inherit)
 
     #
     # Types of service provided
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100324/43183f3e/attachment-0001.html>


More information about the calendarserver-changes mailing list