From 1a6f06eaf2eebe461936608954f4465f42a67ae7 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 29 Mar 2025 23:00:06 +0100 Subject: [PATCH] refactor(moonraker): move setup functions into MoonrakerSetupService Signed-off-by: Dominik Willner --- .../klipper/services/klipper_setup_service.py | 2 + .../moonraker/menus/moonraker_remove_menu.py | 88 ++-- .../components/moonraker/moonraker_remove.py | 121 ------ kiauh/components/moonraker/moonraker_setup.py | 271 ------------ .../services/moonraker_setup_service.py | 407 ++++++++++++++++++ kiauh/components/moonraker/utils/utils.py | 47 +- kiauh/core/menus/install_menu.py | 5 +- kiauh/core/menus/update_menu.py | 5 +- kiauh/procedures/switch_repo.py | 4 +- 9 files changed, 499 insertions(+), 451 deletions(-) delete mode 100644 kiauh/components/moonraker/moonraker_remove.py delete mode 100644 kiauh/components/moonraker/moonraker_setup.py create mode 100644 kiauh/components/moonraker/services/moonraker_setup_service.py diff --git a/kiauh/components/klipper/services/klipper_setup_service.py b/kiauh/components/klipper/services/klipper_setup_service.py index d390b86..9c5a695 100644 --- a/kiauh/components/klipper/services/klipper_setup_service.py +++ b/kiauh/components/klipper/services/klipper_setup_service.py @@ -102,6 +102,8 @@ class KlipperSetupService: self.moonraker_list = self.misvc.get_all_instances() def install(self) -> None: + self.__refresh_state() + Logger.print_status("Installing Klipper ...") match_moonraker: bool = False diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 706b21c..6729c3e 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -11,8 +11,8 @@ from __future__ import annotations import textwrap from typing import Type -from components.moonraker import moonraker_remove -from core.menus import Option +from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService +from core.menus import FooterType, Option from core.menus.base_menu import BaseMenu from core.types.color import Color @@ -21,14 +21,19 @@ from core.types.color import Color class MoonrakerRemoveMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "Remove Moonraker" self.title_color = Color.RED self.previous_menu: Type[BaseMenu] | None = previous_menu - self.remove_moonraker_service = False - self.remove_moonraker_dir = False - self.remove_moonraker_env = False - self.remove_moonraker_polkit = False - self.selection_state = False + self.footer_type = FooterType.BACK + + self.rm_svc = False + self.rm_dir = False + self.rm_env = False + self.rm_pk = False + self.select_state = False + + self.mrsvc = MoonrakerSetupService() def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: from core.menus.remove_menu import RemoveMenu @@ -48,17 +53,18 @@ class MoonrakerRemoveMenu(BaseMenu): def print_menu(self) -> None: checked = f"[{Color.apply('x', Color.CYAN)}]" unchecked = "[ ]" - o1 = checked if self.remove_moonraker_service else unchecked - o2 = checked if self.remove_moonraker_dir else unchecked - o3 = checked if self.remove_moonraker_env else unchecked - o4 = checked if self.remove_moonraker_polkit else unchecked + o1 = checked if self.rm_svc else unchecked + o2 = checked if self.rm_dir else unchecked + o3 = checked if self.rm_env else unchecked + o4 = checked if self.rm_pk else unchecked + sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything" menu = textwrap.dedent( f""" ╟───────────────────────────────────────────────────────╢ ║ Enter a number and hit enter to select / deselect ║ ║ the specific option for removal. ║ ╟───────────────────────────────────────────────────────╢ - ║ a) {self._get_selection_state_str():37} ║ + ║ a) {sel_state:49} ║ ╟───────────────────────────────────────────────────────╢ ║ 1) {o1} Remove Service ║ ║ 2) {o2} Remove Local Repository ║ @@ -72,57 +78,33 @@ class MoonrakerRemoveMenu(BaseMenu): print(menu, end="") def toggle_all(self, **kwargs) -> None: - self.selection_state = not self.selection_state - self.remove_moonraker_service = self.selection_state - self.remove_moonraker_dir = self.selection_state - self.remove_moonraker_env = self.selection_state - self.remove_moonraker_polkit = self.selection_state + self.select_state = not self.select_state + self.rm_svc = self.select_state + self.rm_dir = self.select_state + self.rm_env = self.select_state + self.rm_pk = self.select_state def toggle_remove_moonraker_service(self, **kwargs) -> None: - self.remove_moonraker_service = not self.remove_moonraker_service + self.rm_svc = not self.rm_svc def toggle_remove_moonraker_dir(self, **kwargs) -> None: - self.remove_moonraker_dir = not self.remove_moonraker_dir + self.rm_dir = not self.rm_dir def toggle_remove_moonraker_env(self, **kwargs) -> None: - self.remove_moonraker_env = not self.remove_moonraker_env + self.rm_env = not self.rm_env def toggle_remove_moonraker_polkit(self, **kwargs) -> None: - self.remove_moonraker_polkit = not self.remove_moonraker_polkit + self.rm_pk = not self.rm_pk def run_removal_process(self, **kwargs) -> None: - if ( - not self.remove_moonraker_service - and not self.remove_moonraker_dir - and not self.remove_moonraker_env - and not self.remove_moonraker_polkit - ): - print( - Color.apply( - "Nothing selected! Select options to remove first.", Color.RED - ) - ) + if not self.rm_svc and not self.rm_dir and not self.rm_env and not self.rm_pk: + msg = "Nothing selected! Select options to remove first." + print(Color.apply(msg, Color.RED)) return - moonraker_remove.run_moonraker_removal( - self.remove_moonraker_service, - self.remove_moonraker_dir, - self.remove_moonraker_env, - self.remove_moonraker_polkit, - ) + self.mrsvc.remove(self.rm_svc, self.rm_dir, self.rm_env, self.rm_pk) - self.remove_moonraker_service = False - self.remove_moonraker_dir = False - self.remove_moonraker_env = False - self.remove_moonraker_polkit = False - - self._go_back() - - def _get_selection_state_str(self) -> str: - return ( - "Select everything" if not self.selection_state else "Deselect everything" - ) - - def _go_back(self, **kwargs) -> None: - if self.previous_menu is not None: - self.previous_menu().run() + self.rm_svc = False + self.rm_dir = False + self.rm_env = False + self.rm_pk = False diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py deleted file mode 100644 index 8ed6dd1..0000000 --- a/kiauh/components/moonraker/moonraker_remove.py +++ /dev/null @@ -1,121 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from subprocess import DEVNULL, PIPE, CalledProcessError, run -from typing import List - -from components.klipper.klipper_dialogs import print_instance_overview -from components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR -from components.moonraker.moonraker import Moonraker -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from utils.fs_utils import run_remove_routines -from utils.input_utils import get_selection_input -from utils.instance_utils import get_instances -from utils.sys_utils import unit_file_exists - - -def run_moonraker_removal( - remove_service: bool, - remove_dir: bool, - remove_env: bool, - remove_polkit: bool, -) -> None: - instances = get_instances(Moonraker) - - if remove_service: - Logger.print_status("Removing Moonraker instances ...") - if instances: - instances_to_remove = select_instances_to_remove(instances) - remove_instances(instances_to_remove) - else: - Logger.print_info("No Moonraker Services installed! Skipped ...") - - delete_remaining: bool = remove_polkit or remove_dir or remove_env - if delete_remaining and unit_file_exists("moonraker", suffix="service"): - Logger.print_info("There are still other Moonraker services installed") - Logger.print_info( - "● Moonraker PolicyKit rules were not removed.", prefix=False - ) - Logger.print_info(f"● '{MOONRAKER_DIR}' was not removed.", prefix=False) - Logger.print_info(f"● '{MOONRAKER_ENV_DIR}' was not removed.", prefix=False) - else: - if remove_polkit: - Logger.print_status("Removing all Moonraker policykit rules ...") - remove_polkit_rules() - if remove_dir: - Logger.print_status("Removing Moonraker local repository ...") - run_remove_routines(MOONRAKER_DIR) - if remove_env: - Logger.print_status("Removing Moonraker Python environment ...") - run_remove_routines(MOONRAKER_ENV_DIR) - - -def select_instances_to_remove( - instances: List[Moonraker], -) -> List[Moonraker] | None: - start_index = 1 - options = [str(i + start_index) for i in range(len(instances))] - options.extend(["a", "b"]) - instance_map = {options[i]: instances[i] for i in range(len(instances))} - - print_instance_overview( - instances, - start_index=start_index, - show_index=True, - show_select_all=True, - ) - selection = get_selection_input("Select Moonraker instance to remove", options) - - instances_to_remove = [] - if selection == "b": - return None - elif selection == "a": - instances_to_remove.extend(instances) - else: - instances_to_remove.append(instance_map[selection]) - - return instances_to_remove - - -def remove_instances( - instance_list: List[Moonraker] | None, -) -> None: - if not instance_list: - Logger.print_info("No Moonraker instances found. Skipped ...") - return - for instance in instance_list: - Logger.print_status(f"Removing instance {instance.service_file_path.stem} ...") - InstanceManager.remove(instance) - delete_moonraker_env_file(instance) - - -def remove_polkit_rules() -> None: - if not MOONRAKER_DIR.exists(): - log = "Cannot remove policykit rules. Moonraker directory not found." - Logger.print_warn(log) - return - - try: - cmd = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"] - run(cmd, stderr=PIPE, stdout=DEVNULL, check=True) - except CalledProcessError as e: - Logger.print_error(f"Error while removing policykit rules: {e}") - - Logger.print_ok("Policykit rules successfully removed!") - - -def delete_moonraker_env_file(instance: Moonraker): - Logger.print_status(f"Remove '{instance.env_file}'") - if not instance.env_file.exists(): - msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..." - Logger.print_info(msg) - return - run_remove_routines(instance.env_file) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py deleted file mode 100644 index 8752486..0000000 --- a/kiauh/components/moonraker/moonraker_setup.py +++ /dev/null @@ -1,271 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import subprocess -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker import ( - EXIT_MOONRAKER_SETUP, - MOONRAKER_DEPS_JSON_FILE, - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, - MOONRAKER_INSTALL_SCRIPT, - MOONRAKER_REPO_URL, - MOONRAKER_REQ_FILE, - MOONRAKER_SPEEDUPS_REQ_FILE, - POLKIT_FILE, - POLKIT_LEGACY_FILE, - POLKIT_SCRIPT, - POLKIT_USR_FILE, -) -from components.moonraker.moonraker import Moonraker -from components.moonraker.moonraker_dialogs import print_moonraker_overview -from components.moonraker.services.moonraker_instance_service import ( - MoonrakerInstanceService, -) -from components.moonraker.utils.sysdeps_parser import SysDepsParser -from components.moonraker.utils.utils import ( - backup_moonraker_dir, - create_example_moonraker_conf, - load_sysdeps_json, -) -from components.webui_client.client_utils import ( - enable_mainsail_remotemode, - get_existing_clients, -) -from components.webui_client.mainsail_data import MainsailData -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color -from utils.common import check_install_dependencies -from utils.fs_utils import check_file_exist -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import ( - get_confirm, - get_selection_input, -) -from utils.instance_utils import get_instances -from utils.sys_utils import ( - check_python_version, - cmd_sysctl_manage, - cmd_sysctl_service, - create_python_venv, - get_ipv4_addr, - install_python_requirements, - parse_packages_from_file, -) - - -def install_moonraker() -> None: - klipper_list: List[Klipper] = get_instances(Klipper) - - if not check_moonraker_install_requirements(klipper_list): - return - - instance_service = MoonrakerInstanceService() - instance_service.load_instances() - - moonraker_list: List[Moonraker] = instance_service.get_all_instances() - new_instances: List[Moonraker] = [] - selected_option: str | Klipper - - if len(klipper_list) == 1: - suffix: str = klipper_list[0].suffix - new_inst = instance_service.create_new_instance(suffix) - new_instances.append(new_inst) - - else: - print_moonraker_overview( - klipper_list, - moonraker_list, - show_index=True, - show_select_all=True, - ) - options = {str(i + 1): k for i, k in enumerate(klipper_list)} - additional_options = {"a": None, "b": None} - options = {**options, **additional_options} - question = "Select Klipper instance to setup Moonraker for" - selected_option = get_selection_input(question, options) - - if selected_option == "b": - Logger.print_status(EXIT_MOONRAKER_SETUP) - return - - if selected_option == "a": - new_inst_list: List[Moonraker] = [ - instance_service.create_new_instance(k.suffix) for k in klipper_list - ] - new_instances.extend(new_inst_list) - else: - klipper_instance: Klipper | None = options.get(selected_option) - if klipper_instance is None: - raise Exception("Error selecting instance!") - new_inst = instance_service.create_new_instance(klipper_instance.suffix) - new_instances.append(new_inst) - - create_example_cfg = get_confirm("Create example moonraker.conf?") - - try: - check_install_dependencies() - setup_moonraker_prerequesites() - install_moonraker_polkit() - - ports_map = instance_service.get_instance_port_map() - for instance in new_instances: - instance.create() - cmd_sysctl_service(instance.service_file_path.name, "enable") - - if create_example_cfg: - # if a webclient and/or it's config is installed, patch - # its update section to the config - clients = get_existing_clients() - create_example_moonraker_conf(instance, ports_map, clients) - - cmd_sysctl_service(instance.service_file_path.name, "start") - - cmd_sysctl_manage("daemon-reload") - - # if mainsail is installed, and we installed - # multiple moonraker instances, we enable mainsails remote mode - if MainsailData().client_dir.exists() and len(moonraker_list) > 1: - enable_mainsail_remotemode() - - instance_service.load_instances() - new_instances = [ - instance_service.get_instance_by_suffix(i.suffix) for i in new_instances - ] - - ip: str = get_ipv4_addr() - # noinspection HttpUrlsUsage - url_list = [ - f"● {i.service_file_path.stem}: http://{ip}:{i.port}" - for i in new_instances - if i.port - ] - dialog_content = [] - if url_list: - dialog_content.append("You can access Moonraker via the following URL:") - dialog_content.extend(url_list) - - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Moonraker successfully installed!", - custom_color=Color.GREEN, - content=dialog_content, - ) - - except Exception as e: - Logger.print_error(f"Error while installing Moonraker: {e}") - return - - -def check_moonraker_install_requirements(klipper_list: List[Klipper]) -> bool: - def check_klipper_instances() -> bool: - if len(klipper_list) >= 1: - return True - - Logger.print_warn("Klipper not installed!") - Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") - return False - - return check_python_version(3, 7) and check_klipper_instances() - - -def setup_moonraker_prerequesites() -> None: - settings = KiauhSettings() - default_repo = (MOONRAKER_REPO_URL, "master") - repo = settings.moonraker.repositories - # pull the first repo defined in kiauh.cfg or fallback to the official Moonraker repo - repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo - git_clone_wrapper(repo, MOONRAKER_DIR, branch) - - # install moonraker dependencies and create python virtualenv - install_moonraker_packages() - if create_python_venv(MOONRAKER_ENV_DIR): - install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) - install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_SPEEDUPS_REQ_FILE) - - -def install_moonraker_packages() -> None: - Logger.print_status("Parsing Moonraker system dependencies ...") - - moonraker_deps = [] - if MOONRAKER_DEPS_JSON_FILE.exists(): - Logger.print_info( - f"Parsing system dependencies from {MOONRAKER_DEPS_JSON_FILE.name} ..." - ) - parser = SysDepsParser() - sysdeps = load_sysdeps_json(MOONRAKER_DEPS_JSON_FILE) - moonraker_deps.extend(parser.parse_dependencies(sysdeps)) - - elif MOONRAKER_INSTALL_SCRIPT.exists(): - Logger.print_warn(f"{MOONRAKER_DEPS_JSON_FILE.name} not found!") - Logger.print_info( - f"Parsing system dependencies from {MOONRAKER_INSTALL_SCRIPT.name} ..." - ) - moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT) - - if not moonraker_deps: - raise ValueError("Error parsing Moonraker dependencies!") - - check_install_dependencies({*moonraker_deps}) - - -def install_moonraker_polkit() -> None: - Logger.print_status("Installing Moonraker policykit rules ...") - - legacy_file_exists = check_file_exist(POLKIT_LEGACY_FILE, True) - polkit_file_exists = check_file_exist(POLKIT_FILE, True) - usr_file_exists = check_file_exist(POLKIT_USR_FILE, True) - - if legacy_file_exists or (polkit_file_exists and usr_file_exists): - Logger.print_info("Moonraker policykit rules are already installed.") - return - - try: - command = [POLKIT_SCRIPT, "--disable-systemctl"] - result = subprocess.run( - command, - stderr=subprocess.PIPE, - stdout=subprocess.DEVNULL, - text=True, - ) - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", False) - Logger.print_error("Installing Moonraker policykit rules failed!") - return - - Logger.print_ok("Moonraker policykit rules successfully installed!") - except subprocess.CalledProcessError as e: - log = f"Error while installing Moonraker policykit rules: {e.stderr.decode()}" - Logger.print_error(log) - - -def update_moonraker() -> None: - if not get_confirm("Update Moonraker now?"): - return - - settings = KiauhSettings() - if settings.kiauh.backup_before_update: - backup_moonraker_dir() - - instances = get_instances(Moonraker) - InstanceManager.stop_all(instances) - - git_pull_wrapper(MOONRAKER_DIR) - - # install possible new system packages - install_moonraker_packages() - # install possible new python dependencies - install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) - - InstanceManager.start_all(instances) diff --git a/kiauh/components/moonraker/services/moonraker_setup_service.py b/kiauh/components/moonraker/services/moonraker_setup_service.py new file mode 100644 index 0000000..7f6e6b3 --- /dev/null +++ b/kiauh/components/moonraker/services/moonraker_setup_service.py @@ -0,0 +1,407 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2025 Dominik Willner # +# # +# This file is part of KIAUH - Klipper Installation And Update Helper # +# https://github.com/dw-0/kiauh # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # +from __future__ import annotations + +from copy import copy +from subprocess import DEVNULL, PIPE, CalledProcessError, run +from typing import List + +from components.klipper.klipper import Klipper +from components.klipper.klipper_dialogs import print_instance_overview +from components.klipper.services.klipper_instance_service import KlipperInstanceService +from components.moonraker import ( + EXIT_MOONRAKER_SETUP, + MOONRAKER_DIR, + MOONRAKER_ENV_DIR, + MOONRAKER_REPO_URL, + MOONRAKER_REQ_FILE, + MOONRAKER_SPEEDUPS_REQ_FILE, + POLKIT_FILE, + POLKIT_LEGACY_FILE, + POLKIT_SCRIPT, + POLKIT_USR_FILE, +) +from components.moonraker.moonraker import Moonraker +from components.moonraker.moonraker_dialogs import print_moonraker_overview +from components.moonraker.services.moonraker_instance_service import ( + MoonrakerInstanceService, +) +from components.moonraker.utils.utils import ( + backup_moonraker_dir, + create_example_moonraker_conf, + install_moonraker_packages, + remove_polkit_rules, +) +from components.webui_client.client_utils import ( + enable_mainsail_remotemode, + get_existing_clients, +) +from components.webui_client.mainsail_data import MainsailData +from core.instance_manager.instance_manager import InstanceManager +from core.logger import DialogType, Logger +from core.services.message_service import Message, MessageService +from core.settings.kiauh_settings import KiauhSettings +from core.types.color import Color +from utils.common import check_install_dependencies +from utils.fs_utils import check_file_exist, run_remove_routines +from utils.git_utils import git_clone_wrapper, git_pull_wrapper +from utils.input_utils import ( + get_confirm, + get_selection_input, +) +from utils.sys_utils import ( + check_python_version, + cmd_sysctl_manage, + cmd_sysctl_service, + create_python_venv, + get_ipv4_addr, + install_python_requirements, + unit_file_exists, +) + + +# noinspection PyMethodMayBeStatic +class MoonrakerSetupService: + __cls_instance = None + + kisvc: KlipperInstanceService + misvc: MoonrakerInstanceService + msgsvc = MessageService + + settings: KiauhSettings + klipper_list: List[Klipper] + moonraker_list: List[Moonraker] + + def __new__(cls) -> "MoonrakerSetupService": + if cls.__cls_instance is None: + cls.__cls_instance = super(MoonrakerSetupService, cls).__new__(cls) + return cls.__cls_instance + + def __init__(self) -> None: + if not hasattr(self, "__initialized"): + self.__initialized = False + if self.__initialized: + return + self.__initialized = True + self.__init_state() + + def __init_state(self) -> None: + self.settings = KiauhSettings() + + self.kisvc = KlipperInstanceService() + self.kisvc.load_instances() + self.klipper_list = self.kisvc.get_all_instances() + + self.misvc = MoonrakerInstanceService() + self.misvc.load_instances() + self.moonraker_list = self.misvc.get_all_instances() + + self.msgsvc = MessageService() + + def __refresh_state(self) -> None: + self.kisvc.load_instances() + self.klipper_list = self.kisvc.get_all_instances() + + self.misvc.load_instances() + self.moonraker_list = self.misvc.get_all_instances() + + def install(self) -> None: + self.__refresh_state() + + if not self.__check_requirements(self.klipper_list): + return + + new_instances: List[Moonraker] = [] + selected_option: str | Klipper + + if len(self.klipper_list) == 1: + suffix: str = self.klipper_list[0].suffix + new_inst = self.misvc.create_new_instance(suffix) + new_instances.append(new_inst) + + else: + print_moonraker_overview( + self.klipper_list, + self.moonraker_list, + show_index=True, + show_select_all=True, + ) + options = {str(i + 1): k for i, k in enumerate(self.klipper_list)} + additional_options = {"a": None, "b": None} + options = {**options, **additional_options} + question = "Select Klipper instance to setup Moonraker for" + selected_option = get_selection_input(question, options) + + if selected_option == "b": + Logger.print_status(EXIT_MOONRAKER_SETUP) + return + + if selected_option == "a": + new_inst_list: List[Moonraker] = [ + self.misvc.create_new_instance(k.suffix) for k in self.klipper_list + ] + new_instances.extend(new_inst_list) + else: + klipper_instance: Klipper | None = options.get(selected_option) + if klipper_instance is None: + raise Exception("Error selecting instance!") + new_inst = self.misvc.create_new_instance(klipper_instance.suffix) + new_instances.append(new_inst) + + create_example_cfg = get_confirm("Create example moonraker.conf?") + + try: + self.__run_setup(new_instances, create_example_cfg) + except Exception as e: + Logger.print_error(f"Error while installing Moonraker: {e}") + return + + def update(self) -> None: + Logger.print_dialog( + DialogType.WARNING, + [ + "Be careful if there are ongoing prints running!", + "All Moonraker instances will be restarted during the update process and " + "ongoing prints COULD FAIL.", + ], + ) + + if not get_confirm("Update Moonraker now?"): + return + + self.__refresh_state() + + if self.settings.kiauh.backup_before_update: + backup_moonraker_dir() + + InstanceManager.stop_all(self.moonraker_list) + git_pull_wrapper(MOONRAKER_DIR) + install_moonraker_packages() + install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) + InstanceManager.start_all(self.moonraker_list) + + def remove( + self, + remove_service: bool, + remove_dir: bool, + remove_env: bool, + remove_polkit: bool, + ) -> None: + self.__refresh_state() + + completion_msg = Message( + title="Moonraker Removal Process completed", + color=Color.GREEN, + ) + + if remove_service: + Logger.print_status("Removing Moonraker instances ...") + if self.moonraker_list: + instances_to_remove = self.__get_instances_to_remove() + self.__remove_instances(instances_to_remove) + if instances_to_remove: + instance_names = [ + i.service_file_path.stem for i in instances_to_remove + ] + txt = f"● Moonraker instances removed: {', '.join(instance_names)}" + completion_msg.text.append(txt) + else: + Logger.print_info("No Moonraker Services installed! Skipped ...") + + if (remove_polkit or remove_dir or remove_env) and unit_file_exists( + "moonraker", suffix="service" + ): + completion_msg.text = [ + "Some Klipper services are still installed:", + "● Moonraker PolicyKit rules were not removed, even though selected for removal.", + f"● '{MOONRAKER_DIR}' was not removed, even though selected for removal.", + f"● '{MOONRAKER_ENV_DIR}' was not removed, even though selected for removal.", + ] + else: + if remove_polkit: + Logger.print_status("Removing all Moonraker policykit rules ...") + if remove_polkit_rules(): + completion_msg.text.append("● Moonraker policykit rules removed") + if remove_dir: + Logger.print_status("Removing Moonraker local repository ...") + if run_remove_routines(MOONRAKER_DIR): + completion_msg.text.append("● Moonraker local repository removed") + if remove_env: + Logger.print_status("Removing Moonraker Python environment ...") + if run_remove_routines(MOONRAKER_ENV_DIR): + completion_msg.text.append("● Moonraker Python environment removed") + + if completion_msg.text: + completion_msg.text.insert(0, "The following actions were performed:") + else: + completion_msg.color = Color.YELLOW + completion_msg.centered = True + completion_msg.text = ["Nothing to remove."] + + self.msgsvc.set_message(completion_msg) + + def __run_setup( + self, new_instances: List[Moonraker], create_example_cfg: bool + ) -> None: + check_install_dependencies() + self.__install_deps() + + ports_map = self.misvc.get_instance_port_map() + for i in new_instances: + i.create() + cmd_sysctl_service(i.service_file_path.name, "enable") + + if create_example_cfg: + # if a webclient and/or it's config is installed, patch + # its update section to the config + clients = get_existing_clients() + create_example_moonraker_conf(i, ports_map, clients) + + cmd_sysctl_service(i.service_file_path.name, "start") + + cmd_sysctl_manage("daemon-reload") + + # if mainsail is installed, and we installed + # multiple moonraker instances, we enable mainsails remote mode + if MainsailData().client_dir.exists() and len(self.moonraker_list) > 1: + enable_mainsail_remotemode() + + self.misvc.load_instances() + new_instances = [ + self.misvc.get_instance_by_suffix(i.suffix) for i in new_instances + ] + + ip: str = get_ipv4_addr() + # noinspection HttpUrlsUsage + url_list = [ + f"● {i.service_file_path.stem}: http://{ip}:{i.port}" + for i in new_instances + if i.port + ] + dialog_content = [] + if url_list: + dialog_content.append("You can access Moonraker via the following URL:") + dialog_content.extend(url_list) + + Logger.print_dialog( + DialogType.CUSTOM, + custom_title="Moonraker successfully installed!", + custom_color=Color.GREEN, + content=dialog_content, + ) + + def __check_requirements(self, klipper_list: List[Klipper]) -> bool: + is_klipper_installed = len(klipper_list) >= 1 + if not is_klipper_installed: + Logger.print_warn("Klipper not installed!") + Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") + + is_python_ok = check_python_version(3, 7) + + return is_klipper_installed and is_python_ok + + def __install_deps(self) -> None: + default_repo = (MOONRAKER_REPO_URL, "master") + repo = self.settings.moonraker.repositories + # pull the first repo defined in kiauh.cfg or fallback to the official Moonraker repo + repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo + git_clone_wrapper(repo, MOONRAKER_DIR, branch) + + try: + install_moonraker_packages() + if create_python_venv(MOONRAKER_ENV_DIR): + install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) + install_python_requirements( + MOONRAKER_ENV_DIR, MOONRAKER_SPEEDUPS_REQ_FILE + ) + self.__install_polkit() + except Exception: + Logger.print_error("Error during installation of Moonraker requirements!") + raise + + def __install_polkit(self) -> None: + Logger.print_status("Installing Moonraker policykit rules ...") + + legacy_file_exists = check_file_exist(POLKIT_LEGACY_FILE, True) + polkit_file_exists = check_file_exist(POLKIT_FILE, True) + usr_file_exists = check_file_exist(POLKIT_USR_FILE, True) + + if legacy_file_exists or (polkit_file_exists and usr_file_exists): + Logger.print_info("Moonraker policykit rules are already installed.") + return + + try: + command = [POLKIT_SCRIPT, "--disable-systemctl"] + result = run( + command, + stderr=PIPE, + stdout=DEVNULL, + text=True, + ) + if result.returncode != 0 or result.stderr: + Logger.print_error(f"{result.stderr}", False) + Logger.print_error("Installing Moonraker policykit rules failed!") + return + + Logger.print_ok("Moonraker policykit rules successfully installed!") + except CalledProcessError as e: + log = ( + f"Error while installing Moonraker policykit rules: {e.stderr.decode()}" + ) + Logger.print_error(log) + + def __get_instances_to_remove(self) -> List[Moonraker] | None: + start_index = 1 + curr_instances: List[Moonraker] = self.moonraker_list + instance_count = len(curr_instances) + + options = [str(i + start_index) for i in range(instance_count)] + options.extend(["a", "b"]) + instance_map = { + options[i]: self.moonraker_list[i] for i in range(instance_count) + } + + print_instance_overview( + self.moonraker_list, + start_index=start_index, + show_index=True, + show_select_all=True, + ) + selection = get_selection_input("Select Moonraker instance to remove", options) + + if selection == "b": + return None + elif selection == "a": + return copy(self.moonraker_list) + + return [instance_map[selection]] + + def __remove_instances( + self, + instance_list: List[Moonraker] | None, + ) -> None: + if not instance_list: + return + + for instance in instance_list: + Logger.print_status( + f"Removing instance {instance.service_file_path.stem} ..." + ) + InstanceManager.remove(instance) + self.__delete_env_file(instance) + + self.__refresh_state() + + def __delete_env_file(self, instance: Moonraker): + Logger.print_status(f"Remove '{instance.env_file}'") + if not instance.env_file.exists(): + msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..." + Logger.print_info(msg) + return + run_remove_routines(instance.env_file) diff --git a/kiauh/components/moonraker/utils/utils.py b/kiauh/components/moonraker/utils/utils.py index d5d9149..e358367 100644 --- a/kiauh/components/moonraker/utils/utils.py +++ b/kiauh/components/moonraker/utils/utils.py @@ -9,6 +9,7 @@ import json import shutil from pathlib import Path +from subprocess import DEVNULL, PIPE, CalledProcessError, run from typing import Dict, List, Optional from components.moonraker import ( @@ -16,10 +17,13 @@ from components.moonraker import ( MOONRAKER_BACKUP_DIR, MOONRAKER_DB_BACKUP_DIR, MOONRAKER_DEFAULT_PORT, + MOONRAKER_DEPS_JSON_FILE, MOONRAKER_DIR, MOONRAKER_ENV_DIR, + MOONRAKER_INSTALL_SCRIPT, ) from components.moonraker.moonraker import Moonraker +from components.moonraker.utils.sysdeps_parser import SysDepsParser from components.webui_client.base_data import BaseWebClient from core.backup_manager.backup_manager import BackupManager from core.logger import Logger @@ -27,10 +31,11 @@ from core.submodules.simple_config_parser.src.simple_config_parser.simple_config SimpleConfigParser, ) from core.types.component_status import ComponentStatus -from utils.common import get_install_status +from utils.common import check_install_dependencies, get_install_status from utils.instance_utils import get_instances from utils.sys_utils import ( get_ipv4_addr, + parse_packages_from_file, ) @@ -38,6 +43,46 @@ def get_moonraker_status() -> ComponentStatus: return get_install_status(MOONRAKER_DIR, MOONRAKER_ENV_DIR, Moonraker) +def install_moonraker_packages() -> None: + Logger.print_status("Parsing Moonraker system dependencies ...") + + moonraker_deps = [] + if MOONRAKER_DEPS_JSON_FILE.exists(): + Logger.print_info( + f"Parsing system dependencies from {MOONRAKER_DEPS_JSON_FILE.name} ..." + ) + parser = SysDepsParser() + sysdeps = load_sysdeps_json(MOONRAKER_DEPS_JSON_FILE) + moonraker_deps.extend(parser.parse_dependencies(sysdeps)) + + elif MOONRAKER_INSTALL_SCRIPT.exists(): + Logger.print_warn(f"{MOONRAKER_DEPS_JSON_FILE.name} not found!") + Logger.print_info( + f"Parsing system dependencies from {MOONRAKER_INSTALL_SCRIPT.name} ..." + ) + moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT) + + if not moonraker_deps: + raise ValueError("Error parsing Moonraker dependencies!") + + check_install_dependencies({*moonraker_deps}) + + +def remove_polkit_rules() -> bool: + if not MOONRAKER_DIR.exists(): + log = "Cannot remove policykit rules. Moonraker directory not found." + Logger.print_warn(log) + return False + + try: + cmd = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"] + run(cmd, stderr=PIPE, stdout=DEVNULL, check=True) + return True + except CalledProcessError as e: + Logger.print_error(f"Error while removing policykit rules: {e}") + return False + + def create_example_moonraker_conf( instance: Moonraker, ports_map: Dict[str, int], diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 831a355..4fccfb9 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -14,7 +14,7 @@ from typing import Type from components.crowsnest.crowsnest import install_crowsnest from components.klipper.services.klipper_setup_service import KlipperSetupService from components.klipperscreen.klipperscreen import install_klipperscreen -from components.moonraker import moonraker_setup +from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService from components.webui_client.client_config.client_config_setup import ( install_client_config, ) @@ -37,6 +37,7 @@ class InstallMenu(BaseMenu): self.title_color = Color.GREEN self.previous_menu: Type[BaseMenu] | None = previous_menu self.klsvc = KlipperSetupService() + self.mrsvc = MoonrakerSetupService() def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: from core.menus.main_menu import MainMenu @@ -79,7 +80,7 @@ class InstallMenu(BaseMenu): self.klsvc.install() def install_moonraker(self, **kwargs) -> None: - moonraker_setup.install_moonraker() + self.mrsvc.install() def install_mainsail(self, **kwargs) -> None: client: MainsailData = MainsailData() diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 5b6c77c..d1050f3 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -20,7 +20,7 @@ from components.klipperscreen.klipperscreen import ( get_klipperscreen_status, update_klipperscreen, ) -from components.moonraker.moonraker_setup import update_moonraker +from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService from components.moonraker.utils.utils import get_moonraker_status from components.webui_client.client_config.client_config_setup import ( update_client_config, @@ -197,7 +197,8 @@ class UpdateMenu(BaseMenu): self._run_update_routine("klipper", klsvc.update) def update_moonraker(self, **kwargs) -> None: - self._run_update_routine("moonraker", update_moonraker) + mrsvc = MoonrakerSetupService() + self._run_update_routine("moonraker", mrsvc.update) def update_mainsail(self, **kwargs) -> None: self._run_update_routine( diff --git a/kiauh/procedures/switch_repo.py b/kiauh/procedures/switch_repo.py index 24796da..920e440 100644 --- a/kiauh/procedures/switch_repo.py +++ b/kiauh/procedures/switch_repo.py @@ -27,7 +27,9 @@ from components.moonraker import ( MOONRAKER_REQ_FILE, ) from components.moonraker.moonraker import Moonraker -from components.moonraker.moonraker_setup import install_moonraker_packages +from components.moonraker.services.moonraker_setup_service import ( + install_moonraker_packages, +) from core.backup_manager.backup_manager import BackupManager, BackupManagerException from core.instance_manager.instance_manager import InstanceManager from core.logger import Logger