# http://inamidst.com/saxo/ # Created by Sean B. Palmer import argparse import codecs import os import signal import sys import threading import time # Save PEP 3122! if "." in __name__: from . import common else: import common sys.stderr = codecs.getwriter("utf-8")(sys.stderr.detach()) sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) E_PYTHON_VERSION = """ Your version of the python programming language is too old to run saxo. Use a newer version if available. Otherwise you can get a newer version from here: http://www.python.org/ Or install one using your system's package manager. If you are using OS X for example, python3 can be installed using homebrew: http://mxcl.github.io/homebrew/ """ try: import sqlite3 except ImportError: print("Error: sqlite3 is not installed", file=sys.stderr) print("Please build Python against the sqlite libraries", file=sys.stderr) sys.exit(1) else: if not sqlite3.threadsafety: print("Error: Your sqlite3 is not thread-safe", file=sys.stderr) sys.exit(1) if sys.version_info < (3, 3): common.error("requires python 3.3 or later", E_PYTHON_VERSION) def action(function): action.names[function.__name__] = function return function action.names = {} lock = threading.Lock() def debug(*args, **kargs): with lock: try: print(*args, **kargs) sys.stdout.flush() except BrokenPipeError: sys.exit() # Options def version(args, v): print("saxo %s" % v) USAGE = """ Usage: saxo -v saxo create [ directory ] saxo [ -f ] [ --log ] start [ directory ] saxo stop [ directory ] saxo active [ directory ] Try `saxo -h` for more detailed usage """ def usage(args, v): version(args, v) print(USAGE.rstrip()) HELP = """ Usage: saxo -v / --version Show the current saxo version saxo create [ directory ] Create a default configuration file saxo [ Options ] start [ directory ] Starts a bot. Options: -f / --foreground Don't run the bot as a daemon -l / --log Save output to a log file in saxo base saxo stop [ directory ] Stops a bot saxo active [ directory ] Shows whether a bot is active """ def help(args, v): version(args, v) print(HELP.rstrip()) def base_option(args): if args.directory is None: return os.path.expanduser("~/.saxo") return args.directory # Actions # eval $(./saxo bash) @action def bash(arg): # Save PEP 3122! if "." in __name__: from . import saxo else: import saxo import pipes path = pipes.quote(os.path.join(saxo.path, "commands")) print('export PATH=$PATH:%s' % path) pythonpath = pipes.quote(saxo.path) if "PYTHONPATH" in os.environ: print('export PYTHONPATH=$PYTHONPATH:%s' % pythonpath) else: print('export PYTHONPATH=%s' % pythonpath) @action def create(args): # Save PEP 3122! if "." in __name__: from . import create else: import create # def default(base=None) # — create.py create.default(args.directory) return 0 @action def shell(args): base = base_option(args) # Save PEP 3122! if "." in __name__: from .saxo import path as saxo_path else: from saxo import path as saxo_path def subshell(base, commands): path = os.environ.get("PATH", "") os.environ["PATH"] = commands + os.pathsep + path os.environ["PYTHONPATH"] = saxo_path os.environ["SAXO_SHELL"] = "1" os.environ["SAXO_BASE"] = base os.environ["SAXO_BOT"] = "saxo" os.environ["SAXO_COMMANDS"] = commands os.environ["SAXO_NICK"] = os.environ["USER"] os.environ["SAXO_SENDER"] = os.environ["USER"] shell = os.environ.get("SHELL", "sh") os.system(shell) commands = os.path.join(base, "commands") subshell(base, commands) return 0 @action def console(args): # TODO: Reduce code duplication base = base_option(args) # Save PEP 3122! if "." in __name__: from . import saxo else: import saxo path = os.environ.get("PATH", "") commands = os.path.join(base, "commands") # TODO: Reload symlinks # e.g. the pip-installation test changes them os.environ["PATH"] = commands + os.pathsep + path os.environ["PYTHONPATH"] = saxo.path os.environ["SAXO_SHELL"] = "1" os.environ["SAXO_BASE"] = base os.environ["SAXO_BOT"] = "saxo" os.environ["SAXO_COMMANDS"] = commands os.environ["SAXO_NICK"] = os.environ["USER"] os.environ["SAXO_SENDER"] = os.environ["USER"] def do(cmd, arg): import subprocess cmd = os.path.join(os.environ["SAXO_COMMANDS"], cmd) try: octets = subprocess.check_output([cmd, arg]) except Exception as err: return "Error: %s" % err return octets.decode("utf-8") while True: sys.stdout.write("> ") sys.stdout.flush() try: i = sys.stdin.readline() except: break i = i.rstrip("\r\n") if " " in i: cmd, arg = i.split(" ", 1) else: cmd, arg = i, "" if cmd.startswith("."): saxo.client(cmd[1:], *eval("(%s)" % arg)) elif cmd.startswith("!"): print(do(cmd[1:], arg).rstrip("\r\n")) elif cmd == "saxo.path": print(saxo.path) elif cmd == "saxo.base": print(base) elif not cmd: break else: print("Unknown command:", repr(cmd)) @action def log(args): base = base_option(args) print(os.path.join(base, "log")) @action def start(args): import json # Quit any previously connected instances if "." in __name__: from .saxo import client, data else: from saxo import client, data base = base_option(args) try: client("quit", base=base) except FileNotFoundError as err: ... except ConnectionRefusedError as err: ... except PermissionError as err: debug("Error: Unable to connect to internal socket", file=sys.stderr) debug("Check permissions on the config dir", file=sys.stderr) sys.stderr.flush() sys.exit(1) else: debug("Warning: Client may already have been running!") sys.stdout.flush() if not os.path.isdir(base): debug("Error: The saxo config dir does not exist", file=sys.stderr) debug("Location: %s" % base, file=sys.stderr) sys.stderr.flush() sys.exit(1) pidfile = os.path.join(base, "pid") if not args.foreground: # Save PEP 3122! if "." in __name__: from . import daemon else: import daemon if args.log: log = os.path.join(base, "log") if os.path.exists(log): import shutil modified = time.gmtime(os.path.getmtime("saxo")) log2 = time.strftime("log-%Y%m%d-%H%M%S.txt", modified) shutil.move(log, os.path.join(base, log2)) output = open(log, "w") else: output = open(os.devnull, "w") daemon.start(pidfile, output) else: # This is duplicated variatim from daemon.py import atexit with open(pidfile, "w") as f: f.write(str(os.getpid()) + "\n") def delete_pidfile(): if os.path.isfile(pidfile): os.remove(pidfile) atexit.register(delete_pidfile) if args.action == "start": # Otherwise you get recursion if args.action == "restart" os.environ["SAXO_BASE"] = base sys_args = json.dumps(args.original) data("args", sys_args, command="script.py", check=False) # Save PEP 3122! if "." in __name__: from . import irc else: import irc irc.start(base) return 0 @action def restart(args): import json base = base_option(args) os.environ["SAXO_BASE"] = base if "." in __name__: from .saxo import data, version else: from saxo import data, version sys_args = json.loads(data("args", command="script.py", check=False)) if sys_args[0] == "restart": debug("Recursion detected!", sys_args) debug("Not restarting") sys.exit(1) debug("Stopping existing instance...") stop(args) argv = [sys.argv[0]] + sys_args debug("Restarting using:", argv) main(argv, version) @action def status(args): base = base_option(args) # Quit any previously connected instances if "." in __name__: from .saxo import client else: from saxo import client try: client("noop", base=base) except FileNotFoundError as err: debug("not running") except ConnectionRefusedError as err: debug("not running") else: debug("running") @action def stop(args): base = base_option(args) if "." in __name__: from .saxo import client else: from saxo import client try: client("quit", base=base) except: ... pidfile = os.path.join(base, "pid") if not os.path.exists(pidfile): common.error("There is no bot currently running") with open(pidfile, encoding="ascii") as f: text = f.read() pid = int(text.rstrip("\n")) # TODO: Make this less crude # This can give a ProcessLookupError: [Errno 3] No such process try: os.kill(pid, signal.SIGTERM) except ProcessLookupError: ... # os.kill(pid, signal.SIGKILL) return 0 @action def test(args): if args.directory is not None: common.error("Tests cannot be run in conjunction with a directory") import queue import shutil import socket import subprocess import tempfile # Save PEP 3122! if "." in __name__: from . import saxo else: import saxo saxo_script = sys.modules["__main__"].__file__ saxo_test_server = os.path.join(saxo.path, "test", "server.py") tmp = tempfile.mkdtemp() outgoing = queue.Queue() if not sys.executable: common.error("Couldn't find the python executable") if not os.path.isdir(tmp): common.error("There is no %s directory" % tmp) print("python executable:", sys.executable) print("saxo path:", saxo.path) print("saxo script:", saxo_script) print("saxo test server:", saxo_test_server) print() sys.stdout.flush() def run_server(): server = subprocess.Popen([sys.executable, "-u", saxo_test_server], stdout=subprocess.PIPE) for line in server.stdout: line = line.decode("utf-8", "replace") line = line.rstrip("\n") outgoing.put("S: " + line) outgoing.put("Server finished") def run_client(): saxo_test = os.path.join(tmp, "saxo-test") outgoing.put("Running in %s" % saxo_test) cmd = [sys.executable, saxo_script, "create", saxo_test] code = subprocess.call(cmd) if code: print("Error creating the client configuration") sys.exit(1) test_config = os.path.join(saxo.path, "test", "config") saxo_test_config = os.path.join(saxo_test, "config") with open(test_config) as f: with open(saxo_test_config, "w") as w: for line in f: line = line.replace("localhost", socket.gethostname()) w.write(line) # shutil.copy2(test_config, saxo_test_config) client = subprocess.Popen([sys.executable, "-u", saxo_script, "-f", "start", saxo_test], stdout=subprocess.PIPE) for line in client.stdout: line = line.decode("utf-8", "replace") line = line.rstrip("\n") outgoing.put("C: " + line) manifest01 = {"commands", "config", "database.sqlite3", "pid", "plugins"} manifest02 = manifest01 | {"client.sock"} if set(os.listdir(saxo_test)) <= manifest01: shutil.rmtree(saxo_test) elif set(os.listdir(saxo_test)) <= manifest02: outgoing.put("Warning: client.sock had not been removed") shutil.rmtree(saxo_test) else: outgoing.put("Refusing to delete the saxo test directory") outgoing.put("Data was found which does not match the manifest") outgoing.put(saxo_test) common.thread(run_server) common.thread(run_client) error = False completed = False client_buffer = [] while True: line = outgoing.get() if line.startswith("S: "): print(line) if line.startswith("S: ERROR"): error = True if line.startswith("S: Tests complete"): completed = True if not line.startswith("S: Test"): for c in client_buffer: print(c) del client_buffer[:] elif line.startswith("C: "): client_buffer.append(line) else: print(line) sys.stdout.flush() if line == "Server finished": break if not os.listdir(tmp): os.rmdir(tmp) else: print("Warning: Did not remove:", tmp) if completed and (not error): sys.exit(0) else: sys.exit(1) @action def soft(args): import site if args.directory == "on": # Save PEP 3122! if "." in __name__: from .saxo import path as saxo_path else: from saxo import path as saxo_path if not os.path.isdir(site.USER_SITE): os.makedirs(site.USER_SITE) debug("Warning: Created", site.USER_SITE) saxo_pth = os.path.join(site.USER_SITE, "saxo.pth") if not os.path.exists(saxo_pth): with open(saxo_pth, "w") as f: f.write(saxo_path) debug("Created saxo.pth in", site.USER_SITE) else: debug("Error: %s already exists" % saxo_pth, file=sys.stderr) sys.exit(1) elif args.directory == "off": saxo_pth = os.path.join(site.USER_SITE, "saxo.pth") if os.path.isfile(saxo_pth): os.remove(saxo_pth) else: debug("Error: %s not found" % saxo_pth, file=sys.stderr) sys.exit(1) debug("Warning: May also need to remove %s" % site.USER_SITE) else: debug("Error: Was expecting 'on' or 'off'", file=sys.stderr) sys.exit(1) @action def info(args): # Save PEP 3122! if "." in __name__: from .saxo import path as saxo_path else: from saxo import path as saxo_path directory = args.directory args.directory = None base = base_option(args) args.directory = directory def check(path): if os.path.isdir(path): print(path) else: debug("-", file=sys.stderr) sys.exit(1) if args.directory == "base": check(base) elif args.directory == "path": check(saxo_path) elif args.directory == "base.commands": check(os.path.join(base, "commands")) elif args.directory == "path.commands": check(os.path.join(saxo_path, "commands")) else: debug("?", file=sys.stderr) sys.exit(1) def main(argv, v): # NOTE: No default for argv, because what script name would we use? description = "Control saxo irc bot instances" parser = argparse.ArgumentParser(description=description, add_help=False) parser.add_argument("-f", "--foreground", action="store_true", help="run in the foreground instead of as a daemon") parser.add_argument("-h", "--help", action="store_true", help="show a short help message") parser.add_argument("-o", "--output", metavar="filename", help="removed command: use -l / --log with no arg instead") parser.add_argument("-l", "--log", action="store_true", help="log to the standard log file") parser.add_argument("-v", "--version", action="store_true", help="show the current saxo version") parser.add_argument("action", nargs="?", help="use --help to show the available actions") parser.add_argument("directory", nargs="?", help="the path to the saxo configuration directory") args = parser.parse_args(argv[1:]) args.original = argv[1:] if args.output: print("Sorry, -o / --output has been removed!") print("Use -l / --log instead, with no arguments") sys.exit(2) if args.help: help(args, v) elif args.version: version(args, v) elif args.action: if args.action in action.names: code = action.names[args.action](args) if isinstance(code, int): sys.exit(code) elif args.action.startswith("."): # Save PEP 3122! if "." in __name__: from . import saxo else: import saxo def do(cmd, arg): import subprocess cmd = os.path.join(saxo.path, "commands", cmd) try: octets = subprocess.check_output([cmd, arg]) except Exception as err: raise err # return "Error: %s" % err return octets.decode("utf-8").rstrip("\r\n") print(do(args.action[1:], args.directory or "")) else: common.error("unrecognised action: %s" % args.action, code=2) else: usage(args, v)