diff --git a/app/classes/controllers/dummy_servers_controller.py b/app/classes/controllers/dummy_servers_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..5c156d90894e8ba61d719db61a418f6a333b19a2 --- /dev/null +++ b/app/classes/controllers/dummy_servers_controller.py @@ -0,0 +1,86 @@ +import asyncio +import logging +from typing import Dict, Callable + +from twisted.internet.defer import Deferred +from twisted.internet.tcp import Port + +from app.classes.shared.dummy_servers import DummyServers, DummyServerFactory +from app.classes.shared.singleton import Singleton + +logger = logging.getLogger(__name__) + + +class DummyServersController(metaclass=Singleton): + """Controller for dummy servers.""" + + def __init__(self): + self.dummy_servers = DummyServers() + self.dummy_by_id: Dict[int, Port] = {} + + def create_dummy_server( + self, + server_id: int, + server_port: int, + hibernation_motd: str, + wake_up_message: str, + online_mode: bool = True, + max_players: int = 1, + player_connected_callback: Callable = None, + ): + """Starts a new dummy server with the defined configuration. + Parameters: + server_id: The real server id. + server_port: The port to bind the dummy server. + hibernation_motd: The server description in the multiplayer menu. + wake_up_message: The message shown to the player when they connect. + online_mode: Enable online mode for the dummy server. + max_players: The max players allowed. + player_connected_callback: The callback for when a player connects to the server. Will be called + immediately after the "Connecting to server..." message appears to a player that is joining. + """ + + async def do_create(): + factory = DummyServerFactory() + + factory.motd = hibernation_motd + factory.close_message = wake_up_message + factory.max_players = max_players + factory.online_mode = online_mode + factory.player_connected_callback = player_connected_callback + + self.dummy_by_id[int(server_id)] = self.dummy_servers.reactor.listenTCP( + server_port, factory, interface="0.0.0.0" + ) + logger.info(f"Created dummy server in port {server_port}.") + + asyncio.run_coroutine_threadsafe(do_create(), self.dummy_servers.loop) + + def stop_dummy_server(self, server_id, port_freed_callback: Callable = None): + """Stops a dummy server for the server_id specified and frees up the used port. + Parameters: + server_id: The real server id. + port_freed_callback: The function that will be called when the port used by the dummy server + is completely freed. + """ + + async def do_stop(): + port: Port = self.dummy_by_id.pop(int(server_id), None) + if port: + deferred: Deferred = port.loseConnection() + if deferred and port_freed_callback: + deferred.addCallback(lambda ret: port_freed_callback()) + deferred.addErrback( + lambda err: logger.error( + "Error while trying to stop dummy server: ", err + ) + ) + elif port_freed_callback: + port_freed_callback() + elif port_freed_callback: + port_freed_callback() + + asyncio.run_coroutine_threadsafe(do_stop(), self.dummy_servers.loop) + + def is_dummy_server_running(self, server_id): + return int(server_id) in self.dummy_by_id diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index ca6c8d222a51c7767306eecffa1f88f8c9acd499..278d7967b07bf4bd13fdd3ffb19fcb02575eebcb 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -1,3 +1,4 @@ +import datetime import os import logging import time @@ -5,6 +6,7 @@ import json import pathlib import typing as t +from app.classes.controllers.dummy_servers_controller import DummyServersController from app.classes.controllers.roles_controller import RolesController from app.classes.shared.file_helpers import FileHelpers @@ -36,6 +38,10 @@ class ServersController(metaclass=Singleton): self.management_helper = management_helper self.servers_list = [] self.stats = Stats(self.helper, self) + self.dummy_servers = DummyServersController() + self.inactive_server_timeout = self.helper.get_setting( + "inactive_server_timeout", 10 + ) # ********************************************************************************** # Generic Servers Methods @@ -97,12 +103,33 @@ class ServersController(metaclass=Singleton): @staticmethod def update_server(server_obj): + if not server_obj.close_when_inactive: + server_obj.hibernation_motd = None + server_obj.wake_up_message = None + ret = HelperServers.update_server(server_obj) - server_instance: ServerInstance = ServersController().get_server_instance_by_id( + servers_controller = ServersController() + server_instance: ServerInstance = servers_controller.get_server_instance_by_id( server_obj.server_id ) server_instance.update_server_instance() + # Check if there's the need to open/close a dummy server when updating + is_running = bool(server_instance.stats_helper.get_server_stats()["running"]) + if ( + not is_running + and not servers_controller.dummy_servers.is_dummy_server_running( + server_obj.server_id + ) + ): + servers_controller.check_inactive_servers() + # Restart dummy server in case the server port was updated while it's running + elif not is_running: + servers_controller.dummy_servers.stop_dummy_server( + server_obj.server_id, + port_freed_callback=servers_controller.check_inactive_servers, + ) + return ret def get_history_stats(self, server_id, days): @@ -136,6 +163,8 @@ class ServersController(metaclass=Singleton): return server.stats_helper.get_import_status() def remove_server(self, server_id): + self.dummy_servers.stop_dummy_server(server_id) + roles_list = PermissionsServers.get_roles_from_server(server_id) for role in roles_list: role_id = role.role_id @@ -579,3 +608,61 @@ class ServersController(metaclass=Singleton): log_file_path, logs_delete_after ): os.remove(log_file_path) + + def run_dummy_server(self, server_obj: ServerInstance): + self.dummy_servers.create_dummy_server( + server_obj.server_id, + server_obj.server_object.server_port, + hibernation_motd=server_obj.server_object.hibernation_motd or "", + wake_up_message=server_obj.server_object.wake_up_message or "", + online_mode=False, + max_players=1, + player_connected_callback=lambda: server_obj.start_server(None), + ) + + def check_inactive_servers(self): + logger.info("Checking inactive servers.") + + servers = self.servers_list + for server in servers: + server_obj: ServerInstance = server["server_obj"] + stats = server_obj.get_servers_stats() + + is_running = bool(stats["running"]) + close_when_inactive = server_obj.server_object.close_when_inactive + + if is_running and close_when_inactive and int(stats["online"]) == 0: + running_diff = datetime.datetime.now(datetime.timezone.utc).replace( + tzinfo=None + ) - datetime.datetime.strptime(stats["started"], "%Y-%m-%d %H:%M:%S") + + if (running_diff.total_seconds() / 60) > self.inactive_server_timeout: + logger.info( + "No players running for server {0} after {1} minute timeout, stopping and running " + "dummy server on its port ({2}).".format( + server_obj.server_id, + self.inactive_server_timeout, + server_obj.server_object.server_port, + ) + ) + + self.stop_server(server_obj.server_id) + self.run_dummy_server(server_obj) + pass + elif ( + not is_running + and close_when_inactive + and not self.dummy_servers.is_dummy_server_running(server_obj.server_id) + ): + logger.info( + "Server {0} has close_when_inactive activated, running new dummy server on its port ({1}).".format( + server_obj.server_id, server_obj.server_object.server_port + ) + ) + self.run_dummy_server(server_obj) + elif ( + not is_running + and not close_when_inactive + and self.dummy_servers.is_dummy_server_running(server_obj.server_id) + ): + self.dummy_servers.stop_dummy_server(server_obj.server_id) diff --git a/app/classes/models/servers.py b/app/classes/models/servers.py index a83fd0a2a6fb8d65ccf5526bebc853b5de142a09..ec21751c61e454d8e616dec7b04a5a355af58861 100644 --- a/app/classes/models/servers.py +++ b/app/classes/models/servers.py @@ -42,6 +42,9 @@ class Servers(BaseModel): created_by = IntegerField(default=-100) shutdown_timeout = IntegerField(default=60) ignored_exits = CharField(default="0") + close_when_inactive = BooleanField(default=False) + hibernation_motd = CharField(default=None, null=True) + wake_up_message = CharField(default=None, null=True) class Meta: table_name = "servers" @@ -71,6 +74,9 @@ class HelperServers: created_by: int, server_port: int = 25565, server_host: str = "127.0.0.1", + close_when_inactive: bool = False, + hibernation_motd: str = None, + wake_up_message: str = None, ) -> int: """Create a server in the database @@ -87,6 +93,9 @@ class HelperServers: server_port: The port the server will be monitored on, defaults to 25565 server_host: The host the server will be monitored on, defaults to 127.0.0.1 show_status: Should Crafty show this server on the public status page + close_when_inactive: Should Crafty close this server when inactive and run it again when someone tries to connect + hibernation_motd: Motd that should be shown in dummy server when hibernating + wake_up_message: Message sent to the player when they try to connect to a dummy server when hibernating Returns: int: The new server's id @@ -111,6 +120,9 @@ class HelperServers: Servers.backup_path: backup_path, Servers.type: server_type, Servers.created_by: created_by, + Servers.close_when_inactive: close_when_inactive, + Servers.hibernation_motd: hibernation_motd, + Servers.wake_up_message: wake_up_message, } ).execute() diff --git a/app/classes/shared/dummy_servers.py b/app/classes/shared/dummy_servers.py new file mode 100644 index 0000000000000000000000000000000000000000..d5e047fda80eab0a5a2f4d5b49060d2c4ed93d9a --- /dev/null +++ b/app/classes/shared/dummy_servers.py @@ -0,0 +1,72 @@ +import logging +import sys +import threading +import asyncio +from typing import Callable + +from quarry.net.server import ServerFactory, ServerProtocol +from twisted.internet import asyncioreactor + +from app.classes.shared.singleton import Singleton + +logger = logging.getLogger(__name__) + + +class DummyServers(metaclass=Singleton): + """Manager for the dummy server thread""" + + def __init__(self): + self.thread = threading.Thread( + daemon=True, + target=self.run_thread, + name="dummy_servers_thread", + ) + self.reactor: asyncioreactor.AsyncioSelectorReactor | None = None + self.loop: asyncio.AbstractEventLoop | None = None + + def init_servers(self): + self.thread.start() + + def run_thread(self): + if "twisted.internet.reactor" in sys.modules: + del sys.modules["twisted.internet.reactor"] + + self.loop = asyncio.new_event_loop() + asyncioreactor.install(self.loop) + + from twisted.internet import reactor + + self.reactor = reactor + self.reactor.run(installSignalHandlers=False) + + +class DummyServerProtocol(ServerProtocol): + """Class that implements what to do after client connects to dummy server.""" + + def player_joined(self): + logger.info("Player connected in server. Stopping dummy server.") + ServerProtocol.player_joined(self) + self.close(self.factory.close_message) + + if self.factory.player_connected_callback: + self.factory.player_connected_callback() + + +class DummyServerFactory(ServerFactory): + """Factory class for a dummy server.""" + + def __init__( + self, + motd: str = "", + close_message: str = "", + online_mode: bool = False, + max_players: int = 20, + player_connected_callback: Callable = None, + ): + super().__init__() + self.protocol = DummyServerProtocol + self.motd = motd + self.close_message = close_message + self.online_mode = online_mode + self.max_players = max_players + self.player_connected_callback = player_connected_callback diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 489115ae976fe331a361eb69834ec1cd9c52c902..c9efff83e5b6bc210cefe0293d73c0913092816b 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -481,6 +481,7 @@ class Helpers: "monitored_mounts": mounts, "dir_size_poll_freq_minutes": 5, "crafty_logs_delete_after_days": 0, + "inactive_server_timeout": 10, } def get_all_settings(self): diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index c1a11158aba7d0d828fe313165189cde8c014084..3c298c53357c4a7d84fd2b3954f3858b706f2b6f 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -21,6 +21,7 @@ from tzlocal.utils import ZoneInfoNotFoundError from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.jobstores.base import JobLookupError +from app.classes.controllers.dummy_servers_controller import DummyServersController from app.classes.minecraft.stats import Stats from app.classes.minecraft.mc_ping import ping, ping_bedrock from app.classes.models.servers import HelperServers, Servers @@ -133,6 +134,7 @@ class ServerInstance: self.server_object = HelperServers.get_server_obj(self.server_id) self.stats_helper = HelperServerStats(self.server_id) self.last_backup_failed = False + self.dummy_servers = DummyServersController() try: with open( os.path.join(self.server_object.path, "db_stats", "players_cache.json"), @@ -314,280 +316,305 @@ class ServerInstance: Console.critical(f"Unable to write/access {self.server_path}") def start_server(self, user_id, forge_install=False): - if not user_id: - user_lang = self.helper.get_setting("language") - else: - user_lang = HelperUsers.get_user_lang_by_id(user_id) - - # Checks if user is currently attempting to move global server - # dir - if self.helper.dir_migration: - self.helper.websocket_helper.broadcast_user( - user_id, - "send_start_error", - { - "error": self.helper.translation.translate( - "error", - "migration", - user_lang, - ) - }, - ) - return False + def do_start(): + if not user_id: + user_lang = self.helper.get_setting("language") + else: + user_lang = HelperUsers.get_user_lang_by_id(user_id) - if self.stats_helper.get_import_status() and not forge_install: - if user_id: + # Checks if user is currently attempting to move global server + # dir + if self.helper.dir_migration: self.helper.websocket_helper.broadcast_user( user_id, "send_start_error", { "error": self.helper.translation.translate( - "error", "not-downloaded", user_lang + "error", + "migration", + user_lang, ) }, ) - return False - - logger.info( - f"Start command detected. Reloading settings from DB for server {self.name}" - ) - self.setup_server_run_command() - # fail safe in case we try to start something already running - if self.check_running(): - logger.error("Server is already running - Cancelling Startup") - Console.error("Server is already running - Cancelling Startup") - return False - if self.check_update(): - logger.error("Server is updating. Terminating startup.") - return False + return False - logger.info(f"Launching Server {self.name} with command {self.server_command}") - Console.info(f"Launching Server {self.name} with command {self.server_command}") + if self.stats_helper.get_import_status() and not forge_install: + if user_id: + self.helper.websocket_helper.broadcast_user( + user_id, + "send_start_error", + { + "error": self.helper.translation.translate( + "error", "not-downloaded", user_lang + ) + }, + ) + return False - # Checks for eula. Creates one if none detected. - # If EULA is detected and not set to true we offer to set it true. - e_flag = False - if Helpers.check_file_exists(os.path.join(self.settings["path"], "eula.txt")): - with open( - os.path.join(self.settings["path"], "eula.txt"), "r", encoding="utf-8" - ) as f: - line = f.readline().lower() - e_flag = line in [ - "eula=true", - "eula = true", - "eula= true", - "eula =true", - ] - # If this is a forge installer we're running we can bypass the eula checks. - if forge_install is True: - e_flag = True - if not e_flag and self.settings["type"] == "minecraft-java": - if user_id: - self.helper.websocket_helper.broadcast_user( - user_id, "send_eula_bootbox", {"id": self.server_id} - ) - else: - logger.error( - "Autostart failed due to EULA being false. " - "Agree not sent due to auto start." - ) - return False - if Helpers.is_os_windows(): - logger.info("Windows Detected") - else: - logger.info("Unix Detected") + logger.info( + f"Start command detected. Reloading settings from DB for server {self.name}" + ) + self.setup_server_run_command() + # fail safe in case we try to start something already running + if self.check_running(): + logger.error("Server is already running - Cancelling Startup") + Console.error("Server is already running - Cancelling Startup") + return False + if self.check_update(): + logger.error("Server is updating. Terminating startup.") + return False - logger.info( - f"Starting server in {self.server_path} with command: {self.server_command}" - ) + logger.info( + f"Launching Server {self.name} with command {self.server_command}" + ) + Console.info( + f"Launching Server {self.name} with command {self.server_command}" + ) - # checks to make sure file is openable (downloaded) and exists. - try: - with open( - os.path.join( - self.server_path, - HelperServers.get_server_data_by_id(self.server_id)["executable"], - ), - "r", - encoding="utf-8", + # Checks for eula. Creates one if none detected. + # If EULA is detected and not set to true we offer to set it true. + e_flag = False + if Helpers.check_file_exists( + os.path.join(self.settings["path"], "eula.txt") ): - # Can open the file - pass - - except: - if user_id: - self.helper.websocket_helper.broadcast_user( - user_id, - "send_start_error", - { - "error": self.helper.translation.translate( - "error", "not-downloaded", user_lang - ) - }, - ) - return + with open( + os.path.join(self.settings["path"], "eula.txt"), + "r", + encoding="utf-8", + ) as f: + line = f.readline().lower() + e_flag = line in [ + "eula=true", + "eula = true", + "eula= true", + "eula =true", + ] + # If this is a forge installer we're running we can bypass the eula checks. + if forge_install is True: + e_flag = True + if not e_flag and self.settings["type"] == "minecraft-java": + if user_id: + self.helper.websocket_helper.broadcast_user( + user_id, "send_eula_bootbox", {"id": self.server_id} + ) + else: + logger.error( + "Autostart failed due to EULA being false. " + "Agree not sent due to auto start." + ) + return False + if Helpers.is_os_windows(): + logger.info("Windows Detected") + else: + logger.info("Unix Detected") - if ( - not Helpers.is_os_windows() - and HelperServers.get_server_type_by_id(self.server_id) - == "minecraft-bedrock" - ): logger.info( - f"Bedrock and Unix detected for server {self.name}. " - f"Switching to appropriate execution string" + f"Starting server in {self.server_path} with command: {self.server_command}" ) - my_env = os.environ - my_env["LD_LIBRARY_PATH"] = self.server_path + + # checks to make sure file is openable (downloaded) and exists. try: - self.process = subprocess.Popen( - self.server_command, - cwd=self.server_path, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=my_env, - ) - except Exception as ex: - logger.error( - f"Server {self.name} failed to start with error code: {ex}" - ) + with open( + os.path.join( + self.server_path, + HelperServers.get_server_data_by_id(self.server_id)[ + "executable" + ], + ), + "r", + encoding="utf-8", + ): + # Can open the file + pass + + except: if user_id: self.helper.websocket_helper.broadcast_user( user_id, "send_start_error", { "error": self.helper.translation.translate( - "error", "start-error", user_lang - ).format(self.name, ex) + "error", "not-downloaded", user_lang + ) }, ) - if forge_install: - # Reset import status if failed while forge installing - self.stats_helper.finish_import() - return False + return - else: - try: - self.process = subprocess.Popen( - self.server_command, - cwd=self.server_path, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, + if ( + not Helpers.is_os_windows() + and HelperServers.get_server_type_by_id(self.server_id) + == "minecraft-bedrock" + ): + logger.info( + f"Bedrock and Unix detected for server {self.name}. " + f"Switching to appropriate execution string" ) - except Exception as ex: - # Checks for java on initial fail - if not self.helper.detect_java(): + my_env = os.environ + my_env["LD_LIBRARY_PATH"] = self.server_path + try: + self.process = subprocess.Popen( + self.server_command, + cwd=self.server_path, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=my_env, + ) + except Exception as ex: + logger.error( + f"Server {self.name} failed to start with error code: {ex}" + ) if user_id: self.helper.websocket_helper.broadcast_user( user_id, "send_start_error", { "error": self.helper.translation.translate( - "error", "noJava", user_lang - ).format(self.name) + "error", "start-error", user_lang + ).format(self.name, ex) }, ) + if forge_install: + # Reset import status if failed while forge installing + self.stats_helper.finish_import() return False - logger.error( - f"Server {self.name} failed to start with error code: {ex}" - ) - if user_id: - self.helper.websocket_helper.broadcast_user( - user_id, - "send_start_error", - { - "error": self.helper.translation.translate( - "error", "start-error", user_lang - ).format(self.name, ex) - }, - ) - if forge_install: - # Reset import status if failed while forge installing - self.stats_helper.finish_import() - return False - out_buf = ServerOutBuf(self.helper, self.process, self.server_id) - - logger.debug(f"Starting virtual terminal listener for server {self.name}") - threading.Thread( - target=out_buf.check, daemon=True, name=f"{self.server_id}_virtual_terminal" - ).start() + else: + try: + self.process = subprocess.Popen( + self.server_command, + cwd=self.server_path, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + except Exception as ex: + # Checks for java on initial fail + if not self.helper.detect_java(): + if user_id: + self.helper.websocket_helper.broadcast_user( + user_id, + "send_start_error", + { + "error": self.helper.translation.translate( + "error", "noJava", user_lang + ).format(self.name) + }, + ) + return False + logger.error( + f"Server {self.name} failed to start with error code: {ex}" + ) + if user_id: + self.helper.websocket_helper.broadcast_user( + user_id, + "send_start_error", + { + "error": self.helper.translation.translate( + "error", "start-error", user_lang + ).format(self.name, ex) + }, + ) + if forge_install: + # Reset import status if failed while forge installing + self.stats_helper.finish_import() + return False - self.is_crashed = False - self.stats_helper.server_crash_reset() + out_buf = ServerOutBuf(self.helper, self.process, self.server_id) - self.start_time = str(datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")) + logger.debug(f"Starting virtual terminal listener for server {self.name}") + threading.Thread( + target=out_buf.check, + daemon=True, + name=f"{self.server_id}_virtual_terminal", + ).start() - if self.process.poll() is None: - logger.info(f"Server {self.name} running with PID {self.process.pid}") - Console.info(f"Server {self.name} running with PID {self.process.pid}") self.is_crashed = False self.stats_helper.server_crash_reset() - self.record_server_stats() - check_internet_thread = threading.Thread( - target=self.check_internet_thread, - daemon=True, - args=( - user_id, - user_lang, - ), - name=f"{self.name}_Internet", + + self.start_time = str( + datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") ) - check_internet_thread.start() - # Checks if this is the servers first run. - if self.stats_helper.get_first_run(): - self.stats_helper.set_first_run() - loc_server_port = self.stats_helper.get_server_stats()["server_port"] - # Sends port reminder message. - self.helper.websocket_helper.broadcast_user( - user_id, - "send_start_error", - { - "error": self.helper.translation.translate( - "error", "portReminder", user_lang - ).format(self.name, loc_server_port) - }, + + if self.process.poll() is None: + logger.info(f"Server {self.name} running with PID {self.process.pid}") + Console.info(f"Server {self.name} running with PID {self.process.pid}") + self.is_crashed = False + self.stats_helper.server_crash_reset() + self.record_server_stats() + check_internet_thread = threading.Thread( + target=self.check_internet_thread, + daemon=True, + args=( + user_id, + user_lang, + ), + name=f"{self.name}_Internet", ) - server_users = PermissionsServers.get_server_user_list(self.server_id) - for user in server_users: - if user != user_id: + check_internet_thread.start() + # Checks if this is the servers first run. + if self.stats_helper.get_first_run(): + self.stats_helper.set_first_run() + loc_server_port = self.stats_helper.get_server_stats()[ + "server_port" + ] + # Sends port reminder message. + self.helper.websocket_helper.broadcast_user( + user_id, + "send_start_error", + { + "error": self.helper.translation.translate( + "error", "portReminder", user_lang + ).format(self.name, loc_server_port) + }, + ) + server_users = PermissionsServers.get_server_user_list( + self.server_id + ) + for user in server_users: + if user != user_id: + self.helper.websocket_helper.broadcast_user( + user, "send_start_reload", {} + ) + else: + server_users = PermissionsServers.get_server_user_list( + self.server_id + ) + for user in server_users: self.helper.websocket_helper.broadcast_user( user, "send_start_reload", {} ) else: - server_users = PermissionsServers.get_server_user_list(self.server_id) - for user in server_users: - self.helper.websocket_helper.broadcast_user( - user, "send_start_reload", {} - ) - else: - logger.warning( - f"Server PID {self.process.pid} died right after starting " - f"- is this a server config issue?" - ) - Console.critical( - f"Server PID {self.process.pid} died right after starting " - f"- is this a server config issue?" - ) + logger.warning( + f"Server PID {self.process.pid} died right after starting " + f"- is this a server config issue?" + ) + Console.critical( + f"Server PID {self.process.pid} died right after starting " + f"- is this a server config issue?" + ) - if self.settings["crash_detection"]: - logger.info( - f"Server {self.name} has crash detection enabled " - f"- starting watcher task" - ) - Console.info( - f"Server {self.name} has crash detection enabled " - f"- starting watcher task" - ) + if self.settings["crash_detection"]: + logger.info( + f"Server {self.name} has crash detection enabled " + f"- starting watcher task" + ) + Console.info( + f"Server {self.name} has crash detection enabled " + f"- starting watcher task" + ) - self.server_scheduler.add_job( - self.detect_crash, "interval", seconds=30, id=f"c_{self.server_id}" - ) + self.server_scheduler.add_job( + self.detect_crash, "interval", seconds=30, id=f"c_{self.server_id}" + ) - # If this is a forge install we'll call the watcher to do the things - if forge_install: - self.forge_install_watcher() + # If this is a forge install we'll call the watcher to do the things + if forge_install: + self.forge_install_watcher() + + self.dummy_servers.stop_dummy_server( + self.server_id, port_freed_callback=do_start + ) def check_internet_thread(self, user_id, user_lang): if user_id: diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index acdc1cac6bf465350bf659d9cf70ca229b59ffea..773bd7d26b0458f67167efe5a41139953a56b567 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -16,6 +16,7 @@ from app.classes.models.management import HelpersManagement from app.classes.models.users import HelperUsers from app.classes.controllers.users_controller import UsersController from app.classes.shared.console import Console +from app.classes.shared.dummy_servers import DummyServers from app.classes.shared.file_helpers import FileHelpers from app.classes.shared.helpers import Helpers from app.classes.shared.main_controller import Controller @@ -57,6 +58,8 @@ class TasksManager: self.users_controller: UsersController = self.controller.users + self.dummy_server_manager = DummyServers() + self.webserver_thread = threading.Thread( target=self.tornado.run_tornado, daemon=True, name="tornado_thread" ) @@ -81,6 +84,9 @@ class TasksManager: self.reload_schedule_from_db() + def start_dummy_servers(self): + self.dummy_server_manager.init_servers() + def get_main_thread_run_status(self): return self.main_thread_exiting @@ -203,6 +209,14 @@ class TasksManager: # self.scheduler.add_job( # self.scheduler.print_jobs, "interval", seconds=10, id="-1" # ) + self.controller.servers.check_inactive_servers() + self.scheduler.add_job( + self.controller.servers.check_inactive_servers, + "interval", + minutes=1, + id="check_inactive_servers", + start_date=datetime.datetime.now(), + ) # load schedules from DB for schedule in schedules: diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 20c76c1a98a084281161b8c328f4f12a74eeae93..d4a810012434c72989b052d3b552c165033204a3 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -536,6 +536,9 @@ class PanelHandler(BaseHandler): "auto_start": server_temp_obj["auto_start"], "crash_detection": server_temp_obj["crash_detection"], "show_status": server_temp_obj["show_status"], + "close_when_inactive": server_temp_obj["close_when_inactive"], + "hibernation_motd": server_temp_obj["hibernation_motd"], + "wake_up_message": server_temp_obj["wake_up_message"], "ignored_exits": server_temp_obj["ignored_exits"], }, "running": False, diff --git a/app/classes/web/routes/api/servers/server/index.py b/app/classes/web/routes/api/servers/server/index.py index afe02a0b72b2ecb3f5dd17fa5ee73e1e36ad2fa3..2570c1af2ce9b27b892d8b8af3bcbaf76d8012c6 100644 --- a/app/classes/web/routes/api/servers/server/index.py +++ b/app/classes/web/routes/api/servers/server/index.py @@ -21,6 +21,9 @@ server_patch_schema = { "auto_start": {"type": "boolean"}, "auto_start_delay": {"type": "integer", "minimum": 0}, "crash_detection": {"type": "boolean"}, + "close_when_inactive": {"type": "boolean"}, + "hibernation_motd": {"type": "string"}, + "wake_up_message": {"type": "string"}, "stop_command": {"type": "string"}, "executable_update_url": {"type": "string"}, "server_ip": {"type": "string", "minLength": 1}, @@ -42,6 +45,9 @@ basic_server_patch_schema = { "auto_start": {"type": "boolean"}, "auto_start_delay": {"type": "integer", "minimum": 0}, "crash_detection": {"type": "boolean"}, + "close_when_inactive": {"type": "boolean"}, + "hibernation_motd": {"type": "string"}, + "wake_up_message": {"type": "string"}, "stop_command": {"type": "string"}, "shutdown_timeout": {"type": "integer"}, "logs_delete_after": {"type": "integer", "minimum": 0}, diff --git a/app/frontend/templates/panel/server_config.html b/app/frontend/templates/panel/server_config.html index 913a2000365b3da0ec9302c381100bf779960886..54c464ab70fbd0bd3641a9715839728673ee3e23 100644 --- a/app/frontend/templates/panel/server_config.html +++ b/app/frontend/templates/panel/server_config.html @@ -166,6 +166,35 @@ +