sopel.cli.run   F
last analyzed

Complexity

Total Complexity 99

Size/Duplication

Total Lines 689
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 99
eloc 408
dl 0
loc 689
rs 2
c 0
b 0
f 0

15 Functions

Rating   Name   Duplication   Size   Complexity  
B build_parser() 0 93 1
A command_configure() 0 7 2
F command_legacy() 0 129 25
B check_not_root() 0 21 6
B command_restart() 0 31 6
C command_start() 0 45 9
A add_legacy_options() 0 54 1
A get_running_pid() 0 19 4
B command_stop() 0 37 7
A print_version() 0 7 1
A get_pid_filename() 0 19 4
A get_configuration() 0 29 2
F run() 0 74 22
A print_config() 0 11 3
B main() 0 36 6

How to fix   Complexity   

Complexity

Complex classes like sopel.cli.run often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python2.7
2
# coding=utf-8
3
"""
4
Sopel - An IRC Bot
5
Copyright 2008, Sean B. Palmer, inamidst.com
6
Copyright © 2012-2014, Elad Alfassa <[email protected]>
7
Licensed under the Eiffel Forum License 2.
8
9
https://sopel.chat
10
"""
11
from __future__ import unicode_literals, absolute_import, print_function, division
12
13
import argparse
14
import logging
15
import os
16
import platform
17
import signal
18
import sys
19
import time
20
21
from sopel import bot, config, logger, tools, __version__
22
from . import utils
23
24
if sys.version_info < (2, 7):
25
    tools.stderr('Error: Requires Python 2.7 or later. Try python2.7 sopel')
26
    sys.exit(1)
27
if sys.version_info.major == 2:
28
    tools.stderr('Warning: Python 2.x is near end of life. Sopel support at that point is TBD.')
29
if sys.version_info.major == 3 and sys.version_info.minor < 3:
30
    tools.stderr('Error: When running on Python 3, Python 3.3 is required.')
31
    sys.exit(1)
32
33
LOGGER = logging.getLogger(__name__)
34
35
ERR_CODE = 1
36
"""Error code: program exited with an error"""
37
ERR_CODE_NO_RESTART = 2
38
"""Error code: program exited with an error and should not be restarted
39
40
This error code is used to prevent systemd from restarting the bot when it
41
encounters such an error case.
42
"""
43
44
45
def run(settings, pid_file, daemon=False):
46
    delay = 20
47
48
    if not settings.core.ca_certs:
49
        tools.stderr(
50
            'Could not open CA certificates file. SSL will not work properly!')
51
52
    def signal_handler(sig, frame):
53
        if sig == signal.SIGUSR1 or sig == signal.SIGTERM or sig == signal.SIGINT:
54
            LOGGER.warning('Got quit signal, shutting down.')
55
            p.quit('Closing')
56
        elif sig == signal.SIGUSR2 or sig == signal.SIGILL:
57
            LOGGER.warning('Got restart signal, shutting down and restarting.')
58
            p.restart('Restarting')
59
60
    # Define empty variable `p` for bot
61
    p = None
62
    while True:
63
        if p and p.hasquit:  # Check if `hasquit` was set for bot during disconnected phase
64
            break
65
        try:
66
            p = bot.Sopel(settings, daemon=daemon)
67
            if hasattr(signal, 'SIGUSR1'):
68
                signal.signal(signal.SIGUSR1, signal_handler)
69
            if hasattr(signal, 'SIGTERM'):
70
                signal.signal(signal.SIGTERM, signal_handler)
71
            if hasattr(signal, 'SIGINT'):
72
                signal.signal(signal.SIGINT, signal_handler)
73
            if hasattr(signal, 'SIGUSR2'):
74
                signal.signal(signal.SIGUSR2, signal_handler)
75
            if hasattr(signal, 'SIGILL'):
76
                signal.signal(signal.SIGILL, signal_handler)
77
            p.setup()
78
        except KeyboardInterrupt:
79
            break
80
        except Exception:
81
            # In that case, there is nothing we can do.
82
            # If the bot can't setup itself, then it won't run.
83
            # This is a critical case scenario, where the user should have
84
            # direct access to the exception traceback right in the console.
85
            # Besides, we can't know if logging has been set up or not, so
86
            # we can't rely on that here.
87
            tools.stderr('Unexpected error in bot setup')
88
            raise
89
90
        try:
