Hi Chris/Mike,

I found and fixed one other issue and updated the patch.  Here's the
summary:
- Added support for setting the ownership and permissions for an FCGI socket
- Fixed bug where FCGI socket reference count was not getting decremented on
spawn error

I updated the unit tests to verify the reference count fix.

Thanks,

Roger

On Fri, Oct 23, 2009 at 11:55 AM, Roger Hoover <[email protected]>wrote:

> Thanks, everyone.  Attached is the patch with full unit tests.
>
> Chris, I need to update the manual as well for the new fcgi-program
> options, socket_owner and socket_mode.  I forget where that is.  In another
> svn repo?
>
> Thanks,
>
> Roger
>
>
> On Fri, Oct 23, 2009 at 9:57 AM, Chris McDonough <[email protected]> wrote:
>
>> Roger Hoover wrote:
>>
>>> Hmmm...the more I think about it., socket_owner and socket_mode are
>>> indeed better names.  Chris/Mike, do you want to weigh in on consistency vs.
>>> clarity?
>>>
>>
>> I'd be fine with either.
>>
>> - C
>>
>>
>>
>>> On Fri, Oct 23, 2009 at 9:08 AM, Roger Hoover 
>>> <[email protected]<mailto:
>>> [email protected]>> wrote:
>>>
>>>
>>>
>>>    On Fri, Oct 23, 2009 at 8:57 AM, Grzegorz Nosek <[email protected]
>>>    <mailto:[email protected]>> wrote:
>>>
>>>        On Fri, Oct 23, 2009 at 08:33:28AM -0700, Roger Hoover wrote:
>>>         > I'm most of the way there on #2.  The issue is that as far as
>>>        I can tell
>>>         > there's no way to find out the umask for a user so I don't
>>>        automatically
>>>         > know what permissions to chmod the FCGI socket with.  Now,
>>>        the choices are
>>>         >
>>>         > a) Don't chmod the FCGI socket, just chown it to the uid/gid
>>>        of the user the
>>>         > process will run as
>>>         >
>>>         > b) Add socket_chown, socket_chmod args that only apply to
>>>        unix domain
>>>         > sockets.  This allows the most control for the user but the
>>>        fact that the
>>>         > params don't always make sense is a bit awkward.
>>>         >
>>>         > [fcgi-program:test]
>>>         > command=/foo/bar.fcgi
>>>         > socket=unix:///tmp/test.socket
>>>         > socket_chown=rhoover:wheel ; this option would only apply to
>>>        unix domain
>>>         > sockets
>>>         > socket_chmod=0777 ; this option would only apply to unix
>>>        domain sockets
>>>         > user=nobody
>>>         > process_name=foo_%(process_num)s
>>>         > numprocs=2
>>>         >
>>>         > Anyone have an opinion here?
>>>
>>>        I'm for explicit owner and mode options. Apache-style FastCGI
>>>        wrappers
>>>        are a pain.
>>>
>>>
>>>    Thanks.  I was leaning this direction.
>>>
>>>
>>>        Also, my vote would go to naming these options "socket_owner" and
>>>        "socket_mode" (or "socket_perm(s)"?) as I've heard enough of
>>>        "setting
>>>        chmods" in my day ;)
>>>
>>>
>>>    That makes sense but I'm going for consistency with the existing
>>>    unix_http_server section of the config.
>>> http://supervisord.org/manual/current/configuration.html#unix_http_server
>>>
>>>
>>>        Best regards,
>>>         Grzegorz Nosek
>>>
>>>
>>>
>>>
>>
>
Index: src/supervisor/options.py
===================================================================
--- src/supervisor/options.py   (revision 898)
+++ src/supervisor/options.py   (working copy)
@@ -645,6 +645,25 @@
                 continue
             program_name = section.split(':', 1)[1]
             priority = integer(get(section, 'priority', 999))
