|
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
|
|
|
|