mirror of
https://github.com/evennia/evennia.git
synced 2026-03-26 09:46:32 +01:00
Start reworking launcher for sending instructions
This commit is contained in:
parent
84e0f463a5
commit
b4d2fe7284
5 changed files with 231 additions and 67 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -63,10 +63,6 @@ CMDLINE_HELP = \
|
|||
are stored in the game's server/ directory.
|
||||
"""
|
||||
|
||||
PROCESS_ERROR = \
|
||||
"""
|
||||
{component} process error: {traceback}.
|
||||
"""
|
||||
|
||||
PROCESS_IOERROR = \
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue