diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc69fc9..f47beb06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ ## --- [4.0.12] - 2022/09/04 ### New features - Win Portable Updater will now be included in Windows Package ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/446)) +- Bedrock Server Creator ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/443)) ### Bug fixes -- Fix performance issues on server metrics panels (Temporarily setting to 24hr query) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/440)) +- Fix performance issues on server metrics panels 'with metrics range' ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/440)) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/448)) - Fix no id on import3 servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/442)) +- Fix functionality of bedrock update ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/441)) ### Tweaks TBD ### Lang diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index c9434b13..1bf81564 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -105,10 +105,9 @@ class ServersController(metaclass=Singleton): server_instance.update_server_instance() return ret - def get_history_stats(self, server_id): - now = datetime.datetime.now() + def get_history_stats(self, server_id, days): srv = ServersController().get_server_instance_by_id(server_id) - return srv.stats_helper.get_history_stats(server_id) + return srv.stats_helper.get_history_stats(server_id, days) @staticmethod def update_unloaded_server(server_obj): diff --git a/app/classes/models/server_stats.py b/app/classes/models/server_stats.py index d6c6efdc..29fdb856 100644 --- a/app/classes/models/server_stats.py +++ b/app/classes/models/server_stats.py @@ -138,8 +138,8 @@ class HelperServerStats: ) return server_data - def get_history_stats(self, server_id): - max_age = datetime.datetime.now() - timedelta(days=1) + def get_history_stats(self, server_id, num_days): + max_age = datetime.datetime.now() - timedelta(days=num_days) return ( ServerStats.select() .where(ServerStats.created > max_age) diff --git a/app/classes/shared/file_helpers.py b/app/classes/shared/file_helpers.py index 04ec3305..28edbef7 100644 --- a/app/classes/shared/file_helpers.py +++ b/app/classes/shared/file_helpers.py @@ -64,6 +64,11 @@ class FileHelpers: FileHelpers.copy_dir(src_path, dest_path) FileHelpers.del_dirs(src_path) + @staticmethod + def move_dir_exist(src_path, dest_path): + FileHelpers.copy_dir(src_path, dest_path, True) + FileHelpers.del_dirs(src_path) + @staticmethod def move_file(src_path, dest_path): FileHelpers.copy_file(src_path, dest_path) @@ -290,7 +295,7 @@ class FileHelpers: for item in os.listdir(full_root_path): if os.path.isdir(os.path.join(full_root_path, item)): try: - FileHelpers.move_dir( + FileHelpers.move_dir_exist( os.path.join(full_root_path, item), os.path.join(new_dir, item), ) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 40a57219..be278578 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -107,6 +107,42 @@ class Helpers: logger.error(f"Unable to check for new crafty version! \n{e}") return False + @staticmethod + def get_latest_bedrock_url(): + """ + Get latest bedrock executable url \n\n + returns url if successful, False if not + """ + url = "https://minecraft.net/en-us/download/server/bedrock/" + headers = { + "Accept-Encoding": "identity", + "Accept-Language": "en", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/104.0.0.0 Safari/537.36" + ), + } + target_win = 'https://minecraft.azureedge.net/bin-win/[^"]*' + target_linux = 'https://minecraft.azureedge.net/bin-linux/[^"]*' + + try: + # Get minecraft server download page + # (hopefully the don't change the structure) + download_page = get(url, headers=headers) + + # Search for our string targets + win_download_url = re.search(target_win, download_page.text).group(0) + linux_download_url = re.search(target_linux, download_page.text).group(0) + + if os.name == "nt": + return win_download_url + + return linux_download_url + except Exception as e: + logger.error(f"Unable to resolve remote bedrock download url! \n{e}") + return False + @staticmethod def find_java_installs(): # If we're windows return oracle java versions, diff --git a/app/classes/shared/import_helper.py b/app/classes/shared/import_helper.py index 769ebc3a..88ee91fc 100644 --- a/app/classes/shared/import_helper.py +++ b/app/classes/shared/import_helper.py @@ -3,6 +3,7 @@ import time import shutil import logging import threading +import urllib from app.classes.controllers.server_perms_controller import PermissionsServers from app.classes.controllers.servers_controller import ServersController @@ -214,3 +215,43 @@ class ImportHelpers: os.chmod(full_jar_path, 0o2760) # deletes temp dir FileHelpers.del_dirs(temp_dir) + + def download_bedrock_server(self, path, new_id): + download_thread = threading.Thread( + target=self.download_threaded_bedrock_server, + daemon=True, + args=(path, new_id), + name=f"{new_id}_download", + ) + download_thread.start() + + def download_threaded_bedrock_server(self, path, new_id): + + # downloads zip from remote url + try: + bedrock_url = Helpers.get_latest_bedrock_url() + if bedrock_url.lower().startswith("https"): + urllib.request.urlretrieve( + bedrock_url, + os.path.join(path, "bedrock_server.zip"), + ) + + unzip_path = os.path.join(path, "bedrock_server.zip") + unzip_path = self.helper.wtol_path(unzip_path) + # unzips archive that was downloaded. + FileHelpers.unzip_file(unzip_path) + # adjusts permissions for execution if os is not windows + if not self.helper.is_os_windows(): + os.chmod(os.path.join(path, "bedrock_server"), 0o0744) + + # we'll delete the zip we downloaded now + os.remove(os.path.join(path, "bedrock_server.zip")) + except Exception as e: + logger.critical( + f"Failed to download bedrock executable during server creation! \n{e}" + ) + + ServersController.finish_import(new_id) + server_users = PermissionsServers.get_server_user_list(new_id) + for user in server_users: + self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {}) diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py index cfda4c8d..68645b86 100644 --- a/app/classes/shared/main_controller.py +++ b/app/classes/shared/main_controller.py @@ -697,6 +697,49 @@ class Controller: ) return new_id + def create_bedrock_server(self, server_name, user_id): + server_id = Helpers.create_uuid() + new_server_dir = os.path.join(self.helper.servers_dir, server_id) + backup_path = os.path.join(self.helper.backup_path, server_id) + server_exe = "bedrock_server" + if Helpers.is_os_windows(): + # if this is windows we will override the linux bedrock server name. + server_exe = "bedrock_server.exe" + new_server_dir = Helpers.wtol_path(new_server_dir) + backup_path = Helpers.wtol_path(backup_path) + new_server_dir.replace(" ", "^ ") + backup_path.replace(" ", "^ ") + + Helpers.ensure_dir_exists(new_server_dir) + Helpers.ensure_dir_exists(backup_path) + + full_jar_path = os.path.join(new_server_dir, server_exe) + + if Helpers.is_os_windows(): + server_command = f'"{full_jar_path}"' + else: + server_command = f"./{server_exe}" + logger.debug("command: " + server_command) + server_log_file = "" + server_stop = "stop" + + new_id = self.register_server( + server_name, + server_id, + new_server_dir, + backup_path, + server_command, + server_exe, + server_log_file, + server_stop, + "19132", + user_id, + server_type="minecraft-bedrock", + ) + ServersController.set_import(new_id) + self.import_helper.download_bedrock_server(new_server_dir, new_id) + return new_id + def import_bedrock_zip_server( self, server_name: str, diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index 0b2c5b96..6501fb1c 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -9,6 +9,7 @@ import threading import logging.config import subprocess import html +import urllib.request # TZLocal is set as a hidden import on win pipeline from tzlocal import get_localzone @@ -337,7 +338,7 @@ class ServerInstance: "eula =true", ] - if not e_flag: + 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} @@ -1078,66 +1079,91 @@ class ServerInstance: ) # checks if backup directory already exists if os.path.isdir(backup_dir): - backup_executable = os.path.join(backup_dir, "old_server.jar") + backup_executable = os.path.join(backup_dir, self.settings["executable"]) else: logger.info( f"Executable backup directory not found for Server: {self.name}." f" Creating one." ) os.mkdir(backup_dir) - backup_executable = os.path.join(backup_dir, "old_server.jar") + backup_executable = os.path.join(backup_dir, self.settings["executable"]) - if os.path.isfile(backup_executable): - # removes old backup - logger.info(f"Old backup found for server: {self.name}. Removing...") - os.remove(backup_executable) - logger.info(f"Old backup removed for server: {self.name}.") - else: - logger.info(f"No old backups found for server: {self.name}") + if len(os.listdir(backup_dir)) > 0: + # removes old backup + logger.info(f"Old backups found for server: {self.name}. Removing...") + for item in os.listdir(backup_dir): + os.remove(os.path.join(backup_dir, item)) + logger.info(f"Old backups removed for server: {self.name}.") + else: + logger.info(f"No old backups found for server: {self.name}") current_executable = os.path.join( Helpers.get_os_understandable_path(self.settings["path"]), self.settings["executable"], ) - # copies to backup dir - FileHelpers.copy_file(current_executable, backup_executable) + try: + # copies to backup dir + FileHelpers.copy_file(current_executable, backup_executable) + except FileNotFoundError: + logger.error("Could not create backup of jarfile. File not found.") - # boolean returns true for false for success - downloaded = Helpers.download_file( - self.settings["executable_update_url"], current_executable - ) + # wait for backup + while self.is_backingup: + time.sleep(10) - while self.stats_helper.get_server_stats()["updating"]: - if downloaded and not self.is_backingup: - logger.info("Executable updated successfully. Starting Server") + # check if backup was successful + if self.last_backup_failed: + server_users = PermissionsServers.get_server_user_list(self.server_id) + for user in server_users: + self.helper.websocket_helper.broadcast_user( + user, + "notification", + "Backup failed for " + self.name + ". canceling update.", + ) + return False - self.stats_helper.set_update(False) - if len(self.helper.websocket_helper.clients) > 0: - # There are clients - self.check_update() - server_users = PermissionsServers.get_server_user_list( - self.server_id + # lets download the files + if HelperServers.get_server_type_by_id(self.server_id) != "minecraft-bedrock": + # boolean returns true for false for success + downloaded = Helpers.download_file( + self.settings["executable_update_url"], current_executable + ) + else: + # downloads zip from remote url + try: + bedrock_url = Helpers.get_latest_bedrock_url() + if bedrock_url.lower().startswith("https"): + urllib.request.urlretrieve( + bedrock_url, + os.path.join(self.settings["path"], "bedrock_server.zip"), ) - for user in server_users: - self.helper.websocket_helper.broadcast_user( - user, - "notification", - "Executable update finished for " + self.name, - ) - time.sleep(3) - self.helper.websocket_helper.broadcast_page( - "/panel/server_detail", - "update_button_status", - { - "isUpdating": self.check_update(), - "server_id": self.server_id, - "wasRunning": was_started, - }, - ) - self.helper.websocket_helper.broadcast_page( - "/panel/dashboard", "send_start_reload", {} + + unzip_path = os.path.join(self.settings["path"], "bedrock_server.zip") + unzip_path = self.helper.wtol_path(unzip_path) + # unzips archive that was downloaded. + FileHelpers.unzip_file(unzip_path) + # adjusts permissions for execution if os is not windows + if not self.helper.is_os_windows(): + os.chmod( + os.path.join(self.settings["path"], "bedrock_server"), 0o0744 ) + + # we'll delete the zip we downloaded now + os.remove(os.path.join(self.settings["path"], "bedrock_server.zip")) + downloaded = True + except Exception as e: + logger.critical( + f"Failed to download bedrock executable for update \n{e}" + ) + + if downloaded: + logger.info("Executable updated successfully. Starting Server") + + self.stats_helper.set_update(False) + if len(self.helper.websocket_helper.clients) > 0: + # There are clients + self.check_update() server_users = PermissionsServers.get_server_user_list(self.server_id) for user in server_users: self.helper.websocket_helper.broadcast_user( @@ -1145,29 +1171,52 @@ class ServerInstance: "notification", "Executable update finished for " + self.name, ) - - self.management_helper.add_to_audit_log_raw( - "Alert", - "-1", - self.server_id, - "Executable update finished for " + self.name, - self.settings["server_ip"], + # sleep so first notif can completely run + time.sleep(3) + self.helper.websocket_helper.broadcast_page( + "/panel/server_detail", + "update_button_status", + { + "isUpdating": self.check_update(), + "server_id": self.server_id, + "wasRunning": was_started, + }, ) - if was_started: - self.start_server() - elif not downloaded and not self.is_backingup: - time.sleep(5) - server_users = PermissionsServers.get_server_user_list(self.server_id) - for user in server_users: - self.helper.websocket_helper.broadcast_user( - user, - "notification", - "Executable update failed for " - + self.name - + ". Check log file for details.", - ) - logger.error("Executable download failed.") - self.stats_helper.set_update(False) + self.helper.websocket_helper.broadcast_page( + "/panel/dashboard", "send_start_reload", {} + ) + self.helper.websocket_helper.broadcast_page( + "/panel/server_detail", "remove_spinner", {} + ) + server_users = PermissionsServers.get_server_user_list(self.server_id) + for user in server_users: + self.helper.websocket_helper.broadcast_user( + user, + "notification", + "Executable update finished for " + self.name, + ) + + self.management_helper.add_to_audit_log_raw( + "Alert", + "-1", + self.server_id, + "Executable update finished for " + self.name, + self.settings["server_ip"], + ) + if was_started: + self.start_server() + else: + server_users = PermissionsServers.get_server_user_list(self.server_id) + for user in server_users: + self.helper.websocket_helper.broadcast_user( + user, + "notification", + "Executable update failed for " + + self.name + + ". Check log file for details.", + ) + logger.error("Executable download failed.") + self.stats_helper.set_update(False) # ********************************************************************************** # Minecraft Servers Statistics diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index a8bac6e2..c7708f39 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -756,8 +756,21 @@ class PanelHandler(BaseHandler): page_data["backup_path"] = Helpers.wtol_path(server_info["backup_path"]) if subpage == "metrics": + try: + days = int(self.get_argument("days", "1")) + except ValueError as e: + self.redirect( + f"/panel/error?error=Type error: Argument must be an int {e}" + ) + page_data["options"] = [1, 2, 3] + if not days in page_data["options"]: + page_data["options"].insert(0, days) + else: + page_data["options"].insert( + 0, page_data["options"].pop(page_data["options"].index(days)) + ) page_data["history_stats"] = self.controller.servers.get_history_stats( - server_id + server_id, days ) def get_banned_players_html(): @@ -1500,17 +1513,18 @@ class PanelHandler(BaseHandler): if Helpers.is_os_windows(): server_path.replace(" ", "^ ") server_path = Helpers.wtol_path(server_path) - log_path = self.get_argument("log_path", None) - if Helpers.is_os_windows(): - log_path.replace(" ", "^ ") - log_path = Helpers.wtol_path(log_path) - if not self.helper.validate_traversal(server_obj.path, log_path): - log_path = "" + log_path = self.get_argument("log_path", "") + if log_path: + if Helpers.is_os_windows(): + log_path.replace(" ", "^ ") + log_path = Helpers.wtol_path(log_path) + if not self.helper.validate_traversal(server_obj.path, log_path): + log_path = "" executable = self.get_argument("executable", None) execution_command = self.get_argument("execution_command", None) server_ip = self.get_argument("server_ip", None) server_port = self.get_argument("server_port", None) - executable_update_url = self.get_argument("executable_update_url", None) + executable_update_url = self.get_argument("executable_update_url", "") show_status = int(float(self.get_argument("show_status", "0"))) else: execution_command = server_obj.execution_command diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py index c13198ed..1c1d6fc6 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -97,6 +97,7 @@ class ServerHandler(BaseHandler): "version_data": self.helper.get_version_string(), "user_data": exec_user, "user_role": exec_user_role, + "online": Helpers.check_internet(), "roles": list_roles, "super_user": exec_user["superuser"], "user_crafty_permissions": exec_user_crafty_permissions, @@ -173,7 +174,6 @@ class ServerHandler(BaseHandler): ) return - page_data["online"] = Helpers.check_internet() page_data["server_types"] = self.controller.server_jars.get_serverjar_data() page_data["js_server_types"] = json.dumps( self.controller.server_jars.get_serverjar_data() @@ -548,25 +548,14 @@ class ServerHandler(BaseHandler): self.get_remote_ip(), ) else: - if len(server_parts) != 2: - self.redirect("/panel/error?error=Invalid server data") - return - server_type, server_version = server_parts - # TODO: add server type check here and call the correct server - # add functions if not a jar - new_server_id = self.controller.create_jar_server( - server_type, - server_version, + + new_server_id = self.controller.create_bedrock_server( server_name, - min_mem, - max_mem, - port, exec_user["user_id"], ) self.controller.management.add_to_audit_log( exec_user["user_id"], - f"created a {server_version} {str(server_type).capitalize()} " - f'server named "{server_name}"', + "created a Bedrock " f'server named "{server_name}"', # Example: Admin created a 1.16.5 Bukkit server named "survival" new_server_id, self.get_remote_ip(), diff --git a/app/frontend/templates/base.html b/app/frontend/templates/base.html index 1ab50da8..100ab16c 100755 --- a/app/frontend/templates/base.html +++ b/app/frontend/templates/base.html @@ -378,7 +378,7 @@ } bootbox.confirm({ title: "{% raw translate('error', 'eulaTitle', data['lang']) %}", - message: "{% raw translate('error', 'eulaMsg', data['lang']) %}

EULA

{% raw translate('error', 'eulaAgree', data['lang']) %}", + message: "{% raw translate('error', 'eulaMsg', data['lang']) %}Minecraft EULA", buttons: { confirm: { label: 'Yes', diff --git a/app/frontend/templates/panel/dashboard.html b/app/frontend/templates/panel/dashboard.html index 23ebf2c7..c89d00b2 100644 --- a/app/frontend/templates/panel/dashboard.html +++ b/app/frontend/templates/panel/dashboard.html @@ -186,7 +186,8 @@ {% elif server['stats']['updating']%} - {{ translate('serverTerm', 'updating', +  {{ translate('serverTerm', 'updating', data['lang']) }} {% elif server['stats']['waiting_start']%} diff --git a/app/frontend/templates/panel/server_config.html b/app/frontend/templates/panel/server_config.html index e1c38e1e..d6117683 100644 --- a/app/frontend/templates/panel/server_config.html +++ b/app/frontend/templates/panel/server_config.html @@ -67,7 +67,7 @@ placeholder="{{ translate('serverConfig', 'serverPath', data['lang']) }}" required> - + {% if data['server_stats']['server_type'] != "minecraft-bedrock" %}
+ {% end %}