91
            p.run(settings.core.host, int(settings.core.port))
92
        except KeyboardInterrupt:
93
            break
94
        except Exception:
95
            err_log = logging.getLogger('sopel.exceptions')
96
            err_log.exception('Critical exception in core')
97
            err_log.error('----------------------------------------')
98
            # TODO: This should be handled by command_start
99
            # All we should need here is a return value, but replacing the
100
            # os._exit() call below (at the end) broke ^C.
101
            # This one is much harder to test, so until that one's sorted it
102
            # isn't worth the risk of trying to remove this one.
103
            os.unlink(pid_file)
104
            os._exit(1)
105
106
        if not isinstance(delay, int):
107
            break
108
        if p.wantsrestart:
109
            return -1
110
        if p.hasquit:
111
            break
112
        LOGGER.warning('Disconnected. Reconnecting in %s seconds...', delay)
113
        time.sleep(delay)
114
    # TODO: This should be handled by command_start
115
    # All we should need here is a return value, but making this
116
    # a return makes Sopel hang on ^C after it says "Closed!"
117
    os.unlink(pid_file)
118
    os._exit(0)
119
120
121
def add_legacy_options(parser):
122
    # TL;DR: option -d/--fork is not deprecated.
123
    # When the legacy action is replaced in Sopel 8, 'start' will become the
124
    # new default action, with its arguments.
125
    # The option -d/--fork is used by both actions (start and legacy),
126
    # and it has the same meaning and behavior, therefore it is not deprecated.
127
    parser.add_argument("-d", '--fork', action="store_true",
128
                        dest="daemonize",
129
                        help="Daemonize Sopel.")
130
    parser.add_argument("-q", '--quit', action="store_true", dest="quit",
131
                        help=(
132
                            "Gracefully quit Sopel "
133
                            "(deprecated, and will be removed in Sopel 8; "
134
                            "use ``sopel stop`` instead)"))
135
    parser.add_argument("-k", '--kill', action="store_true", dest="kill",
136
                        help=(
137
                            "Kill Sopel "
138
                            "(deprecated, and will be removed in Sopel 8; "
139
                            "use ``sopel stop --kill`` instead)"))
140
    parser.add_argument("-r", '--restart', action="store_true", dest="restart",
141
                        help=(
142
                            "Restart Sopel "
143
                            "(deprecated, and will be removed in Sopel 8; "
144
                            "use `sopel restart` instead)"))
145
    parser.add_argument("-l", '--list', action="store_true",
146
                        dest="list_configs",
147
                        help=(
148
                            "List all config files found"
149
                            "(deprecated, and will be removed in Sopel 8; "
150
                            "use ``sopel-config list`` instead)"))
151
    parser.add_argument('--quiet', action="store_true", dest="quiet",
152
                        help="Suppress all output")
153
    parser.add_argument('-w', '--configure-all', action='store_true',
154
                        dest='wizard',
155
                        help=(
156
                            "Run the configuration wizard "
157
                            "(deprecated, and will be removed in Sopel 8; "
158
                            "use `sopel configure` instead)"))
159
    parser.add_argument('--configure-modules', action='store_true',
160
                        dest='mod_wizard',
161
                        help=(
162
                            "Run the configuration wizard, but only for the "
163
                            "module configuration options "
164
                            "(deprecated, and will be removed in Sopel 8; "
165
                            "use ``sopel configure --modules`` instead)"))
166
    parser.add_argument('-v', action="store_true",
167
                        dest='version_legacy',
168
                        help=(
169
                            "Show version number and exit "
170
                            "(deprecated, and will be removed in Sopel 8; "
171
                            "use ``-V/--version`` instead)"))
172
    parser.add_argument('-V', '--version', action='store_true',
173
                        dest='version',
174
                        help='Show version number and exit')
175
176
177
def build_parser():
178
    """Build an ``argparse.ArgumentParser`` for the bot"""
179
    parser = argparse.ArgumentParser(description='Sopel IRC Bot',
180
                                     usage='%(prog)s [options]')
181
    add_legacy_options(parser)
182
    utils.add_common_arguments(parser)
183
184
    subparsers = parser.add_subparsers(
185
        title='subcommands',
186
        description='List of Sopel\'s subcommands',
187
        dest='action',
188
        metavar='{start,configure,stop,restart}')
189
190
    # manage `legacy` subcommand
191
    parser_legacy = subparsers.add_parser('legacy')
