1#!/usr/bin/env python -u
2
3"""supervisorctl -- control applications run by supervisord from the cmd line.
4
5Usage: %s [options] [action [arguments]]
6
7Options:
8-c/--configuration FILENAME -- configuration file path (searches if not given)
9-h/--help -- print usage message and exit
10-i/--interactive -- start an interactive shell after executing commands
11-s/--serverurl URL -- URL on which supervisord server is listening
12     (default "http://localhost:9001").
13-u/--username USERNAME -- username to use for authentication with server
14-p/--password PASSWORD -- password to use for authentication with server
15-r/--history-file -- keep a readline history (if readline is available)
16
17action [arguments] -- see below
18
19Actions are commands like "tail" or "stop".  If -i is specified or no action is
20specified on the command line, a "shell" interpreting actions typed
21interactively is started.  Use the action "help" to find out about available
22actions.
23"""
24
25import cmd
26import errno
27import getpass
28import socket
29import sys
30import threading
31
32from supervisor.compat import xmlrpclib
33from supervisor.compat import urlparse
34from supervisor.compat import unicode
35from supervisor.compat import raw_input
36from supervisor.compat import as_string
37
38from supervisor.medusa import asyncore_25 as asyncore
39
40from supervisor.options import ClientOptions
41from supervisor.options import make_namespec
42from supervisor.options import split_namespec
43from supervisor import xmlrpc
44from supervisor import states
45from supervisor import http_client
46
47class LSBInitExitStatuses:
48    SUCCESS = 0
49    GENERIC = 1
50    INVALID_ARGS = 2
51    UNIMPLEMENTED_FEATURE = 3
52    INSUFFICIENT_PRIVILEGES = 4
53    NOT_INSTALLED = 5
54    NOT_RUNNING = 7
55
56class LSBStatusExitStatuses:
57    NOT_RUNNING = 3
58    UNKNOWN = 4
59
60DEAD_PROGRAM_FAULTS = (xmlrpc.Faults.SPAWN_ERROR,
61                       xmlrpc.Faults.ABNORMAL_TERMINATION,
62                       xmlrpc.Faults.NOT_RUNNING)
63
64class fgthread(threading.Thread):
65    """ A subclass of threading.Thread, with a kill() method.
66    To be used for foreground output/error streaming.
67    http://mail.python.org/pipermail/python-list/2004-May/260937.html
68    """
69
70    def __init__(self, program, ctl):
71        threading.Thread.__init__(self)
72        self.killed = False
73        self.program = program
74        self.ctl = ctl
75        self.listener = http_client.Listener()
76        self.output_handler = http_client.HTTPHandler(self.listener,
77                                                      self.ctl.options.username,
78                                                      self.ctl.options.password)
79        self.error_handler = http_client.HTTPHandler(self.listener,
80                                                     self.ctl.options.username,
81                                                     self.ctl.options.password)
82
83    def start(self): # pragma: no cover
84        # Start the thread
85        self.__run_backup = self.run
86        self.run = self.__run
87        threading.Thread.start(self)
88
89    def run(self): # pragma: no cover
90        self.output_handler.get(self.ctl.options.serverurl,
91                                '/logtail/%s/stdout' % self.program)
92        self.error_handler.get(self.ctl.options.serverurl,
93                               '/logtail/%s/stderr' % self.program)
94        asyncore.loop()
95
96    def __run(self): # pragma: no cover
97        # Hacked run function, which installs the trace
98        sys.settrace(self.globaltrace)
99        self.__run_backup()
100        self.run = self.__run_backup
101
102    def globaltrace(self, frame, why, arg):
103        if why == 'call':
104            return self.localtrace
105        else:
106            return None
107
108    def localtrace(self, frame, why, arg):
109        if self.killed:
110            if why == 'line':
111                raise SystemExit()
112        return self.localtrace
113
114    def kill(self):
115        self.output_handler.close()
116        self.error_handler.close()
117        self.killed = True
118
119class Controller(cmd.Cmd):
120
121    def __init__(self, options, completekey='tab', stdin=None,
122                 stdout=None):
123        self.options = options
124        self.prompt = self.options.prompt + '> '
125        self.options.plugins = []
126        self.vocab = ['help']
127        self._complete_info = None
128        self.exitstatus = LSBInitExitStatuses.SUCCESS
129        cmd.Cmd.__init__(self, completekey, stdin, stdout)
130        for name, factory, kwargs in self.options.plugin_factories:
131            plugin = factory(self, **kwargs)
132            for a in dir(plugin):
133                if a.startswith('do_') and callable(getattr(plugin, a)):
134                    self.vocab.append(a[3:])
135            self.options.plugins.append(plugin)
136            plugin.name = name
137
138    def emptyline(self):
139        # We don't want a blank line to repeat the last command.
140        return
141
142    def default(self, line):
143        self.output('*** Unknown syntax: %s' % line)
144        self.exitstatus = LSBInitExitStatuses.GENERIC
145
146    def exec_cmdloop(self, args, options):
147        try:
148            import readline
149            delims = readline.get_completer_delims()
150            delims = delims.replace(':', '')  # "group:process" as one word
151            delims = delims.replace('*', '')  # "group:*" as one word
152            delims = delims.replace('-', '')  # names with "-" as one word
153            readline.set_completer_delims(delims)
154
155            if options.history_file:
156                try:
157                    readline.read_history_file(options.history_file)
158                except IOError:
159                    pass
160
161                def save():
162                    try:
163                        readline.write_history_file(options.history_file)
164                    except IOError:
165                        pass
166
167                import atexit
168                atexit.register(save)
169        except ImportError:
170            pass
171        try:
172            self.cmdqueue.append('status')
173            self.cmdloop()
174        except KeyboardInterrupt:
175            self.output('')
176            pass
177
178    def set_exitstatus_from_xmlrpc_fault(self, faultcode, ignored_faultcode=None):
179        if faultcode in (ignored_faultcode, xmlrpc.Faults.SUCCESS):
180            pass
181        elif faultcode in DEAD_PROGRAM_FAULTS:
182            self.exitstatus = LSBInitExitStatuses.NOT_RUNNING
183        else:
184            self.exitstatus = LSBInitExitStatuses.GENERIC
185
186    def onecmd(self, line):
187        """ Override the onecmd method to:
188          - catch and print all exceptions
189          - call 'do_foo' on plugins rather than ourself
190        """
191        cmd, arg, line = self.parseline(line)
192        if not line:
193            return self.emptyline()
194        if cmd is None:
195            return self.default(line)
196        self._complete_info = None
197        self.lastcmd = line
198
199        if cmd == '':
200            return self.default(line)
201        else:
202            do_func = self._get_do_func(cmd)
203            if do_func is None:
204                return self.default(line)
205            try:
206                try:
207                    return do_func(arg)
208                except xmlrpclib.ProtocolError as e:
209                    if e.errcode == 401:
210                        if self.options.interactive:
211                            self.output('Server requires authentication')
212                            username = raw_input('Username:')
213                            password = getpass.getpass(prompt='Password:')
214                            self.output('')
215                            self.options.username = username
216                            self.options.password = password
217                            return self.onecmd(line)
218                        else:
219                            self.output('Server requires authentication')
220                            self.exitstatus = LSBInitExitStatuses.GENERIC
221                    else:
222                        self.exitstatus = LSBInitExitStatuses.GENERIC
223                        raise
224                do_func(arg)
225            except Exception:
226                (file, fun, line), t, v, tbinfo = asyncore.compact_traceback()
227                error = 'error: %s, %s: file: %s line: %s' % (t, v, file, line)
228                self.output(error)
229                self.exitstatus = LSBInitExitStatuses.GENERIC
230
231    def _get_do_func(self, cmd):
232        func_name = 'do_' + cmd
233        func = getattr(self, func_name, None)
234        if not func:
235            for plugin in self.options.plugins:
236                func = getattr(plugin, func_name, None)
237                if func is not None:
238                    break
239        return func
240
241    def output(self, message):
242        if isinstance(message, unicode):
243            message = message.encode('utf-8')
244        self.stdout.write(message + '\n')
245
246    def get_supervisor(self):
247        return self.get_server_proxy('supervisor')
248
249    def get_server_proxy(self, namespace=None):
250        proxy = self.options.getServerProxy()
251        if namespace is None:
252            return proxy
253        else:
254            return getattr(proxy, namespace)
255
256    def upcheck(self):
257        try:
258            supervisor = self.get_supervisor()
259            api = supervisor.getVersion() # deprecated
260            from supervisor import rpcinterface
261            if api != rpcinterface.API_VERSION:
262                self.output(
263                    'Sorry, this version of supervisorctl expects to '
264                    'talk to a server with API version %s, but the '
265                    'remote version is %s.' % (rpcinterface.API_VERSION, api))
266                self.exitstatus = LSBInitExitStatuses.NOT_INSTALLED
267                return False
268        except xmlrpclib.Fault as e:
269            if e.faultCode == xmlrpc.Faults.UNKNOWN_METHOD:
270                self.output(
271                    'Sorry, supervisord responded but did not recognize '
272                    'the supervisor namespace commands that supervisorctl '
273                    'uses to control it.  Please check that the '
274                    '[rpcinterface:supervisor] section is enabled in the '
275                    'configuration file (see sample.conf).')
276                self.exitstatus = LSBInitExitStatuses.UNIMPLEMENTED_FEATURE
277                return False
278            self.exitstatus = LSBInitExitStatuses.GENERIC
279            raise
280        except socket.error as e:
281            if e.args[0] == errno.ECONNREFUSED:
282                self.output('%s refused connection' % self.options.serverurl)
283                self.exitstatus = LSBInitExitStatuses.INSUFFICIENT_PRIVILEGES
284                return False
285            elif e.args[0] == errno.ENOENT:
286                self.output('%s no such file' % self.options.serverurl)
287                self.exitstatus = LSBInitExitStatuses.NOT_RUNNING
288                return False
289            self.exitstatus = LSBInitExitStatuses.GENERIC
290            raise
291        return True
292
293    def complete(self, text, state, line=None):
294        """Completer function that Cmd will register with readline using
295        readline.set_completer().  This function will be called by readline
296        as complete(text, state) where text is a fragment to complete and
297        state is an integer (0..n).  Each call returns a string with a new
298        completion.  When no more are available, None is returned."""
299        if line is None: # line is only set in tests
300            import readline
301            line = readline.get_line_buffer()
302
303        matches = []
304        # blank line completes to action list
305        if not line.strip():
306            matches = self._complete_actions(text)
307        else:
308            words = line.split()
309            action = words[0]
310            # incomplete action completes to action list
311            if len(words) == 1 and not line.endswith(' '):
312                matches = self._complete_actions(text)
313            # actions that accept an action name
314            elif action in ('help'):
315                matches = self._complete_actions(text)
316            # actions that accept a group name
317            elif action in ('add', 'remove', 'update'):
318                matches = self._complete_groups(text)
319            # actions that accept a process name
320            elif action in ('clear', 'fg', 'pid', 'restart', 'signal',
321                            'start', 'status', 'stop', 'tail'):
322                matches = self._complete_processes(text)
323        if len(matches) > state:
324            return matches[state]
325
326    def _complete_actions(self, text):
327        """Build a completion list of action names matching text"""
328        return [ a + ' ' for a in self.vocab if a.startswith(text)]
329
330    def _complete_groups(self, text):
331        """Build a completion list of group names matching text"""
332        groups = []
333        for info in self._get_complete_info():
334            if info['group'] not in groups:
335                groups.append(info['group'])
336        return [ g + ' ' for g in groups if g.startswith(text) ]
337
338    def _complete_processes(self, text):
339        """Build a completion list of process names matching text"""
340        processes = []
341        for info in self._get_complete_info():
342            if ':' in text or info['name'] != info['group']:
343                processes.append('%s:%s' % (info['group'], info['name']))
344                if '%s:*' % info['group'] not in processes:
345                    processes.append('%s:*' % info['group'])
346            else:
347                processes.append(info['name'])
348        return [ p + ' ' for p in processes if p.startswith(text) ]
349
350    def _get_complete_info(self):
351        """Get all process info used for completion.  We cache this between
352        commands to reduce XML-RPC calls because readline may call
353        complete() many times if the user hits tab only once."""
354        if self._complete_info is None:
355            self._complete_info = self.get_supervisor().getAllProcessInfo()
356        return self._complete_info
357
358    def do_help(self, arg):
359        if arg.strip() == 'help':
360            self.help_help()
361        else:
362            for plugin in self.options.plugins:
363                plugin.do_help(arg)
364
365    def help_help(self):
366        self.output("help\t\tPrint a list of available actions")
367        self.output("help <action>\tPrint help for <action>")
368
369    def do_EOF(self, arg):
370        self.output('')
371        return 1
372
373    def help_EOF(self):
374        self.output("To quit, type ^D or use the quit command")
375
376def get_names(inst):
377    names = []
378    classes = [inst.__class__]
379    while classes:
380        aclass = classes.pop(0)
381        if aclass.__bases__:
382            classes = classes + list(aclass.__bases__)
383        names = names + dir(aclass)
384    return names
385
386class ControllerPluginBase:
387    name = 'unnamed'
388
389    def __init__(self, controller):
390        self.ctl = controller
391
392    def _doc_header(self):
393        return "%s commands (type help <topic>):" % self.name
394    doc_header = property(_doc_header)
395
396    def do_help(self, arg):
397        if arg:
398            # XXX check arg syntax
399            try:
400                func = getattr(self, 'help_' + arg)
401            except AttributeError:
402                try:
403                    doc = getattr(self, 'do_' + arg).__doc__
404                    if doc:
405                        self.ctl.output(doc)
406                        return
407                except AttributeError:
408                    pass
409                self.ctl.output(self.ctl.nohelp % (arg,))
410                return
411            func()
412        else:
413            names = get_names(self)
414            cmds_doc = []
415            cmds_undoc = []
416            help = {}
417            for name in names:
418                if name[:5] == 'help_':
419                    help[name[5:]]=1
420            names.sort()
421            # There can be duplicates if routines overridden
422            prevname = ''
423            for name in names:
424                if name[:3] == 'do_':
425                    if name == prevname:
426                        continue
427                    prevname = name
428                    cmd=name[3:]
429                    if cmd in help:
430                        cmds_doc.append(cmd)
431                        del help[cmd]
432                    elif getattr(self, name).__doc__:
433                        cmds_doc.append(cmd)
434                    else:
435                        cmds_undoc.append(cmd)
436            self.ctl.output('')
437            self.ctl.print_topics(self.doc_header, cmds_doc, 15, 80)
438
439def not_all_langs():
440    enc = getattr(sys.stdout, 'encoding', None) or ''
441    return None if enc.lower().startswith('utf') else sys.stdout.encoding
442
443def check_encoding(ctl):
444    problematic_enc = not_all_langs()
445    if problematic_enc:
446        ctl.output('Warning: sys.stdout.encoding is set to %s, so Unicode '
447                   'output may fail. Check your LANG and PYTHONIOENCODING '
448                   'environment settings.' % problematic_enc)
449
450class DefaultControllerPlugin(ControllerPluginBase):
451    name = 'default'
452    listener = None # for unit tests
453    def _tailf(self, path):
454        check_encoding(self.ctl)
455        self.ctl.output('==> Press Ctrl-C to exit <==')
456
457        username = self.ctl.options.username
458        password = self.ctl.options.password
459        handler = None
460        try:
461            # Python's urllib2 (at least as of Python 2.4.2) isn't up
462            # to this task; it doesn't actually implement a proper
463            # HTTP/1.1 client that deals with chunked responses (it
464            # always sends a Connection: close header).  We use a
465            # homegrown client based on asyncore instead.  This makes
466            # me sad.
467            if self.listener is None:
468                listener = http_client.Listener()
469            else:
470                listener = self.listener # for unit tests
471            handler = http_client.HTTPHandler(listener, username, password)
472            handler.get(self.ctl.options.serverurl, path)
473            asyncore.loop()
474        except KeyboardInterrupt:
475            if handler:
476                handler.close()
477            self.ctl.output('')
478            return
479
480    def do_tail(self, arg):
481        if not self.ctl.upcheck():
482            return
483
484        args = arg.split()
485
486        if len(args) < 1:
487            self.ctl.output('Error: too few arguments')
488            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
489            self.help_tail()
490            return
491
492        elif len(args) > 3:
493            self.ctl.output('Error: too many arguments')
494            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
495            self.help_tail()
496            return
497
498        modifier = None
499
500        if args[0].startswith('-'):
501            modifier = args.pop(0)
502
503        if len(args) == 1:
504            name = args[-1]
505            channel = 'stdout'
506        else:
507            if args:
508                name = args[0]
509                channel = args[-1].lower()
510                if channel not in ('stderr', 'stdout'):
511                    self.ctl.output('Error: bad channel %r' % channel)
512                    self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
513                    return
514            else:
515                self.ctl.output('Error: tail requires process name')
516                self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
517                return
518
519        bytes = 1600
520
521        if modifier is not None:
522            what = modifier[1:]
523            if what == 'f':
524                bytes = None
525            else:
526                try:
527                    bytes = int(what)
528                except:
529                    self.ctl.output('Error: bad argument %s' % modifier)
530                    self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
531                    return
532
533        supervisor = self.ctl.get_supervisor()
534
535        if bytes is None:
536            return self._tailf('/logtail/%s/%s' % (name, channel))
537
538        else:
539            check_encoding(self.ctl)
540            try:
541                if channel == 'stdout':
542                    output = supervisor.readProcessStdoutLog(name,
543                                                             -bytes, 0)
544                else:
545                    output = supervisor.readProcessStderrLog(name,
546                                                             -bytes, 0)
547            except xmlrpclib.Fault as e:
548                self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
549                template = '%s: ERROR (%s)'
550                if e.faultCode == xmlrpc.Faults.NO_FILE:
551                    self.ctl.output(template % (name, 'no log file'))
552                elif e.faultCode == xmlrpc.Faults.FAILED:
553                    self.ctl.output(template % (name,
554                                             'unknown error reading log'))
555                elif e.faultCode == xmlrpc.Faults.BAD_NAME:
556                    self.ctl.output(template % (name,
557                                             'no such process name'))
558                else:
559                    raise
560            else:
561                self.ctl.output(output)
562
563    def help_tail(self):
564        self.ctl.output(
565            "tail [-f] <name> [stdout|stderr] (default stdout)\n"
566            "Ex:\n"
567            "tail -f <name>\t\tContinuous tail of named process stdout\n"
568            "\t\t\tCtrl-C to exit.\n"
569            "tail -100 <name>\tlast 100 *bytes* of process stdout\n"
570            "tail <name> stderr\tlast 1600 *bytes* of process stderr"
571            )
572
573    def do_maintail(self, arg):
574        if not self.ctl.upcheck():
575            return
576
577        args = arg.split()
578
579        if len(args) > 1:
580            self.ctl.output('Error: too many arguments')
581            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
582            self.help_maintail()
583            return
584
585        elif len(args) == 1:
586            if args[0].startswith('-'):
587                what = args[0][1:]
588                if what == 'f':
589                    path = '/mainlogtail'
590                    return self._tailf(path)
591                try:
592                    what = int(what)
593                except:
594                    self.ctl.output('Error: bad argument %s' % args[0])
595                    self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
596                    return
597                else:
598                    bytes = what
599            else:
600                self.ctl.output('Error: bad argument %s' % args[0])
601                self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
602                return
603
604        else:
605            bytes = 1600
606
607        supervisor = self.ctl.get_supervisor()
608
609        try:
610            output = supervisor.readLog(-bytes, 0)
611        except xmlrpclib.Fault as e:
612            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
613            template = '%s: ERROR (%s)'
614            if e.faultCode == xmlrpc.Faults.NO_FILE:
615                self.ctl.output(template % ('supervisord', 'no log file'))
616            elif e.faultCode == xmlrpc.Faults.FAILED:
617                self.ctl.output(template % ('supervisord',
618                                         'unknown error reading log'))
619            else:
620                raise
621        else:
622            self.ctl.output(output)
623
624    def help_maintail(self):
625        self.ctl.output(
626            "maintail -f \tContinuous tail of supervisor main log file"
627            " (Ctrl-C to exit)\n"
628            "maintail -100\tlast 100 *bytes* of supervisord main log file\n"
629            "maintail\tlast 1600 *bytes* of supervisor main log file\n"
630            )
631
632    def do_quit(self, arg):
633        return self.ctl.do_EOF(arg)
634
635    def help_quit(self):
636        self.ctl.output("quit\tExit the supervisor shell.")
637
638    do_exit = do_quit
639
640    def help_exit(self):
641        self.ctl.output("exit\tExit the supervisor shell.")
642
643    def _show_statuses(self, process_infos):
644        namespecs, maxlen = [], 30
645        for i, info in enumerate(process_infos):
646            namespecs.append(make_namespec(info['group'], info['name']))
647            if len(namespecs[i]) > maxlen:
648                maxlen = len(namespecs[i])
649
650        template = '%(namespec)-' + str(maxlen+3) + 's%(state)-10s%(desc)s'
651        for i, info in enumerate(process_infos):
652            line = template % {'namespec': namespecs[i],
653                               'state': info['statename'],
654                               'desc': info['description']}
655            self.ctl.output(line)
656
657    def do_status(self, arg):
658        # XXX In case upcheck fails, we override the exitstatus which
659        # should only return 4 for do_status
660        # TODO review this
661        if not self.ctl.upcheck():
662            self.ctl.exitstatus = LSBStatusExitStatuses.UNKNOWN
663            return
664
665        supervisor = self.ctl.get_supervisor()
666        all_infos = supervisor.getAllProcessInfo()
667
668        names = as_string(arg).split()
669        if not names or "all" in names:
670            matching_infos = all_infos
671        else:
672            matching_infos = []
673
674            for name in names:
675                bad_name = True
676                group_name, process_name = split_namespec(name)
677
678                for info in all_infos:
679                    matched = info['group'] == group_name
680                    if process_name is not None:
681                        matched = matched and info['name'] == process_name
682
683                    if matched:
684                        bad_name = False
685                        matching_infos.append(info)
686
687                if bad_name:
688                    if process_name is None:
689                        msg = "%s: ERROR (no such group)" % group_name
690                    else:
691                        msg = "%s: ERROR (no such process)" % name
692                    self.ctl.output(msg)
693                    self.ctl.exitstatus = LSBStatusExitStatuses.UNKNOWN
694        self._show_statuses(matching_infos)
695
696        for info in matching_infos:
697            if info['state'] in states.STOPPED_STATES:
698                self.ctl.exitstatus = LSBStatusExitStatuses.NOT_RUNNING
699
700    def help_status(self):
701        self.ctl.output("status <name>\t\tGet status for a single process")
702        self.ctl.output("status <gname>:*\tGet status for all "
703                        "processes in a group")
704        self.ctl.output("status <name> <name>\tGet status for multiple named "
705                        "processes")
706        self.ctl.output("status\t\t\tGet all process status info")
707
708    def do_pid(self, arg):
709        supervisor = self.ctl.get_supervisor()
710        if not self.ctl.upcheck():
711            return
712        names = arg.split()
713        if not names:
714            pid = supervisor.getPID()
715            self.ctl.output(str(pid))
716        elif 'all' in names:
717            for info in supervisor.getAllProcessInfo():
718                self.ctl.output(str(info['pid']))
719        else:
720            for name in names:
721                try:
722                    info = supervisor.getProcessInfo(name)
723                except xmlrpclib.Fault as e:
724                    self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
725                    if e.faultCode == xmlrpc.Faults.BAD_NAME:
726                        self.ctl.output('No such process %s' % name)
727                    else:
728                        raise
729                else:
730                    pid = info['pid']
731                    self.ctl.output(str(pid))
732                    if pid == 0:
733                        self.ctl.exitstatus = LSBInitExitStatuses.NOT_RUNNING
734
735    def help_pid(self):
736        self.ctl.output("pid\t\t\tGet the PID of supervisord.")
737        self.ctl.output("pid <name>\t\tGet the PID of a single "
738            "child process by name.")
739        self.ctl.output("pid all\t\t\tGet the PID of every child "
740            "process, one per line.")
741
742    def _startresult(self, result):
743        name = make_namespec(result['group'], result['name'])
744        code = result['status']
745        template = '%s: ERROR (%s)'
746        if code == xmlrpc.Faults.BAD_NAME:
747            return template % (name, 'no such process')
748        elif code == xmlrpc.Faults.NO_FILE:
749            return template % (name, 'no such file')
750        elif code == xmlrpc.Faults.NOT_EXECUTABLE:
751            return template % (name, 'file is not executable')
752        elif code == xmlrpc.Faults.ALREADY_STARTED:
753            return template % (name, 'already started')
754        elif code == xmlrpc.Faults.SPAWN_ERROR:
755            return template % (name, 'spawn error')
756        elif code == xmlrpc.Faults.ABNORMAL_TERMINATION:
757            return template % (name, 'abnormal termination')
758        elif code == xmlrpc.Faults.SUCCESS:
759            return '%s: started' % name
760        # assertion
761        raise ValueError('Unknown result code %s for %s' % (code, name))
762
763    def do_start(self, arg):
764        if not self.ctl.upcheck():
765            return
766
767        names = arg.split()
768        supervisor = self.ctl.get_supervisor()
769
770        if not names:
771            self.ctl.output("Error: start requires a process name")
772            self.ctl.exitstatus = LSBInitExitStatuses.INVALID_ARGS
773            self.help_start()
774            return
775
776        if 'all' in names:
777            results = supervisor.startAllProcesses()
778            for result in results:
779                self.ctl.output(self._startresult(result))
780                self.ctl.set_exitstatus_from_xmlrpc_fault(result['status'], xmlrpc.Faults.ALREADY_STARTED)
781        else:
782            for name in names:
783                group_name, process_name = split_namespec(name)
784                if process_name is None:
785                    try:
786                        results = supervisor.startProcessGroup(group_name)
787                        for result in results:
788                            self.ctl.output(self._startresult(result))
789                            self.ctl.set_exitstatus_from_xmlrpc_fault(result['status'], xmlrpc.Faults.ALREADY_STARTED)
790                    except xmlrpclib.Fault as e:
791                        if e.faultCode == xmlrpc.Faults.BAD_NAME:
792                            error = "%s: ERROR (no such group)" % group_name
793                            self.ctl.output(error)
794                            self.ctl.exitstatus = LSBInitExitStatuses.INVALID_ARGS
795                        else:
796                            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
797                            raise
798                else:
799                    try:
800                        result = supervisor.startProcess(name)
801                    except xmlrpclib.Fault as e:
802                        error = {'status': e.faultCode,
803                                  'name': process_name,
804                                  'group': group_name,
805                                  'description': e.faultString}
806                        self.ctl.output(self._startresult(error))
807                        self.ctl.set_exitstatus_from_xmlrpc_fault(error['status'], xmlrpc.Faults.ALREADY_STARTED)
808                    else:
809                        name = make_namespec(group_name, process_name)
810                        self.ctl.output('%s: started' % name)
811
812    def help_start(self):
813        self.ctl.output("start <name>\t\tStart a process")
814        self.ctl.output("start <gname>:*\t\tStart all processes in a group")
815        self.ctl.output(
816            "start <name> <name>\tStart multiple processes or groups")
817        self.ctl.output("start all\t\tStart all processes")
818
819    def _signalresult(self, result, success='signalled'):
820        name = make_namespec(result['group'], result['name'])
821        code = result['status']
822        fault_string = result['description']
823        template = '%s: ERROR (%s)'
824        if code == xmlrpc.Faults.BAD_NAME:
825            return template % (name, 'no such process')
826        elif code == xmlrpc.Faults.BAD_SIGNAL:
827            return template % (name, 'bad signal name')
828        elif code == xmlrpc.Faults.NOT_RUNNING:
829            return template % (name, 'not running')
830        elif code == xmlrpc.Faults.SUCCESS:
831            return '%s: %s' % (name, success)
832        elif code == xmlrpc.Faults.FAILED:
833            return fault_string
834        # assertion
835        raise ValueError('Unknown result code %s for %s' % (code, name))
836
837    def _stopresult(self, result):
838        return self._signalresult(result, success='stopped')
839
840    def do_stop(self, arg):
841        if not self.ctl.upcheck():
842            return
843
844        names = arg.split()
845        supervisor = self.ctl.get_supervisor()
846
847        if not names:
848            self.ctl.output('Error: stop requires a process name')
849            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
850            self.help_stop()
851            return
852
853        if 'all' in names:
854            results = supervisor.stopAllProcesses()
855            for result in results:
856                self.ctl.output(self._stopresult(result))
857                self.ctl.set_exitstatus_from_xmlrpc_fault(result['status'], xmlrpc.Faults.NOT_RUNNING)
858        else:
859            for name in names:
860                group_name, process_name = split_namespec(name)
861                if process_name is None:
862                    try:
863                        results = supervisor.stopProcessGroup(group_name)
864
865                        for result in results:
866                            self.ctl.output(self._stopresult(result))
867                            self.ctl.set_exitstatus_from_xmlrpc_fault(result['status'], xmlrpc.Faults.NOT_RUNNING)
868                    except xmlrpclib.Fault as e:
869                        self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
870                        if e.faultCode == xmlrpc.Faults.BAD_NAME:
871                            error = "%s: ERROR (no such group)" % group_name
872                            self.ctl.output(error)
873                        else:
874                            raise
875                else:
876                    try:
877                        supervisor.stopProcess(name)
878                    except xmlrpclib.Fault as e:
879                        error = {'status': e.faultCode,
880                                 'name': process_name,
881                                 'group': group_name,
882                                 'description':e.faultString}
883                        self.ctl.output(self._stopresult(error))
884                        self.ctl.set_exitstatus_from_xmlrpc_fault(error['status'], xmlrpc.Faults.NOT_RUNNING)
885                    else:
886                        name = make_namespec(group_name, process_name)
887                        self.ctl.output('%s: stopped' % name)
888
889    def help_stop(self):
890        self.ctl.output("stop <name>\t\tStop a process")
891        self.ctl.output("stop <gname>:*\t\tStop all processes in a group")
892        self.ctl.output("stop <name> <name>\tStop multiple processes or groups")
893        self.ctl.output("stop all\t\tStop all processes")
894
895    def do_signal(self, arg):
896        if not self.ctl.upcheck():
897            return
898
899        args = arg.split()
900        if len(args) < 2:
901            self.ctl.output(
902                'Error: signal requires a signal name and a process name')
903            self.help_signal()
904            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
905            return
906
907        sig = args[0]
908        names = args[1:]
909        supervisor = self.ctl.get_supervisor()
910
911        if 'all' in names:
912            results = supervisor.signalAllProcesses(sig)
913
914            for result in results:
915                self.ctl.output(self._signalresult(result))
916                self.ctl.set_exitstatus_from_xmlrpc_fault(result['status'])
917        else:
918            for name in names:
919                group_name, process_name = split_namespec(name)
920                if process_name is None:
921                    try:
922                        results = supervisor.signalProcessGroup(
923                            group_name, sig
924                            )
925                        for result in results:
926                            self.ctl.output(self._signalresult(result))
927                            self.ctl.set_exitstatus_from_xmlrpc_fault(result['status'])
928                    except xmlrpclib.Fault as e:
929                        if e.faultCode == xmlrpc.Faults.BAD_NAME:
930                            error = "%s: ERROR (no such group)" % group_name
931                            self.ctl.output(error)
932                            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
933                        else:
934                            raise
935                else:
936                    try:
937                        supervisor.signalProcess(name, sig)
938                    except xmlrpclib.Fault as e:
939                        error = {'status': e.faultCode,
940                                 'name': process_name,
941                                 'group': group_name,
942                                 'description':e.faultString}
943                        self.ctl.output(self._signalresult(error))
944                        self.ctl.set_exitstatus_from_xmlrpc_fault(error['status'])
945                    else:
946                        name = make_namespec(group_name, process_name)
947                        self.ctl.output('%s: signalled' % name)
948
949    def help_signal(self):
950        self.ctl.output("signal <signal name> <name>\t\tSignal a process")
951        self.ctl.output("signal <signal name> <gname>:*\t\tSignal all processes in a group")
952        self.ctl.output("signal <signal name> <name> <name>\tSignal multiple processes or groups")
953        self.ctl.output("signal <signal name> all\t\tSignal all processes")
954
955    def do_restart(self, arg):
956        if not self.ctl.upcheck():
957            return
958
959        names = arg.split()
960
961        if not names:
962            self.ctl.output('Error: restart requires a process name')
963            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
964            self.help_restart()
965            return
966
967        self.do_stop(arg)
968        self.do_start(arg)
969
970    def help_restart(self):
971        self.ctl.output("restart <name>\t\tRestart a process")
972        self.ctl.output("restart <gname>:*\tRestart all processes in a group")
973        self.ctl.output("restart <name> <name>\tRestart multiple processes or "
974                     "groups")
975        self.ctl.output("restart all\t\tRestart all processes")
976        self.ctl.output("Note: restart does not reread config files. For that,"
977                        " see reread and update.")
978
979    def do_shutdown(self, arg):
980        if self.ctl.options.interactive:
981            yesno = raw_input('Really shut the remote supervisord process '
982                              'down y/N? ')
983            really = yesno.lower().startswith('y')
984        else:
985            really = 1
986
987        if really:
988            supervisor = self.ctl.get_supervisor()
989            try:
990                supervisor.shutdown()
991            except xmlrpclib.Fault as e:
992                if e.faultCode == xmlrpc.Faults.SHUTDOWN_STATE:
993                    self.ctl.output('ERROR: already shutting down')
994                else:
995                    self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
996                    raise
997            except socket.error as e:
998                self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
999                if e.args[0] == errno.ECONNREFUSED:
1000                    msg = 'ERROR: %s refused connection (already shut down?)'
1001                    self.ctl.output(msg % self.ctl.options.serverurl)
1002                elif e.args[0] == errno.ENOENT:
1003                    msg = 'ERROR: %s no such file (already shut down?)'
1004                    self.ctl.output(msg % self.ctl.options.serverurl)
1005                else:
1006                    raise
1007            else:
1008                self.ctl.output('Shut down')
1009
1010    def help_shutdown(self):
1011        self.ctl.output("shutdown \tShut the remote supervisord down.")
1012
1013    def do_reload(self, arg):
1014        if arg:
1015            self.ctl.output('Error: reload accepts no arguments')
1016            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1017            self.help_reload()
1018            return
1019
1020        if self.ctl.options.interactive:
1021            yesno = raw_input('Really restart the remote supervisord process '
1022                              'y/N? ')
1023            really = yesno.lower().startswith('y')
1024        else:
1025            really = 1
1026        if really:
1027            supervisor = self.ctl.get_supervisor()
1028            try:
1029                supervisor.restart()
1030            except xmlrpclib.Fault as e:
1031                self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1032                if e.faultCode == xmlrpc.Faults.SHUTDOWN_STATE:
1033                    self.ctl.output('ERROR: already shutting down')
1034                else:
1035                    raise
1036            else:
1037                self.ctl.output('Restarted supervisord')
1038
1039    def help_reload(self):
1040        self.ctl.output("reload \t\tRestart the remote supervisord.")
1041
1042    def _formatChanges(self, added_changed_dropped_tuple):
1043        added, changed, dropped = added_changed_dropped_tuple
1044        changedict = {}
1045        for n, t in [(added, 'available'),
1046                     (changed, 'changed'),
1047                     (dropped, 'disappeared')]:
1048            changedict.update(dict(zip(n, [t] * len(n))))
1049
1050        if changedict:
1051            names = list(changedict.keys())
1052            names.sort()
1053            for name in names:
1054                self.ctl.output("%s: %s" % (name, changedict[name]))
1055        else:
1056            self.ctl.output("No config updates to processes")
1057
1058    def _formatConfigInfo(self, configinfo):
1059        name = make_namespec(configinfo['group'], configinfo['name'])
1060        formatted = { 'name': name }
1061        if configinfo['inuse']:
1062            formatted['inuse'] = 'in use'
1063        else:
1064            formatted['inuse'] = 'avail'
1065        if configinfo['autostart']:
1066            formatted['autostart'] = 'auto'
1067        else:
1068            formatted['autostart'] = 'manual'
1069        formatted['priority'] = "%s:%s" % (configinfo['group_prio'],
1070                                           configinfo['process_prio'])
1071
1072        template = '%(name)-32s %(inuse)-9s %(autostart)-9s %(priority)s'
1073        return template % formatted
1074
1075    def do_avail(self, arg):
1076        if arg:
1077            self.ctl.output('Error: avail accepts no arguments')
1078            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1079            self.help_avail()
1080            return
1081
1082        supervisor = self.ctl.get_supervisor()
1083        try:
1084            configinfo = supervisor.getAllConfigInfo()
1085        except xmlrpclib.Fault as e:
1086            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1087            if e.faultCode == xmlrpc.Faults.SHUTDOWN_STATE:
1088                self.ctl.output('ERROR: supervisor shutting down')
1089            else:
1090                raise
1091        else:
1092            for pinfo in configinfo:
1093                self.ctl.output(self._formatConfigInfo(pinfo))
1094
1095    def help_avail(self):
1096        self.ctl.output("avail\t\t\tDisplay all configured processes")
1097
1098    def do_reread(self, arg):
1099        if arg:
1100            self.ctl.output('Error: reread accepts no arguments')
1101            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1102            self.help_reread()
1103            return
1104
1105        supervisor = self.ctl.get_supervisor()
1106        try:
1107            result = supervisor.reloadConfig()
1108        except xmlrpclib.Fault as e:
1109            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1110            if e.faultCode == xmlrpc.Faults.SHUTDOWN_STATE:
1111                self.ctl.output('ERROR: supervisor shutting down')
1112            elif e.faultCode == xmlrpc.Faults.CANT_REREAD:
1113                self.ctl.output("ERROR: %s" % e.faultString)
1114            else:
1115                raise
1116        else:
1117            self._formatChanges(result[0])
1118
1119    def help_reread(self):
1120        self.ctl.output("reread \t\t\tReload the daemon's configuration files without add/remove")
1121
1122    def do_add(self, arg):
1123        names = arg.split()
1124
1125        supervisor = self.ctl.get_supervisor()
1126        for name in names:
1127            try:
1128                supervisor.addProcessGroup(name)
1129            except xmlrpclib.Fault as e:
1130                if e.faultCode == xmlrpc.Faults.SHUTDOWN_STATE:
1131                    self.ctl.output('ERROR: shutting down')
1132                    self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1133                elif e.faultCode == xmlrpc.Faults.ALREADY_ADDED:
1134                    self.ctl.output('ERROR: process group already active')
1135                elif e.faultCode == xmlrpc.Faults.BAD_NAME:
1136                    self.ctl.output("ERROR: no such process/group: %s" % name)
1137                    self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1138                else:
1139                    self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1140                    raise
1141            else:
1142                self.ctl.output("%s: added process group" % name)
1143
1144    def help_add(self):
1145        self.ctl.output("add <name> [...]\tActivates any updates in config "
1146                        "for process/group")
1147
1148    def do_remove(self, arg):
1149        names = arg.split()
1150
1151        supervisor = self.ctl.get_supervisor()
1152        for name in names:
1153            try:
1154                supervisor.removeProcessGroup(name)
1155            except xmlrpclib.Fault as e:
1156                self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1157                if e.faultCode == xmlrpc.Faults.STILL_RUNNING:
1158                    self.ctl.output('ERROR: process/group still running: %s'
1159                                      % name)
1160                elif e.faultCode == xmlrpc.Faults.BAD_NAME:
1161                    self.ctl.output("ERROR: no such process/group: %s" % name)
1162                else:
1163                    raise
1164            else:
1165                self.ctl.output("%s: removed process group" % name)
1166
1167    def help_remove(self):
1168        self.ctl.output("remove <name> [...]\tRemoves process/group from "
1169                        "active config")
1170
1171    def do_update(self, arg):
1172        def log(name, message):
1173            self.ctl.output("%s: %s" % (name, message))
1174
1175        supervisor = self.ctl.get_supervisor()
1176        try:
1177            result = supervisor.reloadConfig()
1178        except xmlrpclib.Fault as e:
1179            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1180            if e.faultCode == xmlrpc.Faults.SHUTDOWN_STATE:
1181                self.ctl.output('ERROR: already shutting down')
1182                return
1183            else:
1184                raise
1185
1186        added, changed, removed = result[0]
1187        valid_gnames = set(arg.split())
1188
1189        # If all is specified treat it as if nothing was specified.
1190        if "all" in valid_gnames:
1191            valid_gnames = set()
1192
1193        # If any gnames are specified we need to verify that they are
1194        # valid in order to print a useful error message.
1195        if valid_gnames:
1196            groups = set()
1197            for info in supervisor.getAllProcessInfo():
1198                groups.add(info['group'])
1199            # New gnames would not currently exist in this set so
1200            # add those as well.
1201            groups.update(added)
1202
1203            for gname in valid_gnames:
1204                if gname not in groups:
1205                    self.ctl.output('ERROR: no such group: %s' % gname)
1206                    self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1207
1208        for gname in removed:
1209            if valid_gnames and gname not in valid_gnames:
1210                continue
1211            results = supervisor.stopProcessGroup(gname)
1212            log(gname, "stopped")
1213
1214            fails = [res for res in results
1215                     if res['status'] == xmlrpc.Faults.FAILED]
1216            if fails:
1217                self.ctl.output("%s: %s" % (gname, "has problems; not removing"))
1218                self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1219                continue
1220            supervisor.removeProcessGroup(gname)
1221            log(gname, "removed process group")
1222
1223        for gname in changed:
1224            if valid_gnames and gname not in valid_gnames:
1225                continue
1226            supervisor.stopProcessGroup(gname)
1227            log(gname, "stopped")
1228
1229            supervisor.removeProcessGroup(gname)
1230            supervisor.addProcessGroup(gname)
1231            log(gname, "updated process group")
1232
1233        for gname in added:
1234            if valid_gnames and gname not in valid_gnames:
1235                continue
1236            supervisor.addProcessGroup(gname)
1237            log(gname, "added process group")
1238
1239    def help_update(self):
1240        self.ctl.output("update\t\t\tReload config and add/remove as necessary, and will restart affected programs")
1241        self.ctl.output("update all\t\tReload config and add/remove as necessary, and will restart affected programs")
1242        self.ctl.output("update <gname> [...]\tUpdate specific groups")
1243
1244    def _clearresult(self, result):
1245        name = make_namespec(result['group'], result['name'])
1246        code = result['status']
1247        template = '%s: ERROR (%s)'
1248        if code == xmlrpc.Faults.BAD_NAME:
1249            return template % (name, 'no such process')
1250        elif code == xmlrpc.Faults.FAILED:
1251            return template % (name, 'failed')
1252        elif code == xmlrpc.Faults.SUCCESS:
1253            return '%s: cleared' % name
1254        raise ValueError('Unknown result code %s for %s' % (code, name))
1255
1256    def do_clear(self, arg):
1257        if not self.ctl.upcheck():
1258            return
1259
1260        names = arg.split()
1261
1262        if not names:
1263            self.ctl.output('Error: clear requires a process name')
1264            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1265            self.help_clear()
1266            return
1267
1268        supervisor = self.ctl.get_supervisor()
1269
1270        if 'all' in names:
1271            results = supervisor.clearAllProcessLogs()
1272            for result in results:
1273                self.ctl.output(self._clearresult(result))
1274                self.ctl.set_exitstatus_from_xmlrpc_fault(result['status'])
1275        else:
1276            for name in names:
1277                group_name, process_name = split_namespec(name)
1278                try:
1279                    supervisor.clearProcessLogs(name)
1280                except xmlrpclib.Fault as e:
1281                    error = {'status': e.faultCode,
1282                             'name': process_name,
1283                             'group': group_name,
1284                             'description': e.faultString}
1285                    self.ctl.output(self._clearresult(error))
1286                    self.ctl.set_exitstatus_from_xmlrpc_fault(error['status'])
1287                else:
1288                    name = make_namespec(group_name, process_name)
1289                    self.ctl.output('%s: cleared' % name)
1290
1291    def help_clear(self):
1292        self.ctl.output("clear <name>\t\tClear a process' log files.")
1293        self.ctl.output(
1294            "clear <name> <name>\tClear multiple process' log files")
1295        self.ctl.output("clear all\t\tClear all process' log files")
1296
1297    def do_open(self, arg):
1298        url = arg.strip()
1299        parts = urlparse.urlparse(url)
1300        if parts[0] not in ('unix', 'http'):
1301            self.ctl.output('ERROR: url must be http:// or unix://')
1302            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1303            return
1304        self.ctl.options.serverurl = url
1305        # TODO review this
1306        old_exitstatus = self.ctl.exitstatus
1307        self.do_status('')
1308        self.ctl.exitstatus = old_exitstatus
1309
1310    def help_open(self):
1311        self.ctl.output("open <url>\tConnect to a remote supervisord process.")
1312        self.ctl.output("\t\t(for UNIX domain socket, use unix:///socket/path)")
1313
1314    def do_version(self, arg):
1315        if arg:
1316            self.ctl.output('Error: version accepts no arguments')
1317            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1318            self.help_version()
1319            return
1320
1321        if not self.ctl.upcheck():
1322            return
1323        supervisor = self.ctl.get_supervisor()
1324        self.ctl.output(supervisor.getSupervisorVersion())
1325
1326    def help_version(self):
1327        self.ctl.output(
1328            "version\t\t\tShow the version of the remote supervisord "
1329            "process")
1330
1331    def do_fg(self, arg):
1332        if not self.ctl.upcheck():
1333            return
1334
1335        names = arg.split()
1336        if not names:
1337            self.ctl.output('ERROR: no process name supplied')
1338            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1339            self.help_fg()
1340            return
1341        if len(names) > 1:
1342            self.ctl.output('ERROR: too many process names supplied')
1343            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1344            return
1345
1346        name = names[0]
1347        supervisor = self.ctl.get_supervisor()
1348
1349        try:
1350            info = supervisor.getProcessInfo(name)
1351        except xmlrpclib.Fault as e:
1352            if e.faultCode == xmlrpc.Faults.BAD_NAME:
1353                self.ctl.output('ERROR: bad process name supplied')
1354                self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1355            else:
1356                self.ctl.output('ERROR: ' + str(e))
1357            return
1358
1359        if info['state'] != states.ProcessStates.RUNNING:
1360            self.ctl.output('ERROR: process not running')
1361            self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
1362            return
1363
1364        self.ctl.output('==> Press Ctrl-C to exit <==')
1365
1366        a = None
1367        try:
1368            # this thread takes care of the output/error messages
1369            a = fgthread(name, self.ctl)
1370            a.start()
1371
1372            # this takes care of the user input
1373            while True:
1374                inp = raw_input() + '\n'
1375                try:
1376                    supervisor.sendProcessStdin(name, inp)
1377                except xmlrpclib.Fault as e:
1378                    if e.faultCode == xmlrpc.Faults.NOT_RUNNING:
1379                        self.ctl.output('Process got killed')
1380                    else:
1381                        self.ctl.output('ERROR: ' + str(e))
1382                    self.ctl.output('Exiting foreground')
1383                    a.kill()
1384                    return
1385
1386                info = supervisor.getProcessInfo(name)
1387                if info['state'] != states.ProcessStates.RUNNING:
1388                    self.ctl.output('Process got killed')
1389                    self.ctl.output('Exiting foreground')
1390                    a.kill()
1391                    return
1392        except (KeyboardInterrupt, EOFError):
1393            self.ctl.output('Exiting foreground')
1394            if a:
1395                a.kill()
1396
1397    def help_fg(self,args=None):
1398        self.ctl.output('fg <process>\tConnect to a process in foreground mode')
1399        self.ctl.output("\t\tCtrl-C to exit")
1400
1401
1402def main(args=None, options=None):
1403    if options is None:
1404        options = ClientOptions()
1405
1406    options.realize(args, doc=__doc__)
1407    c = Controller(options)
1408
1409    if options.args:
1410        c.onecmd(" ".join(options.args))
1411        sys.exit(c.exitstatus)
1412
1413    if options.interactive:
1414        c.exec_cmdloop(args, options)
1415        sys.exit(0)  # exitstatus always 0 for interactive mode
1416
1417if __name__ == "__main__":
1418    main()
1419