+            
+            proc_uid = name_to_uid(get(section, 'user', None))
+            
+            socket_owner = get(section, 'socket_owner', None)
+            if socket_owner is not None:
+                try:
+                    socket_owner = colon_separated_user_group(socket_owner)
+                except ValueError:
+                    raise ValueError('Invalid socket_owner value %s'
+                                                                % 
socket_owner)                
+                
+            socket_mode = get(section, 'socket_mode', None)
+            if socket_mode is not None:
+                try:
+                    socket_mode = octal_type(socket_mode)
+                except (TypeError, ValueError):
+                    raise ValueError('Invalid socket_mode value %s'
+                                                                % socket_mode)
+            
             socket = get(section, 'socket', None)
             if not socket:
                 raise ValueError('[%s] section requires a "socket" line' %
@@ -654,7 +673,8 @@
                           'program_name':program_name}
             socket = expand(socket, expansions, 'socket')
             try:
-                socket_config = self.parse_fcgi_socket(socket)
+                socket_config = self.parse_fcgi_socket(socket, proc_uid,
+                                                    socket_owner, socket_mode)
             except ValueError, e:
                 raise ValueError('%s in [%s] socket' % (str(e), section))
             
@@ -669,7 +689,7 @@
         groups.sort()
         return groups
 
-    def parse_fcgi_socket(self, sock):
+    def parse_fcgi_socket(self, sock, proc_uid, socket_owner, socket_mode):
         if sock.startswith('unix://'):
             path = sock[7:]
             #Check it's an absolute path
@@ -677,8 +697,22 @@
                 raise ValueError("Unix socket path %s is not an absolute path",
                                  path)
             path = normalize_path(path)
-            return UnixStreamSocketConfig(path)
+            
+            if socket_owner is None:
+                uid = os.getuid()
+                if proc_uid is not None and proc_uid != uid:
+                    socket_owner = (proc_uid, self.get_gid_for_uid(proc_uid))
+                    
+            if socket_mode is None:
+                socket_mode = 0700
+            
+            return UnixStreamSocketConfig(path, owner=socket_owner,
+                                                mode=socket_mode)
         
+        if socket_owner is not None or socket_mode is not None:
+            raise ValueError("socket_owner and socket_mode params should"
+                    + " only be used with a Unix domain socket")
+        
         m = re.match(r'tcp://([^\s:]+):(\d+)$', sock)
         if m:
             host = m.group(1)
@@ -686,6 +720,11 @@
             return InetStreamSocketConfig(host, port)
         
         raise ValueError("Bad socket format %s", sock)
+        
+    def get_gid_for_uid(self, uid):
+        import pwd
+        pwrec = pwd.getpwuid(uid)
+        return pwrec[3]
 
     def processes_from_section(self, parser, section, group_name,
                                klass=None):
Index: src/supervisor/datatypes.py
===================================================================
--- src/supervisor/datatypes.py (revision 898)
+++ src/supervisor/datatypes.py (working copy)
@@ -202,7 +202,7 @@
     def addr(self): # pragma: no cover
         raise NotImplementedError
         
-    def create(self): # pragma: no cover
+    def create_and_bind(self): # pragma: no cover
         raise NotImplementedError
 
 class InetStreamSocketConfig(SocketConfig):
@@ -219,28 +219,65 @@
     def addr(self):
         return (self.host, self.port)
         
-    def create(self):
+    def create_and_bind(self):
         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        sock.bind(self.addr())
         return sock
         
 class UnixStreamSocketConfig(SocketConfig):
     """ Unix domain socket config helper """
 
     path = None # Unix domain socket path
+    mode = None # Unix permission mode bits for socket
+    owner = None # Tuple (uid, gid) for Unix ownership of socket
+    sock = None # socket object
     
-    def __init__(self, path):
+    def __init__(self, path, **kwargs):
         self.path = path
         self.url = 'unix://%s' % (path)
+        self.mode = kwargs.get('mode', None)
+        self.owner = kwargs.get('owner', None)
         
     def addr(self):
         return self.path
         
-    def create(self):
+    def create_and_bind(self):
         if os.path.exists(self.path):
             os.unlink(self.path)
         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        sock.bind(self.addr())
+        try:
+            self._chown()
+            self._chmod()
+        except:
+            sock.close()
+            os.unlink(self.path)
+            raise
         return sock
