From 09d0c99a212d11f4ff0e756cf137cdee81db01cf Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 20 Jan 2018 12:41:27 +0100 Subject: [PATCH] Remove dependence on .restart file, add reboot option to launcher --- evennia/server/amp_client.py | 7 +- evennia/server/evennia_launcher.py | 219 ++++++++++++++++++---------- evennia/server/portal/amp_server.py | 8 +- evennia/server/portal/portal.py | 6 +- evennia/server/server.py | 58 ++------ evennia/server/sessionhandler.py | 4 +- evennia/utils/logger.py | 2 +- 7 files changed, 173 insertions(+), 131 deletions(-) diff --git a/evennia/server/amp_client.py b/evennia/server/amp_client.py index 603924190a..294250b9e7 100644 --- a/evennia/server/amp_client.py +++ b/evennia/server/amp_client.py @@ -104,7 +104,7 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol): info_dict = self.factory.server.get_info_dict() super(AMPServerClientProtocol, self).connectionMade() # first thing we do is to request the Portal to sync all sessions - # back with the Server side + # back with the Server side. We also need the startup mode (reload, reset, shutdown) self.send_AdminServer2Portal(amp.DUMMYSESSION, operation=amp.PSYNC, info_dict=info_dict) def data_to_portal(self, command, sessid, **kwargs): @@ -212,7 +212,10 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol): server_sessionhandler.portal_disconnect_all() elif operation == amp.PSYNC: # portal_session_sync - # force a resync of sessions from the portal side + # force a resync of sessions from the portal side. This happens on + # first server-connect. + server_restart_mode = kwargs.get("server_restart_mode", "shutdown") + self.factory.server.run_init_hooks(server_restart_mode) server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata")) elif operation == amp.SRELOAD: # server reload diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 58442d2678..296cf61b12 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -305,20 +305,24 @@ HELP_ENTRY = \ MENU = \ """ +----Evennia Launcher-------------------------------------------+ - | | + {gameinfo} +--- Common operations -----------------------------------------+ - | 1) Start Portal and Server (also restart downed Server) | - | 2) Reload Server (update on code changes) | - | 3) Stop Portal and Server (full shutdown) | + | 1) Start (also restart stopped Server) | + | 2) Reload (stop/start Server in 'reload' mode) | + | 3) Stop (shutdown Portal and Server) | + | 4) Reboot (shutdown then restart) | +--- Other -----------------------------------------------------+ - | 4) Reset Server (Server shutdown with restart) | - | 5) Stop Server only | - | 6) Kill Portal + Server (send kill signal to process) | - | 7) Kill Server only | + | 5) Reset (stop/start Server in 'shutdown' mode) | + | 6) Stop Server only | + | 7) Kill Server only (send kill signal to process) | + | 8) Kill Portal + Server | +--- Information -----------------------------------------------+ - | 8) Tail log file | - | 9) Run status | - | 10) Port info | + | 9) Tail log file | + | 10) Run status | + | 11) Port info | + +--- Testing ---------------------------------------------------+ + | 12) Test gamedir (run gamedir test suite, if any) | + | 13) Test Evennia (run evennia test suite) | +---------------------------------------------------------------+ | h) Help i) About info q) Abort | +---------------------------------------------------------------+""" @@ -452,7 +456,8 @@ ARG_OPTIONS = \ start - launch server+portal if not running reload - restart server (code refresh) stop - shutdown server+portal - reset - mimic server shutdown but with auto-restart + reboot - shutdown server+portal, then start again + reset - restart server in shutdown-mode (not reload mode) sstart - start only server (requires portal) kill - send kill signal to portal+server (force) skill = send kill signal only to server @@ -461,10 +466,13 @@ ARG_OPTIONS = \ menu - show a menu of options Other input, like migrate and shell is passed on to Django.""" +# ------------------------------------------------------------ +# +# Private helper functions +# +# ------------------------------------------------------------ -# Info formatting - -def print_info(portal_info_dict, server_info_dict): +def _print_info(portal_info_dict, server_info_dict): """ Format info dicts from the Portal/Server for display @@ -498,6 +506,43 @@ def print_info(portal_info_dict, server_info_dict): print(top_border + "\n" + info + '\n' + border) +def _parse_status(response): + "Unpack the status information" + return pickle.loads(response['status']) + + +def _get_twistd_cmdline(pprofiler, sprofiler): + """ + Compile the command line for starting a Twisted application using the 'twistd' executable. + + """ + portal_cmd = [TWISTED_BINARY, + "--python={}".format(PORTAL_PY_FILE)] + server_cmd = [TWISTED_BINARY, + "--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=cprofile", + "--profile={}".format(PPROFILER_LOGFILE)]) + if sprofiler: + server_cmd.extend(["--savestats", + "--profiler=cprofile", + "--profile={}".format(SPROFILER_LOGFILE)]) + + return portal_cmd, server_cmd + + +def _reactor_stop(): + if not NO_REACTOR_STOP: + reactor.stop() + + # ------------------------------------------------------------ # # Protocol Evennia launcher - Portal/Server communication @@ -560,11 +605,6 @@ class AMPLauncherProtocol(amp.AMP): return {"status": ""} -def _reactor_stop(): - if not NO_REACTOR_STOP: - reactor.stop() - - def send_instruction(operation, arguments, callback=None, errback=None): """ Send instruction and handle the response. @@ -579,12 +619,10 @@ def send_instruction(operation, arguments, callback=None, errback=None): def _callback(result): if callback: callback(result) - # prot.transport.loseConnection() def _errback(fail): if errback: errback(fail) - # prot.transport.loseConnection() def _on_connect(prot): """ @@ -620,37 +658,6 @@ def send_instruction(operation, arguments, callback=None, errback=None): deferred.addCallbacks(_on_connect, _on_connect_fail) REACTOR_RUN = True -def _parse_status(response): - "Unpack the status information" - return pickle.loads(response['status']) - - -def _get_twistd_cmdline(pprofiler, sprofiler): - """ - Compile the command line for starting a Twisted application using the 'twistd' executable. - - """ - portal_cmd = [TWISTED_BINARY, - "--python={}".format(PORTAL_PY_FILE)] - server_cmd = [TWISTED_BINARY, - "--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=cprofile", - "--profile={}".format(PPROFILER_LOGFILE)]) - if sprofiler: - server_cmd.extend(["--savestats", - "--profiler=cprofile", - "--profile={}".format(SPROFILER_LOGFILE)]) - - return portal_cmd, server_cmd - def query_status(callback=None): """ @@ -693,9 +700,10 @@ def wait_for_status(portal_running=True, server_running=True, callback=None, err Repeat the status ping until the desired state combination is achieved. Args: - portal_running (bool or None): Desired portal run-state. If None, any state is accepted. - server_running (bool or None): Desired server run-state. If None, any state is accepted. - the portal must be running. + portal_running (bool or None): Desired portal run-state. If None, any state + is accepted. + server_running (bool or None): Desired server run-state. If None, any state + is accepted. The portal must be running. callback (callable): Will be called with portal_state, server_state when condition is fulfilled. errback (callable): Will be called with portal_state, server_state if the @@ -748,13 +756,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 @@ -771,7 +779,7 @@ def start_evennia(pprofiler=False, sprofiler=False): print("... Server started.\nEvennia running.") if response: _, _, _, _, pinfo, sinfo = response - print_info(pinfo, sinfo) + _print_info(pinfo, sinfo) _reactor_stop() def _portal_started(*args): @@ -836,10 +844,9 @@ def reload_evennia(sprofiler=False, reset=False): send_instruction(SSTART, server_cmd) def _portal_not_running(fail): - print("Evennia not running. Starting from scratch ...") + print("Evennia not running. Starting up ...") start_evennia() - # get portal status send_instruction(PSTATUS, None, _portal_running, _portal_not_running) @@ -869,12 +876,51 @@ def stop_evennia(): wait_for_status(False, None, _portal_stopped) def _portal_not_running(fail): - print("Evennia is not running.") + print("Evennia not running.") _reactor_stop() send_instruction(PSTATUS, None, _portal_running, _portal_not_running) +def reboot_evennia(pprofiler=False, sprofiler=False): + """ + This is essentially an evennia stop && evennia start except we make sure + the system has successfully shut down before starting it again. + + If evennia was not running, start it. + + """ + global AMP_CONNECTION + + def _portal_stopped(*args): + print("... Portal stopped. Evennia shut down. Rebooting ...") + global AMP_CONNECTION + AMP_CONNECTION = None + start_evennia(pprofiler, sprofiler) + + def _server_stopped(*args): + print("... Server stopped.\nStopping Portal ...") + send_instruction(PSHUTD, {}) + wait_for_status(False, None, _portal_stopped) + + def _portal_running(response): + prun, srun, ppid, spid, _, _ = _parse_status(response) + if srun: + print("Server stopping ...") + send_instruction(SSHUTD, {}) + wait_for_status_reply(_server_stopped) + else: + print("Server already stopped.\nStopping Portal ...") + send_instruction(PSHUTD, {}) + wait_for_status(False, None, _portal_stopped) + + def _portal_not_running(fail): + print("Evennia not running. Starting up ...") + start_evennia() + + 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) @@ -908,7 +954,7 @@ def query_info(): """ def _got_status(status): _, _, _, _, pinfo, sinfo = _parse_status(status) - print_info(pinfo, sinfo) + _print_info(pinfo, sinfo) _reactor_stop() def _portal_running(response): @@ -1435,6 +1481,12 @@ def error_check_python_modules(): _imp(settings.BASE_SCRIPT_TYPECLASS) +# ------------------------------------------------------------ +# +# Options +# +# ------------------------------------------------------------ + def init_game_directory(path, check_db=True): """ Try to analyze the given path to find settings.py - this defines @@ -1623,8 +1675,11 @@ def run_menu(): """ while True: # menu loop + gamedir = "/{}".format(os.path.basename(GAMEDIR)) + leninfo = len(gamedir) + line = "|" + " " * (60 - leninfo) + gamedir + " " * 3 + "|" - print(MENU) + print(MENU.format(gameinfo=line)) inp = input(" option > ") # quitting and help @@ -1652,22 +1707,24 @@ def run_menu(): elif inp == 3: stop_evennia() elif inp == 4: - reload_evennia(False, True) + reboot_evennia(False, False) elif inp == 5: - stop_server_only() + reload_evennia(False, True) elif inp == 6: - kill(PORTAL_PIDFILE, 'Portal') - kill(SERVER_PIDFILE, 'Server') + stop_server_only() elif inp == 7: kill(SERVER_PIDFILE, 'Server') elif inp == 8: + kill(PORTAL_PIDFILE, 'Portal') + kill(SERVER_PIDFILE, 'Server') + elif inp == 9: if not SERVER_LOGFILE: init_game_directory(CURRENT_DIR, check_db=False) tail_server_log(SERVER_LOGFILE) print(" Tailing logfile {} ...".format(SERVER_LOGFILE)) - elif inp == 9: - query_status() elif inp == 10: + query_status() + elif inp == 11: query_info() else: print("Not a valid option.") @@ -1686,37 +1743,36 @@ def main(): parser.add_argument( '--gamedir', nargs=1, action='store', dest='altgamedir', metavar="", - help="Location of gamedir (default: current location)") + help="location of gamedir (default: current location)") parser.add_argument( '--init', action='store', dest="init", metavar="", - help="Creates a new gamedir 'name' at current location.") + help="creates a new gamedir 'name' at current location") parser.add_argument( '--log', '-l', action='store_true', dest='tail_log', default=False, - help="Tail the server logfile to standard out.") + help="tail the server logfile to console") parser.add_argument( '--list', nargs='+', action='store', dest='listsetting', metavar="all|", - help=("List values for one or more server settings. Use 'all' to \n list all " - "available keys.")) + help=("list settings, use 'all' to list all available keys")) parser.add_argument( '--settings', nargs=1, action='store', dest='altsettings', default=None, metavar="", - help=("Start evennia with alternative settings file from\n" + help=("start evennia with alternative settings file from\n" " gamedir/server/conf/. (default is settings.py)")) parser.add_argument( '--initsettings', action='store_true', dest="initsettings", default=False, - help="Create a new, empty settings file as\n gamedir/server/conf/settings.py.") + help="create a new, empty settings file as\n gamedir/server/conf/settings.py") parser.add_argument( '--profiler', action='store_true', dest='profiler', default=False, - help="Start given server component under the Python profiler.") + help="start given server component under the Python profiler") parser.add_argument( '--dummyrunner', nargs=1, action='store', dest='dummyrunner', metavar="", - help="Test a server by connecting dummy accounts to it.") + help="test a server by connecting dummy accounts to it") parser.add_argument( '-v', '--version', action='store_true', dest='show_version', default=False, - help="Show version info.") + help="show version info") parser.add_argument( "operation", nargs='?', default="noop", @@ -1800,7 +1856,8 @@ def main(): # launch menu for operation init_game_directory(CURRENT_DIR, check_db=True) run_menu() - elif option in ('status', 'info', 'start', 'reload', 'reset', 'stop', 'sstop', 'kill', 'skill'): + elif option in ('status', 'info', 'start', 'reload', 'reboot', + 'reset', 'stop', 'sstop', 'kill', 'skill'): # operate the server directly if not SERVER_LOGFILE: init_game_directory(CURRENT_DIR, check_db=True) @@ -1812,6 +1869,8 @@ def main(): start_evennia(args.profiler, args.profiler) elif option == 'reload': reload_evennia(args.profiler) + elif option == 'reboot': + reboot_evennia(args.profiler, args.profiler) elif option == 'reset': reload_evennia(args.profiler, reset=True) elif option == 'stop': diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index ed51b8952a..6fa671e886 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -202,6 +202,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET) elif mode == 'shutdown': self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD) + self.factory.portal.server_restart_mode = mode # sending amp data @@ -233,8 +234,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): def send_AdminPortal2Server(self, session, operation="", **kwargs): """ Send Admin instructions from the Portal to the Server. - Executed - on the Portal. + Executed on the Portal. Args: session (Session): Session. @@ -403,9 +403,13 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): elif operation == amp.PSYNC: # portal sync # Server has (re-)connected and wants the session data from portal self.factory.server_info_dict = kwargs.get("info_dict", {}) + # this defaults to 'shutdown' or whatever value set in server_stop + server_restart_mode = self.factory.portal.server_restart_mode + sessdata = self.factory.portal.sessions.get_all_sync_data() self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.PSYNC, + server_restart_mode=server_restart_mode, sessiondata=sessdata) self.factory.portal.sessions.at_server_connection() diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index d7fcadcbcb..27034f5785 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -85,8 +85,9 @@ INFO_DICT = {"servername": SERVERNAME, "version": VERSION, "errors": "", "info": # ------------------------------------------------------------- # Portal Service object - # ------------------------------------------------------------- + + class Portal(object): """ @@ -113,13 +114,12 @@ class Portal(object): self.sessions.portal = self self.process_id = os.getpid() self.server_process_id = None + self.server_restart_mode = "shutdown" # set a callback if the server is killed abruptly, # by Ctrl-C, reboot etc. reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True) - self.game_running = False - def get_info_dict(self): "Return the Portal info, for display." return INFO_DICT diff --git a/evennia/server/server.py b/evennia/server/server.py index c665fc143e..6ba5f32ef8 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -203,11 +203,6 @@ class Evennia(object): reactor.callLater(1, d.callback, None) reactor.sigInt = _wrap_sigint_handler - self.game_running = True - - # track the server time - self.run_init_hooks() - # Server startup methods def sqlite3_prep(self): @@ -299,9 +294,13 @@ class Evennia(object): last=last_initial_setup_step) initial_setup.handle_setup(int(last_initial_setup_step)) - def run_init_hooks(self): + def run_init_hooks(self, mode): """ - Called every server start + Called by the amp client once receiving sync back from Portal + + Args: + mode (str): One of shutdown, reload or reset + """ from evennia.objects.models import ObjectDB @@ -311,47 +310,24 @@ class Evennia(object): [o.at_init() for o in ObjectDB.get_all_cached_instances()] [p.at_init() for p in AccountDB.get_all_cached_instances()] - mode = self.getset_restart_mode() - # call correct server hook based on start file value if mode == 'reload': - # True was the old reload flag, kept for compatibilty + logger.log_msg("Server successfully reloaded.") self.at_server_reload_start() elif mode == 'reset': # only run hook, don't purge sessions self.at_server_cold_start() - elif mode in ('reset', 'shutdown'): + logger.log_msg("Evennia Server successfully restarted in 'reset' mode.") + elif mode == 'shutdown': self.at_server_cold_start() # clear eventual lingering session storages ObjectDB.objects.clear_all_sessids() + logger.log_msg("Evennia Server successfully started.") # always call this regardless of start type self.at_server_start() - def getset_restart_mode(self, mode=None): - """ - This manages the flag file that tells the runner if the server is - reloading, resetting or shutting down. - - Args: - mode (string or None, optional): Valid values are - 'reload', 'reset', 'shutdown' and `None`. If mode is `None`, - no change will be done to the flag file. - Returns: - mode (str): The currently active restart mode, either just - set or previously set. - - """ - if mode is None: - with open(SERVER_RESTART, 'r') as f: - # mode is either shutdown, reset or reload - mode = f.read() - else: - with open(SERVER_RESTART, 'w') as f: - f.write(str(mode)) - return mode - @defer.inlineCallbacks - def shutdown(self, mode=None, _reactor_stopping=False): + def shutdown(self, mode='reload', _reactor_stopping=False): """ Shuts down the server from inside it. @@ -362,7 +338,6 @@ class Evennia(object): at_shutdown hooks called but sessions will not be disconnected. 'shutdown' - like reset, but server will not auto-restart. - None - keep currently set flag from flag file. _reactor_stopping - this is set if server is stopped by a kill command OR this method was already called once - in both cases the reactor is @@ -373,10 +348,7 @@ class Evennia(object): # once; we don't need to run the shutdown procedure again. defer.returnValue(None) - mode = self.getset_restart_mode(mode) - from evennia.objects.models import ObjectDB - #from evennia.accounts.models import AccountDB from evennia.server.models import ServerConfig from evennia.utils import gametime as _GAMETIME_MODULE @@ -455,13 +427,15 @@ class Evennia(object): if SERVER_STARTSTOP_MODULE: SERVER_STARTSTOP_MODULE.at_server_reload_start() - def at_post_portal_sync(self): + def at_post_portal_sync(self, mode): """ This is called just after the portal has finished syncing back data to the server after reconnecting. + + Args: + mode (str): One of reload, reset or shutdown. + """ - # one of reload, reset or shutdown - mode = self.getset_restart_mode() from evennia.scripts.monitorhandler import MONITOR_HANDLER MONITOR_HANDLER.restore(mode == 'reload') diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index e76efd7405..8c6f6193b8 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -379,8 +379,10 @@ class ServerSessionHandler(SessionHandler): self[sessid] = sess sess.at_sync() + mode = 'reload' + # tell the server hook we synced - self.server.at_post_portal_sync() + self.server.at_post_portal_sync(mode) # announce the reconnection self.announce_all(_(" ... Server restarted.")) diff --git a/evennia/utils/logger.py b/evennia/utils/logger.py index 30af5d2226..4b9307819c 100644 --- a/evennia/utils/logger.py +++ b/evennia/utils/logger.py @@ -197,7 +197,7 @@ def log_server(servermsg): except Exception as e: servermsg = str(e) for line in servermsg.splitlines(): - log_msg('[SRV] %s' % line) + log_msg('[Server] %s' % line) def log_warn(warnmsg):