From b4d2fe728464be7d96f269aa173b20ececedd34f Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 13 Jan 2018 20:11:02 +0100 Subject: [PATCH] Start reworking launcher for sending instructions --- evennia/server/amp_client.py | 4 + evennia/server/evennia_launcher.py | 212 ++++++++++++++++++++++++---- evennia/server/evennia_runner.py | 4 - evennia/server/portal/amp.py | 28 +--- evennia/server/portal/amp_server.py | 50 ++++--- 5 files changed, 231 insertions(+), 67 deletions(-) diff --git a/evennia/server/amp_client.py b/evennia/server/amp_client.py index fd36499561..a76245912b 100644 --- a/evennia/server/amp_client.py +++ b/evennia/server/amp_client.py @@ -125,6 +125,10 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol): # receiving AMP data + @amp.MsgStatus.responder + def server_receive_status(self, question): + return {"status": "OK"} + @amp.MsgPortal2Server.responder @amp.catch_traceback def server_receive_msgportal2server(self, packed_data): diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 94395f09c4..a9452ae785 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -20,6 +20,12 @@ import importlib from distutils.version import LooseVersion from argparse import ArgumentParser from subprocess import Popen, check_output, call, CalledProcessError, STDOUT + +try: + import cPickle as pickle +except ImportError: + import pickle + from twisted.protocols import amp from twisted.internet import reactor, endpoints import django @@ -67,15 +73,19 @@ PORTAL_RESTART = None SERVER_PY_FILE = None PORTAL_PY_FILE = None +SPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "server.prof") +PPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "portal.prof") + TEST_MODE = False ENFORCED_SETTING = False # communication constants SRELOAD = chr(14) # server reloading (have portal start a new server) -PSTART = chr(15) # server+portal start +SSTART = chr(15) # server start PSHUTD = chr(16) # portal (+server) shutdown -PSTATUS = chr(17) # ping server or portal status +SSHUTD = chr(17) # server-only shutdown +PSTATUS = chr(18) # ping server or portal status # requirements PYTHON_MIN = '2.7' @@ -85,11 +95,11 @@ DJANGO_REC = '1.11' sys.path[1] = EVENNIA_ROOT -#------------------------------------------------------------ +# ------------------------------------------------------------ # # Messages # -#------------------------------------------------------------ +# ------------------------------------------------------------ CREATED_NEW_GAMEDIR = \ """ @@ -416,6 +426,10 @@ NOTE_TEST_CUSTOM = \ on the game dir.) """ +PROCESS_ERROR = \ + """ + {component} process error: {traceback}. + """ # ------------------------------------------------------------ # @@ -429,8 +443,8 @@ class MsgStatus(amp.Command): Ping between AMP services """ - key = "AMPPing" - arguments = [('question', amp.String())] + key = "MsgStatus" + arguments = [('status', amp.String())] errors = {Exception: 'EXCEPTION'} response = [('status', amp.String())] @@ -442,12 +456,12 @@ class MsgLauncher2Portal(amp.Command): """ key = "MsgLauncher2Portal" arguments = [('operation', amp.String()), - ('argument', amp.String())] + ('arguments', amp.String())] errors = {Exception: 'EXCEPTION'} response = [('result', amp.String())] -def send_instruction(instruction, argument, callback, errback): +def send_instruction(instruction, arguments, callback, errback): """ Send instruction and handle the response. @@ -473,14 +487,22 @@ def send_instruction(instruction, argument, callback, errback): reactor.stop() if instruction == PSTATUS: - prot.callRemote(MsgStatus, question="").addCallbacks(_callback, _errback) + prot.callRemote(MsgStatus, status="").addCallbacks(_callback, _errback) else: - prot.callRemote(MsgLauncher2Portal, instruction, argument).addCallbacks( - _callback, _errback) + prot.callRemote( + MsgLauncher2Portal, + instruction=instruction, + arguments=pickle.dumps(arguments, pickle.HIGHEST_PROTOCOL).addCallbacks( + _callback, _errback)) + + def _on_connect_fail(fail): + "This is called if portal is not reachable." + errback(fail) + reactor.stop() point = endpoints.TCP4ClientEndpoint(reactor, AMP_HOST, AMP_PORT) deferred = endpoints.connectProtocol(point, amp.AMP()) - deferred.addCallbacks(_on_connect, errback) + deferred.addCallbacks(_on_connect, _on_connect_fail) reactor.run() @@ -489,16 +511,33 @@ def send_status(): Send ping to portal """ - import time - t0 = time.time() - def _callback(status): - print("STATUS returned: %s (%gms)" % (status, (time.time()-t0) * 1000)) + def _callback(response): + pstatus, sstatus = response['status'].split("|") + print("Portal: {}\nServer: {}".format(pstatus, sstatus)) - def _errback(err): - print("STATUS returned: %s" % err) + def _errback(fail): + pstatus, sstatus = "NOT RUNNING", "NOT RUNNING" + print("Portal: {}\nServer: {}".format(pstatus, sstatus)) send_instruction(PSTATUS, None, _callback, _errback) + +def send_repeating_status(callback=None): + """ + Repeat the status ping until a reply is returned or timeout is reached. + + Args: + callback (callable): Takes the response on a successful status-reply + """ + def _callback(response): + pstatus, sstatus = response['status'].split("|") + print("Portal: {}\nServer: {}".format(pstatus, sstatus)) + + def _errback(fail): + send_instruction(PSTATUS, None, _callback, _errback) + + send_instruction(PSTATUS, None, callback or _callback, _errback) + # ------------------------------------------------------------ # # Helper functions @@ -506,6 +545,118 @@ def send_status(): # ------------------------------------------------------------ +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) + + 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 + else: + print("Server starting {}...".format("(under cProfile)" if pprofiler else "")) + send_instruction(SSTART, server_cmd, lambda x: 0, lambda e: 0) # TODO + + def _portal_cold_started(response): + "Called once the portal is up after a cold boot. It needs to know how to start the Server." + send_instruction(SSTART, server_cmd, lambda x: 0, lambda e: 0) # TODO + + def _portal_not_running(fail): + print("Portal starting {}...".format("(under cProfile)" if sprofiler else "")) + try: + Popen(portal_cmd) + except Exception as e: + print(PROCESS_ERROR.format(component="Portal", traceback=e)) + send_repeating_status(_portal_cold_started) + + # first, check if the portal/server is running already + send_instruction(PSTATUS, None, _portal_running, _portal_not_running) + + +def reload_evennia(sprofiler=False): + """ + This will instruct the Portal to reboot the Server component. + + """ + _, server_cmd = get_twistd_cmdline(False, sprofiler) + + def _portal_running(response): + _, server_running = [stat == 'RUNNING' for stat in response['status'].split("|")] + if server_running: + print("Server reloading ...") + else: + print("Server down. Starting anew.") + send_instruction(SRELOAD, server_cmd, lambda x: 0, lambda e: 0) # TODO + + def _portal_not_running(fail): + print("Evennia not running. Starting from scratch ...") + start_evennia() + + # get portal status + send_instruction(PSTATUS, None, _portal_running, _portal_not_running) + + +def stop_evennia(): + """ + This instructs the Portal to stop the Server and then itself. + + """ + def _portal_running(response): + _, server_running = [stat == 'RUNNING' for stat in response['status'].split("|")] + print("Portal stopping ...") + if server_running: + print("Server stopping ...") + send_instruction(PSHUTD, {}, lambda x: 0, lambda e: 0) # TODO + + def _portal_not_running(fail): + print("Evennia is not running.") + + send_instruction(PSTATUS, None, _portal_running, _portal_not_running) + + +def stop_server_only(): + """ + Only stop the Server-component of Evennia (this is not useful except for debug) + + """ + def _portal_running(response): + _, server_running = [stat == 'RUNNING' for stat in response['status'].split("|")] + if server_running: + print("Server stopping ...") + send_instruction(SSHUTD, {}, lambda x: 0, lambda e: 0) # TODO + else: + print("Server is not running.") + + def _portal_not_running(fail): + print("Evennia is not running.") + + send_instruction(PSTATUS, None, _portal_running, _portal_not_running) + + def evennia_version(): """ Get the Evennia version info from the main package. @@ -645,10 +796,10 @@ def create_settings_file(init=True, secret_settings=False): if os.path.exists(settings_path): inp = input("%s already exists. Do you want to reset it? y/[N]> " % settings_path) if not inp.lower() == 'y': - print ("Aborted.") + print("Aborted.") return else: - print ("Reset the settings file.") + print("Reset the settings file.") default_settings_path = os.path.join(EVENNIA_TEMPLATE, "server", "conf", "settings.py") shutil.copy(default_settings_path, settings_path) @@ -912,7 +1063,7 @@ def error_check_python_modules(): _imp(settings.COMMAND_PARSER) _imp(settings.SEARCH_AT_RESULT) _imp(settings.CONNECTION_SCREEN_MODULE) - #imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False) + # imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False) for path in settings.LOCK_FUNC_MODULES: _imp(path, split=False) @@ -1280,7 +1431,7 @@ def server_operation(mode, service, interactive, profiler, logserver=False, doex elif mode == 'stop': if os.name == "nt": - print ( + print( "(Obs: You can use a single Ctrl-C to skip " "Windows' annoying 'Terminate batch job (Y/N)?' prompts.)") # stop processes, avoiding reload @@ -1357,7 +1508,8 @@ def main(): 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 django-admin commands.)") + "evennia migration|flush|shell|dbshell (see the django documentation for more " + "django-admin commands.)") args, unknown_args = parser.parse_known_args() @@ -1422,10 +1574,20 @@ def main(): # launch menu for operation init_game_directory(CURRENT_DIR, check_db=True) run_menu() - elif option in ('start', 'reload', 'stop'): + elif option in ('sstart', 'sreload', 'sstop', 'ssstop', 'start', 'reload', 'stop'): # operate the server directly init_game_directory(CURRENT_DIR, check_db=True) - server_operation(option, service, args.interactive, args.profiler, args.logserver, doexit=args.doexit) + if option == "sstart": + start_evennia(False, args.profiler) + elif option == 'sreload': + reload_evennia(args.profiler) + elif option == 'sstop': + stop_evennia() + elif option == 'ssstop': + stop_server_only() + else: + server_operation(option, service, args.interactive, + args.profiler, args.logserver, doexit=args.doexit) elif option != "noop": # pass-through to django manager check_db = False diff --git a/evennia/server/evennia_runner.py b/evennia/server/evennia_runner.py index 83e7bf4093..27fc187211 100644 --- a/evennia/server/evennia_runner.py +++ b/evennia/server/evennia_runner.py @@ -63,10 +63,6 @@ CMDLINE_HELP = \ are stored in the game's server/ directory. """ -PROCESS_ERROR = \ - """ - {component} process error: {traceback}. - """ PROCESS_IOERROR = \ """ diff --git a/evennia/server/portal/amp.py b/evennia/server/portal/amp.py index 1578b8f1f4..fc6848a577 100644 --- a/evennia/server/portal/amp.py +++ b/evennia/server/portal/amp.py @@ -38,9 +38,10 @@ SCONN = chr(11) # server creating new connection (for irc bots and etc) PCONNSYNC = chr(12) # portal post-syncing a session PDISCONNALL = chr(13) # portal session disconnect all SRELOAD = chr(14) # server reloading (have portal start a new server) -PSTART = chr(15) # server+portal start +SSTART = chr(15) # server start (portal must already be running anyway) PSHUTD = chr(16) # portal (+server) shutdown -PSTATUS = chr(17) # ping server or portal status +SSHUTD = chr(17) # server-only shutdown +PSTATUS = chr(18) # ping server or portal status AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed) BATCH_RATE = 250 # max commands/sec before switching to batch-sending @@ -150,7 +151,7 @@ class MsgLauncher2Portal(amp.Command): """ key = "MsgLauncher2Portal" arguments = [('operation', amp.String()), - ('argument', amp.String())] + ('arguments', amp.String())] errors = {Exception: 'EXCEPTION'} response = [('result', amp.String())] @@ -210,8 +211,8 @@ class MsgStatus(amp.Command): Check Status between AMP services """ - key = "AMPPing" - arguments = [('question', amp.String())] + key = "MsgStatus" + arguments = [('status', amp.String())] errors = {Exception: 'EXCEPTION'} response = [('status', amp.String())] @@ -342,23 +343,6 @@ class AMPMultiConnectionProtocol(amp.AMP): self.errback, command.key)) return DeferredList(deferreds) - def send_status(self, port, callback, errback): - """ - Ping to the given AMP port. - - Args: - port (int): The port to ping - callback (callable): This will be called with the port that replied to the ping. - errback (callable0: This will be called with the port that failed to reply. - - """ - targets = [(protcl, protcl.getHost()[1]) for protcl in self.factory.broadcasts] - deferreds = [] - for protcl, port in ((protcl, prt) for protcl, prt in targets if prt == port): - deferreds.append(protcl.callRemote(MsgStatus, status=True).addCallback( - callback, port).addErrback(errback, port)) - return DeferredList(deferreds) - # generic function send/recvs def send_FunctionCall(self, modulepath, functionname, *args, **kwargs): diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index 343680aa89..4b18e91c27 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -96,43 +96,61 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): """ return self.data_out(amp.AdminPortal2Server, session.sessid, operation=operation, **kwargs) - def sendPingPortal2Server(self, callback): - """ - Send ping to check if Server is alive. - - """ - # receive amp data @amp.MsgStatus.responder - def portal_receive_status(self, question): - return {"status": "All well"} + @amp.catch_traceback + def portal_receive_status(self, status): + """ + Check if Server is running + """ + # 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 + if server_connected: + return {"status": "RUNNING|RUNNING"} + else: + return {"status": "RUNNING|NOT RUNNING"} @amp.MsgLauncher2Portal.responder @amp.catch_traceback - def portal_receive_launcher2portal(self, operation, argument): + def portal_receive_launcher2portal(self, operation, arguments): """ Receives message arriving from evennia_launcher. This method is executed on the Portal. Args: operation (str): The action to perform. - argument (str): A possible argument to the instruction, or the empty string. + arguments (str): Possible argument to the instruction, or the empty string. Returns: result (dict): The result back to the launcher. Notes: - This is the entrypoint for controlling the entire Evennia system from the - evennia launcher. + This is the entrypoint for controlling the entire Evennia system from the evennia + launcher. It can obviously only accessed when the Portal is already up and running. """ - if operation == amp.PSTART: # portal start (server start or reload) - pass + server_connected = any(1 for prtcl in self.factory.broadcasts + if prtcl is not self and prtcl.transport.connected) + + if operation == amp.SSTART: # portal start (server start or reload) + # first, check if server is already running + if server_connected: + return {"result": "Server already running (PID {}).".format(0)} # TODO store and send PID + else: + self.start_server(amp.loads(arguments)) + return {"result": "Server started with PID {}.".format(0)} # TODO elif operation == amp.SRELOAD: # reload server - pass + if server_connected: + self.reload_server(amp.loads(arguments)) + else: + self.start_server(amp.loads(arguments)) elif operation == amp.PSHUTD: # portal + server shutdown - pass + if server_connected: + self.stop_server(amp.loads(arguments)) + self.factory.portal.shutdown(restart=False) else: raise Exception("operation %(op)s not recognized." % {'op': operation}) # fallback