+        
+    def get_mode(self):
+        return self.mode
+        
+    def get_owner(self):
+        return self.owner
+        
+    def _chmod(self):
+        if self.mode is not None:
+            try:
+                os.chmod(self.path, self.mode)
+            except Exception, e:
+                raise ValueError("Could not change permissions of socket "
+                                    + "file: %s" % (e))
+        
+    def _chown(self):
+        if self.owner is not None:
+            try:
+                os.chown(self.path, self.owner[0], self.owner[1])
+            except Exception, e:
+                raise ValueError("Could not change ownership of socket file: "
+                                    + "%s" % (e))
+                
 
 def colon_separated_user_group(arg):
     try:
Index: src/supervisor/tests/test_options.py
===================================================================
--- src/supervisor/tests/test_options.py        (revision 898)
+++ src/supervisor/tests/test_options.py        (working copy)
@@ -8,6 +8,7 @@
 import signal
 import shutil
 import errno
+from mock import Mock, patch, sentinel
 
 from supervisor.tests.base import DummySupervisor
 from supervisor.tests.base import DummyLogger
@@ -784,49 +785,93 @@
         config.read_string(text)
         instance = self._makeOne()
         
self.assertRaises(ValueError,instance.process_groups_from_parser,config)
-
+    
     def test_fcgi_programs_from_parser(self):
         from supervisor.options import FastCGIGroupConfig
         from supervisor.options import FastCGIProcessConfig
         text = lstrip("""\
         [fcgi-program:foo]
-        socket=unix:///tmp/%(program_name)s.sock
+        socket = unix:///tmp/%(program_name)s.sock
+        socket_owner = testuser:testgroup
+        socket_mode = 0666
         process_name = %(program_name)s_%(process_num)s
         command = /bin/foo
         numprocs = 2
         priority = 1
 
         [fcgi-program:bar]
-        socket=tcp://localhost:6000
+        socket = unix:///tmp/%(program_name)s.sock
         process_name = %(program_name)s_%(process_num)s
         command = /bin/bar
+        user = testuser
         numprocs = 3
+        
+        [fcgi-program:flub]
+        socket = unix:///tmp/%(program_name)s.sock
+        command = /bin/flub
+        
+        [fcgi-program:cub]
+        socket = tcp://localhost:6000
+        command = /bin/cub
         """)
         from supervisor.options import UnhosedConfigParser
         config = UnhosedConfigParser()
         config.read_string(text)
         instance = self._makeOne()
-        gconfigs = instance.process_groups_from_parser(config)
-        self.assertEqual(len(gconfigs), 2)
+        
+        #Patch pwd and grp module functions to give us sentinel
+        #uid/gid values so that the test does not depend on
+        #any specific system users
+        pwd_mock = Mock()
+        pwd_mock.return_value = (None, None, sentinel.uid, sentinel.gid)
+        grp_mock = Mock()
+        grp_mock.return_value = (None, None, sentinel.gid)
+        @patch('pwd.getpwuid', pwd_mock)
+        @patch('pwd.getpwnam', pwd_mock)
+        @patch('grp.getgrnam', grp_mock)
+        def get_process_groups(instance, config):
+            return instance.process_groups_from_parser(config)
 
-        gconfig0 = gconfigs[0]
-        self.assertEqual(gconfig0.__class__, FastCGIGroupConfig)
-        self.assertEqual(gconfig0.name, 'foo')
-        self.assertEqual(gconfig0.priority, 1)
-        self.assertEqual(gconfig0.socket_config.url,
-                         'unix:///tmp/foo.sock')
-        self.assertEqual(len(gconfig0.process_configs), 2)
-        self.assertEqual(gconfig0.process_configs[0].__class__,
-                         FastCGIProcessConfig)
-        self.assertEqual(gconfig0.process_configs[1].__class__,
-                         FastCGIProcessConfig)
+        gconfigs = get_process_groups(instance, config)
+        exp_owner = (sentinel.uid, sentinel.gid)
+
+        self.assertEqual(len(gconfigs), 4)
+
+        gconf_foo = gconfigs[0]
+        self.assertEqual(gconf_foo.__class__, FastCGIGroupConfig)
+        self.assertEqual(gconf_foo.name, 'foo')
+        self.assertEqual(gconf_foo.priority, 1)
+        self.assertEqual(gconf_foo.socket_config.url,
+                                'unix:///tmp/foo.sock')
+        self.assertEqual(exp_owner, gconf_foo.socket_config.get_owner())
+        self.assertEqual(0666, gconf_foo.socket_config.get_mode())
+        self.assertEqual(len(gconf_foo.process_configs), 2)
+        pconfig_foo = gconf_foo.process_configs[0]
+        self.assertEqual(pconfig_foo.__class__, FastCGIProcessConfig)
         
