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