Преглед изворни кода

Add ability to send arbitrary UNIX signals to processes:
* Extend the Process class to handle non-killing signals
* Expose this via the XML-RPC API

Casey Callendrello пре 12 година
родитељ
комит
4a4dffbb05

+ 4 - 0
docs/api.rst

@@ -305,6 +305,10 @@ Process Control
 
     .. automethod:: stopAllProcesses
 
+    .. automethod:: sendProcessSignal
+
+    .. automethod:: sendGroupSignal
+
     .. automethod:: sendProcessStdin
 
     .. automethod:: sendRemoteCommEvent

+ 39 - 0
supervisor/process.py

@@ -439,6 +439,45 @@ class Subprocess:
 
         return None
 
+    def signal(self, sig):
+        """Send a signal to the subprocess, without intending to kill it.
+
+        Return None if the signal was sent, or an error message string
+        if an error occurred or if the subprocess is not running.
+        """
+        options = self.config.options
+        if not self.pid:
+            msg = ("attempted to send %s sig %s but it wasn't running" %
+                   (self.config.name, signame(sig)))
+            options.logger.debug(msg)
+            return msg
+
+        options.logger.debug('sending %s (pid %s) sig %s'
+                             % (self.config.name,
+                                self.pid,
+                                signame(sig))
+                             )
+
+        self._assertInState(ProcessStates.RUNNING,ProcessStates.STARTING,
+                            ProcessStates.STOPPING)
+
+        try:
+            options.kill(self.pid, sig)
+        except Exception, e:
+            io = StringIO.StringIO()
+            traceback.print_exc(file=io)
+            tb = io.getvalue()
+            msg = 'unknown problem sending sig %s (%s):%s' % (
+                                self.config.name, self.pid, tb)
+            options.logger.critical(msg)
+            self.change_state(ProcessStates.UNKNOWN)
+            self.pid = 0
+            return msg
+
+        return None
+
+
+
     def finish(self, pid, sts):
         """ The process was reaped and we need to report and manage its state
         """

+ 61 - 0
supervisor/rpcinterface.py

@@ -3,6 +3,7 @@ import time
 import datetime
 import errno
 import types
+import signal
 
 from supervisor.options import readFile
 from supervisor.options import tailFile
@@ -458,6 +459,66 @@ class SupervisorNamespaceRPCInterface:
         killall.rpcinterface = self
         return killall # deferred
 
