Working start/reload/reset/stop from launcher

This commit is contained in:
Griatch 2018-01-14 22:53:36 +01:00
parent ded5d106b9
commit 5656b841d6
7 changed files with 136 additions and 56 deletions

View file

@ -192,15 +192,21 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
elif operation == amp.PSYNC: # portal_session_sync
# force a resync of sessions from the portal side
server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata"))
elif operation == amp.SRELOAD: # server reload
# shut down in reload mode
server_sessionhandler.all_sessions_portal_sync()
server_sessionhandler.server.shutdown(mode='reload')
elif operation == amp.SRESET:
# shut down in reset mode
server_sessionhandler.all_sessions_portal_sync()
server_sessionhandler.server.shutdown(mode='reset')
elif operation == amp.SSHUTD: # server shutdown
# shutdown in stop mode
server_sessionhandler.server.shutdown(mode='shutdown')
else:
raise Exception("operation %(op)s not recognized." % {'op': operation})
return {}

View file

@ -19,7 +19,7 @@ import shutil
import importlib
from distutils.version import LooseVersion
from argparse import ArgumentParser
from subprocess import Popen, check_output, call, CalledProcessError, STDOUT
from subprocess import Popen, check_output, call, CalledProcessError, STDOUT, PIPE
try:
import cPickle as pickle
@ -88,6 +88,7 @@ SSTART = chr(15) # server start
PSHUTD = chr(16) # portal (+server) shutdown
SSHUTD = chr(17) # server-only shutdown
PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # shutdown server in reset mode
# requirements
PYTHON_MIN = '2.7'
@ -519,13 +520,18 @@ def _get_twistd_cmdline(pprofiler, sprofiler):
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)]
"--logfile={}".format(SERVER_LOGFILE),
"--python={}".format(SERVER_PY_FILE)]
if os.name != 'nt':
# PID files only for UNIX
portal_cmd.append("--pidfile={}".format(PORTAL_PIDFILE))
server_cmd.append("--pidfile={}".format(SERVER_PIDFILE))
if pprofiler:
portal_cmd.extend(["--savestats",
"--profiler=cprofiler",
@ -534,6 +540,8 @@ def _get_twistd_cmdline(pprofiler, sprofiler):
server_cmd.extend(["--savestats",
"--profiler=cprofiler",
"--profile={}".format(SPROFILER_LOGFILE)])
return portal_cmd, server_cmd
@ -552,7 +560,6 @@ def query_status(repeat=False):
reactor.stop()
def _errback(fail):
print("status fail: %s", fail)
pstatus, sstatus = False, False
print("Portal: {}\nServer: {}".format(wmap[pstatus], wmap[sstatus]))
reactor.stop()
@ -591,7 +598,7 @@ def wait_for_status(portal_running=True, server_running=True, callback=None, err
if errback:
errback(prun, srun)
else:
print("Timeout.")
print("Connection to Evennia timed out. Try again.")
reactor.stop()
else:
reactor.callLater(rate, wait_for_status,
@ -613,7 +620,7 @@ def wait_for_status(portal_running=True, server_running=True, callback=None, err
if errback:
errback(portal_running, server_running)
else:
print("Timeout.")
print("Connection to Evennia timed out. Try again.")
reactor.stop()
else:
reactor.callLater(rate, wait_for_status,
@ -622,14 +629,13 @@ def wait_for_status(portal_running=True, server_running=True, callback=None, err
return send_instruction(PSTATUS, None, _callback, _errback)
# ------------------------------------------------------------
#
# Operational functions
#
# ------------------------------------------------------------
def start_evennia(pprofiler=False, sprofiler=False):
"""
This will start Evennia anew by launching the Evennia Portal (which in turn
@ -638,8 +644,12 @@ def start_evennia(pprofiler=False, sprofiler=False):
"""
portal_cmd, server_cmd = _get_twistd_cmdline(pprofiler, sprofiler)
def _fail(fail):
print(fail)
reactor.stop()
def _server_started(*args):
print("... Server started.\nEvennia running.", args)
print("... Server started.\nEvennia running.")
reactor.stop()
def _portal_started(*args):
@ -653,21 +663,22 @@ def start_evennia(pprofiler=False, sprofiler=False):
reactor.stop()
else:
print("Server starting {}...".format("(under cProfile)" if pprofiler else ""))
send_instruction(SSTART, server_cmd, _server_started)
send_instruction(SSTART, server_cmd, _server_started, _fail)
def _portal_not_running(fail):
print("Portal starting {}...".format("(under cProfile)" if sprofiler else ""))
try:
Popen(portal_cmd, env=getenv())
Popen(portal_cmd, env=getenv(), bufsize=-1)
except Exception as e:
print(PROCESS_ERROR.format(component="Portal", traceback=e))
reactor.stop()
wait_for_status(True, None, _portal_started)
send_instruction(PSTATUS, None, _portal_running, _portal_not_running)
reactor.run()
def reload_evennia(sprofiler=False):
def reload_evennia(sprofiler=False, reset=False):
"""
This will instruct the Portal to reboot the Server component.
@ -675,18 +686,23 @@ def reload_evennia(sprofiler=False):
_, server_cmd = _get_twistd_cmdline(False, sprofiler)
def _server_restarted(*args):
print("... Server re-started.", args)
print("... Server re-started.")
reactor.stop()
def _server_reloaded(*args):
print("... Server reloaded.", args)
print("... Server {}.".format("reset" if reset else "reloaded"))
reactor.stop()
def _server_not_running(*args):
send_instruction(SSTART, server_cmd)
wait_for_status(True, True, _server_reloaded)
def _portal_running(response):
_, srun, _, _ = _parse_status(response)
if srun:
print("Server reloading ...")
send_instruction(SRELOAD, server_cmd, _server_reloaded)
print("Server {}...".format("resetting" if reset else "reloading"))
send_instruction(SRESET if reset else SRELOAD, server_cmd)
wait_for_status(True, False, _server_not_running)
else:
print("Server down. Re-starting ...")
send_instruction(SSTART, server_cmd, _server_restarted)
@ -710,7 +726,7 @@ def stop_evennia():
reactor.stop()
def _server_stopped(*args):
print("... Server stopped.\nStopping Portal ...", args)
print("... Server stopped.\nStopping Portal ...")
send_instruction(PSHUTD, {})
wait_for_status(False, None, _portal_stopped)
@ -718,7 +734,8 @@ def stop_evennia():
prun, srun, ppid, spid = _parse_status(response)
if srun:
print("Server stopping ...")
send_instruction(SSHUTD, {}, _server_stopped)
send_instruction(SSHUTD, {})
wait_for_status(True, False, _server_stopped)
else:
print("Server already stopped.\nStopping Portal ...")
send_instruction(PSHUTD, {})
@ -726,6 +743,7 @@ def stop_evennia():
def _portal_not_running(fail):
print("Evennia is not running.")
reactor.stop()
send_instruction(PSTATUS, None, _portal_running, _portal_not_running)
reactor.run()
@ -741,8 +759,8 @@ def stop_server_only():
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 stopping ...")
send_instruction(SSHUTD, {})
wait_for_status(True, False, _server_stopped)
@ -1239,7 +1257,7 @@ def init_game_directory(path, check_db=True):
AMP_INTERFACE = settings.AMP_INTERFACE
SERVER_PY_FILE = os.path.join(EVENNIA_LIB, "server", "server.py")
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "portal", "portal", "portal.py")
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "server", "portal", "portal.py")
SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid")
PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid")
@ -1602,9 +1620,6 @@ def main():
"service", metavar="component", nargs='?', default="all",
help=("Which component to operate on: "
"'server', 'portal' or 'all' (default if not set)."))
parser.add_argument(
"--status", action='store_true', dest='get_status',
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 "
@ -1656,11 +1671,6 @@ def main():
print(ERROR_INITSETTINGS)
sys.exit()
if args.get_status:
init_game_directory(CURRENT_DIR, check_db=True)
query_status()
sys.exit()
if args.dummyrunner:
# launch the dummy runner
init_game_directory(CURRENT_DIR, check_db=True)
@ -1673,13 +1683,17 @@ def main():
# launch menu for operation
init_game_directory(CURRENT_DIR, check_db=True)
run_menu()
elif option in ('sstart', 'sreload', 'sstop', 'ssstop', 'start', 'reload', 'stop'):
elif option in ('status', 'sstart', 'sreload', 'sreset', 'sstop', 'ssstop', 'start', 'reload', 'stop'):
# operate the server directly
init_game_directory(CURRENT_DIR, check_db=True)
if option == "status":
query_status()
if option == "sstart":
start_evennia(False, args.profiler)
elif option == 'sreload':
reload_evennia(args.profiler)
elif option == 'sreset':
reload_evennia(args.profiler, reset=True)
elif option == 'sstop':
stop_evennia()
elif option == 'ssstop':

View file

@ -346,6 +346,7 @@ def main():
del portal_argv[-2]
# Start processes
print("server_argv:", server_argv, portal_argv)
start_services(server_argv, portal_argv, doexit=args.doexit)

View file

@ -89,8 +89,8 @@ def catch_traceback(func):
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
print(err)
return decorator

View file

@ -8,7 +8,7 @@ import os
import sys
from twisted.internet import protocol
from evennia.server.portal import amp
from subprocess import Popen
from subprocess import Popen, STDOUT, PIPE
from evennia.utils import logger
@ -48,6 +48,8 @@ class AMPServerFactory(protocol.ServerFactory):
self.portal = portal
self.protocol = AMPServerProtocol
self.broadcasts = []
self.server_connection = None
self.disconnect_callbacks = {}
def buildProtocol(self, addr):
"""
@ -80,12 +82,45 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
"""
# start the Server
process = Popen(server_twistd_cmd, env=getenv())
# store the pid for future reference
try:
process = Popen(server_twistd_cmd, env=getenv(), bufsize=-1, stdout=PIPE, stderr=STDOUT)
except Exception:
self.factory.portal.server_process_id = None
logger.log_trace()
return 0
# there is a short window before the server logger is up where we must
# catch the stdout of the Server or eventual tracebacks will be lost.
with process.stdout as out:
logger.log_server(out.read())
# store the pid and launch argument for future reference
self.factory.portal.server_process_id = process.pid
self.factory.portal.server_twistd_cmd = server_twistd_cmd
return process.pid
def connectionLost(self, reason):
"""
Set up a simple callback mechanism to let the amp-server wait for a connection to close.
"""
callback, args, kwargs = self.factory.disconnect_callbacks.pop(self, (None, None, None))
if callback:
try:
callback(*args, **kwargs)
except Exception:
logger.log_trace()
def wait_for_disconnect(self, callback, *args, **kwargs):
"""
Add a callback for when this connection is lost.
Args:
callback (callable): Will be called with *args, **kwargs
once this protocol is disconnected.
"""
self.factory.disconnect_callbacks[self] = (callback, args, kwargs)
def stop_server(self, mode='shutdown'):
"""
Shut down server in one or more modes.
@ -95,11 +130,11 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
"""
if mode == 'reload':
self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.SRELOAD)
if mode == 'reset':
self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.SRESET)
if mode == 'shutdown':
self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.SSHUTD)
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRELOAD)
elif mode == 'reset':
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET)
elif mode == 'shutdown':
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD)
# sending amp data
@ -148,8 +183,8 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
"""
# 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)
server_connected = (self.factory.server_connection and
self.factory.server_connection.transport.connected)
server_pid = self.factory.portal.server_process_id
portal_pid = os.getpid()
@ -180,14 +215,14 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
def _retval(success, txt):
return {"result": amp.dumps((success, txt))}
server_connected = any(1 for prtcl in self.factory.broadcasts
if prtcl is not self and prtcl.transport.connected)
server_connected = (self.factory.server_connection and
self.factory.server_connection.transport.connected)
server_pid = self.factory.portal.server_process_id
logger.log_msg("AMP SERVER operation == %s received" % (ord(operation)))
logger.log_msg("AMP SERVER arguments: %s" % (amp.loads(arguments)))
if operation == amp.SSTART: # portal start
if operation == amp.SSTART: # portal start #15
# first, check if server is already running
if server_connected:
return _retval(False,
@ -195,27 +230,36 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
else:
spid = self.start_server(amp.loads(arguments))
return _retval(True, "Server started with PID {spid}.".format(spid=spid))
elif operation == amp.SRELOAD: # reload server
elif operation == amp.SRELOAD: # reload server #14
if server_connected:
self.stop(mode='reload')
spid = self.start_server(amp.loads(arguments))
return _retval(True, "Server restarted with PID {spid}.".format(spid=spid))
# don't restart until the server connection goes down
self.stop_server(mode='reload')
else:
spid = self.start_server(amp.loads(arguments))
return _retval(True, "Server started with PID {spid}.".format(spid=spid))
elif operation == amp.SRESET: # reload server
elif operation == amp.SRESET: # reload server #19
if server_connected:
self.stop_server(mode='reset')
spid = self.start_server(amp.loads(arguments))
return _retval(True, "Server restarted with PID {spid}.".format(spid=spid))
else:
spid = self.start_server(amp.loads(arguments))
return _retval(True, "Server started with PID {spid}.".format(spid=spid))
elif operation == amp.PSHUTD: # portal + server shutdown
elif operation == amp.SSHUTD: # server-only shutdown #17
if server_connected:
self.stop_server(mode='shutdown')
return _retval(True, "Server stopped.")
else:
return _retval(False, "Server not running")
elif operation == amp.PSHUTD: # portal + server shutdown #16
if server_connected:
self.stop_server(mode='shutdown')
return _retval(True, "Server stopped.")
self.factory.portal.shutdown(restart=False)
else:
raise Exception("operation %(op)s not recognized." % {'op': operation})
# fallback
@ -254,6 +298,9 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
operation = kwargs.pop("operation")
portal_sessionhandler = self.factory.portal.sessions
# store this transport since we know it comes from the Server
self.factory.server_connection = self
if operation == amp.SLOGIN: # server_session_login
# a session has authenticated; sync it.
session = portal_sessionhandler.get(sessid)
@ -271,12 +318,14 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))
elif operation == amp.SRELOAD: # server reload
self.stop_server(mode='reload')
self.start(self.factory.portal.server_twisted_cmd)
self.factory.server_connection.wait_for_disconnect(
self.start_server, self.factory.portal.server_twisted_cmd)
self.stop_server(mode='reload')
elif operation == amp.SRESET: # server reset
self.stop_server(mode='reset')
self.start(self.factory.portal.server_twisted_cmd)
self.factory.server_connection.wait_for_disconnect(
self.start_server, self.factory.portal.server_twisted_cmd)
self.stop_server(mode='reset')
elif operation == amp.SSHUTD: # server-only shutdown
self.stop_server(mode='shutdown')

View file

@ -366,6 +366,7 @@ class Evennia(object):
once - in both cases the reactor is
dead/stopping already.
"""
print("server.shutdown mode=", mode)
if _reactor_stopping and hasattr(self, "shutdown_complete"):
# this means we have already passed through this method
# once; we don't need to run the shutdown procedure again.

View file

@ -124,6 +124,15 @@ def log_err(errmsg):
log_errmsg = log_err
def log_server(servermsg):
try:
servermsg = str(servermsg)
except Exception as e:
servermsg = str(e)
for line in servermsg.splitlines():
log_msg('[SRV] %s' % line)
def log_warn(warnmsg):
"""
Prints/logs any warnings that aren't critical but should be noted.