192
    add_legacy_options(parser_legacy)
193
    utils.add_common_arguments(parser_legacy)
194
195
    # manage `start` subcommand
196
    parser_start = subparsers.add_parser(
197
        'start',
198
        description='Start a Sopel instance. '
199
                    'This command requires an existing configuration file '
200
                    'that can be generated with ``sopel configure``.',
201
        help='Start a Sopel instance')
202
    parser_start.add_argument(
203
        '-d', '--fork',
204
        dest='daemonize',
205
        action='store_true',
206
        default=False,
207
        help='Run Sopel as a daemon (fork). This bot will safely run in the '
208
             'background. The instance will be named after the name of the '
209
             'configuration file used to run it. '
210
             'To stop it, use ``sopel stop`` (with the same configuration).')
211
    parser_start.add_argument(
212
        '--quiet',
213
        action="store_true",
214
        dest="quiet",
215
        help="Suppress all output")
216
    utils.add_common_arguments(parser_start)
217
218
    # manage `configure` subcommand
219
    parser_configure = subparsers.add_parser(
220
        'configure',
221
        description='Run the configuration wizard. It can be used to create '
222
                    'a new configuration file or to update an existing one.',
223
        help='Sopel\'s Wizard tool')
224
    parser_configure.add_argument(
225
        '--modules',
226
        action='store_true',
227
        default=False,
228
        dest='modules',
229
        help='Check for Sopel plugins that require configuration, and run '
230
             'their configuration wizards.')
231
    utils.add_common_arguments(parser_configure)
232
233
    # manage `stop` subcommand
234
    parser_stop = subparsers.add_parser(
235
        'stop',
236
        description='Stop a running Sopel instance. '
237
                    'This command determines the instance to quit by the name '
238
                    'of the configuration file used ("default", or the one '
239
                    'from the ``-c``/``--config`` option). '
240
                    'This command should be used when the bot is running in '
241
                    'the background from ``sopel start -d``, and should not '
242
                    'be used when Sopel is managed by a process manager '
243
                    '(like systemd or supervisor).',
244
        help='Stop a running Sopel instance')
245
    parser_stop.add_argument(
246
        '-k', '--kill',
247
        action='store_true',
248
        default=False,
249
        help='Kill Sopel without a graceful quit')
250
    parser_stop.add_argument(
251
        '--quiet',
252
        action="store_true",
253
        dest="quiet",
254
        help="Suppress all output")
255
    utils.add_common_arguments(parser_stop)
256
257
    # manage `restart` subcommand
258
    parser_restart = subparsers.add_parser(
259
        'restart',
260
        description='Restart a running Sopel instance',
261
        help='Restart a running Sopel instance')
262
    parser_restart.add_argument(
263
        '--quiet',
264
        action="store_true",
265
        dest="quiet",
266
        help="Suppress all output")
267
    utils.add_common_arguments(parser_restart)
268
269
    return parser
270
271
272
def check_not_root():
273
    """Check if root is running the bot.
274
275
    It raises a ``RuntimeError`` if the user has root privileges on Linux or
276
    if it is the ``Administrator`` account on Windows.
277
    """
278
    opersystem = platform.system()
279
    if opersystem in ["Linux", "Darwin"]:
280
        # Linux/Mac
281
        if os.getuid() == 0 or os.geteuid() == 0:
282
            raise RuntimeError('Error: Do not run Sopel with root privileges.')
283
    elif opersystem in ["Windows"]:
284
        # Windows
285
        if os.environ.get("USERNAME") == "Administrator":
286
            raise RuntimeError('Error: Do not run Sopel as Administrator.')
287
    else:
288
        tools.stderr(
289
            "Warning: %s is an uncommon operating system platform. "
290
            "Sopel should still work, but please contact Sopel's developers "
291
            "if you experience issues."
292
            % opersystem)
293
294
295
def print_version():
296
    """Print Python version and Sopel version on stdout."""
297
    py_ver = '%s.%s.%s' % (sys.version_info.major,
298
                           sys.version_info.minor,
299
                           sys.version_info.micro)
300
    print('Sopel %s (running on Python %s)' % (__version__, py_ver))
301
    print('https://sopel.chat/')
302
303
304
def print_config(configdir):
305
    """Print list of available configurations from config directory."""
306
    configs = utils.enumerate_configs(configdir)
307
    print('Config files in %s:' % configdir)