-        gconfig1 = gconfigs[1]
-        self.assertEqual(gconfig1.name, 'bar')
-        self.assertEqual(gconfig1.priority, 999)
-        self.assertEqual(gconfig1.socket_config.url,
+        gconf_bar = gconfigs[2]
+        self.assertEqual(gconf_bar.name, 'bar')
+        self.assertEqual(gconf_bar.priority, 999)
+        self.assertEqual(gconf_bar.socket_config.url,
+                         'unix:///tmp/bar.sock')
+        self.assertEqual(exp_owner, gconf_bar.socket_config.get_owner())
+        self.assertEqual(0700, gconf_bar.socket_config.get_mode())
+        self.assertEqual(len(gconf_bar.process_configs), 3)
+        
+        gconf_flub = gconfigs[1]
+        self.assertEqual(gconf_flub.name, 'flub')
+        self.assertEqual(gconf_flub.socket_config.url,
+                         'unix:///tmp/flub.sock')
+        self.assertEqual(None, gconf_flub.socket_config.get_owner())
+        self.assertEqual(0700, gconf_flub.socket_config.get_mode())
+        self.assertEqual(len(gconf_flub.process_configs), 1)
+        
+        gconf_cub = gconfigs[3]
+        self.assertEqual(gconf_cub.name, 'cub')
+        self.assertEqual(gconf_cub.socket_config.url,
                          'tcp://localhost:6000')
-        self.assertEqual(len(gconfig1.process_configs), 3)
+        self.assertEqual(len(gconf_cub.process_configs), 1)
+        
 
     def test_fcgi_program_no_socket(self):
         text = lstrip("""\
@@ -901,6 +946,58 @@
         config.read_string(text)
         instance = self._makeOne()
         
self.assertRaises(ValueError,instance.process_groups_from_parser,config)
+
+    def test_fcgi_program_socket_owner_set_for_tcp(self):
+        text = lstrip("""\
+        [fcgi-program:foo]
+        socket=tcp://localhost:8000
+        socket_owner=nobody:nobody
+        command = /bin/foo
+        """)
+        from supervisor.options import UnhosedConfigParser
+        config = UnhosedConfigParser()
+        config.read_string(text)
+        instance = self._makeOne()
+        
self.assertRaises(ValueError,instance.process_groups_from_parser,config)
+
+    def test_fcgi_program_socket_mode_set_for_tcp(self):
+        text = lstrip("""\
+        [fcgi-program:foo]
+        socket = tcp://localhost:8000
+        socket_mode = 0777
+        command = /bin/foo
+        """)
+        from supervisor.options import UnhosedConfigParser
+        config = UnhosedConfigParser()
+        config.read_string(text)
+        instance = self._makeOne()
+        
self.assertRaises(ValueError,instance.process_groups_from_parser,config)
+
+    def test_fcgi_program_bad_socket_owner(self):
+        text = lstrip("""\
+        [fcgi-program:foo]
+        socket = unix:///tmp/foo.sock
+        socket_owner = sometotaljunkuserthatshouldnobethere
+        command = /bin/foo
+        """)
+        from supervisor.options import UnhosedConfigParser
+        config = UnhosedConfigParser()
+        config.read_string(text)
+        instance = self._makeOne()
+        
self.assertRaises(ValueError,instance.process_groups_from_parser,config)
+
+    def test_fcgi_program_bad_socket_mode(self):
+        text = lstrip("""\
+        [fcgi-program:foo]
+        socket = unix:///tmp/foo.sock
+        socket_mode = junk
+        command = /bin/foo
+        """)
+        from supervisor.options import UnhosedConfigParser
+        config = UnhosedConfigParser()
+        config.read_string(text)
+        instance = self._makeOne()
+        
self.assertRaises(ValueError,instance.process_groups_from_parser,config)
     
     def test_heterogeneous_process_groups_from_parser(self):
         text = lstrip("""\
Index: src/supervisor/tests/test_datatypes.py
===================================================================
--- src/supervisor/tests/test_datatypes.py      (revision 898)
+++ src/supervisor/tests/test_datatypes.py      (working copy)
@@ -5,6 +5,7 @@
 import unittest
 import socket
 import tempfile
+from mock import Mock, patch, sentinel
 from supervisor import datatypes
 
 class DatatypesTest(unittest.TestCase):
@@ -176,7 +177,7 @@
     def test_url_rejects_urlparse_unrecognized_scheme_with_path(self):
         bad_url = "bad://path"
         self.assertRaises(ValueError, datatypes.url, bad_url)
- 
+
 class InetStreamSocketConfigTests(unittest.TestCase):
     def _getTargetClass(self):
         return datatypes.InetStreamSocketConfig
@@ -187,6 +188,10 @@
     def test_url(self):
         conf = self._makeOne('127.0.0.1', 8675)
         self.assertEqual(conf.url, 'tcp://127.0.0.1:8675')
+
+    def test___str__(self):
+        cfg = self._makeOne('localhost', 65531)
+        self.assertEqual(str(cfg), 'tcp://localhost:65531')
                 
     def test_repr(self):
         conf = self._makeOne('127.0.0.1', 8675)
@@ -205,32 +210,32 @@
         addr = conf.addr()
         self.assertEqual(addr, ('localhost', 5001))
         
-    def test_create(self):
+    def test_create_and_bind(self):
         conf = self._makeOne('127.0.0.1', 8675)
-        sock = conf.create()
+        sock = conf.create_and_bind()
         reuse = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
         self.assertTrue(reuse)
-        sock.close
+        self.assertEquals(conf.addr(), sock.getsockname()) #verifies that bind 
was called
+        sock.close()
         
     def test_same_urls_are_equal(self):
-        conf1 = self._makeOne('localhost', '5001')
-        conf2 = self._makeOne('localhost', '5001')
+        conf1 = self._makeOne('localhost', 5001)
+        conf2 = self._makeOne('localhost', 5001)
         self.assertTrue(conf1 == conf2)
         self.assertFalse(conf1 != conf2)
 
     def test_diff_urls_are_not_equal(self):
-        conf1 = self._makeOne('localhost', '5001')
-        conf2 = self._makeOne('localhost', '5002')
+        conf1 = self._makeOne('localhost', 5001)
+        conf2 = self._makeOne('localhost', 5002)
         self.assertTrue(conf1 != conf2)
         self.assertFalse(conf1 == conf2)
 
     def test_diff_objs_are_not_equal(self):
-        conf1 = self._makeOne('localhost', '5001')
+        conf1 = self._makeOne('localhost', 5001)
         conf2 = 'blah'
         self.assertTrue(conf1 != conf2)
-        self.assertFalse(conf1 == conf2)    
-        
-        
+        self.assertFalse(conf1 == conf2) 
+              
 class UnixStreamSocketConfigTests(unittest.TestCase):
     def _getTargetClass(self):
         return datatypes.UnixStreamSocketConfig
@@ -241,6 +246,10 @@
     def test_url(self):
         conf = self._makeOne('/tmp/foo.sock')
         self.assertEqual(conf.url, 'unix:///tmp/foo.sock')
+        
+    def test___str__(self):
+        cfg = self._makeOne('foo/bar')
+        self.assertEqual(str(cfg), 'unix://foo/bar')
             
     def test_repr(self):
         conf = self._makeOne('/tmp/foo.sock')
@@ -254,14 +263,43 @@
         addr = conf.addr()
         self.assertEqual(addr, '/tmp/foo.sock')
         
-    def test_create(self):
+    def test_create_and_bind(self):
         (tf_fd, tf_name) = tempfile.mkstemp()
-        conf = self._makeOne(tf_name)
-        os.close(tf_fd)
-        sock = conf.create()
-        self.assertFalse(os.path.exists(tf_name))
-        sock.close
+        owner = (sentinel.uid, sentinel.gid)
+        mode = sentinel.mode
+        conf = self._makeOne(tf_name, owner=owner, mode=mode)
         
+        #Patch os.chmod and os.chown functions with mocks
+        #objects so that the test does not depend on
+        #any specific system users or permissions
+        chown_mock = Mock()
+        chmod_mock = Mock()
+        @patch('os.chown', chown_mock)
+        @patch('os.chmod', chmod_mock)
+        def call_create_and_bind(conf):
+            return conf.create_and_bind()
+        
+        sock = call_create_and_bind(conf)
+        self.assertTrue(os.path.exists(tf_name))
+        self.assertEquals(conf.addr(), sock.getsockname()) #verifies that bind 
was called
+        sock.close()
+        self.assertTrue(os.path.exists(tf_name))
+        os.unlink(tf_name)
+        #Verify that os.chown was called with correct args
+        self.assertEquals(1, chown_mock.call_count)
+        path_arg = chown_mock.call_args[0][0]
+        uid_arg = chown_mock.call_args[0][1]
+        gid_arg = chown_mock.call_args[0][2]
+        self.assertEquals(tf_name, path_arg)
+        self.assertEquals(owner[0], uid_arg)
+        self.assertEquals(owner[1], gid_arg)
+        #Verify that os.chmod was called with correct args
+        self.assertEquals(1, chmod_mock.call_count)
+        path_arg = chmod_mock.call_args[0][0]
+        mode_arg = chmod_mock.call_args[0][1]
+        self.assertEquals(tf_name, path_arg)
+        self.assertEquals(mode, mode_arg)
+        
     def test_same_paths_are_equal(self):
         conf1 = self._makeOne('/tmp/foo.sock')
         conf2 = self._makeOne('/tmp/foo.sock')
@@ -278,8 +316,8 @@
         conf1 = self._makeOne('/tmp/foo.sock')
         conf2 = 'blah'
         self.assertTrue(conf1 != conf2)
-        self.assertFalse(conf1 == conf2)    
-
+        self.assertFalse(conf1 == conf2) 
+        
 class RangeCheckedConversionTests(unittest.TestCase):
     def _getTargetClass(self):
         from supervisor.datatypes import RangeCheckedConversion
@@ -341,72 +379,6 @@
         self.assertEqual(addr.family, socket.AF_INET)
         self.assertEqual(addr.address, ('localhost', 8080))
         
-class TestInetStreamSocketConfig(unittest.TestCase):
-    def _getTargetClass(self):
-        from supervisor.datatypes import InetStreamSocketConfig
-        return InetStreamSocketConfig
-        
-    def _makeOne(self, host, port):
-        return self._getTargetClass()(host, port)
-
-    def test_addr(self):
-        cfg = self._makeOne('localhost', 8080)
-        self.assertEqual(cfg.addr(), ('localhost', 8080))
-        
-    def test_create(self):
-        cfg = self._makeOne('localhost', 65531)
-        sock = cfg.create()
-        sock.close()
-
-    def test___str__(self):
-        cfg = self._makeOne('localhost', 65531)
-        self.assertEqual(str(cfg), 'tcp://localhost:65531')
-
-    def test__eq__(self):
-        cfg = self._makeOne('localhost', 65531)
-        cfg2 = self._makeOne('localhost', 65531)
-        self.failUnless(cfg == cfg2)
-        
-    def test__ne__(self):
-        cfg = self._makeOne('localhost', 65531)
-        cfg2 = self._makeOne('localhost', 65532)
-        self.failUnless(cfg != cfg2)
-        
-class TestUnixStreamSocketConfig(unittest.TestCase):
-    def _getTargetClass(self):
-        from supervisor.datatypes import UnixStreamSocketConfig
-        return UnixStreamSocketConfig
-        
-    def _makeOne(self, path):
-        return self._getTargetClass()(path)
-
-    def test_addr(self):
-        cfg = self._makeOne('/foo/bar')
-        self.assertEqual(cfg.addr(), '/foo/bar')
-        
-    def test_create(self):
-        import tempfile
-        fn = tempfile.mktemp()
-        cfg = self._makeOne(fn)
-        sock = cfg.create()
-        sock.close()
-        if os.path.exists(fn):
-            os.unlink(fn)
-
-    def test___str__(self):
-        cfg = self._makeOne('foo/bar')
-        self.assertEqual(str(cfg), 'unix://foo/bar')
-
-    def test__eq__(self):
-        cfg = self._makeOne('/foo/bar')
-        cfg2 = self._makeOne('/foo/bar')
-        self.failUnless(cfg == cfg2)
-        
-    def test__ne__(self):
-        cfg = self._makeOne('/foo/bar')
-        cfg2 = self._makeOne('/foo/bar2')
-        self.failUnless(cfg != cfg2)
-        
 class TestColonSeparatedUserGroup(unittest.TestCase):
     def _callFUT(self, arg):
         from supervisor.datatypes import colon_separated_user_group
Index: src/supervisor/tests/base.py
===================================================================
--- src/supervisor/tests/base.py        (revision 898)
+++ src/supervisor/tests/base.py        (working copy)
@@ -333,7 +333,7 @@
     def __ne__(self, other):
         return not self.__eq__(other)
 
-    def create(self):
+    def create_and_bind(self):
         return DummySocket(self.fd)
 
 class DummySocketManager:
Index: src/supervisor/tests/test_process.py
===================================================================
--- src/supervisor/tests/test_process.py        (revision 898)
+++ src/supervisor/tests/test_process.py        (working copy)
@@ -4,6 +4,7 @@
 import unittest
 import sys
 import errno
+from mock import Mock, patch_object, sentinel
 
 from supervisor.tests.base import DummyOptions
 from supervisor.tests.base import DummyPConfig
@@ -17,6 +18,8 @@
 from supervisor.tests.base import DummyFCGIProcessGroup
 from supervisor.tests.base import DummySocketManager
 
+from supervisor.process import Subprocess
+
 class SubprocessTests(unittest.TestCase):
     def _getTargetClass(self):
         from supervisor.process import Subprocess
@@ -1234,7 +1237,61 @@
         instance.fcgi_sock = 'hello'
         instance.after_finish()
         self.assertTrue(instance.fcgi_sock is None)
+    
+    #Patch Subprocess.finish() method for this test to verify override
+    @patch_object(Subprocess, 'finish', 
Mock(return_value=sentinel.finish_result))
+    def test_finish_override(self):
+        options = DummyOptions()
+        config = DummyPConfig(options, 'good', '/good/filename', uid=1)
+        instance = self._makeOne(config)
+        instance.after_finish = Mock()
+        result = instance.finish(sentinel.pid, sentinel.sts)
+        self.assertEqual(sentinel.finish_result, result,
+                        'FastCGISubprocess.finish() did not pass thru result')
+        self.assertEqual(1, instance.after_finish.call_count,
+                            'FastCGISubprocess.after_finish() not called once')
+        finish_mock = Subprocess.finish
+        self.assertEqual(1, finish_mock.call_count,
+                            'Subprocess.finish() not called once')
+        pid_arg = finish_mock.call_args[0][1]
+        sts_arg = finish_mock.call_args[0][2]
+        self.assertEqual(sentinel.pid, pid_arg,
+                            'Subprocess.finish() pid arg was not passed')
+        self.assertEqual(sentinel.sts, sts_arg,
+                            'Subprocess.finish() sts arg was not passed')
+                            
+    #Patch Subprocess.spawn() method for this test to verify override
+    @patch_object(Subprocess, 'spawn', Mock(return_value=sentinel.ppid))
+    def test_spawn_override_success(self):
+        options = DummyOptions()
+        config = DummyPConfig(options, 'good', '/good/filename', uid=1)
+        instance = self._makeOne(config)
+        instance.before_spawn = Mock()
+        result = instance.spawn()
+        self.assertEqual(sentinel.ppid, result,
+                        'FastCGISubprocess.spawn() did not pass thru result')
+        self.assertEqual(1, instance.before_spawn.call_count,
+                            'FastCGISubprocess.before_spawn() not called once')
+        spawn_mock = Subprocess.spawn
+        self.assertEqual(1, spawn_mock.call_count,
+                            'Subprocess.spawn() not called once')
 
+    #Patch Subprocess.spawn() method for this test to verify error handling
+    @patch_object(Subprocess, 'spawn', Mock(return_value=None))
+    def test_spawn_error(self):
+        options = DummyOptions()
+        config = DummyPConfig(options, 'good', '/good/filename', uid=1)
+        instance = self._makeOne(config)
+        instance.before_spawn = Mock()
+        instance.fcgi_sock = 'nuke me on error'
+        result = instance.spawn()
+        self.assertEqual(None, result,
+                        'FastCGISubprocess.spawn() did return None on error')
+        self.assertEqual(1, instance.before_spawn.call_count,
+                            'FastCGISubprocess.before_spawn() not called once')
+        self.assertEqual(None, instance.fcgi_sock,
+                'FastCGISubprocess.spawn() did not remove sock ref on error')  
  
+
 class ProcessGroupBaseTests(unittest.TestCase):
     def _getTargetClass(self):
         from supervisor.process import ProcessGroupBase
Index: src/supervisor/tests/test_socket_manager.py
===================================================================
--- src/supervisor/tests/test_socket_manager.py (revision 898)
+++ src/supervisor/tests/test_socket_manager.py (working copy)
@@ -174,8 +174,7 @@
         sock_manager = self._makeOne(conf)
         sock = sock_manager.get_socket()
         self.assertTrue(sock_manager.is_prepared())
-        self.assertTrue(sock.bind_called)
-        self.assertEqual(sock.bind_addr, 'dummy addr')
+        self.assertFalse(sock.bind_called)
         self.assertTrue(sock.listen_called)
         self.assertEqual(sock.listen_backlog, socket.SOMAXCONN)
         self.assertFalse(sock.close_called)
Index: src/supervisor/process.py
===================================================================
--- src/supervisor/process.py   (revision 898)
+++ src/supervisor/process.py   (working copy)
@@ -579,7 +579,11 @@
         Overrides Subprocess.spawn() so we can hook in before it happens
         """
         self.before_spawn()
-        Subprocess.spawn(self)
+        pid = Subprocess.spawn(self)
+        if pid is None:
+            #Remove object reference to decrement the reference count on error
+            self.fcgi_sock = None
+        return pid
         
     def after_finish(self):
         """
@@ -592,8 +596,9 @@
         """
         Overrides Subprocess.finish() so we can hook in after it happens
         """
-        Subprocess.finish(self, pid, sts)
+        retval = Subprocess.finish(self, pid, sts)
         self.after_finish()
+        return retval
 
     def _prepare_child_fds(self):
         """
Index: src/supervisor/socket_manager.py
===================================================================
--- src/supervisor/socket_manager.py    (revision 898)
+++ src/supervisor/socket_manager.py    (working copy)
@@ -107,8 +107,7 @@
         if not self.prepared:
             if self.logger:
                 self.logger.info('Creating socket %s' % self.socket_config)
-            self.socket = self.socket_config.create()
-            self.socket.bind(self.socket_config.addr())
+            self.socket = self.socket_config.create_and_bind()
             self.socket.listen(socket.SOMAXCONN)
             self.prepared = True
     
Index: setup.py
===================================================================
--- setup.py    (revision 898)
+++ setup.py    (working copy)
@@ -87,7 +87,7 @@
     )],
     install_requires = requires,
     extras_require = {'iterparse':['cElementTree >= 1.0.2']},
-    tests_require = requires,
+    tests_require = requires + ['mock >= 0.5.0'],
     include_package_data = True,
     zip_safe = False,
     namespace_packages = ['supervisor'],
_______________________________________________
Supervisor-users mailing list
[email protected]
http://lists.supervisord.org/mailman/listinfo/supervisor-users

Reply via email to