+
+    def sendProcessSignal(self, name, signal = signal.SIGHUP):
+        """ Send an arbitrary UNIX signal to the process named by name
+
+        @param string name The name of the process to signal (or 'group:name')
+        @param int signal the integer UNIX signal to send. SIGHUP by default.
+        @return boolean result
+        """
+
+        self._update('sendProcessSignal')
+
+        group, process = self._getGroupAndProcess(name)
+
+        if process is None:
+            group_name, process_name = split_namespec(name)
+            return self.sendGroupSignal(group_name, signal = signal)
+
+        if process.get_state() not in RUNNING_STATES:
+           raise RPCError(Faults.NOT_RUNNING)
+
+        msg = process.signal(signal)
+
+        if not msg is None:
+            raise RPCError(Faults.FAILED, msg)
+
+        cb = lambda: True
+        cb.delay = 0
+        cb.rpcinterface = self
+
+        return cb
+
+
+    def sendGroupSignal(self, name, signal = signal.SIGHUP):
+        """ Send a signal to all processes in the group named 'name'
+
+        @param string name  The group name
+        @param int signal   The signal to be sent. SIGHUP by default
+        @return array result
+        """
+
+        self._update('sendGroupSignal')
+
+        group = self.supervisord.process_groups.get(name)
+        
+        if group is None:
+            raise RPCError(Faults.BAD_NAME, name)
+
+        processes = group.processes.values()
+        processes.sort()
+        processes = [(group, process) for process in processes]
+
+        sendall = make_allfunc(processes, isRunning, self.sendProcessSignal,
+                               signal = signal)
+        sendall.rpcinterface = self
+
+        sendall.delay = 0
+        sendall.rpcinterface = self
+        return sendall
+
+
     def getAllConfigInfo(self):
         """ Get info about all available process configurations. Each struct
         represents a single process (i.e. groups get flattened).

+ 5 - 0
supervisor/tests/base.py

@@ -375,6 +375,7 @@ class DummyProcess:
     stdin_buffer = '' # buffer of characters to send to child process' stdin
     listener_state = None
     group = None
+    sent_signal = None
 
     def __init__(self, config, state=None):
         self.config = config
@@ -429,6 +430,10 @@ class DummyProcess:
     def kill(self, signal):
         self.killed_with = signal
 
+    def signal(self, signal):
+        self.sent_signal = signal
+
+
     def spawn(self):
         self.spawned = True
         from supervisor.process import ProcessStates

+ 52 - 0
supervisor/tests/test_process.py

@@ -900,6 +900,58 @@ class SubprocessTests(unittest.TestCase):
         self.assertEqual(event.extra_values, [('pid', 11)])
         self.assertEqual(event.from_state, ProcessStates.RUNNING)
 
+    def test_signal(self):
+        options = DummyOptions()
+
+        killedpid = []
+        killedsig = []
+
+        def kill(pid, sig):
+            killedpid.append(pid)
+            killedsig.append(sig)
+
+        options.kill = kill
+
+        config = DummyPConfig(options, 'test', '/test')
+        instance = self._makeOne(config)
+        instance.pid = 11
+
+        from supervisor.states import ProcessStates
+        instance.state = ProcessStates.RUNNING
+
+        instance.signal(signal.SIGWINCH )
+
+        self.assertEqual(killedpid, [instance.pid,])
+        self.assertEqual(killedsig, [signal.SIGWINCH,])
+
+        self.assertEqual(options.logger.data[0], 'sending test (pid 11) sig SIGWINCH')
+
+    def test_signal_stopped(self):
+        options = DummyOptions()
+
+        killedpid = []
+        killedsig = []
+
+        def kill(pid, sig):
+            killedpid.append(pid)
+            killedsig.append(sig)
+
+        options.kill = kill #don't actually start killing random processes...
+
+        config = DummyPConfig(options, 'test', '/test')
+        instance = self._makeOne(config)
+        instance.pid = None
+
+        from supervisor.states import ProcessStates
+        instance.state = ProcessStates.STOPPED
+
+        instance.signal(signal.SIGWINCH )
+
+        self.assertEqual(options.logger.data[0], "attempted to send test sig SIGWINCH "
+                                                    "but it wasn't running")
+
+        self.assertEqual(killedpid, [])
+
     def test_finish_stopping_state(self):
         options = DummyOptions()
         config = DummyPConfig(options, 'notthere', '/notthere',

+ 71 - 0
supervisor/tests/test_rpcinterfaces.py

@@ -837,6 +837,77 @@ class SupervisorNamespaceXMLRPCInterfaceTests(TestBase):
              ]
             )
 
+    def test_sendProcessSignal(self):
+        options = DummyOptions()
+        pconfig = DummyPConfig(options, 'foo', '/bin/foo')
+        from supervisor.process import ProcessStates
+        supervisord = PopulatedDummySupervisor(options, 'foo', pconfig)
+        supervisord.set_procattr('foo', 'state', ProcessStates.RUNNING)
+        interface = self._makeOne(supervisord)
+
+        result = interface.sendProcessSignal('foo', 10)()
+
+        self.assertEqual(interface.update_text, 'sendProcessSignal')
+        self.assertEqual(result, True)
+        p = supervisord.process_groups[supervisord.group_name].processes['foo']
+        self.assertEqual(p.sent_signal, 10 )
+
+    def test_sendGroupSignal(self):
+        options = DummyOptions()
+        pconfig1 = DummyPConfig(options, 'process1', '/bin/foo')
+        pconfig2 = DummyPConfig(options, 'process2', '/bin/foo2')
+        from supervisor.process import ProcessStates
+        supervisord = PopulatedDummySupervisor(options, 'foo', pconfig1,
+                                               pconfig2)
+        supervisord.set_procattr('process1', 'state', ProcessStates.RUNNING)
+        supervisord.set_procattr('process2', 'state', ProcessStates.RUNNING)
+        interface = self._makeOne(supervisord)
+        callback = interface.sendGroupSignal('foo', 10)
+        self.assertEqual(interface.update_text, 'sendGroupSignal')
+        from supervisor import http
+
+        result = http.NOT_DONE_YET
+        while result is http.NOT_DONE_YET:
+            result = callback()
+
+        self.assertEqual(result, [
+            {'status':80,'group':'foo','name': 'process1','description': 'OK'},
+            {'status':80,'group':'foo','name': 'process2','description': 'OK'},
+            ] )
+        process1 = supervisord.process_groups['foo'].processes['process1']
+        self.assertEqual(process1.sent_signal, 10)
+        process2 = supervisord.process_groups['foo'].processes['process2']
+        self.assertEqual(process2.sent_signal, 10)
+
+    def test_sendProcessGroupSignal(self):
+        """ Test that sending foo:* works """
+        options = DummyOptions()
+        pconfig1 = DummyPConfig(options, 'process1', '/bin/foo')
+        pconfig2 = DummyPConfig(options, 'process2', '/bin/foo2')
+        from supervisor.process import ProcessStates
+        supervisord = PopulatedDummySupervisor(options, 'foo', pconfig1,
+                                               pconfig2)
+        supervisord.set_procattr('process1', 'state', ProcessStates.RUNNING)
+        supervisord.set_procattr('process2', 'state', ProcessStates.RUNNING)
+        interface = self._makeOne(supervisord)
+        callback = interface.sendProcessSignal('foo:*', 10)
+        self.assertEqual(interface.update_text, 'sendGroupSignal')
+        from supervisor import http
+        
+        result = http.NOT_DONE_YET
+        while result is http.NOT_DONE_YET:
+            result = callback()
+
+        self.assertEqual(result, [
+            {'status':80,'group':'foo','name': 'process1','description': 'OK'},
+            {'status':80,'group':'foo','name': 'process2','description': 'OK'},
+            ] )
+        process1 = supervisord.process_groups['foo'].processes['process1']
+        self.assertEqual(process1.sent_signal, 10)
+        process2 = supervisord.process_groups['foo'].processes['process2']
+        self.assertEqual(process2.sent_signal, 10)
+
+
     def test_getAllConfigInfo(self):
         options = DummyOptions()
         supervisord = DummySupervisor(options, 'foo')