diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index ee6c955366..4ae2699e60 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -42,7 +42,6 @@ EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(_ import evennia # noqa EVENNIA_LIB = os.path.join(os.path.dirname(os.path.abspath(evennia.__file__))) EVENNIA_SERVER = os.path.join(EVENNIA_LIB, "server") -EVENNIA_RUNNER = os.path.join(EVENNIA_SERVER, "evennia_runner.py") EVENNIA_TEMPLATE = os.path.join(EVENNIA_LIB, "game_template") EVENNIA_PROFILING = os.path.join(EVENNIA_SERVER, "profiling") EVENNIA_DUMMYRUNNER = os.path.join(EVENNIA_PROFILING, "dummyrunner.py") @@ -462,18 +461,19 @@ SERVER_INFO = \ ARG_OPTIONS = \ """Actions on installed server. One of: - start - launch server+portal if not running - reload - restart server in 'reload' mode - stop - shutdown server+portal - reboot - shutdown server+portal, then start again - reset - restart server in 'shutdown' mode - istart - start server in the foreground (until reload) - sstop - stop only server - kill - send kill signal to portal+server (force) - skill - send kill signal only to server - status - show server and portal run state - info - show server and portal port info - menu - show a menu of options + start - launch server+portal if not running + reload - restart server in 'reload' mode + stop - shutdown server+portal + reboot - shutdown server+portal, then start again + reset - restart server in 'shutdown' mode + istart - start server in foreground (until reload) + ipstart - start portal in foreground + sstop - stop only server + kill - send kill signal to portal+server (force) + skill - send kill signal only to server + status - show server and portal run state + info - show server and portal port info + menu - show a menu of options Others, like migrate, test and shell is passed on to Django.""" # ------------------------------------------------------------ @@ -974,6 +974,47 @@ def start_server_interactive(): stop_server_only(when_stopped=_iserver, interactive=True) +def start_portal_interactive(): + """ + Start the Portal under control of the launcher process (foreground) + + Notes: + In a normal start, the launcher waits for the Portal to start, then + tells it to start the Server. Since we can't do this here, we instead + start the Server first and then starts the Portal - the Server will + auto-reconnect to the Portal. To allow the Server to be reloaded, this + relies on a fixed server server-cmdline stored as a fallback on the + portal application in evennia/server/portal/portal.py. + + """ + def _iportal(fail): + portal_twistd_cmd, server_twistd_cmd = _get_twistd_cmdline(False, False) + portal_twistd_cmd.append("--nodaemon") + + # starting Server first - it will auto-connect once Portal comes up + if _is_windows(): + # Windows requires special care + create_no_window = 0x08000000 + Popen(server_twistd_cmd, env=getenv(), bufsize=-1, + creationflags=create_no_window) + else: + Popen(server_twistd_cmd, env=getenv(), bufsize=-1) + + print("Starting Portal in interactive mode (stop with Ctrl-C)...") + try: + Popen(portal_twistd_cmd, env=getenv(), stderr=STDOUT).wait() + except KeyboardInterrupt: + print("... Stopped Portal with Ctrl-C.") + else: + print("... Portal stopped (leaving interactive mode).") + + def _portal_running(response): + print("Evennia must be shut down completely before running Portal in interactive mode.") + _reactor_stop() + + send_instruction(PSTATUS, None, _portal_running, _iportal) + + def stop_server_only(when_stopped=None, interactive=False): """ Only stop the Server-component of Evennia (this is not useful except for debug) @@ -981,7 +1022,8 @@ def stop_server_only(when_stopped=None, interactive=False): Args: when_stopped (callable): This will be called with no arguments when Server has stopped (or if it had already stopped when this is called). - interactive (bool, optional): Set if this is called as part of the interactive reload mechanism. + interactive (bool, optional): Set if this is called as part of the interactive reload + mechanism. """ def _server_stopped(*args): @@ -1972,7 +2014,7 @@ def main(): # launch menu for operation init_game_directory(CURRENT_DIR, check_db=True) run_menu() - elif option in ('status', 'info', 'start', 'istart', 'reload', 'reboot', + elif option in ('status', 'info', 'start', 'istart', 'ipstart', 'reload', 'reboot', 'reset', 'stop', 'sstop', 'kill', 'skill'): # operate the server directly if not SERVER_LOGFILE: @@ -1985,6 +2027,8 @@ def main(): start_evennia(args.profiler, args.profiler) elif option == "istart": start_server_interactive() + elif option == "ipstart": + start_portal_interactive() elif option == 'reload': reload_evennia(args.profiler) elif option == 'reboot': diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index 38e39fb464..c07b5c121d 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -336,9 +336,9 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): elif operation == amp.PSHUTD: # portal + server shutdown #16 if server_connected: self.factory.server_connection.wait_for_disconnect( - self.factory.portal.shutdown, restart=False) + self.factory.portal.shutdown ) else: - self.factory.portal.shutdown(restart=False) + self.factory.portal.shutdown() else: raise Exception("operation %(op)s not recognized." % {'op': operation}) @@ -414,7 +414,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): elif operation == amp.PSHUTD: # full server+server shutdown self.factory.server_connection.wait_for_disconnect( - self.factory.portal.shutdown, restart=False) + self.factory.portal.shutdown) self.stop_server(mode='shutdown') elif operation == amp.PSYNC: # portal sync diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 6b15cde73a..91b3efc7bc 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -12,6 +12,7 @@ from builtins import object import sys import os +from os.path import dirname, abspath from twisted.application import internet, service from twisted.internet import protocol, reactor from twisted.python.log import ILogObserver @@ -113,41 +114,46 @@ class Portal(object): self.server_restart_mode = "shutdown" self.server_info_dict = {} + # in non-interactive portal mode, this gets overwritten by + # cmdline sent by the evennia launcher + self.server_twistd_cmd = self._get_backup_server_twistd_cmd() + # set a callback if the server is killed abruptly, # by Ctrl-C, reboot etc. - reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True) + reactor.addSystemEventTrigger('before', 'shutdown', + self.shutdown, _reactor_stopping=True, _stop_server=True) + + def _get_backup_server_twistd_cmd(self): + """ + For interactive Portal mode there is no way to get the server cmdline from the launcher, so + we need to guess it here (it's very likely to not change) + + Returns: + server_twistd_cmd (list): An instruction for starting the server, to pass to Popen. + """ + server_twistd_cmd = [ + "twistd", + "--python={}".format(os.path.join(dirname(dirname(abspath(__file__))), "server.py"))] + if os.name != 'nt': + gamedir = os.getcwd() + server_twistd_cmd.append("--pidfile={}".format( + os.path.join(gamedir, "server", "server.pid"))) + return server_twistd_cmd def get_info_dict(self): "Return the Portal info, for display." return INFO_DICT - def set_restart_mode(self, mode=None): - """ - This manages the flag file that tells the runner if the server - should be restarted or is shutting down. - - Args: - mode (bool or None): Valid modes are True/False and None. - If mode is None, no change will be done to the flag file. - - """ - if mode is None: - return - with open(PORTAL_RESTART, 'w') as f: - f.write(str(mode)) - - def shutdown(self, restart=None, _reactor_stopping=False): + def shutdown(self, _reactor_stopping=False, _stop_server=False): """ Shuts down the server from inside it. Args: - restart (bool or None, optional): True/False sets the - flags so the server will be restarted or not. If None, the - current flag setting (set at initialization or previous - runs) is used. _reactor_stopping (bool, optional): This is set if server is already in the process of shutting down; in this case we don't need to stop it again. + _stop_server (bool, optional): Only used in portal-interactive mode; + makes sure to stop the Server cleanly. Note that restarting (regardless of the setting) will not work if the Portal is currently running in daemon mode. In that @@ -158,8 +164,10 @@ class Portal(object): # we get here due to us calling reactor.stop below. No need # to do the shutdown procedure again. return + self.sessions.disconnect_all() - self.set_restart_mode(restart) + if _stop_server: + self.amp_protocol.stop_server(mode='shutdown') if not _reactor_stopping: # shutting down the reactor will trigger another signal. We set @@ -179,9 +187,11 @@ class Portal(object): application = service.Application('Portal') # custom logging -logfile = logger.WeeklyLogFile(os.path.basename(settings.PORTAL_LOG_FILE), - os.path.dirname(settings.PORTAL_LOG_FILE)) -application.setComponent(ILogObserver, logger.PortalLogObserver(logfile).emit) + +if "--nodaemon" not in sys.argv: + logfile = logger.WeeklyLogFile(os.path.basename(settings.PORTAL_LOG_FILE), + os.path.dirname(settings.PORTAL_LOG_FILE)) + application.setComponent(ILogObserver, logger.PortalLogObserver(logfile).emit) # The main Portal server program. This sets up the database # and is where we store all the other services. @@ -331,7 +341,8 @@ if WEBSERVER_ENABLED: factory.noisy = False factory.protocol = webclient.WebSocketClient factory.sessionhandler = PORTAL_SESSIONS - websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=w_interface) + websocket_service = internet.TCPServer(port, WebSocketFactory(factory), + interface=w_interface) websocket_service.setName('EvenniaWebSocket%s:%s' % (w_ifacestr, port)) PORTAL.services.addService(websocket_service) websocket_started = True