From 57003f9d03802904008c4337b65721d7b6f6c06c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 22 Apr 2015 16:56:07 +0200 Subject: [PATCH 01/12] Introducing daemon mode --- nemubot/__main__.py | 48 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 992c3ad..0e4433e 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -38,6 +38,9 @@ def main(): default=["./modules/"], help="directory to use as modules store") + parser.add_argument("-d", "--debug", action="store_true", + help="don't deamonize, keep in foreground") + parser.add_argument("-l", "--logfile", default="./nemubot.log", help="Path to store logs") @@ -64,6 +67,38 @@ def main(): args.files = [ x for x in map(os.path.abspath, args.files)] args.modules_path = [ x for x in map(os.path.abspath, args.modules_path)] + # Daemonize + if not args.debug: + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s" % err) + sys.exit(1) + + os.setsid() + os.umask(0) + os.chdir('/') + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s" % err) + sys.exit(1) + + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + # Setup loggin interface import logging logger = logging.getLogger("nemubot") @@ -72,11 +107,12 @@ def main(): formatter = logging.Formatter( '%(asctime)s %(name)s %(levelname)s %(message)s') - ch = logging.StreamHandler() - ch.setFormatter(formatter) - if args.verbose < 2: - ch.setLevel(logging.INFO) - logger.addHandler(ch) + if args.debug: + ch = logging.StreamHandler() + ch.setFormatter(formatter) + if args.verbose < 2: + ch.setLevel(logging.INFO) + logger.addHandler(ch) fh = logging.FileHandler(args.logfile) fh.setFormatter(formatter) @@ -148,7 +184,7 @@ def main(): "the prompt.") context.quit() - print("Waiting for other threads shuts down...") + logger.info("Waiting for other threads shuts down...") sys.exit(0) if __name__ == "__main__": From 93f7061e08f26f41f6dacf7b128717323b0012b8 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 8 May 2015 00:20:14 +0200 Subject: [PATCH 02/12] Remove prompt at launch --- nemubot/__main__.py | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 0e4433e..80ae7f9 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -155,35 +155,9 @@ def main(): for module in args.module: __import__(module) - print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__, - os.getpid())) - while True: - from nemubot.prompt.reset import PromptReset - try: - context.start() - if prmpt.run(context): - break - except PromptReset as e: - if e.type == "quit": - break + context.start() + context.join() - try: - import imp - # Reload all other modules - imp.reload(nemubot) - imp.reload(nemubot.prompt) - nemubot.reload() - import nemubot.bot - context = nemubot.bot.hotswap(context) - prmpt = nemubot.prompt.hotswap(prmpt) - print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % - nemubot.__version__) - except: - logger.exception("\033[1;31mUnable to reload the prompt due to " - "errors.\033[0m Fix them before trying to reload " - "the prompt.") - - context.quit() logger.info("Waiting for other threads shuts down...") sys.exit(0) From 3ecab04f195b785ed6e9c5e89ec6195e5043c910 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 9 May 2015 13:20:56 +0200 Subject: [PATCH 03/12] Do a proper close on SIGINT and SIGTERM --- nemubot/__main__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 80ae7f9..8fa2b51 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -18,6 +18,7 @@ def main(): import os + import signal import sys # Parse command line arguments @@ -155,6 +156,12 @@ def main(): for module in args.module: __import__(module) + # Signals handling + def sighandler(signum, frame): + context.quit() + signal.signal(signal.SIGINT, sighandler) + signal.signal(signal.SIGTERM, sighandler) + context.start() context.join() From 3ec1173c00ab75785f64cfc1c9088c3437567e8f Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 12 May 2015 10:41:13 +0200 Subject: [PATCH 04/12] Catch SIGHUP: deep reload --- nemubot/__main__.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 8fa2b51..2ec45a9 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -157,14 +157,40 @@ def main(): __import__(module) # Signals handling - def sighandler(signum, frame): + def sigtermhandler(signum, frame): + """On SIGTERM and SIGINT, quit nicely""" context.quit() - signal.signal(signal.SIGINT, sighandler) - signal.signal(signal.SIGTERM, sighandler) + signal.signal(signal.SIGINT, sigtermhandler) + signal.signal(signal.SIGTERM, sigtermhandler) + def sighuphandler(signum, frame): + """On SIGHUP, perform a deep reload""" + import imp + + # Reload nemubot Python modules + imp.reload(nemubot) + nemubot.reload() + + # Hotswap context + import nemubot.bot + context = nemubot.bot.hotswap(context) + + # Reload configuration file + for path in args.files: + if os.path.isfile(path): + context.sync_queue.put_nowait(["loadconf", path]) + signal.signal(signal.SIGHUP, sighuphandler) + + # Here we go! context.start() - context.join() + # context can change when performing an hotswap, always join the latest context + oldcontext = None + while oldcontext != context: + oldcontext = context + context.join() + + # Wait for consumers logger.info("Waiting for other threads shuts down...") sys.exit(0) From e720d3c99ace346119d14ece4de0890a6f659862 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 16 May 2015 10:24:08 +0200 Subject: [PATCH 05/12] Extract deamonize to a dedicated function that can be called from anywhere --- nemubot/__init__.py | 38 ++++++++++++++++++++++++++++++++++++++ nemubot/__main__.py | 31 ++----------------------------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 84403e0..f21ff64 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -40,6 +40,44 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) +def daemonize(): + """Detach the running process to run as a daemon + """ + + import os + import sys + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s\n" % err) + sys.exit(1) + + os.setsid() + os.umask(0) + os.chdir('/') + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s\n" % err) + sys.exit(1) + + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + def reload(): """Reload code of all Python modules used by nemubot """ diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 2ec45a9..73cc91a 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -70,35 +70,8 @@ def main(): # Daemonize if not args.debug: - try: - pid = os.fork() - if pid > 0: - sys.exit(0) - except OSError as err: - sys.stderr.write("Unable to fork: %s" % err) - sys.exit(1) - - os.setsid() - os.umask(0) - os.chdir('/') - - try: - pid = os.fork() - if pid > 0: - sys.exit(0) - except OSError as err: - sys.stderr.write("Unable to fork: %s" % err) - sys.exit(1) - - sys.stdout.flush() - sys.stderr.flush() - si = open(os.devnull, 'r') - so = open(os.devnull, 'a+') - se = open(os.devnull, 'a+') - - os.dup2(si.fileno(), sys.stdin.fileno()) - os.dup2(so.fileno(), sys.stdout.fileno()) - os.dup2(se.fileno(), sys.stderr.fileno()) + from nemubot import daemonize + daemonize() # Setup loggin interface import logging From 8380e565a4d29908187c2171913e06a956c1195a Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 15 May 2015 00:05:12 +0200 Subject: [PATCH 06/12] Catch SIGUSR1: log threads stack traces --- nemubot/__main__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 73cc91a..c08e30e 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -154,6 +154,15 @@ def main(): context.sync_queue.put_nowait(["loadconf", path]) signal.signal(signal.SIGHUP, sighuphandler) + def sigusr1handler(signum, frame): + """On SIGHUSR1, display stacktraces""" + import traceback + for threadId, stack in sys._current_frames().items(): + logger.debug("########### Thread %d:\n%s", + threadId, + "".join(traceback.format_stack(stack))) + signal.signal(signal.SIGUSR1, sigusr1handler) + # Here we go! context.start() From 19d8ede570077ad9bdb9612b914c3f854ce058c9 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 18 May 2015 07:36:49 +0200 Subject: [PATCH 07/12] New CLI argument: --pidfile, path to store the daemon PID --- nemubot/__init__.py | 5 +++++ nemubot/__main__.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index f21ff64..90a4d01 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -40,6 +40,11 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) +def attach(pid): + print("TODO, attach to %d" % pid) + return 0 + + def daemonize(): """Detach the running process to run as a daemon """ diff --git a/nemubot/__main__.py b/nemubot/__main__.py index c08e30e..59e609c 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -42,6 +42,9 @@ def main(): parser.add_argument("-d", "--debug", action="store_true", help="don't deamonize, keep in foreground") + parser.add_argument("-P", "--pidfile", default="./nemubot.pid", + help="Path to the file where store PID") + parser.add_argument("-l", "--logfile", default="./nemubot.log", help="Path to store logs") @@ -64,15 +67,33 @@ def main(): # Resolve relatives paths args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) + args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) args.logfile = os.path.abspath(os.path.expanduser(args.logfile)) args.files = [ x for x in map(os.path.abspath, args.files)] args.modules_path = [ x for x in map(os.path.abspath, args.modules_path)] + # Check if an instance is already launched + if args.pidfile is not None and os.path.isfile(args.pidfile): + with open(args.pidfile, "r") as f: + pid = int(f.readline()) + try: + os.kill(pid, 0) + except OSError: + pass + else: + from nemubot import attach + sys.exit(attach(pid)) + # Daemonize if not args.debug: from nemubot import daemonize daemonize() + # Store PID to pidfile + if args.pidfile is not None: + with open(args.pidfile, "w+") as f: + f.write(str(os.getpid())) + # Setup loggin interface import logging logger = logging.getLogger("nemubot") From fcca0cd5ec29a56981c26f9b676aa3839fc65c64 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 20 May 2015 06:12:50 +0200 Subject: [PATCH 08/12] New argument: --socketfile that create a socket for internal communication --- nemubot/__init__.py | 4 ++-- nemubot/__main__.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 90a4d01..4c1401c 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -40,8 +40,8 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) -def attach(pid): - print("TODO, attach to %d" % pid) +def attach(socketfile): + print("TODO: Attach to Unix socket at: %s" % socketfile) return 0 diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 59e609c..1c9b310 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -45,6 +45,9 @@ def main(): parser.add_argument("-P", "--pidfile", default="./nemubot.pid", help="Path to the file where store PID") + parser.add_argument("-S", "--socketfile", default="./nemubot.sock", + help="path where open the socket for internal communication") + parser.add_argument("-l", "--logfile", default="./nemubot.log", help="Path to store logs") @@ -68,6 +71,7 @@ def main(): # Resolve relatives paths args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) + args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile)) args.logfile = os.path.abspath(os.path.expanduser(args.logfile)) args.files = [ x for x in map(os.path.abspath, args.files)] args.modules_path = [ x for x in map(os.path.abspath, args.modules_path)] @@ -187,6 +191,11 @@ def main(): # Here we go! context.start() + if args.socketfile: + from nemubot.server.socket import SocketListener + context.add_server(SocketListener(context.add_server, "master_socket", + sock_location=args.socketfile)) + # context can change when performing an hotswap, always join the latest context oldcontext = None while oldcontext != context: From 5e9056c7a4023eec23e00402b021b80362a8cb89 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 24 May 2015 16:47:22 +0200 Subject: [PATCH 09/12] Fix and improve reload process --- nemubot/__init__.py | 3 +++ nemubot/__main__.py | 15 +++++++++++---- nemubot/bot.py | 12 ++++++++---- nemubot/server/abstract.py | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 4c1401c..fee2eda 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -89,6 +89,9 @@ def reload(): import imp + import nemubot.bot + imp.reload(nemubot.bot) + import nemubot.channel imp.reload(nemubot.channel) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 1c9b310..620f4f6 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -141,7 +141,8 @@ def main(): # Register the hook for futur import from nemubot.importer import ModuleFinder - sys.meta_path.append(ModuleFinder(context.modules_paths, context.add_module)) + module_finder = ModuleFinder(context.modules_paths, context.add_module) + sys.meta_path.append(module_finder) # Load requested configuration files for path in args.files: @@ -164,6 +165,9 @@ def main(): def sighuphandler(signum, frame): """On SIGHUP, perform a deep reload""" import imp + nonlocal nemubot, context, module_finder + + logger.debug("SIGHUP receive, iniate reload procedure...") # Reload nemubot Python modules imp.reload(nemubot) @@ -173,6 +177,11 @@ def main(): import nemubot.bot context = nemubot.bot.hotswap(context) + # Reload ModuleFinder + sys.meta_path.remove(module_finder) + module_finder = ModuleFinder(context.modules_paths, context.add_module) + sys.meta_path.append(module_finder) + # Reload configuration file for path in args.files: if os.path.isfile(path): @@ -188,9 +197,6 @@ def main(): "".join(traceback.format_stack(stack))) signal.signal(signal.SIGUSR1, sigusr1handler) - # Here we go! - context.start() - if args.socketfile: from nemubot.server.socket import SocketListener context.add_server(SocketListener(context.add_server, "master_socket", @@ -200,6 +206,7 @@ def main(): oldcontext = None while oldcontext != context: oldcontext = context + context.start() context.join() # Wait for consumers diff --git a/nemubot/bot.py b/nemubot/bot.py index db2f653..a12dd7a 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -143,6 +143,7 @@ class Bot(threading.Thread): from select import select from nemubot.server import _lock, _rlist, _wlist, _xlist + logger.info("Starting main loop") self.stop = False while not self.stop: with _lock: @@ -208,6 +209,7 @@ class Bot(threading.Thread): from nemubot.tools.config import load_file load_file(path, self) self.sync_queue.task_done() + logger.info("Ending main loop") @@ -509,14 +511,16 @@ def hotswap(bak): bak.stop = True if bak.event_timer is not None: bak.event_timer.cancel() + + # Unload modules + for mod in [k for k in bak.modules.keys()]: + bak.unload_module(mod) + + # Save datastore bak.datastore.close() new = Bot(str(bak.ip), bak.modules_paths, bak.datastore) new.servers = bak.servers - new.modules = bak.modules - new.modules_configuration = bak.modules_configuration - new.events = bak.events - new.hooks = bak.hooks new._update_event_timer() return new diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index 99d10d5..b3c70ca 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -115,7 +115,7 @@ class AbstractServer(io.IOBase): """ self._sending_queue.put(self.format(message)) - self.logger.debug("Message '%s' appended to Queue", message) + self.logger.debug("Message '%s' appended to write queue", message) if self not in _wlist: _wlist.append(self) From 27f1b74eef5ae3433235c6a8caa617173d3bb910 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 16 Jul 2015 20:31:34 +0200 Subject: [PATCH 10/12] Remove legacy prompt --- modules/cmd_server.py | 204 ------------------------------------- nemubot/__init__.py | 5 - nemubot/__main__.py | 4 - nemubot/prompt/__init__.py | 144 -------------------------- nemubot/prompt/builtins.py | 132 ------------------------ nemubot/prompt/error.py | 23 ----- nemubot/prompt/reset.py | 23 ----- setup.py | 1 - 8 files changed, 536 deletions(-) delete mode 100644 modules/cmd_server.py delete mode 100644 nemubot/prompt/__init__.py delete mode 100644 nemubot/prompt/builtins.py delete mode 100644 nemubot/prompt/error.py delete mode 100644 nemubot/prompt/reset.py diff --git a/modules/cmd_server.py b/modules/cmd_server.py deleted file mode 100644 index 8fdadb5..0000000 --- a/modules/cmd_server.py +++ /dev/null @@ -1,204 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import traceback -import sys - -from nemubot.hooks import hook - -nemubotversion = 3.4 -NODATA = True - - -def getserver(toks, context, prompt, mandatory=False, **kwargs): - """Choose the server in toks or prompt. - This function modify the tokens list passed as argument""" - - if len(toks) > 1 and toks[1] in context.servers: - return context.servers[toks.pop(1)] - elif not mandatory or prompt.selectedServer: - return prompt.selectedServer - else: - from nemubot.prompt.error import PromptError - raise PromptError("Please SELECT a server or give its name in argument.") - - -@hook("prompt_cmd", "close") -def close(toks, context, **kwargs): - """Disconnect and forget (remove from the servers list) the server""" - srv = getserver(toks, context=context, mandatory=True, **kwargs) - - if srv.close(): - del context.servers[srv.id] - return 0 - return 1 - - -@hook("prompt_cmd", "connect") -def connect(toks, **kwargs): - """Make the connexion to a server""" - srv = getserver(toks, mandatory=True, **kwargs) - - return not srv.open() - - -@hook("prompt_cmd", "disconnect") -def disconnect(toks, **kwargs): - """Close the connection to a server""" - srv = getserver(toks, mandatory=True, **kwargs) - - return not srv.close() - - -@hook("prompt_cmd", "discover") -def discover(toks, context, **kwargs): - """Discover a new bot on a server""" - srv = getserver(toks, context=context, mandatory=True, **kwargs) - - if len(toks) > 1 and "!" in toks[1]: - bot = context.add_networkbot(srv, name) - return not bot.connect() - else: - print(" %s is not a valid fullname, for example: " - "nemubot!nemubotV3@bot.nemunai.re" % ''.join(toks[1:1])) - return 1 - - -@hook("prompt_cmd", "join") -@hook("prompt_cmd", "leave") -@hook("prompt_cmd", "part") -def join(toks, **kwargs): - """Join or leave a channel""" - srv = getserver(toks, mandatory=True, **kwargs) - - if len(toks) <= 2: - print("%s: not enough arguments." % toks[0]) - return 1 - - if toks[0] == "join": - if len(toks) > 2: - srv.write("JOIN %s %s" % (toks[1], toks[2])) - else: - srv.write("JOIN %s" % toks[1]) - - elif toks[0] == "leave" or toks[0] == "part": - if len(toks) > 2: - srv.write("PART %s :%s" % (toks[1], " ".join(toks[2:]))) - else: - srv.write("PART %s" % toks[1]) - - return 0 - - -@hook("prompt_cmd", "save") -def save_mod(toks, context, **kwargs): - """Force save module data""" - if len(toks) < 2: - print("save: not enough arguments.") - return 1 - - wrn = 0 - for mod in toks[1:]: - if mod in context.modules: - context.modules[mod].save() - print("save: module `%s´ saved successfully" % mod) - else: - wrn += 1 - print("save: no module named `%s´" % mod) - return wrn - - -@hook("prompt_cmd", "send") -def send(toks, **kwargs): - """Send a message on a channel""" - srv = getserver(toks, mandatory=True, **kwargs) - - # Check the server is connected - if not srv.connected: - print ("send: server `%s' not connected." % srv.id) - return 2 - - if len(toks) <= 3: - print ("send: not enough arguments.") - return 1 - - if toks[1] not in srv.channels: - print ("send: channel `%s' not authorized in server `%s'." - % (toks[1], srv.id)) - return 3 - - from nemubot.message import Text - srv.send_response(Text(" ".join(toks[2:]), server=None, - to=[toks[1]])) - return 0 - - -@hook("prompt_cmd", "zap") -def zap(toks, **kwargs): - """Hard change connexion state""" - srv = getserver(toks, mandatory=True, **kwargs) - - srv.connected = not srv.connected - - -@hook("prompt_cmd", "top") -def top(toks, context, **kwargs): - """Display consumers load information""" - print("Queue size: %d, %d thread(s) running (counter: %d)" % - (context.cnsr_queue.qsize(), - len(context.cnsr_thrd), - context.cnsr_thrd_size)) - if len(context.events) > 0: - print("Events registered: %d, next in %d seconds" % - (len(context.events), - context.events[0].time_left.seconds)) - else: - print("No events registered") - - for th in context.cnsr_thrd: - if th.is_alive(): - print(("#" * 15 + " Stack trace for thread %u " + "#" * 15) % - th.ident) - traceback.print_stack(sys._current_frames()[th.ident]) - - -@hook("prompt_cmd", "netstat") -def netstat(toks, context, **kwargs): - """Display sockets in use and many other things""" - if len(context.network) > 0: - print("Distant bots connected: %d:" % len(context.network)) - for name, bot in context.network.items(): - print("# %s:" % name) - print(" * Declared hooks:") - lvl = 0 - for hlvl in bot.hooks: - lvl += 1 - for hook in (hlvl.all_pre + hlvl.all_post + hlvl.cmd_rgxp + - hlvl.cmd_default + hlvl.ask_rgxp + - hlvl.ask_default + hlvl.msg_rgxp + - hlvl.msg_default): - print(" %s- %s" % (' ' * lvl * 2, hook)) - for kind in ["irc_hook", "cmd_hook", "ask_hook", "msg_hook"]: - print(" %s- <%s> %s" % (' ' * lvl * 2, kind, - ", ".join(hlvl.__dict__[kind].keys()))) - print(" * My tag: %d" % bot.my_tag) - print(" * Tags in use (%d):" % bot.inc_tag) - for tag, (cmd, data) in bot.tags.items(): - print(" - %11s: %s « %s »" % (tag, cmd, data)) - else: - print("No distant bot connected") diff --git a/nemubot/__init__.py b/nemubot/__init__.py index fee2eda..b85ce6c 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -117,11 +117,6 @@ def reload(): nemubot.message.reload() - import nemubot.prompt - imp.reload(nemubot.prompt) - - nemubot.prompt.reload() - import nemubot.server rl, wl, xl = nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist imp.reload(nemubot.server) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 620f4f6..079049c 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -135,10 +135,6 @@ def main(): if args.no_connect: context.noautoconnect = True - # Load the prompt - import nemubot.prompt - prmpt = nemubot.prompt.Prompt() - # Register the hook for futur import from nemubot.importer import ModuleFinder module_finder = ModuleFinder(context.modules_paths, context.add_module) diff --git a/nemubot/prompt/__init__.py b/nemubot/prompt/__init__.py deleted file mode 100644 index c491d99..0000000 --- a/nemubot/prompt/__init__.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import shlex -import sys -import traceback - -from nemubot.prompt import builtins - - -class Prompt: - - def __init__(self): - self.selectedServer = None - self.lastretcode = 0 - - self.HOOKS_CAPS = dict() - self.HOOKS_LIST = dict() - - def add_cap_hook(self, name, call, data=None): - self.HOOKS_CAPS[name] = lambda t, c: call(t, data=data, - context=c, prompt=self) - - def add_list_hook(self, name, call): - self.HOOKS_LIST[name] = call - - def lex_cmd(self, line): - """Return an array of tokens - - Argument: - line -- the line to lex - """ - - try: - cmds = shlex.split(line) - except: - exc_type, exc_value, _ = sys.exc_info() - sys.stderr.write(traceback.format_exception_only(exc_type, - exc_value)[0]) - return - - bgn = 0 - - # Separate commands (command separator: ;) - for i in range(0, len(cmds)): - if cmds[i][-1] == ';': - if i != bgn: - yield cmds[bgn:i] - bgn = i + 1 - - # Return rest of the command (that not end with a ;) - if bgn != len(cmds): - yield cmds[bgn:] - - def exec_cmd(self, toks, context): - """Execute the command - - Arguments: - toks -- lexed tokens to executes - context -- current bot context - """ - - if toks[0] in builtins.CAPS: - self.lastretcode = builtins.CAPS[toks[0]](toks, context, self) - elif toks[0] in self.HOOKS_CAPS: - self.lastretcode = self.HOOKS_CAPS[toks[0]](toks, context) - else: - print("Unknown command: `%s'" % toks[0]) - self.lastretcode = 127 - - def getPS1(self): - """Get the PS1 associated to the selected server""" - if self.selectedServer is None: - return "nemubot" - else: - return self.selectedServer.id - - def run(self, context): - """Launch the prompt - - Argument: - context -- current bot context - """ - - from nemubot.prompt.error import PromptError - from nemubot.prompt.reset import PromptReset - - while True: # Stopped by exception - try: - line = input("\033[0;33m%s\033[0;%dm§\033[0m " % - (self.getPS1(), 31 if self.lastretcode else 32)) - cmds = self.lex_cmd(line.strip()) - for toks in cmds: - try: - self.exec_cmd(toks, context) - except PromptReset: - raise - except PromptError as e: - print(e.message) - self.lastretcode = 128 - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, - exc_traceback) - except KeyboardInterrupt: - print("") - except EOFError: - print("quit") - return True - - -def hotswap(bak): - p = Prompt() - p.HOOKS_CAPS = bak.HOOKS_CAPS - p.HOOKS_LIST = bak.HOOKS_LIST - return p - - -def reload(): - import imp - - import nemubot.prompt.builtins - imp.reload(nemubot.prompt.builtins) - - import nemubot.prompt.error - imp.reload(nemubot.prompt.error) - - import nemubot.prompt.reset - imp.reload(nemubot.prompt.reset) diff --git a/nemubot/prompt/builtins.py b/nemubot/prompt/builtins.py deleted file mode 100644 index e78c3dd..0000000 --- a/nemubot/prompt/builtins.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -def end(toks, context, prompt): - """Quit the prompt for reload or exit""" - from nemubot.prompt.reset import PromptReset - - if toks[0] == "refresh": - raise PromptReset("refresh") - elif toks[0] == "reset": - raise PromptReset("reset") - raise PromptReset("quit") - - -def liste(toks, context, prompt): - """Show some lists""" - if len(toks) > 1: - for l in toks[1:]: - l = l.lower() - if l == "server" or l == "servers": - for srv in context.servers.keys(): - print (" - %s (state: %s) ;" % (srv, - "connected" if context.servers[srv].connected else "disconnected")) - if len(context.servers) == 0: - print (" > No server loaded") - - elif l == "mod" or l == "mods" or l == "module" or l == "modules": - for mod in context.modules.keys(): - print (" - %s ;" % mod) - if len(context.modules) == 0: - print (" > No module loaded") - - elif l in prompt.HOOKS_LIST: - f, d = prompt.HOOKS_LIST[l] - f(d, context, prompt) - - else: - print (" Unknown list `%s'" % l) - return 2 - return 0 - else: - print (" Please give a list to show: servers, ...") - return 1 - - -def load(toks, context, prompt): - """Load an XML configuration file""" - if len(toks) > 1: - from nemubot.tools.config import load_file - - for filename in toks[1:]: - load_file(filename, context) - else: - print ("Not enough arguments. `load' takes a filename.") - return 1 - - -def select(toks, context, prompt): - """Select the current server""" - if (len(toks) == 2 and toks[1] != "None" and - toks[1] != "nemubot" and toks[1] != "none"): - if toks[1] in context.servers: - prompt.selectedServer = context.servers[toks[1]] - else: - print ("select: server `%s' not found." % toks[1]) - return 1 - else: - prompt.selectedServer = None - - -def unload(toks, context, prompt): - """Unload a module""" - if len(toks) == 2 and toks[1] == "all": - for name in context.modules.keys(): - context.unload_module(name) - elif len(toks) > 1: - for name in toks[1:]: - if context.unload_module(name): - print (" Module `%s' successfully unloaded." % name) - else: - print (" No module `%s' loaded, can't unload!" % name) - return 2 - else: - print ("Not enough arguments. `unload' takes a module name.") - return 1 - - -def debug(toks, context, prompt): - """Enable/Disable debug mode on a module""" - if len(toks) > 1: - for name in toks[1:]: - if name in context.modules: - context.modules[name].DEBUG = not context.modules[name].DEBUG - if context.modules[name].DEBUG: - print (" Module `%s' now in DEBUG mode." % name) - else: - print (" Debug for module module `%s' disabled." % name) - else: - print (" No module `%s' loaded, can't debug!" % name) - return 2 - else: - print ("Not enough arguments. `debug' takes a module name.") - return 1 - - -# Register build-ins -CAPS = { - 'quit': end, # Disconnect all server and quit - 'exit': end, # Alias for quit - 'reset': end, # Reload the prompt - 'refresh': end, # Reload the prompt but save modules - 'load': load, # Load a servers or module configuration file - 'unload': unload, # Unload a module and remove it from the list - 'select': select, # Select a server - 'list': liste, # Show lists - 'debug': debug, # Pass a module in debug mode -} diff --git a/nemubot/prompt/error.py b/nemubot/prompt/error.py deleted file mode 100644 index 3d426d6..0000000 --- a/nemubot/prompt/error.py +++ /dev/null @@ -1,23 +0,0 @@ -# coding=utf-8 - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -class PromptError(Exception): - - def __init__(self, message): - super(PromptError, self).__init__(message) - self.message = message diff --git a/nemubot/prompt/reset.py b/nemubot/prompt/reset.py deleted file mode 100644 index 57da9f8..0000000 --- a/nemubot/prompt/reset.py +++ /dev/null @@ -1,23 +0,0 @@ -# coding=utf-8 - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -class PromptReset(Exception): - - def __init__(self, type): - super(PromptReset, self).__init__("Prompt reset asked") - self.type = type diff --git a/setup.py b/setup.py index 37f4aef..dc448ca 100755 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ setup( 'nemubot.hooks', 'nemubot.message', 'nemubot.message.printer', - 'nemubot.prompt', 'nemubot.server', 'nemubot.server.message', 'nemubot.tools', From 47ada614fa9c5078b89e4e9351c1875179d4e76c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 18 Jul 2015 14:01:56 +0200 Subject: [PATCH 11/12] Can attach to the main process --- nemubot/__init__.py | 67 +++++++++++++++++++++++++++++++++++++--- nemubot/__main__.py | 2 +- nemubot/server/socket.py | 13 ++++++++ 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index b85ce6c..9f02039 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -20,6 +20,7 @@ __version__ = '4.0.dev3' __author__ = 'nemunaire' from nemubot.modulecontext import ModuleContext + context = ModuleContext(None, None) @@ -40,8 +41,62 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) -def attach(socketfile): - print("TODO: Attach to Unix socket at: %s" % socketfile) +def attach(pid, socketfile): + import socket + import sys + + print("nemubot is already launched with PID %d. Attaching to Unix socket at: %s" % (pid, socketfile)) + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.connect(socketfile) + except socket.error as e: + sys.stderr.write(str(e)) + sys.stderr.write("\n") + return 1 + + from select import select + try: + print("Connection established.") + while True: + rl, wl, xl = select([sys.stdin, sock], [], []) + + if sys.stdin in rl: + line = sys.stdin.readline().strip() + if line == "exit" or line == "quit": + return 0 + elif line == "reload": + import os, signal + os.kill(pid, signal.SIGHUP) + print("Reload signal sent. Please wait...") + + elif line == "shutdown": + import os, signal + os.kill(pid, signal.SIGTERM) + print("Shutdown signal sent. Please wait...") + + elif line == "kill": + import os, signal + os.kill(pid, signal.SIGKILL) + print("Signal sent...") + return 0 + + elif line == "stack" or line == "stacks": + import os, signal + os.kill(pid, signal.SIGUSR1) + print("Debug signal sent. Consult logs.") + + else: + sock.send(line.encode() + b'\r\n') + + if sock in rl: + sys.stdout.write(sock.recv(2048).decode()) + except KeyboardInterrupt: + pass + except: + return 1 + finally: + sock.close() return 0 @@ -118,9 +173,13 @@ def reload(): nemubot.message.reload() import nemubot.server - rl, wl, xl = nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist + rl = nemubot.server._rlist + wl = nemubot.server._wlist + xl = nemubot.server._xlist imp.reload(nemubot.server) - nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist = rl, wl, xl + nemubot.server._rlist = rl + nemubot.server._wlist = wl + nemubot.server._xlist = xl nemubot.server.reload() diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 079049c..efc31bd 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -86,7 +86,7 @@ def main(): pass else: from nemubot import attach - sys.exit(attach(pid)) + sys.exit(attach(pid, args.socketfile)) # Daemonize if not args.debug: diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 052579b..f810906 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import nemubot.message as message from nemubot.message.printer.socket import Socket as SocketPrinter from nemubot.server.abstract import AbstractServer @@ -132,6 +133,18 @@ class SocketServer(AbstractServer): yield line + def parse(self, line): + import shlex + + line = line.strip().decode() + try: + args = shlex.split(line) + except ValueError: + args = line.split(' ') + + yield message.Command(cmd=args[0], args=args[1:], server=self) + + class SocketListener(AbstractServer): def __init__(self, new_server_cb, id, sock_location=None, host=None, port=None, ssl=None): From e943a556267c5c5ca2958208482ded870afaf929 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 4 Sep 2015 17:07:21 +0200 Subject: [PATCH 12/12] wip event manager --- nemubot/event/__init__.py | 65 ++++++++++++++++++++------------------- nemubot/modulecontext.py | 6 ++-- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/nemubot/event/__init__.py b/nemubot/event/__init__.py index 96f226a..345200d 100644 --- a/nemubot/event/__init__.py +++ b/nemubot/event/__init__.py @@ -24,8 +24,8 @@ class ModuleEvent: """Representation of a event initiated by a bot module""" def __init__(self, call=None, call_data=None, func=None, func_data=None, - cmp=None, cmp_data=None, interval=60, offset=0, times=1): - + cmp=None, cmp_data=None, end_call=None, end_data=None, + interval=60, offset=0, times=1, max_attempt=-1): """Initialize the event Keyword arguments: @@ -35,9 +35,12 @@ class ModuleEvent: func_data -- Argument(s) (single or dict) to pass as argument OR if no func, initial data to watch cmp -- Boolean function called to check changes cmp_data -- Argument(s) (single or dict) to pass as argument OR if no cmp, data compared to previous + end_call -- Function called when times or max_attempt reach 0 (mainly for interaction with the event manager) + end_data -- Argument(s) (single or dict) to pass as argument interval -- Time in seconds between each check (default: 60) offset -- Time in seconds added to interval before the first check (default: 0) times -- Number of times the event has to be realized before being removed; -1 for no limit (default: 1) + max_attempt -- Maximum number of times the event will be checked """ # What have we to check? @@ -46,16 +49,17 @@ class ModuleEvent: # How detect a change? self.cmp = cmp - self.cmp_data = None if cmp_data is not None: self.cmp_data = cmp_data - elif self.func is not None: + elif callable(self.func): if self.func_data is None: self.cmp_data = self.func() elif isinstance(self.func_data, dict): self.cmp_data = self.func(**self.func_data) else: self.cmp_data = self.func(self.func_data) + else: + self.cmp_data = None # What should we call when? self.call = call @@ -64,46 +68,29 @@ class ModuleEvent: else: self.call_data = func_data - # Store times - self.offset = timedelta(seconds=offset) # Time to wait before the first check + # Store time between each event self.interval = timedelta(seconds=interval) - self._end = None # Cache - # How many times do this event? self.times = times - @property - def current(self): - """Return the date of the near check""" - if self.times != 0: - if self._end is None: - self._end = datetime.now(timezone.utc) + self.offset + self.interval - return self._end - return None + # Cache the time of the next occurence + self.next_occur = datetime.now(timezone.utc) + timedelta(seconds=offset) + self.interval - @property - def next(self): - """Return the date of the next check""" - if self.times != 0: - if self._end is None: - return self.current - elif self._end < datetime.now(timezone.utc): - self._end += self.interval - return self._end - return None @property def time_left(self): """Return the time left before/after the near check""" - if self.current is not None: - return self.current - datetime.now(timezone.utc) - return 99999 # TODO: 99999 is not a valid time to return + + return self.next_occur - datetime.now(timezone.utc) + def check(self): """Run a check and realized the event if this is time""" + self.max_attempt -= 1 + # Get initial data - if self.func is None: + if not callable(self.func): d_init = self.func_data elif self.func_data is None: d_init = self.func() @@ -113,7 +100,7 @@ class ModuleEvent: d_init = self.func(self.func_data) # then compare with current data - if self.cmp is None: + if not callable(self.cmp): if self.cmp_data is None: rlz = True else: @@ -138,3 +125,19 @@ class ModuleEvent: self.call(d_init, **self.call_data) else: self.call(d_init, self.call_data) + + # Is it finished? + if self.times == 0 or self.max_attempt == 0: + if not callable(self.end_call): + pass # TODO: log a WARN here + else: + if self.end_data is None: + self.end_call() + elif isinstance(self.end_data, dict): + self.end_call(**self.end_data) + else: + self.end_call(self.end_data) + + # Not finished, ready to next one! + else: + self.next_occur += self.interval diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 5b47278..0814215 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -51,7 +51,7 @@ class ModuleContext: self.config = ModuleState("module") self.hooks = list() - self.events = list() + self.events = list() # Un eventManager, qui contient une liste globale et un thread global et quelques méthodes statique, mais chaque événement est exécuté dans son propre contexte :) self.debug = context.verbosity > 0 if context is not None else False # Define some callbacks @@ -79,7 +79,9 @@ class ModuleContext: def subtreat(msg): yield from context.treater.treat_msg(msg) def add_event(evt, eid=None): - return context.add_event(evt, eid, module_src=module) + eid = context.add_event(evt, eid, module_src=module) + self.events.append(eid) + return eid def del_event(evt): return context.del_event(evt, module_src=module)