Ver Fonte

- Supervisorctl now supports persistent readline history. To
enable, add "history_file = <pathname>" to the '[supervisorctl']
section in your supervisord.conf file.

- Multiple commands may now be issued on one supervisorctl command
line, e.g. "restart prog; tail -f prog". Separate commands with a
single semicolon; they will be executed in order as you would
expect.

Chris McDonough há 17 anos atrás
pai
commit
21efaf2bf9

+ 8 - 1
CHANGES.txt

@@ -1,6 +1,13 @@
 Next Release
 
-  - ...
+  - Supervisorctl now supports persistent readline history.  To
+    enable, add "history_file = <pathname>" to the '[supervisorctl']
+    section in your supervisord.conf file.
+
+  - Multiple commands may now be issued on one supervisorctl command
+    line, e.g. "restart prog; tail -f prog".  Separate commands with a
+    single semicolon; they will be executed in order as you would
+    expect.
 
 3.0a4
 

+ 12 - 0
src/supervisor/options.py

@@ -1277,6 +1277,7 @@ class ClientOptions(Options):
     serverurl = None
     username = None
     password = None
+    history_file = None
 
     def __init__(self):
         Options.__init__(self)
@@ -1287,6 +1288,7 @@ class ClientOptions(Options):
         self.configroot.supervisorctl.serverurl = None
         self.configroot.supervisorctl.username = None
         self.configroot.supervisorctl.password = None
+        self.configroot.supervisorctl.history_file = None
 
 
         self.add("interactive", "supervisorctl.interactive", "i",
@@ -1296,6 +1298,7 @@ class ClientOptions(Options):
                  url, default="http://localhost:9001")
         self.add("username", "supervisorctl.username", "u:", "username=")
         self.add("password", "supervisorctl.password", "p:", "password=")
+        self.add("history", "supervisorctl.history_file", "r:", "history_file=")
 
     def realize(self, *arg, **kw):
         Options.realize(self, *arg, **kw)
@@ -1326,6 +1329,15 @@ class ClientOptions(Options):
         section.prompt = config.getdefault('prompt', 'supervisor')
         section.username = config.getdefault('username', None)
         section.password = config.getdefault('password', None)
+        history_file = config.getdefault('history_file', None)
+
+        if history_file:
+            history_file = normalize_path(history_file)
+            section.history_file = history_file
+            self.history_file = history_file
+        else:
+            section.history_file = None
+            self.history_file = None
         
         return section
 

+ 1 - 0
src/supervisor/skel/sample.conf

@@ -42,6 +42,7 @@ serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
 ;username=chris              ; should be same as http_username if set
 ;password=123                ; should be same as http_password if set
 ;prompt=mysupervisor         ; cmd line prompt (default "supervisor")
+;history_file=~/.sc_history  ; use readline history if available
 
 ; The below sample program section shows all possible program subsection values,
 ; create one or more 'real' program: sections to be able to control them under

+ 31 - 5
src/supervisor/supervisorctl.py

@@ -24,6 +24,7 @@ Options:
      (default "http://localhost:9001").  
 -u/--username -- username to use for authentication with server
 -p/--password -- password to use for authentication with server
+-r/--history-file -- keep a readline history (if readline is available)
 
 action [arguments] -- see below
 
