From bc61576262a3a7a61719ff5073e76a50772d4d88 Mon Sep 17 00:00:00 2001 From: pachuco Date: Fri, 15 Jul 2016 10:02:42 +0300 Subject: [PATCH 1/4] Implemented a bot restart command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapted from 3b7dcad in PR 1103. I just removed some stuff that broke. — dgw --- sopel/__init__.py | 2 ++ sopel/irc.py | 7 +++++++ sopel/modules/admin.py | 13 +++++++++++++ sopel/run_script.py | 8 +++++++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/sopel/__init__.py b/sopel/__init__.py index bdd8ba6ed2..22f3a051c4 100644 --- a/sopel/__init__.py +++ b/sopel/__init__.py @@ -100,6 +100,8 @@ def signal_handler(sig, frame): if not isinstance(delay, int): break + if p.restart: + return -1 if p.hasquit: break stderr('Warning: Disconnected. Reconnecting in %s seconds...' % delay) diff --git a/sopel/irc.py b/sopel/irc.py index 7b259ab08d..e99cd03f67 100644 --- a/sopel/irc.py +++ b/sopel/irc.py @@ -62,6 +62,7 @@ def __init__(self, config): self.ca_certs = ca_certs self.enabled_capabilities = set() self.hasquit = False + self.restart = False self.sending = threading.RLock() self.writing_lock = threading.Lock() @@ -176,6 +177,12 @@ def initiate_connect(self, host, port): print('KeyboardInterrupt') self.quit('KeyboardInterrupt') + def restart(self, message): + """Disconnect from IRC and restart the bot.""" + self.write(['QUIT'], message) + self.restart = True + self.hasquit = True + def quit(self, message): """Disconnect from IRC and close the bot.""" self.write(['QUIT'], message) diff --git a/sopel/modules/admin.py b/sopel/modules/admin.py index 85e271a3ae..98f703cc1f 100644 --- a/sopel/modules/admin.py +++ b/sopel/modules/admin.py @@ -74,6 +74,19 @@ def part(bot, trigger): bot.part(channel) +@sopel.module.require_privmsg +@sopel.module.require_owner +@sopel.module.commands('restart') +@sopel.module.priority('low') +def restart(bot, trigger): + """Restart the bot. This is an owner-only command.""" + quit_message = trigger.group(2) + if not quit_message: + quit_message = 'Restart on command from %s' % trigger.nick + + bot.restart(quit_message) + + @sopel.module.require_privmsg @sopel.module.require_owner @sopel.module.commands('quit') diff --git a/sopel/run_script.py b/sopel/run_script.py index 536ec8138a..5895aac7a0 100755 --- a/sopel/run_script.py +++ b/sopel/run_script.py @@ -330,7 +330,13 @@ def main(argv=None): pid_file.write(str(os.getpid())) # Step Seven: Initialize and run Sopel - run(config_module, pid_file_path) + ret = run(config_module, pid_file_path) + os.unlink(pid_file_path) + if ret == -1: + os.execv(sys.executable, ['python'] + sys.argv) + else: + return ret + except KeyboardInterrupt: print("\n\nInterrupted") return ERR_CODE From d0cab50f4e4275ca10fccf21bd46b2a423cf5b28 Mon Sep 17 00:00:00 2001 From: dgw Date: Mon, 28 May 2018 07:28:27 -0500 Subject: [PATCH 2/4] Un-shadow bot.restart() to fix .restart command "TypeError: 'bool' object is not callable" is all .restart would do, because `restart` was defined as a class method and then overridden as a boolean flag when the class was instantiated. Kept the restart() method name, renamed the flag, Bob's your uncle. --- sopel/__init__.py | 2 +- sopel/irc.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sopel/__init__.py b/sopel/__init__.py index 22f3a051c4..ed339c6de2 100644 --- a/sopel/__init__.py +++ b/sopel/__init__.py @@ -100,7 +100,7 @@ def signal_handler(sig, frame): if not isinstance(delay, int): break - if p.restart: + if p.wantsrestart: return -1 if p.hasquit: break diff --git a/sopel/irc.py b/sopel/irc.py index e99cd03f67..7eb5c4a043 100644 --- a/sopel/irc.py +++ b/sopel/irc.py @@ -62,7 +62,7 @@ def __init__(self, config): self.ca_certs = ca_certs self.enabled_capabilities = set() self.hasquit = False - self.restart = False + self.wantsrestart = False self.sending = threading.RLock() self.writing_lock = threading.Lock() @@ -180,7 +180,7 @@ def initiate_connect(self, host, port): def restart(self, message): """Disconnect from IRC and restart the bot.""" self.write(['QUIT'], message) - self.restart = True + self.wantsrestart = True self.hasquit = True def quit(self, message): From d392ed58192ba65acd3ff511464ef0212c9654b5 Mon Sep 17 00:00:00 2001 From: dgw Date: Tue, 29 May 2018 07:13:55 -0500 Subject: [PATCH 3/4] core: implement --restart (-r) command-line option --- sopel/__init__.py | 7 +++++++ sopel/run_script.py | 21 +++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/sopel/__init__.py b/sopel/__init__.py index ed339c6de2..bab8a340e1 100644 --- a/sopel/__init__.py +++ b/sopel/__init__.py @@ -71,6 +71,9 @@ def signal_handler(sig, frame): if sig == signal.SIGUSR1 or sig == signal.SIGTERM or sig == signal.SIGINT: stderr('Got quit signal, shutting down.') p.quit('Closing') + elif sig == signal.SIGUSR2 or sig == signal.SIGILL: + stderr('Got restart signal.') + p.restart('Restarting') while True: try: p = bot.Sopel(config, daemon=daemon) @@ -80,6 +83,10 @@ def signal_handler(sig, frame): signal.signal(signal.SIGTERM, signal_handler) if hasattr(signal, 'SIGINT'): signal.signal(signal.SIGINT, signal_handler) + if hasattr(signal, 'SIGUSR2'): + signal.signal(signal.SIGUSR2, signal_handler) + if hasattr(signal, 'SIGILL'): + signal.signal(signal.SIGILL, signal_handler) sopel.logger.setup_logging(p) p.run(config.core.host, int(config.core.port)) except KeyboardInterrupt: diff --git a/sopel/run_script.py b/sopel/run_script.py index 5895aac7a0..f7662b05aa 100755 --- a/sopel/run_script.py +++ b/sopel/run_script.py @@ -117,7 +117,7 @@ def find_config(config_dir, name, extension='.cfg'): def build_parser(): """Build an ``argparse.ArgumentParser`` for the bot""" parser = argparse.ArgumentParser(description='Sopel IRC Bot', - usage='%(prog)s [options]') + usage='%(prog)s [options]') parser.add_argument('-c', '--config', metavar='filename', help='use a specific configuration file') parser.add_argument("-d", '--fork', action="store_true", @@ -126,6 +126,8 @@ def build_parser(): help="Gracefully quit Sopel") parser.add_argument("-k", '--kill', action="store_true", dest="kill", help="Kill Sopel") + parser.add_argument("-r", '--restart', action="store_true", dest="restart", + help="Restart Sopel") parser.add_argument("-l", '--list', action="store_true", dest="list_configs", help="List all config files found") @@ -303,9 +305,9 @@ def main(argv=None): old_pid = get_running_pid(pid_file_path) if old_pid is not None and tools.check_pid(old_pid): - if not opts.quit and not opts.kill: + if not opts.quit and not opts.kill and not opts.restart: stderr('There\'s already a Sopel instance running with this config file') - stderr('Try using the --quit or the --kill options') + stderr('Try using either the --quit, --restart, or --kill option') return ERR_CODE elif opts.kill: stderr('Killing the Sopel') @@ -316,9 +318,20 @@ def main(argv=None): if hasattr(signal, 'SIGUSR1'): os.kill(old_pid, signal.SIGUSR1) else: + # Windows will not generate SIGTERM itself + # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal os.kill(old_pid, signal.SIGTERM) return - elif opts.kill or opts.quit: + elif opts.restart: + stderr('Asking Sopel to restart') + if hasattr(signal, 'SIGUSR2'): + os.kill(old_pid, signal.SIGUSR2) + else: + # Windows will not generate SIGILL itself + # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal + os.kill(old_pid, signal.SIGILL) + return + elif opts.kill or opts.quit or opts.restart: stderr('Sopel is not running!') return ERR_CODE From c0c9995e774311090783359496764bbfc1a62e00 Mon Sep 17 00:00:00 2001 From: dgw Date: Thu, 31 Jan 2019 09:51:49 -0600 Subject: [PATCH 4/4] sopel.run: document why Pachuco's cleanup was left out A few commits back, the commit adding the "restart" command originally tried to do some cleanup, too. It was left out of the final version of PR #1333 due to breaking the use of ^C to quit Sopel interactively. These TODO comments will serve as a reminder to eventually figure out why the hell shit broke from something that should be simple, like replacing os._exit() calls (not a great habit) with return values. Something that shouldn't cause any change in function, but somehow did. --- sopel/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sopel/__init__.py b/sopel/__init__.py index bab8a340e1..5e00070e6b 100644 --- a/sopel/__init__.py +++ b/sopel/__init__.py @@ -102,6 +102,11 @@ def signal_handler(sig, frame): logfile.write(trace) logfile.write('----------------------------------------\n\n') logfile.close() + # TODO: This should be handled in run_script + # All we should need here is a return value, but replacing the + # os._exit() call below (at the end) broke ^C. + # This one is much harder to test, so until that one's sorted it + # isn't worth the risk of trying to remove this one. os.unlink(pid_file) os._exit(1) @@ -113,5 +118,8 @@ def signal_handler(sig, frame): break stderr('Warning: Disconnected. Reconnecting in %s seconds...' % delay) time.sleep(delay) + # TODO: This should be handled in run_script + # All we should need here is a return value, but making this + # a return makes Sopel hang on ^C after it says "Closed!" os.unlink(pid_file) os._exit(0)