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

View file

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

View file

@ -63,10 +63,6 @@ CMDLINE_HELP = \
are stored in the game's server/ directory.
"""
PROCESS_ERROR = \
"""
{component} process error: {traceback}.
"""
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
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):

View file

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