Browse Source

- Added 'directory' option to process config. If you set this
option, supervisor will chdir to this directory before executing
the child program (and thus it will be the child's cwd).

Chris McDonough 18 years ago
parent
commit
40e15ceeb8
8 changed files with 131 additions and 58 deletions
  1. 4 0
      CHANGES.txt
  2. 4 0
      README.txt
  3. 0 2
      TODO.txt
  4. 2 1
      sample.conf
  5. 5 2
      src/supervisor/options.py
  6. 68 52
      src/supervisor/process.py
  7. 9 1
      src/supervisor/tests/base.py
  8. 39 0
      src/supervisor/tests/test_process.py

+ 4 - 0
CHANGES.txt

@@ -143,6 +143,10 @@ Next Release
     match short processnames, fullly-specified group:process names,
     and groupsplats on the same line for any of these commands.
 
+  - Added 'directory' option to process config.  If you set this
+    option, supervisor will chdir to this directory before executing
+    the child program (and thus it will be the child's cwd).
+
 3.0a2
 
   - Fixed the README.txt example for defining the supervisor RPC

+ 4 - 0
README.txt

@@ -551,6 +551,10 @@ Configuration File '[program:x]' Section Settings
   of the shell used to start "supervisord" except for the ones
   overridden here.  See "Subprocess Environment" below.
 
+  'directory' -- a file path representing a directory to which
+  supervisord should chdir before exec'ing the child.  Default: no
+  cwd.
+
   Note that a '[program:x]' section actually represents a "homogeneous
   process group" to supervisor (new in 3.0).  The members of the group
   are defined by the combination of the 'numprocs and 'process_name'

+ 0 - 2
TODO.txt

@@ -10,8 +10,6 @@
     h.getfile().  See
     http://mail.python.org/pipermail/patches/2002-February/007375.html
 
-- Add a new cwd option that will chdir after the fork-exec.
-
 - Fix CVS so not all checkins come from "chrism".
 
 - FATAL state for supervisor.

+ 2 - 1
sample.conf

@@ -61,7 +61,8 @@ serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
 ;stderr_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
 ;stderr_logfile_backups=10     ; # of stderr logfile backups (default 10)
 ;stderr_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)
-;environment=A=1,B=2           ; process environment additions
+;environment=A=1,B=2           ; process environment additions (def no adds)
+;directory=/tmp                ; directory to cwd to before exec (def no cwd)
 
 ; The below sample eventlistener section shows all possible
 ; eventlistener subsection values, create one or more 'real'

+ 5 - 2
src/supervisor/options.py

@@ -653,6 +653,7 @@ class ServerOptions(Options):
         environment_str = get(section, 'environment', '')
         stdout_cmaxbytes = byte_size(get(section,'stdout_capture_maxbytes','0'))
         stderr_cmaxbytes = byte_size(get(section,'stderr_capture_maxbytes','0'))
+        directory = get(section, 'directory', None)
 
         command = get(section, 'command', None)
         if command is None:
@@ -726,7 +727,8 @@ class ServerOptions(Options):
                 stopwaitsecs=stopwaitsecs,
                 exitcodes=exitcodes,
                 redirect_stderr=redirect_stderr,
-                environment=environment)
+                environment=environment,
+                directory=directory)
 
             programs.append(pconfig)
 
@@ -1319,7 +1321,7 @@ class ProcessConfig(Config):
                  stderr_logfile, stderr_capture_maxbytes,
                  stderr_logfile_backups, stderr_logfile_maxbytes,
                  stopsignal, stopwaitsecs, exitcodes, redirect_stderr,
-                 environment=None):
+                 environment=None, directory=None):
         self.options = options
         self.name = name
         self.command = command
@@ -1342,6 +1344,7 @@ class ProcessConfig(Config):
         self.exitcodes = exitcodes
         self.redirect_stderr = redirect_stderr
         self.environment = environment
+        self.directory = directory
 
     def create_autochildlogs(self):
         # temporary logfiles which are erased at start time

+ 68 - 52
src/supervisor/process.py

