diff --git a/evennia/server/amp_client.py b/evennia/server/amp_client.py index 9552ad2122..fd36499561 100644 --- a/evennia/server/amp_client.py +++ b/evennia/server/amp_client.py @@ -126,6 +126,7 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol): # receiving AMP data @amp.MsgPortal2Server.responder + @amp.catch_traceback def server_receive_msgportal2server(self, packed_data): """ Receives message arriving to server. This method is executed @@ -142,6 +143,7 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol): return {} @amp.AdminPortal2Server.responder + @amp.catch_traceback def server_receive_adminportal2server(self, packed_data): """ Receives admin data from the Portal (allows the portal to diff --git a/evennia/server/portal/amp.py b/evennia/server/portal/amp.py index 9968c22a9e..0d3537cd85 100644 --- a/evennia/server/portal/amp.py +++ b/evennia/server/portal/amp.py @@ -5,6 +5,7 @@ This module acts as a central place for AMP-servers and -clients to get commands """ from __future__ import print_function +from functools import wraps import time from twisted.protocols import amp from collections import defaultdict, namedtuple @@ -17,9 +18,10 @@ except ImportError: import pickle from twisted.internet.defer import DeferredList, Deferred -from evennia.utils import logger from evennia.utils.utils import to_str, variable_from_module +# delayed import +_LOGGER = None # communication bits # (chr(9) and chr(10) are \t and \n, so skipping them) @@ -30,12 +32,15 @@ PSYNC = chr(3) # portal session sync SLOGIN = chr(4) # server session login SDISCONN = chr(5) # server session disconnect SDISCONNALL = chr(6) # server session disconnect all -SSHUTD = chr(7) # server shutdown (shutdown portal too) +SSHUTD = chr(7) # server shutdown SSYNC = chr(8) # server session sync 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 +PSHUTD = chr(16) # portal (+server) shutdown +PPING = chr(17) # 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 @@ -71,6 +76,21 @@ def loads(data): return pickle.loads(to_str(data)) +@wraps +def catch_traceback(func): + "Helper decorator" + def decorator(*args, **kwargs): + try: + func(*args, **kwargs) + except Exception: + global _LOGGER + if not _LOGGER: + from evennia.utils import logger as _LOGGER + _LOGGER.log_trace() + raise # make sure the error is visible on the other side of the connection too + return decorator + + # AMP Communication Command types class Compressed(amp.String): @@ -123,6 +143,18 @@ class Compressed(amp.String): return zlib.decompress(inString) +class MsgLauncher2Portal(amp.Command): + """ + Message Launcher -> Portal + + """ + key = "MsgLauncher2Portal" + arguments = [('operation', amp.String()), + ('argument', amp.String())] + errors = {Exception: 'EXCEPTION'} + response = [('result', amp.String())] + + class MsgPortal2Server(amp.Command): """ Message Portal -> Server @@ -173,6 +205,17 @@ class AdminServer2Portal(amp.Command): response = [] +class MsgPing(amp.Command): + """ + Ping between AMP services + + """ + key = "AMPPing" + arguments = [('ping', amp.Boolean())] + errors = {Exception: 'EXCEPTION'} + response = [('pong', amp.Boolean())] + + class FunctionCall(amp.Command): """ Bidirectional Server <-> Portal @@ -259,9 +302,12 @@ class AMPMultiConnectionProtocol(amp.AMP): info (str): Error string. """ + global _LOGGER + if not _LOGGER: + from evennia.utils import logger as _LOGGER e.trap(Exception) - logger.log_err("AMP Error for %(info)s: %(e)s" % {'info': info, - 'e': e.getErrorMessage()}) + _LOGGER.log_err("AMP Error for %(info)s: %(e)s" % {'info': info, + 'e': e.getErrorMessage()}) def data_in(self, packed_data): """ @@ -292,10 +338,28 @@ class AMPMultiConnectionProtocol(amp.AMP): """ deferreds = [] - for prot in self.factory.broadcasts: - deferreds.append(prot.callRemote(command, - packed_data=dumps((sessid, kwargs)))) - return DeferredList(deferreds, fireOnOneErrback=1).addErrback(self.errback, command.key) + for protcl in self.factory.broadcasts: + deferreds.append(protcl.callRemote(command, + packed_data=dumps((sessid, kwargs))).addErrback( + self.errback, command.key)) + return DeferredList(deferreds) + + def send_ping(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(MsgPing, ping=True).addCallback( + callback, port).addErrback(errback, port)) + return DeferredList(deferreds) # generic function send/recvs @@ -323,6 +387,7 @@ class AMPMultiConnectionProtocol(amp.AMP): lambda r: loads(r["result"])).addErrback(self.errback, "FunctionCall") @FunctionCall.responder + @catch_traceback def receive_functioncall(self, module, function, func_args, func_kwargs): """ This allows Portal- and Server-process to call an arbitrary diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index 3c5bf175d2..8915feef8a 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -96,9 +96,48 @@ 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.MsgLauncher2Portal.responder + @amp.catch_traceback + def portal_receive_launcher2portal(self, operation, argument): + """ + 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. + + Returns: + result (dict): The result back to the launcher. + + Notes: + This is the entrypoint for controlling the entire Evennia system from the + evennia launcher. + + """ + if operation == amp.PPING: # check portal and server status + pass + elif operation == amp.PSTART: # portal start (server start or reload) + pass + elif operation == amp.SRELOAD: # reload server + pass + elif operation == amp.PSHUTD: # portal + server shutdown + pass + else: + raise Exception("operation %(op)s not recognized." % {'op': operation}) + # fallback + return {"result": ""} + @amp.MsgServer2Portal.responder + @amp.catch_traceback def portal_receive_server2portal(self, packed_data): """ Receives message arriving to Portal from Server. @@ -115,6 +154,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): return {} @amp.AdminServer2Portal.responder + @amp.catch_traceback def portal_receive_adminserver2portal(self, packed_data): """