Further work on controlling the portal with AMP

This commit is contained in:
Griatch 2018-01-14 00:30:13 +01:00
parent 1b9e5f21c4
commit b2f4348049
5 changed files with 113 additions and 58 deletions

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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()