def __init__(self, argv=None, *, cmd=None, **kw_options): ''' Initialise the command line. Raises `GetoptError` for unrecognised options. Parameters: * `argv`: optional command line arguments including the main command name if `cmd` is not specified. The default is `sys.argv`. The contents of `argv` are copied, permitting desctructive parsing of `argv`. * `options`: a optional object for command state and context. If not specified a new `SimpleNamespace` is allocated for use as `options`, and prefilled with `.cmd` set to `cmd` and other values as set by `.apply_defaults()` if such a method is provided. * `cmd`: optional command name for context; if this is not specified it is taken from `argv.pop(0)`. Other keyword arguments are applied to `self.options` as attributes. The command line arguments are parsed according to the optional `GETOPT_SPEC` class attribute (default `''`). If `getopt_spec` is not empty then `apply_opts(opts)` is called to apply the supplied options to the state where `opts` is the return from `getopt.getopt(argv,getopt_spec)`. After the option parse, if the first command line argument *foo* has a corresponding method `cmd_`*foo* then that argument is removed from the start of `argv` and `self.cmd_`*foo*`(argv,options,cmd=`*foo*`)` is called and its value returned. Otherwise `self.main(argv,options)` is called and its value returned. If the command implementation requires some setup or teardown then this may be provided by the `run_context` context manager method, called with `cmd=`*subcmd* for subcommands and with `cmd=None` for `main`. ''' subcmds = self.subcommands() has_subcmds = subcmds and list(subcmds) != ['help'] options = self.options = self.OPTIONS_CLASS() if argv is None: argv = list(sys.argv) if cmd is not None: # consume the first argument anyway argv.pop(0) else: argv = list(argv) if cmd is None: cmd = basename(argv.pop(0)) log_level = getattr(options, 'log_level', None) loginfo = setup_logging(cmd, level=log_level) # post: argv is list of arguments after the command name self.cmd = cmd self.loginfo = loginfo self.apply_defaults() # override the default options for option, value in kw_options.items(): setattr(options, option, value) self._argv = argv self._run = lambda subcmd, command, argv: 2 self._subcmd = None self._printed_usage = False # we catch GetoptError from this suite... subcmd = None # default: no subcmd specific usage available short_usage = False try: getopt_spec = getattr(self, 'GETOPT_SPEC', '') # catch bare -h or --help if no 'h' in the getopt_spec if ('h' not in getopt_spec and len(argv) == 1 and argv[0] in ('-h', '-help', '--help')): argv = ['help'] else: # we do this regardless in order to honour '--' try: opts, argv = getopt(argv, getopt_spec, '') except GetoptError: short_usage = True raise self.apply_opts(opts) # we do this regardless so that subclasses can do some presubcommand parsing # after any command line options argv = self._argv = self.apply_preargv(argv) # now prepare self._run, a callable if not has_subcmds: # no subcommands, just use the main() method try: main = self.main except AttributeError: # pylint: disable=raise-missing-from raise GetoptError("no main method and no subcommand methods") self._run = _MethodSubCommand(None, main) else: # expect a subcommand on the command line if not argv: default_argv = getattr(self, 'SUBCOMMAND_ARGV_DEFAULT', None) if not default_argv: short_usage = True raise GetoptError( "missing subcommand, expected one of: %s" % (', '.join(sorted(subcmds.keys())),) ) argv = ( [default_argv] if isinstance(default_argv, str) else list(default_argv) ) subcmd = argv.pop(0) subcmd_ = subcmd.replace('-', '_') try: subcommand = subcmds[subcmd_] except KeyError: # pylint: disable=raise-missing-from short_usage = True bad_subcmd = subcmd subcmd = None raise GetoptError( "%s: unrecognised subcommand, expected one of: %s" % ( bad_subcmd, ', '.join(sorted(subcmds.keys())), ) ) self._run = subcommand self._subcmd = subcmd except GetoptError as e: if self.getopt_error_handler(cmd, self.options, e, self.usage_text(subcmd=subcmd, short=short_usage)): self._printed_usage = True return raise
def main(argv=None): ''' Command line main programme. ''' if argv is None: argv = sys.argv cmd = basename(argv.pop(0)) usage = USAGE.format(cmd=cmd, TEST_RATE=TEST_RATE, VARRUN=VARRUN) setup_logging(cmd) badopts = False try: if not argv: raise GetoptError("missing arguments") arg0 = argv[0] if arg0 == 'disable': argv.pop(0) for name in argv: SvcD([], name=name).disable() return 0 if arg0 == 'enable': argv.pop(0) for name in argv: SvcD([], name=name).enable() return 0 if arg0 == 'restart': argv.pop(0) for name in argv: SvcD([], name=name).restart() return 0 if arg0 == 'stop': argv.pop(0) for name in argv: SvcD([], name=name).stop() return 0 once = False use_lock = False lock_name = None name = None svc_pidfile = None # pid file for the service process mypidfile = None # pid file for the svcd quiet = False sig_shcmd = None test_shcmd = None test_rate = TEST_RATE uid = os.geteuid() username = getpwuid(uid).pw_name run_uid = uid run_username = username test_uid = uid test_username = username test_flags = {} trace = sys.stderr.isatty() opts, argv = getopt(argv, '1lF:L:n:p:P:qs:t:T:u:U:x') for opt, value in opts: with Pfx(opt): if opt == '-1': once = True elif opt == '-l': use_lock = True elif opt == '-F': for flagname in value.split(','): with Pfx(flagname): truthiness = True if flagname.startswith('!'): truthiness = False flagname = flagname[1:] if not flagname: warning("invalid empty flag name") badopts = True else: test_flags[flagname] = truthiness elif opt == '-L': use_lock = True lock_name = value elif opt == '-n': name = value elif opt == '-p': svc_pidfile = value elif opt == '-P': mypidfile = value elif opt == '-q': quiet = True elif opt == '-s': sig_shcmd = value elif opt == '-t': test_shcmd = value elif opt == '-T': try: test_rate = int(value) except ValueError as e: raise GetoptError( "testrate should be a valid integer: %s" % (e, )) elif opt == '-u': run_username = value run_uid = getpwnam(run_username).pw_uid elif opt == '-U': test_username = value test_uid = getpwnam(test_username).pw_uid elif opt == '-x': trace = True else: raise RuntimeError("unhandled option") if use_lock and name is None: raise GetoptError("-l (lock) requires a name (-n)") if not argv: raise GetoptError("missing command") except GetoptError as e: warning("%s", e) badopts = True if badopts: print(usage, file=sys.stderr) return 2 if sig_shcmd is None: sig_func = None else: def sig_func(): argv = ['sh', ('-xc' if trace else '-c'), sig_shcmd] if test_uid != uid: su_shcmd = 'exec ' + quotecmd(argv) if trace: su_shcmd = 'set -x; ' + su_shcmd argv = ['su', test_username, '-c', su_shcmd] P = LockedPopen(argv, stdin=DEVNULL, stdout=PIPE) sig_text = P.stdout.read() returncode = P.wait() if returncode != 0: warning("returncode %s from %r", returncode, sig_shcmd) sig_text = None return sig_text if test_shcmd is None: test_func = None else: def test_func(): with Pfx("main.test_func: shcmd=%r", test_shcmd): argv = ['sh', '-c', test_shcmd] if test_uid != uid: argv = ['su', test_username, 'exec ' + quotecmd(argv)] shcmd_ok = callproc(argv, stdin=DEVNULL) == 0 if not quiet: info("exit status != 0") return shcmd_ok if run_uid != uid: argv = ['su', run_username, 'exec ' + quotecmd(argv)] if use_lock: argv = ['lock', '--', 'svcd-' + name] + argv S = SvcD(argv, name=name, pidfile=svc_pidfile, sig_func=sig_func, test_flags=test_flags, test_func=test_func, test_rate=test_rate, once=once, quiet=quiet, trace=trace) def signal_handler(*_): S.stop() S.wait() S.flag_stop = False sys.exit(1) signal(SIGHUP, signal_handler) signal(SIGINT, signal_handler) signal(SIGTERM, signal_handler) if S.pidfile or mypidfile: if mypidfile is None: pidfile_base, pidfile_ext = splitext(S.pidfile) mypidfile = pidfile_base + '-svcd' + pidfile_ext with PidFileManager(mypidfile): S.start() S.wait() else: S.start() S.wait()