Start reworking launcher for sending instructions

This commit is contained in:
Griatch 2018-01-13 20:11:02 +01:00
parent 84e0f463a5
commit b4d2fe7284
5 changed files with 231 additions and 67 deletions

View file

@ -125,6 +125,10 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
# receiving AMP data # receiving AMP data
@amp.MsgStatus.responder
def server_receive_status(self, question):
return {"status": "OK"}
@amp.MsgPortal2Server.responder @amp.MsgPortal2Server.responder
@amp.catch_traceback @amp.catch_traceback
def server_receive_msgportal2server(self, packed_data): def server_receive_msgportal2server(self, packed_data):

View file

@ -20,6 +20,12 @@ import importlib
from distutils.version import LooseVersion from distutils.version import LooseVersion
from argparse import ArgumentParser from argparse import ArgumentParser
from subprocess import Popen, check_output, call, CalledProcessError, STDOUT 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.protocols import amp
from twisted.internet import reactor, endpoints from twisted.internet import reactor, endpoints
import django import django
@ -67,15 +73,19 @@ PORTAL_RESTART = None
SERVER_PY_FILE = None SERVER_PY_FILE = None
PORTAL_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 TEST_MODE = False
ENFORCED_SETTING = False ENFORCED_SETTING = False
# communication constants # communication constants
SRELOAD = chr(14) # server reloading (have portal start a new server) 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 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 # requirements
PYTHON_MIN = '2.7' PYTHON_MIN = '2.7'
@ -85,11 +95,11 @@ DJANGO_REC = '1.11'
sys.path[1] = EVENNIA_ROOT sys.path[1] = EVENNIA_ROOT
#------------------------------------------------------------ # ------------------------------------------------------------
# #
# Messages # Messages
# #
#------------------------------------------------------------ # ------------------------------------------------------------
CREATED_NEW_GAMEDIR = \ CREATED_NEW_GAMEDIR = \
""" """
@ -416,6 +426,10 @@ NOTE_TEST_CUSTOM = \
on the game dir.) on the game dir.)
""" """
PROCESS_ERROR = \
"""
{component} process error: {traceback}.
"""
# ------------------------------------------------------------ # ------------------------------------------------------------
# #
@ -429,8 +443,8 @@ class MsgStatus(amp.Command):
Ping between AMP services Ping between AMP services
""" """
key = "AMPPing" key = "MsgStatus"
arguments = [('question', amp.String())] arguments = [('status', amp.String())]
errors = {Exception: 'EXCEPTION'} errors = {Exception: 'EXCEPTION'}
response = [('status', amp.String())] response = [('status', amp.String())]
@ -442,12 +456,12 @@ class MsgLauncher2Portal(amp.Command):
""" """
key = "MsgLauncher2Portal" key = "MsgLauncher2Portal"
arguments = [('operation', amp.String()), arguments = [('operation', amp.String()),
('argument', amp.String())] ('arguments', amp.String())]
errors = {Exception: 'EXCEPTION'} errors = {Exception: 'EXCEPTION'}
response = [('result', amp.String())] response = [('result', amp.String())]
def send_instruction(instruction, argument, callback, errback): def send_instruction(instruction, arguments, callback, errback):
""" """
Send instruction and handle the response. Send instruction and handle the response.
@ -473,14 +487,22 @@ def send_instruction(instruction, argument, callback, errback):
reactor.stop() reactor.stop()
if instruction == PSTATUS: if instruction == PSTATUS:
prot.callRemote(MsgStatus, question="").addCallbacks(_callback, _errback) prot.callRemote(MsgStatus, status="").addCallbacks(_callback, _errback)
else: else:
prot.callRemote(MsgLauncher2Portal, instruction, argument).addCallbacks( prot.callRemote(
_callback, _errback) 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) point = endpoints.TCP4ClientEndpoint(reactor, AMP_HOST, AMP_PORT)
deferred = endpoints.connectProtocol(point, amp.AMP()) deferred = endpoints.connectProtocol(point, amp.AMP())
deferred.addCallbacks(_on_connect, errback) deferred.addCallbacks(_on_connect, _on_connect_fail)
reactor.run() reactor.run()
@ -489,16 +511,33 @@ def send_status():
Send ping to portal Send ping to portal
""" """
import time def _callback(response):
t0 = time.time() pstatus, sstatus = response['status'].split("|")
def _callback(status): print("Portal: {}\nServer: {}".format(pstatus, sstatus))
print("STATUS returned: %s (%gms)" % (status, (time.time()-t0) * 1000))
def _errback(err): def _errback(fail):
print("STATUS returned: %s" % err) pstatus, sstatus = "NOT RUNNING", "NOT RUNNING"
print("Portal: {}\nServer: {}".format(pstatus, sstatus))
send_instruction(PSTATUS, None, _callback, _errback) 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 # 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(): def evennia_version():
""" """
Get the Evennia version info from the main package. 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): if os.path.exists(settings_path):
inp = input("%s already exists. Do you want to reset it? y/[N]> " % settings_path) inp = input("%s already exists. Do you want to reset it? y/[N]> " % settings_path)
if not inp.lower() == 'y': if not inp.lower() == 'y':
print ("Aborted.") print("Aborted.")
return return
else: else:
print ("Reset the settings file.") print("Reset the settings file.")
default_settings_path = os.path.join(EVENNIA_TEMPLATE, "server", "conf", "settings.py") default_settings_path = os.path.join(EVENNIA_TEMPLATE, "server", "conf", "settings.py")
shutil.copy(default_settings_path, settings_path) shutil.copy(default_settings_path, settings_path)
@ -912,7 +1063,7 @@ def error_check_python_modules():
_imp(settings.COMMAND_PARSER) _imp(settings.COMMAND_PARSER)
_imp(settings.SEARCH_AT_RESULT) _imp(settings.SEARCH_AT_RESULT)
_imp(settings.CONNECTION_SCREEN_MODULE) _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: for path in settings.LOCK_FUNC_MODULES:
_imp(path, split=False) _imp(path, split=False)
@ -1280,7 +1431,7 @@ def server_operation(mode, service, interactive, profiler, logserver=False, doex
elif mode == 'stop': elif mode == 'stop':
if os.name == "nt": if os.name == "nt":
print ( print(
"(Obs: You can use a single Ctrl-C to skip " "(Obs: You can use a single Ctrl-C to skip "
"Windows' annoying 'Terminate batch job (Y/N)?' prompts.)") "Windows' annoying 'Terminate batch job (Y/N)?' prompts.)")
# stop processes, avoiding reload # stop processes, avoiding reload
@ -1357,7 +1508,8 @@ def main():
default=None, help='Get current server status.') default=None, help='Get current server status.')
parser.epilog = ( parser.epilog = (
"Common usage: evennia start|stop|reload. Django-admin database commands:" "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() args, unknown_args = parser.parse_known_args()
@ -1422,10 +1574,20 @@ def main():
# launch menu for operation # launch menu for operation
init_game_directory(CURRENT_DIR, check_db=True) init_game_directory(CURRENT_DIR, check_db=True)
run_menu() run_menu()
elif option in ('start', 'reload', 'stop'): elif option in ('sstart', 'sreload', 'sstop', 'ssstop', 'start', 'reload', 'stop'):
# operate the server directly # operate the server directly
init_game_directory(CURRENT_DIR, check_db=True) 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": elif option != "noop":
# pass-through to django manager # pass-through to django manager
check_db = False check_db = False

View file

@ -63,10 +63,6 @@ CMDLINE_HELP = \
are stored in the game's server/ directory. are stored in the game's server/ directory.
""" """
PROCESS_ERROR = \
"""
{component} process error: {traceback}.
"""
PROCESS_IOERROR = \ PROCESS_IOERROR = \
""" """

View file

@ -38,9 +38,10 @@ SCONN = chr(11) # server creating new connection (for irc bots and etc)
PCONNSYNC = chr(12) # portal post-syncing a session PCONNSYNC = chr(12) # portal post-syncing a session
PDISCONNALL = chr(13) # portal session disconnect all PDISCONNALL = chr(13) # portal session disconnect all
SRELOAD = chr(14) # server reloading (have portal start a new server) 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 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) 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 BATCH_RATE = 250 # max commands/sec before switching to batch-sending
@ -150,7 +151,7 @@ class MsgLauncher2Portal(amp.Command):
""" """
key = "MsgLauncher2Portal" key = "MsgLauncher2Portal"
arguments = [('operation', amp.String()), arguments = [('operation', amp.String()),
('argument', amp.String())] ('arguments', amp.String())]
errors = {Exception: 'EXCEPTION'} errors = {Exception: 'EXCEPTION'}
response = [('result', amp.String())] response = [('result', amp.String())]
@ -210,8 +211,8 @@ class MsgStatus(amp.Command):
Check Status between AMP services Check Status between AMP services
""" """
key = "AMPPing" key = "MsgStatus"
arguments = [('question', amp.String())] arguments = [('status', amp.String())]
errors = {Exception: 'EXCEPTION'} errors = {Exception: 'EXCEPTION'}
response = [('status', amp.String())] response = [('status', amp.String())]
@ -342,23 +343,6 @@ class AMPMultiConnectionProtocol(amp.AMP):
self.errback, command.key)) self.errback, command.key))
return DeferredList(deferreds) 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 # generic function send/recvs
def send_FunctionCall(self, modulepath, functionname, *args, **kwargs): def send_FunctionCall(self, modulepath, functionname, *args, **kwargs):

