summaryrefslogtreecommitdiff
path: root/src/mailman/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/bin')
-rw-r--r--src/mailman/bin/mailman.py39
-rw-r--r--src/mailman/bin/master.py42
-rw-r--r--src/mailman/bin/runner.py2
-rw-r--r--src/mailman/bin/tests/test_mailman.py20
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',))