@@ -170,11 +170,10 @@ class Subprocess:
 
         Return the process id.  If the fork() call fails, return None.
         """
-        pname = self.config.name
         options = self.config.options
 
         if self.pid:
-            msg = 'process %r already running' % pname
+            msg = 'process %r already running' % self.config.name
             options.logger.warn(msg)
             return
 
@@ -205,7 +204,7 @@ class Subprocess:
             code = why[0]
             if code == errno.EMFILE:
                 # too many file descriptors open
-                msg = 'too many open files to spawn %r' % pname
+                msg = 'too many open files to spawn %r' % self.config.name
             else:
                 msg = 'unknown error: %s' % errno.errorcode.get(code, code)
             self.record_spawnerr(msg)
@@ -219,7 +218,8 @@ class Subprocess:
             code = why[0]
             if code == errno.EAGAIN:
                 # process table full
-                msg  = 'Too many processes in process table to spawn %r' % pname
+                msg  = ('Too many processes in process table to spawn %r' %
+                        self.config.name)
             else:
                 msg = 'unknown error: %s' % errno.errorcode.get(code, code)
 
@@ -231,65 +231,81 @@ class Subprocess:
             return
 
         if pid != 0:
-            # Parent
-            self.pid = pid
-            options.close_child_pipes(self.pipes)
-            options.logger.info('spawned: %r with pid %s' % (pname, pid))
-            self.spawnerr = None
-            self.delay = time.time() + self.config.startsecs
-            options.pidhistory[pid] = self
-            return pid
+            return self._spawn_as_parent(pid)
         
         else:
-            # Child
+            return self._spawn_as_child(filename, argv)
+
+    def _spawn_as_parent(self, pid):
+        # Parent
+        self.pid = pid
+        options = self.config.options
+        options.close_child_pipes(self.pipes)
+        options.logger.info('spawned: %r with pid %s' % (self.config.name, pid))
+        self.spawnerr = None
+        self.delay = time.time() + self.config.startsecs
+        options.pidhistory[pid] = self
+        return pid
+
+    def _spawn_as_child(self, filename, argv):
+        options = self.config.options
+        try:
+            # prevent child from receiving signals sent to the
+            # parent by calling os.setpgrp to create a new process
+            # group for the child; this prevents, for instance,
+            # the case of child processes being sent a SIGINT when
+            # running supervisor in foreground mode and Ctrl-C in
+            # the terminal window running supervisord is pressed.
+            # Presumably it also prevents HUP, etc received by
+            # supervisord from being sent to children.
+            options.setpgrp()
+            options.dup2(self.pipes['child_stdin'], 0)
+            options.dup2(self.pipes['child_stdout'], 1)
+            if self.config.redirect_stderr:
+                options.dup2(self.pipes['child_stdout'], 2)
+            else:
+                options.dup2(self.pipes['child_stderr'], 2)
+            for i in range(3, options.minfds):
+                options.close_fd(i)
+            # sending to fd 2 will put this output in the stderr log
+            msg = self.set_uid()
+            if msg:
+                uid = self.config.uid
+                s = 'supervisor: error trying to setuid to %s ' % uid
+                options.write(2, s)
+                options.write(2, "(%s)\n" % msg)
+            env = os.environ.copy()
+            env['SUPERVISOR_ENABLED'] = '1'
+            serverurl = self.config.options.serverurl
+            if serverurl:
+                env['SUPERVISOR_SERVER_URL'] = serverurl
+            env['SUPERVISOR_PROCESS_NAME'] = self.config.name
+            if self.group:
+                env['SUPERVISOR_GROUP_NAME'] = self.group.config.name
+            if self.config.environment is not None:
+                env.update(self.config.environment)
             try:
-                # prevent child from receiving signals sent to the
-                # parent by calling os.setpgrp to create a new process
-                # group for the child; this prevents, for instance,
-                # the case of child processes being sent a SIGINT when
-                # running supervisor in foreground mode and Ctrl-C in
-                # the terminal window running supervisord is pressed.
-                # Presumably it also prevents HUP, etc received by
-                # supervisord from being sent to children.
-                options.setpgrp()
-                options.dup2(self.pipes['child_stdin'], 0)
-                options.dup2(self.pipes['child_stdout'], 1)
-                if self.config.redirect_stderr:
-                    options.dup2(self.pipes['child_stdout'], 2)
-                else:
-                    options.dup2(self.pipes['child_stderr'], 2)
-                for i in range(3, options.minfds):
-                    options.close_fd(i)
-                # sending to fd 2 will put this output in the stderr log
-                msg = self.set_uid()
-                if msg:
-                    uid = self.config.uid
-                    s = 'supervisor: error trying to setuid to %s ' % uid
-                    options.write(2, s)
-                    options.write(2, "(%s)\n" % msg)
+                cwd = self.config.directory
+                if cwd is not None:
+                    options.chdir(cwd)
+            except OSError, why:
+                code = errno.errorcode.get(why[0], why[0])
+                msg = "couldn't chdir to %s: %s\n" % (cwd, code)
+                options.write(2, msg)
+            else:
                 try:
-                    env = os.environ.copy()
-                    env['SUPERVISOR_ENABLED'] = '1'
-                    serverurl = self.config.options.serverurl
-                    if serverurl:
-                        env['SUPERVISOR_SERVER_URL'] = serverurl
-                    env['SUPERVISOR_PROCESS_NAME'] = self.config.name
-                    if self.group:
-                        env['SUPERVISOR_GROUP_NAME'] = self.group.config.name
-                    if self.config.environment is not None:
-                        env.update(self.config.environment)
                     options.execve(filename, argv, env)
                 except OSError, why:
-                    code = why[0]
-                    options.write(2, "couldn't exec %s: %s\n" % (
-                        argv[0], errno.errorcode.get(code, code)))
+                    code = errno.errorcode.get(why[0], why[0])
+                    msg = "couldn't exec %s: %s\n" % (argv[0], code)
+                    options.write(2, msg)
                 except:
                     (file, fun, line), t,v,tbinfo = asyncore.compact_traceback()
                     error = '%s, %s: file: %s line: %s' % (t, v, file, line)
                     options.write(2, "couldn't exec %s: %s\n" % (filename,
                                                                  error))
-            finally:
-                options._exit(127)
+        finally:
+            options._exit(127)
 
     def stop(self):
         """ Administrative stop """

+ 9 - 1
src/supervisor/tests/base.py

@@ -65,6 +65,8 @@ class DummyOptions:
         self.readfd_result = ''
         self.parse_warnings = []
         self.serverurl = 'http://localhost:9001'
+        self.changed_directory = False
+        self.chdir_error = None
 
     def getLogger(self, *args, **kw):
         logger = DummyLogger()
@@ -232,6 +234,11 @@ class DummyOptions:
             return self.openreturn
         return open(name, mode)
 
+    def chdir(self, dir):
+        if self.chdir_error:
+            raise OSError(self.chdir_error)
+        self.changed_directory = True
+
 class DummyLogger:
     def __init__(self):
         self.reopened = False
@@ -410,7 +417,7 @@ class DummyPConfig:
                  stderr_logfile_backups=0, stderr_logfile_maxbytes=0,
                  redirect_stderr=False,
                  stopsignal=None, stopwaitsecs=10,
-                 exitcodes=(0,2), environment=None):
+                 exitcodes=(0,2), environment=None, directory=None):
         self.options = options
         self.name = name
         self.command = command
@@ -436,6 +443,7 @@ class DummyPConfig:
         self.stopwaitsecs = stopwaitsecs
         self.exitcodes = exitcodes
         self.environment = environment
+        self.directory = directory
         self.autochildlogs_created = False
 
     def create_autochildlogs(self):

+ 39 - 0
src/supervisor/tests/test_process.py

@@ -314,6 +314,45 @@ class SubprocessTests(unittest.TestCase):
                          ('/good/filename', ['/good/filename']) )
         self.assertEqual(options._exitcode, 127)
 
+    def test_spawn_as_child_cwd_ok(self):
+        options = DummyOptions()
+        options.forkpid = 0
+        config = DummyPConfig(options, 'good', '/good/filename',
+                              directory='/tmp')
+        instance = self._makeOne(config)
+        result = instance.spawn()
+        self.assertEqual(result, None)
+        self.assertEqual(options.parent_pipes_closed, None)
+        self.assertEqual(options.child_pipes_closed, None)
+        self.assertEqual(options.pgrp_set, True)
+        self.assertEqual(len(options.duped), 3)
+        self.assertEqual(len(options.fds_closed), options.minfds - 3)
+        self.assertEqual(options.written, {})
+        self.assertEqual(options.execv_args,
+                         ('/good/filename', ['/good/filename']) )
+        self.assertEqual(options._exitcode, 127)
+        self.assertEqual(options.changed_directory, True)
+
+    def test_spawn_as_child_cwd_fail(self):
+        options = DummyOptions()
+        options.forkpid = 0
+        options.chdir_error = 2
+        config = DummyPConfig(options, 'good', '/good/filename',
+                              directory='/tmp')
+        instance = self._makeOne(config)
+        result = instance.spawn()
+        self.assertEqual(result, None)
+        self.assertEqual(options.parent_pipes_closed, None)
+        self.assertEqual(options.child_pipes_closed, None)
+        self.assertEqual(options.pgrp_set, True)
+        self.assertEqual(len(options.duped), 3)
+        self.assertEqual(len(options.fds_closed), options.minfds - 3)
+        self.assertEqual(options.execv_args, None)
+        self.assertEqual(options.written,
+                         {2: "couldn't chdir to /tmp: ENOENT\n"})
+        self.assertEqual(options._exitcode, 127)
+        self.assertEqual(options.changed_directory, False)
+
     def test_spawn_as_child_execv_fail_oserror(self):
         options = DummyOptions()
         options.forkpid = 0