diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 99eab5bb84..806f51b621 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -463,7 +463,7 @@ class MsgLauncher2Portal(amp.Command): response = [('result', amp.String())] -def send_instruction(operation, arguments, callback=None, errback=None, autostop=False): +def send_instruction(operation, arguments, callback=None, errback=None): """ Send instruction and handle the response. @@ -484,20 +484,15 @@ def send_instruction(operation, arguments, callback=None, errback=None, autostop if callback: callback(result) prot.transport.loseConnection() - if autostop: - reactor.stop() def _errback(fail): if errback: errback(fail) prot.transport.loseConnection() - if autostop: - reactor.stop() if operation == PSTATUS: prot.callRemote(MsgStatus, status="").addCallbacks(_callback, _errback) else: - print("callRemote MsgLauncher %s %s" % (ord(operation), arguments)) prot.callRemote( MsgLauncher2Portal, operation=operation, @@ -507,8 +502,6 @@ def send_instruction(operation, arguments, callback=None, errback=None, autostop def _on_connect_fail(fail): "This is called if portal is not reachable." errback(fail) - if autostop: - reactor.stop() point = endpoints.TCP4ClientEndpoint(reactor, AMP_HOST, AMP_PORT) deferred = endpoints.connectProtocol(point, amp.AMP()) @@ -516,20 +509,55 @@ def send_instruction(operation, arguments, callback=None, errback=None, autostop return deferred -def send_status(repeat=False): +def _parse_status(response): + "Unpack the status information" + return pickle.loads(response['status']) + + +def _get_twistd_cmdline(pprofiler, sprofiler): """ - Send ping to portal + 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)] + if pprofiler: + portal_cmd.extend(["--savestats", + "--profiler=cprofiler", + "--profile={}".format(PPROFILER_LOGFILE)]) + if sprofiler: + server_cmd.extend(["--savestats", + "--profiler=cprofiler", + "--profile={}".format(SPROFILER_LOGFILE)]) + return portal_cmd, server_cmd + + +def query_status(repeat=False): + """ + Send status ping to portal + + """ + wmap = {True: "RUNNING", + False: "NOT RUNNING"} + def _callback(response): - pstatus, sstatus = response['status'].split("|") - print("Portal: {}\nServer: {}".format(pstatus, sstatus)) + pstatus, sstatus, ppid, spid = _parse_status(response) + print("Portal: {} (pid {})\nServer: {} (pid {})".format( + wmap[pstatus], ppid, wmap[sstatus], spid)) + reactor.stop() def _errback(fail): - pstatus, sstatus = "NOT RUNNING", "NOT RUNNING" - print("Portal: {}\nServer: {}".format(pstatus, sstatus)) + print("status fail: %s", fail) + pstatus, sstatus = False, False + print("Portal: {}\nServer: {}".format(wmap[pstatus], wmap[sstatus])) + reactor.stop() - send_instruction(PSTATUS, None, _callback, _errback, autostop=True) + send_instruction(PSTATUS, None, _callback, _errback) reactor.run() @@ -550,7 +578,7 @@ def wait_for_status(portal_running=True, server_running=True, callback=None, err retries (int): How many times to retry before timing out and calling `errback`. """ def _callback(response): - prun, srun = [stat == 'RUNNING' for stat in response['status'].split("|")] + prun, srun, _, _ = _parse_status(response) if ((portal_running is None or prun == portal_running) and (server_running is None or srun == server_running)): # the correct state was achieved @@ -594,52 +622,34 @@ def wait_for_status(portal_running=True, server_running=True, callback=None, err return send_instruction(PSTATUS, None, _callback, _errback) + + # ------------------------------------------------------------ # -# Helper functions +# Operational functions # # ------------------------------------------------------------ - -def get_twistd_cmdline(pprofiler, sprofiler): - - 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)] - if pprofiler: - portal_cmd.extend(["--savestats", - "--profiler=cprofiler", - "--profile={}".format(PPROFILER_LOGFILE)]) - if sprofiler: - server_cmd.extend(["--savestats", - "--profiler=cprofiler", - "--profile={}".format(SPROFILER_LOGFILE)]) - return portal_cmd, server_cmd - - def start_evennia(pprofiler=False, sprofiler=False): """ This will start Evennia anew by launching the Evennia Portal (which in turn will start the Server) """ - portal_cmd, server_cmd = get_twistd_cmdline(pprofiler, sprofiler) + portal_cmd, server_cmd = _get_twistd_cmdline(pprofiler, sprofiler) def _server_started(*args): - print("... Server started.\nEvennia running.") + print("... Server started.\nEvennia running.", args) reactor.stop() def _portal_started(*args): send_instruction(SSTART, server_cmd, _server_started) def _portal_running(response): - _, server_running = [stat == 'RUNNING' for stat in response['status'].split("|")] - print("Portal is already running as process {pid}. Not restarted.".format(pid=0)) # TODO PID - if server_running: - print("Server is already running as process {pid}. Not restarted.".format(pid=0)) # TODO PID + prun, srun, ppid, spid = _parse_status(response) + print("Portal is already running as process {pid}. Not restarted.".format(pid=ppid)) + if srun: + print("Server is already running as process {pid}. Not restarted.".format(pid=spid)) reactor.stop() else: print("Server starting {}...".format("(under cProfile)" if pprofiler else "")) @@ -648,7 +658,7 @@ def start_evennia(pprofiler=False, sprofiler=False): def _portal_not_running(fail): print("Portal starting {}...".format("(under cProfile)" if sprofiler else "")) try: - Popen(portal_cmd) + Popen(portal_cmd, env=getenv()) except Exception as e: print(PROCESS_ERROR.format(component="Portal", traceback=e)) wait_for_status(True, None, _portal_started) @@ -662,7 +672,7 @@ def reload_evennia(sprofiler=False): This will instruct the Portal to reboot the Server component. """ - _, server_cmd = get_twistd_cmdline(False, sprofiler) + _, server_cmd = _get_twistd_cmdline(False, sprofiler) def _server_restarted(*args): print("... Server re-started.", args) @@ -673,8 +683,8 @@ def reload_evennia(sprofiler=False): 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 reloading ...") send_instruction(SRELOAD, server_cmd, _server_reloaded) else: @@ -700,16 +710,17 @@ def stop_evennia(): reactor.stop() def _server_stopped(*args): - print("... Server stopped.", args) + print("... Server stopped.\nStopping Portal ...", args) send_instruction(PSHUTD, {}) wait_for_status(False, None, _portal_stopped) def _portal_running(response): - _, server_running = [stat == 'RUNNING' for stat in response['status'].split("|")] - if server_running: + prun, srun, ppid, spid = _parse_status(response) + if srun: print("Server stopping ...") send_instruction(SSHUTD, {}, _server_stopped) else: + print("Server already stopped.\nStopping Portal ...") send_instruction(PSHUTD, {}) wait_for_status(False, False, _portal_stopped) @@ -1647,7 +1658,7 @@ def main(): if args.get_status: init_game_directory(CURRENT_DIR, check_db=True) - send_status() + query_status() sys.exit() if args.dummyrunner: diff --git a/evennia/server/portal/amp.py b/evennia/server/portal/amp.py index fc6848a577..2887aab8ec 100644 --- a/evennia/server/portal/amp.py +++ b/evennia/server/portal/amp.py @@ -83,11 +83,12 @@ def catch_traceback(func): def decorator(*args, **kwargs): try: func(*args, **kwargs) - except Exception: + except Exception as err: global _LOGGER 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 return decorator diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index a9b27f639c..20a73a70a7 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -4,8 +4,25 @@ communication to the AMP clients connecting to it (by default these are the Evennia Server and the evennia launcher). """ +import os +import sys from twisted.internet import protocol from evennia.server.portal import amp +from subprocess import Popen + + +def getenv(): + """ + Get current environment and add PYTHONPATH. + + Returns: + env (dict): Environment global dict. + + """ + sep = ";" if os.name == 'nt' else ":" + env = os.environ.copy() + env['PYTHONPATH'] = sep.join(sys.path) + return env class AMPServerFactory(protocol.ServerFactory): @@ -66,6 +83,20 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): sessiondata=sessdata) self.factory.portal.sessions.at_server_connection() + def start_server(self, server_twistd_cmd): + """ + (Re-)Launch the Evennia server. + + Args: + server_twisted_cmd (list): The server start instruction + to pass to POpen to start the server. + + """ + # start the server + process = Popen(server_twistd_cmd, env=getenv()) + # store the pid for future reference + self.portal.server_process_id = process.pid + # sending amp data def send_MsgPortal2Server(self, session, **kwargs): @@ -103,16 +134,25 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): @amp.catch_traceback def portal_receive_status(self, status): """ - Check if Server is running + Returns run-status for the server/portal. + + Args: + status (str): Not used. + Returns: + status (dict): The status is a tuple + (portal_running, server_running, portal_pid, server_pid). + """ # 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) - # return portal|server RUNNING/NOT RUNNING + server_pid = self.factory.portal.server_process_id + portal_pid = os.getpid() + if server_connected: - return {"status": "RUNNING|RUNNING"} + return {"status": amp.dumps((True, True, portal_pid, server_pid))} else: - return {"status": "RUNNING|NOT RUNNING"} + return {"status": amp.dumps((True, False, portal_pid, server_pid))} @amp.MsgLauncher2Portal.responder @amp.catch_traceback @@ -140,10 +180,10 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): print("AMP SERVER arguments: %s" % (amp.loads(arguments))) return {"result": "Received."} - if operation == amp.SSTART: # portal start (server start or reload) + if operation == amp.SSTART: # portal start # first, check if server is already running if server_connected: - return {"result": "Server already running (PID {}).".format(0)} # TODO store and send PID + return {"result": "Server already running at PID={}"} else: self.start_server(amp.loads(arguments)) return {"result": "Server started with PID {}.".format(0)} # TODO diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 293d1d6db5..5d4c9aae19 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -105,6 +105,8 @@ class Portal(object): self.amp_protocol = None # set by amp factory self.sessions = PORTAL_SESSIONS self.sessions.portal = self + self.process_id = os.getpid() + self.server_process_id = None # set a callback if the server is killed abruptly, # by Ctrl-C, reboot etc. diff --git a/evennia/server/server.py b/evennia/server/server.py index be282527f6..fab6d9675f 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -173,6 +173,7 @@ class Evennia(object): self.amp_protocol = None # set by amp factory self.sessions = SESSIONS self.sessions.server = self + self.process_id = os.getpid() # Database-specific startup optimizations. self.sqlite3_prep()