diff options
Diffstat (limited to 'src/mailman/bin')
| -rw-r--r-- | src/mailman/bin/mailman.py | 39 | ||||
| -rw-r--r-- | src/mailman/bin/master.py | 42 | ||||
| -rw-r--r-- | src/mailman/bin/runner.py | 2 | ||||
| -rw-r--r-- | src/mailman/bin/tests/test_mailman.py | 20 |
4 files changed, 65 insertions, 38 deletions
diff --git a/src/mailman/bin/mailman.py b/src/mailman/bin/mailman.py index 6b8cb26da..0ea29edbb 100644 --- a/src/mailman/bin/mailman.py +++ b/src/mailman/bin/mailman.py @@ -16,7 +16,6 @@ # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. """The 'mailman' command dispatcher.""" - import click from contextlib import ExitStack @@ -35,17 +34,21 @@ class Subcommands(click.MultiCommand): def __init__(self, *args, **kws): super().__init__(*args, **kws) self._commands = {} - # Look at all modules in the mailman.bin package and if they are - # prepared to add a subcommand, let them do so. I'm still undecided as - # to whether this should be pluggable or not. If so, then we'll - # probably have to partially parse the arguments now, then initialize - # the system, then find the plugins. Punt on this for now. - add_components('mailman.commands', ICLISubCommand, self._commands) + self._loaded = False + + def _load(self): + # Load commands lazily as commands in plugins can only be found after + # the configuration file is loaded. + if not self._loaded: + add_components('commands', ICLISubCommand, self._commands) + self._loaded = True - def list_commands(self, ctx): - return sorted(self._commands) # pragma: nocover + def list_commands(self, ctx): # pragma: nocover + self._load() + return sorted(self._commands) def get_command(self, ctx, name): + self._load() try: return self._commands[name].command except KeyError as error: @@ -85,10 +88,11 @@ class Subcommands(click.MultiCommand): super().format_commands(ctx, formatter) -@click.group( - cls=Subcommands, - context_settings=dict(help_option_names=['-h', '--help'])) -@click.pass_context +def initialize_config(ctx, param, value): + if not ctx.resilient_parsing: + initialize(value) + + @click.option( '-C', '--config', 'config_file', envvar='MAILMAN_CONFIG_FILE', @@ -96,7 +100,12 @@ class Subcommands(click.MultiCommand): help=_("""\ Configuration file to use. If not given, the environment variable MAILMAN_CONFIG_FILE is consulted and used if set. If neither are given, a - default configuration file is loaded.""")) + default configuration file is loaded."""), + is_eager=True, callback=initialize_config) +@click.group( + cls=Subcommands, + context_settings=dict(help_option_names=['-h', '--help'])) +@click.pass_context @click.version_option(MAILMAN_VERSION_FULL, message='%(version)s') @public def main(ctx, config_file): @@ -106,6 +115,4 @@ def main(ctx, config_file): Copyright 1998-2017 by the Free Software Foundation, Inc. http://www.list.org """ - # Initialize the system. Honor the -C flag if given. - initialize(config_file) # click handles dispatching to the subcommand via the Subcommands class. diff --git a/src/mailman/bin/master.py b/src/mailman/bin/master.py index 0b273d332..8339f0fc2 100644 --- a/src/mailman/bin/master.py +++ b/src/mailman/bin/master.py @@ -45,22 +45,23 @@ SUBPROC_START_WAIT = timedelta(seconds=20) # Environment variables to forward into subprocesses. PRESERVE_ENVS = ( 'COVERAGE_PROCESS_START', - 'MAILMAN_EXTRA_TESTING_CFG', 'LANG', 'LANGUAGE', - 'LC_CTYPE', - 'LC_NUMERIC', - 'LC_TIME', + 'LC_ADDRESS', + 'LC_ALL', 'LC_COLLATE', - 'LC_MONETARY', + 'LC_CTYPE', + 'LC_IDENTIFICATION', + 'LC_MEASUREMENT', 'LC_MESSAGES', - 'LC_PAPER', + 'LC_MONETARY', 'LC_NAME', - 'LC_ADDRESS', + 'LC_NUMERIC', + 'LC_PAPER', 'LC_TELEPHONE', - 'LC_MEASUREMENT', - 'LC_IDENTIFICATION', - 'LC_ALL', + 'LC_TIME', + 'MAILMAN_EXTRA_TESTING_CFG', + 'PYTHONPATH', ) @@ -188,6 +189,9 @@ class PIDWatcher: def __init__(self): self._pids = {} + def __contains__(self, pid): + return pid in self._pids.keys() + def __iter__(self): # Safely iterate over all the keys in the dictionary. Because # asynchronous signals are involved, the dictionary's size could @@ -312,16 +316,16 @@ class Loop: env = {'MAILMAN_UNDER_MASTER_CONTROL': '1'} # Craft the command line arguments for the exec() call. rswitch = '--runner=' + spec - # Wherever master lives, so too must live the runner script. - exe = os.path.join(config.BIN_DIR, 'runner') - # config.PYTHON, which is the absolute path to the Python interpreter, - # must be given as argv[0] due to Python's library search algorithm. - args = [sys.executable, sys.executable, exe, rswitch] # Always pass the explicit path to the configuration file to the # sub-runners. This avoids any debate about which cfg file is used. config_file = (config.filename if self._config_file is None else self._config_file) - args.extend(['-C', config_file]) + # Wherever master lives, so too must live the runner script. + exe = os.path.join(config.BIN_DIR, 'runner') # pragma: nocover + # config.PYTHON, which is the absolute path to the Python interpreter, + # must be given as argv[0] due to Python's library search algorithm. + args = [sys.executable, sys.executable, exe, # pragma: nocover + '-C', config_file, rswitch] log = logging.getLogger('mailman.runner') log.debug('starting: %s', args) # We must pass this environment variable through if it's set, @@ -399,9 +403,13 @@ class Loop: except ChildProcessError: # No children? We're done. break - except InterruptedError: # pragma: nocover + except InterruptedError: # pragma: nocover # If the system call got interrupted, just restart it. continue + if pid not in self._kids: # pragma: nocover + # This is not a runner subprocess that we own. E.g. maybe a + # plugin started it. + continue # Find out why the subprocess exited by getting the signal # received or exit status. if os.WIFSIGNALED(status): diff --git a/src/mailman/bin/runner.py b/src/mailman/bin/runner.py index bf84bc3e5..b4780dcfb 100644 --- a/src/mailman/bin/runner.py +++ b/src/mailman/bin/runner.py @@ -151,14 +151,12 @@ def main(ctx, config_file, verbose, list_runners, once, runner_spec): run this way, the environment variable $MAILMAN_UNDER_MASTER_CONTROL will be set which subtly changes some error handling behavior. """ - global log if runner_spec is None and not list_runners: ctx.fail(_('No runner name given.')) # Initialize the system. Honor the -C flag if given. - initialize(config_file, verbose) log = logging.getLogger('mailman.runner') if verbose: diff --git a/src/mailman/bin/tests/test_mailman.py b/src/mailman/bin/tests/test_mailman.py index 73941d468..db13166c7 100644 --- a/src/mailman/bin/tests/test_mailman.py +++ b/src/mailman/bin/tests/test_mailman.py @@ -29,6 +29,7 @@ from mailman.interfaces.command import ICLISubCommand from mailman.testing.layers import ConfigLayer from mailman.utilities.datetime import now from mailman.utilities.modules import add_components +from pkg_resources import resource_filename from unittest.mock import patch @@ -38,7 +39,19 @@ class TestMailmanCommand(unittest.TestCase): def setUp(self): self._command = CliRunner() - def test_mailman_command_without_subcommand_prints_help(self): + def test_mailman_command_config(self): + config_path = resource_filename('mailman.testing', 'testing.cfg') + with patch('mailman.bin.mailman.initialize') as init: + self._command.invoke(main, ('-C', config_path, 'info')) + init.assert_called_once_with(config_path) + + def test_mailman_command_no_config(self): + with patch('mailman.bin.mailman.initialize') as init: + self._command.invoke(main, ('info',)) + init.assert_called_once_with(None) + + @patch('mailman.bin.mailman.initialize') + def test_mailman_command_without_subcommand_prints_help(self, mock): # Issue #137: Running `mailman` without a subcommand raises an # AttributeError. result = self._command.invoke(main) @@ -49,14 +62,15 @@ class TestMailmanCommand(unittest.TestCase): self.assertEqual(lines[0], 'Usage: main [OPTIONS] COMMAND [ARGS]...') # The help output includes a list of subcommands, in sorted order. commands = {} - add_components('mailman.commands', ICLISubCommand, commands) + add_components('commands', ICLISubCommand, commands) help_commands = list( line.split()[0].strip() for line in lines[-len(commands):] ) self.assertEqual(sorted(commands), help_commands) - def test_mailman_command_with_bad_subcommand_prints_help(self): + @patch('mailman.bin.mailman.initialize') + def test_mailman_command_with_bad_subcommand_prints_help(self, mock): # Issue #137: Running `mailman` without a subcommand raises an # AttributeError. result = self._command.invoke(main, ('not-a-subcommand',)) |