View file

@ -96,43 +96,61 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
""" """
return self.data_out(amp.AdminPortal2Server, session.sessid, operation=operation, **kwargs) 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 # receive amp data
@amp.MsgStatus.responder @amp.MsgStatus.responder
def portal_receive_status(self, question): @amp.catch_traceback
return {"status": "All well"} 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.MsgLauncher2Portal.responder
@amp.catch_traceback @amp.catch_traceback
def portal_receive_launcher2portal(self, operation, argument): def portal_receive_launcher2portal(self, operation, arguments):
""" """
Receives message arriving from evennia_launcher. Receives message arriving from evennia_launcher.
This method is executed on the Portal. This method is executed on the Portal.
Args: Args:
operation (str): The action to perform. 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: Returns:
result (dict): The result back to the launcher. result (dict): The result back to the launcher.
Notes: Notes:
This is the entrypoint for controlling the entire Evennia system from the This is the entrypoint for controlling the entire Evennia system from the evennia
evennia launcher. launcher. It can obviously only accessed when the Portal is already up and running.
""" """
if operation == amp.PSTART: # portal start (server start or reload) server_connected = any(1 for prtcl in self.factory.broadcasts
pass 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 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 elif operation == amp.PSHUTD: # portal + server shutdown
pass if server_connected:
self.stop_server(amp.loads(arguments))
self.factory.portal.shutdown(restart=False)
else: else:
raise Exception("operation %(op)s not recognized." % {'op': operation}) raise Exception("operation %(op)s not recognized." % {'op': operation})
# fallback # fallback