From 5656b841d6ec8fb347398aec6b372f5a9fa952a0 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 14 Jan 2018 22:53:36 +0100 Subject: [PATCH] Working start/reload/reset/stop from launcher --- evennia/server/amp_client.py | 6 ++ evennia/server/evennia_launcher.py | 76 +++++++++++++--------- evennia/server/evennia_runner.py | 1 + evennia/server/portal/amp.py | 2 +- evennia/server/portal/amp_server.py | 97 ++++++++++++++++++++++------- evennia/server/server.py | 1 + evennia/utils/logger.py | 9 +++ 7 files changed, 136 insertions(+), 56 deletions(-) diff --git a/evennia/server/amp_client.py b/evennia/server/amp_client.py index bf6b3d9fc8..5c29825d23 100644 --- a/evennia/server/amp_client.py +++ b/evennia/server/amp_client.py @@ -192,15 +192,21 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol): elif operation == amp.PSYNC: # portal_session_sync # force a resync of sessions from the portal side server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata")) + elif operation == amp.SRELOAD: # server reload # shut down in reload mode + server_sessionhandler.all_sessions_portal_sync() server_sessionhandler.server.shutdown(mode='reload') + elif operation == amp.SRESET: # shut down in reset mode + server_sessionhandler.all_sessions_portal_sync() server_sessionhandler.server.shutdown(mode='reset') + elif operation == amp.SSHUTD: # server shutdown # shutdown in stop mode server_sessionhandler.server.shutdown(mode='shutdown') + else: raise Exception("operation %(op)s not recognized." % {'op': operation}) return {} diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 806f51b621..c2d29ad961 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -19,7 +19,7 @@ import shutil import importlib from distutils.version import LooseVersion from argparse import ArgumentParser -from subprocess import Popen, check_output, call, CalledProcessError, STDOUT +from subprocess import Popen, check_output, call, CalledProcessError, STDOUT, PIPE try: import cPickle as pickle @@ -88,6 +88,7 @@ SSTART = chr(15) # server start PSHUTD = chr(16) # portal (+server) shutdown SSHUTD = chr(17) # server-only shutdown PSTATUS = chr(18) # ping server or portal status +SRESET = chr(19) # shutdown server in reset mode # requirements PYTHON_MIN = '2.7' @@ -519,13 +520,18 @@ def _get_twistd_cmdline(pprofiler, sprofiler): Compile the command line for starting a Twisted application using the 'twistd' executable. """ - portal_cmd = [TWISTED_BINARY, "--logfile={}".format(PORTAL_LOGFILE), "--python={}".format(PORTAL_PY_FILE)] server_cmd = [TWISTED_BINARY, - "--logfile={}".format(PORTAL_LOGFILE), - "--python={}".format(PORTAL_PY_FILE)] + "--logfile={}".format(SERVER_LOGFILE), + "--python={}".format(SERVER_PY_FILE)] + + if os.name != 'nt': + # PID files only for UNIX + portal_cmd.append("--pidfile={}".format(PORTAL_PIDFILE)) + server_cmd.append("--pidfile={}".format(SERVER_PIDFILE)) + if pprofiler: portal_cmd.extend(["--savestats", "--profiler=cprofiler", @@ -534,6 +540,8 @@ def _get_twistd_cmdline(pprofiler, sprofiler): server_cmd.extend(["--savestats", "--profiler=cprofiler", "--profile={}".format(SPROFILER_LOGFILE)]) + + return portal_cmd, server_cmd @@ -552,7 +560,6 @@ def query_status(repeat=False): reactor.stop() def _errback(fail): - print("status fail: %s", fail) pstatus, sstatus = False, False print("Portal: {}\nServer: {}".format(wmap[pstatus], wmap[sstatus])) reactor.stop() @@ -591,7 +598,7 @@ def wait_for_status(portal_running=True, server_running=True, callback=None, err if errback: errback(prun, srun) else: - print("Timeout.") + print("Connection to Evennia timed out. Try again.") reactor.stop() else: reactor.callLater(rate, wait_for_status, @@ -613,7 +620,7 @@ def wait_for_status(portal_running=True, server_running=True, callback=None, err if errback: errback(portal_running, server_running) else: - print("Timeout.") + print("Connection to Evennia timed out. Try again.") reactor.stop() else: reactor.callLater(rate, wait_for_status, @@ -622,14 +629,13 @@ def wait_for_status(portal_running=True, server_running=True, callback=None, err return send_instruction(PSTATUS, None, _callback, _errback) - - # ------------------------------------------------------------ # # Operational functions # # ------------------------------------------------------------ + def start_evennia(pprofiler=False, sprofiler=False): """ This will start Evennia anew by launching the Evennia Portal (which in turn @@ -638,8 +644,12 @@ def start_evennia(pprofiler=False, sprofiler=False): """ portal_cmd, server_cmd = _get_twistd_cmdline(pprofiler, sprofiler) + def _fail(fail): + print(fail) + reactor.stop() + def _server_started(*args): - print("... Server started.\nEvennia running.", args) + print("... Server started.\nEvennia running.") reactor.stop() def _portal_started(*args): @@ -653,21 +663,22 @@ def start_evennia(pprofiler=False, sprofiler=False): reactor.stop() else: print("Server starting {}...".format("(under cProfile)" if pprofiler else "")) - send_instruction(SSTART, server_cmd, _server_started) + send_instruction(SSTART, server_cmd, _server_started, _fail) def _portal_not_running(fail): print("Portal starting {}...".format("(under cProfile)" if sprofiler else "")) try: - Popen(portal_cmd, env=getenv()) + Popen(portal_cmd, env=getenv(), bufsize=-1) except Exception as e: print(PROCESS_ERROR.format(component="Portal", traceback=e)) + reactor.stop() wait_for_status(True, None, _portal_started) send_instruction(PSTATUS, None, _portal_running, _portal_not_running) reactor.run() -def reload_evennia(sprofiler=False): +def reload_evennia(sprofiler=False, reset=False): """ This will instruct the Portal to reboot the Server component. @@ -675,18 +686,23 @@ def reload_evennia(sprofiler=False): _, server_cmd = _get_twistd_cmdline(False, sprofiler) def _server_restarted(*args): - print("... Server re-started.", args) + print("... Server re-started.") reactor.stop() def _server_reloaded(*args): - print("... Server reloaded.", args) + print("... Server {}.".format("reset" if reset else "reloaded")) reactor.stop() + def _server_not_running(*args): + send_instruction(SSTART, server_cmd) + wait_for_status(True, True, _server_reloaded) + def _portal_running(response): _, srun, _, _ = _parse_status(response) if srun: - print("Server reloading ...") - send_instruction(SRELOAD, server_cmd, _server_reloaded) + print("Server {}...".format("resetting" if reset else "reloading")) + send_instruction(SRESET if reset else SRELOAD, server_cmd) + wait_for_status(True, False, _server_not_running) else: print("Server down. Re-starting ...") send_instruction(SSTART, server_cmd, _server_restarted) @@ -710,7 +726,7 @@ def stop_evennia(): reactor.stop() def _server_stopped(*args): - print("... Server stopped.\nStopping Portal ...", args) + print("... Server stopped.\nStopping Portal ...") send_instruction(PSHUTD, {}) wait_for_status(False, None, _portal_stopped) @@ -718,7 +734,8 @@ def stop_evennia(): prun, srun, ppid, spid = _parse_status(response) if srun: print("Server stopping ...") - send_instruction(SSHUTD, {}, _server_stopped) + send_instruction(SSHUTD, {}) + wait_for_status(True, False, _server_stopped) else: print("Server already stopped.\nStopping Portal ...") send_instruction(PSHUTD, {}) @@ -726,6 +743,7 @@ def stop_evennia(): def _portal_not_running(fail): print("Evennia is not running.") + reactor.stop() send_instruction(PSTATUS, None, _portal_running, _portal_not_running) reactor.run() @@ -741,8 +759,8 @@ def stop_server_only(): reactor.stop() def _portal_running(response): - _, server_running = [stat == 'RUNNING' for stat in response['status'].split("|")] - if server_running: + _, srun, _, _ = _parse_status(response) + if srun: print("Server stopping ...") send_instruction(SSHUTD, {}) wait_for_status(True, False, _server_stopped) @@ -1239,7 +1257,7 @@ def init_game_directory(path, check_db=True): AMP_INTERFACE = settings.AMP_INTERFACE SERVER_PY_FILE = os.path.join(EVENNIA_LIB, "server", "server.py") - PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "portal", "portal", "portal.py") + PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "server", "portal", "portal.py") SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid") PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid") @@ -1602,9 +1620,6 @@ def main(): "service", metavar="component", nargs='?', default="all", help=("Which component to operate on: " "'server', 'portal' or 'all' (default if not set).")) - parser.add_argument( - "--status", action='store_true', dest='get_status', - default=None, help='Get current server status.') parser.epilog = ( "Common usage: evennia start|stop|reload. Django-admin database commands:" "evennia migration|flush|shell|dbshell (see the django documentation for more " @@ -1656,11 +1671,6 @@ def main(): print(ERROR_INITSETTINGS) sys.exit() - if args.get_status: - init_game_directory(CURRENT_DIR, check_db=True) - query_status() - sys.exit() - if args.dummyrunner: # launch the dummy runner init_game_directory(CURRENT_DIR, check_db=True) @@ -1673,13 +1683,17 @@ def main(): # launch menu for operation init_game_directory(CURRENT_DIR, check_db=True) run_menu() - elif option in ('sstart', 'sreload', 'sstop', 'ssstop', 'start', 'reload', 'stop'): + elif option in ('status', 'sstart', 'sreload', 'sreset', 'sstop', 'ssstop', 'start', 'reload', 'stop'): # operate the server directly init_game_directory(CURRENT_DIR, check_db=True) + if option == "status": + query_status() if option == "sstart": start_evennia(False, args.profiler) elif option == 'sreload': reload_evennia(args.profiler) + elif option == 'sreset': + reload_evennia(args.profiler, reset=True) elif option == 'sstop': stop_evennia() elif option == 'ssstop': diff --git a/evennia/server/evennia_runner.py b/evennia/server/evennia_runner.py index 27fc187211..d920c181e8 100644 --- a/evennia/server/evennia_runner.py +++ b/evennia/server/evennia_runner.py @@ -346,6 +346,7 @@ def main(): del portal_argv[-2] # Start processes + print("server_argv:", server_argv, portal_argv) start_services(server_argv, portal_argv, doexit=args.doexit) diff --git a/evennia/server/portal/amp.py b/evennia/server/portal/amp.py index 8f08b46b0f..6da13905c2 100644 --- a/evennia/server/portal/amp.py +++ b/evennia/server/portal/amp.py @@ -89,8 +89,8 @@ def catch_traceback(func): if not _LOGGER: from evennia.utils import logger as _LOGGER _LOGGER.log_trace() - print("error", err) raise # make sure the error is visible on the other side of the connection too + print(err) return decorator diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index 1163012371..6b746c10a8 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -8,7 +8,7 @@ import os import sys from twisted.internet import protocol from evennia.server.portal import amp -from subprocess import Popen +from subprocess import Popen, STDOUT, PIPE from evennia.utils import logger @@ -48,6 +48,8 @@ class AMPServerFactory(protocol.ServerFactory): self.portal = portal self.protocol = AMPServerProtocol self.broadcasts = [] + self.server_connection = None + self.disconnect_callbacks = {} def buildProtocol(self, addr): """ @@ -80,12 +82,45 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): """ # start the Server - process = Popen(server_twistd_cmd, env=getenv()) - # store the pid for future reference + try: + process = Popen(server_twistd_cmd, env=getenv(), bufsize=-1, stdout=PIPE, stderr=STDOUT) + except Exception: + self.factory.portal.server_process_id = None + logger.log_trace() + return 0 + # there is a short window before the server logger is up where we must + # catch the stdout of the Server or eventual tracebacks will be lost. + with process.stdout as out: + logger.log_server(out.read()) + + # store the pid and launch argument for future reference self.factory.portal.server_process_id = process.pid self.factory.portal.server_twistd_cmd = server_twistd_cmd return process.pid + def connectionLost(self, reason): + """ + Set up a simple callback mechanism to let the amp-server wait for a connection to close. + + """ + callback, args, kwargs = self.factory.disconnect_callbacks.pop(self, (None, None, None)) + if callback: + try: + callback(*args, **kwargs) + except Exception: + logger.log_trace() + + def wait_for_disconnect(self, callback, *args, **kwargs): + """ + Add a callback for when this connection is lost. + + Args: + callback (callable): Will be called with *args, **kwargs + once this protocol is disconnected. + + """ + self.factory.disconnect_callbacks[self] = (callback, args, kwargs) + def stop_server(self, mode='shutdown'): """ Shut down server in one or more modes. @@ -95,11 +130,11 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): """ if mode == 'reload': - self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.SRELOAD) - if mode == 'reset': - self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.SRESET) - if mode == 'shutdown': - self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.SSHUTD) + self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRELOAD) + elif mode == 'reset': + self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET) + elif mode == 'shutdown': + self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD) # sending amp data @@ -148,8 +183,8 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): """ # check if the server is connected - server_connected = any(1 for prtcl in self.factory.broadcasts - if prtcl is not self and prtcl.transport.connected) + server_connected = (self.factory.server_connection and + self.factory.server_connection.transport.connected) server_pid = self.factory.portal.server_process_id portal_pid = os.getpid() @@ -180,14 +215,14 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): def _retval(success, txt): return {"result": amp.dumps((success, txt))} - server_connected = any(1 for prtcl in self.factory.broadcasts - if prtcl is not self and prtcl.transport.connected) + server_connected = (self.factory.server_connection and + self.factory.server_connection.transport.connected) server_pid = self.factory.portal.server_process_id logger.log_msg("AMP SERVER operation == %s received" % (ord(operation))) logger.log_msg("AMP SERVER arguments: %s" % (amp.loads(arguments))) - if operation == amp.SSTART: # portal start + if operation == amp.SSTART: # portal start #15 # first, check if server is already running if server_connected: return _retval(False, @@ -195,27 +230,36 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): else: spid = self.start_server(amp.loads(arguments)) return _retval(True, "Server started with PID {spid}.".format(spid=spid)) - elif operation == amp.SRELOAD: # reload server + + elif operation == amp.SRELOAD: # reload server #14 if server_connected: - self.stop(mode='reload') - spid = self.start_server(amp.loads(arguments)) - return _retval(True, "Server restarted with PID {spid}.".format(spid=spid)) + # don't restart until the server connection goes down + self.stop_server(mode='reload') else: spid = self.start_server(amp.loads(arguments)) return _retval(True, "Server started with PID {spid}.".format(spid=spid)) - elif operation == amp.SRESET: # reload server + + elif operation == amp.SRESET: # reload server #19 if server_connected: self.stop_server(mode='reset') - spid = self.start_server(amp.loads(arguments)) return _retval(True, "Server restarted with PID {spid}.".format(spid=spid)) else: spid = self.start_server(amp.loads(arguments)) return _retval(True, "Server started with PID {spid}.".format(spid=spid)) - elif operation == amp.PSHUTD: # portal + server shutdown + + elif operation == amp.SSHUTD: # server-only shutdown #17 + if server_connected: + self.stop_server(mode='shutdown') + return _retval(True, "Server stopped.") + else: + return _retval(False, "Server not running") + + elif operation == amp.PSHUTD: # portal + server shutdown #16 if server_connected: self.stop_server(mode='shutdown') return _retval(True, "Server stopped.") self.factory.portal.shutdown(restart=False) + else: raise Exception("operation %(op)s not recognized." % {'op': operation}) # fallback @@ -254,6 +298,9 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): operation = kwargs.pop("operation") portal_sessionhandler = self.factory.portal.sessions + # store this transport since we know it comes from the Server + self.factory.server_connection = self + if operation == amp.SLOGIN: # server_session_login # a session has authenticated; sync it. session = portal_sessionhandler.get(sessid) @@ -271,12 +318,14 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason")) elif operation == amp.SRELOAD: # server reload - self.stop_server(mode='reload') - self.start(self.factory.portal.server_twisted_cmd) + self.factory.server_connection.wait_for_disconnect( + self.start_server, self.factory.portal.server_twisted_cmd) + self.stop_server(mode='reload') elif operation == amp.SRESET: # server reset - self.stop_server(mode='reset') - self.start(self.factory.portal.server_twisted_cmd) + self.factory.server_connection.wait_for_disconnect( + self.start_server, self.factory.portal.server_twisted_cmd) + self.stop_server(mode='reset') elif operation == amp.SSHUTD: # server-only shutdown self.stop_server(mode='shutdown') diff --git a/evennia/server/server.py b/evennia/server/server.py index fab6d9675f..070ddd4695 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -366,6 +366,7 @@ class Evennia(object): once - in both cases the reactor is dead/stopping already. """ + print("server.shutdown mode=", mode) if _reactor_stopping and hasattr(self, "shutdown_complete"): # this means we have already passed through this method # once; we don't need to run the shutdown procedure again. diff --git a/evennia/utils/logger.py b/evennia/utils/logger.py index b248278ce1..a8bcfb00e7 100644 --- a/evennia/utils/logger.py +++ b/evennia/utils/logger.py @@ -124,6 +124,15 @@ def log_err(errmsg): log_errmsg = log_err +def log_server(servermsg): + try: + servermsg = str(servermsg) + except Exception as e: + servermsg = str(e) + for line in servermsg.splitlines(): + log_msg('[SRV] %s' % line) + + def log_warn(warnmsg): """ Prints/logs any warnings that aren't critical but should be noted.