308
    configfile = None
309
    for configfile in configs:
310
        print('\t%s' % configfile)
311
    if not configfile:
312
        print('\tNone found')
313
314
    print('-------------------------')
315
316
317
def get_configuration(options):
318
    """Get or create a configuration object from ``options``.
319
320
    :param options: argument parser's options
321
    :type options: ``argparse.Namespace``
322
    :return: a configuration object
323
    :rtype: :class:`sopel.config.Config`
324
325
    This may raise a :exc:`sopel.config.ConfigurationError` if the
326
    configuration file is invalid.
327
328
    .. seealso::
329
330
       The configuration file is loaded by
331
       :func:`~sopel.cli.run.utils.load_settings` or created using the
332
       configuration wizard.
333
334
    """
335
    try:
336
        settings = utils.load_settings(options)
337
    except config.ConfigurationNotFound as error:
338
        print(
339
            "Welcome to Sopel!\n"
340
            "I can't seem to find the configuration file, "
341
            "so let's generate it!\n")
342
        settings = utils.wizard(error.filename)
343
344
    settings._is_daemonized = options.daemonize
345
    return settings
346
347
348
def get_pid_filename(options, pid_dir):
349
    """Get the pid file name in ``pid_dir`` from the given ``options``.
350
351
    :param options: command line options
352
    :param str pid_dir: path to the pid directory
353
    :return: absolute filename of the pid file
354
355
    By default, it's ``sopel.pid``, but if a configuration filename is given
356
    in the ``options``, its basename is used to generate the filename, as:
357
    ``sopel-{basename}.pid`` instead.
358
    """
359
    name = 'sopel.pid'
360
    if options.config and options.config != 'default':
361
        basename = os.path.basename(options.config)
362
        if basename.endswith('.cfg'):
363
            basename = basename[:-4]
364
        name = 'sopel-%s.pid' % basename
365
366
    return os.path.abspath(os.path.join(pid_dir, name))
367
368
369
def get_running_pid(filename):
370
    """Retrieve the PID number from the given ``filename``.
371
372
    :param str filename: path to file to read the PID from
373
    :return: the PID number of a Sopel instance if running, ``None`` otherwise
374
    :rtype: integer
375
376
    This function tries to retrieve a PID number from the given ``filename``,
377
    as an integer, and returns ``None`` if the file is not found or if the
378
    content is not an integer.
379
    """
380
    if not os.path.isfile(filename):
381
        return
382
383
    with open(filename, 'r') as pid_file:
384
        try:
385
            return int(pid_file.read())
386
        except ValueError:
387
            pass
388
389
390
def command_start(opts):
391
    """Start a Sopel instance"""
392
    # Step One: Get the configuration file and prepare to run
393
    try:
394
        config_module = get_configuration(opts)
395
    except config.ConfigurationError as e:
396
        tools.stderr(e)
397
        return ERR_CODE_NO_RESTART
398
399
    if config_module.core.not_configured:
400
        tools.stderr('Bot is not configured, can\'t start')
401
        return ERR_CODE_NO_RESTART
402
403
    # Step Two: Handle process-lifecycle options and manage the PID file
404
    pid_dir = config_module.core.pid_dir
405
    pid_file_path = get_pid_filename(opts, pid_dir)
406
    pid = get_running_pid(pid_file_path)
407
408
    if pid is not None and tools.check_pid(pid):
409
        tools.stderr('There\'s already a Sopel instance running '
410
                     'with this config file.')
411
        tools.stderr('Try using either the `sopel stop` '
412
                     'or the `sopel restart` command.')
413
        return ERR_CODE
414
415
    if opts.daemonize:
416
        child_pid = os.fork()
417
        if child_pid != 0:
418
            return
419
420
    with open(pid_file_path, 'w') as pid_file:
421
        pid_file.write(str(os.getpid()))
422
423
    # Step Three: Run Sopel
424
    ret = run(config_module, pid_file_path)
425
426
    # Step Four: Shutdown Clean-Up
427
    os.unlink(pid_file_path)
428
429
    if ret == -1:
430
        # Restart
431
        os.execv(sys.executable, ['python'] + sys.argv)
432
    else:
433
        # Quit
434
        return ret
435
436
437
def command_configure(opts):
438
    """Sopel Configuration Wizard"""
439
    configpath = utils.find_config(opts.configdir, opts.config)
440
    if opts.modules:
441
        utils.plugins_wizard(configpath)
442
    else:
443
        utils.wizard(configpath)
444
445
446
def command_stop(opts):
447
    """Stop a running Sopel instance"""
448
    # Get Configuration
449
    try:
450
        settings = utils.load_settings(opts)
451
    except config.ConfigurationNotFound as error:
452
        tools.stderr('Configuration "%s" not found' % error.filename)
453
        return ERR_CODE
454
455
    if settings.core.not_configured:
456
        tools.stderr('Sopel is not configured, can\'t stop')
457
        return ERR_CODE
458
459
    # Configure logging
460
    logger.setup_logging(settings)
461
462
    # Get Sopel's PID
463
    filename = get_pid_filename(opts, settings.core.pid_dir)
464
    pid = get_running_pid(filename)
465
466
    if pid is None or not tools.check_pid(pid):
467
        tools.stderr('Sopel is not running!')
468
        return ERR_CODE
469
470
    # Stop Sopel
471
    if opts.kill:
472
        tools.stderr('Killing the Sopel')
473
        os.kill(pid, signal.SIGKILL)
474
        return
475
476
    tools.stderr('Signaling Sopel to stop gracefully')
477
    if hasattr(signal, 'SIGUSR1'):
478
        os.kill(pid, signal.SIGUSR1)
479
    else:
480
        # Windows will not generate SIGTERM itself
481
        # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal
482
        os.kill(pid, signal.SIGTERM)
483
484
485
def command_restart(opts):
486
    """Restart a running Sopel instance"""
487
    # Get Configuration
488
    try:
489
        settings = utils.load_settings(opts)
490
    except config.ConfigurationNotFound as error:
491
        tools.stderr('Configuration "%s" not found' % error.filename)
492
        return ERR_CODE
493
494
    if settings.core.not_configured:
495
        tools.stderr('Sopel is not configured, can\'t stop')
496
        return ERR_CODE
497
498
    # Configure logging
499
    logger.setup_logging(settings)
500
501
    # Get Sopel's PID
502
    filename = get_pid_filename(opts, settings.core.pid_dir)
503
    pid = get_running_pid(filename)
504
505
    if pid is None or not tools.check_pid(pid):
506
        tools.stderr('Sopel is not running!')
507
        return ERR_CODE
508
509
    tools.stderr('Asking Sopel to restart')
510
    if hasattr(signal, 'SIGUSR2'):
511
        os.kill(pid, signal.SIGUSR2)
512
    else:
513
        # Windows will not generate SIGILL itself
514
        # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal
515
        os.kill(pid, signal.SIGILL)
516
517
518
def command_legacy(opts):
519
    """Legacy Sopel run script
520
521
    The ``legacy`` command manages the old-style ``sopel`` command line tool.
522
    Most of its features are replaced by the following commands:
523
524
    * ``sopel start`` replaces the default behavior (run the bot)
525
    * ``sopel stop`` replaces the ``--quit/--kill`` options
526
    * ``sopel restart`` replaces the ``--restart`` option
527
    * ``sopel configure`` replaces the
528
      ``-w/--configure-all/--configure-modules`` options
529
530
    The ``-v`` option for "version" is deprecated, ``-V/--version`` should be
531
    used instead.
532
533
    .. seealso::
534
535
       The github issue `#1471`__ tracks various changes requested for future
536
       versions of Sopel, some of them related to this legacy command.
537
538
       .. __: https://github.com/sopel-irc/sopel/issues/1471
539
540
    """
541
    # Step One: Handle "No config needed" options
542
    if opts.version:
543
        print_version()
544
        return
545
    elif opts.version_legacy:
546
        tools.stderr(
547
            'WARNING: option -v is deprecated; '
548
            'use `sopel -V/--version` instead')
549
        print_version()
550
        return
551
552
    configpath = utils.find_config(opts.configdir, opts.config)
553
554
    if opts.wizard:
555
        tools.stderr(
556
            'WARNING: option -w/--configure-all is deprecated; '
557
            'use `sopel configure` instead')
558
        utils.wizard(configpath)
559
        return
560
561
    if opts.mod_wizard:
562
        tools.stderr(
563
            'WARNING: option --configure-modules is deprecated; '
564
            'use `sopel configure --modules` instead')
565
        utils.plugins_wizard(configpath)
566
        return
567
568
    if opts.list_configs:
569
        tools.stderr(
570
            'WARNING: option --list is deprecated; '
571
            'use `sopel-config list` instead')
572
        print_config(opts.configdir)
573
        return
574
575
    # Step Two: Get the configuration file and prepare to run
576
    try:
577
        config_module = get_configuration(opts)
578
    except config.ConfigurationError as e:
579
        tools.stderr(e)
580
        return ERR_CODE_NO_RESTART
581
582
    if config_module.core.not_configured:
583
        tools.stderr('Bot is not configured, can\'t start')
584
        return ERR_CODE_NO_RESTART
585
586
    # Step Three: Handle process-lifecycle options and manage the PID file
587
    pid_dir = config_module.core.pid_dir
588
    pid_file_path = get_pid_filename(opts, pid_dir)
589
    old_pid = get_running_pid(pid_file_path)
590
591
    if old_pid is not None and tools.check_pid(old_pid):
592
        if not opts.quit and not opts.kill and not opts.restart:
593
            tools.stderr(
594
                'There\'s already a Sopel instance running with this config file')
595
            tools.stderr(
596
                'Try using either the `sopel stop` command or the `sopel restart` command')
597
            return ERR_CODE
598
        elif opts.kill:
599
            tools.stderr(
600
                'WARNING: option -k/--kill is deprecated; '
601
                'use `sopel stop --kill` instead')
602
            tools.stderr('Killing the Sopel')
603
            os.kill(old_pid, signal.SIGKILL)
604
            return
605
        elif opts.quit:
606
            tools.stderr(
607
                'WARNING: options -q/--quit is deprecated; '
608
                'use `sopel stop` instead')
609
            tools.stderr('Signaling Sopel to stop gracefully')
610
            if hasattr(signal, 'SIGUSR1'):
611
                os.kill(old_pid, signal.SIGUSR1)
612
            else:
613
                # Windows will not generate SIGTERM itself
614
                # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal
615
                os.kill(old_pid, signal.SIGTERM)
616
            return
617
        elif opts.restart:
618
            tools.stderr(
619
                'WARNING: options --restart is deprecated; '
620
                'use `sopel restart` instead')
621
            tools.stderr('Asking Sopel to restart')
622
            if hasattr(signal, 'SIGUSR2'):
623
                os.kill(old_pid, signal.SIGUSR2)
624
            else:
625
                # Windows will not generate SIGILL itself
626
                # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal
627
                os.kill(old_pid, signal.SIGILL)
628
            return
629
    elif opts.kill or opts.quit or opts.restart:
630
        tools.stderr('Sopel is not running!')
631
        return ERR_CODE
632
633
    if opts.daemonize:
634
        child_pid = os.fork()
635
        if child_pid != 0:
636
            return
637
    with open(pid_file_path, 'w') as pid_file:
638
        pid_file.write(str(os.getpid()))
639
640
    # Step Four: Initialize and run Sopel
641
    ret = run(config_module, pid_file_path)
642
    os.unlink(pid_file_path)
643
    if ret == -1:
644
        os.execv(sys.executable, ['python'] + sys.argv)
645
    else:
646
        return ret
647
648
649
def main(argv=None):
650
    """Sopel run script entry point"""
651
    try:
652
        # Step One: Parse The Command Line
653
        parser = build_parser()
654
655
        # make sure to have an action first (`legacy` by default)
656
        # TODO: `start` should be the default in Sopel 8
657
        argv = argv or sys.argv[1:]
658
        if not argv:
659
            argv = ['legacy']
660
        elif argv[0].startswith('-') and argv[0] not in ['-h', '--help']:
661
            argv = ['legacy'] + argv
662
663
        opts = parser.parse_args(argv)
664
665
        # Step Two: "Do not run as root" checks
666
        try:
667
            check_not_root()
668
        except RuntimeError as err:
669
            tools.stderr('%s' % err)
670
            return ERR_CODE
671
672
        # Step Three: Handle command
673
        action = getattr(opts, 'action', 'legacy')
674
        command = {
675
            'legacy': command_legacy,
676
            'start': command_start,
677
            'configure': command_configure,
678
            'stop': command_stop,
679
            'restart': command_restart,
680
        }.get(action)
681
        return command(opts)
682
    except KeyboardInterrupt:
683
        print("\n\nInterrupted")
684
        return ERR_CODE
685
686
687
if __name__ == '__main__':
688
    sys.exit(main())
689