@@ -34,6 +35,7 @@ actions.
 """
 
 import cmd
+import os
 import sys
 import getpass
 import xmlrpclib
@@ -47,6 +49,7 @@ from supervisor.options import split_namespec
 from supervisor import xmlrpc
 
 
+
 class Controller(cmd.Cmd):
 
     def __init__(self, options, completekey='tab', stdin=None, stdout=None):
@@ -62,6 +65,13 @@ class Controller(cmd.Cmd):
         """ Override the onecmd method to catch and print all exceptions
         """
         origline = line
+        # allow for composite commands in interactive mode
+        # (e.g. "restart prog; tail -f prog")
+        lines = line.split(';') # don't filter(None, line.split), as we pop
+        line = lines.pop(0)
+        # stuffing the remainder into cmdqueue will cause cmdloop to
+        # call us again for each command.
+        self.cmdqueue.extend(lines)
         cmd, arg, line = self.parseline(line)
         if not line:
             return self.emptyline()
@@ -201,10 +211,14 @@ class Controller(cmd.Cmd):
             name = args[-1]
             channel = 'stdout'
         else:
-            name = args[0]
-            channel = args[-1].lower()
-            if channel not in ('stderr', 'stdout'):
-                self._output('Error: bad channel %r' % channel)
+            if args:
+                name = args[0]
+                channel = args[-1].lower()
+                if channel not in ('stderr', 'stdout'):
+                    self._output('Error: bad channel %r' % channel)
+                    return
+            else:
+                self._output('Error: tail requires process name')
                 return
 
         bytes = 1600
@@ -620,10 +634,22 @@ def main(args=None, options=None):
     if options.interactive:
         try:
             import readline
+            if options.history_file:
+                try:
+                    readline.read_history_file(options.history_file)
+                except IOError:
+                    pass
+                def save():
+                    try:
+                        readline.write_history_file(options.history_file)
+                    except IOError:
+                        pass
+                import atexit
+                atexit.register(save)
         except ImportError:
             pass
         try:
-            c.onecmd('status')
+            c.cmdqueue.append('status')
             c.cmdloop()
         except KeyboardInterrupt:
             c._output('')

+ 34 - 0
src/supervisor/tests/test_options.py

@@ -14,6 +14,40 @@ from supervisor.tests.base import DummyPConfig
 from supervisor.tests.base import DummyProcess
 from supervisor.tests.base import lstrip
 
+class ClientOptionsTests(unittest.TestCase):
+    def _getTargetClass(self):
+        from supervisor.options import ClientOptions
+        return ClientOptions
+
+    def _makeOne(self):
+        return self._getTargetClass()()
+        
+    def test_options(self):
+        tempdir = tempfile.gettempdir()
+        s = lstrip("""[supervisorctl]
+        serverurl=http://localhost:9001
+        username=chris
+        password=123
+        prompt=mysupervisor
+        history_file=%s/sc_history
+        """ % tempdir)
+
+        from StringIO import StringIO
+        fp = StringIO(s)
+        instance = self._makeOne()
+        instance.configfile = fp
+        instance.realize(args=[])
+        self.assertEqual(instance.interactive, True)
+        history_file = os.path.join(tempdir, 'sc_history')
+        self.assertEqual(instance.history_file, history_file)
+        options = instance.configroot.supervisorctl
+        self.assertEqual(options.prompt, 'mysupervisor')
+        self.assertEqual(options.serverurl, 'http://localhost:9001')
+        self.assertEqual(options.username, 'chris')
+        self.assertEqual(options.password, '123')
+        self.assertEqual(options.history_file, history_file)
+                   
+
 class ServerOptionsTests(unittest.TestCase):
     def _getTargetClass(self):
         from supervisor.options import ServerOptions

+ 19 - 0
src/supervisor/tests/test_supervisorctl.py

@@ -63,6 +63,15 @@ class ControllerTests(unittest.TestCase):
             controller.stdout.getvalue().find('Documented commands') != -1
             )
 
+    def test_onecmd_multi_colonseparated(self):
+        options = DummyClientOptions()
+        controller = self._makeOne(options)
+        controller.stdout = StringIO()
+        result = controller.onecmd('version; version')
+        self.assertEqual(result, None)
+        self.assertEqual(controller.cmdqueue, [' version'])
+        self.assertEqual(controller.stdout.getvalue(), '3000\n')
+
     def test_tail_toofewargs(self):
         options = DummyClientOptions()
         controller = self._makeOne(options)
@@ -81,6 +90,15 @@ class ControllerTests(unittest.TestCase):
         lines = controller.stdout.getvalue().split('\n')
         self.assertEqual(lines[0], 'Error: too many arguments')
 
+    def test_tail_f_noprocname(self):
+        options = DummyClientOptions()
+        controller = self._makeOne(options)
+        controller.stdout = StringIO()
+        result = controller.do_tail('-f')
+        self.assertEqual(result, None)
+        lines = controller.stdout.getvalue().split('\n')
+        self.assertEqual(lines[0], 'Error: tail requires process name')
+
     def test_tail_defaults(self):
         options = DummyClientOptions()
         controller = self._makeOne(options)
@@ -482,6 +500,7 @@ class DummyClientOptions:
         self.serverurl = 'http://localhost:9001'
         self.username = 'chrism'
         self.password = '123'
+        self.history_file = None
         self._server = DummyRPCServer()
 
     def getServerProxy(self):