From ce0daa52ae3fd2e66345ec76f64acb5b09010bd8 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 26 Oct 2023 13:44:43 +0200 Subject: [PATCH 001/296] feat(klipper): implement instance manager and klipper installer in python Signed-off-by: Dominik Willner --- .gitignore | 5 + kiauh.py | 15 ++ kiauh.sh | 158 ++++++++------ kiauh/__init__.py | 0 kiauh/instance_manager/__init__.py | 0 kiauh/instance_manager/base_instance.py | 85 ++++++++ kiauh/instance_manager/instance_manager.py | 151 +++++++++++++ kiauh/main.py | 20 ++ kiauh/menus/__init__.py | 0 kiauh/menus/advanced_menu.py | 40 ++++ kiauh/menus/base_menu.py | 177 ++++++++++++++++ kiauh/menus/install_menu.py | 93 ++++++++ kiauh/menus/main_menu.py | 65 ++++++ kiauh/menus/remove_menu.py | 109 ++++++++++ kiauh/menus/settings_menu.py | 36 ++++ kiauh/menus/update_menu.py | 108 ++++++++++ kiauh/modules/__init__.py | 0 kiauh/modules/klipper/__init__.py | 0 kiauh/modules/klipper/klipper.py | 164 ++++++++++++++ kiauh/modules/klipper/klipper_setup.py | 236 +++++++++++++++++++++ kiauh/modules/klipper/klipper_utils.py | 39 ++++ kiauh/modules/klipper/res/klipper.env | 1 + kiauh/modules/klipper/res/klipper.service | 18 ++ kiauh/utils/__init__.py | 0 kiauh/utils/constants.py | 29 +++ kiauh/utils/input_utils.py | 87 ++++++++ kiauh/utils/logger.py | 51 +++++ kiauh/utils/system_utils.py | 203 ++++++++++++++++++ 28 files changed, 1821 insertions(+), 69 deletions(-) create mode 100644 kiauh.py create mode 100644 kiauh/__init__.py create mode 100644 kiauh/instance_manager/__init__.py create mode 100644 kiauh/instance_manager/base_instance.py create mode 100644 kiauh/instance_manager/instance_manager.py create mode 100644 kiauh/main.py create mode 100644 kiauh/menus/__init__.py create mode 100644 kiauh/menus/advanced_menu.py create mode 100644 kiauh/menus/base_menu.py create mode 100644 kiauh/menus/install_menu.py create mode 100644 kiauh/menus/main_menu.py create mode 100644 kiauh/menus/remove_menu.py create mode 100644 kiauh/menus/settings_menu.py create mode 100644 kiauh/menus/update_menu.py create mode 100644 kiauh/modules/__init__.py create mode 100644 kiauh/modules/klipper/__init__.py create mode 100644 kiauh/modules/klipper/klipper.py create mode 100644 kiauh/modules/klipper/klipper_setup.py create mode 100644 kiauh/modules/klipper/klipper_utils.py create mode 100644 kiauh/modules/klipper/res/klipper.env create mode 100644 kiauh/modules/klipper/res/klipper.service create mode 100644 kiauh/utils/__init__.py create mode 100644 kiauh/utils/constants.py create mode 100644 kiauh/utils/input_utils.py create mode 100644 kiauh/utils/logger.py create mode 100644 kiauh/utils/system_utils.py diff --git a/.gitignore b/.gitignore index 560b8c5..83a9457 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ .vscode +.idea +.pytest_cache +.kiauh-env *.code-workspace klipper_repos.txt +klipper_repos.json + diff --git a/kiauh.py b/kiauh.py new file mode 100644 index 0000000..ec12ca5 --- /dev/null +++ b/kiauh.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 kiauh.main import main + +if __name__ == "__main__": + main() diff --git a/kiauh.sh b/kiauh.sh index aac5a8e..9b2d782 100755 --- a/kiauh.sh +++ b/kiauh.sh @@ -12,77 +12,97 @@ set -e clear -### sourcing all additional scripts -KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")" -for script in "${KIAUH_SRCDIR}/scripts/"*.sh; do . "${script}"; done -for script in "${KIAUH_SRCDIR}/scripts/ui/"*.sh; do . "${script}"; done +function main() { + local python_command + local entrypoint -#===================================================# -#=================== UPDATE KIAUH ==================# -#===================================================# - -function update_kiauh() { - status_msg "Updating KIAUH ..." - - cd "${KIAUH_SRCDIR}" - git reset --hard && git pull - - ok_msg "Update complete! Please restart KIAUH." - exit 0 -} - -#===================================================# -#=================== KIAUH STATUS ==================# -#===================================================# - -function kiauh_update_avail() { - [[ ! -d "${KIAUH_SRCDIR}/.git" ]] && return - local origin head - - cd "${KIAUH_SRCDIR}" - - ### abort if not on master branch - ! git branch -a | grep -q "\* master" && return - - ### compare commit hash - git fetch -q - origin=$(git rev-parse --short=8 origin/master) - head=$(git rev-parse --short=8 HEAD) - - if [[ ${origin} != "${head}" ]]; then - echo "true" + if command -v python3 &>/dev/null; then + python_command="python3" + elif command -v python &>/dev/null; then + python_command="python" + else + echo "Python is not installed. Please install Python and try again." + exit 1 fi + + entrypoint=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") + + ${python_command} "${entrypoint}/kiauh.py" } -function kiauh_update_dialog() { - [[ ! $(kiauh_update_avail) == "true" ]] && return - top_border - echo -e "|${green} New KIAUH update available! ${white}|" - hr - echo -e "|${green} View Changelog: https://git.io/JnmlX ${white}|" - blank_line - echo -e "|${yellow} It is recommended to keep KIAUH up to date. Updates ${white}|" - echo -e "|${yellow} usually contain bugfixes, important changes or new ${white}|" - echo -e "|${yellow} features. Please consider updating! ${white}|" - bottom_border +main - local yn - read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn - while true; do - case "${yn}" in - Y|y|Yes|yes|"") - do_action "update_kiauh" - break;; - N|n|No|no) - break;; - *) - deny_action "kiauh_update_dialog";; - esac - done -} - -check_euid -init_logfile -set_globals -kiauh_update_dialog -main_menu +#### sourcing all additional scripts +#KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")" +#for script in "${KIAUH_SRCDIR}/scripts/"*.sh; do . "${script}"; done +#for script in "${KIAUH_SRCDIR}/scripts/ui/"*.sh; do . "${script}"; done +# +##===================================================# +##=================== UPDATE KIAUH ==================# +##===================================================# +# +#function update_kiauh() { +# status_msg "Updating KIAUH ..." +# +# cd "${KIAUH_SRCDIR}" +# git reset --hard && git pull +# +# ok_msg "Update complete! Please restart KIAUH." +# exit 0 +#} +# +##===================================================# +##=================== KIAUH STATUS ==================# +##===================================================# +# +#function kiauh_update_avail() { +# [[ ! -d "${KIAUH_SRCDIR}/.git" ]] && return +# local origin head +# +# cd "${KIAUH_SRCDIR}" +# +# ### abort if not on master branch +# ! git branch -a | grep -q "\* master" && return +# +# ### compare commit hash +# git fetch -q +# origin=$(git rev-parse --short=8 origin/master) +# head=$(git rev-parse --short=8 HEAD) +# +# if [[ ${origin} != "${head}" ]]; then +# echo "true" +# fi +#} +# +#function kiauh_update_dialog() { +# [[ ! $(kiauh_update_avail) == "true" ]] && return +# top_border +# echo -e "|${green} New KIAUH update available! ${white}|" +# hr +# echo -e "|${green} View Changelog: https://git.io/JnmlX ${white}|" +# blank_line +# echo -e "|${yellow} It is recommended to keep KIAUH up to date. Updates ${white}|" +# echo -e "|${yellow} usually contain bugfixes, important changes or new ${white}|" +# echo -e "|${yellow} features. Please consider updating! ${white}|" +# bottom_border +# +# local yn +# read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn +# while true; do +# case "${yn}" in +# Y|y|Yes|yes|"") +# do_action "update_kiauh" +# break;; +# N|n|No|no) +# break;; +# *) +# deny_action "kiauh_update_dialog";; +# esac +# done +#} +# +#check_euid +#init_logfile +#set_globals +#kiauh_update_dialog +#main_menu diff --git a/kiauh/__init__.py b/kiauh/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/instance_manager/__init__.py b/kiauh/instance_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/instance_manager/base_instance.py b/kiauh/instance_manager/base_instance.py new file mode 100644 index 0000000..2f749b3 --- /dev/null +++ b/kiauh/instance_manager/base_instance.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 abc import abstractmethod, ABC +from pathlib import Path +from typing import List, Optional + + +class BaseInstance(ABC): + @classmethod + def blacklist(cls) -> List[str]: + return [] + + def __init__(self, prefix: Optional[str], name: Optional[str], + user: Optional[str], data_dir_name: Optional[str]): + self._prefix = prefix + self._name = name + self._user = user + self._data_dir_name = data_dir_name + self.data_dir = f"{Path.home()}/{self._data_dir_name}_data" + self.cfg_dir = f"{self.data_dir}/config" + self.log_dir = f"{self.data_dir}/logs" + self.comms_dir = f"{self.data_dir}/comms" + self.sysd_dir = f"{self.data_dir}/systemd" + + @property + def prefix(self) -> str: + return self._prefix + + @prefix.setter + def prefix(self, value) -> None: + self._prefix = value + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value) -> None: + self._name = value + + @property + def user(self) -> str: + return self._user + + @user.setter + def user(self, value) -> None: + self._user = value + + @property + def data_dir_name(self) -> str: + return self._data_dir_name + + @data_dir_name.setter + def data_dir_name(self, value) -> None: + self._data_dir_name = value + + @abstractmethod + def create(self) -> None: + raise NotImplementedError("Subclasses must implement the create method") + + @abstractmethod + def read(self) -> None: + raise NotImplementedError("Subclasses must implement the read method") + + @abstractmethod + def update(self) -> None: + raise NotImplementedError("Subclasses must implement the update method") + + @abstractmethod + def delete(self, del_remnants: bool) -> None: + raise NotImplementedError("Subclasses must implement the delete method") + + @abstractmethod + def get_service_file_name(self) -> str: + raise NotImplementedError( + "Subclasses must implement the get_service_file_name method") diff --git a/kiauh/instance_manager/instance_manager.py b/kiauh/instance_manager/instance_manager.py new file mode 100644 index 0000000..0bbdf14 --- /dev/null +++ b/kiauh/instance_manager/instance_manager.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import os +import re +import subprocess +from pathlib import Path +from typing import Optional, List, Type, Union + +from kiauh.instance_manager.base_instance import BaseInstance +from kiauh.utils.constants import SYSTEMD +from kiauh.utils.logger import Logger + + +# noinspection PyMethodMayBeStatic +class InstanceManager: + def __init__(self, instance_type: Type[BaseInstance], + current_instance: Optional[BaseInstance] = None) -> None: + self.instance_type = instance_type + self.current_instance = current_instance + self.instance_name = current_instance.name if current_instance is not None else None + self.instances = [] + + def get_current_instance(self) -> BaseInstance: + return self.current_instance + + def set_current_instance(self, instance: BaseInstance) -> None: + self.current_instance = instance + self.instance_name = f"{instance.prefix}-{instance.name}" if instance.name else instance.prefix + + def create_instance(self) -> None: + if self.current_instance is not None: + try: + self.current_instance.create() + except (OSError, subprocess.CalledProcessError) as e: + Logger.print_error(f"Creating instance failed: {e}") + raise + else: + raise ValueError("current_instance cannot be None") + + def delete_instance(self, del_remnants=False) -> None: + if self.current_instance is not None: + try: + self.current_instance.delete(del_remnants) + except (OSError, subprocess.CalledProcessError) as e: + Logger.print_error(f"Removing instance failed: {e}") + raise + else: + raise ValueError("current_instance cannot be None") + + def enable_instance(self) -> None: + Logger.print_info(f"Enabling {self.instance_name}.service ...") + try: + command = ["sudo", "systemctl", "enable", + f"{self.instance_name}.service"] + if subprocess.run(command, check=True): + Logger.print_ok(f"{self.instance_name}.service enabled.") + except subprocess.CalledProcessError as e: + Logger.print_error( + f"Error enabling service {self.instance_name}.service:") + Logger.print_error(f"{e}") + + def disable_instance(self) -> None: + Logger.print_info(f"Disabling {self.instance_name}.service ...") + try: + command = ["sudo", "systemctl", "disable", + f"{self.instance_name}.service"] + if subprocess.run(command, check=True): + Logger.print_ok(f"{self.instance_name}.service disabled.") + except subprocess.CalledProcessError as e: + Logger.print_error( + f"Error disabling service {self.instance_name}.service:") + Logger.print_error(f"{e}") + + def start_instance(self) -> None: + Logger.print_info(f"Starting {self.instance_name}.service ...") + try: + command = ["sudo", "systemctl", "start", + f"{self.instance_name}.service"] + if subprocess.run(command, check=True): + Logger.print_ok(f"{self.instance_name}.service started.") + except subprocess.CalledProcessError as e: + Logger.print_error( + f"Error starting service {self.instance_name}.service:") + Logger.print_error(f"{e}") + + def stop_instance(self) -> None: + Logger.print_info(f"Stopping {self.instance_name}.service ...") + try: + command = ["sudo", "systemctl", "stop", + f"{self.instance_name}.service"] + if subprocess.run(command, check=True): + Logger.print_ok(f"{self.instance_name}.service stopped.") + except subprocess.CalledProcessError as e: + Logger.print_error( + f"Error stopping service {self.instance_name}.service:") + Logger.print_error(f"{e}") + raise + + def reload_daemon(self) -> None: + Logger.print_info("Reloading systemd manager configuration ...") + try: + command = ["sudo", "systemctl", "daemon-reload"] + if subprocess.run(command, check=True): + Logger.print_ok("Systemd manager configuration reloaded") + except subprocess.CalledProcessError as e: + Logger.print_error("Error reloading systemd manager configuration:") + Logger.print_error(f"{e}") + raise + + def get_instances(self) -> List[BaseInstance]: + if not self.instances: + self._find_instances() + + return sorted(self.instances, + key=lambda x: self._sort_instance_list(x.name)) + + def _find_instances(self) -> None: + prefix = self.instance_type.__name__.lower() + pattern = re.compile(f"{prefix}(-[0-9a-zA-Z]+)?.service") + + excluded = self.instance_type.blacklist() + service_list = [ + os.path.join(SYSTEMD, service) + for service in os.listdir(SYSTEMD) + if pattern.search(service) + and not any(s in service for s in excluded)] + + instance_list = [ + self.instance_type(name=self._get_instance_name(Path(service))) + for service in service_list] + + self.instances = instance_list + + def _get_instance_name(self, file_path: Path) -> Union[str, None]: + full_name = str(file_path).split("/")[-1].split(".")[0] + if full_name.isalnum(): + return None + + return full_name.split("-")[-1] + + def _sort_instance_list(self, s): + return int(s) if s.isdigit() else s diff --git a/kiauh/main.py b/kiauh/main.py new file mode 100644 index 0000000..738b036 --- /dev/null +++ b/kiauh/main.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 kiauh.menus.main_menu import MainMenu +from kiauh.utils.logger import Logger + + +def main(): + try: + MainMenu().start() + except KeyboardInterrupt: + Logger.print_ok("\nHappy printing!\n", prefix=False) diff --git a/kiauh/menus/__init__.py b/kiauh/menus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/menus/advanced_menu.py b/kiauh/menus/advanced_menu.py new file mode 100644 index 0000000..ddc3315 --- /dev/null +++ b/kiauh/menus/advanced_menu.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap + +from kiauh.menus.base_menu import BaseMenu +from kiauh.utils.constants import COLOR_YELLOW, RESET_FORMAT + + +class AdvancedMenu(BaseMenu): + def __init__(self): + super().__init__( + header=True, + options={}, + footer_type="back" + ) + + def print_menu(self): + menu = textwrap.dedent(f""" + /=======================================================\\ + | {COLOR_YELLOW}~~~~~~~~~~~~~ [ Advanced Menu ] ~~~~~~~~~~~~~{RESET_FORMAT} | + |-------------------------------------------------------| + | Klipper & API: | Mainsail: | + | 0) [Rollback] | 5) [Theme installer] | + | | | + | Firmware: | System: | + | 1) [Build only] | 6) [Change hostname] | + | 2) [Flash only] | | + | 3) [Build + Flash] | Extras: | + | 4) [Get MCU ID] | 7) [G-Code Shell Command] | + """)[1:] + print(menu, end="") diff --git a/kiauh/menus/base_menu.py b/kiauh/menus/base_menu.py new file mode 100644 index 0000000..6cb5be6 --- /dev/null +++ b/kiauh/menus/base_menu.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import subprocess +import sys +import textwrap +from abc import abstractmethod, ABC +from typing import Dict, Any + +from kiauh.utils.constants import COLOR_GREEN, COLOR_YELLOW, COLOR_RED, \ + COLOR_CYAN, RESET_FORMAT + + +def clear(): + subprocess.call("clear", shell=True) + + +def print_header(): + header = textwrap.dedent(f""" + /=======================================================\\ + | {COLOR_CYAN}~~~~~~~~~~~~~~~~~ [ KIAUH ] ~~~~~~~~~~~~~~~~~{RESET_FORMAT} | + | {COLOR_CYAN} Klipper Installation And Update Helper {RESET_FORMAT} | + | {COLOR_CYAN}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{RESET_FORMAT} | + \=======================================================/ + """)[1:] + print(header, end="") + + +def print_quit_footer(): + footer = textwrap.dedent(f""" + |-------------------------------------------------------| + | {COLOR_RED}Q) Quit{RESET_FORMAT} | + \=======================================================/ + """)[1:] + print(footer, end="") + + +def print_back_footer(): + footer = textwrap.dedent(f""" + |-------------------------------------------------------| + | {COLOR_GREEN}B) « Back{RESET_FORMAT} | + \=======================================================/ + """)[1:] + print(footer, end="") + + +def print_back_help_footer(): + footer = textwrap.dedent(f""" + |-------------------------------------------------------| + | {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_RED}Q) Quit{RESET_FORMAT} | + \=======================================================/ + """)[1:] + print(footer, end="") + + +def print_back_quit_footer(): + footer = textwrap.dedent(f""" + |-------------------------------------------------------| + | {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_YELLOW}H) Help [?]{RESET_FORMAT} | + \=======================================================/ + """)[1:] + print(footer, end="") + + +def print_back_quit_help_footer(): + footer = textwrap.dedent(f""" + |-------------------------------------------------------| + | {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_RED}Q) Quit{RESET_FORMAT} | {COLOR_YELLOW}H) Help [?]{RESET_FORMAT} | + \=======================================================/ + """)[1:] + print(footer, end="") + + +class BaseMenu(ABC): + QUIT_FOOTER = "quit" + BACK_FOOTER = "back" + BACK_HELP_FOOTER = "back_help" + BACK_QUIT_FOOTER = "back_quit" + BACK_QUIT_HELP_FOOTER = "back_quit_help" + + def __init__(self, options: Dict[int, Any], options_offset=0, header=True, + footer_type="quit"): + self.options = options + self.options_offset = options_offset + self.header = header + self.footer_type = footer_type + + @abstractmethod + def print_menu(self): + raise NotImplementedError( + "Subclasses must implement the print_menu method") + + def print_footer(self): + footer_type_map = { + self.QUIT_FOOTER: print_quit_footer, + self.BACK_FOOTER: print_back_footer, + self.BACK_HELP_FOOTER: print_back_help_footer, + self.BACK_QUIT_FOOTER: print_back_quit_footer, + self.BACK_QUIT_HELP_FOOTER: print_back_quit_help_footer + } + footer_function = footer_type_map.get(self.footer_type, + print_quit_footer) + footer_function() + + def display(self): + # clear() + if self.header: + print_header() + self.print_menu() + self.print_footer() + + def handle_user_input(self): + while True: + choice = input(f"{COLOR_CYAN}###### Perform action: {RESET_FORMAT}") + + error_msg = f"{COLOR_RED}Invalid input.{RESET_FORMAT}" \ + if choice.isalpha() \ + else f"{COLOR_RED}Invalid input. Select a number between {min(self.options)} and {max(self.options)}.{RESET_FORMAT}" + + if choice.isdigit() and 0 <= int(choice) < len(self.options): + return choice + elif choice.isalpha(): + allowed_input = { + "quit": ["q"], + "back": ["b"], + "back_help": ["b", "h"], + "back_quit": ["b", "q"], + "back_quit_help": ["b", "q", "h"] + } + if self.footer_type in allowed_input and choice.lower() in \ + allowed_input[self.footer_type]: + return choice + else: + print(error_msg) + else: + print(error_msg) + + def start(self): + while True: + self.display() + choice = self.handle_user_input() + + if choice == "q": + print(f"{COLOR_GREEN}###### Happy printing!{RESET_FORMAT}") + sys.exit(0) + elif choice == "b": + return + elif choice == "p": + print("help!") + else: + self.execute_option(int(choice)) + + def execute_option(self, choice): + option = self.options.get(choice, None) + + if isinstance(option, type) and issubclass(option, BaseMenu): + self.navigate_to_submenu(option) + elif callable(option): + option() + elif option is None: + raise NotImplementedError(f"No implementation for option {choice}") + else: + raise TypeError( + f"Type {type(option)} of option {choice} not of type BaseMenu or Method") + + def navigate_to_submenu(self, submenu_class): + submenu = submenu_class() + submenu.previous_menu = self + submenu.start() diff --git a/kiauh/menus/install_menu.py b/kiauh/menus/install_menu.py new file mode 100644 index 0000000..91ae994 --- /dev/null +++ b/kiauh/menus/install_menu.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap + +from kiauh.menus.base_menu import BaseMenu +from kiauh.modules.klipper import klipper_setup +from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT + + +# noinspection PyMethodMayBeStatic +class InstallMenu(BaseMenu): + def __init__(self): + super().__init__( + header=True, + options={ + 1: self.install_klipper, + 2: self.install_moonraker, + 3: self.install_mainsail, + 4: self.install_fluidd, + 5: self.install_klipperscreen, + 6: self.install_pretty_gcode, + 7: self.install_telegram_bot, + 8: self.install_obico, + 9: self.install_octoeverywhere, + 10: self.install_mobileraker, + 11: self.install_crowsnest + }, + footer_type="back" + ) + + def print_menu(self): + menu = textwrap.dedent(f""" + /=======================================================\\ + | {COLOR_GREEN}~~~~~~~~~~~ [ Installation Menu ] ~~~~~~~~~~~{RESET_FORMAT} | + |-------------------------------------------------------| + | You need this menu usually only for installing | + | all necessary dependencies for the various | + | functions on a completely fresh system. | + |-------------------------------------------------------| + | Firmware & API: | Other: | + | 1) [Klipper] | 6) [PrettyGCode] | + | 2) [Moonraker] | 7) [Telegram Bot] | + | | 8) $(obico_install_title) | + | Klipper Webinterface: | 9) [OctoEverywhere] | + | 3) [Mainsail] | 10) [Mobileraker] | + | 4) [Fluidd] | | + | | Webcam Streamer: | + | Touchscreen GUI: | 11) [Crowsnest] | + | 5) [KlipperScreen] | | + """)[1:] + print(menu, end="") + + def install_klipper(self): + klipper_setup.run_klipper_setup(install=True) + + def install_moonraker(self): + print("install_moonraker") + + def install_mainsail(self): + print("install_mainsail") + + def install_fluidd(self): + print("install_fluidd") + + def install_klipperscreen(self): + print("install_klipperscreen") + + def install_pretty_gcode(self): + print("install_pretty_gcode") + + def install_telegram_bot(self): + print("install_telegram_bot") + + def install_obico(self): + print("install_obico") + + def install_octoeverywhere(self): + print("install_octoeverywhere") + + def install_mobileraker(self): + print("install_mobileraker") + + def install_crowsnest(self): + print("install_crowsnest") diff --git a/kiauh/menus/main_menu.py b/kiauh/menus/main_menu.py new file mode 100644 index 0000000..97a029a --- /dev/null +++ b/kiauh/menus/main_menu.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap + +from kiauh.menus.advanced_menu import AdvancedMenu +from kiauh.menus.base_menu import BaseMenu +from kiauh.menus.install_menu import InstallMenu +from kiauh.menus.remove_menu import RemoveMenu +from kiauh.menus.settings_menu import SettingsMenu +from kiauh.menus.update_menu import UpdateMenu +from kiauh.utils.constants import COLOR_MAGENTA, COLOR_CYAN, RESET_FORMAT + + +class MainMenu(BaseMenu): + def __init__(self): + super().__init__( + header=True, + options={ + 0: self.test, + 1: InstallMenu, + 2: UpdateMenu, + 3: RemoveMenu, + 4: AdvancedMenu, + 5: None, + 6: SettingsMenu + }, + footer_type="quit" + ) + + def print_menu(self): + menu = textwrap.dedent(f""" + /=======================================================\\ + | {COLOR_CYAN}~~~~~~~~~~~~~~~ [ Main Menu ] ~~~~~~~~~~~~~~~{RESET_FORMAT} | + |-------------------------------------------------------| + | 0) [Log-Upload] | Klipper: | + | | Repo: | + | 1) [Install] | | + | 2) [Update] | Moonraker: | + | 3) [Remove] | Repo: | + | 4) [Advanced] | | + | 5) [Backup] | Mainsail: | + | | Fluidd: | + | 6) [Settings] | KlipperScreen: | + | | Mobileraker: | + | | | + | | Crowsnest: | + | | Telegram Bot: | + | | Obico: | + | | OctoEverywhere: | + |-------------------------------------------------------| + | {COLOR_CYAN}KIAUH v6.0.0{RESET_FORMAT} | Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT} | + """)[1:] + print(menu, end="") + + def test(self): + print("blub") diff --git a/kiauh/menus/remove_menu.py b/kiauh/menus/remove_menu.py new file mode 100644 index 0000000..9b414fa --- /dev/null +++ b/kiauh/menus/remove_menu.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap + +from kiauh.menus.base_menu import BaseMenu +from kiauh.modules.klipper import klipper_setup +from kiauh.utils.constants import COLOR_RED, RESET_FORMAT + + +# noinspection PyMethodMayBeStatic +class RemoveMenu(BaseMenu): + def __init__(self): + super().__init__( + header=True, + options={ + 1: self.remove_klipper, + 2: self.remove_moonraker, + 3: self.remove_mainsail, + 4: self.remove_mainsail_config, + 5: self.remove_fluidd, + 6: self.remove_fluidd_config, + 7: self.remove_klipperscreen, + 8: self.remove_crowsnest, + 9: self.remove_mjpgstreamer, + 10: self.remove_pretty_gcode, + 11: self.remove_telegram_bot, + 12: self.remove_obico, + 13: self.remove_octoeverywhere, + 14: self.remove_mobileraker, + 15: self.remove_nginx, + }, + footer_type="back" + ) + + def print_menu(self): + menu = textwrap.dedent(f""" + /=======================================================\\ + | {COLOR_RED}~~~~~~~~~~~~~~ [ Remove Menu ] ~~~~~~~~~~~~~~{RESET_FORMAT} | + |-------------------------------------------------------| + | INFO: Configurations and/or any backups will be kept! | + |-------------------------------------------------------| + | Firmware & API: | Webcam Streamer: | + | 1) [Klipper] | 8) [Crowsnest] | + | 2) [Moonraker] | 9) [MJPG-Streamer] | + | | | + | Klipper Webinterface: | Other: | + | 3) [Mainsail] | 10) [PrettyGCode] | + | 4) [Mainsail-Config] | 11) [Telegram Bot] | + | 5) [Fluidd] | 12) [Obico for Klipper] | + | 6) [Fluidd-Config] | 13) [OctoEverywhere] | + | | 14) [Mobileraker] | + | Touchscreen GUI: | 15) [NGINX] | + | 7) [KlipperScreen] | | + """)[1:] + print(menu, end="") + + def remove_klipper(self): + klipper_setup.run_klipper_setup(install=False) + + def remove_moonraker(self): + print("remove_moonraker") + + def remove_mainsail(self): + print("remove_mainsail") + + def remove_mainsail_config(self): + print("remove_mainsail_config") + + def remove_fluidd(self): + print("remove_fluidd") + + def remove_fluidd_config(self): + print("remove_fluidd_config") + + def remove_klipperscreen(self): + print("remove_klipperscreen") + + def remove_crowsnest(self): + print("remove_crowsnest") + + def remove_mjpgstreamer(self): + print("remove_mjpgstreamer") + + def remove_pretty_gcode(self): + print("remove_pretty_gcode") + + def remove_telegram_bot(self): + print("remove_telegram_bot") + + def remove_obico(self): + print("remove_obico") + + def remove_octoeverywhere(self): + print("remove_octoeverywhere") + + def remove_mobileraker(self): + print("remove_mobileraker") + + def remove_nginx(self): + print("remove_nginx") diff --git a/kiauh/menus/settings_menu.py b/kiauh/menus/settings_menu.py new file mode 100644 index 0000000..df2f6c4 --- /dev/null +++ b/kiauh/menus/settings_menu.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 kiauh.menus.base_menu import BaseMenu + + +# noinspection PyMethodMayBeStatic +class SettingsMenu(BaseMenu): + def __init__(self): + super().__init__( + header=True, + options={} + ) + + def print_menu(self): + print("self") + + def execute_option_p(self): + # Implement the functionality for Option P + print("Executing Option P") + + def execute_option_q(self): + # Implement the functionality for Option Q + print("Executing Option Q") + + def execute_option_r(self): + # Implement the functionality for Option R + print("Executing Option R") diff --git a/kiauh/menus/update_menu.py b/kiauh/menus/update_menu.py new file mode 100644 index 0000000..f95865a --- /dev/null +++ b/kiauh/menus/update_menu.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap + +from kiauh.menus.base_menu import BaseMenu +from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT + + +# noinspection PyMethodMayBeStatic +class UpdateMenu(BaseMenu): + def __init__(self): + super().__init__( + header=True, + options={ + 0: self.update_all, + 1: self.update_klipper, + 2: self.update_moonraker, + 3: self.update_mainsail, + 4: self.update_fluidd, + 5: self.update_klipperscreen, + 6: self.update_pgc_for_klipper, + 7: self.update_telegram_bot, + 8: self.update_moonraker_obico, + 9: self.update_octoeverywhere, + 10: self.update_mobileraker, + 11: self.update_crowsnest, + 12: self.upgrade_system_packages, + }, + footer_type="back" + ) + + def print_menu(self): + menu = textwrap.dedent(f""" + /=======================================================\\ + | {COLOR_GREEN}~~~~~~~~~~~~~~ [ Update Menu ] ~~~~~~~~~~~~~~{RESET_FORMAT} | + |-------------------------------------------------------| + | 0) [Update all] | | | + | | Current: | Latest: | + | Klipper & API: |--------------|--------------| + | 1) [Klipper] | | | + | 2) [Moonraker] | | | + | | | | + | Klipper Webinterface: |--------------|--------------| + | 3) [Mainsail] | | | + | 4) [Fluidd] | | | + | | | | + | Touchscreen GUI: |--------------|--------------| + | 5) [KlipperScreen] | | | + | | | | + | Other: |--------------|--------------| + | 6) [PrettyGCode] | | | + | 7) [Telegram Bot] | | | + | 8) [Obico for Klipper] | | | + | 9) [OctoEverywhere] | | | + | 10) [Mobileraker] | | | + | 11) [Crowsnest] | | | + | |-----------------------------| + | 12) [System] | | | + """)[1:] + print(menu, end="") + + def update_all(self): + print("update_all") + + def update_klipper(self): + print("update_klipper") + + def update_moonraker(self): + print("update_moonraker") + + def update_mainsail(self): + print("update_mainsail") + + def update_fluidd(self): + print("update_fluidd") + + def update_klipperscreen(self): + print("update_klipperscreen") + + def update_pgc_for_klipper(self): + print("update_pgc_for_klipper") + + def update_telegram_bot(self): + print("update_telegram_bot") + + def update_moonraker_obico(self): + print("update_moonraker_obico") + + def update_octoeverywhere(self): + print("update_octoeverywhere") + + def update_mobileraker(self): + print("update_mobileraker") + + def update_crowsnest(self): + print("update_crowsnest") + + def upgrade_system_packages(self): + print("upgrade_system_packages") diff --git a/kiauh/modules/__init__.py b/kiauh/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/modules/klipper/__init__.py b/kiauh/modules/klipper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py new file mode 100644 index 0000000..e7d2826 --- /dev/null +++ b/kiauh/modules/klipper/klipper.py @@ -0,0 +1,164 @@ +# !/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import os +import pwd +import shutil +import subprocess +from pathlib import Path +from typing import List + +from kiauh.instance_manager.base_instance import BaseInstance +from kiauh.utils.constants import SYSTEMD, KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.utils.logger import Logger +from kiauh.utils.system_utils import create_directory + + +# noinspection PyMethodMayBeStatic +class Klipper(BaseInstance): + @classmethod + def blacklist(cls) -> List[str]: + return ["None", "mcu"] + + def __init__(self, name: str): + super().__init__(name=name, + prefix="klipper", + user=pwd.getpwuid(os.getuid())[0], + data_dir_name=self._get_data_dir_from_name(name)) + self.klipper_dir = KLIPPER_DIR + self.env_dir = KLIPPER_ENV_DIR + self.cfg_file = f"{self.cfg_dir}/printer.cfg" + self.log = f"{self.log_dir}/klippy.log" + self.serial = f"{self.comms_dir}/klippy.serial" + self.uds = f"{self.comms_dir}/klippy.sock" + + def create(self) -> None: + Logger.print_info("Creating Klipper Instance") + module_path = os.path.dirname(os.path.abspath(__file__)) + service_template_path = os.path.join(module_path, "res", + "klipper.service") + env_template_file_path = os.path.join(module_path, "res", "klipper.env") + service_file_name = self.get_service_file_name(extension=True) + service_file_target = f"{SYSTEMD}/{service_file_name}" + env_file_target = os.path.abspath(f"{self.sysd_dir}/klipper.env") + + # create folder structure + dirs = [self.data_dir, self.cfg_dir, self.log_dir, + self.comms_dir, self.sysd_dir] + for _dir in dirs: + create_directory(Path(_dir)) + + try: + # writing the klipper service file (requires sudo!) + service_content = self._prep_service_file(service_template_path, + env_file_target) + command = ["sudo", "tee", service_file_target] + subprocess.run(command, input=service_content.encode(), + stdout=subprocess.DEVNULL, check=True) + Logger.print_ok(f"Service file created: {service_file_target}") + + # writing the klipper.env file + env_file_content = self._prep_env_file(env_template_file_path) + with open(env_file_target, "w") as env_file: + env_file.write(env_file_content) + Logger.print_ok(f"Env file created: {env_file_target}") + + except subprocess.CalledProcessError as e: + Logger.print_error( + f"Error creating service file {service_file_target}: {e}") + raise + except OSError as e: + Logger.print_error( + f"Error creating env file {env_file_target}: {e}") + raise + + def read(self) -> None: + print("Reading Klipper Instance") + + def update(self) -> None: + print("Updating Klipper Instance") + + def delete(self, del_remnants: bool) -> None: + service_file = self.get_service_file_name(extension=True) + service_file_path = self._get_service_file_path() + + Logger.print_info(f"Deleting Klipper Instance: {service_file}") + + try: + command = ["sudo", "rm", "-f", service_file_path] + subprocess.run(command, check=True) + Logger.print_ok(f"Service file deleted: {service_file_path}") + except subprocess.CalledProcessError as e: + Logger.print_error(f"Error deleting service file: {e}") + raise + + if del_remnants: + self._delete_klipper_remnants() + + def _delete_klipper_remnants(self) -> None: + try: + Logger.print_info(f"Delete {self.klipper_dir} ...") + shutil.rmtree(Path(self.klipper_dir)) + Logger.print_info(f"Delete {self.env_dir} ...") + shutil.rmtree(Path(self.env_dir)) + except FileNotFoundError: + Logger.print_info("Cannot delete Klipper directories. Not found.") + except PermissionError as e: + Logger.print_error(f"Error deleting Klipper directories: {e}") + raise + + Logger.print_ok("Directories successfully deleted.") + + def get_service_file_name(self, extension=False) -> str: + name = self.prefix if self.name is None else self.prefix + '-' + self.name + return name if not extension else f"{name}.service" + + def _get_service_file_path(self): + return f"{SYSTEMD}/{self.get_service_file_name(extension=True)}" + + def _get_data_dir_from_name(self, name: str) -> str: + if name is None: + return "printer" + elif int(name.isdigit()): + return f"printer_{name}" + else: + return name + + def _prep_service_file(self, service_template_path, env_file_path): + try: + with open(service_template_path, "r") as template_file: + template_content = template_file.read() + except FileNotFoundError: + Logger.print_error( + f"Unable to open {service_template_path} - File not found") + raise + service_content = template_content.replace("%USER%", self.user) + service_content = service_content.replace("%KLIPPER_DIR%", + self.klipper_dir) + service_content = service_content.replace("%ENV%", self.env_dir) + service_content = service_content.replace("%ENV_FILE%", env_file_path) + return service_content + + def _prep_env_file(self, env_template_file_path): + try: + with open(env_template_file_path, "r") as env_file: + env_template_file_content = env_file.read() + except FileNotFoundError: + Logger.print_error( + f"Unable to open {env_template_file_path} - File not found") + raise + env_file_content = env_template_file_content.replace("%KLIPPER_DIR%", + self.klipper_dir) + env_file_content = env_file_content.replace("%CFG%", self.cfg_file) + env_file_content = env_file_content.replace("%SERIAL%", self.serial) + env_file_content = env_file_content.replace("%LOG%", self.log) + env_file_content = env_file_content.replace("%UDS%", self.uds) + return env_file_content diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py new file mode 100644 index 0000000..8deab8a --- /dev/null +++ b/kiauh/modules/klipper/klipper_setup.py @@ -0,0 +1,236 @@ +# !/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import os +import re +import subprocess +from pathlib import Path +from typing import Optional, List, Union + +from kiauh.instance_manager.instance_manager import InstanceManager +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.klipper.klipper_utils import print_instance_overview +from kiauh.utils.constants import KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.utils.input_utils import get_user_confirm, get_user_number_input, \ + get_user_string_input, get_user_selection_input +from kiauh.utils.logger import Logger +from kiauh.utils.system_utils import parse_packages_from_file, \ + clone_repo, create_python_venv, \ + install_python_requirements, update_system_package_lists, \ + install_system_packages + + +def run_klipper_setup(install: bool) -> None: + instance_manager = InstanceManager(Klipper) + instance_list = instance_manager.get_instances() + instances_installed = len(instance_list) + + is_klipper_installed = check_klipper_installation(instance_manager) + if not install and not is_klipper_installed: + Logger.print_warn("Klipper not installed!") + return + + if install: + add_additional = handle_existing_instances(instance_manager) + if is_klipper_installed and not add_additional: + Logger.print_info("Exiting Klipper setup ...") + return + + install_klipper(instance_manager) + + if not install: + if instances_installed == 1: + remove_single_instance(instance_manager) + else: + remove_multi_instance(instance_manager) + + +def check_klipper_installation(instance_manager: InstanceManager) -> bool: + instance_list = instance_manager.get_instances() + instances_installed = len(instance_list) + + if instances_installed < 1: + return False + + return True + + +def handle_existing_instances(instance_manager: InstanceManager) -> bool: + instance_list = instance_manager.get_instances() + instance_count = len(instance_list) + + if instance_count > 0: + print_instance_overview(instance_list) + if not get_user_confirm("Add new instances?"): + return False + + return True + + +def install_klipper(instance_manager: InstanceManager) -> None: + instance_list = instance_manager.get_instances() + if_adding = " additional" if len(instance_list) > 0 else "" + install_count = get_user_number_input( + f"Number of{if_adding} Klipper instances to set up", + 1, default=1) + + instance_names = set_instance_names(instance_list, install_count) + + if len(instance_list) < 1: + setup_klipper_prerequesites() + + for name in instance_names: + current_instance = Klipper(name=name) + instance_manager.set_current_instance(current_instance) + instance_manager.create_instance() + instance_manager.enable_instance() + instance_manager.start_instance() + + instance_manager.reload_daemon() + + # step 4: check/handle conflicting packages/services + + # step 5: check for required group membership + + +def setup_klipper_prerequesites() -> None: + # clone klipper TODO: read branch and url from json to allow forks + url = "https://github.com/Klipper3D/klipper" + branch = "master" + clone_repo(Path(KLIPPER_DIR), url, branch) + + # install klipper dependencies and create python virtualenv + install_klipper_packages(Path(KLIPPER_DIR)) + create_python_venv(Path(KLIPPER_ENV_DIR)) + klipper_py_req = Path(f"{KLIPPER_DIR}/scripts/klippy-requirements.txt") + install_python_requirements(Path(KLIPPER_ENV_DIR), klipper_py_req) + + +def install_klipper_packages(klipper_dir: Path) -> None: + script = f"{klipper_dir}/scripts/install-debian.sh" + packages = parse_packages_from_file(script) + packages = [pkg.replace("python-dev", "python3-dev") for pkg in packages] + # Add dfu-util for octopi-images + packages.append("dfu-util") + # Add dbus requirement for DietPi distro + if os.path.exists("/boot/dietpi/.version"): + packages.append("dbus") + + update_system_package_lists(silent=False) + install_system_packages(packages) + + +def set_instance_names(instance_list, install_count: int) -> List[ + Union[str, None]]: + instance_count = len(instance_list) + + # default single instance install + if instance_count == 0 and install_count == 1: + return [None] + + # new multi instance install + elif ((instance_count == 0 and install_count > 1) + # or convert single instance install to multi instance install + or (instance_count == 1 and install_count >= 1)): + if get_user_confirm("Assign custom names?", False): + return assign_custom_names(instance_count, install_count, None) + else: + _range = range(1, install_count + 1) + return [str(i) for i in _range] + + # existing multi instance install + elif instance_count > 1: + if has_custom_names(instance_list): + return assign_custom_names(instance_count, install_count, + instance_list) + else: + start = get_highest_index(instance_list) + 1 + _range = range(start, start + install_count) + return [str(i) for i in _range] + + +def has_custom_names(instance_list: List[Klipper]) -> bool: + pattern = re.compile("^\d+$") + for instance in instance_list: + if not pattern.match(instance.name): + return True + + return False + + +def assign_custom_names(instance_count: int, install_count: int, + instance_list: Optional[List[Klipper]]) -> List[str]: + instance_names = [] + exclude = Klipper.blacklist() + + # if an instance_list is provided, exclude all existing instance names + if instance_list is not None: + for instance in instance_list: + exclude.append(instance.name) + + for i in range(instance_count + install_count): + question = f"Enter name for instance {i + 1}" + name = get_user_string_input(question, exclude=exclude) + instance_names.append(name) + exclude.append(name) + + return instance_names + + +def get_highest_index(instance_list: List[Klipper]) -> int: + indices = [int(instance.name.split('-')[-1]) for instance in instance_list] + return max(indices) + + +def remove_single_instance(instance_manager: InstanceManager) -> None: + instance_list = instance_manager.get_instances() + try: + instance_manager.set_current_instance(instance_list[0]) + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance(del_remnants=True) + instance_manager.reload_daemon() + except (OSError, subprocess.CalledProcessError): + Logger.print_error("Removing instance failed!") + return + + +def remove_multi_instance(instance_manager: InstanceManager) -> None: + instance_list = instance_manager.get_instances() + print_instance_overview(instance_list, show_index=True, + show_select_all=True) + + options = [str(i) for i in range(len(instance_list))] + options.extend(["a", "A", "b", "B"]) + + selection = get_user_selection_input( + "Select Klipper instance to remove", options) + print(selection) + + if selection == "b".lower(): + return + elif selection == "a".lower(): + Logger.print_info("Removing all Klipper instances ...") + for instance in instance_list: + instance_manager.set_current_instance(instance) + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance(del_remnants=True) + else: + instance = instance_list[int(selection)] + Logger.print_info( + f"Removing Klipper instance: {instance.get_service_file_name()}") + instance_manager.set_current_instance(instance) + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance(del_remnants=False) + + instance_manager.reload_daemon() diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py new file mode 100644 index 0000000..c67eb8a --- /dev/null +++ b/kiauh/modules/klipper/klipper_utils.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 typing import List + +from kiauh.instance_manager.base_instance import BaseInstance +from kiauh.menus.base_menu import print_back_footer +from kiauh.utils.constants import COLOR_GREEN, COLOR_CYAN, COLOR_YELLOW, \ + RESET_FORMAT + + +def print_instance_overview(instances: List[BaseInstance], show_index=False, + show_select_all=False): + headline = f"{COLOR_GREEN}The following Klipper instances were found:{RESET_FORMAT}" + + print("/=======================================================\\") + print(f"|{'{:^64}'.format(headline)}|") + print("|-------------------------------------------------------|") + + if show_select_all: + select_all = f" {COLOR_YELLOW}a) Select all{RESET_FORMAT}" + print(f"|{'{:64}'.format(select_all)}|") + print("| |") + + for i, s in enumerate(instances): + index = f"{i})" if show_index else "●" + instance = s.get_service_file_name() + line = f"{'{:53}'.format(f'{index} {instance}')}" + print(f"| {COLOR_CYAN}{line}{RESET_FORMAT}|") + + print_back_footer() diff --git a/kiauh/modules/klipper/res/klipper.env b/kiauh/modules/klipper/res/klipper.env new file mode 100644 index 0000000..b56553e --- /dev/null +++ b/kiauh/modules/klipper/res/klipper.env @@ -0,0 +1 @@ +KLIPPER_ARGS="%KLIPPER_DIR%/klippy/klippy.py %CFG% -I %SERIAL% -l %LOG% -a %UDS%" diff --git a/kiauh/modules/klipper/res/klipper.service b/kiauh/modules/klipper/res/klipper.service new file mode 100644 index 0000000..b41788f --- /dev/null +++ b/kiauh/modules/klipper/res/klipper.service @@ -0,0 +1,18 @@ +[Unit] +Description=Klipper 3D Printer Firmware SV1 +Documentation=https://www.klipper3d.org/ +After=network-online.target +Wants=udev.target + +[Install] +WantedBy=multi-user.target + +[Service] +Type=simple +User=%USER% +RemainAfterExit=yes +WorkingDirectory=%KLIPPER_DIR% +EnvironmentFile=%ENV_FILE% +ExecStart=%ENV%/bin/python $KLIPPER_ARGS +Restart=always +RestartSec=10 diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py new file mode 100644 index 0000000..3c96aab --- /dev/null +++ b/kiauh/utils/constants.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 pathlib import Path + +# text colors and formats +COLOR_MAGENTA = "\033[35m" # magenta +COLOR_GREEN = "\033[92m" # bright green +COLOR_YELLOW = "\033[93m" # bright yellow +COLOR_RED = "\033[91m" # bright red +COLOR_CYAN = "\033[96m" # bright cyan +RESET_FORMAT = "\033[0m" # reset format + +SYSTEMD = "/etc/systemd/system" + +KLIPPER_DIR = f"{Path.home()}/klipper" +KLIPPER_ENV_DIR = f"{Path.home()}/klippy-env" +MOONRAKER_DIR = f"{Path.home()}/moonraker" +MOONRAKER_ENV_DIR = f"{Path.home()}/moonraker-env" +MAINSAIL_DIR = f"{Path.home()}/mainsail" +FLUIDD_DIR = f"{Path.home()}/fluidd" diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py new file mode 100644 index 0000000..c6327d6 --- /dev/null +++ b/kiauh/utils/input_utils.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 typing import Optional, List + +from kiauh.utils.logger import Logger +from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT + + +def get_user_confirm(question: str, default_choice=True) -> bool: + options_confirm = ["y", "yes"] + options_decline = ["n", "no"] + + if default_choice: + def_choice = "(Y/n)" + options_confirm.append("") + else: + def_choice = "(y/N)" + options_decline.append("") + + while True: + choice = ( + input(f"{COLOR_CYAN}###### {question} {def_choice} {RESET_FORMAT}") + .strip() + .lower()) + + if choice in options_confirm: + return True + elif choice in options_decline: + return False + else: + Logger.print_error("Invalid choice. Please select 'y' or 'n'.") + + +def get_user_number_input(question: str, min_count: int, max_count=None, + default=None) -> int: + _question = question + f" (default={default})" if default else question + _question = f"{COLOR_CYAN}###### {_question}: {RESET_FORMAT}" + while True: + try: + num = input(_question) + if num == "": + return default + + if max_count is not None: + if min_count <= int(num) <= max_count: + return int(num) + else: + raise ValueError + elif int(num) >= min_count: + return int(num) + else: + raise ValueError + except ValueError: + Logger.print_error("Invalid choice. Please select a valid number.") + + +def get_user_string_input(question: str, exclude=Optional[List]) -> str: + while True: + _input = (input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}") + .strip()) + + if _input.isalnum() and _input not in exclude: + return _input + + Logger.print_error("Invalid choice. Please enter a valid value.") + if _input in exclude: + Logger.print_error("This value is already in use/reserved.") + + +def get_user_selection_input(question: str, option_list: List) -> str: + while True: + _input = (input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}") + .strip()) + + if _input in option_list: + return _input + + Logger.print_error("Invalid choice. Please enter a valid value.") diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py new file mode 100644 index 0000000..f6adc0d --- /dev/null +++ b/kiauh/utils/logger.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 kiauh.utils.constants import COLOR_GREEN, COLOR_YELLOW, COLOR_RED, \ + COLOR_MAGENTA, RESET_FORMAT + + +class Logger: + + @staticmethod + def info(msg): + # log to kiauh.log + pass + + @staticmethod + def warn(msg): + # log to kiauh.log + pass + + @staticmethod + def error(msg): + # log to kiauh.log + pass + + @staticmethod + def print_ok(msg, prefix=True, end="\n") -> None: + message = f"[OK] {msg}" if prefix else msg + print(f"{COLOR_GREEN}{message}{RESET_FORMAT}", end=end) + + @staticmethod + def print_warn(msg, prefix=True, end="\n") -> None: + message = f"[WARN] {msg}" if prefix else msg + print(f"{COLOR_YELLOW}{message}{RESET_FORMAT}", end=end) + + @staticmethod + def print_error(msg, prefix=True, end="\n") -> None: + message = f"[ERROR] {msg}" if prefix else msg + print(f"{COLOR_RED}{message}{RESET_FORMAT}", end=end) + + @staticmethod + def print_info(msg, prefix=True, end="\n") -> None: + message = f"###### {msg}" if prefix else msg + print(f"{COLOR_MAGENTA}{message}{RESET_FORMAT}", end=end) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py new file mode 100644 index 0000000..c089e90 --- /dev/null +++ b/kiauh/utils/system_utils.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import os +import shutil +import subprocess +import sys +import time +from pathlib import Path +from typing import List + +from kiauh.utils.constants import COLOR_RED, RESET_FORMAT +from kiauh.utils.logger import Logger +from kiauh.utils.input_utils import get_user_confirm + + +def kill(opt_err_msg=None) -> None: + """ + Kill the application. + + Parameters + ---------- + opt_err_msg : str + optional, additional error message to display + + Returns + ---------- + None + """ + + if opt_err_msg: + Logger.print_error(opt_err_msg) + Logger.print_error("A critical error has occured. KIAUH was terminated.") + sys.exit(1) + + +def clone_repo(target_dir: Path, url: str, branch: str) -> None: + Logger.print_info(f"Cloning repository from {url}") + if not target_dir.exists(): + try: + command = ["git", "clone", f"{url}"] + subprocess.run(command, check=True) + + command = ["git", "checkout", f"{branch}"] + subprocess.run(command, cwd=target_dir, check=True) + + Logger.print_ok("Clone successfull!") + except subprocess.CalledProcessError as e: + print("Error cloning repository:", e.output.decode()) + else: + overwrite_target = get_user_confirm( + "Target directory already exists. Overwrite?") + if overwrite_target: + try: + shutil.rmtree(target_dir) + clone_repo(target_dir, url, branch) + except OSError as e: + print("Error removing existing repository:", e.strerror) + else: + print("Skipping re-clone of repository ...") + + +def parse_packages_from_file(source_file) -> List[str]: + packages = [] + print("Reading dependencies...") + with open(source_file, 'r') as file: + for line in file: + line = line.strip() + if line.startswith("PKGLIST="): + line = line.replace("\"", "") + line = line.replace("PKGLIST=", "") + line = line.replace('${PKGLIST}', '') + packages.extend(line.split()) + return packages + + +def create_python_venv(target: Path) -> None: + Logger.print_info("Set up Python virtual environment ...") + if not target.exists(): + try: + command = ["python3", "-m", "venv", f"{target}"] + result = subprocess.run(command, stderr=subprocess.PIPE, text=True) + if result.returncode != 0 or result.stderr: + print(f"{COLOR_RED}{result.stderr}{RESET_FORMAT}") + Logger.print_error("Setup of virtualenv failed!") + return + + Logger.print_ok("Setup of virtualenv successfull!") + except subprocess.CalledProcessError as e: + print("Error setting up virtualenv:", e.output.decode()) + else: + overwrite_venv = get_user_confirm( + "Virtualenv already exists. Re-create?") + if overwrite_venv: + try: + shutil.rmtree(target) + create_python_venv(target) + except OSError as e: + Logger.print_error( + f"Error removing existing virtualenv: {e.strerror}", + False) + else: + print("Skipping re-creation of virtualenv ...") + + +def update_python_pip(target: Path) -> None: + Logger.print_info("Updating pip ...") + try: + command = [f"{target}/bin/pip", "install", "-U", "pip"] + result = subprocess.run(command, stderr=subprocess.PIPE, text=True) + if result.returncode != 0 or result.stderr: + Logger.print_error(f"{result.stderr}", False) + Logger.print_error("Updating pip failed!") + return + + Logger.print_ok("Updating pip successfull!") + except subprocess.CalledProcessError as e: + print("Error updating pip:", e.output.decode()) + + +def install_python_requirements(target: Path, requirements: Path) -> None: + update_python_pip(target) + Logger.print_info("Installing Python requirements ...") + try: + command = [f"{target}/bin/pip", "install", "-r", f"{requirements}"] + result = subprocess.run(command, stderr=subprocess.PIPE, text=True) + if result.returncode != 0 or result.stderr: + Logger.print_error(f"{result.stderr}", False) + Logger.print_error("Installing Python requirements failed!") + return + + Logger.print_ok("Installing Python requirements successfull!") + except subprocess.CalledProcessError as e: + print("Error installing Python requirements:", e.output.decode()) + + +def update_system_package_lists(silent: bool, rls_info_change=False) -> None: + cache_mtime = 0 + cache_files = [ + "/var/lib/apt/periodic/update-success-stamp", + "/var/lib/apt/lists" + ] + for cache_file in cache_files: + if Path(cache_file).exists(): + cache_mtime = max(cache_mtime, os.path.getmtime(cache_file)) + + update_age = int(time.time() - cache_mtime) + update_interval = 6 * 3600 # 48hrs + + if update_age <= update_interval: + return + + if not silent: + print("Updating package list...") + + try: + command = ["sudo", "apt-get", "update"] + if rls_info_change: + command.append("--allow-releaseinfo-change") + + result = subprocess.run(command, stderr=subprocess.PIPE, text=True) + if result.returncode != 0 or result.stderr: + Logger.print_error(f"{result.stderr}", False) + Logger.print_error("Updating system package list failed!") + return + + Logger.print_ok("System package list updated successfully!") + except subprocess.CalledProcessError as e: + kill(f"Error updating system package list:\n{e.stderr.decode()}") + + +def install_system_packages(packages: List) -> None: + try: + command = ["sudo", "apt-get", "install", "-y"] + for pkg in packages: + command.append(pkg) + subprocess.run(command, stderr=subprocess.PIPE, check=True) + + Logger.print_ok("Packages installed successfully.") + except subprocess.CalledProcessError as e: + kill(f"Error installing packages:\n{e.stderr.decode()}") + + +def create_directory(_dir: Path) -> None: + try: + if not os.path.isdir(_dir): + Logger.print_info(f"Create directory: {_dir}") + os.makedirs(_dir, exist_ok=True) + Logger.print_ok("Directory created!") + else: + Logger.print_info( + f"Directory already exists: {_dir}\nSkip creation ...") + except OSError as e: + Logger.print_error(f"Error creating folder: {e}") + raise From 2a100c29344730959ad8de2d4e011a548c69b318 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 28 Oct 2023 19:33:18 +0200 Subject: [PATCH 002/296] feat(klipper): check for required user-groups Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper.py | 4 +-- kiauh/modules/klipper/klipper_setup.py | 35 ++++++++++++++++++++++++-- kiauh/modules/klipper/klipper_utils.py | 18 +++++++++++++ kiauh/utils/constants.py | 6 +++++ 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index e7d2826..81f0ceb 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -17,7 +17,7 @@ from pathlib import Path from typing import List from kiauh.instance_manager.base_instance import BaseInstance -from kiauh.utils.constants import SYSTEMD, KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.utils.constants import CURRENT_USER, SYSTEMD, KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.utils.logger import Logger from kiauh.utils.system_utils import create_directory @@ -31,7 +31,7 @@ class Klipper(BaseInstance): def __init__(self, name: str): super().__init__(name=name, prefix="klipper", - user=pwd.getpwuid(os.getuid())[0], + user=CURRENT_USER, data_dir_name=self._get_data_dir_from_name(name)) self.klipper_dir = KLIPPER_DIR self.env_dir = KLIPPER_ENV_DIR diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 8deab8a..d20d294 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -11,14 +11,15 @@ import os import re +import grp import subprocess from pathlib import Path from typing import Optional, List, Union from kiauh.instance_manager.instance_manager import InstanceManager from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.klipper.klipper_utils import print_instance_overview -from kiauh.utils.constants import KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.modules.klipper.klipper_utils import print_instance_overview, print_missing_usergroup_dialog +from kiauh.utils.constants import CURRENT_USER, KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.utils.input_utils import get_user_confirm, get_user_number_input, \ get_user_string_input, get_user_selection_input from kiauh.utils.logger import Logger @@ -99,6 +100,7 @@ def install_klipper(instance_manager: InstanceManager) -> None: # step 4: check/handle conflicting packages/services # step 5: check for required group membership + check_user_groups() def setup_klipper_prerequesites() -> None: @@ -234,3 +236,32 @@ def remove_multi_instance(instance_manager: InstanceManager) -> None: instance_manager.delete_instance(del_remnants=False) instance_manager.reload_daemon() + +def check_user_groups(): + current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] + + missing_groups = [] + if "tty" not in current_groups: + missing_groups.append("tty") + if "dialout" not in current_groups: + missing_groups.append("dialout") + + if not missing_groups: + return + + print_missing_usergroup_dialog(missing_groups) + if not get_user_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): + Logger.warn("Skipped adding user to required groups. You might encounter issues.") + return + + try: + for group in missing_groups: + Logger.print_info(f"Adding user '{CURRENT_USER}' to group {group} ...") + command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER] + subprocess.run(command, check=True) + Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.") + except subprocess.CalledProcessError as e: + Logger.print_error(f"Unable to add user to usergroups: {e}") + raise + + Logger.print_warn("Remember to relog/restart this machine for the group(s) to be applied!") diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index c67eb8a..445e3ce 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -37,3 +37,21 @@ def print_instance_overview(instances: List[BaseInstance], show_index=False, print(f"| {COLOR_CYAN}{line}{RESET_FORMAT}|") print_back_footer() + +def print_missing_usergroup_dialog(missing_groups) -> None: + print("/=======================================================\\") + print(f"| {COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT} |") + if "tty" in missing_groups: + print(f"| {COLOR_CYAN}● tty{RESET_FORMAT} |") + if "dialout" in missing_groups: + print(f"| {COLOR_CYAN}● dialout{RESET_FORMAT} |") + print("| |") + print("| It is possible that you won't be able to successfully |") + print("| connect and/or flash the controller board without |") + print("| your user being a member of that group. |") + print("| If you want to add the current user to the group(s) |") + print("| listed above, answer with 'Y'. Else skip with 'n'. |") + print("| |") + print(f"| {COLOR_YELLOW}INFO:{RESET_FORMAT} |") + print(f"| {COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT} |") + print("\\=======================================================/") diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py index 3c96aab..f795697 100644 --- a/kiauh/utils/constants.py +++ b/kiauh/utils/constants.py @@ -9,6 +9,9 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import os +import pwd + from pathlib import Path # text colors and formats @@ -19,6 +22,9 @@ COLOR_RED = "\033[91m" # bright red COLOR_CYAN = "\033[96m" # bright cyan RESET_FORMAT = "\033[0m" # reset format +# current user +CURRENT_USER = pwd.getpwuid(os.getuid())[0] + SYSTEMD = "/etc/systemd/system" KLIPPER_DIR = f"{Path.home()}/klipper" From bfff3019cb3bed7a4b1dcdde8a0c3a6ac9bb3c81 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 28 Oct 2023 22:09:46 +0200 Subject: [PATCH 003/296] fix(InstanceManager): fix TypeError if instance name is None Signed-off-by: Dominik Willner --- kiauh/instance_manager/instance_manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kiauh/instance_manager/instance_manager.py b/kiauh/instance_manager/instance_manager.py index 0bbdf14..c4847ef 100644 --- a/kiauh/instance_manager/instance_manager.py +++ b/kiauh/instance_manager/instance_manager.py @@ -148,4 +148,7 @@ class InstanceManager: return full_name.split("-")[-1] def _sort_instance_list(self, s): + if s is None: + return + return int(s) if s.isdigit() else s From 84a530be7deaf762f12582b81c3fcb6e1af26812 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 28 Oct 2023 23:52:51 +0200 Subject: [PATCH 004/296] fix(klipper): handle disruptive system packages/services Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 31 ++++++++++++++++++++++++-- kiauh/utils/system_utils.py | 11 ++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index d20d294..652a9f5 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -9,10 +9,11 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import grp import os import re -import grp import subprocess +import textwrap from pathlib import Path from typing import Optional, List, Union @@ -26,7 +27,7 @@ from kiauh.utils.logger import Logger from kiauh.utils.system_utils import parse_packages_from_file, \ clone_repo, create_python_venv, \ install_python_requirements, update_system_package_lists, \ - install_system_packages + install_system_packages, mask_system_service def run_klipper_setup(install: bool) -> None: @@ -98,6 +99,7 @@ def install_klipper(instance_manager: InstanceManager) -> None: instance_manager.reload_daemon() # step 4: check/handle conflicting packages/services + handle_disruptive_system_packages() # step 5: check for required group membership check_user_groups() @@ -237,6 +239,7 @@ def remove_multi_instance(instance_manager: InstanceManager) -> None: instance_manager.reload_daemon() + def check_user_groups(): current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] @@ -265,3 +268,27 @@ def check_user_groups(): raise Logger.print_warn("Remember to relog/restart this machine for the group(s) to be applied!") + + +def handle_disruptive_system_packages() -> None: + services = [] + brltty_status = subprocess.run(["systemctl", "is-enabled", "brltty"], capture_output=True, text=True) + modem_manager_status = subprocess.run(["systemctl", "is-enabled", "ModemManager"], capture_output=True, text=True) + + if "enabled" in brltty_status.stdout: + services.append("brltty") + if "enabled" in modem_manager_status.stdout: + services.append("ModemManager") + + for service in services if services else []: + try: + Logger.print_info(f"{service} service detected! Masking {service} service ...") + mask_system_service(service) + Logger.print_ok(f"{service} service masked!") + except subprocess.CalledProcessError: + warn_msg = textwrap.dedent(f""" + KIAUH was unable to mask the {service} system service. + Please fix the problem manually. Otherwise, this may have + undesirable effects on the operation of Klipper. + """)[1:] + Logger.print_warn(warn_msg) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index c089e90..af85d24 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -18,8 +18,8 @@ from pathlib import Path from typing import List from kiauh.utils.constants import COLOR_RED, RESET_FORMAT -from kiauh.utils.logger import Logger from kiauh.utils.input_utils import get_user_confirm +from kiauh.utils.logger import Logger def kill(opt_err_msg=None) -> None: @@ -201,3 +201,12 @@ def create_directory(_dir: Path) -> None: except OSError as e: Logger.print_error(f"Error creating folder: {e}") raise + + +def mask_system_service(service_name: str) -> None: + try: + command = ["sudo", "systemctl", "mask", service_name] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + Logger.print_error(f"Unable to mask system service {service_name}: {e.stderr.decode()}") + raise From 358c666da9bf6123cef6625006be4d16ae6ce45a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 29 Oct 2023 00:31:34 +0200 Subject: [PATCH 005/296] feat(style): use black code style / formatter Signed-off-by: Dominik Willner --- kiauh/instance_manager/base_instance.py | 12 ++- kiauh/instance_manager/instance_manager.py | 49 ++++++------ kiauh/main.py | 2 +- kiauh/menus/advanced_menu.py | 12 ++- kiauh/menus/base_menu.py | 75 ++++++++++++------- kiauh/menus/install_menu.py | 10 ++- kiauh/menus/main_menu.py | 10 ++- kiauh/menus/remove_menu.py | 8 +- kiauh/menus/settings_menu.py | 5 +- kiauh/menus/update_menu.py | 8 +- kiauh/modules/klipper/klipper.py | 58 +++++++++------ kiauh/modules/klipper/klipper_setup.py | 86 ++++++++++++++-------- kiauh/modules/klipper/klipper_utils.py | 29 +++++--- kiauh/utils/input_utils.py | 16 ++-- kiauh/utils/logger.py | 10 ++- kiauh/utils/system_utils.py | 28 ++++--- pyproject.toml | 13 ++++ 17 files changed, 261 insertions(+), 170 deletions(-) create mode 100644 pyproject.toml diff --git a/kiauh/instance_manager/base_instance.py b/kiauh/instance_manager/base_instance.py index 2f749b3..169ff74 100644 --- a/kiauh/instance_manager/base_instance.py +++ b/kiauh/instance_manager/base_instance.py @@ -19,8 +19,13 @@ class BaseInstance(ABC): def blacklist(cls) -> List[str]: return [] - def __init__(self, prefix: Optional[str], name: Optional[str], - user: Optional[str], data_dir_name: Optional[str]): + def __init__( + self, + prefix: Optional[str], + name: Optional[str], + user: Optional[str], + data_dir_name: Optional[str], + ): self._prefix = prefix self._name = name self._user = user @@ -82,4 +87,5 @@ class BaseInstance(ABC): @abstractmethod def get_service_file_name(self) -> str: raise NotImplementedError( - "Subclasses must implement the get_service_file_name method") + "Subclasses must implement the get_service_file_name method" + ) diff --git a/kiauh/instance_manager/instance_manager.py b/kiauh/instance_manager/instance_manager.py index c4847ef..b1924b1 100644 --- a/kiauh/instance_manager/instance_manager.py +++ b/kiauh/instance_manager/instance_manager.py @@ -22,11 +22,16 @@ from kiauh.utils.logger import Logger # noinspection PyMethodMayBeStatic class InstanceManager: - def __init__(self, instance_type: Type[BaseInstance], - current_instance: Optional[BaseInstance] = None) -> None: + def __init__( + self, + instance_type: Type[BaseInstance], + current_instance: Optional[BaseInstance] = None, + ) -> None: self.instance_type = instance_type self.current_instance = current_instance - self.instance_name = current_instance.name if current_instance is not None else None + self.instance_name = ( + current_instance.name if current_instance is not None else None + ) self.instances = [] def get_current_instance(self) -> BaseInstance: @@ -34,7 +39,9 @@ class InstanceManager: def set_current_instance(self, instance: BaseInstance) -> None: self.current_instance = instance - self.instance_name = f"{instance.prefix}-{instance.name}" if instance.name else instance.prefix + self.instance_name = ( + f"{instance.prefix}-{instance.name}" if instance.name else instance.prefix + ) def create_instance(self) -> None: if self.current_instance is not None: @@ -59,49 +66,41 @@ class InstanceManager: def enable_instance(self) -> None: Logger.print_info(f"Enabling {self.instance_name}.service ...") try: - command = ["sudo", "systemctl", "enable", - f"{self.instance_name}.service"] + command = ["sudo", "systemctl", "enable", f"{self.instance_name}.service"] if subprocess.run(command, check=True): Logger.print_ok(f"{self.instance_name}.service enabled.") except subprocess.CalledProcessError as e: - Logger.print_error( - f"Error enabling service {self.instance_name}.service:") + Logger.print_error(f"Error enabling service {self.instance_name}.service:") Logger.print_error(f"{e}") def disable_instance(self) -> None: Logger.print_info(f"Disabling {self.instance_name}.service ...") try: - command = ["sudo", "systemctl", "disable", - f"{self.instance_name}.service"] + command = ["sudo", "systemctl", "disable", f"{self.instance_name}.service"] if subprocess.run(command, check=True): Logger.print_ok(f"{self.instance_name}.service disabled.") except subprocess.CalledProcessError as e: - Logger.print_error( - f"Error disabling service {self.instance_name}.service:") + Logger.print_error(f"Error disabling service {self.instance_name}.service:") Logger.print_error(f"{e}") def start_instance(self) -> None: Logger.print_info(f"Starting {self.instance_name}.service ...") try: - command = ["sudo", "systemctl", "start", - f"{self.instance_name}.service"] + command = ["sudo", "systemctl", "start", f"{self.instance_name}.service"] if subprocess.run(command, check=True): Logger.print_ok(f"{self.instance_name}.service started.") except subprocess.CalledProcessError as e: - Logger.print_error( - f"Error starting service {self.instance_name}.service:") + Logger.print_error(f"Error starting service {self.instance_name}.service:") Logger.print_error(f"{e}") def stop_instance(self) -> None: Logger.print_info(f"Stopping {self.instance_name}.service ...") try: - command = ["sudo", "systemctl", "stop", - f"{self.instance_name}.service"] + command = ["sudo", "systemctl", "stop", f"{self.instance_name}.service"] if subprocess.run(command, check=True): Logger.print_ok(f"{self.instance_name}.service stopped.") except subprocess.CalledProcessError as e: - Logger.print_error( - f"Error stopping service {self.instance_name}.service:") + Logger.print_error(f"Error stopping service {self.instance_name}.service:") Logger.print_error(f"{e}") raise @@ -120,8 +119,7 @@ class InstanceManager: if not self.instances: self._find_instances() - return sorted(self.instances, - key=lambda x: self._sort_instance_list(x.name)) + return sorted(self.instances, key=lambda x: self._sort_instance_list(x.name)) def _find_instances(self) -> None: prefix = self.instance_type.__name__.lower() @@ -131,12 +129,13 @@ class InstanceManager: service_list = [ os.path.join(SYSTEMD, service) for service in os.listdir(SYSTEMD) - if pattern.search(service) - and not any(s in service for s in excluded)] + if pattern.search(service) and not any(s in service for s in excluded) + ] instance_list = [ self.instance_type(name=self._get_instance_name(Path(service))) - for service in service_list] + for service in service_list + ] self.instances = instance_list diff --git a/kiauh/main.py b/kiauh/main.py index 738b036..2bab667 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -15,6 +15,6 @@ from kiauh.utils.logger import Logger def main(): try: - MainMenu().start() + MainMenu().start() except KeyboardInterrupt: Logger.print_ok("\nHappy printing!\n", prefix=False) diff --git a/kiauh/menus/advanced_menu.py b/kiauh/menus/advanced_menu.py index ddc3315..9b0ca85 100644 --- a/kiauh/menus/advanced_menu.py +++ b/kiauh/menus/advanced_menu.py @@ -17,14 +17,11 @@ from kiauh.utils.constants import COLOR_YELLOW, RESET_FORMAT class AdvancedMenu(BaseMenu): def __init__(self): - super().__init__( - header=True, - options={}, - footer_type="back" - ) + super().__init__(header=True, options={}, footer_type="back") def print_menu(self): - menu = textwrap.dedent(f""" + menu = textwrap.dedent( + f""" /=======================================================\\ | {COLOR_YELLOW}~~~~~~~~~~~~~ [ Advanced Menu ] ~~~~~~~~~~~~~{RESET_FORMAT} | |-------------------------------------------------------| @@ -36,5 +33,6 @@ class AdvancedMenu(BaseMenu): | 2) [Flash only] | | | 3) [Build + Flash] | Extras: | | 4) [Get MCU ID] | 7) [G-Code Shell Command] | - """)[1:] + """ + )[1:] print(menu, end="") diff --git a/kiauh/menus/base_menu.py b/kiauh/menus/base_menu.py index 6cb5be6..799b5af 100644 --- a/kiauh/menus/base_menu.py +++ b/kiauh/menus/base_menu.py @@ -15,8 +15,13 @@ import textwrap from abc import abstractmethod, ABC from typing import Dict, Any -from kiauh.utils.constants import COLOR_GREEN, COLOR_YELLOW, COLOR_RED, \ - COLOR_CYAN, RESET_FORMAT +from kiauh.utils.constants import ( + COLOR_GREEN, + COLOR_YELLOW, + COLOR_RED, + COLOR_CYAN, + RESET_FORMAT, +) def clear(): @@ -24,58 +29,70 @@ def clear(): def print_header(): - header = textwrap.dedent(f""" + header = textwrap.dedent( + f""" /=======================================================\\ | {COLOR_CYAN}~~~~~~~~~~~~~~~~~ [ KIAUH ] ~~~~~~~~~~~~~~~~~{RESET_FORMAT} | | {COLOR_CYAN} Klipper Installation And Update Helper {RESET_FORMAT} | | {COLOR_CYAN}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{RESET_FORMAT} | \=======================================================/ - """)[1:] + """ + )[1:] print(header, end="") def print_quit_footer(): - footer = textwrap.dedent(f""" + footer = textwrap.dedent( + f""" |-------------------------------------------------------| | {COLOR_RED}Q) Quit{RESET_FORMAT} | \=======================================================/ - """)[1:] + """ + )[1:] print(footer, end="") def print_back_footer(): - footer = textwrap.dedent(f""" + footer = textwrap.dedent( + f""" |-------------------------------------------------------| | {COLOR_GREEN}B) « Back{RESET_FORMAT} | \=======================================================/ - """)[1:] + """ + )[1:] print(footer, end="") def print_back_help_footer(): - footer = textwrap.dedent(f""" + footer = textwrap.dedent( + f""" |-------------------------------------------------------| | {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_RED}Q) Quit{RESET_FORMAT} | \=======================================================/ - """)[1:] + """ + )[1:] print(footer, end="") def print_back_quit_footer(): - footer = textwrap.dedent(f""" + footer = textwrap.dedent( + f""" |-------------------------------------------------------| | {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_YELLOW}H) Help [?]{RESET_FORMAT} | \=======================================================/ - """)[1:] + """ + )[1:] print(footer, end="") def print_back_quit_help_footer(): - footer = textwrap.dedent(f""" + footer = textwrap.dedent( + f""" |-------------------------------------------------------| | {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_RED}Q) Quit{RESET_FORMAT} | {COLOR_YELLOW}H) Help [?]{RESET_FORMAT} | \=======================================================/ - """)[1:] + """ + )[1:] print(footer, end="") @@ -86,8 +103,9 @@ class BaseMenu(ABC): BACK_QUIT_FOOTER = "back_quit" BACK_QUIT_HELP_FOOTER = "back_quit_help" - def __init__(self, options: Dict[int, Any], options_offset=0, header=True, - footer_type="quit"): + def __init__( + self, options: Dict[int, Any], options_offset=0, header=True, footer_type="quit" + ): self.options = options self.options_offset = options_offset self.header = header @@ -95,8 +113,7 @@ class BaseMenu(ABC): @abstractmethod def print_menu(self): - raise NotImplementedError( - "Subclasses must implement the print_menu method") + raise NotImplementedError("Subclasses must implement the print_menu method") def print_footer(self): footer_type_map = { @@ -104,10 +121,9 @@ class BaseMenu(ABC): self.BACK_FOOTER: print_back_footer, self.BACK_HELP_FOOTER: print_back_help_footer, self.BACK_QUIT_FOOTER: print_back_quit_footer, - self.BACK_QUIT_HELP_FOOTER: print_back_quit_help_footer + self.BACK_QUIT_HELP_FOOTER: print_back_quit_help_footer, } - footer_function = footer_type_map.get(self.footer_type, - print_quit_footer) + footer_function = footer_type_map.get(self.footer_type, print_quit_footer) footer_function() def display(self): @@ -121,9 +137,11 @@ class BaseMenu(ABC): while True: choice = input(f"{COLOR_CYAN}###### Perform action: {RESET_FORMAT}") - error_msg = f"{COLOR_RED}Invalid input.{RESET_FORMAT}" \ - if choice.isalpha() \ + error_msg = ( + f"{COLOR_RED}Invalid input.{RESET_FORMAT}" + if choice.isalpha() else f"{COLOR_RED}Invalid input. Select a number between {min(self.options)} and {max(self.options)}.{RESET_FORMAT}" + ) if choice.isdigit() and 0 <= int(choice) < len(self.options): return choice @@ -133,10 +151,12 @@ class BaseMenu(ABC): "back": ["b"], "back_help": ["b", "h"], "back_quit": ["b", "q"], - "back_quit_help": ["b", "q", "h"] + "back_quit_help": ["b", "q", "h"], } - if self.footer_type in allowed_input and choice.lower() in \ - allowed_input[self.footer_type]: + if ( + self.footer_type in allowed_input + and choice.lower() in allowed_input[self.footer_type] + ): return choice else: print(error_msg) @@ -169,7 +189,8 @@ class BaseMenu(ABC): raise NotImplementedError(f"No implementation for option {choice}") else: raise TypeError( - f"Type {type(option)} of option {choice} not of type BaseMenu or Method") + f"Type {type(option)} of option {choice} not of type BaseMenu or Method" + ) def navigate_to_submenu(self, submenu_class): submenu = submenu_class() diff --git a/kiauh/menus/install_menu.py b/kiauh/menus/install_menu.py index 91ae994..54b5148 100644 --- a/kiauh/menus/install_menu.py +++ b/kiauh/menus/install_menu.py @@ -32,13 +32,14 @@ class InstallMenu(BaseMenu): 8: self.install_obico, 9: self.install_octoeverywhere, 10: self.install_mobileraker, - 11: self.install_crowsnest + 11: self.install_crowsnest, }, - footer_type="back" + footer_type="back", ) def print_menu(self): - menu = textwrap.dedent(f""" + menu = textwrap.dedent( + f""" /=======================================================\\ | {COLOR_GREEN}~~~~~~~~~~~ [ Installation Menu ] ~~~~~~~~~~~{RESET_FORMAT} | |-------------------------------------------------------| @@ -56,7 +57,8 @@ class InstallMenu(BaseMenu): | | Webcam Streamer: | | Touchscreen GUI: | 11) [Crowsnest] | | 5) [KlipperScreen] | | - """)[1:] + """ + )[1:] print(menu, end="") def install_klipper(self): diff --git a/kiauh/menus/main_menu.py b/kiauh/menus/main_menu.py index 97a029a..9cffebb 100644 --- a/kiauh/menus/main_menu.py +++ b/kiauh/menus/main_menu.py @@ -31,13 +31,14 @@ class MainMenu(BaseMenu): 3: RemoveMenu, 4: AdvancedMenu, 5: None, - 6: SettingsMenu + 6: SettingsMenu, }, - footer_type="quit" + footer_type="quit", ) def print_menu(self): - menu = textwrap.dedent(f""" + menu = textwrap.dedent( + f""" /=======================================================\\ | {COLOR_CYAN}~~~~~~~~~~~~~~~ [ Main Menu ] ~~~~~~~~~~~~~~~{RESET_FORMAT} | |-------------------------------------------------------| @@ -58,7 +59,8 @@ class MainMenu(BaseMenu): | | OctoEverywhere: | |-------------------------------------------------------| | {COLOR_CYAN}KIAUH v6.0.0{RESET_FORMAT} | Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT} | - """)[1:] + """ + )[1:] print(menu, end="") def test(self): diff --git a/kiauh/menus/remove_menu.py b/kiauh/menus/remove_menu.py index 9b414fa..1fb2492 100644 --- a/kiauh/menus/remove_menu.py +++ b/kiauh/menus/remove_menu.py @@ -38,11 +38,12 @@ class RemoveMenu(BaseMenu): 14: self.remove_mobileraker, 15: self.remove_nginx, }, - footer_type="back" + footer_type="back", ) def print_menu(self): - menu = textwrap.dedent(f""" + menu = textwrap.dedent( + f""" /=======================================================\\ | {COLOR_RED}~~~~~~~~~~~~~~ [ Remove Menu ] ~~~~~~~~~~~~~~{RESET_FORMAT} | |-------------------------------------------------------| @@ -60,7 +61,8 @@ class RemoveMenu(BaseMenu): | | 14) [Mobileraker] | | Touchscreen GUI: | 15) [NGINX] | | 7) [KlipperScreen] | | - """)[1:] + """ + )[1:] print(menu, end="") def remove_klipper(self): diff --git a/kiauh/menus/settings_menu.py b/kiauh/menus/settings_menu.py index df2f6c4..dcef3df 100644 --- a/kiauh/menus/settings_menu.py +++ b/kiauh/menus/settings_menu.py @@ -15,10 +15,7 @@ from kiauh.menus.base_menu import BaseMenu # noinspection PyMethodMayBeStatic class SettingsMenu(BaseMenu): def __init__(self): - super().__init__( - header=True, - options={} - ) + super().__init__(header=True, options={}) def print_menu(self): print("self") diff --git a/kiauh/menus/update_menu.py b/kiauh/menus/update_menu.py index f95865a..0e9a78a 100644 --- a/kiauh/menus/update_menu.py +++ b/kiauh/menus/update_menu.py @@ -35,11 +35,12 @@ class UpdateMenu(BaseMenu): 11: self.update_crowsnest, 12: self.upgrade_system_packages, }, - footer_type="back" + footer_type="back", ) def print_menu(self): - menu = textwrap.dedent(f""" + menu = textwrap.dedent( + f""" /=======================================================\\ | {COLOR_GREEN}~~~~~~~~~~~~~~ [ Update Menu ] ~~~~~~~~~~~~~~{RESET_FORMAT} | |-------------------------------------------------------| @@ -65,7 +66,8 @@ class UpdateMenu(BaseMenu): | 11) [Crowsnest] | | | | |-----------------------------| | 12) [System] | | | - """)[1:] + """ + )[1:] print(menu, end="") def update_all(self): diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 81f0ceb..e21d6a0 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -10,7 +10,6 @@ # ======================================================================= # import os -import pwd import shutil import subprocess from pathlib import Path @@ -29,10 +28,12 @@ class Klipper(BaseInstance): return ["None", "mcu"] def __init__(self, name: str): - super().__init__(name=name, - prefix="klipper", - user=CURRENT_USER, - data_dir_name=self._get_data_dir_from_name(name)) + super().__init__( + name=name, + prefix="klipper", + user=CURRENT_USER, + data_dir_name=self._get_data_dir_from_name(name), + ) self.klipper_dir = KLIPPER_DIR self.env_dir = KLIPPER_ENV_DIR self.cfg_file = f"{self.cfg_dir}/printer.cfg" @@ -43,26 +44,35 @@ class Klipper(BaseInstance): def create(self) -> None: Logger.print_info("Creating Klipper Instance") module_path = os.path.dirname(os.path.abspath(__file__)) - service_template_path = os.path.join(module_path, "res", - "klipper.service") + service_template_path = os.path.join(module_path, "res", "klipper.service") env_template_file_path = os.path.join(module_path, "res", "klipper.env") service_file_name = self.get_service_file_name(extension=True) service_file_target = f"{SYSTEMD}/{service_file_name}" env_file_target = os.path.abspath(f"{self.sysd_dir}/klipper.env") # create folder structure - dirs = [self.data_dir, self.cfg_dir, self.log_dir, - self.comms_dir, self.sysd_dir] + dirs = [ + self.data_dir, + self.cfg_dir, + self.log_dir, + self.comms_dir, + self.sysd_dir, + ] for _dir in dirs: create_directory(Path(_dir)) try: # writing the klipper service file (requires sudo!) - service_content = self._prep_service_file(service_template_path, - env_file_target) + service_content = self._prep_service_file( + service_template_path, env_file_target + ) command = ["sudo", "tee", service_file_target] - subprocess.run(command, input=service_content.encode(), - stdout=subprocess.DEVNULL, check=True) + subprocess.run( + command, + input=service_content.encode(), + stdout=subprocess.DEVNULL, + check=True, + ) Logger.print_ok(f"Service file created: {service_file_target}") # writing the klipper.env file @@ -73,11 +83,11 @@ class Klipper(BaseInstance): except subprocess.CalledProcessError as e: Logger.print_error( - f"Error creating service file {service_file_target}: {e}") + f"Error creating service file {service_file_target}: {e}" + ) raise except OSError as e: - Logger.print_error( - f"Error creating env file {env_file_target}: {e}") + Logger.print_error(f"Error creating env file {env_file_target}: {e}") raise def read(self) -> None: @@ -118,7 +128,7 @@ class Klipper(BaseInstance): Logger.print_ok("Directories successfully deleted.") def get_service_file_name(self, extension=False) -> str: - name = self.prefix if self.name is None else self.prefix + '-' + self.name + name = self.prefix if self.name is None else self.prefix + "-" + self.name return name if not extension else f"{name}.service" def _get_service_file_path(self): @@ -138,11 +148,11 @@ class Klipper(BaseInstance): template_content = template_file.read() except FileNotFoundError: Logger.print_error( - f"Unable to open {service_template_path} - File not found") + f"Unable to open {service_template_path} - File not found" + ) raise service_content = template_content.replace("%USER%", self.user) - service_content = service_content.replace("%KLIPPER_DIR%", - self.klipper_dir) + service_content = service_content.replace("%KLIPPER_DIR%", self.klipper_dir) service_content = service_content.replace("%ENV%", self.env_dir) service_content = service_content.replace("%ENV_FILE%", env_file_path) return service_content @@ -153,10 +163,12 @@ class Klipper(BaseInstance): env_template_file_content = env_file.read() except FileNotFoundError: Logger.print_error( - f"Unable to open {env_template_file_path} - File not found") + f"Unable to open {env_template_file_path} - File not found" + ) raise - env_file_content = env_template_file_content.replace("%KLIPPER_DIR%", - self.klipper_dir) + env_file_content = env_template_file_content.replace( + "%KLIPPER_DIR%", self.klipper_dir + ) env_file_content = env_file_content.replace("%CFG%", self.cfg_file) env_file_content = env_file_content.replace("%SERIAL%", self.serial) env_file_content = env_file_content.replace("%LOG%", self.log) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 652a9f5..e0a88cd 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -19,15 +19,27 @@ from typing import Optional, List, Union from kiauh.instance_manager.instance_manager import InstanceManager from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.klipper.klipper_utils import print_instance_overview, print_missing_usergroup_dialog +from kiauh.modules.klipper.klipper_utils import ( + print_instance_overview, + print_missing_usergroup_dialog, +) from kiauh.utils.constants import CURRENT_USER, KLIPPER_DIR, KLIPPER_ENV_DIR -from kiauh.utils.input_utils import get_user_confirm, get_user_number_input, \ - get_user_string_input, get_user_selection_input +from kiauh.utils.input_utils import ( + get_user_confirm, + get_user_number_input, + get_user_string_input, + get_user_selection_input, +) from kiauh.utils.logger import Logger -from kiauh.utils.system_utils import parse_packages_from_file, \ - clone_repo, create_python_venv, \ - install_python_requirements, update_system_package_lists, \ - install_system_packages, mask_system_service +from kiauh.utils.system_utils import ( + parse_packages_from_file, + clone_repo, + create_python_venv, + install_python_requirements, + update_system_package_lists, + install_system_packages, + mask_system_service, +) def run_klipper_setup(install: bool) -> None: @@ -81,8 +93,8 @@ def install_klipper(instance_manager: InstanceManager) -> None: instance_list = instance_manager.get_instances() if_adding = " additional" if len(instance_list) > 0 else "" install_count = get_user_number_input( - f"Number of{if_adding} Klipper instances to set up", - 1, default=1) + f"Number of{if_adding} Klipper instances to set up", 1, default=1 + ) instance_names = set_instance_names(instance_list, install_count) @@ -132,8 +144,7 @@ def install_klipper_packages(klipper_dir: Path) -> None: install_system_packages(packages) -def set_instance_names(instance_list, install_count: int) -> List[ - Union[str, None]]: +def set_instance_names(instance_list, install_count: int) -> List[Union[str, None]]: instance_count = len(instance_list) # default single instance install @@ -141,9 +152,11 @@ def set_instance_names(instance_list, install_count: int) -> List[ return [None] # new multi instance install - elif ((instance_count == 0 and install_count > 1) - # or convert single instance install to multi instance install - or (instance_count == 1 and install_count >= 1)): + elif ( + (instance_count == 0 and install_count > 1) + # or convert single instance install to multi instance install + or (instance_count == 1 and install_count >= 1) + ): if get_user_confirm("Assign custom names?", False): return assign_custom_names(instance_count, install_count, None) else: @@ -153,8 +166,7 @@ def set_instance_names(instance_list, install_count: int) -> List[ # existing multi instance install elif instance_count > 1: if has_custom_names(instance_list): - return assign_custom_names(instance_count, install_count, - instance_list) + return assign_custom_names(instance_count, install_count, instance_list) else: start = get_highest_index(instance_list) + 1 _range = range(start, start + install_count) @@ -170,8 +182,9 @@ def has_custom_names(instance_list: List[Klipper]) -> bool: return False -def assign_custom_names(instance_count: int, install_count: int, - instance_list: Optional[List[Klipper]]) -> List[str]: +def assign_custom_names( + instance_count: int, install_count: int, instance_list: Optional[List[Klipper]] +) -> List[str]: instance_names = [] exclude = Klipper.blacklist() @@ -190,7 +203,7 @@ def assign_custom_names(instance_count: int, install_count: int, def get_highest_index(instance_list: List[Klipper]) -> int: - indices = [int(instance.name.split('-')[-1]) for instance in instance_list] + indices = [int(instance.name.split("-")[-1]) for instance in instance_list] return max(indices) @@ -209,14 +222,12 @@ def remove_single_instance(instance_manager: InstanceManager) -> None: def remove_multi_instance(instance_manager: InstanceManager) -> None: instance_list = instance_manager.get_instances() - print_instance_overview(instance_list, show_index=True, - show_select_all=True) + print_instance_overview(instance_list, show_index=True, show_select_all=True) options = [str(i) for i in range(len(instance_list))] options.extend(["a", "A", "b", "B"]) - selection = get_user_selection_input( - "Select Klipper instance to remove", options) + selection = get_user_selection_input("Select Klipper instance to remove", options) print(selection) if selection == "b".lower(): @@ -231,7 +242,8 @@ def remove_multi_instance(instance_manager: InstanceManager) -> None: else: instance = instance_list[int(selection)] Logger.print_info( - f"Removing Klipper instance: {instance.get_service_file_name()}") + f"Removing Klipper instance: {instance.get_service_file_name()}" + ) instance_manager.set_current_instance(instance) instance_manager.stop_instance() instance_manager.disable_instance() @@ -254,7 +266,9 @@ def check_user_groups(): print_missing_usergroup_dialog(missing_groups) if not get_user_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): - Logger.warn("Skipped adding user to required groups. You might encounter issues.") + Logger.warn( + "Skipped adding user to required groups. You might encounter issues." + ) return try: @@ -267,13 +281,19 @@ def check_user_groups(): Logger.print_error(f"Unable to add user to usergroups: {e}") raise - Logger.print_warn("Remember to relog/restart this machine for the group(s) to be applied!") + Logger.print_warn( + "Remember to relog/restart this machine for the group(s) to be applied!" + ) def handle_disruptive_system_packages() -> None: services = [] - brltty_status = subprocess.run(["systemctl", "is-enabled", "brltty"], capture_output=True, text=True) - modem_manager_status = subprocess.run(["systemctl", "is-enabled", "ModemManager"], capture_output=True, text=True) + brltty_status = subprocess.run( + ["systemctl", "is-enabled", "brltty"], capture_output=True, text=True + ) + modem_manager_status = subprocess.run( + ["systemctl", "is-enabled", "ModemManager"], capture_output=True, text=True + ) if "enabled" in brltty_status.stdout: services.append("brltty") @@ -282,13 +302,17 @@ def handle_disruptive_system_packages() -> None: for service in services if services else []: try: - Logger.print_info(f"{service} service detected! Masking {service} service ...") + Logger.print_info( + f"{service} service detected! Masking {service} service ..." + ) mask_system_service(service) Logger.print_ok(f"{service} service masked!") except subprocess.CalledProcessError: - warn_msg = textwrap.dedent(f""" + warn_msg = textwrap.dedent( + f""" KIAUH was unable to mask the {service} system service. Please fix the problem manually. Otherwise, this may have undesirable effects on the operation of Klipper. - """)[1:] + """ + )[1:] Logger.print_warn(warn_msg) diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 445e3ce..0d42b86 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -13,12 +13,12 @@ from typing import List from kiauh.instance_manager.base_instance import BaseInstance from kiauh.menus.base_menu import print_back_footer -from kiauh.utils.constants import COLOR_GREEN, COLOR_CYAN, COLOR_YELLOW, \ - RESET_FORMAT +from kiauh.utils.constants import COLOR_GREEN, COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT -def print_instance_overview(instances: List[BaseInstance], show_index=False, - show_select_all=False): +def print_instance_overview( + instances: List[BaseInstance], show_index=False, show_select_all=False +): headline = f"{COLOR_GREEN}The following Klipper instances were found:{RESET_FORMAT}" print("/=======================================================\\") @@ -38,13 +38,20 @@ def print_instance_overview(instances: List[BaseInstance], show_index=False, print_back_footer() + def print_missing_usergroup_dialog(missing_groups) -> None: print("/=======================================================\\") - print(f"| {COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT} |") + print( + f"| {COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT} |" + ) if "tty" in missing_groups: - print(f"| {COLOR_CYAN}● tty{RESET_FORMAT} |") + print( + f"| {COLOR_CYAN}● tty{RESET_FORMAT} |" + ) if "dialout" in missing_groups: - print(f"| {COLOR_CYAN}● dialout{RESET_FORMAT} |") + print( + f"| {COLOR_CYAN}● dialout{RESET_FORMAT} |" + ) print("| |") print("| It is possible that you won't be able to successfully |") print("| connect and/or flash the controller board without |") @@ -52,6 +59,10 @@ def print_missing_usergroup_dialog(missing_groups) -> None: print("| If you want to add the current user to the group(s) |") print("| listed above, answer with 'Y'. Else skip with 'n'. |") print("| |") - print(f"| {COLOR_YELLOW}INFO:{RESET_FORMAT} |") - print(f"| {COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT} |") + print( + f"| {COLOR_YELLOW}INFO:{RESET_FORMAT} |" + ) + print( + f"| {COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT} |" + ) print("\\=======================================================/") diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index c6327d6..aac7a48 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -11,8 +11,8 @@ from typing import Optional, List -from kiauh.utils.logger import Logger from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT +from kiauh.utils.logger import Logger def get_user_confirm(question: str, default_choice=True) -> bool: @@ -30,7 +30,8 @@ def get_user_confirm(question: str, default_choice=True) -> bool: choice = ( input(f"{COLOR_CYAN}###### {question} {def_choice} {RESET_FORMAT}") .strip() - .lower()) + .lower() + ) if choice in options_confirm: return True @@ -40,8 +41,9 @@ def get_user_confirm(question: str, default_choice=True) -> bool: Logger.print_error("Invalid choice. Please select 'y' or 'n'.") -def get_user_number_input(question: str, min_count: int, max_count=None, - default=None) -> int: +def get_user_number_input( + question: str, min_count: int, max_count=None, default=None +) -> int: _question = question + f" (default={default})" if default else question _question = f"{COLOR_CYAN}###### {_question}: {RESET_FORMAT}" while True: @@ -65,8 +67,7 @@ def get_user_number_input(question: str, min_count: int, max_count=None, def get_user_string_input(question: str, exclude=Optional[List]) -> str: while True: - _input = (input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}") - .strip()) + _input = input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}").strip() if _input.isalnum() and _input not in exclude: return _input @@ -78,8 +79,7 @@ def get_user_string_input(question: str, exclude=Optional[List]) -> str: def get_user_selection_input(question: str, option_list: List) -> str: while True: - _input = (input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}") - .strip()) + _input = input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}").strip() if _input in option_list: return _input diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index f6adc0d..da54fb7 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -9,12 +9,16 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from kiauh.utils.constants import COLOR_GREEN, COLOR_YELLOW, COLOR_RED, \ - COLOR_MAGENTA, RESET_FORMAT +from kiauh.utils.constants import ( + COLOR_GREEN, + COLOR_YELLOW, + COLOR_RED, + COLOR_MAGENTA, + RESET_FORMAT, +) class Logger: - @staticmethod def info(msg): # log to kiauh.log diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index af85d24..7765a56 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -57,7 +57,8 @@ def clone_repo(target_dir: Path, url: str, branch: str) -> None: print("Error cloning repository:", e.output.decode()) else: overwrite_target = get_user_confirm( - "Target directory already exists. Overwrite?") + "Target directory already exists. Overwrite?" + ) if overwrite_target: try: shutil.rmtree(target_dir) @@ -71,13 +72,13 @@ def clone_repo(target_dir: Path, url: str, branch: str) -> None: def parse_packages_from_file(source_file) -> List[str]: packages = [] print("Reading dependencies...") - with open(source_file, 'r') as file: + with open(source_file, "r") as file: for line in file: line = line.strip() if line.startswith("PKGLIST="): - line = line.replace("\"", "") + line = line.replace('"', "") line = line.replace("PKGLIST=", "") - line = line.replace('${PKGLIST}', '') + line = line.replace("${PKGLIST}", "") packages.extend(line.split()) return packages @@ -97,16 +98,15 @@ def create_python_venv(target: Path) -> None: except subprocess.CalledProcessError as e: print("Error setting up virtualenv:", e.output.decode()) else: - overwrite_venv = get_user_confirm( - "Virtualenv already exists. Re-create?") + overwrite_venv = get_user_confirm("Virtualenv already exists. Re-create?") if overwrite_venv: try: shutil.rmtree(target) create_python_venv(target) except OSError as e: Logger.print_error( - f"Error removing existing virtualenv: {e.strerror}", - False) + f"Error removing existing virtualenv: {e.strerror}", False + ) else: print("Skipping re-creation of virtualenv ...") @@ -144,10 +144,7 @@ def install_python_requirements(target: Path, requirements: Path) -> None: def update_system_package_lists(silent: bool, rls_info_change=False) -> None: cache_mtime = 0 - cache_files = [ - "/var/lib/apt/periodic/update-success-stamp", - "/var/lib/apt/lists" - ] + cache_files = ["/var/lib/apt/periodic/update-success-stamp", "/var/lib/apt/lists"] for cache_file in cache_files: if Path(cache_file).exists(): cache_mtime = max(cache_mtime, os.path.getmtime(cache_file)) @@ -196,8 +193,7 @@ def create_directory(_dir: Path) -> None: os.makedirs(_dir, exist_ok=True) Logger.print_ok("Directory created!") else: - Logger.print_info( - f"Directory already exists: {_dir}\nSkip creation ...") + Logger.print_info(f"Directory already exists: {_dir}\nSkip creation ...") except OSError as e: Logger.print_error(f"Error creating folder: {e}") raise @@ -208,5 +204,7 @@ def mask_system_service(service_name: str) -> None: command = ["sudo", "systemctl", "mask", service_name] subprocess.run(command, stderr=subprocess.PIPE, check=True) except subprocess.CalledProcessError as e: - Logger.print_error(f"Unable to mask system service {service_name}: {e.stderr.decode()}") + Logger.print_error( + f"Unable to mask system service {service_name}: {e.stderr.decode()}" + ) raise diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..be1fddf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[tool.black] +line-length = 88 +target-version = ['py38'] +include = '\.pyi?$' +exclude = ''' +( + \.git/ + | \.github/ + | docs/ + | resources/ + | scripts/ +) +''' From 1e0c74b54989c958dc678b453122e66fe28512b7 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 29 Oct 2023 01:29:27 +0200 Subject: [PATCH 006/296] style: rename input functions Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 20 ++++++++++---------- kiauh/utils/input_utils.py | 8 ++++---- kiauh/utils/system_utils.py | 8 +++----- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index e0a88cd..9098d2e 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -25,10 +25,10 @@ from kiauh.modules.klipper.klipper_utils import ( ) from kiauh.utils.constants import CURRENT_USER, KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.utils.input_utils import ( - get_user_confirm, - get_user_number_input, - get_user_string_input, - get_user_selection_input, + get_confirm, + get_number_input, + get_string_input, + get_selection_input, ) from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( @@ -83,7 +83,7 @@ def handle_existing_instances(instance_manager: InstanceManager) -> bool: if instance_count > 0: print_instance_overview(instance_list) - if not get_user_confirm("Add new instances?"): + if not get_confirm("Add new instances?"): return False return True @@ -92,7 +92,7 @@ def handle_existing_instances(instance_manager: InstanceManager) -> bool: def install_klipper(instance_manager: InstanceManager) -> None: instance_list = instance_manager.get_instances() if_adding = " additional" if len(instance_list) > 0 else "" - install_count = get_user_number_input( + install_count = get_number_input( f"Number of{if_adding} Klipper instances to set up", 1, default=1 ) @@ -157,7 +157,7 @@ def set_instance_names(instance_list, install_count: int) -> List[Union[str, Non # or convert single instance install to multi instance install or (instance_count == 1 and install_count >= 1) ): - if get_user_confirm("Assign custom names?", False): + if get_confirm("Assign custom names?", False): return assign_custom_names(instance_count, install_count, None) else: _range = range(1, install_count + 1) @@ -195,7 +195,7 @@ def assign_custom_names( for i in range(instance_count + install_count): question = f"Enter name for instance {i + 1}" - name = get_user_string_input(question, exclude=exclude) + name = get_string_input(question, exclude=exclude) instance_names.append(name) exclude.append(name) @@ -227,7 +227,7 @@ def remove_multi_instance(instance_manager: InstanceManager) -> None: options = [str(i) for i in range(len(instance_list))] options.extend(["a", "A", "b", "B"]) - selection = get_user_selection_input("Select Klipper instance to remove", options) + selection = get_selection_input("Select Klipper instance to remove", options) print(selection) if selection == "b".lower(): @@ -265,7 +265,7 @@ def check_user_groups(): return print_missing_usergroup_dialog(missing_groups) - if not get_user_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): + if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): Logger.warn( "Skipped adding user to required groups. You might encounter issues." ) diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index aac7a48..eaf97f6 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -15,7 +15,7 @@ from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT from kiauh.utils.logger import Logger -def get_user_confirm(question: str, default_choice=True) -> bool: +def get_confirm(question: str, default_choice=True) -> bool: options_confirm = ["y", "yes"] options_decline = ["n", "no"] @@ -41,7 +41,7 @@ def get_user_confirm(question: str, default_choice=True) -> bool: Logger.print_error("Invalid choice. Please select 'y' or 'n'.") -def get_user_number_input( +def get_number_input( question: str, min_count: int, max_count=None, default=None ) -> int: _question = question + f" (default={default})" if default else question @@ -65,7 +65,7 @@ def get_user_number_input( Logger.print_error("Invalid choice. Please select a valid number.") -def get_user_string_input(question: str, exclude=Optional[List]) -> str: +def get_string_input(question: str, exclude=Optional[List]) -> str: while True: _input = input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}").strip() @@ -77,7 +77,7 @@ def get_user_string_input(question: str, exclude=Optional[List]) -> str: Logger.print_error("This value is already in use/reserved.") -def get_user_selection_input(question: str, option_list: List) -> str: +def get_selection_input(question: str, option_list: List) -> str: while True: _input = input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}").strip() diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 7765a56..6a9bde6 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -18,7 +18,7 @@ from pathlib import Path from typing import List from kiauh.utils.constants import COLOR_RED, RESET_FORMAT -from kiauh.utils.input_utils import get_user_confirm +from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger @@ -56,9 +56,7 @@ def clone_repo(target_dir: Path, url: str, branch: str) -> None: except subprocess.CalledProcessError as e: print("Error cloning repository:", e.output.decode()) else: - overwrite_target = get_user_confirm( - "Target directory already exists. Overwrite?" - ) + overwrite_target = get_confirm("Target directory already exists. Overwrite?") if overwrite_target: try: shutil.rmtree(target_dir) @@ -98,7 +96,7 @@ def create_python_venv(target: Path) -> None: except subprocess.CalledProcessError as e: print("Error setting up virtualenv:", e.output.decode()) else: - overwrite_venv = get_user_confirm("Virtualenv already exists. Re-create?") + overwrite_venv = get_confirm("Virtualenv already exists. Re-create?") if overwrite_venv: try: shutil.rmtree(target) From 623bd7553bb862d32907bbe077b261eb9cb0eabd Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 29 Oct 2023 13:25:21 +0100 Subject: [PATCH 007/296] feat(RepoManager): implement RepoManager Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 11 ++- kiauh/repo_manager/__init__.py | 0 kiauh/repo_manager/repo_manager.py | 105 +++++++++++++++++++++++++ kiauh/utils/system_utils.py | 25 ------ 4 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 kiauh/repo_manager/__init__.py create mode 100644 kiauh/repo_manager/repo_manager.py diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 9098d2e..ec9eb81 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -23,6 +23,7 @@ from kiauh.modules.klipper.klipper_utils import ( print_instance_overview, print_missing_usergroup_dialog, ) +from kiauh.repo_manager.repo_manager import RepoManager from kiauh.utils.constants import CURRENT_USER, KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.utils.input_utils import ( get_confirm, @@ -33,7 +34,6 @@ from kiauh.utils.input_utils import ( from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( parse_packages_from_file, - clone_repo, create_python_venv, install_python_requirements, update_system_package_lists, @@ -119,9 +119,12 @@ def install_klipper(instance_manager: InstanceManager) -> None: def setup_klipper_prerequesites() -> None: # clone klipper TODO: read branch and url from json to allow forks - url = "https://github.com/Klipper3D/klipper" - branch = "master" - clone_repo(Path(KLIPPER_DIR), url, branch) + repo_manager = RepoManager( + repo="https://github.com/Klipper3D/klipper", + branch="master", + target_dir=KLIPPER_DIR, + ) + repo_manager.clone_repo() # install klipper dependencies and create python virtualenv install_klipper_packages(Path(KLIPPER_DIR)) diff --git a/kiauh/repo_manager/__init__.py b/kiauh/repo_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/repo_manager/repo_manager.py b/kiauh/repo_manager/repo_manager.py new file mode 100644 index 0000000..dfffe31 --- /dev/null +++ b/kiauh/repo_manager/repo_manager.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import shutil +import subprocess +from pathlib import Path +from typing import Union + +from kiauh.utils.input_utils import get_confirm +from kiauh.utils.logger import Logger + + +# noinspection PyMethodMayBeStatic +class RepoManager: + def __init__(self, repo: str, branch: str, target_dir: str): + self._repo = repo + self._branch = branch + self._method = self._get_method() + self._target_dir = target_dir + + @property + def repo(self) -> str: + return self._repo + + @repo.setter + def repo(self, value) -> None: + self._repo = value + + @property + def branch(self) -> str: + return self._branch + + @branch.setter + def branch(self, value) -> None: + self._branch = value + + @property + def method(self) -> str: + return self._method + + @method.setter + def method(self, value) -> None: + self._method = value + + @property + def target_dir(self) -> str: + return self._target_dir + + @target_dir.setter + def target_dir(self, value) -> None: + self._target_dir = value + + def clone_repo(self): + log = f"Cloning repository from '{self.repo}' with method '{self.method}'" + Logger.print_info(log) + try: + question = "Target directory already exists. Overwrite?" + if Path(self.target_dir).exists() and get_confirm(question): + shutil.rmtree(self.target_dir) + else: + Logger.print_info("Skipping re-clone of repository ...") + return + + self._clone() + self._checkout() + except subprocess.CalledProcessError: + log = "An unexpected error occured during cloning of the repository." + Logger.print_error(log) + return + except OSError as e: + Logger.print_error(f"Error removing existing repository: {e.strerror}") + return + + def _clone(self): + try: + command = ["git", "clone", f"{self.repo}"] + subprocess.run(command, check=True) + + Logger.print_ok("Clone successfull!") + except subprocess.CalledProcessError as e: + log = f"Error cloning repository {self.repo}: {e.stderr.decode()}" + Logger.print_error(log) + raise + + def _checkout(self): + try: + command = ["git", "checkout", f"{self.branch}"] + subprocess.run(command, cwd=self.target_dir, check=True) + + Logger.print_ok("Checkout successfull!") + except subprocess.CalledProcessError as e: + log = f"Error checking out branch {self.branch}: {e.stderr.decode()}" + Logger.print_error(log) + raise + + def _get_method(self) -> str: + return "ssh" if self.repo.startswith("git") else "https" diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 6a9bde6..73ad1b0 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -42,31 +42,6 @@ def kill(opt_err_msg=None) -> None: sys.exit(1) -def clone_repo(target_dir: Path, url: str, branch: str) -> None: - Logger.print_info(f"Cloning repository from {url}") - if not target_dir.exists(): - try: - command = ["git", "clone", f"{url}"] - subprocess.run(command, check=True) - - command = ["git", "checkout", f"{branch}"] - subprocess.run(command, cwd=target_dir, check=True) - - Logger.print_ok("Clone successfull!") - except subprocess.CalledProcessError as e: - print("Error cloning repository:", e.output.decode()) - else: - overwrite_target = get_confirm("Target directory already exists. Overwrite?") - if overwrite_target: - try: - shutil.rmtree(target_dir) - clone_repo(target_dir, url, branch) - except OSError as e: - print("Error removing existing repository:", e.strerror) - else: - print("Skipping re-clone of repository ...") - - def parse_packages_from_file(source_file) -> List[str]: packages = [] print("Reading dependencies...") From 09e874214b6accae9dc1e6e77a3afd23d3f7302d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 29 Oct 2023 17:19:26 +0100 Subject: [PATCH 008/296] feat(ConfigManager): implement ConfigManager Signed-off-by: Dominik Willner --- .gitignore | 4 +- kiauh.cfg.example | 12 +++++ kiauh/config_manager/__init__.py | 0 kiauh/config_manager/config_manager.py | 61 ++++++++++++++++++++++++++ kiauh/modules/klipper/klipper_setup.py | 15 +++++-- kiauh/repo_manager/repo_manager.py | 14 +++--- klipper_repos.txt.example | 18 -------- 7 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 kiauh.cfg.example create mode 100644 kiauh/config_manager/__init__.py create mode 100644 kiauh/config_manager/config_manager.py delete mode 100644 klipper_repos.txt.example diff --git a/.gitignore b/.gitignore index 83a9457..8105baa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,4 @@ .pytest_cache .kiauh-env *.code-workspace -klipper_repos.txt -klipper_repos.json - +kiauh.cfg diff --git a/kiauh.cfg.example b/kiauh.cfg.example new file mode 100644 index 0000000..0ba34db --- /dev/null +++ b/kiauh.cfg.example @@ -0,0 +1,12 @@ +[kiauh] +backup_before_update: False + +[klipper] +repository_url: https://github.com/Klipper3d/klipper +branch: master +method: https + +[moonraker] +repository_url: https://github.com/Klipper3d/klipper +branch: master +method: https diff --git a/kiauh/config_manager/__init__.py b/kiauh/config_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/config_manager/config_manager.py b/kiauh/config_manager/config_manager.py new file mode 100644 index 0000000..0f80647 --- /dev/null +++ b/kiauh/config_manager/config_manager.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import os +import configparser +from pathlib import Path +from typing import Union + +from kiauh.utils.logger import Logger + + +# noinspection PyMethodMayBeStatic +class ConfigManager: + def __init__(self): + self.config_file = self._get_cfg_location() + self.config = configparser.ConfigParser() + + def read_config(self) -> None: + if not self.config_file: + Logger.print_error("Unable to read config file. File not found.") + return + + self.config.read_file(open(self.config_file, "r")) + + def write_config(self) -> None: + with open(self.config_file, "w") as cfg: + self.config.write(cfg) + + def get_value(self, section: str, key: str) -> Union[str, None]: + if not self.config.has_section(section): + log = f"Section not defined. Unable to read section: [{section}]." + Logger.print_error(log) + return None + + if not self.config.has_option(section, key): + log = f"Option not defined in section [{section}]. Unable to read option: '{key}'." + Logger.print_error(log) + return None + + return self.config.get(section, key) + + def set_value(self, section: str, key: str, value: str): + self.config.set(section, key, value) + + def check_config_exists(self) -> bool: + return True if self._get_cfg_location() else False + + def _get_cfg_location(self) -> str: + current_dir = os.path.dirname(os.path.abspath(__file__)) + project_dir = os.path.dirname(os.path.dirname(current_dir)) + cfg_path = os.path.join(project_dir, "kiauh.cfg") + + return cfg_path if Path(cfg_path).exists() else None diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index ec9eb81..865d2bf 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -17,6 +17,7 @@ import textwrap from pathlib import Path from typing import Optional, List, Union +from kiauh.config_manager.config_manager import ConfigManager from kiauh.instance_manager.instance_manager import InstanceManager from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.klipper.klipper_utils import ( @@ -118,10 +119,18 @@ def install_klipper(instance_manager: InstanceManager) -> None: def setup_klipper_prerequesites() -> None: - # clone klipper TODO: read branch and url from json to allow forks + cm = ConfigManager() + cm.read_config() + + repo = ( + cm.get_value("klipper", "repository_url") + or "https://github.com/Klipper3D/klipper" + ) + branch = cm.get_value("klipper", "branch") or "master" + repo_manager = RepoManager( - repo="https://github.com/Klipper3D/klipper", - branch="master", + repo=repo, + branch=branch, target_dir=KLIPPER_DIR, ) repo_manager.clone_repo() diff --git a/kiauh/repo_manager/repo_manager.py b/kiauh/repo_manager/repo_manager.py index dfffe31..b1501d2 100644 --- a/kiauh/repo_manager/repo_manager.py +++ b/kiauh/repo_manager/repo_manager.py @@ -9,10 +9,9 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import os import shutil import subprocess -from pathlib import Path -from typing import Union from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger @@ -62,12 +61,11 @@ class RepoManager: log = f"Cloning repository from '{self.repo}' with method '{self.method}'" Logger.print_info(log) try: - question = "Target directory already exists. Overwrite?" - if Path(self.target_dir).exists() and get_confirm(question): + if os.path.exists(self.target_dir): + if not get_confirm("Target directory already exists. Overwrite?"): + Logger.print_info("Skipping re-clone of repository ...") + return shutil.rmtree(self.target_dir) - else: - Logger.print_info("Skipping re-clone of repository ...") - return self._clone() self._checkout() @@ -81,7 +79,7 @@ class RepoManager: def _clone(self): try: - command = ["git", "clone", f"{self.repo}"] + command = ["git", "clone", self.repo, self.target_dir] subprocess.run(command, check=True) Logger.print_ok("Clone successfull!") diff --git a/klipper_repos.txt.example b/klipper_repos.txt.example deleted file mode 100644 index 6cc3393..0000000 --- a/klipper_repos.txt.example +++ /dev/null @@ -1,18 +0,0 @@ -# This file acts as an example file. -# -# 1) Make a copy of this file and rename it to 'klipper_repos.txt' -# 2) Add your custom Klipper repository to the bottom of that copy -# 3) Save the file -# -# Back in KIAUH you can now go into -> [Settings] and use action '2' to set a different Klipper repository -# -# Make sure to always separate the repository and the branch with a ','. -# , -> https://github.com/Klipper3d/klipper,master -# If you omit a branch, it will always default to 'master' -# -# You are allowed to omit the 'https://github.com/' part of the repository URL -# Down below are now a few examples of what is considered as valid: -https://github.com/Klipper3d/klipper,master -https://github.com/Klipper3d/klipper -Klipper3d/klipper,master -Klipper3d/klipper From c9e8c4807e295c720aa79e54add06d54e8ea6ae0 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Tue, 31 Oct 2023 20:54:44 +0100 Subject: [PATCH 009/296] feat(klipper): convert single to multi instance Signed-off-by: Dominik Willner --- kiauh/instance_manager/base_instance.py | 8 - kiauh/modules/klipper/klipper.py | 84 ++++----- kiauh/modules/klipper/klipper_dialogs.py | 98 +++++++++++ kiauh/modules/klipper/klipper_setup.py | 182 +++++-------------- kiauh/modules/klipper/klipper_utils.py | 212 ++++++++++++++++++----- kiauh/utils/input_utils.py | 31 ++-- 6 files changed, 374 insertions(+), 241 deletions(-) create mode 100644 kiauh/modules/klipper/klipper_dialogs.py diff --git a/kiauh/instance_manager/base_instance.py b/kiauh/instance_manager/base_instance.py index 169ff74..3dcda97 100644 --- a/kiauh/instance_manager/base_instance.py +++ b/kiauh/instance_manager/base_instance.py @@ -72,14 +72,6 @@ class BaseInstance(ABC): def create(self) -> None: raise NotImplementedError("Subclasses must implement the create method") - @abstractmethod - def read(self) -> None: - raise NotImplementedError("Subclasses must implement the read method") - - @abstractmethod - def update(self) -> None: - raise NotImplementedError("Subclasses must implement the update method") - @abstractmethod def delete(self, del_remnants: bool) -> None: raise NotImplementedError("Subclasses must implement the delete method") diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index e21d6a0..5c187e2 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -50,36 +50,12 @@ class Klipper(BaseInstance): service_file_target = f"{SYSTEMD}/{service_file_name}" env_file_target = os.path.abspath(f"{self.sysd_dir}/klipper.env") - # create folder structure - dirs = [ - self.data_dir, - self.cfg_dir, - self.log_dir, - self.comms_dir, - self.sysd_dir, - ] - for _dir in dirs: - create_directory(Path(_dir)) - try: - # writing the klipper service file (requires sudo!) - service_content = self._prep_service_file( - service_template_path, env_file_target + self.create_folder_structure() + self.write_service_file( + service_template_path, service_file_target, env_file_target ) - command = ["sudo", "tee", service_file_target] - subprocess.run( - command, - input=service_content.encode(), - stdout=subprocess.DEVNULL, - check=True, - ) - Logger.print_ok(f"Service file created: {service_file_target}") - - # writing the klipper.env file - env_file_content = self._prep_env_file(env_template_file_path) - with open(env_file_target, "w") as env_file: - env_file.write(env_file_content) - Logger.print_ok(f"Env file created: {env_file_target}") + self.write_env_file(env_template_file_path, env_file_target) except subprocess.CalledProcessError as e: Logger.print_error( @@ -90,12 +66,6 @@ class Klipper(BaseInstance): Logger.print_error(f"Error creating env file {env_file_target}: {e}") raise - def read(self) -> None: - print("Reading Klipper Instance") - - def update(self) -> None: - print("Updating Klipper Instance") - def delete(self, del_remnants: bool) -> None: service_file = self.get_service_file_name(extension=True) service_file_path = self._get_service_file_path() @@ -113,6 +83,45 @@ class Klipper(BaseInstance): if del_remnants: self._delete_klipper_remnants() + def create_folder_structure(self) -> None: + dirs = [ + self.data_dir, + self.cfg_dir, + self.log_dir, + self.comms_dir, + self.sysd_dir, + ] + for _dir in dirs: + create_directory(Path(_dir)) + + def write_service_file( + self, service_template_path: str, service_file_target: str, env_file_target: str + ): + service_content = self._prep_service_file( + service_template_path, env_file_target + ) + command = ["sudo", "tee", service_file_target] + subprocess.run( + command, + input=service_content.encode(), + stdout=subprocess.DEVNULL, + check=True, + ) + Logger.print_ok(f"Service file created: {service_file_target}") + + def write_env_file(self, env_template_file_path: str, env_file_target: str): + env_file_content = self._prep_env_file(env_template_file_path) + with open(env_file_target, "w") as env_file: + env_file.write(env_file_content) + Logger.print_ok(f"Env file created: {env_file_target}") + + def get_service_file_name(self, extension=False) -> str: + name = self.prefix if self.name is None else self.prefix + "-" + self.name + return name if not extension else f"{name}.service" + + def _get_service_file_path(self): + return f"{SYSTEMD}/{self.get_service_file_name(extension=True)}" + def _delete_klipper_remnants(self) -> None: try: Logger.print_info(f"Delete {self.klipper_dir} ...") @@ -127,13 +136,6 @@ class Klipper(BaseInstance): Logger.print_ok("Directories successfully deleted.") - def get_service_file_name(self, extension=False) -> str: - name = self.prefix if self.name is None else self.prefix + "-" + self.name - return name if not extension else f"{name}.service" - - def _get_service_file_path(self): - return f"{SYSTEMD}/{self.get_service_file_name(extension=True)}" - def _get_data_dir_from_name(self, name: str) -> str: if name is None: return "printer" diff --git a/kiauh/modules/klipper/klipper_dialogs.py b/kiauh/modules/klipper/klipper_dialogs.py new file mode 100644 index 0000000..204ea43 --- /dev/null +++ b/kiauh/modules/klipper/klipper_dialogs.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 typing import List + +from kiauh.instance_manager.base_instance import BaseInstance +from kiauh.menus.base_menu import print_back_footer +from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN + + +def print_instance_overview( + instances: List[BaseInstance], show_index=False, show_select_all=False +): + headline = f"{COLOR_GREEN}The following Klipper instances were found:{RESET_FORMAT}" + + print("/=======================================================\\") + print(f"|{'{:^64}'.format(headline)}|") + print("|-------------------------------------------------------|") + + if show_select_all: + select_all = f" {COLOR_YELLOW}a) Select all{RESET_FORMAT}" + print(f"|{'{:64}'.format(select_all)}|") + print("| |") + + for i, s in enumerate(instances): + index = f"{i})" if show_index else "●" + instance = s.get_service_file_name() + line = f"{'{:53}'.format(f'{index} {instance}')}" + print(f"| {COLOR_CYAN}{line}{RESET_FORMAT}|") + + print_back_footer() + + +def print_select_instance_count_dialog(): + print("/=======================================================\\") + print("| Please select the number of Klipper instances to set |") + print("| up. The number of Klipper instances will determine |") + print("| the amount of printers you can run from this host. |") + print("| |") + print( + f"| {COLOR_YELLOW}WARNING:{RESET_FORMAT} |" + ) + print( + f"| {COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT} |" + ) + print_back_footer() + + +def print_select_custom_name_dialog(): + print("/=======================================================\\") + print("| You can now assign a custom name to each instance. |") + print("| If skipped, each instance will get an index assigned |") + print("| in ascending order, starting at index '1'. |") + print("| |") + print( + f"| {COLOR_YELLOW}INFO:{RESET_FORMAT} |" + ) + print( + f"| {COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT} |" + ) + print_back_footer() + + +def print_missing_usergroup_dialog(missing_groups) -> None: + print("/=======================================================\\") + print( + f"| {COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT} |" + ) + if "tty" in missing_groups: + print( + f"| {COLOR_CYAN}● tty{RESET_FORMAT} |" + ) + if "dialout" in missing_groups: + print( + f"| {COLOR_CYAN}● dialout{RESET_FORMAT} |" + ) + print("| |") + print("| It is possible that you won't be able to successfully |") + print("| connect and/or flash the controller board without |") + print("| your user being a member of that group. |") + print("| If you want to add the current user to the group(s) |") + print("| listed above, answer with 'Y'. Else skip with 'n'. |") + print("| |") + print( + f"| {COLOR_YELLOW}INFO:{RESET_FORMAT} |" + ) + print( + f"| {COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT} |" + ) + print("\\=======================================================/") diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 865d2bf..5c41c40 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -9,27 +9,31 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import grp import os -import re import subprocess -import textwrap from pathlib import Path -from typing import Optional, List, Union +from typing import List, Union from kiauh.config_manager.config_manager import ConfigManager from kiauh.instance_manager.instance_manager import InstanceManager from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.klipper.klipper_utils import ( +from kiauh.modules.klipper.klipper_dialogs import ( print_instance_overview, - print_missing_usergroup_dialog, + print_select_instance_count_dialog, +) +from kiauh.modules.klipper.klipper_utils import ( + handle_convert_single_to_multi_instance_names, + handle_new_multi_instance_names, + handle_existing_multi_instance_names, + handle_disruptive_system_packages, + check_user_groups, + handle_single_to_multi_conversion, ) from kiauh.repo_manager.repo_manager import RepoManager -from kiauh.utils.constants import CURRENT_USER, KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.utils.constants import KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.utils.input_utils import ( get_confirm, get_number_input, - get_string_input, get_selection_input, ) from kiauh.utils.logger import Logger @@ -39,7 +43,6 @@ from kiauh.utils.system_utils import ( install_python_requirements, update_system_package_lists, install_system_packages, - mask_system_service, ) @@ -92,19 +95,37 @@ def handle_existing_instances(instance_manager: InstanceManager) -> bool: def install_klipper(instance_manager: InstanceManager) -> None: instance_list = instance_manager.get_instances() - if_adding = " additional" if len(instance_list) > 0 else "" - install_count = get_number_input( - f"Number of{if_adding} Klipper instances to set up", 1, default=1 - ) + + print_select_instance_count_dialog() + question = f"Number of{' additional' if len(instance_list) > 0 else ''} Klipper instances to set up" + install_count = get_number_input(question, 1, default=1, allow_go_back=True) + if install_count is None: + Logger.print_info("Exiting Klipper setup ...") + return instance_names = set_instance_names(instance_list, install_count) + if instance_names is None: + Logger.print_info("Exiting Klipper setup ...") + return if len(instance_list) < 1: setup_klipper_prerequesites() + convert_single_to_multi = ( + True + if len(instance_list) == 1 + and instance_list[0].name is None + and install_count >= 1 + else False + ) + for name in instance_names: - current_instance = Klipper(name=name) - instance_manager.set_current_instance(current_instance) + if convert_single_to_multi: + handle_single_to_multi_conversion(instance_manager, name) + convert_single_to_multi = False + else: + instance_manager.set_current_instance(Klipper(name=name)) + instance_manager.create_instance() instance_manager.enable_instance() instance_manager.start_instance() @@ -122,11 +143,11 @@ def setup_klipper_prerequesites() -> None: cm = ConfigManager() cm.read_config() - repo = ( + repo = str( cm.get_value("klipper", "repository_url") or "https://github.com/Klipper3D/klipper" ) - branch = cm.get_value("klipper", "branch") or "master" + branch = str(cm.get_value("klipper", "branch") or "master") repo_manager = RepoManager( repo=repo, @@ -159,64 +180,23 @@ def install_klipper_packages(klipper_dir: Path) -> None: def set_instance_names(instance_list, install_count: int) -> List[Union[str, None]]: instance_count = len(instance_list) - # default single instance install + # new single instance install if instance_count == 0 and install_count == 1: return [None] + # convert single instance install to multi install + elif instance_count == 1 and instance_list[0].name is None and install_count >= 1: + return handle_convert_single_to_multi_instance_names(install_count) + # new multi instance install - elif ( - (instance_count == 0 and install_count > 1) - # or convert single instance install to multi instance install - or (instance_count == 1 and install_count >= 1) - ): - if get_confirm("Assign custom names?", False): - return assign_custom_names(instance_count, install_count, None) - else: - _range = range(1, install_count + 1) - return [str(i) for i in _range] + elif instance_count == 0 and install_count > 1: + return handle_new_multi_instance_names(instance_count, install_count) # existing multi instance install elif instance_count > 1: - if has_custom_names(instance_list): - return assign_custom_names(instance_count, install_count, instance_list) - else: - start = get_highest_index(instance_list) + 1 - _range = range(start, start + install_count) - return [str(i) for i in _range] - - -def has_custom_names(instance_list: List[Klipper]) -> bool: - pattern = re.compile("^\d+$") - for instance in instance_list: - if not pattern.match(instance.name): - return True - - return False - - -def assign_custom_names( - instance_count: int, install_count: int, instance_list: Optional[List[Klipper]] -) -> List[str]: - instance_names = [] - exclude = Klipper.blacklist() - - # if an instance_list is provided, exclude all existing instance names - if instance_list is not None: - for instance in instance_list: - exclude.append(instance.name) - - for i in range(instance_count + install_count): - question = f"Enter name for instance {i + 1}" - name = get_string_input(question, exclude=exclude) - instance_names.append(name) - exclude.append(name) - - return instance_names - - -def get_highest_index(instance_list: List[Klipper]) -> int: - indices = [int(instance.name.split("-")[-1]) for instance in instance_list] - return max(indices) + return handle_existing_multi_instance_names( + instance_count, install_count, instance_list + ) def remove_single_instance(instance_manager: InstanceManager) -> None: @@ -262,69 +242,3 @@ def remove_multi_instance(instance_manager: InstanceManager) -> None: instance_manager.delete_instance(del_remnants=False) instance_manager.reload_daemon() - - -def check_user_groups(): - current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] - - missing_groups = [] - if "tty" not in current_groups: - missing_groups.append("tty") - if "dialout" not in current_groups: - missing_groups.append("dialout") - - if not missing_groups: - return - - print_missing_usergroup_dialog(missing_groups) - if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): - Logger.warn( - "Skipped adding user to required groups. You might encounter issues." - ) - return - - try: - for group in missing_groups: - Logger.print_info(f"Adding user '{CURRENT_USER}' to group {group} ...") - command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER] - subprocess.run(command, check=True) - Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.") - except subprocess.CalledProcessError as e: - Logger.print_error(f"Unable to add user to usergroups: {e}") - raise - - Logger.print_warn( - "Remember to relog/restart this machine for the group(s) to be applied!" - ) - - -def handle_disruptive_system_packages() -> None: - services = [] - brltty_status = subprocess.run( - ["systemctl", "is-enabled", "brltty"], capture_output=True, text=True - ) - modem_manager_status = subprocess.run( - ["systemctl", "is-enabled", "ModemManager"], capture_output=True, text=True - ) - - if "enabled" in brltty_status.stdout: - services.append("brltty") - if "enabled" in modem_manager_status.stdout: - services.append("ModemManager") - - for service in services if services else []: - try: - Logger.print_info( - f"{service} service detected! Masking {service} service ..." - ) - mask_system_service(service) - Logger.print_ok(f"{service} service masked!") - except subprocess.CalledProcessError: - warn_msg = textwrap.dedent( - f""" - KIAUH was unable to mask the {service} system service. - Please fix the problem manually. Otherwise, this may have - undesirable effects on the operation of Klipper. - """ - )[1:] - Logger.print_warn(warn_msg) diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 0d42b86..e018250 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -9,60 +9,178 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from typing import List +import os +import re +import grp +import subprocess +import textwrap -from kiauh.instance_manager.base_instance import BaseInstance -from kiauh.menus.base_menu import print_back_footer -from kiauh.utils.constants import COLOR_GREEN, COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT +from typing import List, Union + +from kiauh.instance_manager.instance_manager import InstanceManager +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.klipper.klipper_dialogs import ( + print_missing_usergroup_dialog, + print_select_custom_name_dialog, +) +from kiauh.utils.constants import CURRENT_USER +from kiauh.utils.input_utils import get_confirm, get_string_input +from kiauh.utils.logger import Logger +from kiauh.utils.system_utils import mask_system_service -def print_instance_overview( - instances: List[BaseInstance], show_index=False, show_select_all=False -): - headline = f"{COLOR_GREEN}The following Klipper instances were found:{RESET_FORMAT}" +def assign_custom_names( + instance_count: int, install_count: int, instance_list: List[Klipper] = None +) -> List[str]: + instance_names = [] + exclude = Klipper.blacklist() - print("/=======================================================\\") - print(f"|{'{:^64}'.format(headline)}|") - print("|-------------------------------------------------------|") + # if an instance_list is provided, exclude all existing instance names + if instance_list is not None: + for instance in instance_list: + exclude.append(instance.name) - if show_select_all: - select_all = f" {COLOR_YELLOW}a) Select all{RESET_FORMAT}" - print(f"|{'{:64}'.format(select_all)}|") - print("| |") + for i in range(instance_count + install_count): + question = f"Enter name for instance {i + 1}" + name = get_string_input(question, exclude=exclude) + instance_names.append(name) + exclude.append(name) - for i, s in enumerate(instances): - index = f"{i})" if show_index else "●" - instance = s.get_service_file_name() - line = f"{'{:53}'.format(f'{index} {instance}')}" - print(f"| {COLOR_CYAN}{line}{RESET_FORMAT}|") - - print_back_footer() + return instance_names -def print_missing_usergroup_dialog(missing_groups) -> None: - print("/=======================================================\\") - print( - f"| {COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT} |" +def handle_convert_single_to_multi_instance_names( + install_count: int, +) -> Union[List[str], None]: + print_select_custom_name_dialog() + choice = get_confirm("Assign custom names?", False, allow_go_back=True) + if choice is True: + # instance_count = 0 and install_count + 1 as we want to assign a new name to the existing single install + return assign_custom_names(0, install_count + 1) + elif choice is False: + # "install_count + 2" as we need to account for the existing single install + _range = range(1, install_count + 2) + return [str(i) for i in _range] + + return None + + +def handle_new_multi_instance_names( + instance_count: int, install_count: int +) -> Union[List[str], None]: + print_select_custom_name_dialog() + choice = get_confirm("Assign custom names?", False, allow_go_back=True) + if choice is True: + return assign_custom_names(instance_count, install_count) + elif choice is False: + _range = range(1, install_count + 1) + return [str(i) for i in _range] + + return None + + +def handle_existing_multi_instance_names( + instance_count: int, install_count: int, instance_list: List[Klipper] +) -> List[str]: + if has_custom_names(instance_list): + return assign_custom_names(instance_count, install_count, instance_list) + else: + start = get_highest_index(instance_list) + 1 + _range = range(start, start + install_count) + return [str(i) for i in _range] + + +def handle_single_to_multi_conversion( + instance_manager: InstanceManager, name: str +) -> None: + instance_list = instance_manager.get_instances() + instance_manager.set_current_instance(instance_list[0]) + old_data_dir_name = instance_manager.get_instances()[0].data_dir + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance(del_remnants=False) + instance_manager.set_current_instance(Klipper(name=name)) + new_data_dir_name = instance_manager.get_current_instance().data_dir + try: + os.rename(old_data_dir_name, new_data_dir_name) + except OSError as e: + log = f"Cannot rename {old_data_dir_name} to {new_data_dir_name}:\n{e}" + Logger.print_error(log) + + +def check_user_groups(): + current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] + + missing_groups = [] + if "tty" not in current_groups: + missing_groups.append("tty") + if "dialout" not in current_groups: + missing_groups.append("dialout") + + if not missing_groups: + return + + print_missing_usergroup_dialog(missing_groups) + if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): + log = "Skipped adding user to required groups. You might encounter issues." + Logger.warn(log) + return + + try: + for group in missing_groups: + Logger.print_info(f"Adding user '{CURRENT_USER}' to group {group} ...") + command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER] + subprocess.run(command, check=True) + Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.") + except subprocess.CalledProcessError as e: + Logger.print_error(f"Unable to add user to usergroups: {e}") + raise + + log = "Remember to relog/restart this machine for the group(s) to be applied!" + Logger.print_warn(log) + + +def handle_disruptive_system_packages() -> None: + services = [] + brltty_status = subprocess.run( + ["systemctl", "is-enabled", "brltty"], capture_output=True, text=True ) - if "tty" in missing_groups: - print( - f"| {COLOR_CYAN}● tty{RESET_FORMAT} |" - ) - if "dialout" in missing_groups: - print( - f"| {COLOR_CYAN}● dialout{RESET_FORMAT} |" - ) - print("| |") - print("| It is possible that you won't be able to successfully |") - print("| connect and/or flash the controller board without |") - print("| your user being a member of that group. |") - print("| If you want to add the current user to the group(s) |") - print("| listed above, answer with 'Y'. Else skip with 'n'. |") - print("| |") - print( - f"| {COLOR_YELLOW}INFO:{RESET_FORMAT} |" + modem_manager_status = subprocess.run( + ["systemctl", "is-enabled", "ModemManager"], capture_output=True, text=True ) - print( - f"| {COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT} |" - ) - print("\\=======================================================/") + + if "enabled" in brltty_status.stdout: + services.append("brltty") + if "enabled" in modem_manager_status.stdout: + services.append("ModemManager") + + for service in services if services else []: + try: + Logger.print_info( + f"{service} service detected! Masking {service} service ..." + ) + mask_system_service(service) + Logger.print_ok(f"{service} service masked!") + except subprocess.CalledProcessError: + warn_msg = textwrap.dedent( + f""" + KIAUH was unable to mask the {service} system service. + Please fix the problem manually. Otherwise, this may have + undesirable effects on the operation of Klipper. + """ + )[1:] + Logger.print_warn(warn_msg) + + +def has_custom_names(instance_list: List[Klipper]) -> bool: + pattern = re.compile("^\d+$") + for instance in instance_list: + if not pattern.match(instance.name): + return True + + return False + + +def get_highest_index(instance_list: List[Klipper]) -> int: + indices = [int(instance.name.split("-")[-1]) for instance in instance_list] + return max(indices) diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index eaf97f6..5ab04c5 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -9,15 +9,18 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from typing import Optional, List +from typing import Optional, List, Union from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT from kiauh.utils.logger import Logger -def get_confirm(question: str, default_choice=True) -> bool: +def get_confirm( + question: str, default_choice=True, allow_go_back=False +) -> Union[bool, None]: options_confirm = ["y", "yes"] options_decline = ["n", "no"] + options_go_back = ["b", "B"] if default_choice: def_choice = "(Y/n)" @@ -28,7 +31,7 @@ def get_confirm(question: str, default_choice=True) -> bool: while True: choice = ( - input(f"{COLOR_CYAN}###### {question} {def_choice} {RESET_FORMAT}") + input(f"{COLOR_CYAN}###### {question} {def_choice}: {RESET_FORMAT}") .strip() .lower() ) @@ -37,28 +40,34 @@ def get_confirm(question: str, default_choice=True) -> bool: return True elif choice in options_decline: return False + elif allow_go_back and choice in options_go_back: + return None else: Logger.print_error("Invalid choice. Please select 'y' or 'n'.") def get_number_input( - question: str, min_count: int, max_count=None, default=None -) -> int: + question: str, min_count: int, max_count=None, default=None, allow_go_back=False +) -> Union[int, None]: + options_go_back = ["b", "B"] _question = question + f" (default={default})" if default else question _question = f"{COLOR_CYAN}###### {_question}: {RESET_FORMAT}" while True: try: - num = input(_question) - if num == "": + _input = input(_question) + if allow_go_back and _input in options_go_back: + return None + + if _input == "": return default if max_count is not None: - if min_count <= int(num) <= max_count: - return int(num) + if min_count <= int(_input) <= max_count: + return int(_input) else: raise ValueError - elif int(num) >= min_count: - return int(num) + elif int(_input) >= min_count: + return int(_input) else: raise ValueError except ValueError: From 093da73dd12a5e209a99043cd1ce64ef1f6157bf Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 Nov 2023 22:06:59 +0100 Subject: [PATCH 010/296] refactor(klipper): use constants for commonly used strings Signed-off-by: Dominik Willner --- kiauh/modules/klipper/__init__.py | 19 +++++++++++++++++++ kiauh/modules/klipper/klipper.py | 3 ++- kiauh/modules/klipper/klipper_setup.py | 21 ++++++++++++--------- kiauh/utils/constants.py | 11 ----------- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/kiauh/modules/klipper/__init__.py b/kiauh/modules/klipper/__init__.py index e69de29..a9b4efc 100644 --- a/kiauh/modules/klipper/__init__.py +++ b/kiauh/modules/klipper/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 pathlib import Path + +KLIPPER_DIR = f"{Path.home()}/klipper" +KLIPPER_ENV_DIR = f"{Path.home()}/klippy-env" +KLIPPER_REQUIREMENTS_TXT = f"{KLIPPER_DIR}/scripts/klippy-requirements.txt" +DEFAULT_KLIPPER_REPO_URL = "https://github.com/Klipper3D/klipper" + +EXIT_KLIPPER_SETUP = "Exiting Klipper setup ..." diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 5c187e2..01c92a0 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -16,7 +16,8 @@ from pathlib import Path from typing import List from kiauh.instance_manager.base_instance import BaseInstance -from kiauh.utils.constants import CURRENT_USER, SYSTEMD, KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.utils.constants import CURRENT_USER, SYSTEMD from kiauh.utils.logger import Logger from kiauh.utils.system_utils import create_directory diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 5c41c40..b9d6b43 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -16,6 +16,13 @@ from typing import List, Union from kiauh.config_manager.config_manager import ConfigManager from kiauh.instance_manager.instance_manager import InstanceManager +from kiauh.modules.klipper import ( + EXIT_KLIPPER_SETUP, + DEFAULT_KLIPPER_REPO_URL, + KLIPPER_DIR, + KLIPPER_ENV_DIR, + KLIPPER_REQUIREMENTS_TXT, +) from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.klipper.klipper_dialogs import ( print_instance_overview, @@ -30,7 +37,6 @@ from kiauh.modules.klipper.klipper_utils import ( handle_single_to_multi_conversion, ) from kiauh.repo_manager.repo_manager import RepoManager -from kiauh.utils.constants import KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.utils.input_utils import ( get_confirm, get_number_input, @@ -59,7 +65,7 @@ def run_klipper_setup(install: bool) -> None: if install: add_additional = handle_existing_instances(instance_manager) if is_klipper_installed and not add_additional: - Logger.print_info("Exiting Klipper setup ...") + Logger.print_info(EXIT_KLIPPER_SETUP) return install_klipper(instance_manager) @@ -100,12 +106,12 @@ def install_klipper(instance_manager: InstanceManager) -> None: question = f"Number of{' additional' if len(instance_list) > 0 else ''} Klipper instances to set up" install_count = get_number_input(question, 1, default=1, allow_go_back=True) if install_count is None: - Logger.print_info("Exiting Klipper setup ...") + Logger.print_info(EXIT_KLIPPER_SETUP) return instance_names = set_instance_names(instance_list, install_count) if instance_names is None: - Logger.print_info("Exiting Klipper setup ...") + Logger.print_info(EXIT_KLIPPER_SETUP) return if len(instance_list) < 1: @@ -143,10 +149,7 @@ def setup_klipper_prerequesites() -> None: cm = ConfigManager() cm.read_config() - repo = str( - cm.get_value("klipper", "repository_url") - or "https://github.com/Klipper3D/klipper" - ) + repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL) branch = str(cm.get_value("klipper", "branch") or "master") repo_manager = RepoManager( @@ -159,7 +162,7 @@ def setup_klipper_prerequesites() -> None: # install klipper dependencies and create python virtualenv install_klipper_packages(Path(KLIPPER_DIR)) create_python_venv(Path(KLIPPER_ENV_DIR)) - klipper_py_req = Path(f"{KLIPPER_DIR}/scripts/klippy-requirements.txt") + klipper_py_req = Path(KLIPPER_REQUIREMENTS_TXT) install_python_requirements(Path(KLIPPER_ENV_DIR), klipper_py_req) diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py index f795697..60662ce 100644 --- a/kiauh/utils/constants.py +++ b/kiauh/utils/constants.py @@ -12,8 +12,6 @@ import os import pwd -from pathlib import Path - # text colors and formats COLOR_MAGENTA = "\033[35m" # magenta COLOR_GREEN = "\033[92m" # bright green @@ -21,15 +19,6 @@ COLOR_YELLOW = "\033[93m" # bright yellow COLOR_RED = "\033[91m" # bright red COLOR_CYAN = "\033[96m" # bright cyan RESET_FORMAT = "\033[0m" # reset format - # current user CURRENT_USER = pwd.getpwuid(os.getuid())[0] - SYSTEMD = "/etc/systemd/system" - -KLIPPER_DIR = f"{Path.home()}/klipper" -KLIPPER_ENV_DIR = f"{Path.home()}/klippy-env" -MOONRAKER_DIR = f"{Path.home()}/moonraker" -MOONRAKER_ENV_DIR = f"{Path.home()}/moonraker-env" -MAINSAIL_DIR = f"{Path.home()}/mainsail" -FLUIDD_DIR = f"{Path.home()}/fluidd" From fb09acf660cd93a9e30732ceefd04721f8ba7b1d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 Nov 2023 22:52:34 +0100 Subject: [PATCH 011/296] refactor(utils): reduce complexity Signed-off-by: Dominik Willner --- kiauh/utils/__init__.py | 12 +++++++ kiauh/utils/input_utils.py | 66 +++++++++++++++++++++----------------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index e69de29..3c3f6a5 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +INVALID_CHOICE = "Invalid choice. Please select a valid value." diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 5ab04c5..ba27ec9 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -11,6 +11,7 @@ from typing import Optional, List, Union +from kiauh.utils import INVALID_CHOICE from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT from kiauh.utils.logger import Logger @@ -31,9 +32,7 @@ def get_confirm( while True: choice = ( - input(f"{COLOR_CYAN}###### {question} {def_choice}: {RESET_FORMAT}") - .strip() - .lower() + input(format_question(question + f" {def_choice}", None)).strip().lower() ) if choice in options_confirm: @@ -43,54 +42,63 @@ def get_confirm( elif allow_go_back and choice in options_go_back: return None else: - Logger.print_error("Invalid choice. Please select 'y' or 'n'.") + Logger.print_error(INVALID_CHOICE) def get_number_input( question: str, min_count: int, max_count=None, default=None, allow_go_back=False ) -> Union[int, None]: options_go_back = ["b", "B"] - _question = question + f" (default={default})" if default else question - _question = f"{COLOR_CYAN}###### {_question}: {RESET_FORMAT}" + _question = format_question(question, default) while True: + _input = input(_question) + if allow_go_back and _input in options_go_back: + return None + + if _input == "": + return default + try: - _input = input(_question) - if allow_go_back and _input in options_go_back: - return None - - if _input == "": - return default - - if max_count is not None: - if min_count <= int(_input) <= max_count: - return int(_input) - else: - raise ValueError - elif int(_input) >= min_count: - return int(_input) - else: - raise ValueError + return validate_number_input(_input, min_count, max_count) except ValueError: - Logger.print_error("Invalid choice. Please select a valid number.") + Logger.print_error(INVALID_CHOICE) -def get_string_input(question: str, exclude=Optional[List]) -> str: +def get_string_input(question: str, exclude=Optional[List], default=None) -> str: while True: - _input = input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}").strip() + _input = input(format_question(question, default)).strip() if _input.isalnum() and _input not in exclude: return _input - Logger.print_error("Invalid choice. Please enter a valid value.") + Logger.print_error(INVALID_CHOICE) if _input in exclude: Logger.print_error("This value is already in use/reserved.") -def get_selection_input(question: str, option_list: List) -> str: +def get_selection_input(question: str, option_list: List, default=None) -> str: while True: - _input = input(f"{COLOR_CYAN}###### {question}: {RESET_FORMAT}").strip() + _input = input(format_question(question, default)).strip() if _input in option_list: return _input - Logger.print_error("Invalid choice. Please enter a valid value.") + Logger.print_error(INVALID_CHOICE) + + +def format_question(question: str, default=None) -> str: + formatted_q = question + if default is not None: + formatted_q += f" (default={default})" + + return f"{COLOR_CYAN}###### {formatted_q}: {RESET_FORMAT}" + + +def validate_number_input(value: str, min_count: int, max_count: int) -> int: + if max_count is not None: + if min_count <= int(value) <= max_count: + return int(value) + elif int(value) >= min_count: + return int(value) + + raise ValueError From f9ecad0eca3977856d8b15ab530414ecb776514e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 5 Nov 2023 16:01:09 +0100 Subject: [PATCH 012/296] refactor(klipper): use name "klipper" for single instance setup Signed-off-by: Dominik Willner --- kiauh/instance_manager/instance_manager.py | 15 +++++++-------- kiauh/modules/klipper/klipper.py | 6 +++--- kiauh/modules/klipper/klipper_setup.py | 8 ++++++-- kiauh/utils/input_utils.py | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/kiauh/instance_manager/instance_manager.py b/kiauh/instance_manager/instance_manager.py index b1924b1..f08f43e 100644 --- a/kiauh/instance_manager/instance_manager.py +++ b/kiauh/instance_manager/instance_manager.py @@ -39,9 +39,7 @@ class InstanceManager: def set_current_instance(self, instance: BaseInstance) -> None: self.current_instance = instance - self.instance_name = ( - f"{instance.prefix}-{instance.name}" if instance.name else instance.prefix - ) + self.instance_name = instance.name def create_instance(self) -> None: if self.current_instance is not None: @@ -123,13 +121,16 @@ class InstanceManager: def _find_instances(self) -> None: prefix = self.instance_type.__name__.lower() - pattern = re.compile(f"{prefix}(-[0-9a-zA-Z]+)?.service") + single_pattern = re.compile(f"^{prefix}.service$") + multi_pattern = re.compile(f"^{prefix}(-[0-9a-zA-Z]+)?.service$") excluded = self.instance_type.blacklist() service_list = [ os.path.join(SYSTEMD, service) for service in os.listdir(SYSTEMD) - if pattern.search(service) and not any(s in service for s in excluded) + if multi_pattern.search(service) + and not any(s in service for s in excluded) + or single_pattern.search(service) ] instance_list = [ @@ -141,10 +142,8 @@ class InstanceManager: def _get_instance_name(self, file_path: Path) -> Union[str, None]: full_name = str(file_path).split("/")[-1].split(".")[0] - if full_name.isalnum(): - return None - return full_name.split("-")[-1] + return full_name.split("-")[-1] if "-" in full_name else full_name def _sort_instance_list(self, s): if s is None: diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 01c92a0..326269f 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -26,7 +26,7 @@ from kiauh.utils.system_utils import create_directory class Klipper(BaseInstance): @classmethod def blacklist(cls) -> List[str]: - return ["None", "mcu"] + return ["None", "klipper", "mcu"] def __init__(self, name: str): super().__init__( @@ -117,7 +117,7 @@ class Klipper(BaseInstance): Logger.print_ok(f"Env file created: {env_file_target}") def get_service_file_name(self, extension=False) -> str: - name = self.prefix if self.name is None else self.prefix + "-" + self.name + name = "klipper" if self.name == self.prefix else self.prefix + "-" + self.name return name if not extension else f"{name}.service" def _get_service_file_path(self): @@ -138,7 +138,7 @@ class Klipper(BaseInstance): Logger.print_ok("Directories successfully deleted.") def _get_data_dir_from_name(self, name: str) -> str: - if name is None: + if name == "klipper": return "printer" elif int(name.isdigit()): return f"printer_{name}" diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index b9d6b43..7b343b8 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -185,10 +185,14 @@ def set_instance_names(instance_list, install_count: int) -> List[Union[str, Non # new single instance install if instance_count == 0 and install_count == 1: - return [None] + return ["klipper"] # convert single instance install to multi install - elif instance_count == 1 and instance_list[0].name is None and install_count >= 1: + elif ( + instance_count == 1 + and instance_list[0].name == "klipper" + and install_count >= 1 + ): return handle_convert_single_to_multi_instance_names(install_count) # new multi instance install diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index ba27ec9..280088d 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -68,7 +68,7 @@ def get_string_input(question: str, exclude=Optional[List], default=None) -> str while True: _input = input(format_question(question, default)).strip() - if _input.isalnum() and _input not in exclude: + if _input.isalnum() and _input.lower() not in exclude: return _input Logger.print_error(INVALID_CHOICE) From 515a42f0981009217ef581fdda8027cca221e6ca Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 5 Nov 2023 16:15:19 +0100 Subject: [PATCH 013/296] feat(klipper): implement update function Signed-off-by: Dominik Willner --- kiauh/instance_manager/instance_manager.py | 10 +++++++++ kiauh/menus/update_menu.py | 3 ++- kiauh/modules/klipper/klipper_dialogs.py | 17 ++++++++++++++ kiauh/modules/klipper/klipper_setup.py | 26 ++++++++++++++++++++++ kiauh/repo_manager/repo_manager.py | 18 +++++++++++++++ 5 files changed, 73 insertions(+), 1 deletion(-) diff --git a/kiauh/instance_manager/instance_manager.py b/kiauh/instance_manager/instance_manager.py index f08f43e..5a775c1 100644 --- a/kiauh/instance_manager/instance_manager.py +++ b/kiauh/instance_manager/instance_manager.py @@ -91,6 +91,11 @@ class InstanceManager: Logger.print_error(f"Error starting service {self.instance_name}.service:") Logger.print_error(f"{e}") + def start_all_instance(self) -> None: + for instance in self.instances: + self.set_current_instance(instance) + self.start_instance() + def stop_instance(self) -> None: Logger.print_info(f"Stopping {self.instance_name}.service ...") try: @@ -102,6 +107,11 @@ class InstanceManager: Logger.print_error(f"{e}") raise + def stop_all_instance(self) -> None: + for instance in self.instances: + self.set_current_instance(instance) + self.stop_instance() + def reload_daemon(self) -> None: Logger.print_info("Reloading systemd manager configuration ...") try: diff --git a/kiauh/menus/update_menu.py b/kiauh/menus/update_menu.py index 0e9a78a..f617fe9 100644 --- a/kiauh/menus/update_menu.py +++ b/kiauh/menus/update_menu.py @@ -12,6 +12,7 @@ import textwrap from kiauh.menus.base_menu import BaseMenu +from kiauh.modules.klipper.klipper_setup import update_klipper from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT @@ -74,7 +75,7 @@ class UpdateMenu(BaseMenu): print("update_all") def update_klipper(self): - print("update_klipper") + update_klipper() def update_moonraker(self): print("update_moonraker") diff --git a/kiauh/modules/klipper/klipper_dialogs.py b/kiauh/modules/klipper/klipper_dialogs.py index 204ea43..cc8d15c 100644 --- a/kiauh/modules/klipper/klipper_dialogs.py +++ b/kiauh/modules/klipper/klipper_dialogs.py @@ -96,3 +96,20 @@ def print_missing_usergroup_dialog(missing_groups) -> None: f"| {COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT} |" ) print("\\=======================================================/") + + +def print_update_warn_dialog(): + print("/=======================================================\\") + print( + f"| {COLOR_YELLOW}WARNING: {RESET_FORMAT}|" + ) + print( + f"| {COLOR_YELLOW}Do NOT continue if there are ongoing prints running! {RESET_FORMAT}|" + ) + print( + f"| {COLOR_YELLOW}All Klipper instances will be restarted during the {RESET_FORMAT}|" + ) + print( + f"| {COLOR_YELLOW}update process and any ongoing prints WILL FAIL. {RESET_FORMAT}|" + ) + print("\\=======================================================/") diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 7b343b8..d38b89f 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -27,6 +27,7 @@ from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.klipper.klipper_dialogs import ( print_instance_overview, print_select_instance_count_dialog, + print_update_warn_dialog, ) from kiauh.modules.klipper.klipper_utils import ( handle_convert_single_to_multi_instance_names, @@ -249,3 +250,28 @@ def remove_multi_instance(instance_manager: InstanceManager) -> None: instance_manager.delete_instance(del_remnants=False) instance_manager.reload_daemon() + + +def update_klipper() -> None: + print_update_warn_dialog() + + if not get_confirm("Update Klipper now?"): + return + + instance_manager = InstanceManager(Klipper) + instance_manager.get_instances() + instance_manager.stop_all_instance() + + cm = ConfigManager() + cm.read_config() + + repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL) + branch = str(cm.get_value("klipper", "branch") or "master") + + repo_manager = RepoManager( + repo=repo, + branch=branch, + target_dir=KLIPPER_DIR, + ) + repo_manager.pull_repo() + instance_manager.start_all_instance() diff --git a/kiauh/repo_manager/repo_manager.py b/kiauh/repo_manager/repo_manager.py index b1501d2..4555920 100644 --- a/kiauh/repo_manager/repo_manager.py +++ b/kiauh/repo_manager/repo_manager.py @@ -77,6 +77,15 @@ class RepoManager: Logger.print_error(f"Error removing existing repository: {e.strerror}") return + def pull_repo(self) -> None: + Logger.print_info(f"Updating repository '{self.repo}' ...") + try: + self._pull() + except subprocess.CalledProcessError: + log = "An unexpected error occured during updating the repository." + Logger.print_error(log) + return + def _clone(self): try: command = ["git", "clone", self.repo, self.target_dir] @@ -99,5 +108,14 @@ class RepoManager: Logger.print_error(log) raise + def _pull(self) -> None: + try: + command = ["git", "pull"] + subprocess.run(command, cwd=self.target_dir, check=True) + except subprocess.CalledProcessError as e: + log = f"Error on git pull: {e.stderr.decode()}" + Logger.print_error(log) + raise + def _get_method(self) -> str: return "ssh" if self.repo.startswith("git") else "https" From e12e578098ef2b7612118c68ba79bb7390ec8b9a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 5 Nov 2023 22:56:56 +0100 Subject: [PATCH 014/296] refactor(klipper): rewrite dialogs Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_dialogs.py | 163 +++++++++++++---------- 1 file changed, 92 insertions(+), 71 deletions(-) diff --git a/kiauh/modules/klipper/klipper_dialogs.py b/kiauh/modules/klipper/klipper_dialogs.py index cc8d15c..9bb46f1 100644 --- a/kiauh/modules/klipper/klipper_dialogs.py +++ b/kiauh/modules/klipper/klipper_dialogs.py @@ -9,6 +9,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import textwrap from typing import List from kiauh.instance_manager.base_instance import BaseInstance @@ -20,96 +21,116 @@ def print_instance_overview( instances: List[BaseInstance], show_index=False, show_select_all=False ): headline = f"{COLOR_GREEN}The following Klipper instances were found:{RESET_FORMAT}" - - print("/=======================================================\\") - print(f"|{'{:^64}'.format(headline)}|") - print("|-------------------------------------------------------|") + dialog = textwrap.dedent( + f""" + /=======================================================\\ + |{'{:^64}'.format(headline)}| + |-------------------------------------------------------| + """ + )[1:] if show_select_all: - select_all = f" {COLOR_YELLOW}a) Select all{RESET_FORMAT}" - print(f"|{'{:64}'.format(select_all)}|") - print("| |") + select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}" + dialog += f"| {'{:63}'.format(select_all)}|" + dialog += "| |" for i, s in enumerate(instances): - index = f"{i})" if show_index else "●" - instance = s.get_service_file_name() - line = f"{'{:53}'.format(f'{index} {instance}')}" - print(f"| {COLOR_CYAN}{line}{RESET_FORMAT}|") + line = f"{COLOR_CYAN}{'{i})' if show_index else '●'} {s.get_service_file_name()}{RESET_FORMAT}" + dialog += f"| {'{:63}'.format(line)}|" + print(dialog) print_back_footer() def print_select_instance_count_dialog(): - print("/=======================================================\\") - print("| Please select the number of Klipper instances to set |") - print("| up. The number of Klipper instances will determine |") - print("| the amount of printers you can run from this host. |") - print("| |") - print( - f"| {COLOR_YELLOW}WARNING:{RESET_FORMAT} |" - ) - print( - f"| {COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT} |" - ) + line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | Please select the number of Klipper instances to set | + | up. The number of Klipper instances will determine | + | the amount of printers you can run from this host. | + | | + | {'{:63}'.format(line1)}| + | {'{:63}'.format(line2)}| + """ + )[1:] + + print(dialog, end="") print_back_footer() def print_select_custom_name_dialog(): - print("/=======================================================\\") - print("| You can now assign a custom name to each instance. |") - print("| If skipped, each instance will get an index assigned |") - print("| in ascending order, starting at index '1'. |") - print("| |") - print( - f"| {COLOR_YELLOW}INFO:{RESET_FORMAT} |" - ) - print( - f"| {COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT} |" - ) + line1 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | You can now assign a custom name to each instance. | + | If skipped, each instance will get an index assigned | + | in ascending order, starting at index '1'. | + | | + | {'{:63}'.format(line1)}| + | {'{:63}'.format(line2)}| + """ + )[1:] + + print(dialog, end="") print_back_footer() def print_missing_usergroup_dialog(missing_groups) -> None: - print("/=======================================================\\") - print( - f"| {COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT} |" - ) + line1 = f"{COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT}" + line2 = f"{COLOR_CYAN}● tty{RESET_FORMAT}" + line3 = f"{COLOR_CYAN}● dialout{RESET_FORMAT}" + line4 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}" + line5 = f"{COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT}" + + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | {'{:63}'.format(line1)}| + """ + )[1:] + if "tty" in missing_groups: - print( - f"| {COLOR_CYAN}● tty{RESET_FORMAT} |" - ) + dialog += f"| {'{:63}'.format(line2)}|\n" if "dialout" in missing_groups: - print( - f"| {COLOR_CYAN}● dialout{RESET_FORMAT} |" - ) - print("| |") - print("| It is possible that you won't be able to successfully |") - print("| connect and/or flash the controller board without |") - print("| your user being a member of that group. |") - print("| If you want to add the current user to the group(s) |") - print("| listed above, answer with 'Y'. Else skip with 'n'. |") - print("| |") - print( - f"| {COLOR_YELLOW}INFO:{RESET_FORMAT} |" - ) - print( - f"| {COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT} |" - ) - print("\\=======================================================/") + dialog += f"| {'{:63}'.format(line3)}|\n" + + dialog += textwrap.dedent( + f""" + | | + | It is possible that you won't be able to successfully | + | connect and/or flash the controller board without | + | your user being a member of that group. | + | If you want to add the current user to the group(s) | + | listed above, answer with 'Y'. Else skip with 'n'. | + | | + | {'{:63}'.format(line4)}| + | {'{:63}'.format(line5)}| + \\=======================================================/ + """ + )[1:] + + print(dialog, end="") def print_update_warn_dialog(): - print("/=======================================================\\") - print( - f"| {COLOR_YELLOW}WARNING: {RESET_FORMAT}|" - ) - print( - f"| {COLOR_YELLOW}Do NOT continue if there are ongoing prints running! {RESET_FORMAT}|" - ) - print( - f"| {COLOR_YELLOW}All Klipper instances will be restarted during the {RESET_FORMAT}|" - ) - print( - f"| {COLOR_YELLOW}update process and any ongoing prints WILL FAIL. {RESET_FORMAT}|" - ) - print("\\=======================================================/") + line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}Do NOT continue if there are ongoing prints running!{RESET_FORMAT}" + line3 = f"{COLOR_YELLOW}All Klipper instances will be restarted during the {RESET_FORMAT}" + line4 = f"{COLOR_YELLOW}update process and ongoing prints WILL FAIL.{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | {'{:63}'.format(line1)}| + | {'{:63}'.format(line2)}| + | {'{:63}'.format(line3)}| + | {'{:63}'.format(line4)}| + \\=======================================================/ + """ + )[1:] + + print(dialog, end="") From 44ed3b6ddf8a83a898a8574ae37966c26af66220 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 Nov 2023 23:53:42 +0100 Subject: [PATCH 015/296] feat(kiauh): add .iml to gitignore Signed-off-by: Dominik Willner --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8105baa..0da0757 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .pytest_cache .kiauh-env *.code-workspace +*.iml kiauh.cfg From 68369753fdb51f71e0cadea988ba5f10f33b811f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 Nov 2023 23:56:27 +0100 Subject: [PATCH 016/296] refactor(InstanceManager): rework Signed-off-by: Dominik Willner --- kiauh/instance_manager/base_instance.py | 116 +++++++++++----- kiauh/instance_manager/instance_manager.py | 154 +++++++++++++-------- kiauh/modules/klipper/klipper.py | 32 +---- kiauh/modules/klipper/klipper_setup.py | 70 ++++------ kiauh/modules/klipper/klipper_utils.py | 18 +-- 5 files changed, 227 insertions(+), 163 deletions(-) diff --git a/kiauh/instance_manager/base_instance.py b/kiauh/instance_manager/base_instance.py index 3dcda97..df7e92f 100644 --- a/kiauh/instance_manager/base_instance.py +++ b/kiauh/instance_manager/base_instance.py @@ -11,7 +11,11 @@ from abc import abstractmethod, ABC from pathlib import Path -from typing import List, Optional +from typing import List, Union, Optional, Type, TypeVar + +from kiauh.utils.constants import SYSTEMD, CURRENT_USER + +B = TypeVar(name="B", bound="BaseInstance", covariant=True) class BaseInstance(ABC): @@ -21,43 +25,41 @@ class BaseInstance(ABC): def __init__( self, - prefix: Optional[str], - name: Optional[str], - user: Optional[str], - data_dir_name: Optional[str], + suffix: Optional[str], + instance_type: B = B, ): - self._prefix = prefix - self._name = name - self._user = user - self._data_dir_name = data_dir_name - self.data_dir = f"{Path.home()}/{self._data_dir_name}_data" - self.cfg_dir = f"{self.data_dir}/config" - self.log_dir = f"{self.data_dir}/logs" - self.comms_dir = f"{self.data_dir}/comms" - self.sysd_dir = f"{self.data_dir}/systemd" + self._instance_type = instance_type + self._suffix = suffix + self._user = CURRENT_USER + self._data_dir_name = self.get_data_dir_from_suffix() + self._data_dir = f"{Path.home()}/{self._data_dir_name}_data" + self._cfg_dir = f"{self.data_dir}/config" + self._log_dir = f"{self.data_dir}/logs" + self._comms_dir = f"{self.data_dir}/comms" + self._sysd_dir = f"{self.data_dir}/systemd" @property - def prefix(self) -> str: - return self._prefix + def instance_type(self) -> Type["BaseInstance"]: + return self._instance_type - @prefix.setter - def prefix(self, value) -> None: - self._prefix = value + @instance_type.setter + def instance_type(self, value: Type["BaseInstance"]) -> None: + self._instance_type = value @property - def name(self) -> str: - return self._name + def suffix(self) -> str: + return self._suffix - @name.setter - def name(self, value) -> None: - self._name = value + @suffix.setter + def suffix(self, value: Union[str, None]) -> None: + self._suffix = value @property def user(self) -> str: return self._user @user.setter - def user(self, value) -> None: + def user(self, value: str) -> None: self._user = value @property @@ -65,9 +67,49 @@ class BaseInstance(ABC): return self._data_dir_name @data_dir_name.setter - def data_dir_name(self, value) -> None: + def data_dir_name(self, value: str) -> None: self._data_dir_name = value + @property + def data_dir(self): + return self._data_dir + + @data_dir.setter + def data_dir(self, value: str): + self._data_dir = value + + @property + def cfg_dir(self): + return self._cfg_dir + + @cfg_dir.setter + def cfg_dir(self, value: str): + self._cfg_dir = value + + @property + def log_dir(self): + return self._log_dir + + @log_dir.setter + def log_dir(self, value: str): + self._log_dir = value + + @property + def comms_dir(self): + return self._comms_dir + + @comms_dir.setter + def comms_dir(self, value: str): + self._comms_dir = value + + @property + def sysd_dir(self): + return self._sysd_dir + + @sysd_dir.setter + def sysd_dir(self, value: str): + self._sysd_dir = value + @abstractmethod def create(self) -> None: raise NotImplementedError("Subclasses must implement the create method") @@ -76,8 +118,20 @@ class BaseInstance(ABC): def delete(self, del_remnants: bool) -> None: raise NotImplementedError("Subclasses must implement the delete method") - @abstractmethod - def get_service_file_name(self) -> str: - raise NotImplementedError( - "Subclasses must implement the get_service_file_name method" - ) + def get_service_file_name(self, extension: bool = False) -> str: + name = f"{self.__class__.__name__.lower()}" + if self.suffix is not None: + name += f"-{self.suffix}" + + return name if not extension else f"{name}.service" + + def get_service_file_path(self) -> str: + return f"{SYSTEMD}/{self.get_service_file_name(extension=True)}" + + def get_data_dir_from_suffix(self) -> str: + if self._suffix is None: + return "printer" + elif self._suffix.isdigit(): + return f"printer_{self._suffix}" + else: + return self._suffix diff --git a/kiauh/instance_manager/instance_manager.py b/kiauh/instance_manager/instance_manager.py index 5a775c1..56b886b 100644 --- a/kiauh/instance_manager/instance_manager.py +++ b/kiauh/instance_manager/instance_manager.py @@ -12,34 +12,85 @@ import os import re import subprocess -from pathlib import Path -from typing import Optional, List, Type, Union +from typing import List, Optional, Union, TypeVar from kiauh.instance_manager.base_instance import BaseInstance from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger +I = TypeVar(name="I", bound=BaseInstance, covariant=True) + + # noinspection PyMethodMayBeStatic class InstanceManager: - def __init__( - self, - instance_type: Type[BaseInstance], - current_instance: Optional[BaseInstance] = None, - ) -> None: - self.instance_type = instance_type - self.current_instance = current_instance - self.instance_name = ( - current_instance.name if current_instance is not None else None - ) - self.instances = [] + def __init__(self, instance_type: I) -> None: + self._instance_type = instance_type + self._current_instance: Optional[I] = None + self._instance_suffix: Optional[str] = None + self._instance_service: Optional[str] = None + self._instance_service_path: Optional[str] = None + self._instances: List[I] = [] - def get_current_instance(self) -> BaseInstance: - return self.current_instance + @property + def instance_type(self) -> I: + return self._instance_type - def set_current_instance(self, instance: BaseInstance) -> None: - self.current_instance = instance - self.instance_name = instance.name + @instance_type.setter + def instance_type(self, value: I): + self._instance_type = value + + @property + def current_instance(self) -> I: + return self._current_instance + + @current_instance.setter + def current_instance(self, value: I) -> None: + self._current_instance = value + self.instance_suffix = value.suffix + self.instance_service = value.get_service_file_path() + self.instance_service_path = value.get_service_file_path() + + @property + def instance_suffix(self) -> str: + return self._instance_suffix + + @instance_suffix.setter + def instance_suffix(self, value: str): + self._instance_suffix = value + + @property + def instance_service(self) -> str: + return self._instance_service + + @instance_service.setter + def instance_service(self, value: str): + self._instance_service = value + + @property + def instance_service_path(self) -> str: + return self._instance_service_path + + @instance_service_path.setter + def instance_service_path(self, value: str): + self._instance_service_path = value + + @property + def instances(self) -> List[I]: + print("instances getter called") + if not self._instances: + print("instances none") + self._instances = self._find_instances() + + print("return instances") + print(self._instances) + for instance in self._instances: + print(type(instance), instance.suffix) + return sorted(self._instances, key=lambda x: self._sort_instance_list(x.suffix)) + + @instances.setter + def instances(self, value: List[I]): + self._instances = value def create_instance(self) -> None: if self.current_instance is not None: @@ -62,54 +113,56 @@ class InstanceManager: raise ValueError("current_instance cannot be None") def enable_instance(self) -> None: - Logger.print_info(f"Enabling {self.instance_name}.service ...") + Logger.print_info(f"Enabling {self.instance_service} ...") try: - command = ["sudo", "systemctl", "enable", f"{self.instance_name}.service"] + command = ["sudo", "systemctl", "enable", self.instance_service] if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_name}.service enabled.") + Logger.print_ok(f"{self.instance_suffix}.service enabled.") except subprocess.CalledProcessError as e: - Logger.print_error(f"Error enabling service {self.instance_name}.service:") + Logger.print_error( + f"Error enabling service {self.instance_suffix}.service:" + ) Logger.print_error(f"{e}") def disable_instance(self) -> None: - Logger.print_info(f"Disabling {self.instance_name}.service ...") + Logger.print_info(f"Disabling {self.instance_service} ...") try: - command = ["sudo", "systemctl", "disable", f"{self.instance_name}.service"] + command = ["sudo", "systemctl", "disable", self.instance_service] if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_name}.service disabled.") + Logger.print_ok(f"{self.instance_service} disabled.") except subprocess.CalledProcessError as e: - Logger.print_error(f"Error disabling service {self.instance_name}.service:") + Logger.print_error(f"Error disabling service {self.instance_service}:") Logger.print_error(f"{e}") def start_instance(self) -> None: - Logger.print_info(f"Starting {self.instance_name}.service ...") + Logger.print_info(f"Starting {self.instance_service} ...") try: - command = ["sudo", "systemctl", "start", f"{self.instance_name}.service"] + command = ["sudo", "systemctl", "start", self.instance_service] if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_name}.service started.") + Logger.print_ok(f"{self.instance_service} started.") except subprocess.CalledProcessError as e: - Logger.print_error(f"Error starting service {self.instance_name}.service:") + Logger.print_error(f"Error starting service {self.instance_service}:") Logger.print_error(f"{e}") def start_all_instance(self) -> None: for instance in self.instances: - self.set_current_instance(instance) + self.current_instance = instance self.start_instance() def stop_instance(self) -> None: - Logger.print_info(f"Stopping {self.instance_name}.service ...") + Logger.print_info(f"Stopping {self.instance_service} ...") try: - command = ["sudo", "systemctl", "stop", f"{self.instance_name}.service"] + command = ["sudo", "systemctl", "stop", self.instance_service] if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_name}.service stopped.") + Logger.print_ok(f"{self.instance_service} stopped.") except subprocess.CalledProcessError as e: - Logger.print_error(f"Error stopping service {self.instance_name}.service:") + Logger.print_error(f"Error stopping service {self.instance_service}:") Logger.print_error(f"{e}") raise def stop_all_instance(self) -> None: for instance in self.instances: - self.set_current_instance(instance) + self.current_instance = instance self.stop_instance() def reload_daemon(self) -> None: @@ -123,39 +176,30 @@ class InstanceManager: Logger.print_error(f"{e}") raise - def get_instances(self) -> List[BaseInstance]: - if not self.instances: - self._find_instances() - - return sorted(self.instances, key=lambda x: self._sort_instance_list(x.name)) - - def _find_instances(self) -> None: - prefix = self.instance_type.__name__.lower() - single_pattern = re.compile(f"^{prefix}.service$") - multi_pattern = re.compile(f"^{prefix}(-[0-9a-zA-Z]+)?.service$") - + def _find_instances(self) -> List[I]: + name = self.instance_type.__name__.lower() + pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.service$") excluded = self.instance_type.blacklist() + service_list = [ os.path.join(SYSTEMD, service) for service in os.listdir(SYSTEMD) - if multi_pattern.search(service) - and not any(s in service for s in excluded) - or single_pattern.search(service) + if pattern.search(service) and not any(s in service for s in excluded) ] instance_list = [ - self.instance_type(name=self._get_instance_name(Path(service))) + self.instance_type(suffix=self._get_instance_suffix(service)) for service in service_list ] - self.instances = instance_list + return instance_list - def _get_instance_name(self, file_path: Path) -> Union[str, None]: - full_name = str(file_path).split("/")[-1].split(".")[0] + def _get_instance_suffix(self, file_path: str) -> Union[str, None]: + full_name = file_path.split("/")[-1].split(".")[0] return full_name.split("-")[-1] if "-" in full_name else full_name - def _sort_instance_list(self, s): + def _sort_instance_list(self, s: Union[int, str, None]): if s is None: return diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 326269f..33c4ec3 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -17,7 +17,7 @@ from typing import List from kiauh.instance_manager.base_instance import BaseInstance from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR -from kiauh.utils.constants import CURRENT_USER, SYSTEMD +from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger from kiauh.utils.system_utils import create_directory @@ -26,15 +26,10 @@ from kiauh.utils.system_utils import create_directory class Klipper(BaseInstance): @classmethod def blacklist(cls) -> List[str]: - return ["None", "klipper", "mcu"] + return ["None", "mcu"] - def __init__(self, name: str): - super().__init__( - name=name, - prefix="klipper", - user=CURRENT_USER, - data_dir_name=self._get_data_dir_from_name(name), - ) + def __init__(self, suffix: str = None): + super().__init__(instance_type=self, suffix=suffix) self.klipper_dir = KLIPPER_DIR self.env_dir = KLIPPER_ENV_DIR self.cfg_file = f"{self.cfg_dir}/printer.cfg" @@ -43,7 +38,7 @@ class Klipper(BaseInstance): self.uds = f"{self.comms_dir}/klippy.sock" def create(self) -> None: - Logger.print_info("Creating Klipper Instance") + Logger.print_info("Creating new Klipper Instance ...") module_path = os.path.dirname(os.path.abspath(__file__)) service_template_path = os.path.join(module_path, "res", "klipper.service") env_template_file_path = os.path.join(module_path, "res", "klipper.env") @@ -69,7 +64,7 @@ class Klipper(BaseInstance): def delete(self, del_remnants: bool) -> None: service_file = self.get_service_file_name(extension=True) - service_file_path = self._get_service_file_path() + service_file_path = self.get_service_file_path() Logger.print_info(f"Deleting Klipper Instance: {service_file}") @@ -116,13 +111,6 @@ class Klipper(BaseInstance): env_file.write(env_file_content) Logger.print_ok(f"Env file created: {env_file_target}") - def get_service_file_name(self, extension=False) -> str: - name = "klipper" if self.name == self.prefix else self.prefix + "-" + self.name - return name if not extension else f"{name}.service" - - def _get_service_file_path(self): - return f"{SYSTEMD}/{self.get_service_file_name(extension=True)}" - def _delete_klipper_remnants(self) -> None: try: Logger.print_info(f"Delete {self.klipper_dir} ...") @@ -137,14 +125,6 @@ class Klipper(BaseInstance): Logger.print_ok("Directories successfully deleted.") - def _get_data_dir_from_name(self, name: str) -> str: - if name == "klipper": - return "printer" - elif int(name.isdigit()): - return f"printer_{name}" - else: - return name - def _prep_service_file(self, service_template_path, env_file_path): try: with open(service_template_path, "r") as template_file: diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index d38b89f..e19fb99 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -55,41 +55,30 @@ from kiauh.utils.system_utils import ( def run_klipper_setup(install: bool) -> None: instance_manager = InstanceManager(Klipper) - instance_list = instance_manager.get_instances() + instance_list = instance_manager.instances instances_installed = len(instance_list) - is_klipper_installed = check_klipper_installation(instance_manager) + is_klipper_installed = instances_installed > 0 if not install and not is_klipper_installed: Logger.print_warn("Klipper not installed!") return if install: - add_additional = handle_existing_instances(instance_manager) + add_additional = handle_existing_instances(instance_list) if is_klipper_installed and not add_additional: Logger.print_info(EXIT_KLIPPER_SETUP) return - install_klipper(instance_manager) + install_klipper(instance_manager, instance_list) if not install: if instances_installed == 1: - remove_single_instance(instance_manager) + remove_single_instance(instance_manager, instance_list) else: - remove_multi_instance(instance_manager) + remove_multi_instance(instance_manager, instance_list) -def check_klipper_installation(instance_manager: InstanceManager) -> bool: - instance_list = instance_manager.get_instances() - instances_installed = len(instance_list) - - if instances_installed < 1: - return False - - return True - - -def handle_existing_instances(instance_manager: InstanceManager) -> bool: - instance_list = instance_manager.get_instances() +def handle_existing_instances(instance_list: List[Klipper]) -> bool: instance_count = len(instance_list) if instance_count > 0: @@ -100,9 +89,9 @@ def handle_existing_instances(instance_manager: InstanceManager) -> bool: return True -def install_klipper(instance_manager: InstanceManager) -> None: - instance_list = instance_manager.get_instances() - +def install_klipper( + instance_manager: InstanceManager, instance_list: List[Klipper] +) -> None: print_select_instance_count_dialog() question = f"Number of{' additional' if len(instance_list) > 0 else ''} Klipper instances to set up" install_count = get_number_input(question, 1, default=1, allow_go_back=True) @@ -110,7 +99,7 @@ def install_klipper(instance_manager: InstanceManager) -> None: Logger.print_info(EXIT_KLIPPER_SETUP) return - instance_names = set_instance_names(instance_list, install_count) + instance_names = set_instance_suffix(instance_list, install_count) if instance_names is None: Logger.print_info(EXIT_KLIPPER_SETUP) return @@ -119,11 +108,9 @@ def install_klipper(instance_manager: InstanceManager) -> None: setup_klipper_prerequesites() convert_single_to_multi = ( - True - if len(instance_list) == 1 - and instance_list[0].name is None + len(instance_list) == 1 + and instance_list[0].suffix is None and install_count >= 1 - else False ) for name in instance_names: @@ -131,7 +118,7 @@ def install_klipper(instance_manager: InstanceManager) -> None: handle_single_to_multi_conversion(instance_manager, name) convert_single_to_multi = False else: - instance_manager.set_current_instance(Klipper(name=name)) + instance_manager.current_instance = Klipper(suffix=name) instance_manager.create_instance() instance_manager.enable_instance() @@ -181,19 +168,17 @@ def install_klipper_packages(klipper_dir: Path) -> None: install_system_packages(packages) -def set_instance_names(instance_list, install_count: int) -> List[Union[str, None]]: +def set_instance_suffix( + instance_list: List[Klipper], install_count: int +) -> List[Union[str, None]]: instance_count = len(instance_list) # new single instance install if instance_count == 0 and install_count == 1: - return ["klipper"] + return [None] # convert single instance install to multi install - elif ( - instance_count == 1 - and instance_list[0].name == "klipper" - and install_count >= 1 - ): + elif instance_count == 1 and install_count >= 1 and instance_list[0].suffix is None: return handle_convert_single_to_multi_instance_names(install_count) # new multi instance install @@ -207,10 +192,11 @@ def set_instance_names(instance_list, install_count: int) -> List[Union[str, Non ) -def remove_single_instance(instance_manager: InstanceManager) -> None: - instance_list = instance_manager.get_instances() +def remove_single_instance( + instance_manager: InstanceManager, instance_list: List[Klipper] +) -> None: try: - instance_manager.set_current_instance(instance_list[0]) + instance_manager.current_instance = instance_list[0] instance_manager.stop_instance() instance_manager.disable_instance() instance_manager.delete_instance(del_remnants=True) @@ -220,8 +206,9 @@ def remove_single_instance(instance_manager: InstanceManager) -> None: return -def remove_multi_instance(instance_manager: InstanceManager) -> None: - instance_list = instance_manager.get_instances() +def remove_multi_instance( + instance_manager: InstanceManager, instance_list: List[Klipper] +) -> None: print_instance_overview(instance_list, show_index=True, show_select_all=True) options = [str(i) for i in range(len(instance_list))] @@ -235,7 +222,7 @@ def remove_multi_instance(instance_manager: InstanceManager) -> None: elif selection == "a".lower(): Logger.print_info("Removing all Klipper instances ...") for instance in instance_list: - instance_manager.set_current_instance(instance) + instance_manager.current_instance = instance instance_manager.stop_instance() instance_manager.disable_instance() instance_manager.delete_instance(del_remnants=True) @@ -244,7 +231,7 @@ def remove_multi_instance(instance_manager: InstanceManager) -> None: Logger.print_info( f"Removing Klipper instance: {instance.get_service_file_name()}" ) - instance_manager.set_current_instance(instance) + instance_manager.current_instance = instance instance_manager.stop_instance() instance_manager.disable_instance() instance_manager.delete_instance(del_remnants=False) @@ -259,7 +246,6 @@ def update_klipper() -> None: return instance_manager = InstanceManager(Klipper) - instance_manager.get_instances() instance_manager.stop_all_instance() cm = ConfigManager() diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index e018250..3bb8b04 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -35,10 +35,10 @@ def assign_custom_names( instance_names = [] exclude = Klipper.blacklist() - # if an instance_list is provided, exclude all existing instance names + # if an instance_list is provided, exclude all existing instance suffixes if instance_list is not None: for instance in instance_list: - exclude.append(instance.name) + exclude.append(instance.suffix) for i in range(instance_count + install_count): question = f"Enter name for instance {i + 1}" @@ -93,14 +93,14 @@ def handle_existing_multi_instance_names( def handle_single_to_multi_conversion( instance_manager: InstanceManager, name: str ) -> None: - instance_list = instance_manager.get_instances() - instance_manager.set_current_instance(instance_list[0]) - old_data_dir_name = instance_manager.get_instances()[0].data_dir + instance_list = instance_manager.instances + instance_manager.current_instance = instance_list[0] + old_data_dir_name = instance_manager.instances[0].data_dir instance_manager.stop_instance() instance_manager.disable_instance() instance_manager.delete_instance(del_remnants=False) - instance_manager.set_current_instance(Klipper(name=name)) - new_data_dir_name = instance_manager.get_current_instance().data_dir + instance_manager.current_instance = Klipper(suffix=name) + new_data_dir_name = instance_manager.current_instance.data_dir try: os.rename(old_data_dir_name, new_data_dir_name) except OSError as e: @@ -175,12 +175,12 @@ def handle_disruptive_system_packages() -> None: def has_custom_names(instance_list: List[Klipper]) -> bool: pattern = re.compile("^\d+$") for instance in instance_list: - if not pattern.match(instance.name): + if not pattern.match(instance.suffix): return True return False def get_highest_index(instance_list: List[Klipper]) -> int: - indices = [int(instance.name.split("-")[-1]) for instance in instance_list] + indices = [int(instance.suffix.split("-")[-1]) for instance in instance_list] return max(indices) From 682be48e8d3be5f6379b0a9eb1f23cb8f1a26c3b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 12 Nov 2023 00:01:21 +0100 Subject: [PATCH 017/296] fix(InstanceManager): instance_service should be service file name remove debug prints Signed-off-by: Dominik Willner --- kiauh/instance_manager/instance_manager.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/kiauh/instance_manager/instance_manager.py b/kiauh/instance_manager/instance_manager.py index 56b886b..c2acb14 100644 --- a/kiauh/instance_manager/instance_manager.py +++ b/kiauh/instance_manager/instance_manager.py @@ -48,7 +48,7 @@ class InstanceManager: def current_instance(self, value: I) -> None: self._current_instance = value self.instance_suffix = value.suffix - self.instance_service = value.get_service_file_path() + self.instance_service = value.get_service_file_name() self.instance_service_path = value.get_service_file_path() @property @@ -77,15 +77,9 @@ class InstanceManager: @property def instances(self) -> List[I]: - print("instances getter called") if not self._instances: - print("instances none") self._instances = self._find_instances() - print("return instances") - print(self._instances) - for instance in self._instances: - print(type(instance), instance.suffix) return sorted(self._instances, key=lambda x: self._sort_instance_list(x.suffix)) @instances.setter From 2148d95cf43f1511fa1efce6e8a20a38998e0ea4 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 12 Nov 2023 00:10:52 +0100 Subject: [PATCH 018/296] fix(InstanceManager): return None for suffix if there is none Signed-off-by: Dominik Willner --- kiauh/instance_manager/instance_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/instance_manager/instance_manager.py b/kiauh/instance_manager/instance_manager.py index c2acb14..da75ba9 100644 --- a/kiauh/instance_manager/instance_manager.py +++ b/kiauh/instance_manager/instance_manager.py @@ -191,7 +191,7 @@ class InstanceManager: def _get_instance_suffix(self, file_path: str) -> Union[str, None]: full_name = file_path.split("/")[-1].split(".")[0] - return full_name.split("-")[-1] if "-" in full_name else full_name + return full_name.split("-")[-1] if "-" in full_name else None def _sort_instance_list(self, s: Union[int, str, None]): if s is None: From fe8767113b26e2b9ab0af187682dd2fa66085673 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 12 Nov 2023 00:36:37 +0100 Subject: [PATCH 019/296] refactor(klipper): rework dialogs Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_dialogs.py | 40 ++++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/kiauh/modules/klipper/klipper_dialogs.py b/kiauh/modules/klipper/klipper_dialogs.py index 9bb46f1..1dacd59 100644 --- a/kiauh/modules/klipper/klipper_dialogs.py +++ b/kiauh/modules/klipper/klipper_dialogs.py @@ -24,21 +24,21 @@ def print_instance_overview( dialog = textwrap.dedent( f""" /=======================================================\\ - |{'{:^64}'.format(headline)}| + |{headline:^64}| |-------------------------------------------------------| """ )[1:] if show_select_all: select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}" - dialog += f"| {'{:63}'.format(select_all)}|" - dialog += "| |" + dialog += f"| {select_all:<63}|\n" + dialog += "| |\n" for i, s in enumerate(instances): - line = f"{COLOR_CYAN}{'{i})' if show_index else '●'} {s.get_service_file_name()}{RESET_FORMAT}" - dialog += f"| {'{:63}'.format(line)}|" + line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {s.get_service_file_name()}{RESET_FORMAT}" + dialog += f"| {line:<63}|\n" - print(dialog) + print(dialog, end="") print_back_footer() @@ -52,8 +52,8 @@ def print_select_instance_count_dialog(): | up. The number of Klipper instances will determine | | the amount of printers you can run from this host. | | | - | {'{:63}'.format(line1)}| - | {'{:63}'.format(line2)}| + | {line1:<63}| + | {line2:<63}| """ )[1:] @@ -71,8 +71,8 @@ def print_select_custom_name_dialog(): | If skipped, each instance will get an index assigned | | in ascending order, starting at index '1'. | | | - | {'{:63}'.format(line1)}| - | {'{:63}'.format(line2)}| + | {line1:<63}| + | {line2:<63}| """ )[1:] @@ -90,14 +90,14 @@ def print_missing_usergroup_dialog(missing_groups) -> None: dialog = textwrap.dedent( f""" /=======================================================\\ - | {'{:63}'.format(line1)}| + | {line1:<63}| """ )[1:] if "tty" in missing_groups: - dialog += f"| {'{:63}'.format(line2)}|\n" + dialog += f"| {line2:<63}|\n" if "dialout" in missing_groups: - dialog += f"| {'{:63}'.format(line3)}|\n" + dialog += f"| {line3:<63}|\n" dialog += textwrap.dedent( f""" @@ -108,8 +108,8 @@ def print_missing_usergroup_dialog(missing_groups) -> None: | If you want to add the current user to the group(s) | | listed above, answer with 'Y'. Else skip with 'n'. | | | - | {'{:63}'.format(line4)}| - | {'{:63}'.format(line5)}| + | {line4:<63}| + | {line5:<63}| \\=======================================================/ """ )[1:] @@ -117,7 +117,7 @@ def print_missing_usergroup_dialog(missing_groups) -> None: print(dialog, end="") -def print_update_warn_dialog(): +def print_update_warn_dialog() -> None: line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" line2 = f"{COLOR_YELLOW}Do NOT continue if there are ongoing prints running!{RESET_FORMAT}" line3 = f"{COLOR_YELLOW}All Klipper instances will be restarted during the {RESET_FORMAT}" @@ -125,10 +125,10 @@ def print_update_warn_dialog(): dialog = textwrap.dedent( f""" /=======================================================\\ - | {'{:63}'.format(line1)}| - | {'{:63}'.format(line2)}| - | {'{:63}'.format(line3)}| - | {'{:63}'.format(line4)}| + | {line1:<63}| + | {line2:<63}| + | {line3:<63}| + | {line4:<63}| \\=======================================================/ """ )[1:] From eaf12db27ee43c1ccb631c381ef94fef72e546bd Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 12 Nov 2023 00:39:10 +0100 Subject: [PATCH 020/296] fix(klipper): allow go back when asked for new instances Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index e19fb99..3e612aa 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -83,7 +83,7 @@ def handle_existing_instances(instance_list: List[Klipper]) -> bool: if instance_count > 0: print_instance_overview(instance_list) - if not get_confirm("Add new instances?"): + if not get_confirm("Add new instances?", allow_go_back=True): return False return True From be805c169b0d5974c4e7f5fd16d1595087592448 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 12 Nov 2023 01:13:39 +0100 Subject: [PATCH 021/296] feat(klipper): allow keeping klipper and klipper-env dir during uninstall Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 3e612aa..c4a90a1 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -195,11 +195,17 @@ def set_instance_suffix( def remove_single_instance( instance_manager: InstanceManager, instance_list: List[Klipper] ) -> None: + question = f"Delete {KLIPPER_DIR} and {KLIPPER_ENV_DIR}?" + del_remnants = get_confirm(question, allow_go_back=True) + if del_remnants is None: + Logger.print_info("Exiting Klipper Uninstaller ...") + return + try: instance_manager.current_instance = instance_list[0] instance_manager.stop_instance() instance_manager.disable_instance() - instance_manager.delete_instance(del_remnants=True) + instance_manager.delete_instance(del_remnants=del_remnants) instance_manager.reload_daemon() except (OSError, subprocess.CalledProcessError): Logger.print_error("Removing instance failed!") @@ -215,22 +221,26 @@ def remove_multi_instance( options.extend(["a", "A", "b", "B"]) selection = get_selection_input("Select Klipper instance to remove", options) - print(selection) if selection == "b".lower(): return elif selection == "a".lower(): + question = f"Delete {KLIPPER_DIR} and {KLIPPER_ENV_DIR}?" + del_remnants = get_confirm(question, allow_go_back=True) + if del_remnants is None: + Logger.print_info("Exiting Klipper Uninstaller ...") + return + Logger.print_info("Removing all Klipper instances ...") for instance in instance_list: instance_manager.current_instance = instance instance_manager.stop_instance() instance_manager.disable_instance() - instance_manager.delete_instance(del_remnants=True) + instance_manager.delete_instance(del_remnants=del_remnants) else: instance = instance_list[int(selection)] - Logger.print_info( - f"Removing Klipper instance: {instance.get_service_file_name()}" - ) + log = f"Removing Klipper instance: {instance.get_service_file_name()}" + Logger.print_info(log) instance_manager.current_instance = instance instance_manager.stop_instance() instance_manager.disable_instance() From 6ed5395f17a5e99dabcfcbdde19b1eedc6ae603b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 12 Nov 2023 01:26:43 +0100 Subject: [PATCH 022/296] feat(klipper): check for brltty-udev too Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_utils.py | 30 +++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 3bb8b04..1a93037 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -142,32 +142,36 @@ def check_user_groups(): def handle_disruptive_system_packages() -> None: services = [] - brltty_status = subprocess.run( - ["systemctl", "is-enabled", "brltty"], capture_output=True, text=True - ) - modem_manager_status = subprocess.run( - ["systemctl", "is-enabled", "ModemManager"], capture_output=True, text=True - ) + + command = ["systemctl", "is-enabled", "brltty"] + brltty_status = subprocess.run(command, capture_output=True, text=True) + + command = ["systemctl", "is-enabled", "brltty-udev"] + brltty_udev_status = subprocess.run(command, capture_output=True, text=True) + + command = ["systemctl", "is-enabled", "ModemManager"] + modem_manager_status = subprocess.run(command, capture_output=True, text=True) if "enabled" in brltty_status.stdout: services.append("brltty") + if "enabled" in brltty_udev_status.stdout: + services.append("brltty-udev") if "enabled" in modem_manager_status.stdout: services.append("ModemManager") for service in services if services else []: try: - Logger.print_info( - f"{service} service detected! Masking {service} service ..." - ) + log = f"{service} service detected! Masking {service} service ..." + Logger.print_info(log) mask_system_service(service) Logger.print_ok(f"{service} service masked!") except subprocess.CalledProcessError: warn_msg = textwrap.dedent( f""" - KIAUH was unable to mask the {service} system service. - Please fix the problem manually. Otherwise, this may have - undesirable effects on the operation of Klipper. - """ + KIAUH was unable to mask the {service} system service. + Please fix the problem manually. Otherwise, this may have + undesirable effects on the operation of Klipper. + """ )[1:] Logger.print_warn(warn_msg) From d0d2404132fcc864bbf0339a946b94b7e1122e49 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 12 Nov 2023 23:28:05 +0100 Subject: [PATCH 023/296] refactor(kiauh): move core modules to core package Signed-off-by: Dominik Willner --- kiauh/{config_manager => core}/__init__.py | 0 .../config_manager}/__init__.py | 0 kiauh/{ => core}/config_manager/config_manager.py | 0 kiauh/{menus => core/instance_manager}/__init__.py | 0 kiauh/{ => core}/instance_manager/base_instance.py | 0 .../{ => core}/instance_manager/instance_manager.py | 2 +- kiauh/{repo_manager => core/menus}/__init__.py | 0 kiauh/{ => core}/menus/advanced_menu.py | 2 +- kiauh/{ => core}/menus/base_menu.py | 0 kiauh/{ => core}/menus/install_menu.py | 2 +- kiauh/{ => core}/menus/main_menu.py | 12 ++++++------ kiauh/{ => core}/menus/remove_menu.py | 2 +- kiauh/{ => core}/menus/settings_menu.py | 2 +- kiauh/{ => core}/menus/update_menu.py | 2 +- kiauh/core/repo_manager/__init__.py | 0 kiauh/{ => core}/repo_manager/repo_manager.py | 0 kiauh/main.py | 2 +- kiauh/modules/klipper/klipper.py | 2 +- kiauh/modules/klipper/klipper_dialogs.py | 4 ++-- kiauh/modules/klipper/klipper_setup.py | 6 +++--- kiauh/modules/klipper/klipper_utils.py | 2 +- 21 files changed, 20 insertions(+), 20 deletions(-) rename kiauh/{config_manager => core}/__init__.py (100%) rename kiauh/{instance_manager => core/config_manager}/__init__.py (100%) rename kiauh/{ => core}/config_manager/config_manager.py (100%) rename kiauh/{menus => core/instance_manager}/__init__.py (100%) rename kiauh/{ => core}/instance_manager/base_instance.py (100%) rename kiauh/{ => core}/instance_manager/instance_manager.py (99%) rename kiauh/{repo_manager => core/menus}/__init__.py (100%) rename kiauh/{ => core}/menus/advanced_menu.py (97%) rename kiauh/{ => core}/menus/base_menu.py (100%) rename kiauh/{ => core}/menus/install_menu.py (98%) rename kiauh/{ => core}/menus/main_menu.py (89%) rename kiauh/{ => core}/menus/remove_menu.py (98%) rename kiauh/{ => core}/menus/settings_menu.py (96%) rename kiauh/{ => core}/menus/update_menu.py (98%) create mode 100644 kiauh/core/repo_manager/__init__.py rename kiauh/{ => core}/repo_manager/repo_manager.py (100%) diff --git a/kiauh/config_manager/__init__.py b/kiauh/core/__init__.py similarity index 100% rename from kiauh/config_manager/__init__.py rename to kiauh/core/__init__.py diff --git a/kiauh/instance_manager/__init__.py b/kiauh/core/config_manager/__init__.py similarity index 100% rename from kiauh/instance_manager/__init__.py rename to kiauh/core/config_manager/__init__.py diff --git a/kiauh/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py similarity index 100% rename from kiauh/config_manager/config_manager.py rename to kiauh/core/config_manager/config_manager.py diff --git a/kiauh/menus/__init__.py b/kiauh/core/instance_manager/__init__.py similarity index 100% rename from kiauh/menus/__init__.py rename to kiauh/core/instance_manager/__init__.py diff --git a/kiauh/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py similarity index 100% rename from kiauh/instance_manager/base_instance.py rename to kiauh/core/instance_manager/base_instance.py diff --git a/kiauh/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py similarity index 99% rename from kiauh/instance_manager/instance_manager.py rename to kiauh/core/instance_manager/instance_manager.py index da75ba9..3318d45 100644 --- a/kiauh/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -14,7 +14,7 @@ import re import subprocess from typing import List, Optional, Union, TypeVar -from kiauh.instance_manager.base_instance import BaseInstance +from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger diff --git a/kiauh/repo_manager/__init__.py b/kiauh/core/menus/__init__.py similarity index 100% rename from kiauh/repo_manager/__init__.py rename to kiauh/core/menus/__init__.py diff --git a/kiauh/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py similarity index 97% rename from kiauh/menus/advanced_menu.py rename to kiauh/core/menus/advanced_menu.py index 9b0ca85..b029d4a 100644 --- a/kiauh/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -11,7 +11,7 @@ import textwrap -from kiauh.menus.base_menu import BaseMenu +from kiauh.core.menus.base_menu import BaseMenu from kiauh.utils.constants import COLOR_YELLOW, RESET_FORMAT diff --git a/kiauh/menus/base_menu.py b/kiauh/core/menus/base_menu.py similarity index 100% rename from kiauh/menus/base_menu.py rename to kiauh/core/menus/base_menu.py diff --git a/kiauh/menus/install_menu.py b/kiauh/core/menus/install_menu.py similarity index 98% rename from kiauh/menus/install_menu.py rename to kiauh/core/menus/install_menu.py index 54b5148..f1b299e 100644 --- a/kiauh/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -11,7 +11,7 @@ import textwrap -from kiauh.menus.base_menu import BaseMenu +from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT diff --git a/kiauh/menus/main_menu.py b/kiauh/core/menus/main_menu.py similarity index 89% rename from kiauh/menus/main_menu.py rename to kiauh/core/menus/main_menu.py index 9cffebb..76bcd8b 100644 --- a/kiauh/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -11,12 +11,12 @@ import textwrap -from kiauh.menus.advanced_menu import AdvancedMenu -from kiauh.menus.base_menu import BaseMenu -from kiauh.menus.install_menu import InstallMenu -from kiauh.menus.remove_menu import RemoveMenu -from kiauh.menus.settings_menu import SettingsMenu -from kiauh.menus.update_menu import UpdateMenu +from kiauh.core.menus.advanced_menu import AdvancedMenu +from kiauh.core.menus.base_menu import BaseMenu +from kiauh.core.menus.install_menu import InstallMenu +from kiauh.core.menus.remove_menu import RemoveMenu +from kiauh.core.menus.settings_menu import SettingsMenu +from kiauh.core.menus.update_menu import UpdateMenu from kiauh.utils.constants import COLOR_MAGENTA, COLOR_CYAN, RESET_FORMAT diff --git a/kiauh/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py similarity index 98% rename from kiauh/menus/remove_menu.py rename to kiauh/core/menus/remove_menu.py index 1fb2492..861ca48 100644 --- a/kiauh/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -11,7 +11,7 @@ import textwrap -from kiauh.menus.base_menu import BaseMenu +from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup from kiauh.utils.constants import COLOR_RED, RESET_FORMAT diff --git a/kiauh/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py similarity index 96% rename from kiauh/menus/settings_menu.py rename to kiauh/core/menus/settings_menu.py index dcef3df..d56f284 100644 --- a/kiauh/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -9,7 +9,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from kiauh.menus.base_menu import BaseMenu +from kiauh.core.menus.base_menu import BaseMenu # noinspection PyMethodMayBeStatic diff --git a/kiauh/menus/update_menu.py b/kiauh/core/menus/update_menu.py similarity index 98% rename from kiauh/menus/update_menu.py rename to kiauh/core/menus/update_menu.py index f617fe9..eebe8ee 100644 --- a/kiauh/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -11,7 +11,7 @@ import textwrap -from kiauh.menus.base_menu import BaseMenu +from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper.klipper_setup import update_klipper from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT diff --git a/kiauh/core/repo_manager/__init__.py b/kiauh/core/repo_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py similarity index 100% rename from kiauh/repo_manager/repo_manager.py rename to kiauh/core/repo_manager/repo_manager.py diff --git a/kiauh/main.py b/kiauh/main.py index 2bab667..5f826c8 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -9,7 +9,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from kiauh.menus.main_menu import MainMenu +from kiauh.core.menus.main_menu import MainMenu from kiauh.utils.logger import Logger diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 33c4ec3..2d6c82f 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -15,7 +15,7 @@ import subprocess from pathlib import Path from typing import List -from kiauh.instance_manager.base_instance import BaseInstance +from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger diff --git a/kiauh/modules/klipper/klipper_dialogs.py b/kiauh/modules/klipper/klipper_dialogs.py index 1dacd59..cad0cf7 100644 --- a/kiauh/modules/klipper/klipper_dialogs.py +++ b/kiauh/modules/klipper/klipper_dialogs.py @@ -12,8 +12,8 @@ import textwrap from typing import List -from kiauh.instance_manager.base_instance import BaseInstance -from kiauh.menus.base_menu import print_back_footer +from kiauh.core.instance_manager.base_instance import BaseInstance +from kiauh.core.menus.base_menu import print_back_footer from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index c4a90a1..070eca8 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -14,8 +14,8 @@ import subprocess from pathlib import Path from typing import List, Union -from kiauh.config_manager.config_manager import ConfigManager -from kiauh.instance_manager.instance_manager import InstanceManager +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.modules.klipper import ( EXIT_KLIPPER_SETUP, DEFAULT_KLIPPER_REPO_URL, @@ -37,7 +37,7 @@ from kiauh.modules.klipper.klipper_utils import ( check_user_groups, handle_single_to_multi_conversion, ) -from kiauh.repo_manager.repo_manager import RepoManager +from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.utils.input_utils import ( get_confirm, get_number_input, diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 1a93037..7a1b241 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -17,7 +17,7 @@ import textwrap from typing import List, Union -from kiauh.instance_manager.instance_manager import InstanceManager +from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.klipper.klipper_dialogs import ( print_missing_usergroup_dialog, From 47121f68755083a906fd6e5743b0914e2889752a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 13 Nov 2023 20:06:48 +0100 Subject: [PATCH 024/296] refactor(utils): clean up, add comments Signed-off-by: Dominik Willner --- kiauh/utils/system_utils.py | 89 ++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 73ad1b0..6b22334 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -17,23 +17,15 @@ import time from pathlib import Path from typing import List -from kiauh.utils.constants import COLOR_RED, RESET_FORMAT from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger -def kill(opt_err_msg=None) -> None: +def kill(opt_err_msg: str = "") -> None: """ - Kill the application. - - Parameters - ---------- - opt_err_msg : str - optional, additional error message to display - - Returns - ---------- - None + Kills the application | + :param opt_err_msg: an optional, additional error message + :return: None """ if opt_err_msg: @@ -42,7 +34,14 @@ def kill(opt_err_msg=None) -> None: sys.exit(1) -def parse_packages_from_file(source_file) -> List[str]: +def parse_packages_from_file(source_file: Path) -> List[str]: + """ + Read the package names from bash scripts, when defined like: + PKGLIST="package1 package2 package3" | + :param source_file: path of the sourcefile to read from + :return: a list of package names + """ + packages = [] print("Reading dependencies...") with open(source_file, "r") as file: @@ -53,38 +52,47 @@ def parse_packages_from_file(source_file) -> List[str]: line = line.replace("PKGLIST=", "") line = line.replace("${PKGLIST}", "") packages.extend(line.split()) + return packages def create_python_venv(target: Path) -> None: + """ + Create a python 3 virtualenv at the provided target destination | + :param target: Path where to create the virtualenv at + :return: None + """ Logger.print_info("Set up Python virtual environment ...") if not target.exists(): try: command = ["python3", "-m", "venv", f"{target}"] result = subprocess.run(command, stderr=subprocess.PIPE, text=True) if result.returncode != 0 or result.stderr: - print(f"{COLOR_RED}{result.stderr}{RESET_FORMAT}") + Logger.print_error(f"{result.stderr}", prefix=False) Logger.print_error("Setup of virtualenv failed!") return Logger.print_ok("Setup of virtualenv successfull!") except subprocess.CalledProcessError as e: - print("Error setting up virtualenv:", e.output.decode()) + Logger.print_error(f"Error setting up virtualenv:\n{e.output.decode()}") else: - overwrite_venv = get_confirm("Virtualenv already exists. Re-create?") - if overwrite_venv: + if get_confirm("Virtualenv already exists. Re-create?"): try: shutil.rmtree(target) create_python_venv(target) except OSError as e: - Logger.print_error( - f"Error removing existing virtualenv: {e.strerror}", False - ) + log = f"Error removing existing virtualenv: {e.strerror}" + Logger.print_error(log, False) else: - print("Skipping re-creation of virtualenv ...") + Logger.print_info("Skipping re-creation of virtualenv ...") def update_python_pip(target: Path) -> None: + """ + Updates pip in the provided target destination | + :param target: Path of the virtualenv + :return: None + """ Logger.print_info("Updating pip ...") try: command = [f"{target}/bin/pip", "install", "-U", "pip"] @@ -96,10 +104,16 @@ def update_python_pip(target: Path) -> None: Logger.print_ok("Updating pip successfull!") except subprocess.CalledProcessError as e: - print("Error updating pip:", e.output.decode()) + Logger.print_error(f"Error updating pip:\n{e.output.decode()}") def install_python_requirements(target: Path, requirements: Path) -> None: + """ + Installs the python packages based on a provided requirements.txt | + :param target: Path of the virtualenv + :param requirements: Path to the requirements.txt file + :return: None + """ update_python_pip(target) Logger.print_info("Installing Python requirements ...") try: @@ -112,10 +126,17 @@ def install_python_requirements(target: Path, requirements: Path) -> None: Logger.print_ok("Installing Python requirements successfull!") except subprocess.CalledProcessError as e: - print("Error installing Python requirements:", e.output.decode()) + log = f"Error installing Python requirements:\n{e.output.decode()}" + Logger.print_error(log) def update_system_package_lists(silent: bool, rls_info_change=False) -> None: + """ + Updates the systems package list | + :param silent: Log info to the console or not + :param rls_info_change: Flag for "--allow-releaseinfo-change" + :return: None + """ cache_mtime = 0 cache_files = ["/var/lib/apt/periodic/update-success-stamp", "/var/lib/apt/lists"] for cache_file in cache_files: @@ -129,7 +150,7 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: return if not silent: - print("Updating package list...") + Logger.print_info("Updating package list...") try: command = ["sudo", "apt-get", "update"] @@ -148,6 +169,11 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: def install_system_packages(packages: List) -> None: + """ + Installs a list of system packages | + :param packages: List of system package names + :return: None + """ try: command = ["sudo", "apt-get", "install", "-y"] for pkg in packages: @@ -160,6 +186,11 @@ def install_system_packages(packages: List) -> None: def create_directory(_dir: Path) -> None: + """ + Helper function for creating a directory or skipping if it already exists | + :param _dir: the directory to create + :return: None + """ try: if not os.path.isdir(_dir): Logger.print_info(f"Create directory: {_dir}") @@ -173,11 +204,15 @@ def create_directory(_dir: Path) -> None: def mask_system_service(service_name: str) -> None: + """ + Mask a system service to prevent it from starting | + :param service_name: name of the service to mask + :return: None + """ try: command = ["sudo", "systemctl", "mask", service_name] subprocess.run(command, stderr=subprocess.PIPE, check=True) except subprocess.CalledProcessError as e: - Logger.print_error( - f"Unable to mask system service {service_name}: {e.stderr.decode()}" - ) + log = f"Unable to mask system service {service_name}: {e.stderr.decode()}" + Logger.print_error(log) raise From 1392ca9f82af2a6dbce6edda860c43de76b70698 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 13 Nov 2023 20:07:21 +0100 Subject: [PATCH 025/296] refactor(klipper): pass the script path as a Path to the parse function Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 070eca8..fea9b6d 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -155,7 +155,7 @@ def setup_klipper_prerequesites() -> None: def install_klipper_packages(klipper_dir: Path) -> None: - script = f"{klipper_dir}/scripts/install-debian.sh" + script = Path(f"{klipper_dir}/scripts/install-debian.sh") packages = parse_packages_from_file(script) packages = [pkg.replace("python-dev", "python3-dev") for pkg in packages] # Add dfu-util for octopi-images From a4a3d5eecb96856c3d18544490cf3940d07d1750 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Tue, 14 Nov 2023 21:28:13 +0100 Subject: [PATCH 026/296] feat(BackupManager): implement simple backup manager Signed-off-by: Dominik Willner --- kiauh/__init__.py | 14 +++++ kiauh/core/backup_manager/__init__.py | 0 kiauh/core/backup_manager/backup_manager.py | 69 +++++++++++++++++++++ kiauh/core/config_manager/config_manager.py | 17 +++-- kiauh/modules/klipper/klipper_setup.py | 15 +++-- kiauh/utils/common.py | 25 ++++++++ kiauh/utils/constants.py | 3 + 7 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 kiauh/core/backup_manager/__init__.py create mode 100644 kiauh/core/backup_manager/backup_manager.py create mode 100644 kiauh/utils/common.py diff --git a/kiauh/__init__.py b/kiauh/__init__.py index e69de29..f670517 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -0,0 +1,14 @@ +# !/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 os.path import dirname, abspath + +APPLICATION_ROOT = dirname(dirname(abspath(__file__))) diff --git a/kiauh/core/backup_manager/__init__.py b/kiauh/core/backup_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py new file mode 100644 index 0000000..8a333a5 --- /dev/null +++ b/kiauh/core/backup_manager/backup_manager.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import shutil +from pathlib import Path + +from kiauh.utils.common import get_current_date +from kiauh.utils.constants import KIAUH_BACKUP_DIR +from kiauh.utils.logger import Logger + + +# noinspection PyMethodMayBeStatic +class BackupManager: + def __init__( + self, backup_name: str, source: Path, backup_dir: Path = KIAUH_BACKUP_DIR + ): + self._backup_name = backup_name + self._source = source + self._backup_dir = backup_dir + + @property + def backup_name(self) -> str: + return self._backup_name + + @backup_name.setter + def backup_name(self, value: str): + self._backup_name = value + + @property + def source(self) -> Path: + return self._source + + @source.setter + def source(self, value: Path): + self._source = value + + @property + def backup_dir(self) -> Path: + return self._backup_dir + + @backup_dir.setter + def backup_dir(self, value: Path): + self._backup_dir = value + + def backup(self) -> None: + if self._source is None or not Path(self._source).exists(): + raise OSError + + try: + log = f"Creating backup of {self.backup_name} in {self.backup_dir} ..." + Logger.print_info(log) + date = get_current_date() + dest = Path( + f"{self.backup_dir}/{self.backup_name}/{date.get('date')}-{date.get('time')}" + ) + shutil.copytree(src=self.source, dst=dest) + except OSError as e: + Logger.print_error(f"Unable to backup source directory. Not exist.\n{e}") + return + + Logger.print_ok("Backup successfull!") diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index 0f80647..0237a27 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -14,6 +14,7 @@ import configparser from pathlib import Path from typing import Union +from kiauh import APPLICATION_ROOT from kiauh.utils.logger import Logger @@ -34,7 +35,7 @@ class ConfigManager: with open(self.config_file, "w") as cfg: self.config.write(cfg) - def get_value(self, section: str, key: str) -> Union[str, None]: + def get_value(self, section: str, key: str) -> Union[str, bool, None]: if not self.config.has_section(section): log = f"Section not defined. Unable to read section: [{section}]." Logger.print_error(log) @@ -45,17 +46,21 @@ class ConfigManager: Logger.print_error(log) return None - return self.config.get(section, key) + value = self.config.get(section, key) + if value == "True" or value == "true": + return True + elif value == "False" or value == "false": + return False + else: + return value def set_value(self, section: str, key: str, value: str): self.config.set(section, key, value) - def check_config_exists(self) -> bool: + def check_config_exist(self) -> bool: return True if self._get_cfg_location() else False def _get_cfg_location(self) -> str: - current_dir = os.path.dirname(os.path.abspath(__file__)) - project_dir = os.path.dirname(os.path.dirname(current_dir)) - cfg_path = os.path.join(project_dir, "kiauh.cfg") + cfg_path = os.path.join(APPLICATION_ROOT, "kiauh.cfg") return cfg_path if Path(cfg_path).exists() else None diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index fea9b6d..e7c4d52 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -14,6 +14,7 @@ import subprocess from pathlib import Path from typing import List, Union +from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.modules.klipper import ( @@ -251,16 +252,22 @@ def remove_multi_instance( def update_klipper() -> None: print_update_warn_dialog() - if not get_confirm("Update Klipper now?"): return - instance_manager = InstanceManager(Klipper) - instance_manager.stop_all_instance() - cm = ConfigManager() cm.read_config() + if cm.get_value("kiauh", "backup_before_update"): + backup_manager = BackupManager(source=KLIPPER_DIR, backup_name="klipper") + backup_manager.backup() + backup_manager.backup_name = "klippy-env" + backup_manager.source = KLIPPER_ENV_DIR + backup_manager.backup() + + instance_manager = InstanceManager(Klipper) + instance_manager.stop_all_instance() + repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL) branch = str(cm.get_value("klipper", "branch") or "master") diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py new file mode 100644 index 0000000..bc9a9fb --- /dev/null +++ b/kiauh/utils/common.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 datetime import datetime +from typing import Dict, Literal + + +def get_current_date() -> Dict[Literal["date", "time"], str]: + """ + Get the current date | + :return: a Dict holding a date and time key:value pair + """ + now: datetime = datetime.today() + date: str = now.strftime("%Y-%m-%d") + time: str = now.strftime("%H-%M-%S") + + return {"date": date, "time": time} diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py index 60662ce..08a4398 100644 --- a/kiauh/utils/constants.py +++ b/kiauh/utils/constants.py @@ -11,6 +11,7 @@ import os import pwd +from pathlib import Path # text colors and formats COLOR_MAGENTA = "\033[35m" # magenta @@ -19,6 +20,8 @@ COLOR_YELLOW = "\033[93m" # bright yellow COLOR_RED = "\033[91m" # bright red COLOR_CYAN = "\033[96m" # bright cyan RESET_FORMAT = "\033[0m" # reset format +# kiauh +KIAUH_BACKUP_DIR = f"{Path.home()}/kiauh-backups" # current user CURRENT_USER = pwd.getpwuid(os.getuid())[0] SYSTEMD = "/etc/systemd/system" From 279d000bb050b22755ffd89611ee5f2941d3ed5a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Tue, 14 Nov 2023 21:31:25 +0100 Subject: [PATCH 027/296] refactor(kiauh): specify python3 in shebang Signed-off-by: Dominik Willner --- kiauh.py | 2 +- kiauh/__init__.py | 2 +- kiauh/core/backup_manager/backup_manager.py | 2 +- kiauh/core/config_manager/config_manager.py | 2 +- kiauh/core/instance_manager/base_instance.py | 2 +- kiauh/core/instance_manager/instance_manager.py | 2 +- kiauh/core/menus/advanced_menu.py | 2 +- kiauh/core/menus/base_menu.py | 2 +- kiauh/core/menus/install_menu.py | 2 +- kiauh/core/menus/main_menu.py | 2 +- kiauh/core/menus/remove_menu.py | 2 +- kiauh/core/menus/settings_menu.py | 2 +- kiauh/core/menus/update_menu.py | 2 +- kiauh/core/repo_manager/repo_manager.py | 2 +- kiauh/main.py | 2 +- kiauh/modules/klipper/__init__.py | 2 +- kiauh/modules/klipper/klipper.py | 2 +- kiauh/modules/klipper/klipper_dialogs.py | 2 +- kiauh/modules/klipper/klipper_setup.py | 2 +- kiauh/modules/klipper/klipper_utils.py | 2 +- kiauh/utils/__init__.py | 2 +- kiauh/utils/common.py | 2 +- kiauh/utils/constants.py | 2 +- kiauh/utils/input_utils.py | 2 +- kiauh/utils/logger.py | 2 +- kiauh/utils/system_utils.py | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/kiauh.py b/kiauh.py index ec12ca5..227775f 100644 --- a/kiauh.py +++ b/kiauh.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/__init__.py b/kiauh/__init__.py index f670517..d30786d 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -1,4 +1,4 @@ -# !/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 8a333a5..613ca61 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index 0237a27..a1cd757 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index df7e92f..2fdb188 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 3318d45..a497425 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index b029d4a..6cda33f 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 799b5af..7bb39aa 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index f1b299e..f30b676 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 76bcd8b..be292da 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 861ca48..83f0c07 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index d56f284..8cb7723 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index eebe8ee..bf5bbd3 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index 4555920..9b166cf 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/main.py b/kiauh/main.py index 5f826c8..e064aec 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/modules/klipper/__init__.py b/kiauh/modules/klipper/__init__.py index a9b4efc..2b0d427 100644 --- a/kiauh/modules/klipper/__init__.py +++ b/kiauh/modules/klipper/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 2d6c82f..e071566 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -1,4 +1,4 @@ -# !/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/modules/klipper/klipper_dialogs.py b/kiauh/modules/klipper/klipper_dialogs.py index cad0cf7..d3b86bd 100644 --- a/kiauh/modules/klipper/klipper_dialogs.py +++ b/kiauh/modules/klipper/klipper_dialogs.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index e7c4d52..725a370 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -1,4 +1,4 @@ -# !/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 7a1b241..59059ca 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index 3c3f6a5..203c66a 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index bc9a9fb..4b6a48a 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py index 08a4398..1c0b90e 100644 --- a/kiauh/utils/constants.py +++ b/kiauh/utils/constants.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 280088d..dcddda0 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index da54fb7..7c5e7f5 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 6b22334..868078b 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # From 6128e35d451fd8c9c9c0721964fc4745acda4e55 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 15 Nov 2023 00:25:19 +0100 Subject: [PATCH 028/296] refactor(kiauh): rework menu formatting logic Signed-off-by: Dominik Willner --- kiauh/core/menus/__init__.py | 14 ++++++ kiauh/core/menus/advanced_menu.py | 8 +++- kiauh/core/menus/base_menu.py | 77 ++++++++++++++----------------- kiauh/core/menus/install_menu.py | 12 ++--- kiauh/core/menus/main_menu.py | 17 ++++--- kiauh/core/menus/remove_menu.py | 8 +++- kiauh/core/menus/update_menu.py | 8 +++- 7 files changed, 82 insertions(+), 62 deletions(-) diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index e69de29..23cd836 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +QUIT_FOOTER = "quit" +BACK_FOOTER = "back" +BACK_HELP_FOOTER = "back_help" diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 6cda33f..038409a 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -11,19 +11,23 @@ import textwrap +from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.utils.constants import COLOR_YELLOW, RESET_FORMAT class AdvancedMenu(BaseMenu): def __init__(self): - super().__init__(header=True, options={}, footer_type="back") + super().__init__(header=True, options={}, footer_type=BACK_FOOTER) def print_menu(self): + header = " [ Advanced Menu ] " + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" /=======================================================\\ - | {COLOR_YELLOW}~~~~~~~~~~~~~ [ Advanced Menu ] ~~~~~~~~~~~~~{RESET_FORMAT} | + | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| | Klipper & API: | Mainsail: | | 0) [Rollback] | 5) [Theme installer] | diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 7bb39aa..1950e4b 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -13,8 +13,9 @@ import subprocess import sys import textwrap from abc import abstractmethod, ABC -from typing import Dict, Any +from typing import Dict, Any, Literal +from kiauh.core.menus import QUIT_FOOTER, BACK_FOOTER, BACK_HELP_FOOTER from kiauh.utils.constants import ( COLOR_GREEN, COLOR_YELLOW, @@ -29,12 +30,17 @@ def clear(): def print_header(): + line1 = " [ KIAUH ] " + line2 = "Klipper Installation And Update Helper" + line3 = "" + color = COLOR_CYAN + count = 62 - len(color) - len(RESET_FORMAT) header = textwrap.dedent( f""" /=======================================================\\ - | {COLOR_CYAN}~~~~~~~~~~~~~~~~~ [ KIAUH ] ~~~~~~~~~~~~~~~~~{RESET_FORMAT} | - | {COLOR_CYAN} Klipper Installation And Update Helper {RESET_FORMAT} | - | {COLOR_CYAN}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{RESET_FORMAT} | + | {color}{line1:~^{count}}{RESET_FORMAT} | + | {color}{line2:^{count}}{RESET_FORMAT} | + | {color}{line3:~^{count}}{RESET_FORMAT} | \=======================================================/ """ )[1:] @@ -42,10 +48,13 @@ def print_header(): def print_quit_footer(): + text = "Q) Quit" + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) footer = textwrap.dedent( f""" |-------------------------------------------------------| - | {COLOR_RED}Q) Quit{RESET_FORMAT} | + | {color}{text:^{count}}{RESET_FORMAT} | \=======================================================/ """ )[1:] @@ -53,10 +62,13 @@ def print_quit_footer(): def print_back_footer(): + text = "B) « Back" + color = COLOR_GREEN + count = 62 - len(color) - len(RESET_FORMAT) footer = textwrap.dedent( f""" |-------------------------------------------------------| - | {COLOR_GREEN}B) « Back{RESET_FORMAT} | + | {color}{text:^{count}}{RESET_FORMAT} | \=======================================================/ """ )[1:] @@ -64,32 +76,15 @@ def print_back_footer(): def print_back_help_footer(): + text1 = "B) « Back" + text2 = "H) Help [?]" + color1 = COLOR_GREEN + color2 = COLOR_YELLOW + count = 34 - len(color1) - len(RESET_FORMAT) footer = textwrap.dedent( f""" |-------------------------------------------------------| - | {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_RED}Q) Quit{RESET_FORMAT} | - \=======================================================/ - """ - )[1:] - print(footer, end="") - - -def print_back_quit_footer(): - footer = textwrap.dedent( - f""" - |-------------------------------------------------------| - | {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_YELLOW}H) Help [?]{RESET_FORMAT} | - \=======================================================/ - """ - )[1:] - print(footer, end="") - - -def print_back_quit_help_footer(): - footer = textwrap.dedent( - f""" - |-------------------------------------------------------| - | {COLOR_GREEN}B) « Back{RESET_FORMAT} | {COLOR_RED}Q) Quit{RESET_FORMAT} | {COLOR_YELLOW}H) Help [?]{RESET_FORMAT} | + | {color1}{text1:^{count}}{RESET_FORMAT} | {color2}{text2:^{count}}{RESET_FORMAT} | \=======================================================/ """ )[1:] @@ -97,14 +92,14 @@ def print_back_quit_help_footer(): class BaseMenu(ABC): - QUIT_FOOTER = "quit" - BACK_FOOTER = "back" - BACK_HELP_FOOTER = "back_help" - BACK_QUIT_FOOTER = "back_quit" - BACK_QUIT_HELP_FOOTER = "back_quit_help" - def __init__( - self, options: Dict[int, Any], options_offset=0, header=True, footer_type="quit" + self, + options: Dict[int, Any], + options_offset: int = 0, + header: bool = True, + footer_type: Literal[ + "QUIT_FOOTER", "BACK_FOOTER", "BACK_HELP_FOOTER" + ] = QUIT_FOOTER, ): self.options = options self.options_offset = options_offset @@ -117,11 +112,9 @@ class BaseMenu(ABC): def print_footer(self): footer_type_map = { - self.QUIT_FOOTER: print_quit_footer, - self.BACK_FOOTER: print_back_footer, - self.BACK_HELP_FOOTER: print_back_help_footer, - self.BACK_QUIT_FOOTER: print_back_quit_footer, - self.BACK_QUIT_HELP_FOOTER: print_back_quit_help_footer, + QUIT_FOOTER: print_quit_footer, + BACK_FOOTER: print_back_footer, + BACK_HELP_FOOTER: print_back_help_footer, } footer_function = footer_type_map.get(self.footer_type, print_quit_footer) footer_function() @@ -150,8 +143,6 @@ class BaseMenu(ABC): "quit": ["q"], "back": ["b"], "back_help": ["b", "h"], - "back_quit": ["b", "q"], - "back_quit_help": ["b", "q", "h"], } if ( self.footer_type in allowed_input diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index f30b676..c1e2646 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -11,6 +11,7 @@ import textwrap +from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT @@ -34,18 +35,17 @@ class InstallMenu(BaseMenu): 10: self.install_mobileraker, 11: self.install_crowsnest, }, - footer_type="back", + footer_type=BACK_FOOTER, ) def print_menu(self): + header = " [ Installation Menu ] " + color = COLOR_GREEN + count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" /=======================================================\\ - | {COLOR_GREEN}~~~~~~~~~~~ [ Installation Menu ] ~~~~~~~~~~~{RESET_FORMAT} | - |-------------------------------------------------------| - | You need this menu usually only for installing | - | all necessary dependencies for the various | - | functions on a completely fresh system. | + | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| | Firmware & API: | Other: | | 1) [Klipper] | 6) [PrettyGCode] | diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index be292da..bc77121 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -11,6 +11,7 @@ import textwrap +from kiauh.core.menus import QUIT_FOOTER from kiauh.core.menus.advanced_menu import AdvancedMenu from kiauh.core.menus.base_menu import BaseMenu from kiauh.core.menus.install_menu import InstallMenu @@ -25,7 +26,7 @@ class MainMenu(BaseMenu): super().__init__( header=True, options={ - 0: self.test, + 0: None, 1: InstallMenu, 2: UpdateMenu, 3: RemoveMenu, @@ -33,14 +34,19 @@ class MainMenu(BaseMenu): 5: None, 6: SettingsMenu, }, - footer_type="quit", + footer_type=QUIT_FOOTER, ) def print_menu(self): + header = " [ Main Menu ] " + footer1 = "KIAUH v6.0.0" + footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}" + color = COLOR_CYAN + count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" /=======================================================\\ - | {COLOR_CYAN}~~~~~~~~~~~~~~~ [ Main Menu ] ~~~~~~~~~~~~~~~{RESET_FORMAT} | + | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| | 0) [Log-Upload] | Klipper: | | | Repo: | @@ -58,10 +64,7 @@ class MainMenu(BaseMenu): | | Obico: | | | OctoEverywhere: | |-------------------------------------------------------| - | {COLOR_CYAN}KIAUH v6.0.0{RESET_FORMAT} | Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT} | + | {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} | """ )[1:] print(menu, end="") - - def test(self): - print("blub") diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 83f0c07..51b3d44 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -11,6 +11,7 @@ import textwrap +from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup from kiauh.utils.constants import COLOR_RED, RESET_FORMAT @@ -38,14 +39,17 @@ class RemoveMenu(BaseMenu): 14: self.remove_mobileraker, 15: self.remove_nginx, }, - footer_type="back", + footer_type=BACK_FOOTER, ) def print_menu(self): + header = " [ Remove Menu ] " + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" /=======================================================\\ - | {COLOR_RED}~~~~~~~~~~~~~~ [ Remove Menu ] ~~~~~~~~~~~~~~{RESET_FORMAT} | + | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| | INFO: Configurations and/or any backups will be kept! | |-------------------------------------------------------| diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index bf5bbd3..3413616 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -11,6 +11,7 @@ import textwrap +from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper.klipper_setup import update_klipper from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT @@ -36,14 +37,17 @@ class UpdateMenu(BaseMenu): 11: self.update_crowsnest, 12: self.upgrade_system_packages, }, - footer_type="back", + footer_type=BACK_FOOTER, ) def print_menu(self): + header = " [ Update Menu ] " + color = COLOR_GREEN + count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" /=======================================================\\ - | {COLOR_GREEN}~~~~~~~~~~~~~~ [ Update Menu ] ~~~~~~~~~~~~~~{RESET_FORMAT} | + | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| | 0) [Update all] | | | | | Current: | Latest: | From 458c89a78a6d3bb9cb8ac5ece64202e305f57e3d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 2 Dec 2023 16:52:30 +0100 Subject: [PATCH 029/296] fix(InstanceManager): print service name instead of suffix only Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/instance_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index a497425..eb046fa 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -111,10 +111,10 @@ class InstanceManager: try: command = ["sudo", "systemctl", "enable", self.instance_service] if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_suffix}.service enabled.") + Logger.print_ok(f"{self.instance_service}.service enabled.") except subprocess.CalledProcessError as e: Logger.print_error( - f"Error enabling service {self.instance_suffix}.service:" + f"Error enabling service {self.instance_service}.service:" ) Logger.print_error(f"{e}") From bfb10c742bd2c527a1b2f7ab1789014fa49bd199 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Dec 2023 23:06:30 +0100 Subject: [PATCH 030/296] refactor(kiauh): reword print_info to print_status and implement new print_info method Signed-off-by: Dominik Willner --- kiauh/core/backup_manager/backup_manager.py | 2 +- .../core/instance_manager/instance_manager.py | 41 ++++++++++--------- kiauh/core/repo_manager/repo_manager.py | 10 +++-- kiauh/modules/klipper/klipper.py | 10 ++--- kiauh/modules/klipper/klipper_setup.py | 14 +++---- kiauh/modules/klipper/klipper_utils.py | 4 +- kiauh/utils/constants.py | 1 + kiauh/utils/logger.py | 10 ++++- kiauh/utils/system_utils.py | 15 +++---- 9 files changed, 58 insertions(+), 49 deletions(-) diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 613ca61..ee6b782 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -56,7 +56,7 @@ class BackupManager: try: log = f"Creating backup of {self.backup_name} in {self.backup_dir} ..." - Logger.print_info(log) + Logger.print_status(log) date = get_current_date() dest = Path( f"{self.backup_dir}/{self.backup_name}/{date.get('date')}-{date.get('time')}" diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index eb046fa..fe6455f 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -29,6 +29,7 @@ class InstanceManager: self._current_instance: Optional[I] = None self._instance_suffix: Optional[str] = None self._instance_service: Optional[str] = None + self._instance_service_full: Optional[str] = None self._instance_service_path: Optional[str] = None self._instances: List[I] = [] @@ -67,6 +68,10 @@ class InstanceManager: def instance_service(self, value: str): self._instance_service = value + @property + def instance_service_full(self) -> str: + return f"{self._instance_service}.service" + @property def instance_service_path(self) -> str: return self._instance_service_path @@ -107,35 +112,33 @@ class InstanceManager: raise ValueError("current_instance cannot be None") def enable_instance(self) -> None: - Logger.print_info(f"Enabling {self.instance_service} ...") + Logger.print_status(f"Enabling {self.instance_service_full} ...") try: - command = ["sudo", "systemctl", "enable", self.instance_service] + command = ["sudo", "systemctl", "enable", self.instance_service_full] if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_service}.service enabled.") + Logger.print_ok(f"{self.instance_service_full} enabled.") except subprocess.CalledProcessError as e: - Logger.print_error( - f"Error enabling service {self.instance_service}.service:" - ) + Logger.print_error(f"Error enabling service {self.instance_service_full}:") Logger.print_error(f"{e}") def disable_instance(self) -> None: - Logger.print_info(f"Disabling {self.instance_service} ...") + Logger.print_status(f"Disabling {self.instance_service_full} ...") try: - command = ["sudo", "systemctl", "disable", self.instance_service] + command = ["sudo", "systemctl", "disable", self.instance_service_full] if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_service} disabled.") + Logger.print_ok(f"{self.instance_service_full} disabled.") except subprocess.CalledProcessError as e: - Logger.print_error(f"Error disabling service {self.instance_service}:") + Logger.print_error(f"Error disabling {self.instance_service_full}:") Logger.print_error(f"{e}") def start_instance(self) -> None: - Logger.print_info(f"Starting {self.instance_service} ...") + Logger.print_status(f"Starting {self.instance_service_full} ...") try: - command = ["sudo", "systemctl", "start", self.instance_service] + command = ["sudo", "systemctl", "start", self.instance_service_full] if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_service} started.") + Logger.print_ok(f"{self.instance_service_full} started.") except subprocess.CalledProcessError as e: - Logger.print_error(f"Error starting service {self.instance_service}:") + Logger.print_error(f"Error starting {self.instance_service_full}:") Logger.print_error(f"{e}") def start_all_instance(self) -> None: @@ -144,13 +147,13 @@ class InstanceManager: self.start_instance() def stop_instance(self) -> None: - Logger.print_info(f"Stopping {self.instance_service} ...") + Logger.print_status(f"Stopping {self.instance_service_full} ...") try: - command = ["sudo", "systemctl", "stop", self.instance_service] + command = ["sudo", "systemctl", "stop", self.instance_service_full] if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_service} stopped.") + Logger.print_ok(f"{self.instance_service_full} stopped.") except subprocess.CalledProcessError as e: - Logger.print_error(f"Error stopping service {self.instance_service}:") + Logger.print_error(f"Error stopping {self.instance_service_full}:") Logger.print_error(f"{e}") raise @@ -160,7 +163,7 @@ class InstanceManager: self.stop_instance() def reload_daemon(self) -> None: - Logger.print_info("Reloading systemd manager configuration ...") + Logger.print_status("Reloading systemd manager configuration ...") try: command = ["sudo", "systemctl", "daemon-reload"] if subprocess.run(command, check=True): diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index 9b166cf..ee84c62 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -59,11 +59,13 @@ class RepoManager: def clone_repo(self): log = f"Cloning repository from '{self.repo}' with method '{self.method}'" - Logger.print_info(log) + Logger.print_status(log) try: if os.path.exists(self.target_dir): - if not get_confirm("Target directory already exists. Overwrite?"): - Logger.print_info("Skipping re-clone of repository ...") + if not get_confirm( + "Target directory already exists. Overwrite?", default_choice=False + ): + Logger.print_info("Skipping re-clone of repository.") return shutil.rmtree(self.target_dir) @@ -78,7 +80,7 @@ class RepoManager: return def pull_repo(self) -> None: - Logger.print_info(f"Updating repository '{self.repo}' ...") + Logger.print_status(f"Updating repository '{self.repo}' ...") try: self._pull() except subprocess.CalledProcessError: diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index e071566..e4aa570 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -38,7 +38,7 @@ class Klipper(BaseInstance): self.uds = f"{self.comms_dir}/klippy.sock" def create(self) -> None: - Logger.print_info("Creating new Klipper Instance ...") + Logger.print_status("Creating new Klipper Instance ...") module_path = os.path.dirname(os.path.abspath(__file__)) service_template_path = os.path.join(module_path, "res", "klipper.service") env_template_file_path = os.path.join(module_path, "res", "klipper.env") @@ -66,7 +66,7 @@ class Klipper(BaseInstance): service_file = self.get_service_file_name(extension=True) service_file_path = self.get_service_file_path() - Logger.print_info(f"Deleting Klipper Instance: {service_file}") + Logger.print_status(f"Deleting Klipper Instance: {service_file}") try: command = ["sudo", "rm", "-f", service_file_path] @@ -113,12 +113,12 @@ class Klipper(BaseInstance): def _delete_klipper_remnants(self) -> None: try: - Logger.print_info(f"Delete {self.klipper_dir} ...") + Logger.print_status(f"Delete {self.klipper_dir} ...") shutil.rmtree(Path(self.klipper_dir)) - Logger.print_info(f"Delete {self.env_dir} ...") + Logger.print_status(f"Delete {self.env_dir} ...") shutil.rmtree(Path(self.env_dir)) except FileNotFoundError: - Logger.print_info("Cannot delete Klipper directories. Not found.") + Logger.print_status("Cannot delete Klipper directories. Not found.") except PermissionError as e: Logger.print_error(f"Error deleting Klipper directories: {e}") raise diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 725a370..fb00924 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -67,7 +67,7 @@ def run_klipper_setup(install: bool) -> None: if install: add_additional = handle_existing_instances(instance_list) if is_klipper_installed and not add_additional: - Logger.print_info(EXIT_KLIPPER_SETUP) + Logger.print_status(EXIT_KLIPPER_SETUP) return install_klipper(instance_manager, instance_list) @@ -97,12 +97,12 @@ def install_klipper( question = f"Number of{' additional' if len(instance_list) > 0 else ''} Klipper instances to set up" install_count = get_number_input(question, 1, default=1, allow_go_back=True) if install_count is None: - Logger.print_info(EXIT_KLIPPER_SETUP) + Logger.print_status(EXIT_KLIPPER_SETUP) return instance_names = set_instance_suffix(instance_list, install_count) if instance_names is None: - Logger.print_info(EXIT_KLIPPER_SETUP) + Logger.print_status(EXIT_KLIPPER_SETUP) return if len(instance_list) < 1: @@ -199,7 +199,7 @@ def remove_single_instance( question = f"Delete {KLIPPER_DIR} and {KLIPPER_ENV_DIR}?" del_remnants = get_confirm(question, allow_go_back=True) if del_remnants is None: - Logger.print_info("Exiting Klipper Uninstaller ...") + Logger.print_status("Exiting Klipper Uninstaller ...") return try: @@ -229,10 +229,10 @@ def remove_multi_instance( question = f"Delete {KLIPPER_DIR} and {KLIPPER_ENV_DIR}?" del_remnants = get_confirm(question, allow_go_back=True) if del_remnants is None: - Logger.print_info("Exiting Klipper Uninstaller ...") + Logger.print_status("Exiting Klipper Uninstaller ...") return - Logger.print_info("Removing all Klipper instances ...") + Logger.print_status("Removing all Klipper instances ...") for instance in instance_list: instance_manager.current_instance = instance instance_manager.stop_instance() @@ -241,7 +241,7 @@ def remove_multi_instance( else: instance = instance_list[int(selection)] log = f"Removing Klipper instance: {instance.get_service_file_name()}" - Logger.print_info(log) + Logger.print_status(log) instance_manager.current_instance = instance instance_manager.stop_instance() instance_manager.disable_instance() diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 59059ca..aa11124 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -128,7 +128,7 @@ def check_user_groups(): try: for group in missing_groups: - Logger.print_info(f"Adding user '{CURRENT_USER}' to group {group} ...") + Logger.print_status(f"Adding user '{CURRENT_USER}' to group {group} ...") command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER] subprocess.run(command, check=True) Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.") @@ -162,7 +162,7 @@ def handle_disruptive_system_packages() -> None: for service in services if services else []: try: log = f"{service} service detected! Masking {service} service ..." - Logger.print_info(log) + Logger.print_status(log) mask_system_service(service) Logger.print_ok(f"{service} service masked!") except subprocess.CalledProcessError: diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py index 1c0b90e..37d4a33 100644 --- a/kiauh/utils/constants.py +++ b/kiauh/utils/constants.py @@ -14,6 +14,7 @@ import pwd from pathlib import Path # text colors and formats +COLOR_WHITE = "\033[37m" # white COLOR_MAGENTA = "\033[35m" # magenta COLOR_GREEN = "\033[92m" # bright green COLOR_YELLOW = "\033[93m" # bright yellow diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index 7c5e7f5..864bd77 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -10,6 +10,7 @@ # ======================================================================= # from kiauh.utils.constants import ( + COLOR_WHITE, COLOR_GREEN, COLOR_YELLOW, COLOR_RED, @@ -34,6 +35,11 @@ class Logger: # log to kiauh.log pass + @staticmethod + def print_info(msg, prefix=True, end="\n") -> None: + message = f"[INFO] {msg}" if prefix else msg + print(f"{COLOR_WHITE}{message}{RESET_FORMAT}", end=end) + @staticmethod def print_ok(msg, prefix=True, end="\n") -> None: message = f"[OK] {msg}" if prefix else msg @@ -50,6 +56,6 @@ class Logger: print(f"{COLOR_RED}{message}{RESET_FORMAT}", end=end) @staticmethod - def print_info(msg, prefix=True, end="\n") -> None: - message = f"###### {msg}" if prefix else msg + def print_status(msg, prefix=True, end="\n") -> None: + message = f"\n###### {msg}" if prefix else msg print(f"{COLOR_MAGENTA}{message}{RESET_FORMAT}", end=end) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 868078b..d0d9276 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -62,7 +62,7 @@ def create_python_venv(target: Path) -> None: :param target: Path where to create the virtualenv at :return: None """ - Logger.print_info("Set up Python virtual environment ...") + Logger.print_status("Set up Python virtual environment ...") if not target.exists(): try: command = ["python3", "-m", "venv", f"{target}"] @@ -76,7 +76,7 @@ def create_python_venv(target: Path) -> None: except subprocess.CalledProcessError as e: Logger.print_error(f"Error setting up virtualenv:\n{e.output.decode()}") else: - if get_confirm("Virtualenv already exists. Re-create?"): + if get_confirm("Virtualenv already exists. Re-create?", default_choice=False): try: shutil.rmtree(target) create_python_venv(target) @@ -93,7 +93,7 @@ def update_python_pip(target: Path) -> None: :param target: Path of the virtualenv :return: None """ - Logger.print_info("Updating pip ...") + Logger.print_status("Updating pip ...") try: command = [f"{target}/bin/pip", "install", "-U", "pip"] result = subprocess.run(command, stderr=subprocess.PIPE, text=True) @@ -115,7 +115,7 @@ def install_python_requirements(target: Path, requirements: Path) -> None: :return: None """ update_python_pip(target) - Logger.print_info("Installing Python requirements ...") + Logger.print_status("Installing Python requirements ...") try: command = [f"{target}/bin/pip", "install", "-r", f"{requirements}"] result = subprocess.run(command, stderr=subprocess.PIPE, text=True) @@ -150,7 +150,7 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: return if not silent: - Logger.print_info("Updating package list...") + Logger.print_status("Updating package list...") try: command = ["sudo", "apt-get", "update"] @@ -193,11 +193,8 @@ def create_directory(_dir: Path) -> None: """ try: if not os.path.isdir(_dir): - Logger.print_info(f"Create directory: {_dir}") os.makedirs(_dir, exist_ok=True) - Logger.print_ok("Directory created!") - else: - Logger.print_info(f"Directory already exists: {_dir}\nSkip creation ...") + Logger.print_ok(f"Created directory: {_dir}") except OSError as e: Logger.print_error(f"Error creating folder: {e}") raise From e35e44a76a52baaaadab7190dc4a8a28b8a7b508 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Dec 2023 23:08:41 +0100 Subject: [PATCH 031/296] refactor(kiauh): move create_folders to BaseInstance Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/base_instance.py | 25 ++++++++++++++++++++ kiauh/modules/klipper/klipper.py | 14 +---------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index 2fdb188..9e8143f 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -14,6 +14,7 @@ from pathlib import Path from typing import List, Union, Optional, Type, TypeVar from kiauh.utils.constants import SYSTEMD, CURRENT_USER +from kiauh.utils.system_utils import create_directory B = TypeVar(name="B", bound="BaseInstance", covariant=True) @@ -37,6 +38,7 @@ class BaseInstance(ABC): self._log_dir = f"{self.data_dir}/logs" self._comms_dir = f"{self.data_dir}/comms" self._sysd_dir = f"{self.data_dir}/systemd" + self._gcodes_dir = f"{self.data_dir}/gcodes" @property def instance_type(self) -> Type["BaseInstance"]: @@ -110,6 +112,14 @@ class BaseInstance(ABC): def sysd_dir(self, value: str): self._sysd_dir = value + @property + def gcodes_dir(self): + return self._gcodes_dir + + @gcodes_dir.setter + def gcodes_dir(self, value: str): + self._gcodes_dir = value + @abstractmethod def create(self) -> None: raise NotImplementedError("Subclasses must implement the create method") @@ -118,6 +128,21 @@ class BaseInstance(ABC): def delete(self, del_remnants: bool) -> None: raise NotImplementedError("Subclasses must implement the delete method") + def create_folders(self, add_dirs: List[str] = None) -> None: + dirs = [ + self.data_dir, + self.cfg_dir, + self.log_dir, + self.comms_dir, + self.sysd_dir, + ] + + if add_dirs: + dirs.extend(add_dirs) + + for _dir in dirs: + create_directory(Path(_dir)) + def get_service_file_name(self, extension: bool = False) -> str: name = f"{self.__class__.__name__.lower()}" if self.suffix is not None: diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index e4aa570..636d173 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -19,7 +19,6 @@ from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger -from kiauh.utils.system_utils import create_directory # noinspection PyMethodMayBeStatic @@ -47,7 +46,7 @@ class Klipper(BaseInstance): env_file_target = os.path.abspath(f"{self.sysd_dir}/klipper.env") try: - self.create_folder_structure() + self.create_folders() self.write_service_file( service_template_path, service_file_target, env_file_target ) @@ -79,17 +78,6 @@ class Klipper(BaseInstance): if del_remnants: self._delete_klipper_remnants() - def create_folder_structure(self) -> None: - dirs = [ - self.data_dir, - self.cfg_dir, - self.log_dir, - self.comms_dir, - self.sysd_dir, - ] - for _dir in dirs: - create_directory(Path(_dir)) - def write_service_file( self, service_template_path: str, service_file_target: str, env_file_target: str ): From 57f34b07c60361682afde58bf61b859920a37a6e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Dec 2023 23:09:43 +0100 Subject: [PATCH 032/296] refactor(utils): add more util functions Signed-off-by: Dominik Willner --- kiauh/utils/system_utils.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index d0d9276..ec1026e 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -14,6 +14,7 @@ import shutil import subprocess import sys import time +import socket from pathlib import Path from typing import List @@ -213,3 +214,33 @@ def mask_system_service(service_name: str) -> None: log = f"Unable to mask system service {service_name}: {e.stderr.decode()}" Logger.print_error(log) raise + + +def check_file_exists(file_path: Path) -> bool: + """ + Helper function for checking the existence of a file where + elevated permissions are required | + :param file_path: the absolute path of the file to check + :return: True if file exists, otherwise False + """ + try: + command = ["sudo", "find", file_path] + subprocess.check_output(command, stderr=subprocess.DEVNULL) + return True + except subprocess.CalledProcessError: + return False + + +# this feels hacky and not quite right, but for now it works +# see: https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib +def get_ipv4_addr() -> str: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(0) + try: + # doesn't even have to be reachable + s.connect(("192.255.255.255", 1)) + return s.getsockname()[0] + except Exception: + return "127.0.0.1" + finally: + s.close() From de20f0c4122a25ce3f60287c08ff99288e136439 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Dec 2023 23:10:43 +0100 Subject: [PATCH 033/296] refactor(ConfigManager): allow to take in any config file Signed-off-by: Dominik Willner --- kiauh/__init__.py | 3 ++- kiauh/core/config_manager/config_manager.py | 15 ++------------- kiauh/modules/klipper/klipper_setup.py | 5 +++-- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/kiauh/__init__.py b/kiauh/__init__.py index d30786d..662cd28 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -9,6 +9,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from os.path import dirname, abspath +from os.path import join, dirname, abspath APPLICATION_ROOT = dirname(dirname(abspath(__file__))) +KIAUH_CFG = join(APPLICATION_ROOT, "kiauh.cfg") diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index a1cd757..593773f 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -9,19 +9,16 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os import configparser -from pathlib import Path from typing import Union -from kiauh import APPLICATION_ROOT from kiauh.utils.logger import Logger # noinspection PyMethodMayBeStatic class ConfigManager: - def __init__(self): - self.config_file = self._get_cfg_location() + def __init__(self, cfg_file: str): + self.config_file = cfg_file self.config = configparser.ConfigParser() def read_config(self) -> None: @@ -56,11 +53,3 @@ class ConfigManager: def set_value(self, section: str, key: str, value: str): self.config.set(section, key, value) - - def check_config_exist(self) -> bool: - return True if self._get_cfg_location() else False - - def _get_cfg_location(self) -> str: - cfg_path = os.path.join(APPLICATION_ROOT, "kiauh.cfg") - - return cfg_path if Path(cfg_path).exists() else None diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index fb00924..13008c3 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -14,6 +14,7 @@ import subprocess from pathlib import Path from typing import List, Union +from kiauh import KIAUH_CFG from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager @@ -135,7 +136,7 @@ def install_klipper( def setup_klipper_prerequesites() -> None: - cm = ConfigManager() + cm = ConfigManager(cfg_file=KIAUH_CFG) cm.read_config() repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL) @@ -255,7 +256,7 @@ def update_klipper() -> None: if not get_confirm("Update Klipper now?"): return - cm = ConfigManager() + cm = ConfigManager(cfg_file=KIAUH_CFG) cm.read_config() if cm.get_value("kiauh", "backup_before_update"): From 420b193f4bcb3af5b193dbf9f3dd8ee9ab51380b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Dec 2023 23:13:28 +0100 Subject: [PATCH 034/296] feat(Moonraker): implement Moonraker Signed-off-by: Dominik Willner --- kiauh/core/menus/install_menu.py | 3 +- kiauh/core/menus/remove_menu.py | 3 +- kiauh/modules/moonraker/__init__.py | 31 ++ kiauh/modules/moonraker/moonraker.py | 162 ++++++++ kiauh/modules/moonraker/moonraker_dialogs.py | 72 ++++ kiauh/modules/moonraker/moonraker_setup.py | 346 ++++++++++++++++++ kiauh/modules/moonraker/res/moonraker.conf | 30 ++ kiauh/modules/moonraker/res/moonraker.env | 1 + kiauh/modules/moonraker/res/moonraker.service | 19 + 9 files changed, 665 insertions(+), 2 deletions(-) create mode 100644 kiauh/modules/moonraker/__init__.py create mode 100644 kiauh/modules/moonraker/moonraker.py create mode 100644 kiauh/modules/moonraker/moonraker_dialogs.py create mode 100644 kiauh/modules/moonraker/moonraker_setup.py create mode 100644 kiauh/modules/moonraker/res/moonraker.conf create mode 100644 kiauh/modules/moonraker/res/moonraker.env create mode 100644 kiauh/modules/moonraker/res/moonraker.service diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index c1e2646..03c82f1 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -14,6 +14,7 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup +from kiauh.modules.moonraker import moonraker_setup from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT @@ -65,7 +66,7 @@ class InstallMenu(BaseMenu): klipper_setup.run_klipper_setup(install=True) def install_moonraker(self): - print("install_moonraker") + moonraker_setup.run_moonraker_setup(install=True) def install_mainsail(self): print("install_mainsail") diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 51b3d44..1271557 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -14,6 +14,7 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup +from kiauh.modules.moonraker import moonraker_setup from kiauh.utils.constants import COLOR_RED, RESET_FORMAT @@ -73,7 +74,7 @@ class RemoveMenu(BaseMenu): klipper_setup.run_klipper_setup(install=False) def remove_moonraker(self): - print("remove_moonraker") + moonraker_setup.run_moonraker_setup(install=False) def remove_mainsail(self): print("remove_mainsail") diff --git a/kiauh/modules/moonraker/__init__.py b/kiauh/modules/moonraker/__init__.py new file mode 100644 index 0000000..af0e88f --- /dev/null +++ b/kiauh/modules/moonraker/__init__.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import os +from pathlib import Path + +MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) + +MOONRAKER_DIR = f"{Path.home()}/moonraker" +MOONRAKER_ENV_DIR = f"{Path.home()}/moonraker-env" +MOONRAKER_REQUIREMENTS_TXT = f"{MOONRAKER_DIR}/scripts/moonraker-requirements.txt" +DEFAULT_MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker" +DEFAULT_MOONRAKER_PORT = 7125 + +# introduced due to +# https://github.com/Arksine/moonraker/issues/349 +# https://github.com/Arksine/moonraker/pull/346 +POLKIT_LEGACY_FILE = "/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla" +POLKIT_FILE = "/etc/polkit-1/rules.d/moonraker.rules" +POLKIT_USR_FILE = "/usr/share/polkit-1/rules.d/moonraker.rules" +POLKIT_SCRIPT = f"{Path.home()}/moonraker/scripts/set-policykit-rules.sh" + +EXIT_MOONRAKER_SETUP = "Exiting Moonraker setup ..." diff --git a/kiauh/modules/moonraker/moonraker.py b/kiauh/modules/moonraker/moonraker.py new file mode 100644 index 0000000..df648cf --- /dev/null +++ b/kiauh/modules/moonraker/moonraker.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import os +import shutil +import subprocess +from pathlib import Path +from typing import List, Union + +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.base_instance import BaseInstance +from kiauh.modules.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH +from kiauh.utils.constants import SYSTEMD +from kiauh.utils.logger import Logger + + +# noinspection PyMethodMayBeStatic +class Moonraker(BaseInstance): + @classmethod + def blacklist(cls) -> List[str]: + return ["None", "mcu"] + + def __init__(self, suffix: str = None): + super().__init__(instance_type=self, suffix=suffix) + self.moonraker_dir = MOONRAKER_DIR + self.env_dir = MOONRAKER_ENV_DIR + self.cfg_file = self._get_cfg() + self.port = self._get_port() + self.backup_dir = f"{self.data_dir}/backup" + self.certs_dir = f"{self.data_dir}/certs" + self.db_dir = f"{self.data_dir}/database" + self.log = f"{self.log_dir}/moonraker.log" + + def create(self, create_example_cfg: bool = False) -> None: + Logger.print_status("Creating new Moonraker Instance ...") + service_template_path = os.path.join(MODULE_PATH, "res", "moonraker.service") + env_template_file_path = os.path.join(MODULE_PATH, "res", "moonraker.env") + service_file_name = self.get_service_file_name(extension=True) + service_file_target = f"{SYSTEMD}/{service_file_name}" + env_file_target = os.path.abspath(f"{self.sysd_dir}/moonraker.env") + + try: + self.create_folders([self.backup_dir, self.certs_dir, self.db_dir]) + self.write_service_file( + service_template_path, service_file_target, env_file_target + ) + self.write_env_file(env_template_file_path, env_file_target) + + except subprocess.CalledProcessError as e: + Logger.print_error( + f"Error creating service file {service_file_target}: {e}" + ) + raise + except OSError as e: + Logger.print_error(f"Error writing file: {e}") + raise + + def delete(self, del_remnants: bool) -> None: + service_file = self.get_service_file_name(extension=True) + service_file_path = self.get_service_file_path() + + Logger.print_status(f"Deleting Moonraker Instance: {service_file}") + + try: + command = ["sudo", "rm", "-f", service_file_path] + subprocess.run(command, check=True) + Logger.print_ok(f"Service file deleted: {service_file_path}") + except subprocess.CalledProcessError as e: + Logger.print_error(f"Error deleting service file: {e}") + raise + + if del_remnants: + self._delete_moonraker_remnants() + + def write_service_file( + self, service_template_path: str, service_file_target: str, env_file_target: str + ): + service_content = self._prep_service_file( + service_template_path, env_file_target + ) + command = ["sudo", "tee", service_file_target] + subprocess.run( + command, + input=service_content.encode(), + stdout=subprocess.DEVNULL, + check=True, + ) + Logger.print_ok(f"Service file created: {service_file_target}") + + def write_env_file(self, env_template_file_path: str, env_file_target: str): + env_file_content = self._prep_env_file(env_template_file_path) + with open(env_file_target, "w") as env_file: + env_file.write(env_file_content) + Logger.print_ok(f"Env file created: {env_file_target}") + + def _delete_moonraker_remnants(self) -> None: + try: + Logger.print_status(f"Delete {self.moonraker_dir} ...") + shutil.rmtree(Path(self.moonraker_dir)) + Logger.print_status(f"Delete {self.env_dir} ...") + shutil.rmtree(Path(self.env_dir)) + except FileNotFoundError: + Logger.print_status("Cannot delete Moonraker directories. Not found.") + except PermissionError as e: + Logger.print_error(f"Error deleting Moonraker directories: {e}") + raise + + Logger.print_ok("Directories successfully deleted.") + + def _prep_service_file(self, service_template_path, env_file_path): + try: + with open(service_template_path, "r") as template_file: + template_content = template_file.read() + except FileNotFoundError: + Logger.print_error( + f"Unable to open {service_template_path} - File not found" + ) + raise + service_content = template_content.replace("%USER%", self.user) + service_content = service_content.replace("%MOONRAKER_DIR%", self.moonraker_dir) + service_content = service_content.replace("%ENV%", self.env_dir) + service_content = service_content.replace("%ENV_FILE%", env_file_path) + return service_content + + def _prep_env_file(self, env_template_file_path): + try: + with open(env_template_file_path, "r") as env_file: + env_template_file_content = env_file.read() + except FileNotFoundError: + Logger.print_error( + f"Unable to open {env_template_file_path} - File not found" + ) + raise + env_file_content = env_template_file_content.replace( + "%MOONRAKER_DIR%", self.moonraker_dir + ) + env_file_content = env_file_content.replace("%PRINTER_DATA%", self.data_dir) + return env_file_content + + def _get_cfg(self): + cfg_file_loc = f"{self.cfg_dir}/moonraker.conf" + if Path(cfg_file_loc).is_file(): + return cfg_file_loc + return None + + def _get_port(self) -> Union[int, None]: + if self.cfg_file is None: + return None + + cm = ConfigManager(cfg_file=self.cfg_file) + cm.read_config() + port = cm.get_value("server", "port") + + return int(port) if port is not None else port diff --git a/kiauh/modules/moonraker/moonraker_dialogs.py b/kiauh/modules/moonraker/moonraker_dialogs.py new file mode 100644 index 0000000..6e79b49 --- /dev/null +++ b/kiauh/modules/moonraker/moonraker_dialogs.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap +from typing import List + +from kiauh.core.menus.base_menu import print_back_footer +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN + + +def print_moonraker_overview( + klipper_instances: List[Klipper], + moonraker_instances: List[Moonraker], + show_index=False, + show_select_all=False, +): + headline = f"{COLOR_GREEN}The following instances were found:{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + |{headline:^64}| + |-------------------------------------------------------| + """ + )[1:] + + if show_select_all: + select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}" + dialog += f"| {select_all:<63}|\n" + dialog += "| |\n" + + instance_map = { + k.get_service_file_name(): k.get_service_file_name().replace( + "klipper", "moonraker" + ) + if k.suffix in [m.suffix for m in moonraker_instances] + else "" + for k in klipper_instances + } + + for i, k in enumerate(instance_map): + mr_name = instance_map.get(k) + m = f"<-> {mr_name}" if mr_name != "" else "" + line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {k} {m} {RESET_FORMAT}" + dialog += f"| {line:<63}|\n" + + warn_l1 = f"{COLOR_YELLOW}PLEASE NOTE: {RESET_FORMAT}" + warn_l2 = f"{COLOR_YELLOW}If you select an instance with an existing Moonraker{RESET_FORMAT}" + warn_l3 = f"{COLOR_YELLOW}instance, that Moonraker instance will be re-created!{RESET_FORMAT}" + warning = textwrap.dedent( + f""" + | | + |-------------------------------------------------------| + | {warn_l1:<63}| + | {warn_l2:<63}| + | {warn_l3:<63}| + """ + )[1:] + + dialog += warning + + print(dialog, end="") + print_back_footer() diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py new file mode 100644 index 0000000..0ca2e2f --- /dev/null +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import os +import subprocess +from pathlib import Path +from typing import List + +from kiauh import KIAUH_CFG +from kiauh.core.backup_manager.backup_manager import BackupManager +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.klipper.klipper_dialogs import ( + print_instance_overview, + print_update_warn_dialog, +) +from kiauh.core.repo_manager.repo_manager import RepoManager +from kiauh.modules.moonraker import ( + EXIT_MOONRAKER_SETUP, + DEFAULT_MOONRAKER_REPO_URL, + MOONRAKER_DIR, + MOONRAKER_ENV_DIR, + MOONRAKER_REQUIREMENTS_TXT, + POLKIT_LEGACY_FILE, + POLKIT_FILE, + POLKIT_USR_FILE, + POLKIT_SCRIPT, + DEFAULT_MOONRAKER_PORT, + MODULE_PATH, +) +from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.modules.moonraker.moonraker_dialogs import print_moonraker_overview +from kiauh.utils.input_utils import ( + get_confirm, + get_selection_input, +) +from kiauh.utils.logger import Logger +from kiauh.utils.system_utils import ( + parse_packages_from_file, + create_python_venv, + install_python_requirements, + update_system_package_lists, + install_system_packages, + check_file_exists, + get_ipv4_addr, +) + + +def run_moonraker_setup(install: bool) -> None: + kl_im = InstanceManager(Klipper) + kl_instance_list = kl_im.instances + kl_instance_count = len(kl_instance_list) + mr_im = InstanceManager(Moonraker) + mr_instance_list = mr_im.instances + mr_instance_count = len(mr_instance_list) + + is_klipper_installed = kl_instance_count > 0 + if install and not is_klipper_installed: + Logger.print_warn("Klipper not installed!") + Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") + return + + is_moonraker_installed = mr_instance_count > 0 + if not install and not is_moonraker_installed: + Logger.print_warn("Moonraker not installed!") + return + + if install: + install_moonraker(mr_im, mr_instance_list, kl_instance_list) + + if not install: + remove_moonraker(mr_im, mr_instance_list) + + +def handle_existing_instances(instance_list: List[Klipper]) -> bool: + instance_count = len(instance_list) + + if instance_count > 0: + print_instance_overview(instance_list) + if not get_confirm("Add new instances?", allow_go_back=True): + return False + + return True + + +def install_moonraker( + instance_manager: InstanceManager, + moonraker_instances: List[Moonraker], + klipper_instances: List[Klipper], +) -> None: + print_moonraker_overview( + klipper_instances, moonraker_instances, show_index=True, show_select_all=True + ) + + options = [str(i) for i in range(len(klipper_instances))] + options.extend(["a", "A", "b", "B"]) + question = "Select Klipper instance to setup Moonraker for" + selection = get_selection_input(question, options).lower() + + instance_names = [] + if selection == "b": + Logger.print_status(EXIT_MOONRAKER_SETUP) + return + + elif selection == "a": + for instance in klipper_instances: + instance_names.append(instance.suffix) + + else: + index = int(selection) + instance_names.append(klipper_instances[index].suffix) + + create_example_cfg = get_confirm("Create example moonraker.conf?") + setup_moonraker_prerequesites() + install_moonraker_polkit() + + ports_in_use = [ + instance.port for instance in moonraker_instances if instance.port is not None + ] + for name in instance_names: + current_instance = Moonraker(suffix=name) + + instance_manager.current_instance = current_instance + instance_manager.create_instance() + instance_manager.enable_instance() + + if create_example_cfg: + cfg_dir = current_instance.cfg_dir + Logger.print_status(f"Creating example moonraker.conf in '{cfg_dir}'") + if current_instance.cfg_file is None: + create_example_moonraker_conf(current_instance, ports_in_use) + Logger.print_ok(f"Example moonraker.conf created in '{cfg_dir}'") + else: + Logger.print_info(f"moonraker.conf in '{cfg_dir}' already exists.") + + instance_manager.start_instance() + + instance_manager.reload_daemon() + + +def setup_moonraker_prerequesites() -> None: + cm = ConfigManager(cfg_file=KIAUH_CFG) + cm.read_config() + + repo = str( + cm.get_value("moonraker", "repository_url") or DEFAULT_MOONRAKER_REPO_URL + ) + branch = str(cm.get_value("moonraker", "branch") or "master") + + repo_manager = RepoManager( + repo=repo, + branch=branch, + target_dir=MOONRAKER_DIR, + ) + repo_manager.clone_repo() + + # install moonraker dependencies and create python virtualenv + install_moonraker_packages(Path(MOONRAKER_DIR)) + create_python_venv(Path(MOONRAKER_ENV_DIR)) + moonraker_py_req = Path(MOONRAKER_REQUIREMENTS_TXT) + install_python_requirements(Path(MOONRAKER_ENV_DIR), moonraker_py_req) + + +def install_moonraker_packages(moonraker_dir: Path) -> None: + script = Path(f"{moonraker_dir}/scripts/install-moonraker.sh") + packages = parse_packages_from_file(script) + update_system_package_lists(silent=False) + install_system_packages(packages) + + +def install_moonraker_polkit() -> None: + Logger.print_status("Installing Moonraker policykit rules ...") + + legacy_file_exists = check_file_exists(Path(POLKIT_LEGACY_FILE)) + polkit_file_exists = check_file_exists(Path(POLKIT_FILE)) + usr_file_exists = check_file_exists(Path(POLKIT_USR_FILE)) + + 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 remove_moonraker( + instance_manager: InstanceManager, instance_list: List[Moonraker] +) -> None: + print_instance_overview(instance_list, True, True) + + options = [str(i) for i in range(len(instance_list))] + options.extend(["a", "A", "b", "B"]) + + selection = get_selection_input("Select Moonraker instance to remove", options) + + del_remnants = False + remove_polkit = False + instances_to_remove = [] + if selection == "b".lower(): + return + elif selection == "a".lower(): + question = f"Delete {MOONRAKER_DIR} and {MOONRAKER_ENV_DIR}?" + del_remnants = get_confirm(question, False, True) + instances_to_remove.extend(instance_list) + remove_polkit = True + Logger.print_status("Removing all Moonraker instances ...") + else: + instance = instance_list[int(selection)] + instance_name = instance.get_service_file_name() + instances_to_remove.append(instance) + is_last_instance = len(instance_list) == 1 + if is_last_instance: + question = f"Delete {MOONRAKER_DIR} and {MOONRAKER_ENV_DIR}?" + del_remnants = get_confirm(question, False, True) + remove_polkit = True + Logger.print_status(f"Removing Moonraker instance {instance_name} ...") + + if del_remnants is None: + Logger.print_status("Exiting Moonraker Uninstaller ...") + return + + remove_instances( + instance_manager, + instances_to_remove, + remove_polkit, + del_remnants, + ) + + +def remove_instances( + instance_manager: InstanceManager, + instance_list: List[Moonraker], + remove_polkit: bool, + del_remnants: bool, +) -> None: + for instance in instance_list: + instance_manager.current_instance = instance + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance(del_remnants=del_remnants) + + if remove_polkit: + remove_polkit_rules() + + instance_manager.reload_daemon() + + +def remove_polkit_rules() -> None: + Logger.print_status("Removing all Moonraker policykit rules ...") + if not Path(MOONRAKER_DIR).exists(): + log = "Cannot remove policykit rules. Moonraker directory not found." + Logger.print_warn(log) + return + + try: + command = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"] + subprocess.run( + command, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, check=True + ) + except subprocess.CalledProcessError as e: + Logger.print_error(f"Error while removing policykit rules: {e}") + + Logger.print_ok("Policykit rules successfully removed!") + + +def update_moonraker() -> None: + print_update_warn_dialog() + if not get_confirm("Update Moonraker now?"): + return + + cm = ConfigManager(cfg_file=KIAUH_CFG) + cm.read_config() + + if cm.get_value("kiauh", "backup_before_update"): + backup_manager = BackupManager(source=MOONRAKER_DIR, backup_name="moonraker") + backup_manager.backup() + backup_manager.backup_name = "moonraker-env" + backup_manager.source = MOONRAKER_ENV_DIR + backup_manager.backup() + + instance_manager = InstanceManager(Moonraker) + instance_manager.stop_all_instance() + + repo = str( + cm.get_value("moonraker", "repository_url") or DEFAULT_MOONRAKER_REPO_URL + ) + branch = str(cm.get_value("moonraker", "branch") or "master") + + repo_manager = RepoManager( + repo=repo, + branch=branch, + target_dir=MOONRAKER_DIR, + ) + repo_manager.pull_repo() + instance_manager.start_all_instance() + + +def create_example_moonraker_conf(instance: Moonraker, ports: List[int]) -> None: + port = max(ports) + 1 if ports else DEFAULT_MOONRAKER_PORT + ports.append(port) + instance.port = port + example_cfg_path = os.path.join(MODULE_PATH, "res", "moonraker.conf") + + with open(f"{instance.cfg_dir}/moonraker.conf", "w") as cfg: + cfg.write(_prep_example_moonraker_conf(instance, example_cfg_path)) + + +def _prep_example_moonraker_conf(instance: Moonraker, example_cfg_path: str) -> str: + try: + with open(example_cfg_path, "r") as cfg: + example_cfg_content = cfg.read() + except FileNotFoundError: + Logger.print_error(f"Unable to open {example_cfg_path} - File not found") + raise + + example_cfg_content = example_cfg_content.replace("%PORT%", str(instance.port)) + example_cfg_content = example_cfg_content.replace( + "%UDS%", f"{instance.comms_dir}/klippy.sock" + ) + + ip = get_ipv4_addr().split(".")[:2] + ip.extend(["0", "0/16"]) + example_cfg_content = example_cfg_content.replace("%LAN%", ".".join(ip)) + + return example_cfg_content diff --git a/kiauh/modules/moonraker/res/moonraker.conf b/kiauh/modules/moonraker/res/moonraker.conf new file mode 100644 index 0000000..d6eadd8 --- /dev/null +++ b/kiauh/modules/moonraker/res/moonraker.conf @@ -0,0 +1,30 @@ +[server] +host: 0.0.0.0 +port: %PORT% +klippy_uds_address: %UDS% + +[authorization] +trusted_clients: + %LAN% + 10.0.0.0/8 + 127.0.0.0/8 + 169.254.0.0/16 + 172.16.0.0/12 + 192.168.0.0/16 + FE80::/10 + ::1/128 +cors_domains: + *.lan + *.local + *://localhost + *://localhost:* + *://my.mainsail.xyz + *://app.fluidd.xyz + +[octoprint_compat] + +[history] + +[update_manager] +channel: dev +refresh_interval: 168 diff --git a/kiauh/modules/moonraker/res/moonraker.env b/kiauh/modules/moonraker/res/moonraker.env new file mode 100644 index 0000000..bca6af5 --- /dev/null +++ b/kiauh/modules/moonraker/res/moonraker.env @@ -0,0 +1 @@ +MOONRAKER_ARGS="%MOONRAKER_DIR%/moonraker/moonraker.py -d %PRINTER_DATA%" \ No newline at end of file diff --git a/kiauh/modules/moonraker/res/moonraker.service b/kiauh/modules/moonraker/res/moonraker.service new file mode 100644 index 0000000..696d7ba --- /dev/null +++ b/kiauh/modules/moonraker/res/moonraker.service @@ -0,0 +1,19 @@ +[Unit] +Description=API Server for Klipper SV1 +Documentation=https://moonraker.readthedocs.io/ +Requires=network-online.target +After=network-online.target + +[Install] +WantedBy=multi-user.target + +[Service] +Type=simple +User=%USER% +SupplementaryGroups=moonraker-admin +RemainAfterExit=yes +WorkingDirectory=%MOONRAKER_DIR% +EnvironmentFile=%ENV_FILE% +ExecStart=%ENV%/bin/python $MOONRAKER_ARGS +Restart=always +RestartSec=10 From 9a1a66aa64bc2359724ebd842ccf767187c044de Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 4 Dec 2023 21:33:35 +0100 Subject: [PATCH 035/296] docs(utils): add docstrings Signed-off-by: Dominik Willner --- kiauh/__init__.py | 2 + kiauh/core/backup_manager/backup_manager.py | 2 +- kiauh/utils/common.py | 2 +- kiauh/utils/constants.py | 3 -- kiauh/utils/input_utils.py | 44 +++++++++++++++++++++ kiauh/utils/system_utils.py | 9 ++++- 6 files changed, 55 insertions(+), 7 deletions(-) diff --git a/kiauh/__init__.py b/kiauh/__init__.py index 662cd28..8fc1d9a 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -10,6 +10,8 @@ # ======================================================================= # from os.path import join, dirname, abspath +from pathlib import Path APPLICATION_ROOT = dirname(dirname(abspath(__file__))) KIAUH_CFG = join(APPLICATION_ROOT, "kiauh.cfg") +KIAUH_BACKUP_DIR = f"{Path.home()}/kiauh-backups" diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index ee6b782..db82162 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -12,8 +12,8 @@ import shutil from pathlib import Path +from kiauh import KIAUH_BACKUP_DIR from kiauh.utils.common import get_current_date -from kiauh.utils.constants import KIAUH_BACKUP_DIR from kiauh.utils.logger import Logger diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 4b6a48a..b6a8e87 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -16,7 +16,7 @@ from typing import Dict, Literal def get_current_date() -> Dict[Literal["date", "time"], str]: """ Get the current date | - :return: a Dict holding a date and time key:value pair + :return: Dict holding a date and time key:value pair """ now: datetime = datetime.today() date: str = now.strftime("%Y-%m-%d") diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py index 37d4a33..66b00ff 100644 --- a/kiauh/utils/constants.py +++ b/kiauh/utils/constants.py @@ -11,7 +11,6 @@ import os import pwd -from pathlib import Path # text colors and formats COLOR_WHITE = "\033[37m" # white @@ -21,8 +20,6 @@ COLOR_YELLOW = "\033[93m" # bright yellow COLOR_RED = "\033[91m" # bright red COLOR_CYAN = "\033[96m" # bright cyan RESET_FORMAT = "\033[0m" # reset format -# kiauh -KIAUH_BACKUP_DIR = f"{Path.home()}/kiauh-backups" # current user CURRENT_USER = pwd.getpwuid(os.getuid())[0] SYSTEMD = "/etc/systemd/system" diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index dcddda0..7f9f1f5 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -19,6 +19,13 @@ from kiauh.utils.logger import Logger def get_confirm( question: str, default_choice=True, allow_go_back=False ) -> Union[bool, None]: + """ + Helper method for validating confirmation (yes/no) user input. | + :param question: The question to display + :param default_choice: A default if input was submitted without input + :param allow_go_back: Navigate back to a previous dialog + :return: Either True or False, or None on go_back + """ options_confirm = ["y", "yes"] options_decline = ["n", "no"] options_go_back = ["b", "B"] @@ -48,6 +55,15 @@ def get_confirm( def get_number_input( question: str, min_count: int, max_count=None, default=None, allow_go_back=False ) -> Union[int, None]: + """ + Helper method to get a number input from the user + :param question: The question to display + :param min_count: The lowest allowed value + :param max_count: The highest allowed value (or None) + :param default: Optional default value + :param allow_go_back: Navigate back to a previous dialog + :return: Either the validated number input, or None on go_back + """ options_go_back = ["b", "B"] _question = format_question(question, default) while True: @@ -65,6 +81,13 @@ def get_number_input( def get_string_input(question: str, exclude=Optional[List], default=None) -> str: + """ + Helper method to get a string input from the user + :param question: The question to display + :param exclude: List of strings which are not allowed + :param default: Optional default value + :return: The validated string value + """ while True: _input = input(format_question(question, default)).strip() @@ -77,6 +100,13 @@ def get_string_input(question: str, exclude=Optional[List], default=None) -> str def get_selection_input(question: str, option_list: List, default=None) -> str: + """ + Helper method to get a selection from a list of options from the user + :param question: The question to display + :param option_list: The list of options the user can select from + :param default: Optional default value + :return: The option that was selected by the user + """ while True: _input = input(format_question(question, default)).strip() @@ -87,6 +117,12 @@ def get_selection_input(question: str, option_list: List, default=None) -> str: def format_question(question: str, default=None) -> str: + """ + Helper method to have a standardized formatting of questions | + :param question: The question to display + :param default: If defined, the default option will be displayed to the user + :return: The formatted question string + """ formatted_q = question if default is not None: formatted_q += f" (default={default})" @@ -95,6 +131,14 @@ def format_question(question: str, default=None) -> str: def validate_number_input(value: str, min_count: int, max_count: int) -> int: + """ + Helper method for a simple number input validation. | + :param value: The value to validate + :param min_count: The lowest allowed value + :param max_count: The highest allowed value (or None) + :return: The validated value as Integer + :raises: ValueError if value is invalid + """ if max_count is not None: if min_count <= int(value) <= max_count: return int(value) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index ec1026e..34a2985 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -40,7 +40,7 @@ def parse_packages_from_file(source_file: Path) -> List[str]: Read the package names from bash scripts, when defined like: PKGLIST="package1 package2 package3" | :param source_file: path of the sourcefile to read from - :return: a list of package names + :return: A list of package names """ packages = [] @@ -169,7 +169,7 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: kill(f"Error updating system package list:\n{e.stderr.decode()}") -def install_system_packages(packages: List) -> None: +def install_system_packages(packages: List[str]) -> None: """ Installs a list of system packages | :param packages: List of system package names @@ -234,6 +234,11 @@ def check_file_exists(file_path: Path) -> bool: # this feels hacky and not quite right, but for now it works # see: https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib def get_ipv4_addr() -> str: + """ + Helper function that returns the IPv4 of the current machine + by opening a socket and sending a package to an arbitrary IP. | + :return: Local IPv4 of the current machine + """ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(0) try: From e121ba8a629768bace1c7bf9c35472a69eb704bb Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 4 Dec 2023 22:35:36 +0100 Subject: [PATCH 036/296] feat(Moonraker): add python version check Signed-off-by: Dominik Willner --- kiauh/modules/moonraker/moonraker_setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index 0ca2e2f..24f8a60 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -11,6 +11,7 @@ import os import subprocess +import sys from pathlib import Path from typing import List @@ -63,6 +64,11 @@ def run_moonraker_setup(install: bool) -> None: mr_instance_list = mr_im.instances mr_instance_count = len(mr_instance_list) + if not (sys.version_info.major >= 4 and sys.version_info.minor >= 7): + Logger.print_error("Versioncheck failed!") + Logger.print_error("Python 3. or newer required to run Moonraker.") + return + is_klipper_installed = kl_instance_count > 0 if install and not is_klipper_installed: Logger.print_warn("Klipper not installed!") From f62c10dc8bd2a546adb54bfbc4c1b345847b0145 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 16 Dec 2023 15:38:23 +0100 Subject: [PATCH 037/296] feat(kiauh): add helper methods to check for installed packages Signed-off-by: Dominik Willner --- kiauh/utils/common.py | 22 +++++++++++++++++++++- kiauh/utils/system_utils.py | 20 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index b6a8e87..949d6a4 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -10,7 +10,11 @@ # ======================================================================= # from datetime import datetime -from typing import Dict, Literal +from typing import Dict, Literal, List + +from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT +from kiauh.utils.logger import Logger +from kiauh.utils.system_utils import check_package_install, install_system_packages def get_current_date() -> Dict[Literal["date", "time"], str]: @@ -23,3 +27,19 @@ def get_current_date() -> Dict[Literal["date", "time"], str]: time: str = now.strftime("%H-%M-%S") return {"date": date, "time": time} + + +def check_install_dependencies(deps: List[str]) -> None: + """ + Common helper method to check if dependencies are installed + and if not, install them automatically | + :param deps: List of strings of package names to check if installed + :return: None + """ + requirements = check_package_install(deps) + if requirements: + Logger.print_status("Installing dependencies ...") + Logger.print_info("The following packages need installation:") + for _ in requirements: + print(f"{COLOR_CYAN}● {_}{RESET_FORMAT}") + install_system_packages(requirements) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 34a2985..39d9cac 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -169,6 +169,26 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: kill(f"Error updating system package list:\n{e.stderr.decode()}") +def check_package_install(packages: List[str]) -> List[str]: + """ + Checks the system for installed packages | + :param packages: List of strings of package names + :return: A list containing the names of packages that are not installed + """ + not_installed = [] + for package in packages: + command = ["dpkg-query", "f'${Status}'", "--show", package] + result = subprocess.run( + command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True + ) + if "installed" not in result.stdout.strip("'").split(): + not_installed.append(package) + else: + Logger.print_ok(f"{package} already installed.") + + return not_installed + + def install_system_packages(packages: List[str]) -> None: """ Installs a list of system packages | From f709cf84e7125faecc109bc297643d5bd434fadb Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 16 Dec 2023 15:41:48 +0100 Subject: [PATCH 038/296] feat(kiauh): add helper methods for downloading files Signed-off-by: Dominik Willner --- kiauh/utils/system_utils.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 39d9cac..588f48d 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -11,10 +11,12 @@ import os import shutil +import socket import subprocess import sys import time -import socket +import urllib.error +import urllib.request from pathlib import Path from typing import List @@ -269,3 +271,32 @@ def get_ipv4_addr() -> str: return "127.0.0.1" finally: s.close() + + +def download_file(url: str, target_folder: str, target_name: str, show_progress=True): + target_path = os.path.join(target_folder, target_name) + try: + if show_progress: + urllib.request.urlretrieve(url, target_path, download_progress) + else: + urllib.request.urlretrieve(url, target_path) + except urllib.error.HTTPError as e: + Logger.print_error(f"Download failed! HTTP error occured: {e}") + raise + except urllib.error.URLError as e: + Logger.print_error(f"Download failed! URL error occured: {e}") + raise + except Exception as e: + Logger.print_error(f"Download failed! An error occured: {e}") + raise + + +def download_progress(block_num, block_size, total_size): + downloaded = block_num * block_size + percent = 100 if downloaded >= total_size else downloaded / total_size * 100 + mb = 1024 * 1024 + progress = int(percent / 5) + remaining = "-" * (20 - progress) + dl = f"\rDownloading: [{'#' * progress}{remaining}]{percent:.2f}% ({downloaded/mb:.2f}/{total_size/mb:.2f}MB)" + sys.stdout.write(dl) + sys.stdout.flush() From 545397f5983218577b9cea20ed7770497aeb4b72 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 16 Dec 2023 17:38:01 +0100 Subject: [PATCH 039/296] feat(kiauh): fix typo in check_package_install Signed-off-by: Dominik Willner --- kiauh/utils/system_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 588f48d..9b6fbce 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -179,7 +179,7 @@ def check_package_install(packages: List[str]) -> List[str]: """ not_installed = [] for package in packages: - command = ["dpkg-query", "f'${Status}'", "--show", package] + command = ["dpkg-query", "-f'${Status}'", "--show", package] result = subprocess.run( command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True ) From 1836beab42b10dacd7f773742e413121bbf1d765 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 00:01:25 +0100 Subject: [PATCH 040/296] feat(klipper): add getter for specific properties Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 636d173..ba9e1b5 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -31,10 +31,26 @@ class Klipper(BaseInstance): super().__init__(instance_type=self, suffix=suffix) self.klipper_dir = KLIPPER_DIR self.env_dir = KLIPPER_ENV_DIR - self.cfg_file = f"{self.cfg_dir}/printer.cfg" - self.log = f"{self.log_dir}/klippy.log" - self.serial = f"{self.comms_dir}/klippy.serial" - self.uds = f"{self.comms_dir}/klippy.sock" + self._cfg_file = f"{self.cfg_dir}/printer.cfg" + self._log = f"{self.log_dir}/klippy.log" + self._serial = f"{self.comms_dir}/klippy.serial" + self._uds = f"{self.comms_dir}/klippy.sock" + + @property + def cfg_file(self) -> str: + return self._cfg_file + + @property + def log(self) -> str: + return self._log + + @property + def serial(self) -> str: + return self._serial + + @property + def uds(self) -> str: + return self._uds def create(self) -> None: Logger.print_status("Creating new Klipper Instance ...") @@ -140,8 +156,8 @@ class Klipper(BaseInstance): env_file_content = env_template_file_content.replace( "%KLIPPER_DIR%", self.klipper_dir ) - env_file_content = env_file_content.replace("%CFG%", self.cfg_file) - env_file_content = env_file_content.replace("%SERIAL%", self.serial) - env_file_content = env_file_content.replace("%LOG%", self.log) - env_file_content = env_file_content.replace("%UDS%", self.uds) + env_file_content = env_file_content.replace("%CFG%", self._cfg_file) + env_file_content = env_file_content.replace("%SERIAL%", self._serial) + env_file_content = env_file_content.replace("%LOG%", self._log) + env_file_content = env_file_content.replace("%UDS%", self._uds) return env_file_content From b20613819e173755e01d783006fff53eb4166bca Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 13:00:46 +0100 Subject: [PATCH 041/296] feat(Logger): add "start" parameter Signed-off-by: Dominik Willner --- kiauh/utils/logger.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index 864bd77..8732878 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -36,26 +36,26 @@ class Logger: pass @staticmethod - def print_info(msg, prefix=True, end="\n") -> None: + def print_info(msg, prefix=True, start="", end="\n") -> None: message = f"[INFO] {msg}" if prefix else msg - print(f"{COLOR_WHITE}{message}{RESET_FORMAT}", end=end) + print(f"{COLOR_WHITE}{start}{message}{RESET_FORMAT}", end=end) @staticmethod - def print_ok(msg, prefix=True, end="\n") -> None: + def print_ok(msg, prefix=True, start="", end="\n") -> None: message = f"[OK] {msg}" if prefix else msg - print(f"{COLOR_GREEN}{message}{RESET_FORMAT}", end=end) + print(f"{COLOR_GREEN}{start}{message}{RESET_FORMAT}", end=end) @staticmethod - def print_warn(msg, prefix=True, end="\n") -> None: + def print_warn(msg, prefix=True, start="", end="\n") -> None: message = f"[WARN] {msg}" if prefix else msg - print(f"{COLOR_YELLOW}{message}{RESET_FORMAT}", end=end) + print(f"{COLOR_YELLOW}{start}{message}{RESET_FORMAT}", end=end) @staticmethod - def print_error(msg, prefix=True, end="\n") -> None: + def print_error(msg, prefix=True, start="", end="\n") -> None: message = f"[ERROR] {msg}" if prefix else msg - print(f"{COLOR_RED}{message}{RESET_FORMAT}", end=end) + print(f"{COLOR_RED}{start}{message}{RESET_FORMAT}", end=end) @staticmethod - def print_status(msg, prefix=True, end="\n") -> None: + def print_status(msg, prefix=True, start="", end="\n") -> None: message = f"\n###### {msg}" if prefix else msg - print(f"{COLOR_MAGENTA}{message}{RESET_FORMAT}", end=end) + print(f"{COLOR_MAGENTA}{start}{message}{RESET_FORMAT}", end=end) From 78cefddb2e35a20f002c31d7f0951437b3ba744a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 13:26:22 +0100 Subject: [PATCH 042/296] feat(InstanceManager): add restart service method Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/instance_manager.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index fe6455f..6891139 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -141,11 +141,26 @@ class InstanceManager: Logger.print_error(f"Error starting {self.instance_service_full}:") Logger.print_error(f"{e}") + def restart_instance(self) -> None: + Logger.print_status(f"Restarting {self.instance_service_full} ...") + try: + command = ["sudo", "systemctl", "restart", self.instance_service_full] + if subprocess.run(command, check=True): + Logger.print_ok(f"{self.instance_service_full} restarted.") + except subprocess.CalledProcessError as e: + Logger.print_error(f"Error restarting {self.instance_service_full}:") + Logger.print_error(f"{e}") + def start_all_instance(self) -> None: for instance in self.instances: self.current_instance = instance self.start_instance() + def restart_all_instance(self) -> None: + for instance in self.instances: + self.current_instance = instance + self.restart_instance() + def stop_instance(self) -> None: Logger.print_status(f"Stopping {self.instance_service_full} ...") try: From a80f0bb0e822e46886c16c1c16ae13401e95c185 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 14:42:53 +0100 Subject: [PATCH 043/296] feat(utils): add several util methods Signed-off-by: Dominik Willner --- kiauh/utils/__init__.py | 7 ++ kiauh/utils/res/common_vars.conf | 6 ++ kiauh/utils/res/nginx_cfg | 96 +++++++++++++++++ kiauh/utils/res/upstreams.conf | 25 +++++ kiauh/utils/system_utils.py | 179 ++++++++++++++++++++++++++++++- 5 files changed, 310 insertions(+), 3 deletions(-) create mode 100644 kiauh/utils/res/common_vars.conf create mode 100644 kiauh/utils/res/nginx_cfg create mode 100644 kiauh/utils/res/upstreams.conf diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index 203c66a..108b521 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -8,5 +8,12 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import os +MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) INVALID_CHOICE = "Invalid choice. Please select a valid value." + +# ================== NGINX =====================# +NGINX_SITES_AVAILABLE = "/etc/nginx/sites-available" +NGINX_SITES_ENABLED = "/etc/nginx/sites-enabled" +NGINX_CONFD = "/etc/nginx/conf.d" diff --git a/kiauh/utils/res/common_vars.conf b/kiauh/utils/res/common_vars.conf new file mode 100644 index 0000000..9c3f85e --- /dev/null +++ b/kiauh/utils/res/common_vars.conf @@ -0,0 +1,6 @@ +# /etc/nginx/conf.d/common_vars.conf + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} \ No newline at end of file diff --git a/kiauh/utils/res/nginx_cfg b/kiauh/utils/res/nginx_cfg new file mode 100644 index 0000000..6303674 --- /dev/null +++ b/kiauh/utils/res/nginx_cfg @@ -0,0 +1,96 @@ +# /etc/nginx/sites-available/%NAME% + +server { + listen %PORT%; + + access_log /var/log/nginx/%NAME%-access.log; + error_log /var/log/nginx/%NAME%-error.log; + + # disable this section on smaller hardware like a pi zero + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_proxied expired no-cache no-store private auth; + gzip_comp_level 4; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/json application/xml; + + # web_path from %NAME% static files + root %ROOT_DIR%; + + index index.html; + server_name _; + + # disable max upload size checks + client_max_body_size 0; + + # disable proxy request buffering + proxy_request_buffering off; + + location / { + try_files $uri $uri/ /index.html; + } + + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + location /websocket { + proxy_pass http://apiserver/websocket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 86400; + } + + location ~ ^/(printer|api|access|machine|server)/ { + proxy_pass http://apiserver$request_uri; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Scheme $scheme; + proxy_read_timeout 600; + } + + location /webcam/ { + postpone_output 0; + proxy_buffering off; + proxy_ignore_headers X-Accel-Buffering; + access_log off; + error_log off; + proxy_pass http://mjpgstreamer1/; + } + + location /webcam2/ { + postpone_output 0; + proxy_buffering off; + proxy_ignore_headers X-Accel-Buffering; + access_log off; + error_log off; + proxy_pass http://mjpgstreamer2/; + } + + location /webcam3/ { + postpone_output 0; + proxy_buffering off; + proxy_ignore_headers X-Accel-Buffering; + access_log off; + error_log off; + proxy_pass http://mjpgstreamer3/; + } + + location /webcam4/ { + postpone_output 0; + proxy_buffering off; + proxy_ignore_headers X-Accel-Buffering; + access_log off; + error_log off; + proxy_pass http://mjpgstreamer4/; + } +} diff --git a/kiauh/utils/res/upstreams.conf b/kiauh/utils/res/upstreams.conf new file mode 100644 index 0000000..d04e04a --- /dev/null +++ b/kiauh/utils/res/upstreams.conf @@ -0,0 +1,25 @@ +# /etc/nginx/conf.d/upstreams.conf +upstream apiserver { + ip_hash; + server 127.0.0.1:7125; +} + +upstream mjpgstreamer1 { + ip_hash; + server 127.0.0.1:8080; +} + +upstream mjpgstreamer2 { + ip_hash; + server 127.0.0.1:8081; +} + +upstream mjpgstreamer3 { + ip_hash; + server 127.0.0.1:8082; +} + +upstream mjpgstreamer4 { + ip_hash; + server 127.0.0.1:8083; +} \ No newline at end of file diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 9b6fbce..f14fbb2 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -18,8 +18,15 @@ import time import urllib.error import urllib.request from pathlib import Path -from typing import List +from typing import List, Literal +from zipfile import ZipFile +from kiauh.utils import ( + NGINX_CONFD, + MODULE_PATH, + NGINX_SITES_AVAILABLE, + NGINX_SITES_ENABLED, +) from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger @@ -273,11 +280,22 @@ def get_ipv4_addr() -> str: s.close() -def download_file(url: str, target_folder: str, target_name: str, show_progress=True): +def download_file( + url: str, target_folder: str, target_name: str, show_progress=True +) -> None: + """ + Helper method for downloading files from a provided URL | + :param url: the url to the file + :param target_folder: the target folder to download the file into + :param target_name: the name of the downloaded file + :param show_progress: show download progress or not + :return: None + """ target_path = os.path.join(target_folder, target_name) try: if show_progress: urllib.request.urlretrieve(url, target_path, download_progress) + sys.stdout.write("\n") else: urllib.request.urlretrieve(url, target_path) except urllib.error.HTTPError as e: @@ -291,7 +309,14 @@ def download_file(url: str, target_folder: str, target_name: str, show_progress= raise -def download_progress(block_num, block_size, total_size): +def download_progress(block_num, block_size, total_size) -> None: + """ + Reporthook method for urllib.request.urlretrieve() method call in download_file() | + :param block_num: + :param block_size: + :param total_size: total filesize in bytes + :return: None + """ downloaded = block_num * block_size percent = 100 if downloaded >= total_size else downloaded / total_size * 100 mb = 1024 * 1024 @@ -300,3 +325,151 @@ def download_progress(block_num, block_size, total_size): dl = f"\rDownloading: [{'#' * progress}{remaining}]{percent:.2f}% ({downloaded/mb:.2f}/{total_size/mb:.2f}MB)" sys.stdout.write(dl) sys.stdout.flush() + + +def unzip(file: str, target_dir: str) -> None: + """ + Helper function to unzip a zip-archive into a target directory | + :param file: the zip-file to unzip + :param target_dir: the target directory to extract the files into + :return: None + """ + with ZipFile(file, "r") as _zip: + _zip.extractall(target_dir) + + +def create_upstream_nginx_cfg() -> None: + """ + Creates an upstream.conf in /etc/nginx/conf.d + :return: None + """ + source = os.path.join(MODULE_PATH, "res", "upstreams.conf") + target = os.path.join(NGINX_CONFD, "upstreams.conf") + try: + command = ["sudo", "cp", source, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create upstreams.conf: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def create_common_vars_nginx_cfg() -> None: + """ + Creates a common_vars.conf in /etc/nginx/conf.d + :return: None + """ + source = os.path.join(MODULE_PATH, "res", "common_vars.conf") + target = os.path.join(NGINX_CONFD, "common_vars.conf") + try: + command = ["sudo", "cp", source, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create upstreams.conf: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def create_nginx_cfg(name: str, port: int, root_dir: str) -> None: + """ + Creates an NGINX config from a template file and replaces all placeholders + :param name: name of the config to create + :param port: listen port + :param root_dir: directory of the static files + :return: None + """ + tmp = f"{Path.home()}/{name}.tmp" + shutil.copy(os.path.join(MODULE_PATH, "res", "nginx_cfg"), tmp) + with open(tmp, "r+") as f: + content = f.read() + content = content.replace("%NAME%", name) + content = content.replace("%PORT%", str(port)) + content = content.replace("%ROOT_DIR%", root_dir) + f.seek(0) + f.write(content) + f.truncate() + + target = os.path.join(NGINX_SITES_AVAILABLE, name) + try: + command = ["sudo", "mv", tmp, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create '{target}': {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def delete_default_nginx_cfg() -> None: + """ + Deletes a default NGINX config + :return: None + """ + default_cfg = Path("/etc/nginx/sites-enabled/default") + if not check_file_exists(default_cfg): + return + + try: + command = ["sudo", "rm", default_cfg] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to delete '{default_cfg}': {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def enable_nginx_cfg(name: str) -> None: + """ + Helper method to enable an NGINX config | + :param name: name of the config to enable + :return: None + """ + source = os.path.join(NGINX_SITES_AVAILABLE, name) + target = os.path.join(NGINX_SITES_ENABLED, name) + if check_file_exists(Path(target)): + return + + try: + command = ["sudo", "ln", "-s", source, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create symlink: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def set_nginx_permissions() -> None: + """ + Check if permissions of the users home directory + grant execution rights to group and other and set them if not set. + Required permissions for NGINX to be able to serve Mainsail/Fluidd. + This seems to have become necessary with Ubuntu 21+. | + :return: None + """ + cmd1 = f"ls -ld {Path.home()} | cut -d' ' -f1" + homedir_perm = subprocess.run(cmd1, shell=True, stdout=subprocess.PIPE, text=True) + homedir_perm = homedir_perm.stdout + + if homedir_perm.count("x") < 3: + Logger.print_status("Granting NGINX the required permissions ...") + subprocess.run(["chmod", "og+x", Path.home()]) + Logger.print_ok("Permissions granted.") + + +def control_systemd_service( + name: str, action: Literal["start", "stop", "restart", "disable"] +) -> None: + """ + Helper method to execute several actions for a specific systemd service. | + :param name: the service name + :param action: Either "start", "stop", "restart" or "disable" + :return: None + """ + try: + Logger.print_status(f"{action.capitalize()} {name}.service ...") + command = ["sudo", "systemctl", action, f"{name}.service"] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + Logger.print_ok(f"OK!") + except subprocess.CalledProcessError as e: + log = f"Failed to {action} {name}.service: {e.stderr.decode()}" + Logger.print_error(log) + raise From 80a953a58751ffce8c901b72d0df6e0ca6024070 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 16:10:20 +0100 Subject: [PATCH 044/296] fix(Moonraker): typo in python version check Signed-off-by: Dominik Willner --- kiauh/modules/moonraker/moonraker_setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index 24f8a60..47df121 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -64,9 +64,9 @@ def run_moonraker_setup(install: bool) -> None: mr_instance_list = mr_im.instances mr_instance_count = len(mr_instance_list) - if not (sys.version_info.major >= 4 and sys.version_info.minor >= 7): + if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7): Logger.print_error("Versioncheck failed!") - Logger.print_error("Python 3. or newer required to run Moonraker.") + Logger.print_error("Python 3.7 or newer required to run Moonraker.") return is_klipper_installed = kl_instance_count > 0 From 59d8867c8c8e912f8755013db1f1570482894edb Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 16:12:43 +0100 Subject: [PATCH 045/296] fix(kiauh): copy&paste issue in repo url for Moonraker Signed-off-by: Dominik Willner --- kiauh.cfg.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh.cfg.example b/kiauh.cfg.example index 0ba34db..72f4a21 100644 --- a/kiauh.cfg.example +++ b/kiauh.cfg.example @@ -7,6 +7,6 @@ branch: master method: https [moonraker] -repository_url: https://github.com/Klipper3d/klipper +repository_url: https://github.com/Arksine/moonraker branch: master method: https From 1178d3c73009464ea4de76dd089dfc9bee1e916b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 16:18:44 +0100 Subject: [PATCH 046/296] refactor(Moonraker): skip selection dialog if there is only 1 klipper instance Signed-off-by: Dominik Willner --- kiauh/modules/moonraker/moonraker_setup.py | 26 +++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index 47df121..df2eb7c 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -103,26 +103,30 @@ def install_moonraker( moonraker_instances: List[Moonraker], klipper_instances: List[Klipper], ) -> None: - print_moonraker_overview( - klipper_instances, moonraker_instances, show_index=True, show_select_all=True - ) - - options = [str(i) for i in range(len(klipper_instances))] - options.extend(["a", "A", "b", "B"]) - question = "Select Klipper instance to setup Moonraker for" - selection = get_selection_input(question, options).lower() + selected_klipper_instance = 0 + if len(klipper_instances) > 1: + print_moonraker_overview( + klipper_instances, + moonraker_instances, + show_index=True, + show_select_all=True, + ) + options = [str(i) for i in range(len(klipper_instances))] + options.extend(["a", "A", "b", "B"]) + question = "Select Klipper instance to setup Moonraker for" + selected_klipper_instance = get_selection_input(question, options).lower() instance_names = [] - if selection == "b": + if selected_klipper_instance == "b": Logger.print_status(EXIT_MOONRAKER_SETUP) return - elif selection == "a": + elif selected_klipper_instance == "a": for instance in klipper_instances: instance_names.append(instance.suffix) else: - index = int(selection) + index = int(selected_klipper_instance) instance_names.append(klipper_instances[index].suffix) create_example_cfg = get_confirm("Create example moonraker.conf?") From 30b44144696c32531f92ada0457d90147d68f90c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 18:00:16 +0100 Subject: [PATCH 047/296] feat(Klipper): create example printer.cfg if wanted Signed-off-by: Dominik Willner --- kiauh/modules/klipper/__init__.py | 3 +++ kiauh/modules/klipper/klipper.py | 12 ++++++++++-- kiauh/modules/klipper/klipper_setup.py | 18 ++++++++++++++++-- kiauh/modules/klipper/klipper_utils.py | 21 ++++++++++++++++++++- kiauh/modules/klipper/res/printer.cfg | 11 +++++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 kiauh/modules/klipper/res/printer.cfg diff --git a/kiauh/modules/klipper/__init__.py b/kiauh/modules/klipper/__init__.py index 2b0d427..f355f5d 100644 --- a/kiauh/modules/klipper/__init__.py +++ b/kiauh/modules/klipper/__init__.py @@ -9,8 +9,11 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import os from pathlib import Path +MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) + KLIPPER_DIR = f"{Path.home()}/klipper" KLIPPER_ENV_DIR = f"{Path.home()}/klippy-env" KLIPPER_REQUIREMENTS_TXT = f"{KLIPPER_DIR}/scripts/klippy-requirements.txt" diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index ba9e1b5..02eb639 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -31,7 +31,7 @@ class Klipper(BaseInstance): super().__init__(instance_type=self, suffix=suffix) self.klipper_dir = KLIPPER_DIR self.env_dir = KLIPPER_ENV_DIR - self._cfg_file = f"{self.cfg_dir}/printer.cfg" + self._cfg_file = self._get_cfg() self._log = f"{self.log_dir}/klippy.log" self._serial = f"{self.comms_dir}/klippy.serial" self._uds = f"{self.comms_dir}/klippy.sock" @@ -156,8 +156,16 @@ class Klipper(BaseInstance): env_file_content = env_template_file_content.replace( "%KLIPPER_DIR%", self.klipper_dir ) - env_file_content = env_file_content.replace("%CFG%", self._cfg_file) + env_file_content = env_file_content.replace( + "%CFG%", f"{self.cfg_dir}/printer.cfg" + ) env_file_content = env_file_content.replace("%SERIAL%", self._serial) env_file_content = env_file_content.replace("%LOG%", self._log) env_file_content = env_file_content.replace("%UDS%", self._uds) return env_file_content + + def _get_cfg(self): + cfg_file_loc = f"{self.cfg_dir}/printer.cfg" + if Path(cfg_file_loc).is_file(): + return cfg_file_loc + return None diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 13008c3..dba98c2 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -38,6 +38,7 @@ from kiauh.modules.klipper.klipper_utils import ( handle_disruptive_system_packages, check_user_groups, handle_single_to_multi_conversion, + create_example_printer_cfg, ) from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.utils.input_utils import ( @@ -106,6 +107,8 @@ def install_klipper( Logger.print_status(EXIT_KLIPPER_SETUP) return + create_example_cfg = get_confirm("Create example printer.cfg?") + if len(instance_list) < 1: setup_klipper_prerequesites() @@ -117,13 +120,24 @@ def install_klipper( for name in instance_names: if convert_single_to_multi: - handle_single_to_multi_conversion(instance_manager, name) + current_instance = handle_single_to_multi_conversion(instance_manager, name) convert_single_to_multi = False else: - instance_manager.current_instance = Klipper(suffix=name) + current_instance = Klipper(suffix=name) + instance_manager.current_instance = current_instance instance_manager.create_instance() instance_manager.enable_instance() + + if create_example_cfg: + cfg_dir = current_instance.cfg_dir + Logger.print_status(f"Creating example printer.cfg in '{cfg_dir}'") + if current_instance.cfg_file is None: + create_example_printer_cfg(current_instance) + Logger.print_ok(f"Example printer.cfg created in '{cfg_dir}'") + else: + Logger.print_info(f"printer.cfg in '{cfg_dir}' already exists.") + instance_manager.start_instance() instance_manager.reload_daemon() diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index aa11124..fcbc99a 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -12,12 +12,15 @@ import os import re import grp +import shutil import subprocess import textwrap from typing import List, Union +from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.modules.klipper import MODULE_PATH from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.klipper.klipper_dialogs import ( print_missing_usergroup_dialog, @@ -92,7 +95,7 @@ def handle_existing_multi_instance_names( def handle_single_to_multi_conversion( instance_manager: InstanceManager, name: str -) -> None: +) -> Klipper: instance_list = instance_manager.instances instance_manager.current_instance = instance_list[0] old_data_dir_name = instance_manager.instances[0].data_dir @@ -103,6 +106,7 @@ def handle_single_to_multi_conversion( new_data_dir_name = instance_manager.current_instance.data_dir try: os.rename(old_data_dir_name, new_data_dir_name) + return instance_manager.current_instance except OSError as e: log = f"Cannot rename {old_data_dir_name} to {new_data_dir_name}:\n{e}" Logger.print_error(log) @@ -188,3 +192,18 @@ def has_custom_names(instance_list: List[Klipper]) -> bool: def get_highest_index(instance_list: List[Klipper]) -> int: indices = [int(instance.suffix.split("-")[-1]) for instance in instance_list] return max(indices) + + +def create_example_printer_cfg(instance: Klipper) -> None: + source = os.path.join(MODULE_PATH, "res", "printer.cfg") + target = os.path.join(instance.cfg_dir, "printer.cfg") + try: + shutil.copy(source, target) + except OSError as e: + Logger.print_error(f"Unable to create example printer.cfg:\n{e}") + return + + cm = ConfigManager(target) + cm.read_config() + cm.set_value("virtual_sdcard", "path", instance.gcodes_dir) + cm.write_config() diff --git a/kiauh/modules/klipper/res/printer.cfg b/kiauh/modules/klipper/res/printer.cfg new file mode 100644 index 0000000..88fe7df --- /dev/null +++ b/kiauh/modules/klipper/res/printer.cfg @@ -0,0 +1,11 @@ +[mcu] +serial: /dev/serial/by-id/ + +[virtual_sdcard] +path: %GCODES_DIR% +on_error_gcode: CANCEL_PRINT + +[printer] +kinematics: none +max_velocity: 1000 +max_accel: 1000 From b83f642a13a37a23838842e4d8d70b461e209491 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 18:03:42 +0100 Subject: [PATCH 048/296] refactor(ConfigManager): logging can be silenced Signed-off-by: Dominik Willner --- kiauh/core/config_manager/config_manager.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index 593773f..00cc41b 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -32,15 +32,17 @@ class ConfigManager: with open(self.config_file, "w") as cfg: self.config.write(cfg) - def get_value(self, section: str, key: str) -> Union[str, bool, None]: + def get_value(self, section: str, key: str, silent=False) -> Union[str, bool, None]: if not self.config.has_section(section): - log = f"Section not defined. Unable to read section: [{section}]." - Logger.print_error(log) + if not silent: + log = f"Section not defined. Unable to read section: [{section}]." + Logger.print_error(log) return None if not self.config.has_option(section, key): - log = f"Option not defined in section [{section}]. Unable to read option: '{key}'." - Logger.print_error(log) + if not silent: + log = f"Option not defined in section [{section}]. Unable to read option: '{key}'." + Logger.print_error(log) return None value = self.config.get(section, key) From 3865266da15b2da044648a5386370e187968bfe8 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 18:07:18 +0100 Subject: [PATCH 049/296] refactor(RepoManager): default to master branch if none is provided Signed-off-by: Dominik Willner --- kiauh/core/repo_manager/repo_manager.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index ee84c62..feda37f 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -19,9 +19,14 @@ from kiauh.utils.logger import Logger # noinspection PyMethodMayBeStatic class RepoManager: - def __init__(self, repo: str, branch: str, target_dir: str): + def __init__( + self, + repo: str, + target_dir: str, + branch: str = None, + ): self._repo = repo - self._branch = branch + self._branch = branch if branch is not None else "master" self._method = self._get_method() self._target_dir = target_dir @@ -62,9 +67,8 @@ class RepoManager: Logger.print_status(log) try: if os.path.exists(self.target_dir): - if not get_confirm( - "Target directory already exists. Overwrite?", default_choice=False - ): + question = f"'{self.target_dir}' already exists. Overwrite?" + if not get_confirm(question, default_choice=False): Logger.print_info("Skipping re-clone of repository.") return shutil.rmtree(self.target_dir) From c2e7ee98dfe309c13a3b512e863d4b187349feda Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 18:08:18 +0100 Subject: [PATCH 050/296] feat(Mainsail): implement Mainsail installer Signed-off-by: Dominik Willner --- kiauh.cfg.example | 4 + kiauh/core/menus/install_menu.py | 3 +- kiauh/modules/mainsail/__init__.py | 26 ++ kiauh/modules/mainsail/mainsail_dialogs.py | 95 ++++++ kiauh/modules/mainsail/mainsail_setup.py | 272 ++++++++++++++++++ .../mainsail/res/mainsail-config-updater.conf | 6 + .../mainsail/res/mainsail-updater.conf | 5 + 7 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 kiauh/modules/mainsail/__init__.py create mode 100644 kiauh/modules/mainsail/mainsail_dialogs.py create mode 100644 kiauh/modules/mainsail/mainsail_setup.py create mode 100644 kiauh/modules/mainsail/res/mainsail-config-updater.conf create mode 100644 kiauh/modules/mainsail/res/mainsail-updater.conf diff --git a/kiauh.cfg.example b/kiauh.cfg.example index 72f4a21..e1724d2 100644 --- a/kiauh.cfg.example +++ b/kiauh.cfg.example @@ -10,3 +10,7 @@ method: https repository_url: https://github.com/Arksine/moonraker branch: master method: https + +[mainsail] +default_port: 80 +unstable_releases: False diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 03c82f1..cd72444 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -14,6 +14,7 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup +from kiauh.modules.mainsail import mainsail_setup from kiauh.modules.moonraker import moonraker_setup from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT @@ -69,7 +70,7 @@ class InstallMenu(BaseMenu): moonraker_setup.run_moonraker_setup(install=True) def install_mainsail(self): - print("install_mainsail") + mainsail_setup.run_mainsail_setup(install=True) def install_fluidd(self): print("install_fluidd") diff --git a/kiauh/modules/mainsail/__init__.py b/kiauh/modules/mainsail/__init__.py new file mode 100644 index 0000000..06f2027 --- /dev/null +++ b/kiauh/modules/mainsail/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 pathlib import Path + +import os + +MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) +MAINSAIL_DIR = os.path.join(Path.home(), "mainsail") +MAINSAIL_CONFIG_DIR = os.path.join(Path.home(), "mainsail-config") +MAINSAIL_CONFIG_JSON = os.path.join(MAINSAIL_DIR, "config.json") +MAINSAIL_URL = ( + "https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" +) +MAINSAIL_UNSTABLE_URL = ( + "https://github.com/mainsail-crew/mainsail/releases/download/%TAG%/mainsail.zip" +) +MAINSAIL_CONFIG_REPO_URL = "https://github.com/mainsail-crew/mainsail-config.git" diff --git a/kiauh/modules/mainsail/mainsail_dialogs.py b/kiauh/modules/mainsail/mainsail_dialogs.py new file mode 100644 index 0000000..0c60bbf --- /dev/null +++ b/kiauh/modules/mainsail/mainsail_dialogs.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap + +from kiauh.core.menus.base_menu import print_back_footer +from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN + + +def print_moonraker_not_found_dialog(): + line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}No local Moonraker installation was found!{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | {line1:<63}| + | {line2:<63}| + |-------------------------------------------------------| + | It is possible to install Mainsail without a local | + | Moonraker installation. If you continue, you need to | + | make sure, that Moonraker is installed on another | + | machine in your network. Otherwise Mainsail will NOT | + | work correctly. | + """ + )[1:] + + print(dialog, end="") + print_back_footer() + + +def print_mainsail_already_installed_dialog(): + line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}Mainsail seems to be already installed!{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | {line1:<63}| + | {line2:<63}| + |-------------------------------------------------------| + | If you continue, your current Mainsail installation | + | will be overwritten. You will not loose any printer | + | configurations and the Moonraker database will remain | + | untouched. | + """ + )[1:] + + print(dialog, end="") + print_back_footer() + + +def print_install_mainsail_config_dialog(): + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | It is recommended to use special macros in order to | + | have Mainsail fully functional and working. | + | | + | The recommended macros for Mainsail can be seen here: | + | https://github.com/mainsail-crew/mainsail-config | + | | + | If you already use these macros skip this step. | + | Otherwise you should consider to answer with 'Y' to | + | download the recommended macros. | + \\=======================================================/ + """ + )[1:] + + print(dialog, end="") + + +def print_mainsail_port_select_dialog(port: str): + port = f"{COLOR_CYAN}{port}{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | Please select the port, Mainsail should be served on. | + | If you are unsure what to select, hit Enter to apply | + | the suggested value of: {port:38} | + | | + | In case you need Mainsail to be served on a specific | + | port, you can set it now. Make sure the port is not | + | used by any other application on your system! | + \\=======================================================/ + """ + )[1:] + + print(dialog, end="") diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py new file mode 100644 index 0000000..3dc3607 --- /dev/null +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import json +import os.path +import shutil +from pathlib import Path +from typing import List + +from kiauh import KIAUH_CFG +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.repo_manager.repo_manager import RepoManager +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.mainsail import ( + MAINSAIL_URL, + MAINSAIL_DIR, + MAINSAIL_CONFIG_DIR, + MAINSAIL_CONFIG_REPO_URL, + MAINSAIL_CONFIG_JSON, + MODULE_PATH, +) +from kiauh.modules.mainsail.mainsail_dialogs import ( + print_moonraker_not_found_dialog, + print_mainsail_already_installed_dialog, + print_install_mainsail_config_dialog, + print_mainsail_port_select_dialog, +) +from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.utils.common import check_install_dependencies +from kiauh.utils.input_utils import get_confirm, get_number_input +from kiauh.utils.logger import Logger +from kiauh.utils.system_utils import ( + download_file, + unzip, + create_upstream_nginx_cfg, + create_nginx_cfg, + delete_default_nginx_cfg, + enable_nginx_cfg, + set_nginx_permissions, + get_ipv4_addr, + control_systemd_service, + create_common_vars_nginx_cfg, +) + + +def run_mainsail_setup(install: bool) -> None: + im_mr = InstanceManager(Moonraker) + is_moonraker_installed = len(im_mr.instances) > 0 + + enable_remotemode = False + if not is_moonraker_installed: + print_moonraker_not_found_dialog() + do_continue = get_confirm("Continue Mainsail installation?", allow_go_back=True) + if do_continue: + enable_remotemode = True + else: + return + + is_mainsail_installed = Path(f"{Path.home()}/mainsail").exists() + do_reinstall = False + if is_mainsail_installed: + print_mainsail_already_installed_dialog() + do_reinstall = get_confirm("Re-install Mainsail?", allow_go_back=True) + if do_reinstall: + backup_config_json() + else: + return + + im_kl = InstanceManager(Klipper) + is_klipper_installed = len(im_kl.instances) > 0 + install_ms_config = False + if is_klipper_installed: + print_install_mainsail_config_dialog() + question = "Download the recommended macros?" + install_ms_config = get_confirm(question, allow_go_back=False) + + cm = ConfigManager(cfg_file=KIAUH_CFG) + cm.read_config() + default_port = cm.get_value("mainsail", "default_port") + mainsail_port = default_port if default_port else 80 + if not default_port: + print_mainsail_port_select_dialog(f"{mainsail_port}") + mainsail_port = get_number_input( + "Configure Mainsail for port", + min_count=mainsail_port, + default=mainsail_port, + ) + + check_install_dependencies(["nginx"]) + + try: + download_mainsail() + if do_reinstall: + restore_config_json() + if enable_remotemode: + enable_mainsail_remotemode() + if is_moonraker_installed: + patch_moonraker_conf( + im_mr.instances, + "Mainsail", + "update_manager mainsail", + "mainsail-updater.conf", + ) + im_mr.restart_all_instance() + if is_klipper_installed and install_ms_config: + download_mainsail_config() + patch_moonraker_conf( + im_mr.instances, + "mainsail-config", + "update_manager mainsail-config", + "mainsail-config-updater.conf", + ) + patch_printer_config(im_kl.instances) + im_kl.restart_all_instance() + + create_upstream_nginx_cfg() + create_common_vars_nginx_cfg() + create_mainsail_nginx_cfg(mainsail_port) + if is_klipper_installed: + symlink_webui_nginx_log(im_kl.instances) + control_systemd_service("nginx", "restart") + + except Exception as e: + Logger.print_error(f"Mainsail installation failed!\n{e}") + return + + log = f"Open Mainsail now on: http://{get_ipv4_addr()}:{mainsail_port}" + Logger.print_ok("Mainsail installation complete!", start="\n") + Logger.print_ok(log, prefix=False, end="\n\n") + + +def download_mainsail() -> None: + try: + Logger.print_status("Downloading Mainsail ...") + download_file(MAINSAIL_URL, f"{Path.home()}", "mainsail.zip") + Logger.print_ok("Download complete!") + + Logger.print_status("Extracting mainsail.zip ...") + unzip(f"{Path.home()}/mainsail.zip", MAINSAIL_DIR) + Logger.print_ok("OK!") + + except Exception: + Logger.print_error("Downloading Mainsail failed!") + raise + + +def download_mainsail_config() -> None: + try: + Logger.print_status("Downloading mainsail-config ...") + rm = RepoManager(MAINSAIL_CONFIG_REPO_URL, target_dir=MAINSAIL_CONFIG_DIR) + rm.clone_repo() + except Exception: + Logger.print_error("Downloading mainsail-config failed!") + raise + + +def create_mainsail_nginx_cfg(port: int) -> None: + try: + Logger.print_status("Creating NGINX config for Mainsail ...") + root_dir = MAINSAIL_DIR + delete_default_nginx_cfg() + create_nginx_cfg("mainsail", port, root_dir) + enable_nginx_cfg("mainsail") + set_nginx_permissions() + Logger.print_ok("NGINX config for Mainsail successfully created.") + except Exception: + Logger.print_error("Creating NGINX config for Mainsail failed!") + raise + + +def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: + Logger.print_status("Link NGINX logs into log directory ...") + access_log = Path("/var/log/nginx/mainsail-access.log") + error_log = Path("/var/log/nginx/mainsail-error.log") + + for instance in klipper_instances: + desti_access = Path(instance.log_dir).joinpath("mainsail-access.log") + if not desti_access.exists(): + desti_access.symlink_to(access_log) + + desti_error = Path(instance.log_dir).joinpath("mainsail-error.log") + if not desti_error.exists(): + desti_error.symlink_to(error_log) + + +def patch_moonraker_conf( + moonraker_instances: List[Moonraker], + name: str, + section_name: str, + template_file: str, +) -> None: + for instance in moonraker_instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Add {name} update section to '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + return + + cm = ConfigManager(cfg_file) + cm.read_config() + if cm.config.has_section(section_name): + Logger.print_info("Section already exist. Skipped ...") + return + + template = os.path.join(MODULE_PATH, "res", template_file) + with open(template, "r") as t: + template_content = "\n" + template_content += t.read() + + with open(cfg_file, "a") as f: + f.write(template_content) + + +def patch_printer_config(klipper_instances: List[Klipper]) -> None: + for instance in klipper_instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Including mainsail-config in '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + return + + cm = ConfigManager(cfg_file) + cm.read_config() + if cm.config.has_section("include mainsail.cfg"): + Logger.print_info("Section already exist. Skipped ...") + return + + with open(cfg_file, "a") as f: + f.write("\n[include mainsail.cfg]") + + +def backup_config_json() -> None: + try: + Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") + target = os.path.join(Path.home(), "config.json.kiauh.bak") + shutil.copy(MAINSAIL_CONFIG_JSON, target) + except OSError: + Logger.print_info(f"Unable to backup config.json. Skipped ...") + + +def restore_config_json() -> None: + try: + Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") + source = os.path.join(Path.home(), "config.json.kiauh.bak") + shutil.copy(source, MAINSAIL_CONFIG_JSON) + except OSError: + Logger.print_info(f"Unable to restore config.json. Skipped ...") + + +def enable_mainsail_remotemode() -> None: + with open(MAINSAIL_CONFIG_JSON, "r") as f: + config_data = json.load(f) + + if config_data["instancesDB"] == "browser": + return + + Logger.print_status("Setting instance storage location to 'browser' ...") + config_data["instancesDB"] = "browser" + + with open(MAINSAIL_CONFIG_JSON, "w") as f: + json.dump(config_data, f, indent=4) diff --git a/kiauh/modules/mainsail/res/mainsail-config-updater.conf b/kiauh/modules/mainsail/res/mainsail-config-updater.conf new file mode 100644 index 0000000..02bb789 --- /dev/null +++ b/kiauh/modules/mainsail/res/mainsail-config-updater.conf @@ -0,0 +1,6 @@ +[update_manager mainsail-config] +type: git_repo +primary_branch: master +path: ~/mainsail-config +origin: https://github.com/mainsail-crew/mainsail-config.git +managed_services: klipper diff --git a/kiauh/modules/mainsail/res/mainsail-updater.conf b/kiauh/modules/mainsail/res/mainsail-updater.conf new file mode 100644 index 0000000..f668332 --- /dev/null +++ b/kiauh/modules/mainsail/res/mainsail-updater.conf @@ -0,0 +1,5 @@ +[update_manager mainsail] +type: web +channel: stable +repo: mainsail-crew/mainsail +path: ~/mainsail From 926ba1acb415402ca3603aa8d6fe36b1dff2e1bb Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 21:49:09 +0100 Subject: [PATCH 051/296] feat(ConfigManager): implement own ConfigParser write() method Signed-off-by: Dominik Willner --- kiauh/core/config_manager/config_manager.py | 26 ++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index 00cc41b..4470575 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -19,7 +19,7 @@ from kiauh.utils.logger import Logger class ConfigManager: def __init__(self, cfg_file: str): self.config_file = cfg_file - self.config = configparser.ConfigParser() + self.config = CustomConfigParser() def read_config(self) -> None: if not self.config_file: @@ -55,3 +55,27 @@ class ConfigManager: def set_value(self, section: str, key: str, value: str): self.config.set(section, key, value) + + +class CustomConfigParser(configparser.ConfigParser): + """ + A custom ConfigParser class overwriting the write() method of configparser.Configparser. + Key and value will be delimited by a ": ". + Note the whitespace AFTER the colon, which is the whole reason for that overwrite. + """ + + def write(self, fp, space_around_delimiters=False): + if self._defaults: + fp.write("[%s]\n" % configparser.DEFAULTSECT) + for key, value in self._defaults.items(): + fp.write("%s: %s\n" % (key, str(value).replace("\n", "\n\t"))) + fp.write("\n") + for section in self._sections: + fp.write("[%s]\n" % section) + for key, value in self._sections[section].items(): + if key == "__name__": + continue + if (value is not None) or (self._optcre == self.OPTCRE): + key = ": ".join((key, str(value).replace("\n", "\n\t"))) + fp.write("%s\n" % key) + fp.write("\n") From 5fb4444f03e27baa774bc3f332335f6727602ec6 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 21:50:21 +0100 Subject: [PATCH 052/296] refactor(Moonraker): refactor example moonraker.conf creation Signed-off-by: Dominik Willner --- kiauh/modules/moonraker/moonraker_setup.py | 49 ++------------ kiauh/modules/moonraker/moonraker_utils.py | 75 ++++++++++++++++++++++ kiauh/modules/moonraker/res/moonraker.conf | 1 - 3 files changed, 80 insertions(+), 45 deletions(-) create mode 100644 kiauh/modules/moonraker/moonraker_utils.py diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index df2eb7c..c0dbb73 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -9,7 +9,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os import subprocess import sys from pathlib import Path @@ -35,11 +34,10 @@ from kiauh.modules.moonraker import ( POLKIT_FILE, POLKIT_USR_FILE, POLKIT_SCRIPT, - DEFAULT_MOONRAKER_PORT, - MODULE_PATH, ) from kiauh.modules.moonraker.moonraker import Moonraker from kiauh.modules.moonraker.moonraker_dialogs import print_moonraker_overview +from kiauh.modules.moonraker.moonraker_utils import create_example_moonraker_conf from kiauh.utils.input_utils import ( get_confirm, get_selection_input, @@ -52,7 +50,6 @@ from kiauh.utils.system_utils import ( update_system_package_lists, install_system_packages, check_file_exists, - get_ipv4_addr, ) @@ -133,9 +130,9 @@ def install_moonraker( setup_moonraker_prerequesites() install_moonraker_polkit() - ports_in_use = [ - instance.port for instance in moonraker_instances if instance.port is not None - ] + used_ports_map = { + instance.suffix: instance.port for instance in moonraker_instances + } for name in instance_names: current_instance = Moonraker(suffix=name) @@ -144,13 +141,7 @@ def install_moonraker( instance_manager.enable_instance() if create_example_cfg: - cfg_dir = current_instance.cfg_dir - Logger.print_status(f"Creating example moonraker.conf in '{cfg_dir}'") - if current_instance.cfg_file is None: - create_example_moonraker_conf(current_instance, ports_in_use) - Logger.print_ok(f"Example moonraker.conf created in '{cfg_dir}'") - else: - Logger.print_info(f"moonraker.conf in '{cfg_dir}' already exists.") + create_example_moonraker_conf(current_instance, used_ports_map) instance_manager.start_instance() @@ -324,33 +315,3 @@ def update_moonraker() -> None: ) repo_manager.pull_repo() instance_manager.start_all_instance() - - -def create_example_moonraker_conf(instance: Moonraker, ports: List[int]) -> None: - port = max(ports) + 1 if ports else DEFAULT_MOONRAKER_PORT - ports.append(port) - instance.port = port - example_cfg_path = os.path.join(MODULE_PATH, "res", "moonraker.conf") - - with open(f"{instance.cfg_dir}/moonraker.conf", "w") as cfg: - cfg.write(_prep_example_moonraker_conf(instance, example_cfg_path)) - - -def _prep_example_moonraker_conf(instance: Moonraker, example_cfg_path: str) -> str: - try: - with open(example_cfg_path, "r") as cfg: - example_cfg_content = cfg.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {example_cfg_path} - File not found") - raise - - example_cfg_content = example_cfg_content.replace("%PORT%", str(instance.port)) - example_cfg_content = example_cfg_content.replace( - "%UDS%", f"{instance.comms_dir}/klippy.sock" - ) - - ip = get_ipv4_addr().split(".")[:2] - ip.extend(["0", "0/16"]) - example_cfg_content = example_cfg_content.replace("%LAN%", ".".join(ip)) - - return example_cfg_content diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/modules/moonraker/moonraker_utils.py new file mode 100644 index 0000000..2c08f5e --- /dev/null +++ b/kiauh/modules/moonraker/moonraker_utils.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import os +import shutil +from typing import List, Dict + +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.modules.moonraker import ( + DEFAULT_MOONRAKER_PORT, + MODULE_PATH, +) +from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.utils.logger import Logger +from kiauh.utils.system_utils import ( + get_ipv4_addr, +) + + +def create_example_moonraker_conf( + instance: Moonraker, ports_map: Dict[str, int] +) -> None: + Logger.print_status(f"Creating example moonraker.conf in '{instance.cfg_dir}'") + if instance.cfg_file is not None: + Logger.print_info(f"moonraker.conf in '{instance.cfg_dir}' already exists.") + return + + source = os.path.join(MODULE_PATH, "res", "moonraker.conf") + target = os.path.join(instance.cfg_dir, "moonraker.conf") + try: + shutil.copy(source, target) + except OSError as e: + Logger.print_error(f"Unable to create example moonraker.conf:\n{e}") + return + + cm = ConfigManager(target) + cm.read_config() + + ports = [ + ports_map.get(instance) + for instance in ports_map + if ports_map.get(instance) is not None + ] + if ports_map.get(instance.suffix) is None: + # this could be improved to not increment the max value of the ports list and assign it as the port + # as it can lead to situation where the port for e.g. instance moonraker-2 becomes 7128 if the port + # of moonraker-1 is 7125 and moonraker-3 is 7127 and there are moonraker.conf files for moonraker-1 + # and moonraker-3 already. though, there does not seem to be a very reliable way of always assigning + # the correct port to each instance and the user will likely be required to correct the value manually. + port = max(ports) + 1 if ports else DEFAULT_MOONRAKER_PORT + else: + port = ports_map.get(instance.suffix) + + ports_map[instance.suffix] = port + + uds = f"{instance.comms_dir}/klippy.sock" + ip = get_ipv4_addr().split(".")[:2] + ip.extend(["0", "0/16"]) + trusted_clients = f"\n{'.'.join(ip)}" + trusted_clients += cm.get_value("authorization", "trusted_clients") + + cm.set_value("server", "port", str(port)) + cm.set_value("server", "klippy_uds_address", uds) + cm.set_value("authorization", "trusted_clients", trusted_clients) + + cm.write_config() + Logger.print_ok(f"Example moonraker.conf created in '{instance.cfg_dir}'") diff --git a/kiauh/modules/moonraker/res/moonraker.conf b/kiauh/modules/moonraker/res/moonraker.conf index d6eadd8..d985233 100644 --- a/kiauh/modules/moonraker/res/moonraker.conf +++ b/kiauh/modules/moonraker/res/moonraker.conf @@ -5,7 +5,6 @@ klippy_uds_address: %UDS% [authorization] trusted_clients: - %LAN% 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 From b8640f45a626a01db880eca8ef2502ee847b7596 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 17 Dec 2023 23:30:38 +0100 Subject: [PATCH 053/296] refactor(Klipper): refactor example printer.cfg creation Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 8 +------- kiauh/modules/klipper/klipper_utils.py | 6 ++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index dba98c2..78bc282 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -130,13 +130,7 @@ def install_klipper( instance_manager.enable_instance() if create_example_cfg: - cfg_dir = current_instance.cfg_dir - Logger.print_status(f"Creating example printer.cfg in '{cfg_dir}'") - if current_instance.cfg_file is None: - create_example_printer_cfg(current_instance) - Logger.print_ok(f"Example printer.cfg created in '{cfg_dir}'") - else: - Logger.print_info(f"printer.cfg in '{cfg_dir}' already exists.") + create_example_printer_cfg(current_instance) instance_manager.start_instance() diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index fcbc99a..e3439af 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -195,6 +195,11 @@ def get_highest_index(instance_list: List[Klipper]) -> int: def create_example_printer_cfg(instance: Klipper) -> None: + Logger.print_status(f"Creating example printer.cfg in '{instance.cfg_dir}'") + if instance.cfg_file is not None: + Logger.print_info(f"printer.cfg in '{instance.cfg_dir}' already exists.") + return + source = os.path.join(MODULE_PATH, "res", "printer.cfg") target = os.path.join(instance.cfg_dir, "printer.cfg") try: @@ -207,3 +212,4 @@ def create_example_printer_cfg(instance: Klipper) -> None: cm.read_config() cm.set_value("virtual_sdcard", "path", instance.gcodes_dir) cm.write_config() + Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'") From cd38970bbdc74eca18392efc3f1a1ba3f6980787 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 18 Dec 2023 20:58:27 +0100 Subject: [PATCH 054/296] refactor(Mainsail): move some functions to a mainsail utils module Signed-off-by: Dominik Willner --- kiauh/core/menus/install_menu.py | 2 +- kiauh/modules/mainsail/mainsail_setup.py | 58 +++----------------- kiauh/modules/mainsail/mainsail_utils.py | 67 ++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 kiauh/modules/mainsail/mainsail_utils.py diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index cd72444..b30e4ee 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -70,7 +70,7 @@ class InstallMenu(BaseMenu): moonraker_setup.run_moonraker_setup(install=True) def install_mainsail(self): - mainsail_setup.run_mainsail_setup(install=True) + mainsail_setup.run_mainsail_installation() def install_fluidd(self): print("install_fluidd") diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index 3dc3607..d5f934e 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -9,9 +9,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import json import os.path -import shutil from pathlib import Path from typing import List @@ -25,7 +23,6 @@ from kiauh.modules.mainsail import ( MAINSAIL_DIR, MAINSAIL_CONFIG_DIR, MAINSAIL_CONFIG_REPO_URL, - MAINSAIL_CONFIG_JSON, MODULE_PATH, ) from kiauh.modules.mainsail.mainsail_dialogs import ( @@ -34,6 +31,12 @@ from kiauh.modules.mainsail.mainsail_dialogs import ( print_install_mainsail_config_dialog, print_mainsail_port_select_dialog, ) +from kiauh.modules.mainsail.mainsail_utils import ( + restore_config_json, + enable_mainsail_remotemode, + backup_config_json, + symlink_webui_nginx_log, +) from kiauh.modules.moonraker.moonraker import Moonraker from kiauh.utils.common import check_install_dependencies from kiauh.utils.input_utils import get_confirm, get_number_input @@ -52,7 +55,7 @@ from kiauh.utils.system_utils import ( ) -def run_mainsail_setup(install: bool) -> None: +def run_mainsail_installation() -> None: im_mr = InstanceManager(Moonraker) is_moonraker_installed = len(im_mr.instances) > 0 @@ -177,21 +180,6 @@ def create_mainsail_nginx_cfg(port: int) -> None: raise -def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: - Logger.print_status("Link NGINX logs into log directory ...") - access_log = Path("/var/log/nginx/mainsail-access.log") - error_log = Path("/var/log/nginx/mainsail-error.log") - - for instance in klipper_instances: - desti_access = Path(instance.log_dir).joinpath("mainsail-access.log") - if not desti_access.exists(): - desti_access.symlink_to(access_log) - - desti_error = Path(instance.log_dir).joinpath("mainsail-error.log") - if not desti_error.exists(): - desti_error.symlink_to(error_log) - - def patch_moonraker_conf( moonraker_instances: List[Moonraker], name: str, @@ -238,35 +226,3 @@ def patch_printer_config(klipper_instances: List[Klipper]) -> None: with open(cfg_file, "a") as f: f.write("\n[include mainsail.cfg]") - - -def backup_config_json() -> None: - try: - Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") - target = os.path.join(Path.home(), "config.json.kiauh.bak") - shutil.copy(MAINSAIL_CONFIG_JSON, target) - except OSError: - Logger.print_info(f"Unable to backup config.json. Skipped ...") - - -def restore_config_json() -> None: - try: - Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") - source = os.path.join(Path.home(), "config.json.kiauh.bak") - shutil.copy(source, MAINSAIL_CONFIG_JSON) - except OSError: - Logger.print_info(f"Unable to restore config.json. Skipped ...") - - -def enable_mainsail_remotemode() -> None: - with open(MAINSAIL_CONFIG_JSON, "r") as f: - config_data = json.load(f) - - if config_data["instancesDB"] == "browser": - return - - Logger.print_status("Setting instance storage location to 'browser' ...") - config_data["instancesDB"] = "browser" - - with open(MAINSAIL_CONFIG_JSON, "w") as f: - json.dump(config_data, f, indent=4) diff --git a/kiauh/modules/mainsail/mainsail_utils.py b/kiauh/modules/mainsail/mainsail_utils.py new file mode 100644 index 0000000..92f7a61 --- /dev/null +++ b/kiauh/modules/mainsail/mainsail_utils.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import json +import os +import shutil +from pathlib import Path +from typing import List + +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.mainsail import MAINSAIL_CONFIG_JSON +from kiauh.utils.logger import Logger + + +def backup_config_json() -> None: + try: + Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") + target = os.path.join(Path.home(), "config.json.kiauh.bak") + shutil.copy(MAINSAIL_CONFIG_JSON, target) + except OSError: + Logger.print_info(f"Unable to backup config.json. Skipped ...") + + +def restore_config_json() -> None: + try: + Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") + source = os.path.join(Path.home(), "config.json.kiauh.bak") + shutil.copy(source, MAINSAIL_CONFIG_JSON) + except OSError: + Logger.print_info(f"Unable to restore config.json. Skipped ...") + + +def enable_mainsail_remotemode() -> None: + with open(MAINSAIL_CONFIG_JSON, "r") as f: + config_data = json.load(f) + + if config_data["instancesDB"] == "browser": + return + + Logger.print_status("Setting instance storage location to 'browser' ...") + config_data["instancesDB"] = "browser" + + with open(MAINSAIL_CONFIG_JSON, "w") as f: + json.dump(config_data, f, indent=4) + + +def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: + Logger.print_status("Link NGINX logs into log directory ...") + access_log = Path("/var/log/nginx/mainsail-access.log") + error_log = Path("/var/log/nginx/mainsail-error.log") + + for instance in klipper_instances: + desti_access = Path(instance.log_dir).joinpath("mainsail-access.log") + if not desti_access.exists(): + desti_access.symlink_to(access_log) + + desti_error = Path(instance.log_dir).joinpath("mainsail-error.log") + if not desti_error.exists(): + desti_error.symlink_to(error_log) From 4915896099d123c8f7a6d5d2e4d584630177677c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 21 Dec 2023 22:53:41 +0100 Subject: [PATCH 055/296] feat(Mainsail): remove Mainsail Signed-off-by: Dominik Willner --- kiauh/core/menus/remove_menu.py | 6 +- kiauh/modules/mainsail/mainsail_remove.py | 150 ++++++++++++++++++ kiauh/modules/mainsail/mainsail_utils.py | 2 + kiauh/modules/mainsail/menus/__init__.py | 0 .../mainsail/menus/mainsail_remove_menu.py | 121 ++++++++++++++ kiauh/utils/filesystem_utils.py | 26 +++ 6 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 kiauh/modules/mainsail/mainsail_remove.py create mode 100644 kiauh/modules/mainsail/menus/__init__.py create mode 100644 kiauh/modules/mainsail/menus/mainsail_remove_menu.py create mode 100644 kiauh/utils/filesystem_utils.py diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 1271557..4082c8f 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -14,6 +14,8 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup +from kiauh.modules.mainsail import mainsail_setup +from kiauh.modules.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu from kiauh.modules.moonraker import moonraker_setup from kiauh.utils.constants import COLOR_RED, RESET_FORMAT @@ -26,7 +28,7 @@ class RemoveMenu(BaseMenu): options={ 1: self.remove_klipper, 2: self.remove_moonraker, - 3: self.remove_mainsail, + 3: MainsailRemoveMenu, 4: self.remove_mainsail_config, 5: self.remove_fluidd, 6: self.remove_fluidd_config, @@ -77,7 +79,7 @@ class RemoveMenu(BaseMenu): moonraker_setup.run_moonraker_setup(install=False) def remove_mainsail(self): - print("remove_mainsail") + mainsail_setup.run_mainsail_removal() def remove_mainsail_config(self): print("remove_mainsail_config") diff --git a/kiauh/modules/mainsail/mainsail_remove.py b/kiauh/modules/mainsail/mainsail_remove.py new file mode 100644 index 0000000..525ad3f --- /dev/null +++ b/kiauh/modules/mainsail/mainsail_remove.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + + +import shutil +import subprocess +from pathlib import Path + +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR +from kiauh.modules.mainsail.mainsail_utils import backup_config_json +from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from kiauh.utils.filesystem_utils import remove_file +from kiauh.utils.logger import Logger + + +def run_mainsail_removal( + remove_mainsail: bool, + remove_ms_config: bool, + backup_ms_config_json: bool, + remove_mr_updater_section: bool, + remove_msc_printer_cfg_include: bool, +) -> None: + if backup_ms_config_json: + backup_config_json() + if remove_mainsail: + remove_mainsail_dir() + remove_nginx_config() + remove_nginx_logs() + if remove_mr_updater_section: + remove_updater_section("update_manager mainsail") + if remove_ms_config: + remove_ms_config_dir() + if remove_mr_updater_section: + remove_updater_section("update_manager mainsail-config") + if remove_msc_printer_cfg_include: + remove_printer_cfg_include() + + +def remove_mainsail_dir() -> None: + Logger.print_status("Removing Mainsail ...") + if not Path(MAINSAIL_DIR).exists(): + Logger.print_info(f"'{MAINSAIL_DIR}' does not exist. Skipping ...") + return + + try: + shutil.rmtree(MAINSAIL_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{MAINSAIL_DIR}':\n{e}") + + +def remove_nginx_config() -> None: + Logger.print_status("Removing Mainsails NGINX config ...") + try: + remove_file(Path(NGINX_SITES_AVAILABLE).joinpath("mainsail"), True) + remove_file(Path(NGINX_SITES_ENABLED).joinpath("mainsail"), True) + + except subprocess.CalledProcessError as e: + log = f"Unable to remove Mainsail NGINX config:\n{e.stderr.decode()}" + Logger.print_error(log) + + +def remove_nginx_logs() -> None: + Logger.print_status("Removing Mainsails NGINX logs ...") + try: + remove_file(Path("/var/log/nginx/mainsail-access.log"), True) + remove_file(Path("/var/log/nginx/mainsail-error.log"), True) + + im = InstanceManager(Klipper) + if not im.instances: + return + + for instance in im.instances: + remove_file(Path(instance.log_dir).joinpath("mainsail-access.log")) + remove_file(Path(instance.log_dir).joinpath("mainsail-error.log")) + + except (OSError, subprocess.CalledProcessError) as e: + Logger.print_error(f"Unable to NGINX logs:\n{e}") + + +def remove_updater_section(name: str) -> None: + Logger.print_status("Remove updater section from moonraker.conf ...") + im = InstanceManager(Moonraker) + if not im.instances: + Logger.print_info("Moonraker not installed. Skipping ...") + return + + for instance in im.instances: + log = f"Remove section '{name}' in '{instance.cfg_file}' ..." + Logger.print_status(log) + + if not Path(instance.cfg_file).exists(): + Logger.print_info("Section not present. Skipping ...") + continue + + cm = ConfigManager(instance.cfg_file) + cm.read_config() + if not cm.config.has_section(name): + Logger.print_info("Section not present. Skipped ...") + continue + + cm.config.remove_section(name) + cm.write_config() + + +def remove_ms_config_dir() -> None: + Logger.print_status("Removing mainsail-config ...") + if not Path(MAINSAIL_CONFIG_DIR).exists(): + Logger.print_info(f"'{MAINSAIL_CONFIG_DIR}' does not exist. Skipping ...") + return + + try: + shutil.rmtree(MAINSAIL_CONFIG_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{MAINSAIL_CONFIG_DIR}':\n{e}") + + +def remove_printer_cfg_include() -> None: + Logger.print_status("Remove mainsail-config include from printer.cfg ...") + im = InstanceManager(Klipper) + if not im.instances: + Logger.print_info("Klipper not installed. Skipping ...") + return + + for instance in im.instances: + log = f"Removing include from '{instance.cfg_file}' ..." + Logger.print_status(log) + + if not Path(instance.cfg_file).exists(): + continue + + cm = ConfigManager(instance.cfg_file) + cm.read_config() + if not cm.config.has_section("include mainsail.cfg"): + Logger.print_info("Section not present. Skipped ...") + continue + + cm.config.remove_section("include mainsail.cfg") + cm.write_config() diff --git a/kiauh/modules/mainsail/mainsail_utils.py b/kiauh/modules/mainsail/mainsail_utils.py index 92f7a61..4cfc9d5 100644 --- a/kiauh/modules/mainsail/mainsail_utils.py +++ b/kiauh/modules/mainsail/mainsail_utils.py @@ -20,6 +20,8 @@ from kiauh.modules.mainsail import MAINSAIL_CONFIG_JSON from kiauh.utils.logger import Logger +# TODO: give this method an optional target dir to backup to +# alteratively use the BackupManager for handling this task (probably best) def backup_config_json() -> None: try: Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") diff --git a/kiauh/modules/mainsail/menus/__init__.py b/kiauh/modules/mainsail/menus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/modules/mainsail/menus/mainsail_remove_menu.py b/kiauh/modules/mainsail/menus/mainsail_remove_menu.py new file mode 100644 index 0000000..fd56a60 --- /dev/null +++ b/kiauh/modules/mainsail/menus/mainsail_remove_menu.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap + +from kiauh.core.menus import BACK_HELP_FOOTER +from kiauh.core.menus.base_menu import BaseMenu +from kiauh.modules.mainsail import mainsail_remove +from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN + + +class MainsailRemoveMenu(BaseMenu): + def __init__(self): + super().__init__( + header=False, + options={ + 0: self.toggle_all, + 1: self.toggle_remove_mainsail, + 2: self.toggle_remove_ms_config, + 3: self.toggle_backup_config_json, + 4: self.toggle_remove_updater_section, + 5: self.toggle_remove_printer_cfg_include, + 6: self.run_removal_process, + }, + footer_type=BACK_HELP_FOOTER, + ) + self.remove_mainsail = False + self.remove_ms_config = False + self.backup_config_json = False + self.remove_updater_section = False + self.remove_printer_cfg_include = False + + def print_menu(self) -> None: + header = " [ Remove Mainsail ] " + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" + unchecked = "[ ]" + o1 = checked if self.remove_mainsail else unchecked + o2 = checked if self.remove_ms_config else unchecked + o3 = checked if self.backup_config_json else unchecked + o4 = checked if self.remove_updater_section else unchecked + o5 = checked if self.remove_printer_cfg_include else unchecked + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Enter a number and hit enter to select / deselect | + | the specific option for removal. | + |-------------------------------------------------------| + | 0) Everything | + |-------------------------------------------------------| + | 1) {o1} Remove Mainsail Web UI | + | 2) {o2} Remove mainsail-config | + | 3) {o3} Backup Mainsail config.json | + | | + | printer.cfg & moonraker.conf | + | 4) {o4} Remove Moonraker updater section | + | 5) {o5} Remove printer.cfg include | + |-------------------------------------------------------| + | 6) Start removal | + """ + )[1:] + print(menu, end="") + + def toggle_all(self) -> None: + self.remove_mainsail = True + self.remove_ms_config = True + self.backup_config_json = True + self.remove_updater_section = True + self.remove_printer_cfg_include = True + + def toggle_remove_mainsail(self) -> None: + self.remove_mainsail = not self.remove_mainsail + + def toggle_remove_ms_config(self) -> None: + self.remove_ms_config = not self.remove_ms_config + + def toggle_backup_config_json(self) -> None: + self.backup_config_json = not self.backup_config_json + + def toggle_remove_updater_section(self) -> None: + self.remove_updater_section = not self.remove_updater_section + + def toggle_remove_printer_cfg_include(self) -> None: + self.remove_printer_cfg_include = not self.remove_printer_cfg_include + + def run_removal_process(self) -> None: + if ( + not self.remove_mainsail + and not self.remove_ms_config + and not self.backup_config_json + and not self.remove_updater_section + and not self.remove_printer_cfg_include + ): + error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" + print(error) + return + + mainsail_remove.run_mainsail_removal( + remove_mainsail=self.remove_mainsail, + remove_ms_config=self.remove_ms_config, + backup_ms_config_json=self.backup_config_json, + remove_mr_updater_section=self.remove_updater_section, + remove_msc_printer_cfg_include=self.remove_printer_cfg_include, + ) + + self.remove_mainsail = False + self.remove_ms_config = False + self.backup_config_json = False + self.remove_updater_section = False + self.remove_printer_cfg_include = False diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py new file mode 100644 index 0000000..b89e939 --- /dev/null +++ b/kiauh/utils/filesystem_utils.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import subprocess +from pathlib import Path + + +from kiauh.utils.logger import Logger + + +def remove_file(file_path: Path, sudo=False) -> None: + try: + command = f"{'sudo ' if sudo else ''}rm -f {file_path}" + subprocess.run(command, stderr=subprocess.PIPE, check=True, shell=True) + except subprocess.CalledProcessError as e: + log = f"Cannot remove file {file_path}: {e.stderr.decode()}" + Logger.print_error(log) + raise From 12bd8eb79930de0ffbc9c059e7532af8de6edf09 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 22 Dec 2023 21:25:20 +0100 Subject: [PATCH 056/296] feat(KIAUH): move filesystem related methods to own module Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/base_instance.py | 2 +- kiauh/modules/mainsail/mainsail_setup.py | 16 +- kiauh/modules/moonraker/moonraker_setup.py | 8 +- kiauh/utils/filesystem_utils.py | 145 ++++++++++++++++++- kiauh/utils/system_utils.py | 141 +----------------- 5 files changed, 155 insertions(+), 157 deletions(-) diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index 9e8143f..0251694 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import List, Union, Optional, Type, TypeVar from kiauh.utils.constants import SYSTEMD, CURRENT_USER -from kiauh.utils.system_utils import create_directory +from kiauh.utils.filesystem_utils import create_directory B = TypeVar(name="B", bound="BaseInstance", covariant=True) diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index d5f934e..0b6a45d 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -24,35 +24,31 @@ from kiauh.modules.mainsail import ( MAINSAIL_CONFIG_DIR, MAINSAIL_CONFIG_REPO_URL, MODULE_PATH, -) + ) from kiauh.modules.mainsail.mainsail_dialogs import ( print_moonraker_not_found_dialog, print_mainsail_already_installed_dialog, print_install_mainsail_config_dialog, print_mainsail_port_select_dialog, -) + ) from kiauh.modules.mainsail.mainsail_utils import ( restore_config_json, enable_mainsail_remotemode, backup_config_json, symlink_webui_nginx_log, -) + ) from kiauh.modules.moonraker.moonraker import Moonraker from kiauh.utils.common import check_install_dependencies +from kiauh.utils.filesystem_utils import unzip, create_upstream_nginx_cfg, create_common_vars_nginx_cfg, \ + delete_default_nginx_cfg, create_nginx_cfg, enable_nginx_cfg from kiauh.utils.input_utils import get_confirm, get_number_input from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( download_file, - unzip, - create_upstream_nginx_cfg, - create_nginx_cfg, - delete_default_nginx_cfg, - enable_nginx_cfg, set_nginx_permissions, get_ipv4_addr, control_systemd_service, - create_common_vars_nginx_cfg, -) + ) def run_mainsail_installation() -> None: diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index c0dbb73..4f5a2a3 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -38,6 +38,7 @@ from kiauh.modules.moonraker import ( from kiauh.modules.moonraker.moonraker import Moonraker from kiauh.modules.moonraker.moonraker_dialogs import print_moonraker_overview from kiauh.modules.moonraker.moonraker_utils import create_example_moonraker_conf +from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.input_utils import ( get_confirm, get_selection_input, @@ -49,7 +50,6 @@ from kiauh.utils.system_utils import ( install_python_requirements, update_system_package_lists, install_system_packages, - check_file_exists, ) @@ -181,9 +181,9 @@ def install_moonraker_packages(moonraker_dir: Path) -> None: def install_moonraker_polkit() -> None: Logger.print_status("Installing Moonraker policykit rules ...") - legacy_file_exists = check_file_exists(Path(POLKIT_LEGACY_FILE)) - polkit_file_exists = check_file_exists(Path(POLKIT_FILE)) - usr_file_exists = check_file_exists(Path(POLKIT_USR_FILE)) + legacy_file_exists = check_file_exist(Path(POLKIT_LEGACY_FILE)) + polkit_file_exists = check_file_exist(Path(POLKIT_FILE)) + usr_file_exists = check_file_exist(Path(POLKIT_USR_FILE)) if legacy_file_exists or (polkit_file_exists and usr_file_exists): Logger.print_info("Moonraker policykit rules are already installed.") diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index b89e939..727cc8f 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # # # @@ -9,13 +8,46 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import os +import shutil import subprocess from pathlib import Path +from zipfile import ZipFile - +from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED, MODULE_PATH, NGINX_CONFD from kiauh.utils.logger import Logger +def check_file_exist(file_path: Path) -> bool: + """ + Helper function for checking the existence of a file where + elevated permissions are required | + :param file_path: the absolute path of the file to check + :return: True if file exists, otherwise False + """ + try: + command = ["sudo", "find", file_path] + subprocess.check_output(command, stderr=subprocess.DEVNULL) + return True + except subprocess.CalledProcessError: + return False + + +def create_directory(_dir: Path) -> None: + """ + Helper function for creating a directory or skipping if it already exists | + :param _dir: the directory to create + :return: None + """ + try: + if not os.path.isdir(_dir): + os.makedirs(_dir, exist_ok=True) + Logger.print_ok(f"Created directory: {_dir}") + except OSError as e: + Logger.print_error(f"Error creating folder: {e}") + raise + + def remove_file(file_path: Path, sudo=False) -> None: try: command = f"{'sudo ' if sudo else ''}rm -f {file_path}" @@ -24,3 +56,112 @@ def remove_file(file_path: Path, sudo=False) -> None: log = f"Cannot remove file {file_path}: {e.stderr.decode()}" Logger.print_error(log) raise + +def unzip(file: str, target_dir: str) -> None: + """ + Helper function to unzip a zip-archive into a target directory | + :param file: the zip-file to unzip + :param target_dir: the target directory to extract the files into + :return: None + """ + with ZipFile(file, "r") as _zip: + _zip.extractall(target_dir) + + +def create_upstream_nginx_cfg() -> None: + """ + Creates an upstream.conf in /etc/nginx/conf.d + :return: None + """ + source = os.path.join(MODULE_PATH, "res", "upstreams.conf") + target = os.path.join(NGINX_CONFD, "upstreams.conf") + try: + command = ["sudo", "cp", source, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create upstreams.conf: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def create_common_vars_nginx_cfg() -> None: + """ + Creates a common_vars.conf in /etc/nginx/conf.d + :return: None + """ + source = os.path.join(MODULE_PATH, "res", "common_vars.conf") + target = os.path.join(NGINX_CONFD, "common_vars.conf") + try: + command = ["sudo", "cp", source, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create upstreams.conf: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def create_nginx_cfg(name: str, port: int, root_dir: str) -> None: + """ + Creates an NGINX config from a template file and replaces all placeholders + :param name: name of the config to create + :param port: listen port + :param root_dir: directory of the static files + :return: None + """ + tmp = f"{Path.home()}/{name}.tmp" + shutil.copy(os.path.join(MODULE_PATH, "res", "nginx_cfg"), tmp) + with open(tmp, "r+") as f: + content = f.read() + content = content.replace("%NAME%", name) + content = content.replace("%PORT%", str(port)) + content = content.replace("%ROOT_DIR%", root_dir) + f.seek(0) + f.write(content) + f.truncate() + + target = os.path.join(NGINX_SITES_AVAILABLE, name) + try: + command = ["sudo", "mv", tmp, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create '{target}': {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def delete_default_nginx_cfg() -> None: + """ + Deletes a default NGINX config + :return: None + """ + default_cfg = Path("/etc/nginx/sites-enabled/default") + if not check_file_exist(default_cfg): + return + + try: + command = ["sudo", "rm", default_cfg] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to delete '{default_cfg}': {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def enable_nginx_cfg(name: str) -> None: + """ + Helper method to enable an NGINX config | + :param name: name of the config to enable + :return: None + """ + source = os.path.join(NGINX_SITES_AVAILABLE, name) + target = os.path.join(NGINX_SITES_ENABLED, name) + if check_file_exist(Path(target)): + return + + try: + command = ["sudo", "ln", "-s", source, target] + subprocess.run(command, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + log = f"Unable to create symlink: {e.stderr.decode()}" + Logger.print_error(log) + raise diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index f14fbb2..4925707 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -27,6 +27,7 @@ from kiauh.utils import ( NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED, ) +from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger @@ -215,21 +216,6 @@ def install_system_packages(packages: List[str]) -> None: kill(f"Error installing packages:\n{e.stderr.decode()}") -def create_directory(_dir: Path) -> None: - """ - Helper function for creating a directory or skipping if it already exists | - :param _dir: the directory to create - :return: None - """ - try: - if not os.path.isdir(_dir): - os.makedirs(_dir, exist_ok=True) - Logger.print_ok(f"Created directory: {_dir}") - except OSError as e: - Logger.print_error(f"Error creating folder: {e}") - raise - - def mask_system_service(service_name: str) -> None: """ Mask a system service to prevent it from starting | @@ -245,21 +231,6 @@ def mask_system_service(service_name: str) -> None: raise -def check_file_exists(file_path: Path) -> bool: - """ - Helper function for checking the existence of a file where - elevated permissions are required | - :param file_path: the absolute path of the file to check - :return: True if file exists, otherwise False - """ - try: - command = ["sudo", "find", file_path] - subprocess.check_output(command, stderr=subprocess.DEVNULL) - return True - except subprocess.CalledProcessError: - return False - - # this feels hacky and not quite right, but for now it works # see: https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib def get_ipv4_addr() -> str: @@ -327,116 +298,6 @@ def download_progress(block_num, block_size, total_size) -> None: sys.stdout.flush() -def unzip(file: str, target_dir: str) -> None: - """ - Helper function to unzip a zip-archive into a target directory | - :param file: the zip-file to unzip - :param target_dir: the target directory to extract the files into - :return: None - """ - with ZipFile(file, "r") as _zip: - _zip.extractall(target_dir) - - -def create_upstream_nginx_cfg() -> None: - """ - Creates an upstream.conf in /etc/nginx/conf.d - :return: None - """ - source = os.path.join(MODULE_PATH, "res", "upstreams.conf") - target = os.path.join(NGINX_CONFD, "upstreams.conf") - try: - command = ["sudo", "cp", source, target] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: - log = f"Unable to create upstreams.conf: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def create_common_vars_nginx_cfg() -> None: - """ - Creates a common_vars.conf in /etc/nginx/conf.d - :return: None - """ - source = os.path.join(MODULE_PATH, "res", "common_vars.conf") - target = os.path.join(NGINX_CONFD, "common_vars.conf") - try: - command = ["sudo", "cp", source, target] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: - log = f"Unable to create upstreams.conf: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def create_nginx_cfg(name: str, port: int, root_dir: str) -> None: - """ - Creates an NGINX config from a template file and replaces all placeholders - :param name: name of the config to create - :param port: listen port - :param root_dir: directory of the static files - :return: None - """ - tmp = f"{Path.home()}/{name}.tmp" - shutil.copy(os.path.join(MODULE_PATH, "res", "nginx_cfg"), tmp) - with open(tmp, "r+") as f: - content = f.read() - content = content.replace("%NAME%", name) - content = content.replace("%PORT%", str(port)) - content = content.replace("%ROOT_DIR%", root_dir) - f.seek(0) - f.write(content) - f.truncate() - - target = os.path.join(NGINX_SITES_AVAILABLE, name) - try: - command = ["sudo", "mv", tmp, target] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: - log = f"Unable to create '{target}': {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def delete_default_nginx_cfg() -> None: - """ - Deletes a default NGINX config - :return: None - """ - default_cfg = Path("/etc/nginx/sites-enabled/default") - if not check_file_exists(default_cfg): - return - - try: - command = ["sudo", "rm", default_cfg] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: - log = f"Unable to delete '{default_cfg}': {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def enable_nginx_cfg(name: str) -> None: - """ - Helper method to enable an NGINX config | - :param name: name of the config to enable - :return: None - """ - source = os.path.join(NGINX_SITES_AVAILABLE, name) - target = os.path.join(NGINX_SITES_ENABLED, name) - if check_file_exists(Path(target)): - return - - try: - command = ["sudo", "ln", "-s", source, target] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: - log = f"Unable to create symlink: {e.stderr.decode()}" - Logger.print_error(log) - raise - - def set_nginx_permissions() -> None: """ Check if permissions of the users home directory From 22e8e314dbda2ba98370da33e0fc0a93ac19c90d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 22 Dec 2023 21:41:15 +0100 Subject: [PATCH 057/296] fix(Mainsail): implement missing mainsail cfg symlinking Signed-off-by: Dominik Willner --- kiauh/modules/mainsail/mainsail_setup.py | 39 ++++++++++++++++++------ kiauh/utils/filesystem_utils.py | 19 +++++++++++- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index 0b6a45d..0970251 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -10,6 +10,7 @@ # ======================================================================= # import os.path +import subprocess from pathlib import Path from typing import List @@ -24,23 +25,30 @@ from kiauh.modules.mainsail import ( MAINSAIL_CONFIG_DIR, MAINSAIL_CONFIG_REPO_URL, MODULE_PATH, - ) +) from kiauh.modules.mainsail.mainsail_dialogs import ( print_moonraker_not_found_dialog, print_mainsail_already_installed_dialog, print_install_mainsail_config_dialog, print_mainsail_port_select_dialog, - ) +) from kiauh.modules.mainsail.mainsail_utils import ( restore_config_json, enable_mainsail_remotemode, backup_config_json, symlink_webui_nginx_log, - ) +) from kiauh.modules.moonraker.moonraker import Moonraker from kiauh.utils.common import check_install_dependencies -from kiauh.utils.filesystem_utils import unzip, create_upstream_nginx_cfg, create_common_vars_nginx_cfg, \ - delete_default_nginx_cfg, create_nginx_cfg, enable_nginx_cfg +from kiauh.utils.filesystem_utils import ( + unzip, + create_upstream_nginx_cfg, + create_common_vars_nginx_cfg, + delete_default_nginx_cfg, + create_nginx_cfg, + enable_nginx_cfg, + create_symlink, +) from kiauh.utils.input_utils import get_confirm, get_number_input from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( @@ -48,7 +56,7 @@ from kiauh.utils.system_utils import ( set_nginx_permissions, get_ipv4_addr, control_systemd_service, - ) +) def run_mainsail_installation() -> None: @@ -110,8 +118,9 @@ def run_mainsail_installation() -> None: "mainsail-updater.conf", ) im_mr.restart_all_instance() - if is_klipper_installed and install_ms_config: - download_mainsail_config() + if install_ms_config and is_klipper_installed: + download_mainsail_cfg() + create_mainsail_cfg_symlink(im_kl.instances) patch_moonraker_conf( im_mr.instances, "mainsail-config", @@ -152,7 +161,7 @@ def download_mainsail() -> None: raise -def download_mainsail_config() -> None: +def download_mainsail_cfg() -> None: try: Logger.print_status("Downloading mainsail-config ...") rm = RepoManager(MAINSAIL_CONFIG_REPO_URL, target_dir=MAINSAIL_CONFIG_DIR) @@ -162,6 +171,18 @@ def download_mainsail_config() -> None: raise +def create_mainsail_cfg_symlink(klipper_instances: List[Klipper]) -> None: + Logger.print_status("Create symlink of mainsail.cfg ...") + source = Path(MAINSAIL_CONFIG_DIR, "mainsail.cfg") + for instance in klipper_instances: + target = instance.cfg_dir + Logger.print_status(f"Linking {source} to {target}") + try: + create_symlink(source, target) + except subprocess.CalledProcessError: + Logger.print_error("Creating symlink failed!") + + def create_mainsail_nginx_cfg(port: int) -> None: try: Logger.print_status("Creating NGINX config for Mainsail ...") diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 727cc8f..ad4e868 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -14,7 +14,12 @@ import subprocess from pathlib import Path from zipfile import ZipFile -from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED, MODULE_PATH, NGINX_CONFD +from kiauh.utils import ( + NGINX_SITES_AVAILABLE, + NGINX_SITES_ENABLED, + MODULE_PATH, + NGINX_CONFD, +) from kiauh.utils.logger import Logger @@ -48,6 +53,17 @@ def create_directory(_dir: Path) -> None: raise +def create_symlink(source: Path, target: Path, sudo=False) -> None: + try: + cmd = ["ln", "-sf", source, target] + if sudo: + cmd.insert(0, "sudo") + subprocess.run(cmd, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + Logger.print_error(f"Failed to create symlink: {e}") + raise + + def remove_file(file_path: Path, sudo=False) -> None: try: command = f"{'sudo ' if sudo else ''}rm -f {file_path}" @@ -57,6 +73,7 @@ def remove_file(file_path: Path, sudo=False) -> None: Logger.print_error(log) raise + def unzip(file: str, target_dir: str) -> None: """ Helper function to unzip a zip-archive into a target directory | From 8ff0b9d81d20baa1959bd88be7392cf66efe94a0 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 22 Dec 2023 22:39:43 +0100 Subject: [PATCH 058/296] refactor(Mainsail): refactor methods for removing and checking files Signed-off-by: Dominik Willner --- kiauh/modules/mainsail/mainsail_remove.py | 24 ++++++++++---- kiauh/modules/mainsail/mainsail_setup.py | 8 ++--- kiauh/modules/moonraker/moonraker_setup.py | 6 ++-- kiauh/utils/filesystem_utils.py | 38 +++++++++++++--------- kiauh/utils/system_utils.py | 8 ----- 5 files changed, 47 insertions(+), 37 deletions(-) diff --git a/kiauh/modules/mainsail/mainsail_remove.py b/kiauh/modules/mainsail/mainsail_remove.py index 525ad3f..d09fdb6 100644 --- a/kiauh/modules/mainsail/mainsail_remove.py +++ b/kiauh/modules/mainsail/mainsail_remove.py @@ -41,7 +41,8 @@ def run_mainsail_removal( if remove_mr_updater_section: remove_updater_section("update_manager mainsail") if remove_ms_config: - remove_ms_config_dir() + remove_mainsail_cfg_dir() + remove_mainsail_cfg_symlink() if remove_mr_updater_section: remove_updater_section("update_manager mainsail-config") if remove_msc_printer_cfg_include: @@ -63,8 +64,8 @@ def remove_mainsail_dir() -> None: def remove_nginx_config() -> None: Logger.print_status("Removing Mainsails NGINX config ...") try: - remove_file(Path(NGINX_SITES_AVAILABLE).joinpath("mainsail"), True) - remove_file(Path(NGINX_SITES_ENABLED).joinpath("mainsail"), True) + remove_file(Path(NGINX_SITES_AVAILABLE, "mainsail"), True) + remove_file(Path(NGINX_SITES_ENABLED, "mainsail"), True) except subprocess.CalledProcessError as e: log = f"Unable to remove Mainsail NGINX config:\n{e.stderr.decode()}" @@ -82,8 +83,8 @@ def remove_nginx_logs() -> None: return for instance in im.instances: - remove_file(Path(instance.log_dir).joinpath("mainsail-access.log")) - remove_file(Path(instance.log_dir).joinpath("mainsail-error.log")) + remove_file(Path(instance.log_dir, "mainsail-access.log")) + remove_file(Path(instance.log_dir, "mainsail-error.log")) except (OSError, subprocess.CalledProcessError) as e: Logger.print_error(f"Unable to NGINX logs:\n{e}") @@ -114,7 +115,7 @@ def remove_updater_section(name: str) -> None: cm.write_config() -def remove_ms_config_dir() -> None: +def remove_mainsail_cfg_dir() -> None: Logger.print_status("Removing mainsail-config ...") if not Path(MAINSAIL_CONFIG_DIR).exists(): Logger.print_info(f"'{MAINSAIL_CONFIG_DIR}' does not exist. Skipping ...") @@ -126,6 +127,17 @@ def remove_ms_config_dir() -> None: Logger.print_error(f"Unable to delete '{MAINSAIL_CONFIG_DIR}':\n{e}") +def remove_mainsail_cfg_symlink() -> None: + Logger.print_status("Removing mainsail.cfg symlinks ...") + im = InstanceManager(Moonraker) + for instance in im.instances: + Logger.print_status(f"Removing symlink from '{instance.cfg_dir}' ...") + try: + remove_file(Path(instance.cfg_dir, "mainsail.cfg")) + except subprocess.CalledProcessError: + Logger.print_error("Failed to remove symlink!") + + def remove_printer_cfg_include() -> None: Logger.print_status("Remove mainsail-config include from printer.cfg ...") im = InstanceManager(Klipper) diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index 0970251..5558137 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -42,8 +42,8 @@ from kiauh.modules.moonraker.moonraker import Moonraker from kiauh.utils.common import check_install_dependencies from kiauh.utils.filesystem_utils import ( unzip, - create_upstream_nginx_cfg, - create_common_vars_nginx_cfg, + copy_upstream_nginx_cfg, + copy_common_vars_nginx_cfg, delete_default_nginx_cfg, create_nginx_cfg, enable_nginx_cfg, @@ -130,8 +130,8 @@ def run_mainsail_installation() -> None: patch_printer_config(im_kl.instances) im_kl.restart_all_instance() - create_upstream_nginx_cfg() - create_common_vars_nginx_cfg() + copy_upstream_nginx_cfg() + copy_common_vars_nginx_cfg() create_mainsail_nginx_cfg(mainsail_port) if is_klipper_installed: symlink_webui_nginx_log(im_kl.instances) diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index 4f5a2a3..cde127e 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -181,9 +181,9 @@ def install_moonraker_packages(moonraker_dir: Path) -> None: def install_moonraker_polkit() -> None: Logger.print_status("Installing Moonraker policykit rules ...") - legacy_file_exists = check_file_exist(Path(POLKIT_LEGACY_FILE)) - polkit_file_exists = check_file_exist(Path(POLKIT_FILE)) - usr_file_exists = check_file_exist(Path(POLKIT_USR_FILE)) + legacy_file_exists = check_file_exist(Path(POLKIT_LEGACY_FILE), True) + polkit_file_exists = check_file_exist(Path(POLKIT_FILE), True) + usr_file_exists = check_file_exist(Path(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.") diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index ad4e868..721290c 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -23,19 +23,25 @@ from kiauh.utils import ( from kiauh.utils.logger import Logger -def check_file_exist(file_path: Path) -> bool: +def check_file_exist(file_path: Path, sudo=False) -> bool: """ - Helper function for checking the existence of a file where - elevated permissions are required | + Helper function for checking the existence of a file | :param file_path: the absolute path of the file to check - :return: True if file exists, otherwise False + :param sudo: use sudo if required + :return: True, if file exists, otherwise False """ - try: - command = ["sudo", "find", file_path] - subprocess.check_output(command, stderr=subprocess.DEVNULL) - return True - except subprocess.CalledProcessError: - return False + if sudo: + try: + command = ["sudo", "find", file_path] + subprocess.check_output(command, stderr=subprocess.DEVNULL) + return True + except subprocess.CalledProcessError: + return False + else: + if Path(file_path).exists(): + return True + else: + return False def create_directory(_dir: Path) -> None: @@ -66,8 +72,8 @@ def create_symlink(source: Path, target: Path, sudo=False) -> None: def remove_file(file_path: Path, sudo=False) -> None: try: - command = f"{'sudo ' if sudo else ''}rm -f {file_path}" - subprocess.run(command, stderr=subprocess.PIPE, check=True, shell=True) + cmd = f"{'sudo ' if sudo else ''}rm -f {file_path}" + subprocess.run(cmd, stderr=subprocess.PIPE, check=True, shell=True) except subprocess.CalledProcessError as e: log = f"Cannot remove file {file_path}: {e.stderr.decode()}" Logger.print_error(log) @@ -85,7 +91,7 @@ def unzip(file: str, target_dir: str) -> None: _zip.extractall(target_dir) -def create_upstream_nginx_cfg() -> None: +def copy_upstream_nginx_cfg() -> None: """ Creates an upstream.conf in /etc/nginx/conf.d :return: None @@ -101,7 +107,7 @@ def create_upstream_nginx_cfg() -> None: raise -def create_common_vars_nginx_cfg() -> None: +def copy_common_vars_nginx_cfg() -> None: """ Creates a common_vars.conf in /etc/nginx/conf.d :return: None @@ -152,7 +158,7 @@ def delete_default_nginx_cfg() -> None: :return: None """ default_cfg = Path("/etc/nginx/sites-enabled/default") - if not check_file_exist(default_cfg): + if not check_file_exist(default_cfg, True): return try: @@ -172,7 +178,7 @@ def enable_nginx_cfg(name: str) -> None: """ source = os.path.join(NGINX_SITES_AVAILABLE, name) target = os.path.join(NGINX_SITES_ENABLED, name) - if check_file_exist(Path(target)): + if check_file_exist(Path(target), True): return try: diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 4925707..0b07655 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -19,15 +19,7 @@ import urllib.error import urllib.request from pathlib import Path from typing import List, Literal -from zipfile import ZipFile -from kiauh.utils import ( - NGINX_CONFD, - MODULE_PATH, - NGINX_SITES_AVAILABLE, - NGINX_SITES_ENABLED, -) -from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger From edd5f5c6fdb2a4e388e6415f6b7b11b42a8e3c11 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 22 Dec 2023 22:43:29 +0100 Subject: [PATCH 059/296] refactor(KIAUH): refactor RemoveMenu Signed-off-by: Dominik Willner --- kiauh/core/menus/remove_menu.py | 44 ++++++++++++++------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 4082c8f..4883609 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -29,18 +29,16 @@ class RemoveMenu(BaseMenu): 1: self.remove_klipper, 2: self.remove_moonraker, 3: MainsailRemoveMenu, - 4: self.remove_mainsail_config, 5: self.remove_fluidd, - 6: self.remove_fluidd_config, - 7: self.remove_klipperscreen, - 8: self.remove_crowsnest, - 9: self.remove_mjpgstreamer, - 10: self.remove_pretty_gcode, - 11: self.remove_telegram_bot, - 12: self.remove_obico, - 13: self.remove_octoeverywhere, - 14: self.remove_mobileraker, - 15: self.remove_nginx, + 6: self.remove_klipperscreen, + 7: self.remove_crowsnest, + 8: self.remove_mjpgstreamer, + 9: self.remove_pretty_gcode, + 10: self.remove_telegram_bot, + 11: self.remove_obico, + 12: self.remove_octoeverywhere, + 13: self.remove_mobileraker, + 14: self.remove_nginx, }, footer_type=BACK_FOOTER, ) @@ -57,17 +55,17 @@ class RemoveMenu(BaseMenu): | INFO: Configurations and/or any backups will be kept! | |-------------------------------------------------------| | Firmware & API: | Webcam Streamer: | - | 1) [Klipper] | 8) [Crowsnest] | - | 2) [Moonraker] | 9) [MJPG-Streamer] | + | 1) [Klipper] | 6) [Crowsnest] | + | 2) [Moonraker] | 7) [MJPG-Streamer] | | | | | Klipper Webinterface: | Other: | - | 3) [Mainsail] | 10) [PrettyGCode] | - | 4) [Mainsail-Config] | 11) [Telegram Bot] | - | 5) [Fluidd] | 12) [Obico for Klipper] | - | 6) [Fluidd-Config] | 13) [OctoEverywhere] | - | | 14) [Mobileraker] | - | Touchscreen GUI: | 15) [NGINX] | - | 7) [KlipperScreen] | | + | 3) [Mainsail] | 8) [PrettyGCode] | + | 4) [Fluidd] | 9) [Telegram Bot] | + | | 10) [Obico for Klipper] | + | Touchscreen GUI: | 11) [OctoEverywhere] | + | 5) [KlipperScreen] | 12) [Mobileraker] | + | | 13) [NGINX] | + | | | """ )[1:] print(menu, end="") @@ -78,12 +76,6 @@ class RemoveMenu(BaseMenu): def remove_moonraker(self): moonraker_setup.run_moonraker_setup(install=False) - def remove_mainsail(self): - mainsail_setup.run_mainsail_removal() - - def remove_mainsail_config(self): - print("remove_mainsail_config") - def remove_fluidd(self): print("remove_fluidd") From 3d5e83d5abf982f9628f6e258af238e220785854 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 22 Dec 2023 22:54:29 +0100 Subject: [PATCH 060/296] refactor(Mainsail): remove specific methods and replace by generic ones Signed-off-by: Dominik Willner --- kiauh/modules/mainsail/mainsail_setup.py | 12 +++++--- kiauh/utils/filesystem_utils.py | 39 ------------------------ 2 files changed, 7 insertions(+), 44 deletions(-) diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index 5558137..b8e6615 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -39,15 +39,15 @@ from kiauh.modules.mainsail.mainsail_utils import ( symlink_webui_nginx_log, ) from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from kiauh.utils.common import check_install_dependencies from kiauh.utils.filesystem_utils import ( unzip, copy_upstream_nginx_cfg, copy_common_vars_nginx_cfg, - delete_default_nginx_cfg, create_nginx_cfg, - enable_nginx_cfg, create_symlink, + remove_file, ) from kiauh.utils.input_utils import get_confirm, get_number_input from kiauh.utils.logger import Logger @@ -184,12 +184,14 @@ def create_mainsail_cfg_symlink(klipper_instances: List[Klipper]) -> None: def create_mainsail_nginx_cfg(port: int) -> None: + root_dir = MAINSAIL_DIR + source = Path(NGINX_SITES_AVAILABLE, "mainsail") + target = Path(NGINX_SITES_ENABLED, "mainsail") try: Logger.print_status("Creating NGINX config for Mainsail ...") - root_dir = MAINSAIL_DIR - delete_default_nginx_cfg() + remove_file(Path("/etc/nginx/sites-enabled/default"), True) create_nginx_cfg("mainsail", port, root_dir) - enable_nginx_cfg("mainsail") + create_symlink(source, target, True) set_nginx_permissions() Logger.print_ok("NGINX config for Mainsail successfully created.") except Exception: diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 721290c..b623be2 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -16,7 +16,6 @@ from zipfile import ZipFile from kiauh.utils import ( NGINX_SITES_AVAILABLE, - NGINX_SITES_ENABLED, MODULE_PATH, NGINX_CONFD, ) @@ -150,41 +149,3 @@ def create_nginx_cfg(name: str, port: int, root_dir: str) -> None: log = f"Unable to create '{target}': {e.stderr.decode()}" Logger.print_error(log) raise - - -def delete_default_nginx_cfg() -> None: - """ - Deletes a default NGINX config - :return: None - """ - default_cfg = Path("/etc/nginx/sites-enabled/default") - if not check_file_exist(default_cfg, True): - return - - try: - command = ["sudo", "rm", default_cfg] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: - log = f"Unable to delete '{default_cfg}': {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def enable_nginx_cfg(name: str) -> None: - """ - Helper method to enable an NGINX config | - :param name: name of the config to enable - :return: None - """ - source = os.path.join(NGINX_SITES_AVAILABLE, name) - target = os.path.join(NGINX_SITES_ENABLED, name) - if check_file_exist(Path(target), True): - return - - try: - command = ["sudo", "ln", "-s", source, target] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: - log = f"Unable to create symlink: {e.stderr.decode()}" - Logger.print_error(log) - raise From 14132fc34b6d8ae977e944e380ab3d773e7b4a01 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 23 Dec 2023 17:21:50 +0100 Subject: [PATCH 061/296] refactor(ConfigManager): automatically read config upon ConfigManager instance init Signed-off-by: Dominik Willner --- kiauh/core/config_manager/config_manager.py | 6 +++++- kiauh/modules/klipper/klipper_setup.py | 4 ---- kiauh/modules/klipper/klipper_utils.py | 1 - kiauh/modules/mainsail/mainsail_remove.py | 2 -- kiauh/modules/mainsail/mainsail_setup.py | 3 --- kiauh/modules/moonraker/moonraker.py | 1 - kiauh/modules/moonraker/moonraker_setup.py | 4 ---- kiauh/modules/moonraker/moonraker_utils.py | 7 +++---- 8 files changed, 8 insertions(+), 20 deletions(-) diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index 4470575..24858c8 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -10,6 +10,7 @@ # ======================================================================= # import configparser +from pathlib import Path from typing import Union from kiauh.utils.logger import Logger @@ -21,6 +22,9 @@ class ConfigManager: self.config_file = cfg_file self.config = CustomConfigParser() + if Path(cfg_file).is_file(): + self.read_config() + def read_config(self) -> None: if not self.config_file: Logger.print_error("Unable to read config file. File not found.") @@ -32,7 +36,7 @@ class ConfigManager: with open(self.config_file, "w") as cfg: self.config.write(cfg) - def get_value(self, section: str, key: str, silent=False) -> Union[str, bool, None]: + def get_value(self, section: str, key: str, silent=True) -> Union[str, bool, None]: if not self.config.has_section(section): if not silent: log = f"Section not defined. Unable to read section: [{section}]." diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 78bc282..01c3a94 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -145,8 +145,6 @@ def install_klipper( def setup_klipper_prerequesites() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) - cm.read_config() - repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL) branch = str(cm.get_value("klipper", "branch") or "master") @@ -265,8 +263,6 @@ def update_klipper() -> None: return cm = ConfigManager(cfg_file=KIAUH_CFG) - cm.read_config() - if cm.get_value("kiauh", "backup_before_update"): backup_manager = BackupManager(source=KLIPPER_DIR, backup_name="klipper") backup_manager.backup() diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index e3439af..26d20a6 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -209,7 +209,6 @@ def create_example_printer_cfg(instance: Klipper) -> None: return cm = ConfigManager(target) - cm.read_config() cm.set_value("virtual_sdcard", "path", instance.gcodes_dir) cm.write_config() Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'") diff --git a/kiauh/modules/mainsail/mainsail_remove.py b/kiauh/modules/mainsail/mainsail_remove.py index d09fdb6..6e80fc1 100644 --- a/kiauh/modules/mainsail/mainsail_remove.py +++ b/kiauh/modules/mainsail/mainsail_remove.py @@ -106,7 +106,6 @@ def remove_updater_section(name: str) -> None: continue cm = ConfigManager(instance.cfg_file) - cm.read_config() if not cm.config.has_section(name): Logger.print_info("Section not present. Skipped ...") continue @@ -153,7 +152,6 @@ def remove_printer_cfg_include() -> None: continue cm = ConfigManager(instance.cfg_file) - cm.read_config() if not cm.config.has_section("include mainsail.cfg"): Logger.print_info("Section not present. Skipped ...") continue diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index b8e6615..8de5f50 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -91,7 +91,6 @@ def run_mainsail_installation() -> None: install_ms_config = get_confirm(question, allow_go_back=False) cm = ConfigManager(cfg_file=KIAUH_CFG) - cm.read_config() default_port = cm.get_value("mainsail", "default_port") mainsail_port = default_port if default_port else 80 if not default_port: @@ -214,7 +213,6 @@ def patch_moonraker_conf( return cm = ConfigManager(cfg_file) - cm.read_config() if cm.config.has_section(section_name): Logger.print_info("Section already exist. Skipped ...") return @@ -238,7 +236,6 @@ def patch_printer_config(klipper_instances: List[Klipper]) -> None: return cm = ConfigManager(cfg_file) - cm.read_config() if cm.config.has_section("include mainsail.cfg"): Logger.print_info("Section already exist. Skipped ...") return diff --git a/kiauh/modules/moonraker/moonraker.py b/kiauh/modules/moonraker/moonraker.py index df648cf..d3dbfc6 100644 --- a/kiauh/modules/moonraker/moonraker.py +++ b/kiauh/modules/moonraker/moonraker.py @@ -156,7 +156,6 @@ class Moonraker(BaseInstance): return None cm = ConfigManager(cfg_file=self.cfg_file) - cm.read_config() port = cm.get_value("server", "port") return int(port) if port is not None else port diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index cde127e..cfddacd 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -150,8 +150,6 @@ def install_moonraker( def setup_moonraker_prerequesites() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) - cm.read_config() - repo = str( cm.get_value("moonraker", "repository_url") or DEFAULT_MOONRAKER_REPO_URL ) @@ -291,8 +289,6 @@ def update_moonraker() -> None: return cm = ConfigManager(cfg_file=KIAUH_CFG) - cm.read_config() - if cm.get_value("kiauh", "backup_before_update"): backup_manager = BackupManager(source=MOONRAKER_DIR, backup_name="moonraker") backup_manager.backup() diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/modules/moonraker/moonraker_utils.py index 2c08f5e..ce3d99e 100644 --- a/kiauh/modules/moonraker/moonraker_utils.py +++ b/kiauh/modules/moonraker/moonraker_utils.py @@ -41,9 +41,6 @@ def create_example_moonraker_conf( Logger.print_error(f"Unable to create example moonraker.conf:\n{e}") return - cm = ConfigManager(target) - cm.read_config() - ports = [ ports_map.get(instance) for instance in ports_map @@ -61,9 +58,11 @@ def create_example_moonraker_conf( ports_map[instance.suffix] = port - uds = f"{instance.comms_dir}/klippy.sock" ip = get_ipv4_addr().split(".")[:2] ip.extend(["0", "0/16"]) + uds = f"{instance.comms_dir}/klippy.sock" + + cm = ConfigManager(target) trusted_clients = f"\n{'.'.join(ip)}" trusted_clients += cm.get_value("authorization", "trusted_clients") From b9479db76609d62071d7bf5109d45f0bbfbed202 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 23 Dec 2023 19:42:34 +0100 Subject: [PATCH 062/296] feat(KIAUH): show installation status of Klipper and Moonraker in MainMenu Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 67 +++++++++++++++++++++++++++-------- kiauh/utils/common.py | 55 ++++++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 17 deletions(-) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index bc77121..908d77d 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -18,7 +18,12 @@ from kiauh.core.menus.install_menu import InstallMenu from kiauh.core.menus.remove_menu import RemoveMenu from kiauh.core.menus.settings_menu import SettingsMenu from kiauh.core.menus.update_menu import UpdateMenu -from kiauh.utils.constants import COLOR_MAGENTA, COLOR_CYAN, RESET_FORMAT +from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR +from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.utils.common import get_repo_name, get_install_status_common +from kiauh.utils.constants import COLOR_MAGENTA, COLOR_CYAN, RESET_FORMAT, COLOR_RED class MainMenu(BaseMenu): @@ -36,8 +41,40 @@ class MainMenu(BaseMenu): }, footer_type=QUIT_FOOTER, ) + self.kl_status = None + self.kl_repo = None + self.mr_status = None + self.mr_repo = None + self.ms_status = None + self.fl_status = None + self.ks_status = None + self.mb_status = None + self.cn_status = None + self.tg_status = None + self.ob_status = None + self.oe_status = None + self.init_status() + + def init_status(self) -> None: + status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn", "tg", "ob", "oe"] + for var in status_vars: + setattr(self, f"{var}_status", f"{COLOR_RED}Not installed!{RESET_FORMAT}") + + def fetch_status(self) -> None: + # klipper + self.kl_status = get_install_status_common( + Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR + ) + self.kl_repo = get_repo_name(KLIPPER_DIR) + # moonraker + self.mr_status = get_install_status_common( + Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR + ) + self.mr_repo = get_repo_name(MOONRAKER_DIR) def print_menu(self): + self.fetch_status() + header = " [ Main Menu ] " footer1 = "KIAUH v6.0.0" footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}" @@ -48,21 +85,21 @@ class MainMenu(BaseMenu): /=======================================================\\ | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| - | 0) [Log-Upload] | Klipper: | - | | Repo: | - | 1) [Install] | | - | 2) [Update] | Moonraker: | - | 3) [Remove] | Repo: | - | 4) [Advanced] | | - | 5) [Backup] | Mainsail: | - | | Fluidd: | - | 6) [Settings] | KlipperScreen: | - | | Mobileraker: | + | 0) [Log-Upload] | Klipper: {self.kl_status:<32} | + | | Repo: {self.kl_repo:<32} | + | 1) [Install] |------------------------------------| + | 2) [Update] | Moonraker: {self.mr_status:<32} | + | 3) [Remove] | Repo: {self.mr_repo:<32} | + | 4) [Advanced] |------------------------------------| + | 5) [Backup] | Mainsail: {self.ms_status:<26} | + | | Fluidd: {self.fl_status:<26} | + | 6) [Settings] | KlipperScreen: {self.ks_status:<26} | + | | Mobileraker: {self.mb_status:<26} | | | | - | | Crowsnest: | - | | Telegram Bot: | - | | Obico: | - | | OctoEverywhere: | + | | Crowsnest: {self.cn_status:<26} | + | | Telegram Bot: {self.tg_status:<26} | + | | Obico: {self.ob_status:<26} | + | | OctoEverywhere: {self.oe_status:<26} | |-------------------------------------------------------| | {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} | """ diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 949d6a4..04aa746 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import subprocess # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # @@ -10,9 +11,18 @@ # ======================================================================= # from datetime import datetime -from typing import Dict, Literal, List +from pathlib import Path +from typing import Dict, Literal, List, Type -from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT +from kiauh.core.instance_manager.base_instance import BaseInstance +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.utils.constants import ( + COLOR_CYAN, + RESET_FORMAT, + COLOR_YELLOW, + COLOR_GREEN, + COLOR_RED, +) from kiauh.utils.logger import Logger from kiauh.utils.system_utils import check_package_install, install_system_packages @@ -43,3 +53,44 @@ def check_install_dependencies(deps: List[str]) -> None: for _ in requirements: print(f"{COLOR_CYAN}● {_}{RESET_FORMAT}") install_system_packages(requirements) + + +def get_repo_name(repo_dir: str) -> str: + """ + Helper method to extract the organisation and name of a repository | + :param repo_dir: repository to extract the values from + :return: String in form of "/" + """ + try: + cmd = ["git", "-C", repo_dir, "config", "--get", "remote.origin.url"] + result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) + result = "/".join(result.decode().strip().split("/")[-2:]) + return f"{COLOR_CYAN}{result}{RESET_FORMAT}" + except subprocess.CalledProcessError: + return f"{COLOR_YELLOW}Unknown{RESET_FORMAT}" + + +def get_install_status_common( + instance_type: Type[BaseInstance], repo_dir: str, env_dir: str +) -> str: + """ + Helper method to get the installation status of software components, + which only consist of 3 major parts and if those parts exist, the + component can be considered as "installed". Typically, Klipper or + Moonraker match that criteria. + :param instance_type: The component type + :param repo_dir: the repository directory + :param env_dir: the python environment directory + :return: formatted string, containing the status + """ + im = InstanceManager(instance_type) + dir_exist = Path(repo_dir).exists() + env_dir_exist = Path(env_dir).exists() + instances_exist = len(im.instances) > 0 + status = [dir_exist, env_dir_exist, instances_exist] + if all(status): + return f"{COLOR_GREEN}Installed: {len(im.instances)}{RESET_FORMAT}" + elif not any(status): + return f"{COLOR_RED}Not installed!{RESET_FORMAT}" + else: + return f"{COLOR_YELLOW}Incomplete!{RESET_FORMAT}" From 2f0feb317e419874f0c50e764ee7e5586f8def6a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Dec 2023 12:58:41 +0100 Subject: [PATCH 063/296] refactor(BackupManager): rework backup structure and implement single file backup method Signed-off-by: Dominik Willner --- kiauh/core/backup_manager/backup_manager.py | 65 ++++++++++----------- kiauh/modules/klipper/klipper_setup.py | 8 +-- kiauh/modules/moonraker/moonraker_setup.py | 8 +-- kiauh/utils/common.py | 6 +- 4 files changed, 40 insertions(+), 47 deletions(-) diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index db82162..1ba0beb 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -11,6 +11,7 @@ import shutil from pathlib import Path +from typing import List from kiauh import KIAUH_BACKUP_DIR from kiauh.utils.common import get_current_date @@ -19,51 +20,47 @@ from kiauh.utils.logger import Logger # noinspection PyMethodMayBeStatic class BackupManager: - def __init__( - self, backup_name: str, source: Path, backup_dir: Path = KIAUH_BACKUP_DIR - ): - self._backup_name = backup_name - self._source = source - self._backup_dir = backup_dir + def __init__(self, backup_root_dir: Path = KIAUH_BACKUP_DIR): + self._backup_root_dir = backup_root_dir @property - def backup_name(self) -> str: - return self._backup_name + def backup_root_dir(self) -> Path: + return self._backup_root_dir - @backup_name.setter - def backup_name(self, value: str): - self._backup_name = value + @backup_root_dir.setter + def backup_root_dir(self, value: Path): + self._backup_root_dir = value - @property - def source(self) -> Path: - return self._source + def backup_file(self, files: List[Path] = None, target: Path = None): + if not files: + raise ValueError("Parameter 'files' cannot be None or an empty List!") - @source.setter - def source(self, value: Path): - self._source = value + target = self.backup_root_dir if target is None else target + for file in files: + if Path(file).is_file(): + date = get_current_date().get("date") + time = get_current_date().get("time") + filename = f"{file.stem}-{date}-{time}{file.suffix}" + try: + Path(target).mkdir(exist_ok=True) + shutil.copyfile(file, Path(target, filename)) + except OSError as e: + Logger.print_error(f"Unable to backup '{file}':\n{e}") + continue - @property - def backup_dir(self) -> Path: - return self._backup_dir - - @backup_dir.setter - def backup_dir(self, value: Path): - self._backup_dir = value - - def backup(self) -> None: - if self._source is None or not Path(self._source).exists(): + def backup_directory(self, name: str, source: Path, target: Path = None) -> None: + if source is None or not Path(source).exists(): raise OSError + target = self.backup_root_dir if target is None else target try: - log = f"Creating backup of {self.backup_name} in {self.backup_dir} ..." + log = f"Creating backup of {name} in {target} ..." Logger.print_status(log) - date = get_current_date() - dest = Path( - f"{self.backup_dir}/{self.backup_name}/{date.get('date')}-{date.get('time')}" - ) - shutil.copytree(src=self.source, dst=dest) + date = get_current_date().get("date") + time = get_current_date().get("time") + shutil.copytree(source, Path(target, f"{name}-{date}-{time}")) except OSError as e: - Logger.print_error(f"Unable to backup source directory. Not exist.\n{e}") + Logger.print_error(f"Unable to backup directory '{source}':\n{e}") return Logger.print_ok("Backup successfull!") diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 01c3a94..0e33e51 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -264,11 +264,9 @@ def update_klipper() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) if cm.get_value("kiauh", "backup_before_update"): - backup_manager = BackupManager(source=KLIPPER_DIR, backup_name="klipper") - backup_manager.backup() - backup_manager.backup_name = "klippy-env" - backup_manager.source = KLIPPER_ENV_DIR - backup_manager.backup() + bm = BackupManager() + bm.backup_directory("klipper", KLIPPER_DIR) + bm.backup_directory("klippy-env", KLIPPER_ENV_DIR) instance_manager = InstanceManager(Klipper) instance_manager.stop_all_instance() diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index cfddacd..98b000e 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -290,11 +290,9 @@ def update_moonraker() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) if cm.get_value("kiauh", "backup_before_update"): - backup_manager = BackupManager(source=MOONRAKER_DIR, backup_name="moonraker") - backup_manager.backup() - backup_manager.backup_name = "moonraker-env" - backup_manager.source = MOONRAKER_ENV_DIR - backup_manager.backup() + bm = BackupManager() + bm.backup_directory("moonraker", MOONRAKER_DIR) + bm.backup_directory("moonraker-env", MOONRAKER_ENV_DIR) instance_manager = InstanceManager(Moonraker) instance_manager.stop_all_instance() diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 04aa746..144a4dc 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import subprocess # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # @@ -10,6 +9,7 @@ import subprocess # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import subprocess from datetime import datetime from pathlib import Path from typing import Dict, Literal, List, Type @@ -33,8 +33,8 @@ def get_current_date() -> Dict[Literal["date", "time"], str]: :return: Dict holding a date and time key:value pair """ now: datetime = datetime.today() - date: str = now.strftime("%Y-%m-%d") - time: str = now.strftime("%H-%M-%S") + date: str = now.strftime("%Y%m%d") + time: str = now.strftime("%H%M%S") return {"date": date, "time": time} From 7a6590e86a4b5c1fbc4d4d54077ecf41115a1a9b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Dec 2023 13:53:55 +0100 Subject: [PATCH 064/296] refactor(Mainsail): rework config.json backup Signed-off-by: Dominik Willner --- kiauh/core/backup_manager/backup_manager.py | 8 +++++++- kiauh/modules/mainsail/mainsail_setup.py | 2 +- kiauh/modules/mainsail/mainsail_utils.py | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 1ba0beb..608d418 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -31,22 +31,28 @@ class BackupManager: def backup_root_dir(self, value: Path): self._backup_root_dir = value - def backup_file(self, files: List[Path] = None, target: Path = None): + def backup_file( + self, files: List[Path] = None, target: Path = None, custom_filename=None + ): if not files: raise ValueError("Parameter 'files' cannot be None or an empty List!") target = self.backup_root_dir if target is None else target for file in files: + Logger.print_status(f"Creating backup of {file} ...") if Path(file).is_file(): date = get_current_date().get("date") time = get_current_date().get("time") filename = f"{file.stem}-{date}-{time}{file.suffix}" + filename = custom_filename if custom_filename is not None else filename try: Path(target).mkdir(exist_ok=True) shutil.copyfile(file, Path(target, filename)) except OSError as e: Logger.print_error(f"Unable to backup '{file}':\n{e}") continue + else: + Logger.print_info(f"File '{file}' not found ...") def backup_directory(self, name: str, source: Path, target: Path = None) -> None: if source is None or not Path(source).exists(): diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index 8de5f50..d1fbc54 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -78,7 +78,7 @@ def run_mainsail_installation() -> None: print_mainsail_already_installed_dialog() do_reinstall = get_confirm("Re-install Mainsail?", allow_go_back=True) if do_reinstall: - backup_config_json() + backup_config_json(is_temp=True) else: return diff --git a/kiauh/modules/mainsail/mainsail_utils.py b/kiauh/modules/mainsail/mainsail_utils.py index 4cfc9d5..052809e 100644 --- a/kiauh/modules/mainsail/mainsail_utils.py +++ b/kiauh/modules/mainsail/mainsail_utils.py @@ -15,20 +15,20 @@ import shutil from pathlib import Path from typing import List +from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.mainsail import MAINSAIL_CONFIG_JSON from kiauh.utils.logger import Logger -# TODO: give this method an optional target dir to backup to -# alteratively use the BackupManager for handling this task (probably best) -def backup_config_json() -> None: - try: - Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") - target = os.path.join(Path.home(), "config.json.kiauh.bak") - shutil.copy(MAINSAIL_CONFIG_JSON, target) - except OSError: - Logger.print_info(f"Unable to backup config.json. Skipped ...") +def backup_config_json(is_temp=False) -> None: + Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") + bm = BackupManager() + if is_temp: + fn = Path(Path.home(), "config.json.kiauh.bak") + bm.backup_file([MAINSAIL_CONFIG_JSON], custom_filename=fn) + else: + bm.backup_file([MAINSAIL_CONFIG_JSON]) def restore_config_json() -> None: @@ -37,7 +37,7 @@ def restore_config_json() -> None: source = os.path.join(Path.home(), "config.json.kiauh.bak") shutil.copy(source, MAINSAIL_CONFIG_JSON) except OSError: - Logger.print_info(f"Unable to restore config.json. Skipped ...") + Logger.print_info("Unable to restore config.json. Skipped ...") def enable_mainsail_remotemode() -> None: From 8cb075429685a1bcc7e7123d355968f7132d305f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Dec 2023 14:23:57 +0100 Subject: [PATCH 065/296] feat(KIAUH): show Mainsail install status Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 8 +++++++- kiauh/modules/mainsail/mainsail_utils.py | 13 +++++++++++- kiauh/utils/common.py | 26 ++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 908d77d..ea0c317 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -20,9 +20,13 @@ from kiauh.core.menus.settings_menu import SettingsMenu from kiauh.core.menus.update_menu import UpdateMenu from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.mainsail.mainsail_utils import get_mainsail_status from kiauh.modules.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR from kiauh.modules.moonraker.moonraker import Moonraker -from kiauh.utils.common import get_repo_name, get_install_status_common +from kiauh.utils.common import ( + get_repo_name, + get_install_status_common, +) from kiauh.utils.constants import COLOR_MAGENTA, COLOR_CYAN, RESET_FORMAT, COLOR_RED @@ -71,6 +75,8 @@ class MainMenu(BaseMenu): Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR ) self.mr_repo = get_repo_name(MOONRAKER_DIR) + # mainsail + self.ms_status = get_mainsail_status() def print_menu(self): self.fetch_status() diff --git a/kiauh/modules/mainsail/mainsail_utils.py b/kiauh/modules/mainsail/mainsail_utils.py index 052809e..02c0984 100644 --- a/kiauh/modules/mainsail/mainsail_utils.py +++ b/kiauh/modules/mainsail/mainsail_utils.py @@ -17,10 +17,21 @@ from typing import List from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.mainsail import MAINSAIL_CONFIG_JSON +from kiauh.modules.mainsail import MAINSAIL_CONFIG_JSON, MAINSAIL_DIR +from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_CONFD +from kiauh.utils.common import get_install_status_webui from kiauh.utils.logger import Logger +def get_mainsail_status() -> str: + return get_install_status_webui( + MAINSAIL_DIR, + Path(NGINX_SITES_AVAILABLE, "mainsail"), + Path(NGINX_CONFD, "upstreams.conf"), + Path(NGINX_CONFD, "common_vars.conf"), + ) + + def backup_config_json(is_temp=False) -> None: Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") bm = BackupManager() diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 144a4dc..856534d 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -23,6 +23,7 @@ from kiauh.utils.constants import ( COLOR_GREEN, COLOR_RED, ) +from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.logger import Logger from kiauh.utils.system_utils import check_package_install, install_system_packages @@ -94,3 +95,28 @@ def get_install_status_common( return f"{COLOR_RED}Not installed!{RESET_FORMAT}" else: return f"{COLOR_YELLOW}Incomplete!{RESET_FORMAT}" + + +def get_install_status_webui( + install_dir: Path, nginx_cfg: Path, upstreams_cfg: Path, common_cfg: Path +) -> str: + """ + Helper method to get the installation status of webuis + like Mainsail or Fluidd | + :param install_dir: folder of the static webui files + :param nginx_cfg: the webuis NGINX config + :param upstreams_cfg: the required upstreams.conf + :param common_cfg: the required common_vars.conf + :return: formatted string, containing the status + """ + dir_exist = Path(install_dir).exists() + nginx_cfg_exist = check_file_exist(nginx_cfg) + upstreams_cfg_exist = check_file_exist(upstreams_cfg) + common_cfg_exist = check_file_exist(common_cfg) + status = [dir_exist, nginx_cfg_exist, upstreams_cfg_exist, common_cfg_exist] + if all(status): + return f"{COLOR_GREEN}Installed!{RESET_FORMAT}" + elif not any(status): + return f"{COLOR_RED}Not installed!{RESET_FORMAT}" + else: + return f"{COLOR_YELLOW}Incomplete!{RESET_FORMAT}" From da533fdd672b8afc4cec3a963003f3543100bff8 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Dec 2023 14:35:25 +0100 Subject: [PATCH 066/296] refactor(KIAUH): use util functions for Klipper and Moonraker to get their status Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 22 ++++++---------------- kiauh/modules/klipper/klipper_utils.py | 12 ++++++++++-- kiauh/modules/moonraker/moonraker_utils.py | 14 +++++++++++++- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index ea0c317..b62b312 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -18,15 +18,9 @@ from kiauh.core.menus.install_menu import InstallMenu from kiauh.core.menus.remove_menu import RemoveMenu from kiauh.core.menus.settings_menu import SettingsMenu from kiauh.core.menus.update_menu import UpdateMenu -from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR -from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.klipper.klipper_utils import get_klipper_status from kiauh.modules.mainsail.mainsail_utils import get_mainsail_status -from kiauh.modules.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR -from kiauh.modules.moonraker.moonraker import Moonraker -from kiauh.utils.common import ( - get_repo_name, - get_install_status_common, -) +from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status from kiauh.utils.constants import COLOR_MAGENTA, COLOR_CYAN, RESET_FORMAT, COLOR_RED @@ -66,15 +60,11 @@ class MainMenu(BaseMenu): def fetch_status(self) -> None: # klipper - self.kl_status = get_install_status_common( - Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR - ) - self.kl_repo = get_repo_name(KLIPPER_DIR) + self.kl_status = get_klipper_status().get("status") + self.kl_repo = get_klipper_status().get("repo") # moonraker - self.mr_status = get_install_status_common( - Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR - ) - self.mr_repo = get_repo_name(MOONRAKER_DIR) + self.mr_status = get_moonraker_status().get("status") + self.mr_repo = get_moonraker_status().get("repo") # mainsail self.ms_status = get_mainsail_status() diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 26d20a6..6e059ce 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -16,22 +16,30 @@ import shutil import subprocess import textwrap -from typing import List, Union +from typing import List, Union, Literal, Dict from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.modules.klipper import MODULE_PATH +from kiauh.modules.klipper import MODULE_PATH, KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.klipper.klipper_dialogs import ( print_missing_usergroup_dialog, print_select_custom_name_dialog, ) +from kiauh.utils.common import get_install_status_common, get_repo_name from kiauh.utils.constants import CURRENT_USER from kiauh.utils.input_utils import get_confirm, get_string_input from kiauh.utils.logger import Logger from kiauh.utils.system_utils import mask_system_service +def get_klipper_status() -> Dict[Literal["status", "repo"], str]: + return { + "status": get_install_status_common(Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR), + "repo": get_repo_name(KLIPPER_DIR), + } + + def assign_custom_names( instance_count: int, install_count: int, instance_list: List[Klipper] = None ) -> List[str]: diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/modules/moonraker/moonraker_utils.py index ce3d99e..394a84f 100644 --- a/kiauh/modules/moonraker/moonraker_utils.py +++ b/kiauh/modules/moonraker/moonraker_utils.py @@ -11,20 +11,32 @@ import os import shutil -from typing import List, Dict +from typing import Dict, Literal from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.modules.moonraker import ( DEFAULT_MOONRAKER_PORT, MODULE_PATH, + MOONRAKER_DIR, + MOONRAKER_ENV_DIR, ) from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.utils.common import get_install_status_common, get_repo_name from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( get_ipv4_addr, ) +def get_moonraker_status() -> Dict[Literal["status", "repo"], str]: + return { + "status": get_install_status_common( + Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR + ), + "repo": get_repo_name(MOONRAKER_DIR), + } + + def create_example_moonraker_conf( instance: Moonraker, ports_map: Dict[str, int] ) -> None: From 8aeb01aca0a9bdd6ab730b0067ce80f3aeb28be3 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 00:59:00 +0100 Subject: [PATCH 067/296] refactor(KIAUH): use pathlib instead of os where possible. consistent use of pathlib. Signed-off-by: Dominik Willner --- kiauh/__init__.py | 7 +- kiauh/core/backup_manager/backup_manager.py | 4 +- kiauh/core/config_manager/config_manager.py | 4 +- kiauh/core/instance_manager/base_instance.py | 49 +++++++------ .../core/instance_manager/instance_manager.py | 13 ++-- kiauh/core/repo_manager/repo_manager.py | 4 +- kiauh/modules/klipper/__init__.py | 9 ++- kiauh/modules/klipper/klipper.py | 71 ++++++++++--------- kiauh/modules/klipper/klipper_setup.py | 5 +- kiauh/modules/klipper/klipper_utils.py | 9 +-- kiauh/modules/mainsail/__init__.py | 10 ++- kiauh/modules/mainsail/mainsail_remove.py | 5 +- kiauh/modules/mainsail/mainsail_setup.py | 17 +++-- kiauh/modules/mainsail/mainsail_utils.py | 15 ++-- kiauh/modules/moonraker/__init__.py | 19 ++--- kiauh/modules/moonraker/moonraker.py | 62 +++++++++------- kiauh/modules/moonraker/moonraker_setup.py | 17 +++-- kiauh/modules/moonraker/moonraker_utils.py | 9 ++- kiauh/utils/__init__.py | 11 +-- kiauh/utils/common.py | 10 ++- kiauh/utils/constants.py | 3 +- kiauh/utils/filesystem_utils.py | 42 ++++------- kiauh/utils/system_utils.py | 21 +++--- 23 files changed, 207 insertions(+), 209 deletions(-) diff --git a/kiauh/__init__.py b/kiauh/__init__.py index 8fc1d9a..e5fd81f 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -9,9 +9,8 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from os.path import join, dirname, abspath from pathlib import Path -APPLICATION_ROOT = dirname(dirname(abspath(__file__))) -KIAUH_CFG = join(APPLICATION_ROOT, "kiauh.cfg") -KIAUH_BACKUP_DIR = f"{Path.home()}/kiauh-backups" +APPLICATION_ROOT = Path(__file__).resolve().parent.parent +KIAUH_CFG = APPLICATION_ROOT.joinpath("kiauh.cfg") +KIAUH_BACKUP_DIR = Path.home().joinpath("kiauh-backups") diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 608d418..68fa338 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -47,7 +47,7 @@ class BackupManager: filename = custom_filename if custom_filename is not None else filename try: Path(target).mkdir(exist_ok=True) - shutil.copyfile(file, Path(target, filename)) + shutil.copyfile(file, target.joinpath(filename)) except OSError as e: Logger.print_error(f"Unable to backup '{file}':\n{e}") continue @@ -64,7 +64,7 @@ class BackupManager: Logger.print_status(log) date = get_current_date().get("date") time = get_current_date().get("time") - shutil.copytree(source, Path(target, f"{name}-{date}-{time}")) + shutil.copytree(source, target.joinpath(f"{name}-{date}-{time}")) except OSError as e: Logger.print_error(f"Unable to backup directory '{source}':\n{e}") return diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index 24858c8..5e56626 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -18,11 +18,11 @@ from kiauh.utils.logger import Logger # noinspection PyMethodMayBeStatic class ConfigManager: - def __init__(self, cfg_file: str): + def __init__(self, cfg_file: Path): self.config_file = cfg_file self.config = CustomConfigParser() - if Path(cfg_file).is_file(): + if cfg_file.is_file(): self.read_config() def read_config(self) -> None: diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index 0251694..55f6941 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -14,7 +14,6 @@ from pathlib import Path from typing import List, Union, Optional, Type, TypeVar from kiauh.utils.constants import SYSTEMD, CURRENT_USER -from kiauh.utils.filesystem_utils import create_directory B = TypeVar(name="B", bound="BaseInstance", covariant=True) @@ -32,13 +31,13 @@ class BaseInstance(ABC): self._instance_type = instance_type self._suffix = suffix self._user = CURRENT_USER - self._data_dir_name = self.get_data_dir_from_suffix() - self._data_dir = f"{Path.home()}/{self._data_dir_name}_data" - self._cfg_dir = f"{self.data_dir}/config" - self._log_dir = f"{self.data_dir}/logs" - self._comms_dir = f"{self.data_dir}/comms" - self._sysd_dir = f"{self.data_dir}/systemd" - self._gcodes_dir = f"{self.data_dir}/gcodes" + self._data_dir_name = self.get_data_dir_name_from_suffix() + self._data_dir = Path.home().joinpath(f"{self._data_dir_name}_data") + self._cfg_dir = self.data_dir.joinpath("config") + self._log_dir = self.data_dir.joinpath("logs") + self._comms_dir = self.data_dir.joinpath("comms") + self._sysd_dir = self.data_dir.joinpath("systemd") + self._gcodes_dir = self.data_dir.joinpath("gcodes") @property def instance_type(self) -> Type["BaseInstance"]: @@ -73,51 +72,51 @@ class BaseInstance(ABC): self._data_dir_name = value @property - def data_dir(self): + def data_dir(self) -> Path: return self._data_dir @data_dir.setter - def data_dir(self, value: str): + def data_dir(self, value: str) -> None: self._data_dir = value @property - def cfg_dir(self): + def cfg_dir(self) -> Path: return self._cfg_dir @cfg_dir.setter - def cfg_dir(self, value: str): + def cfg_dir(self, value: str) -> None: self._cfg_dir = value @property - def log_dir(self): + def log_dir(self) -> Path: return self._log_dir @log_dir.setter - def log_dir(self, value: str): + def log_dir(self, value: str) -> None: self._log_dir = value @property - def comms_dir(self): + def comms_dir(self) -> Path: return self._comms_dir @comms_dir.setter - def comms_dir(self, value: str): + def comms_dir(self, value: str) -> None: self._comms_dir = value @property - def sysd_dir(self): + def sysd_dir(self) -> Path: return self._sysd_dir @sysd_dir.setter - def sysd_dir(self, value: str): + def sysd_dir(self, value: str) -> None: self._sysd_dir = value @property - def gcodes_dir(self): + def gcodes_dir(self) -> Path: return self._gcodes_dir @gcodes_dir.setter - def gcodes_dir(self, value: str): + def gcodes_dir(self, value: str) -> None: self._gcodes_dir = value @abstractmethod @@ -128,7 +127,7 @@ class BaseInstance(ABC): def delete(self, del_remnants: bool) -> None: raise NotImplementedError("Subclasses must implement the delete method") - def create_folders(self, add_dirs: List[str] = None) -> None: + def create_folders(self, add_dirs: List[Path] = None) -> None: dirs = [ self.data_dir, self.cfg_dir, @@ -141,7 +140,7 @@ class BaseInstance(ABC): dirs.extend(add_dirs) for _dir in dirs: - create_directory(Path(_dir)) + _dir.mkdir(exist_ok=True) def get_service_file_name(self, extension: bool = False) -> str: name = f"{self.__class__.__name__.lower()}" @@ -150,10 +149,10 @@ class BaseInstance(ABC): return name if not extension else f"{name}.service" - def get_service_file_path(self) -> str: - return f"{SYSTEMD}/{self.get_service_file_name(extension=True)}" + def get_service_file_path(self) -> Path: + return SYSTEMD.joinpath(self.get_service_file_name(extension=True)) - def get_data_dir_from_suffix(self) -> str: + def get_data_dir_name_from_suffix(self) -> str: if self._suffix is None: return "printer" elif self._suffix.isdigit(): diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 6891139..adc72de 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -9,9 +9,9 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os import re import subprocess +from pathlib import Path from typing import List, Optional, Union, TypeVar from kiauh.core.instance_manager.base_instance import BaseInstance @@ -194,9 +194,10 @@ class InstanceManager: excluded = self.instance_type.blacklist() service_list = [ - os.path.join(SYSTEMD, service) - for service in os.listdir(SYSTEMD) - if pattern.search(service) and not any(s in service for s in excluded) + Path(SYSTEMD, service) + for service in SYSTEMD.iterdir() + if pattern.search(service.name) + and not any(s in service.name for s in excluded) ] instance_list = [ @@ -206,8 +207,8 @@ class InstanceManager: return instance_list - def _get_instance_suffix(self, file_path: str) -> Union[str, None]: - full_name = file_path.split("/")[-1].split(".")[0] + def _get_instance_suffix(self, file_path: Path) -> Union[str, None]: + full_name = file_path.name.split(".")[0] return full_name.split("-")[-1] if "-" in full_name else None diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index feda37f..a3343e8 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -9,9 +9,9 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os import shutil import subprocess +from pathlib import Path from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger @@ -66,7 +66,7 @@ class RepoManager: log = f"Cloning repository from '{self.repo}' with method '{self.method}'" Logger.print_status(log) try: - if os.path.exists(self.target_dir): + if Path(self.target_dir).exists(): question = f"'{self.target_dir}' already exists. Overwrite?" if not get_confirm(question, default_choice=False): Logger.print_info("Skipping re-clone of repository.") diff --git a/kiauh/modules/klipper/__init__.py b/kiauh/modules/klipper/__init__.py index f355f5d..e4d32b6 100644 --- a/kiauh/modules/klipper/__init__.py +++ b/kiauh/modules/klipper/__init__.py @@ -9,14 +9,13 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os from pathlib import Path -MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) +MODULE_PATH = Path(__file__).resolve().parent -KLIPPER_DIR = f"{Path.home()}/klipper" -KLIPPER_ENV_DIR = f"{Path.home()}/klippy-env" -KLIPPER_REQUIREMENTS_TXT = f"{KLIPPER_DIR}/scripts/klippy-requirements.txt" +KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") +KLIPPER_REQUIREMENTS_TXT = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") DEFAULT_KLIPPER_REPO_URL = "https://github.com/Klipper3D/klipper" EXIT_KLIPPER_SETUP = "Exiting Klipper setup ..." diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 02eb639..ef064a8 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -9,14 +9,13 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os import shutil import subprocess from pathlib import Path -from typing import List +from typing import List, Union from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger @@ -29,37 +28,36 @@ class Klipper(BaseInstance): def __init__(self, suffix: str = None): super().__init__(instance_type=self, suffix=suffix) - self.klipper_dir = KLIPPER_DIR - self.env_dir = KLIPPER_ENV_DIR + self.klipper_dir: Path = KLIPPER_DIR + self.env_dir: Path = KLIPPER_ENV_DIR self._cfg_file = self._get_cfg() - self._log = f"{self.log_dir}/klippy.log" - self._serial = f"{self.comms_dir}/klippy.serial" - self._uds = f"{self.comms_dir}/klippy.sock" + self._log = self.log_dir.joinpath("klippy.log") + self._serial = self.comms_dir.joinpath("klippy.serial") + self._uds = self.comms_dir.joinpath("klippy.sock") @property - def cfg_file(self) -> str: + def cfg_file(self) -> Path: return self._cfg_file @property - def log(self) -> str: + def log(self) -> Path: return self._log @property - def serial(self) -> str: + def serial(self) -> Path: return self._serial @property - def uds(self) -> str: + def uds(self) -> Path: return self._uds def create(self) -> None: Logger.print_status("Creating new Klipper Instance ...") - module_path = os.path.dirname(os.path.abspath(__file__)) - service_template_path = os.path.join(module_path, "res", "klipper.service") - env_template_file_path = os.path.join(module_path, "res", "klipper.env") + service_template_path = MODULE_PATH.joinpath("res/klipper.service") service_file_name = self.get_service_file_name(extension=True) - service_file_target = f"{SYSTEMD}/{service_file_name}" - env_file_target = os.path.abspath(f"{self.sysd_dir}/klipper.env") + service_file_target = SYSTEMD.joinpath(service_file_name) + env_template_file_path = MODULE_PATH.joinpath("res/klipper.env") + env_file_target = self.sysd_dir.joinpath("klipper.env") try: self.create_folders() @@ -95,8 +93,11 @@ class Klipper(BaseInstance): self._delete_klipper_remnants() def write_service_file( - self, service_template_path: str, service_file_target: str, env_file_target: str - ): + self, + service_template_path: Path, + service_file_target: Path, + env_file_target: Path, + ) -> None: service_content = self._prep_service_file( service_template_path, env_file_target ) @@ -109,7 +110,9 @@ class Klipper(BaseInstance): ) Logger.print_ok(f"Service file created: {service_file_target}") - def write_env_file(self, env_template_file_path: str, env_file_target: str): + def write_env_file( + self, env_template_file_path: Path, env_file_target: Path + ) -> None: env_file_content = self._prep_env_file(env_template_file_path) with open(env_file_target, "w") as env_file: env_file.write(env_file_content) @@ -129,7 +132,9 @@ class Klipper(BaseInstance): Logger.print_ok("Directories successfully deleted.") - def _prep_service_file(self, service_template_path, env_file_path): + def _prep_service_file( + self, service_template_path: Path, env_file_path: Path + ) -> str: try: with open(service_template_path, "r") as template_file: template_content = template_file.read() @@ -139,12 +144,14 @@ class Klipper(BaseInstance): ) raise service_content = template_content.replace("%USER%", self.user) - service_content = service_content.replace("%KLIPPER_DIR%", self.klipper_dir) - service_content = service_content.replace("%ENV%", self.env_dir) - service_content = service_content.replace("%ENV_FILE%", env_file_path) + service_content = service_content.replace( + "%KLIPPER_DIR%", str(self.klipper_dir) + ) + service_content = service_content.replace("%ENV%", str(self.env_dir)) + service_content = service_content.replace("%ENV_FILE%", str(env_file_path)) return service_content - def _prep_env_file(self, env_template_file_path): + def _prep_env_file(self, env_template_file_path: Path) -> str: try: with open(env_template_file_path, "r") as env_file: env_template_file_content = env_file.read() @@ -154,18 +161,18 @@ class Klipper(BaseInstance): ) raise env_file_content = env_template_file_content.replace( - "%KLIPPER_DIR%", self.klipper_dir + "%KLIPPER_DIR%", str(self.klipper_dir) ) env_file_content = env_file_content.replace( "%CFG%", f"{self.cfg_dir}/printer.cfg" ) - env_file_content = env_file_content.replace("%SERIAL%", self._serial) - env_file_content = env_file_content.replace("%LOG%", self._log) - env_file_content = env_file_content.replace("%UDS%", self._uds) + env_file_content = env_file_content.replace("%SERIAL%", str(self.serial)) + env_file_content = env_file_content.replace("%LOG%", str(self.log)) + env_file_content = env_file_content.replace("%UDS%", str(self.uds)) return env_file_content - def _get_cfg(self): - cfg_file_loc = f"{self.cfg_dir}/printer.cfg" - if Path(cfg_file_loc).is_file(): + def _get_cfg(self) -> Union[Path, None]: + cfg_file_loc = self.cfg_dir.joinpath("printer.cfg") + if cfg_file_loc.is_file(): return cfg_file_loc return None diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 0e33e51..6d6fd66 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -9,7 +9,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os import subprocess from pathlib import Path from typing import List, Union @@ -163,13 +162,13 @@ def setup_klipper_prerequesites() -> None: def install_klipper_packages(klipper_dir: Path) -> None: - script = Path(f"{klipper_dir}/scripts/install-debian.sh") + script = klipper_dir.joinpath("scripts/install-debian.sh") packages = parse_packages_from_file(script) packages = [pkg.replace("python-dev", "python3-dev") for pkg in packages] # Add dfu-util for octopi-images packages.append("dfu-util") # Add dbus requirement for DietPi distro - if os.path.exists("/boot/dietpi/.version"): + if Path("/boot/dietpi/.version").exists(): packages.append("dbus") update_system_package_lists(silent=False) diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 6e059ce..bd929c6 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -15,6 +15,7 @@ import grp import shutil import subprocess import textwrap +from pathlib import Path from typing import List, Union, Literal, Dict @@ -113,7 +114,7 @@ def handle_single_to_multi_conversion( instance_manager.current_instance = Klipper(suffix=name) new_data_dir_name = instance_manager.current_instance.data_dir try: - os.rename(old_data_dir_name, new_data_dir_name) + Path(old_data_dir_name).rename(new_data_dir_name) return instance_manager.current_instance except OSError as e: log = f"Cannot rename {old_data_dir_name} to {new_data_dir_name}:\n{e}" @@ -208,8 +209,8 @@ def create_example_printer_cfg(instance: Klipper) -> None: Logger.print_info(f"printer.cfg in '{instance.cfg_dir}' already exists.") return - source = os.path.join(MODULE_PATH, "res", "printer.cfg") - target = os.path.join(instance.cfg_dir, "printer.cfg") + source = MODULE_PATH.joinpath("res/printer.cfg") + target = instance.cfg_dir.joinpath("printer.cfg") try: shutil.copy(source, target) except OSError as e: @@ -217,6 +218,6 @@ def create_example_printer_cfg(instance: Klipper) -> None: return cm = ConfigManager(target) - cm.set_value("virtual_sdcard", "path", instance.gcodes_dir) + cm.set_value("virtual_sdcard", "path", str(instance.gcodes_dir)) cm.write_config() Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'") diff --git a/kiauh/modules/mainsail/__init__.py b/kiauh/modules/mainsail/__init__.py index 06f2027..87dc2f5 100644 --- a/kiauh/modules/mainsail/__init__.py +++ b/kiauh/modules/mainsail/__init__.py @@ -11,12 +11,10 @@ from pathlib import Path -import os - -MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) -MAINSAIL_DIR = os.path.join(Path.home(), "mainsail") -MAINSAIL_CONFIG_DIR = os.path.join(Path.home(), "mainsail-config") -MAINSAIL_CONFIG_JSON = os.path.join(MAINSAIL_DIR, "config.json") +MODULE_PATH = Path(__file__).resolve().parent +MAINSAIL_DIR = Path(Path.home(), "mainsail") +MAINSAIL_CONFIG_DIR = Path(Path.home(), "mainsail-config") +MAINSAIL_CONFIG_JSON = Path(MAINSAIL_DIR, "config.json") MAINSAIL_URL = ( "https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" ) diff --git a/kiauh/modules/mainsail/mainsail_remove.py b/kiauh/modules/mainsail/mainsail_remove.py index 6e80fc1..275ce1f 100644 --- a/kiauh/modules/mainsail/mainsail_remove.py +++ b/kiauh/modules/mainsail/mainsail_remove.py @@ -13,6 +13,7 @@ import shutil import subprocess from pathlib import Path +from typing import List from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager @@ -64,8 +65,8 @@ def remove_mainsail_dir() -> None: def remove_nginx_config() -> None: Logger.print_status("Removing Mainsails NGINX config ...") try: - remove_file(Path(NGINX_SITES_AVAILABLE, "mainsail"), True) - remove_file(Path(NGINX_SITES_ENABLED, "mainsail"), True) + remove_file(NGINX_SITES_AVAILABLE.joinpath("mainsail"), True) + remove_file(NGINX_SITES_ENABLED.joinpath("mainsail"), True) except subprocess.CalledProcessError as e: log = f"Unable to remove Mainsail NGINX config:\n{e.stderr.decode()}" diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index d1fbc54..758c39a 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -9,7 +9,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os.path import subprocess from pathlib import Path from typing import List @@ -72,7 +71,7 @@ def run_mainsail_installation() -> None: else: return - is_mainsail_installed = Path(f"{Path.home()}/mainsail").exists() + is_mainsail_installed = Path.home().joinpath("mainsail").exists() do_reinstall = False if is_mainsail_installed: print_mainsail_already_installed_dialog() @@ -92,9 +91,9 @@ def run_mainsail_installation() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) default_port = cm.get_value("mainsail", "default_port") - mainsail_port = default_port if default_port else 80 + mainsail_port = default_port if default_port else "80" if not default_port: - print_mainsail_port_select_dialog(f"{mainsail_port}") + print_mainsail_port_select_dialog(mainsail_port) mainsail_port = get_number_input( "Configure Mainsail for port", min_count=mainsail_port, @@ -148,11 +147,11 @@ def run_mainsail_installation() -> None: def download_mainsail() -> None: try: Logger.print_status("Downloading Mainsail ...") - download_file(MAINSAIL_URL, f"{Path.home()}", "mainsail.zip") + download_file(MAINSAIL_URL, Path.home(), "mainsail.zip") Logger.print_ok("Download complete!") Logger.print_status("Extracting mainsail.zip ...") - unzip(f"{Path.home()}/mainsail.zip", MAINSAIL_DIR) + unzip(Path.home().joinpath("mainsail.zip"), MAINSAIL_DIR) Logger.print_ok("OK!") except Exception: @@ -184,8 +183,8 @@ def create_mainsail_cfg_symlink(klipper_instances: List[Klipper]) -> None: def create_mainsail_nginx_cfg(port: int) -> None: root_dir = MAINSAIL_DIR - source = Path(NGINX_SITES_AVAILABLE, "mainsail") - target = Path(NGINX_SITES_ENABLED, "mainsail") + source = NGINX_SITES_AVAILABLE.joinpath("mainsail") + target = NGINX_SITES_ENABLED.joinpath("mainsail") try: Logger.print_status("Creating NGINX config for Mainsail ...") remove_file(Path("/etc/nginx/sites-enabled/default"), True) @@ -217,7 +216,7 @@ def patch_moonraker_conf( Logger.print_info("Section already exist. Skipped ...") return - template = os.path.join(MODULE_PATH, "res", template_file) + template = MODULE_PATH.joinpath("res", template_file) with open(template, "r") as t: template_content = "\n" template_content += t.read() diff --git a/kiauh/modules/mainsail/mainsail_utils.py b/kiauh/modules/mainsail/mainsail_utils.py index 02c0984..196e27a 100644 --- a/kiauh/modules/mainsail/mainsail_utils.py +++ b/kiauh/modules/mainsail/mainsail_utils.py @@ -10,7 +10,6 @@ # ======================================================================= # import json -import os import shutil from pathlib import Path from typing import List @@ -26,9 +25,9 @@ from kiauh.utils.logger import Logger def get_mainsail_status() -> str: return get_install_status_webui( MAINSAIL_DIR, - Path(NGINX_SITES_AVAILABLE, "mainsail"), - Path(NGINX_CONFD, "upstreams.conf"), - Path(NGINX_CONFD, "common_vars.conf"), + NGINX_SITES_AVAILABLE.joinpath("mainsail"), + NGINX_CONFD.joinpath("upstreams.conf"), + NGINX_CONFD.joinpath("common_vars.conf"), ) @@ -36,7 +35,7 @@ def backup_config_json(is_temp=False) -> None: Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") bm = BackupManager() if is_temp: - fn = Path(Path.home(), "config.json.kiauh.bak") + fn = Path.home().joinpath("config.json.kiauh.bak") bm.backup_file([MAINSAIL_CONFIG_JSON], custom_filename=fn) else: bm.backup_file([MAINSAIL_CONFIG_JSON]) @@ -45,7 +44,7 @@ def backup_config_json(is_temp=False) -> None: def restore_config_json() -> None: try: Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") - source = os.path.join(Path.home(), "config.json.kiauh.bak") + source = Path.home().joinpath("config.json.kiauh.bak") shutil.copy(source, MAINSAIL_CONFIG_JSON) except OSError: Logger.print_info("Unable to restore config.json. Skipped ...") @@ -71,10 +70,10 @@ def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: error_log = Path("/var/log/nginx/mainsail-error.log") for instance in klipper_instances: - desti_access = Path(instance.log_dir).joinpath("mainsail-access.log") + desti_access = instance.log_dir.joinpath("mainsail-access.log") if not desti_access.exists(): desti_access.symlink_to(access_log) - desti_error = Path(instance.log_dir).joinpath("mainsail-error.log") + desti_error = instance.log_dir.joinpath("mainsail-error.log") if not desti_error.exists(): desti_error.symlink_to(error_log) diff --git a/kiauh/modules/moonraker/__init__.py b/kiauh/modules/moonraker/__init__.py index af0e88f..9e7231e 100644 --- a/kiauh/modules/moonraker/__init__.py +++ b/kiauh/modules/moonraker/__init__.py @@ -9,23 +9,24 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os from pathlib import Path -MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) +MODULE_PATH = Path(__file__).resolve().parent -MOONRAKER_DIR = f"{Path.home()}/moonraker" -MOONRAKER_ENV_DIR = f"{Path.home()}/moonraker-env" -MOONRAKER_REQUIREMENTS_TXT = f"{MOONRAKER_DIR}/scripts/moonraker-requirements.txt" +MOONRAKER_DIR = Path.home().joinpath("moonraker") +MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env") +MOONRAKER_REQUIREMENTS_TXT = MOONRAKER_DIR.joinpath( + "scripts/moonraker-requirements.txt" +) DEFAULT_MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker" DEFAULT_MOONRAKER_PORT = 7125 # introduced due to # https://github.com/Arksine/moonraker/issues/349 # https://github.com/Arksine/moonraker/pull/346 -POLKIT_LEGACY_FILE = "/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla" -POLKIT_FILE = "/etc/polkit-1/rules.d/moonraker.rules" -POLKIT_USR_FILE = "/usr/share/polkit-1/rules.d/moonraker.rules" -POLKIT_SCRIPT = f"{Path.home()}/moonraker/scripts/set-policykit-rules.sh" +POLKIT_LEGACY_FILE = Path("/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla") +POLKIT_FILE = Path("/etc/polkit-1/rules.d/moonraker.rules") +POLKIT_USR_FILE = Path("/usr/share/polkit-1/rules.d/moonraker.rules") +POLKIT_SCRIPT = Path.home().joinpath("moonraker/scripts/set-policykit-rules.sh") EXIT_MOONRAKER_SETUP = "Exiting Moonraker setup ..." diff --git a/kiauh/modules/moonraker/moonraker.py b/kiauh/modules/moonraker/moonraker.py index d3dbfc6..8c6777c 100644 --- a/kiauh/modules/moonraker/moonraker.py +++ b/kiauh/modules/moonraker/moonraker.py @@ -9,7 +9,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os import shutil import subprocess from pathlib import Path @@ -30,22 +29,22 @@ class Moonraker(BaseInstance): def __init__(self, suffix: str = None): super().__init__(instance_type=self, suffix=suffix) - self.moonraker_dir = MOONRAKER_DIR - self.env_dir = MOONRAKER_ENV_DIR + self.moonraker_dir: Path = MOONRAKER_DIR + self.env_dir: Path = MOONRAKER_ENV_DIR self.cfg_file = self._get_cfg() self.port = self._get_port() - self.backup_dir = f"{self.data_dir}/backup" - self.certs_dir = f"{self.data_dir}/certs" - self.db_dir = f"{self.data_dir}/database" - self.log = f"{self.log_dir}/moonraker.log" + self.backup_dir = self.data_dir.joinpath("backup") + self.certs_dir = self.data_dir.joinpath("certs") + self.db_dir = self.data_dir.joinpath("database") + self.log = self.log_dir.joinpath("moonraker.log") def create(self, create_example_cfg: bool = False) -> None: Logger.print_status("Creating new Moonraker Instance ...") - service_template_path = os.path.join(MODULE_PATH, "res", "moonraker.service") - env_template_file_path = os.path.join(MODULE_PATH, "res", "moonraker.env") + service_template_path = MODULE_PATH.joinpath("res/moonraker.service") + env_template_file_path = MODULE_PATH.joinpath("res/moonraker.env") service_file_name = self.get_service_file_name(extension=True) - service_file_target = f"{SYSTEMD}/{service_file_name}" - env_file_target = os.path.abspath(f"{self.sysd_dir}/moonraker.env") + service_file_target = SYSTEMD.joinpath(service_file_name) + env_file_target = self.sysd_dir.joinpath("moonraker.env") try: self.create_folders([self.backup_dir, self.certs_dir, self.db_dir]) @@ -81,8 +80,11 @@ class Moonraker(BaseInstance): self._delete_moonraker_remnants() def write_service_file( - self, service_template_path: str, service_file_target: str, env_file_target: str - ): + self, + service_template_path: Path, + service_file_target: Path, + env_file_target: Path, + ) -> None: service_content = self._prep_service_file( service_template_path, env_file_target ) @@ -95,7 +97,9 @@ class Moonraker(BaseInstance): ) Logger.print_ok(f"Service file created: {service_file_target}") - def write_env_file(self, env_template_file_path: str, env_file_target: str): + def write_env_file( + self, env_template_file_path: Path, env_file_target: Path + ) -> None: env_file_content = self._prep_env_file(env_template_file_path) with open(env_file_target, "w") as env_file: env_file.write(env_file_content) @@ -104,9 +108,9 @@ class Moonraker(BaseInstance): def _delete_moonraker_remnants(self) -> None: try: Logger.print_status(f"Delete {self.moonraker_dir} ...") - shutil.rmtree(Path(self.moonraker_dir)) + shutil.rmtree(self.moonraker_dir) Logger.print_status(f"Delete {self.env_dir} ...") - shutil.rmtree(Path(self.env_dir)) + shutil.rmtree(self.env_dir) except FileNotFoundError: Logger.print_status("Cannot delete Moonraker directories. Not found.") except PermissionError as e: @@ -115,7 +119,9 @@ class Moonraker(BaseInstance): Logger.print_ok("Directories successfully deleted.") - def _prep_service_file(self, service_template_path, env_file_path): + def _prep_service_file( + self, service_template_path: Path, env_file_path: Path + ) -> str: try: with open(service_template_path, "r") as template_file: template_content = template_file.read() @@ -125,12 +131,14 @@ class Moonraker(BaseInstance): ) raise service_content = template_content.replace("%USER%", self.user) - service_content = service_content.replace("%MOONRAKER_DIR%", self.moonraker_dir) - service_content = service_content.replace("%ENV%", self.env_dir) - service_content = service_content.replace("%ENV_FILE%", env_file_path) + service_content = service_content.replace( + "%MOONRAKER_DIR%", str(self.moonraker_dir) + ) + service_content = service_content.replace("%ENV%", str(self.env_dir)) + service_content = service_content.replace("%ENV_FILE%", str(env_file_path)) return service_content - def _prep_env_file(self, env_template_file_path): + def _prep_env_file(self, env_template_file_path: Path) -> str: try: with open(env_template_file_path, "r") as env_file: env_template_file_content = env_file.read() @@ -140,14 +148,16 @@ class Moonraker(BaseInstance): ) raise env_file_content = env_template_file_content.replace( - "%MOONRAKER_DIR%", self.moonraker_dir + "%MOONRAKER_DIR%", str(self.moonraker_dir) + ) + env_file_content = env_file_content.replace( + "%PRINTER_DATA%", str(self.data_dir) ) - env_file_content = env_file_content.replace("%PRINTER_DATA%", self.data_dir) return env_file_content - def _get_cfg(self): - cfg_file_loc = f"{self.cfg_dir}/moonraker.conf" - if Path(cfg_file_loc).is_file(): + def _get_cfg(self) -> Union[Path, None]: + cfg_file_loc = self.cfg_dir.joinpath("moonraker.conf") + if cfg_file_loc.is_file(): return cfg_file_loc return None diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index 98b000e..a883071 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -163,14 +163,13 @@ def setup_moonraker_prerequesites() -> None: repo_manager.clone_repo() # install moonraker dependencies and create python virtualenv - install_moonraker_packages(Path(MOONRAKER_DIR)) - create_python_venv(Path(MOONRAKER_ENV_DIR)) - moonraker_py_req = Path(MOONRAKER_REQUIREMENTS_TXT) - install_python_requirements(Path(MOONRAKER_ENV_DIR), moonraker_py_req) + install_moonraker_packages(MOONRAKER_DIR) + create_python_venv(MOONRAKER_ENV_DIR) + install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT) def install_moonraker_packages(moonraker_dir: Path) -> None: - script = Path(f"{moonraker_dir}/scripts/install-moonraker.sh") + script = moonraker_dir.joinpath("scripts/install-moonraker.sh") packages = parse_packages_from_file(script) update_system_package_lists(silent=False) install_system_packages(packages) @@ -179,9 +178,9 @@ def install_moonraker_packages(moonraker_dir: Path) -> None: def install_moonraker_polkit() -> None: Logger.print_status("Installing Moonraker policykit rules ...") - legacy_file_exists = check_file_exist(Path(POLKIT_LEGACY_FILE), True) - polkit_file_exists = check_file_exist(Path(POLKIT_FILE), True) - usr_file_exists = check_file_exist(Path(POLKIT_USR_FILE), True) + 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.") @@ -267,7 +266,7 @@ def remove_instances( def remove_polkit_rules() -> None: Logger.print_status("Removing all Moonraker policykit rules ...") - if not Path(MOONRAKER_DIR).exists(): + if not MOONRAKER_DIR.exists(): log = "Cannot remove policykit rules. Moonraker directory not found." Logger.print_warn(log) return diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/modules/moonraker/moonraker_utils.py index 394a84f..01ee7d7 100644 --- a/kiauh/modules/moonraker/moonraker_utils.py +++ b/kiauh/modules/moonraker/moonraker_utils.py @@ -9,7 +9,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os import shutil from typing import Dict, Literal @@ -45,8 +44,8 @@ def create_example_moonraker_conf( Logger.print_info(f"moonraker.conf in '{instance.cfg_dir}' already exists.") return - source = os.path.join(MODULE_PATH, "res", "moonraker.conf") - target = os.path.join(instance.cfg_dir, "moonraker.conf") + source = MODULE_PATH.joinpath("res/moonraker.conf") + target = instance.cfg_dir.joinpath("moonraker.conf") try: shutil.copy(source, target) except OSError as e: @@ -72,14 +71,14 @@ def create_example_moonraker_conf( ip = get_ipv4_addr().split(".")[:2] ip.extend(["0", "0/16"]) - uds = f"{instance.comms_dir}/klippy.sock" + uds = instance.comms_dir.joinpath("klippy.sock") cm = ConfigManager(target) trusted_clients = f"\n{'.'.join(ip)}" trusted_clients += cm.get_value("authorization", "trusted_clients") cm.set_value("server", "port", str(port)) - cm.set_value("server", "klippy_uds_address", uds) + cm.set_value("server", "klippy_uds_address", str(uds)) cm.set_value("authorization", "trusted_clients", trusted_clients) cm.write_config() diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index 108b521..f44410e 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -8,12 +8,13 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os -MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) +from pathlib import Path + +MODULE_PATH = Path(__file__).resolve().parent INVALID_CHOICE = "Invalid choice. Please select a valid value." # ================== NGINX =====================# -NGINX_SITES_AVAILABLE = "/etc/nginx/sites-available" -NGINX_SITES_ENABLED = "/etc/nginx/sites-enabled" -NGINX_CONFD = "/etc/nginx/conf.d" +NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") +NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled") +NGINX_CONFD = Path("/etc/nginx/conf.d") diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 856534d..8cb20c8 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -56,7 +56,7 @@ def check_install_dependencies(deps: List[str]) -> None: install_system_packages(requirements) -def get_repo_name(repo_dir: str) -> str: +def get_repo_name(repo_dir: Path) -> str: """ Helper method to extract the organisation and name of a repository | :param repo_dir: repository to extract the values from @@ -72,7 +72,7 @@ def get_repo_name(repo_dir: str) -> str: def get_install_status_common( - instance_type: Type[BaseInstance], repo_dir: str, env_dir: str + instance_type: Type[BaseInstance], repo_dir: Path, env_dir: Path ) -> str: """ Helper method to get the installation status of software components, @@ -85,10 +85,8 @@ def get_install_status_common( :return: formatted string, containing the status """ im = InstanceManager(instance_type) - dir_exist = Path(repo_dir).exists() - env_dir_exist = Path(env_dir).exists() instances_exist = len(im.instances) > 0 - status = [dir_exist, env_dir_exist, instances_exist] + status = [repo_dir.exists(), env_dir.exists(), instances_exist] if all(status): return f"{COLOR_GREEN}Installed: {len(im.instances)}{RESET_FORMAT}" elif not any(status): @@ -109,7 +107,7 @@ def get_install_status_webui( :param common_cfg: the required common_vars.conf :return: formatted string, containing the status """ - dir_exist = Path(install_dir).exists() + dir_exist = install_dir.exists() nginx_cfg_exist = check_file_exist(nginx_cfg) upstreams_cfg_exist = check_file_exist(upstreams_cfg) common_cfg_exist = check_file_exist(common_cfg) diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py index 66b00ff..647b55b 100644 --- a/kiauh/utils/constants.py +++ b/kiauh/utils/constants.py @@ -11,6 +11,7 @@ import os import pwd +from pathlib import Path # text colors and formats COLOR_WHITE = "\033[37m" # white @@ -22,4 +23,4 @@ COLOR_CYAN = "\033[96m" # bright cyan RESET_FORMAT = "\033[0m" # reset format # current user CURRENT_USER = pwd.getpwuid(os.getuid())[0] -SYSTEMD = "/etc/systemd/system" +SYSTEMD = Path("/etc/systemd/system") diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index b623be2..505c1f1 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -8,7 +8,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import os import shutil import subprocess from pathlib import Path @@ -37,27 +36,12 @@ def check_file_exist(file_path: Path, sudo=False) -> bool: except subprocess.CalledProcessError: return False else: - if Path(file_path).exists(): + if file_path.exists(): return True else: return False -def create_directory(_dir: Path) -> None: - """ - Helper function for creating a directory or skipping if it already exists | - :param _dir: the directory to create - :return: None - """ - try: - if not os.path.isdir(_dir): - os.makedirs(_dir, exist_ok=True) - Logger.print_ok(f"Created directory: {_dir}") - except OSError as e: - Logger.print_error(f"Error creating folder: {e}") - raise - - def create_symlink(source: Path, target: Path, sudo=False) -> None: try: cmd = ["ln", "-sf", source, target] @@ -79,14 +63,14 @@ def remove_file(file_path: Path, sudo=False) -> None: raise -def unzip(file: str, target_dir: str) -> None: +def unzip(filepath: Path, target_dir: Path) -> None: """ Helper function to unzip a zip-archive into a target directory | - :param file: the zip-file to unzip + :param filepath: the path to the zip-file to unzip :param target_dir: the target directory to extract the files into :return: None """ - with ZipFile(file, "r") as _zip: + with ZipFile(filepath, "r") as _zip: _zip.extractall(target_dir) @@ -95,8 +79,8 @@ def copy_upstream_nginx_cfg() -> None: Creates an upstream.conf in /etc/nginx/conf.d :return: None """ - source = os.path.join(MODULE_PATH, "res", "upstreams.conf") - target = os.path.join(NGINX_CONFD, "upstreams.conf") + source = MODULE_PATH.joinpath("res/upstreams.conf") + target = NGINX_CONFD.joinpath("upstreams.conf") try: command = ["sudo", "cp", source, target] subprocess.run(command, stderr=subprocess.PIPE, check=True) @@ -111,8 +95,8 @@ def copy_common_vars_nginx_cfg() -> None: Creates a common_vars.conf in /etc/nginx/conf.d :return: None """ - source = os.path.join(MODULE_PATH, "res", "common_vars.conf") - target = os.path.join(NGINX_CONFD, "common_vars.conf") + source = MODULE_PATH.joinpath("res/common_vars.conf") + target = NGINX_CONFD.joinpath("common_vars.conf") try: command = ["sudo", "cp", source, target] subprocess.run(command, stderr=subprocess.PIPE, check=True) @@ -122,7 +106,7 @@ def copy_common_vars_nginx_cfg() -> None: raise -def create_nginx_cfg(name: str, port: int, root_dir: str) -> None: +def create_nginx_cfg(name: str, port: int, root_dir: Path) -> None: """ Creates an NGINX config from a template file and replaces all placeholders :param name: name of the config to create @@ -130,18 +114,18 @@ def create_nginx_cfg(name: str, port: int, root_dir: str) -> None: :param root_dir: directory of the static files :return: None """ - tmp = f"{Path.home()}/{name}.tmp" - shutil.copy(os.path.join(MODULE_PATH, "res", "nginx_cfg"), tmp) + tmp = Path.home().joinpath(f"{name}.tmp") + shutil.copy(MODULE_PATH.joinpath("res/nginx_cfg"), tmp) with open(tmp, "r+") as f: content = f.read() content = content.replace("%NAME%", name) content = content.replace("%PORT%", str(port)) - content = content.replace("%ROOT_DIR%", root_dir) + content = content.replace("%ROOT_DIR%", str(root_dir)) f.seek(0) f.write(content) f.truncate() - target = os.path.join(NGINX_SITES_AVAILABLE, name) + target = NGINX_SITES_AVAILABLE.joinpath(name) try: command = ["sudo", "mv", tmp, target] subprocess.run(command, stderr=subprocess.PIPE, check=True) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 0b07655..be317ea 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -98,7 +98,7 @@ def update_python_pip(target: Path) -> None: """ Logger.print_status("Updating pip ...") try: - command = [f"{target}/bin/pip", "install", "-U", "pip"] + command = [target.joinpath("bin/pip"), "install", "-U", "pip"] result = subprocess.run(command, stderr=subprocess.PIPE, text=True) if result.returncode != 0 or result.stderr: Logger.print_error(f"{result.stderr}", False) @@ -120,7 +120,7 @@ def install_python_requirements(target: Path, requirements: Path) -> None: update_python_pip(target) Logger.print_status("Installing Python requirements ...") try: - command = [f"{target}/bin/pip", "install", "-r", f"{requirements}"] + command = [target.joinpath("bin/pip"), "install", "-r", f"{requirements}"] result = subprocess.run(command, stderr=subprocess.PIPE, text=True) if result.returncode != 0 or result.stderr: Logger.print_error(f"{result.stderr}", False) @@ -141,9 +141,12 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: :return: None """ cache_mtime = 0 - cache_files = ["/var/lib/apt/periodic/update-success-stamp", "/var/lib/apt/lists"] + cache_files = [ + Path("/var/lib/apt/periodic/update-success-stamp"), + Path("/var/lib/apt/lists"), + ] for cache_file in cache_files: - if Path(cache_file).exists(): + if cache_file.exists(): cache_mtime = max(cache_mtime, os.path.getmtime(cache_file)) update_age = int(time.time() - cache_mtime) @@ -244,7 +247,7 @@ def get_ipv4_addr() -> str: def download_file( - url: str, target_folder: str, target_name: str, show_progress=True + url: str, target_folder: Path, target_name: str, show_progress=True ) -> None: """ Helper method for downloading files from a provided URL | @@ -254,7 +257,7 @@ def download_file( :param show_progress: show download progress or not :return: None """ - target_path = os.path.join(target_folder, target_name) + target_path = target_folder.joinpath(target_name) try: if show_progress: urllib.request.urlretrieve(url, target_path, download_progress) @@ -298,8 +301,8 @@ def set_nginx_permissions() -> None: This seems to have become necessary with Ubuntu 21+. | :return: None """ - cmd1 = f"ls -ld {Path.home()} | cut -d' ' -f1" - homedir_perm = subprocess.run(cmd1, shell=True, stdout=subprocess.PIPE, text=True) + cmd = f"ls -ld {Path.home()} | cut -d' ' -f1" + homedir_perm = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, text=True) homedir_perm = homedir_perm.stdout if homedir_perm.count("x") < 3: @@ -321,7 +324,7 @@ def control_systemd_service( Logger.print_status(f"{action.capitalize()} {name}.service ...") command = ["sudo", "systemctl", action, f"{name}.service"] subprocess.run(command, stderr=subprocess.PIPE, check=True) - Logger.print_ok(f"OK!") + Logger.print_ok("OK!") except subprocess.CalledProcessError as e: log = f"Failed to {action} {name}.service: {e.stderr.decode()}" Logger.print_error(log) From 012b6c4bb7ca4eb319140e0944fa35fb164c31ce Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 15:12:08 +0100 Subject: [PATCH 068/296] refactor(Moonraker): rework remove process Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/base_instance.py | 2 +- .../core/instance_manager/instance_manager.py | 8 +- kiauh/core/menus/install_menu.py | 2 +- kiauh/core/menus/remove_menu.py | 8 +- kiauh/modules/moonraker/menus/__init__.py | 0 .../moonraker/menus/moonraker_remove_menu.py | 119 +++++++++++++ kiauh/modules/moonraker/moonraker.py | 19 +-- kiauh/modules/moonraker/moonraker_remove.py | 156 ++++++++++++++++++ kiauh/modules/moonraker/moonraker_setup.py | 138 +++------------- 9 files changed, 308 insertions(+), 144 deletions(-) create mode 100644 kiauh/modules/moonraker/menus/__init__.py create mode 100644 kiauh/modules/moonraker/menus/moonraker_remove_menu.py create mode 100644 kiauh/modules/moonraker/moonraker_remove.py diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index 55f6941..bccacee 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -124,7 +124,7 @@ class BaseInstance(ABC): raise NotImplementedError("Subclasses must implement the create method") @abstractmethod - def delete(self, del_remnants: bool) -> None: + def delete(self) -> None: raise NotImplementedError("Subclasses must implement the delete method") def create_folders(self, add_dirs: List[Path] = None) -> None: diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index adc72de..411a25b 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -83,7 +83,7 @@ class InstanceManager: @property def instances(self) -> List[I]: if not self._instances: - self._instances = self._find_instances() + self._instances = self.find_instances() return sorted(self._instances, key=lambda x: self._sort_instance_list(x.suffix)) @@ -101,10 +101,10 @@ class InstanceManager: else: raise ValueError("current_instance cannot be None") - def delete_instance(self, del_remnants=False) -> None: + def delete_instance(self) -> None: if self.current_instance is not None: try: - self.current_instance.delete(del_remnants) + self.current_instance.delete() except (OSError, subprocess.CalledProcessError) as e: Logger.print_error(f"Removing instance failed: {e}") raise @@ -188,7 +188,7 @@ class InstanceManager: Logger.print_error(f"{e}") raise - def _find_instances(self) -> List[I]: + def find_instances(self) -> List[I]: name = self.instance_type.__name__.lower() pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.service$") excluded = self.instance_type.blacklist() diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index b30e4ee..efb1650 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -67,7 +67,7 @@ class InstallMenu(BaseMenu): klipper_setup.run_klipper_setup(install=True) def install_moonraker(self): - moonraker_setup.run_moonraker_setup(install=True) + moonraker_setup.install_moonraker() def install_mainsail(self): mainsail_setup.run_mainsail_installation() diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 4883609..5526406 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -14,9 +14,8 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper import klipper_setup -from kiauh.modules.mainsail import mainsail_setup from kiauh.modules.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu -from kiauh.modules.moonraker import moonraker_setup +from kiauh.modules.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu from kiauh.utils.constants import COLOR_RED, RESET_FORMAT @@ -27,7 +26,7 @@ class RemoveMenu(BaseMenu): header=True, options={ 1: self.remove_klipper, - 2: self.remove_moonraker, + 2: MoonrakerRemoveMenu, 3: MainsailRemoveMenu, 5: self.remove_fluidd, 6: self.remove_klipperscreen, @@ -73,9 +72,6 @@ class RemoveMenu(BaseMenu): def remove_klipper(self): klipper_setup.run_klipper_setup(install=False) - def remove_moonraker(self): - moonraker_setup.run_moonraker_setup(install=False) - def remove_fluidd(self): print("remove_fluidd") diff --git a/kiauh/modules/moonraker/menus/__init__.py b/kiauh/modules/moonraker/menus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/modules/moonraker/menus/moonraker_remove_menu.py b/kiauh/modules/moonraker/menus/moonraker_remove_menu.py new file mode 100644 index 0000000..c6d0a05 --- /dev/null +++ b/kiauh/modules/moonraker/menus/moonraker_remove_menu.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap + +from kiauh.core.menus import BACK_HELP_FOOTER +from kiauh.core.menus.base_menu import BaseMenu +from kiauh.modules.moonraker import moonraker_remove +from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN + + +class MoonrakerRemoveMenu(BaseMenu): + def __init__(self): + super().__init__( + header=False, + options={ + 0: self.toggle_all, + 1: self.toggle_remove_moonraker_service, + 2: self.toggle_remove_moonraker_dir, + 3: self.toggle_remove_moonraker_env, + 4: self.toggle_remove_moonraker_polkit, + 5: self.toggle_delete_moonraker_logs, + 6: self.run_removal_process, + }, + footer_type=BACK_HELP_FOOTER, + ) + self.remove_moonraker_service = False + self.remove_moonraker_dir = False + self.remove_moonraker_env = False + self.remove_moonraker_polkit = False + self.delete_moonraker_logs = False + + def print_menu(self) -> None: + header = " [ Remove Moonraker ] " + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" + 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 + o5 = checked if self.delete_moonraker_logs else unchecked + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Enter a number and hit enter to select / deselect | + | the specific option for removal. | + |-------------------------------------------------------| + | 0) Select everything | + |-------------------------------------------------------| + | 1) {o1} Remove Service | + | 2) {o2} Remove Local Repository | + | 3) {o3} Remove Python Environment | + | 4) {o4} Remove Policy Kit Rules | + | 5) {o5} Delete all Log-Files | + |-------------------------------------------------------| + | 6) Continue | + """ + )[1:] + print(menu, end="") + + def toggle_all(self) -> None: + self.remove_moonraker_service = True + self.remove_moonraker_dir = True + self.remove_moonraker_env = True + self.remove_moonraker_polkit = True + self.delete_moonraker_logs = True + + def toggle_remove_moonraker_service(self) -> None: + self.remove_moonraker_service = not self.remove_moonraker_service + + def toggle_remove_moonraker_dir(self) -> None: + self.remove_moonraker_dir = not self.remove_moonraker_dir + + def toggle_remove_moonraker_env(self) -> None: + self.remove_moonraker_env = not self.remove_moonraker_env + + def toggle_remove_moonraker_polkit(self) -> None: + self.remove_moonraker_polkit = not self.remove_moonraker_polkit + + def toggle_delete_moonraker_logs(self) -> None: + self.delete_moonraker_logs = not self.delete_moonraker_logs + + def run_removal_process(self) -> 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 + and not self.delete_moonraker_logs + ): + error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" + print(error) + return + + moonraker_remove.run_moonraker_removal( + self.remove_moonraker_service, + self.remove_moonraker_dir, + self.remove_moonraker_env, + self.remove_moonraker_polkit, + self.delete_moonraker_logs, + ) + + self.remove_moonraker_service = False + self.remove_moonraker_dir = False + self.remove_moonraker_env = False + self.remove_moonraker_polkit = False + self.delete_moonraker_logs = False diff --git a/kiauh/modules/moonraker/moonraker.py b/kiauh/modules/moonraker/moonraker.py index 8c6777c..4a1ae12 100644 --- a/kiauh/modules/moonraker/moonraker.py +++ b/kiauh/modules/moonraker/moonraker.py @@ -62,7 +62,7 @@ class Moonraker(BaseInstance): Logger.print_error(f"Error writing file: {e}") raise - def delete(self, del_remnants: bool) -> None: + def delete(self) -> None: service_file = self.get_service_file_name(extension=True) service_file_path = self.get_service_file_path() @@ -76,9 +76,6 @@ class Moonraker(BaseInstance): Logger.print_error(f"Error deleting service file: {e}") raise - if del_remnants: - self._delete_moonraker_remnants() - def write_service_file( self, service_template_path: Path, @@ -105,20 +102,6 @@ class Moonraker(BaseInstance): env_file.write(env_file_content) Logger.print_ok(f"Env file created: {env_file_target}") - def _delete_moonraker_remnants(self) -> None: - try: - Logger.print_status(f"Delete {self.moonraker_dir} ...") - shutil.rmtree(self.moonraker_dir) - Logger.print_status(f"Delete {self.env_dir} ...") - shutil.rmtree(self.env_dir) - except FileNotFoundError: - Logger.print_status("Cannot delete Moonraker directories. Not found.") - except PermissionError as e: - Logger.print_error(f"Error deleting Moonraker directories: {e}") - raise - - Logger.print_ok("Directories successfully deleted.") - def _prep_service_file( self, service_template_path: Path, env_file_path: Path ) -> str: diff --git a/kiauh/modules/moonraker/moonraker_remove.py b/kiauh/modules/moonraker/moonraker_remove.py new file mode 100644 index 0000000..e9631db --- /dev/null +++ b/kiauh/modules/moonraker/moonraker_remove.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import shutil +import subprocess +from typing import List, Union + +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.modules.klipper.klipper_dialogs import print_instance_overview +from kiauh.modules.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR +from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.utils.filesystem_utils import remove_file +from kiauh.utils.input_utils import get_selection_input +from kiauh.utils.logger import Logger + + +def run_moonraker_removal( + remove_service: bool, + remove_dir: bool, + remove_env: bool, + remove_polkit: bool, + delete_logs: bool, +) -> None: + im = InstanceManager(Moonraker) + + if remove_service: + Logger.print_status("Removing Moonraker instances ...") + if im.instances: + instances_to_remove = select_instances_to_remove(im.instances) + remove_instances(im, instances_to_remove) + else: + Logger.print_info("No Moonraker Services installed! Skipped ...") + + im.find_instances() + if (remove_polkit or remove_dir or remove_env) and im.instances: + Logger.print_warn("There are still other Moonraker services installed!") + Logger.print_warn("Therefor the following parts cannot be removed:") + Logger.print_warn( + """ + ● Moonraker PolicyKit rules + ● Moonraker local repository + ● Moonraker Python environment + """, + 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 ...") + remove_moonraker_dir() + if remove_env: + Logger.print_status("Removing Moonraker Python environment ...") + remove_moonraker_env() + + # delete moonraker logs of all instances + if delete_logs: + Logger.print_status("Removing all Moonraker logs ...") + delete_moonraker_logs(im.instances) + + +def select_instances_to_remove( + instances: List[Moonraker], +) -> Union[List[Moonraker], None]: + print_instance_overview(instances, True, True) + + options = [str(i) for i in range(len(instances))] + options.extend(["a", "A", "b", "B"]) + + selection = get_selection_input("Select Moonraker instance to remove", options) + + instances_to_remove = [] + if selection == "b".lower(): + return None + elif selection == "a".lower(): + instances_to_remove.extend(instances) + else: + instance = instances[int(selection)] + instances_to_remove.append(instance) + + return instances_to_remove + + +def remove_instances( + instance_manager: InstanceManager, + instance_list: List[Moonraker], +) -> None: + for instance in instance_list: + Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...") + instance_manager.current_instance = instance + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance() + + instance_manager.reload_daemon() + + +def remove_moonraker_dir() -> None: + if not MOONRAKER_DIR.exists(): + Logger.print_info(f"'{MOONRAKER_DIR}' does not exist. Skipped ...") + return + + try: + shutil.rmtree(MOONRAKER_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{MOONRAKER_DIR}':\n{e}") + + +def remove_moonraker_env() -> None: + if not MOONRAKER_ENV_DIR.exists(): + Logger.print_info(f"'{MOONRAKER_ENV_DIR}' does not exist. Skipped ...") + return + + try: + shutil.rmtree(MOONRAKER_ENV_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{MOONRAKER_ENV_DIR}':\n{e}") + + +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: + command = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"] + subprocess.run( + command, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, check=True + ) + except subprocess.CalledProcessError as e: + Logger.print_error(f"Error while removing policykit rules: {e}") + + Logger.print_ok("Policykit rules successfully removed!") + + +def delete_moonraker_logs(instances: List[Moonraker]) -> None: + all_logfiles = [] + for instance in instances: + all_logfiles = list(instance.log_dir.glob("moonraker.log*")) + if not all_logfiles: + Logger.print_info("No Moonraker logs found. Skipped ...") + return + + for log in all_logfiles: + Logger.print_status(f"Remove '{log}'") + remove_file(log) diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index a883071..3d16a91 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -53,53 +53,32 @@ from kiauh.utils.system_utils import ( ) -def run_moonraker_setup(install: bool) -> None: +def check_moonraker_install_requirements() -> bool: kl_im = InstanceManager(Klipper) kl_instance_list = kl_im.instances kl_instance_count = len(kl_instance_list) - mr_im = InstanceManager(Moonraker) - mr_instance_list = mr_im.instances - mr_instance_count = len(mr_instance_list) if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7): Logger.print_error("Versioncheck failed!") Logger.print_error("Python 3.7 or newer required to run Moonraker.") - return + return False is_klipper_installed = kl_instance_count > 0 - if install and not is_klipper_installed: + if not is_klipper_installed: Logger.print_warn("Klipper not installed!") Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") + return False + + +def install_moonraker() -> None: + if not check_moonraker_install_requirements(): return - is_moonraker_installed = mr_instance_count > 0 - if not install and not is_moonraker_installed: - Logger.print_warn("Moonraker not installed!") - return + kl_im = InstanceManager(Klipper) + klipper_instances = kl_im.instances + mr_im = InstanceManager(Moonraker) + moonraker_instances = mr_im.instances - if install: - install_moonraker(mr_im, mr_instance_list, kl_instance_list) - - if not install: - remove_moonraker(mr_im, mr_instance_list) - - -def handle_existing_instances(instance_list: List[Klipper]) -> bool: - instance_count = len(instance_list) - - if instance_count > 0: - print_instance_overview(instance_list) - if not get_confirm("Add new instances?", allow_go_back=True): - return False - - return True - - -def install_moonraker( - instance_manager: InstanceManager, - moonraker_instances: List[Moonraker], - klipper_instances: List[Klipper], -) -> None: selected_klipper_instance = 0 if len(klipper_instances) > 1: print_moonraker_overview( @@ -136,16 +115,16 @@ def install_moonraker( for name in instance_names: current_instance = Moonraker(suffix=name) - instance_manager.current_instance = current_instance - instance_manager.create_instance() - instance_manager.enable_instance() + mr_im.current_instance = current_instance + mr_im.create_instance() + mr_im.enable_instance() if create_example_cfg: create_example_moonraker_conf(current_instance, used_ports_map) - instance_manager.start_instance() + mr_im.start_instance() - instance_manager.reload_daemon() + mr_im.reload_daemon() def setup_moonraker_prerequesites() -> None: @@ -202,84 +181,15 @@ def install_moonraker_polkit() -> None: Logger.print_error(log) -def remove_moonraker( - instance_manager: InstanceManager, instance_list: List[Moonraker] -) -> None: - print_instance_overview(instance_list, True, True) +def handle_existing_instances(instance_list: List[Klipper]) -> bool: + instance_count = len(instance_list) - options = [str(i) for i in range(len(instance_list))] - options.extend(["a", "A", "b", "B"]) + if instance_count > 0: + print_instance_overview(instance_list) + if not get_confirm("Add new instances?", allow_go_back=True): + return False - selection = get_selection_input("Select Moonraker instance to remove", options) - - del_remnants = False - remove_polkit = False - instances_to_remove = [] - if selection == "b".lower(): - return - elif selection == "a".lower(): - question = f"Delete {MOONRAKER_DIR} and {MOONRAKER_ENV_DIR}?" - del_remnants = get_confirm(question, False, True) - instances_to_remove.extend(instance_list) - remove_polkit = True - Logger.print_status("Removing all Moonraker instances ...") - else: - instance = instance_list[int(selection)] - instance_name = instance.get_service_file_name() - instances_to_remove.append(instance) - is_last_instance = len(instance_list) == 1 - if is_last_instance: - question = f"Delete {MOONRAKER_DIR} and {MOONRAKER_ENV_DIR}?" - del_remnants = get_confirm(question, False, True) - remove_polkit = True - Logger.print_status(f"Removing Moonraker instance {instance_name} ...") - - if del_remnants is None: - Logger.print_status("Exiting Moonraker Uninstaller ...") - return - - remove_instances( - instance_manager, - instances_to_remove, - remove_polkit, - del_remnants, - ) - - -def remove_instances( - instance_manager: InstanceManager, - instance_list: List[Moonraker], - remove_polkit: bool, - del_remnants: bool, -) -> None: - for instance in instance_list: - instance_manager.current_instance = instance - instance_manager.stop_instance() - instance_manager.disable_instance() - instance_manager.delete_instance(del_remnants=del_remnants) - - if remove_polkit: - remove_polkit_rules() - - instance_manager.reload_daemon() - - -def remove_polkit_rules() -> None: - Logger.print_status("Removing all Moonraker policykit rules ...") - if not MOONRAKER_DIR.exists(): - log = "Cannot remove policykit rules. Moonraker directory not found." - Logger.print_warn(log) - return - - try: - command = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"] - subprocess.run( - command, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, check=True - ) - except subprocess.CalledProcessError as e: - Logger.print_error(f"Error while removing policykit rules: {e}") - - Logger.print_ok("Policykit rules successfully removed!") + return True def update_moonraker() -> None: From 142b4498a35a51186694b9aaa4beec3f7f6afa3c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 15:35:13 +0100 Subject: [PATCH 069/296] refactor(Klipper): rework remove process Signed-off-by: Dominik Willner --- kiauh/core/menus/install_menu.py | 2 +- kiauh/core/menus/remove_menu.py | 7 +- kiauh/modules/klipper/klipper.py | 19 +-- kiauh/modules/klipper/klipper_remove.py | 133 +++++++++++++++++ kiauh/modules/klipper/klipper_setup.py | 138 ++++-------------- kiauh/modules/klipper/menus/__init__.py | 0 .../klipper/menus/klipper_remove_menu.py | 109 ++++++++++++++ 7 files changed, 273 insertions(+), 135 deletions(-) create mode 100644 kiauh/modules/klipper/klipper_remove.py create mode 100644 kiauh/modules/klipper/menus/__init__.py create mode 100644 kiauh/modules/klipper/menus/klipper_remove_menu.py diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index efb1650..45ecccb 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -64,7 +64,7 @@ class InstallMenu(BaseMenu): print(menu, end="") def install_klipper(self): - klipper_setup.run_klipper_setup(install=True) + klipper_setup.install_klipper() def install_moonraker(self): moonraker_setup.install_moonraker() diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 5526406..922c2cf 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -13,7 +13,7 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.modules.klipper import klipper_setup +from kiauh.modules.klipper.menus.klipper_remove_menu import KlipperRemoveMenu from kiauh.modules.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu from kiauh.modules.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu from kiauh.utils.constants import COLOR_RED, RESET_FORMAT @@ -25,7 +25,7 @@ class RemoveMenu(BaseMenu): super().__init__( header=True, options={ - 1: self.remove_klipper, + 1: KlipperRemoveMenu, 2: MoonrakerRemoveMenu, 3: MainsailRemoveMenu, 5: self.remove_fluidd, @@ -69,9 +69,6 @@ class RemoveMenu(BaseMenu): )[1:] print(menu, end="") - def remove_klipper(self): - klipper_setup.run_klipper_setup(install=False) - def remove_fluidd(self): print("remove_fluidd") diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index ef064a8..71eb435 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -75,7 +75,7 @@ class Klipper(BaseInstance): Logger.print_error(f"Error creating env file {env_file_target}: {e}") raise - def delete(self, del_remnants: bool) -> None: + def delete(self) -> None: service_file = self.get_service_file_name(extension=True) service_file_path = self.get_service_file_path() @@ -89,9 +89,6 @@ class Klipper(BaseInstance): Logger.print_error(f"Error deleting service file: {e}") raise - if del_remnants: - self._delete_klipper_remnants() - def write_service_file( self, service_template_path: Path, @@ -118,20 +115,6 @@ class Klipper(BaseInstance): env_file.write(env_file_content) Logger.print_ok(f"Env file created: {env_file_target}") - def _delete_klipper_remnants(self) -> None: - try: - Logger.print_status(f"Delete {self.klipper_dir} ...") - shutil.rmtree(Path(self.klipper_dir)) - Logger.print_status(f"Delete {self.env_dir} ...") - shutil.rmtree(Path(self.env_dir)) - except FileNotFoundError: - Logger.print_status("Cannot delete Klipper directories. Not found.") - except PermissionError as e: - Logger.print_error(f"Error deleting Klipper directories: {e}") - raise - - Logger.print_ok("Directories successfully deleted.") - def _prep_service_file( self, service_template_path: Path, env_file_path: Path ) -> str: diff --git a/kiauh/modules/klipper/klipper_remove.py b/kiauh/modules/klipper/klipper_remove.py new file mode 100644 index 0000000..07f2fe9 --- /dev/null +++ b/kiauh/modules/klipper/klipper_remove.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import shutil +from typing import List, Union + +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.klipper.klipper_dialogs import print_instance_overview +from kiauh.utils.filesystem_utils import remove_file +from kiauh.utils.input_utils import get_selection_input +from kiauh.utils.logger import Logger + + +def run_klipper_removal( + remove_service: bool, + remove_dir: bool, + remove_env: bool, + delete_logs: bool, +) -> None: + im = InstanceManager(Klipper) + + if remove_service: + Logger.print_status("Removing Klipper instances ...") + if im.instances: + instances_to_remove = select_instances_to_remove(im.instances) + remove_instances(im, instances_to_remove) + else: + Logger.print_info("No Klipper Services installed! Skipped ...") + + im.find_instances() + if (remove_dir or remove_env) and im.instances: + Logger.print_warn("There are still other Klipper services installed!") + Logger.print_warn("Therefor the following parts cannot be removed:") + Logger.print_warn( + """ + ● Klipper local repository + ● Klipper Python environment + """, + False, + ) + else: + if remove_dir: + Logger.print_status("Removing Klipper local repository ...") + remove_klipper_dir() + if remove_env: + Logger.print_status("Removing Klipper Python environment ...") + remove_klipper_env() + + # delete klipper logs of all instances + if delete_logs: + Logger.print_status("Removing all Klipper logs ...") + delete_klipper_logs(im.instances) + + +def select_instances_to_remove( + instances: List[Klipper], +) -> Union[List[Klipper], None]: + print_instance_overview(instances, True, True) + + options = [str(i) for i in range(len(instances))] + options.extend(["a", "A", "b", "B"]) + + selection = get_selection_input("Select Klipper instance to remove", options) + + instances_to_remove = [] + if selection == "b".lower(): + return None + elif selection == "a".lower(): + instances_to_remove.extend(instances) + else: + instance = instances[int(selection)] + instances_to_remove.append(instance) + + return instances_to_remove + + +def remove_instances( + instance_manager: InstanceManager, + instance_list: List[Klipper], +) -> None: + for instance in instance_list: + Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...") + instance_manager.current_instance = instance + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance() + + instance_manager.reload_daemon() + + +def remove_klipper_dir() -> None: + if not KLIPPER_DIR.exists(): + Logger.print_info(f"'{KLIPPER_DIR}' does not exist. Skipped ...") + return + + try: + shutil.rmtree(KLIPPER_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{KLIPPER_DIR}':\n{e}") + + +def remove_klipper_env() -> None: + if not KLIPPER_ENV_DIR.exists(): + Logger.print_info(f"'{KLIPPER_ENV_DIR}' does not exist. Skipped ...") + return + + try: + shutil.rmtree(KLIPPER_ENV_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{KLIPPER_ENV_DIR}':\n{e}") + + +def delete_klipper_logs(instances: List[Klipper]) -> None: + all_logfiles = [] + for instance in instances: + all_logfiles = list(instance.log_dir.glob("klippy.log*")) + if not all_logfiles: + Logger.print_info("No Klipper logs found. Skipped ...") + return + + for log in all_logfiles: + Logger.print_status(f"Remove '{log}'") + remove_file(log) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 6d6fd66..efa78e5 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -9,7 +9,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import subprocess from pathlib import Path from typing import List, Union @@ -40,11 +39,7 @@ from kiauh.modules.klipper.klipper_utils import ( create_example_printer_cfg, ) from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.utils.input_utils import ( - get_confirm, - get_number_input, - get_selection_input, -) +from kiauh.utils.input_utils import get_confirm, get_number_input from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( parse_packages_from_file, @@ -55,85 +50,52 @@ from kiauh.utils.system_utils import ( ) -def run_klipper_setup(install: bool) -> None: - instance_manager = InstanceManager(Klipper) - instance_list = instance_manager.instances - instances_installed = len(instance_list) +def install_klipper() -> None: + im = InstanceManager(Klipper) - is_klipper_installed = instances_installed > 0 - if not install and not is_klipper_installed: - Logger.print_warn("Klipper not installed!") + add_additional = handle_existing_instances(im.instances) + if len(im.instances) > 0 and not add_additional: + Logger.print_status(EXIT_KLIPPER_SETUP) return - if install: - add_additional = handle_existing_instances(instance_list) - if is_klipper_installed and not add_additional: - Logger.print_status(EXIT_KLIPPER_SETUP) - return - - install_klipper(instance_manager, instance_list) - - if not install: - if instances_installed == 1: - remove_single_instance(instance_manager, instance_list) - else: - remove_multi_instance(instance_manager, instance_list) - - -def handle_existing_instances(instance_list: List[Klipper]) -> bool: - instance_count = len(instance_list) - - if instance_count > 0: - print_instance_overview(instance_list) - if not get_confirm("Add new instances?", allow_go_back=True): - return False - - return True - - -def install_klipper( - instance_manager: InstanceManager, instance_list: List[Klipper] -) -> None: print_select_instance_count_dialog() - question = f"Number of{' additional' if len(instance_list) > 0 else ''} Klipper instances to set up" + question = f"Number of{' additional' if len(im.instances) > 0 else ''} Klipper instances to set up" install_count = get_number_input(question, 1, default=1, allow_go_back=True) if install_count is None: Logger.print_status(EXIT_KLIPPER_SETUP) return - instance_names = set_instance_suffix(instance_list, install_count) + instance_names = set_instance_suffix(im.instances, install_count) if instance_names is None: Logger.print_status(EXIT_KLIPPER_SETUP) return create_example_cfg = get_confirm("Create example printer.cfg?") - if len(instance_list) < 1: + if len(im.instances) < 1: setup_klipper_prerequesites() convert_single_to_multi = ( - len(instance_list) == 1 - and instance_list[0].suffix is None - and install_count >= 1 + len(im.instances) == 1 and im.instances[0].suffix is None and install_count >= 1 ) for name in instance_names: if convert_single_to_multi: - current_instance = handle_single_to_multi_conversion(instance_manager, name) + current_instance = handle_single_to_multi_conversion(im, name) convert_single_to_multi = False else: current_instance = Klipper(suffix=name) - instance_manager.current_instance = current_instance - instance_manager.create_instance() - instance_manager.enable_instance() + im.current_instance = current_instance + im.create_instance() + im.enable_instance() if create_example_cfg: create_example_printer_cfg(current_instance) - instance_manager.start_instance() + im.start_instance() - instance_manager.reload_daemon() + im.reload_daemon() # step 4: check/handle conflicting packages/services handle_disruptive_system_packages() @@ -175,6 +137,17 @@ def install_klipper_packages(klipper_dir: Path) -> None: install_system_packages(packages) +def handle_existing_instances(instance_list: List[Klipper]) -> bool: + instance_count = len(instance_list) + + if instance_count > 0: + print_instance_overview(instance_list) + if not get_confirm("Add new instances?", allow_go_back=True): + return False + + return True + + def set_instance_suffix( instance_list: List[Klipper], install_count: int ) -> List[Union[str, None]]: @@ -199,63 +172,6 @@ def set_instance_suffix( ) -def remove_single_instance( - instance_manager: InstanceManager, instance_list: List[Klipper] -) -> None: - question = f"Delete {KLIPPER_DIR} and {KLIPPER_ENV_DIR}?" - del_remnants = get_confirm(question, allow_go_back=True) - if del_remnants is None: - Logger.print_status("Exiting Klipper Uninstaller ...") - return - - try: - instance_manager.current_instance = instance_list[0] - instance_manager.stop_instance() - instance_manager.disable_instance() - instance_manager.delete_instance(del_remnants=del_remnants) - instance_manager.reload_daemon() - except (OSError, subprocess.CalledProcessError): - Logger.print_error("Removing instance failed!") - return - - -def remove_multi_instance( - instance_manager: InstanceManager, instance_list: List[Klipper] -) -> None: - print_instance_overview(instance_list, show_index=True, show_select_all=True) - - options = [str(i) for i in range(len(instance_list))] - options.extend(["a", "A", "b", "B"]) - - selection = get_selection_input("Select Klipper instance to remove", options) - - if selection == "b".lower(): - return - elif selection == "a".lower(): - question = f"Delete {KLIPPER_DIR} and {KLIPPER_ENV_DIR}?" - del_remnants = get_confirm(question, allow_go_back=True) - if del_remnants is None: - Logger.print_status("Exiting Klipper Uninstaller ...") - return - - Logger.print_status("Removing all Klipper instances ...") - for instance in instance_list: - instance_manager.current_instance = instance - instance_manager.stop_instance() - instance_manager.disable_instance() - instance_manager.delete_instance(del_remnants=del_remnants) - else: - instance = instance_list[int(selection)] - log = f"Removing Klipper instance: {instance.get_service_file_name()}" - Logger.print_status(log) - instance_manager.current_instance = instance - instance_manager.stop_instance() - instance_manager.disable_instance() - instance_manager.delete_instance(del_remnants=False) - - instance_manager.reload_daemon() - - def update_klipper() -> None: print_update_warn_dialog() if not get_confirm("Update Klipper now?"): diff --git a/kiauh/modules/klipper/menus/__init__.py b/kiauh/modules/klipper/menus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/modules/klipper/menus/klipper_remove_menu.py b/kiauh/modules/klipper/menus/klipper_remove_menu.py new file mode 100644 index 0000000..ed9740f --- /dev/null +++ b/kiauh/modules/klipper/menus/klipper_remove_menu.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2023 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 # +# ======================================================================= # + +import textwrap + +from kiauh.core.menus import BACK_HELP_FOOTER +from kiauh.core.menus.base_menu import BaseMenu +from kiauh.modules.klipper import klipper_remove +from kiauh.modules.moonraker import moonraker_remove +from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN + + +class KlipperRemoveMenu(BaseMenu): + def __init__(self): + super().__init__( + header=False, + options={ + 0: self.toggle_all, + 1: self.toggle_remove_klipper_service, + 2: self.toggle_remove_klipper_dir, + 3: self.toggle_remove_klipper_env, + 4: self.toggle_delete_klipper_logs, + 5: self.run_removal_process, + }, + footer_type=BACK_HELP_FOOTER, + ) + self.remove_klipper_service = False + self.remove_klipper_dir = False + self.remove_klipper_env = False + self.delete_klipper_logs = False + + def print_menu(self) -> None: + header = " [ Remove Klipper ] " + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" + unchecked = "[ ]" + o1 = checked if self.remove_klipper_service else unchecked + o2 = checked if self.remove_klipper_dir else unchecked + o3 = checked if self.remove_klipper_env else unchecked + o4 = checked if self.delete_klipper_logs else unchecked + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Enter a number and hit enter to select / deselect | + | the specific option for removal. | + |-------------------------------------------------------| + | 0) Select everything | + |-------------------------------------------------------| + | 1) {o1} Remove Service | + | 2) {o2} Remove Local Repository | + | 3) {o3} Remove Python Environment | + | 4) {o4} Delete all Log-Files | + |-------------------------------------------------------| + | 5) Continue | + """ + )[1:] + print(menu, end="") + + def toggle_all(self) -> None: + self.remove_klipper_service = True + self.remove_klipper_dir = True + self.remove_klipper_env = True + self.delete_klipper_logs = True + + def toggle_remove_klipper_service(self) -> None: + self.remove_klipper_service = not self.remove_klipper_service + + def toggle_remove_klipper_dir(self) -> None: + self.remove_klipper_dir = not self.remove_klipper_dir + + def toggle_remove_klipper_env(self) -> None: + self.remove_klipper_env = not self.remove_klipper_env + + def toggle_delete_klipper_logs(self) -> None: + self.delete_klipper_logs = not self.delete_klipper_logs + + def run_removal_process(self) -> None: + if ( + not self.remove_klipper_service + and not self.remove_klipper_dir + and not self.remove_klipper_env + and not self.delete_klipper_logs + ): + error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" + print(error) + return + + klipper_remove.run_klipper_removal( + self.remove_klipper_service, + self.remove_klipper_dir, + self.remove_klipper_env, + self.delete_klipper_logs, + ) + + self.remove_klipper_service = False + self.remove_klipper_dir = False + self.remove_klipper_env = False + self.delete_klipper_logs = False From fc9fa39eeec3dfdfc6075d9314b7c3eb2531445a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 19:43:30 +0100 Subject: [PATCH 070/296] refactor(Mainsail): use same wording in MainsailRemoveMenu Signed-off-by: Dominik Willner --- kiauh/modules/mainsail/menus/mainsail_remove_menu.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kiauh/modules/mainsail/menus/mainsail_remove_menu.py b/kiauh/modules/mainsail/menus/mainsail_remove_menu.py index fd56a60..071a818 100644 --- a/kiauh/modules/mainsail/menus/mainsail_remove_menu.py +++ b/kiauh/modules/mainsail/menus/mainsail_remove_menu.py @@ -57,17 +57,17 @@ class MainsailRemoveMenu(BaseMenu): | Enter a number and hit enter to select / deselect | | the specific option for removal. | |-------------------------------------------------------| - | 0) Everything | + | 0) Select everything | |-------------------------------------------------------| - | 1) {o1} Remove Mainsail Web UI | + | 1) {o1} Remove Mainsail | | 2) {o2} Remove mainsail-config | - | 3) {o3} Backup Mainsail config.json | + | 3) {o3} Backup config.json | | | | printer.cfg & moonraker.conf | - | 4) {o4} Remove Moonraker updater section | + | 4) {o4} Remove Moonraker update section | | 5) {o5} Remove printer.cfg include | |-------------------------------------------------------| - | 6) Start removal | + | 6) Continue | """ )[1:] print(menu, end="") From b69ecbc9b5a28283fb5966c995e48735ef98b20d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 19:56:43 +0100 Subject: [PATCH 071/296] fix(KIAUH): wrong logic in status detection Signed-off-by: Dominik Willner --- kiauh/utils/common.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 8cb20c8..13ab2c4 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -111,10 +111,12 @@ def get_install_status_webui( nginx_cfg_exist = check_file_exist(nginx_cfg) upstreams_cfg_exist = check_file_exist(upstreams_cfg) common_cfg_exist = check_file_exist(common_cfg) - status = [dir_exist, nginx_cfg_exist, upstreams_cfg_exist, common_cfg_exist] - if all(status): + status = [dir_exist, nginx_cfg_exist] + general_nginx_status = [upstreams_cfg_exist, common_cfg_exist] + + if all(status) and all(general_nginx_status): return f"{COLOR_GREEN}Installed!{RESET_FORMAT}" - elif not any(status): + elif not all(status): return f"{COLOR_RED}Not installed!{RESET_FORMAT}" else: return f"{COLOR_YELLOW}Incomplete!{RESET_FORMAT}" From b4f5c3c1ace8635c002b09be8fcaf1c4b225159d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 20:16:38 +0100 Subject: [PATCH 072/296] refactor(Mainsail): remove mainsail.zip after extracting content Signed-off-by: Dominik Willner --- kiauh/modules/mainsail/mainsail_setup.py | 4 +++- kiauh/utils/system_utils.py | 12 ++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index 758c39a..cb0e85e 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -147,11 +147,13 @@ def run_mainsail_installation() -> None: def download_mainsail() -> None: try: Logger.print_status("Downloading Mainsail ...") - download_file(MAINSAIL_URL, Path.home(), "mainsail.zip") + target = Path.home().joinpath("mainsail.zip") + download_file(MAINSAIL_URL, target, True) Logger.print_ok("Download complete!") Logger.print_status("Extracting mainsail.zip ...") unzip(Path.home().joinpath("mainsail.zip"), MAINSAIL_DIR) + target.unlink(missing_ok=True) Logger.print_ok("OK!") except Exception: diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index be317ea..5d7292e 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -246,24 +246,20 @@ def get_ipv4_addr() -> str: s.close() -def download_file( - url: str, target_folder: Path, target_name: str, show_progress=True -) -> None: +def download_file(url: str, target: Path, show_progress=True) -> None: """ Helper method for downloading files from a provided URL | :param url: the url to the file - :param target_folder: the target folder to download the file into - :param target_name: the name of the downloaded file + :param target: the target path incl filename :param show_progress: show download progress or not :return: None """ - target_path = target_folder.joinpath(target_name) try: if show_progress: - urllib.request.urlretrieve(url, target_path, download_progress) + urllib.request.urlretrieve(url, target, download_progress) sys.stdout.write("\n") else: - urllib.request.urlretrieve(url, target_path) + urllib.request.urlretrieve(url, target) except urllib.error.HTTPError as e: Logger.print_error(f"Download failed! HTTP error occured: {e}") raise From 6c59d58193503f86526e06dad647dd2d28a419cc Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 20:44:04 +0100 Subject: [PATCH 073/296] refactor(KIAUH): use red dash instead of "Unknown" if repo info not available Signed-off-by: Dominik Willner --- kiauh/utils/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 13ab2c4..aebbaf3 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -68,7 +68,7 @@ def get_repo_name(repo_dir: Path) -> str: result = "/".join(result.decode().strip().split("/")[-2:]) return f"{COLOR_CYAN}{result}{RESET_FORMAT}" except subprocess.CalledProcessError: - return f"{COLOR_YELLOW}Unknown{RESET_FORMAT}" + return f"{COLOR_RED}-{RESET_FORMAT}" def get_install_status_common( From b165d888559daad963d508c5a1660df1563d7a77 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 20:57:22 +0100 Subject: [PATCH 074/296] fix(Moonraker): missing return statement if all requirements met Signed-off-by: Dominik Willner --- kiauh/modules/moonraker/moonraker_setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index 3d16a91..430e157 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -69,6 +69,8 @@ def check_moonraker_install_requirements() -> bool: Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") return False + return True + def install_moonraker() -> None: if not check_moonraker_install_requirements(): From a9367cc0640ad1557e9196e8b49b7b422c3af2c5 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 22:01:18 +0100 Subject: [PATCH 075/296] fix(Klipper): remove obsolete method parameter Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index bd929c6..14e6361 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -110,7 +110,7 @@ def handle_single_to_multi_conversion( old_data_dir_name = instance_manager.instances[0].data_dir instance_manager.stop_instance() instance_manager.disable_instance() - instance_manager.delete_instance(del_remnants=False) + instance_manager.delete_instance() instance_manager.current_instance = Klipper(suffix=name) new_data_dir_name = instance_manager.current_instance.data_dir try: From 16a28ffda03af7dcef3ed80ee461884ede2bb95d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 22:03:44 +0100 Subject: [PATCH 076/296] fix(Klipper/Moonraker): config files now always have a Path, are never None anymore Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper.py | 8 +------- kiauh/modules/moonraker/moonraker.py | 10 ++-------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 71eb435..1f11fa2 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -30,7 +30,7 @@ class Klipper(BaseInstance): super().__init__(instance_type=self, suffix=suffix) self.klipper_dir: Path = KLIPPER_DIR self.env_dir: Path = KLIPPER_ENV_DIR - self._cfg_file = self._get_cfg() + self._cfg_file = self.cfg_dir.joinpath("printer.cfg") self._log = self.log_dir.joinpath("klippy.log") self._serial = self.comms_dir.joinpath("klippy.serial") self._uds = self.comms_dir.joinpath("klippy.sock") @@ -153,9 +153,3 @@ class Klipper(BaseInstance): env_file_content = env_file_content.replace("%LOG%", str(self.log)) env_file_content = env_file_content.replace("%UDS%", str(self.uds)) return env_file_content - - def _get_cfg(self) -> Union[Path, None]: - cfg_file_loc = self.cfg_dir.joinpath("printer.cfg") - if cfg_file_loc.is_file(): - return cfg_file_loc - return None diff --git a/kiauh/modules/moonraker/moonraker.py b/kiauh/modules/moonraker/moonraker.py index 4a1ae12..78d60c7 100644 --- a/kiauh/modules/moonraker/moonraker.py +++ b/kiauh/modules/moonraker/moonraker.py @@ -31,7 +31,7 @@ class Moonraker(BaseInstance): super().__init__(instance_type=self, suffix=suffix) self.moonraker_dir: Path = MOONRAKER_DIR self.env_dir: Path = MOONRAKER_ENV_DIR - self.cfg_file = self._get_cfg() + self.cfg_file = self.cfg_dir.joinpath("moonraker.conf") self.port = self._get_port() self.backup_dir = self.data_dir.joinpath("backup") self.certs_dir = self.data_dir.joinpath("certs") @@ -138,14 +138,8 @@ class Moonraker(BaseInstance): ) return env_file_content - def _get_cfg(self) -> Union[Path, None]: - cfg_file_loc = self.cfg_dir.joinpath("moonraker.conf") - if cfg_file_loc.is_file(): - return cfg_file_loc - return None - def _get_port(self) -> Union[int, None]: - if self.cfg_file is None: + if not self.cfg_file.is_file(): return None cm = ConfigManager(cfg_file=self.cfg_file) From d20d82aeacf3ad2124fde3d0e59a2d6e28fa49fe Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 22:06:40 +0100 Subject: [PATCH 077/296] fix(Mainsail): proper check if config exists Signed-off-by: Dominik Willner --- kiauh/modules/mainsail/mainsail_remove.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/kiauh/modules/mainsail/mainsail_remove.py b/kiauh/modules/mainsail/mainsail_remove.py index 275ce1f..4785f27 100644 --- a/kiauh/modules/mainsail/mainsail_remove.py +++ b/kiauh/modules/mainsail/mainsail_remove.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import os # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # @@ -94,16 +95,16 @@ def remove_nginx_logs() -> None: def remove_updater_section(name: str) -> None: Logger.print_status("Remove updater section from moonraker.conf ...") im = InstanceManager(Moonraker) - if not im.instances: - Logger.print_info("Moonraker not installed. Skipping ...") + instances: List[Moonraker] = im.instances + if not instances: + Logger.print_info("Moonraker not installed. Skipped ...") return - for instance in im.instances: - log = f"Remove section '{name}' in '{instance.cfg_file}' ..." - Logger.print_status(log) + for instance in instances: + Logger.print_status(f"Remove section '{name}' in '{instance.cfg_file}' ...") - if not Path(instance.cfg_file).exists(): - Logger.print_info("Section not present. Skipping ...") + if not instance.cfg_file.is_file(): + Logger.print_info(f"'{instance.cfg_file}' does not exist. Skipped ...") continue cm = ConfigManager(instance.cfg_file) From 1b4c76d080dcebd9d499d31b241c65d385ed20fc Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 25 Dec 2023 22:31:18 +0100 Subject: [PATCH 078/296] fix(KIAUH): more file path handling improvements Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_utils.py | 6 ++--- kiauh/modules/mainsail/mainsail_remove.py | 30 ++++++++++++---------- kiauh/modules/moonraker/moonraker_utils.py | 6 ++--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 14e6361..baf1b3a 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -205,12 +205,12 @@ def get_highest_index(instance_list: List[Klipper]) -> int: def create_example_printer_cfg(instance: Klipper) -> None: Logger.print_status(f"Creating example printer.cfg in '{instance.cfg_dir}'") - if instance.cfg_file is not None: - Logger.print_info(f"printer.cfg in '{instance.cfg_dir}' already exists.") + if instance.cfg_file.is_file(): + Logger.print_info(f"'{instance.cfg_file}' already exists.") return source = MODULE_PATH.joinpath("res/printer.cfg") - target = instance.cfg_dir.joinpath("printer.cfg") + target = instance.cfg_file try: shutil.copy(source, target) except OSError as e: diff --git a/kiauh/modules/mainsail/mainsail_remove.py b/kiauh/modules/mainsail/mainsail_remove.py index 4785f27..162e77f 100644 --- a/kiauh/modules/mainsail/mainsail_remove.py +++ b/kiauh/modules/mainsail/mainsail_remove.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import os # ======================================================================= # # Copyright (C) 2020 - 2023 Dominik Willner # @@ -53,7 +52,7 @@ def run_mainsail_removal( def remove_mainsail_dir() -> None: Logger.print_status("Removing Mainsail ...") - if not Path(MAINSAIL_DIR).exists(): + if not MAINSAIL_DIR.exists(): Logger.print_info(f"'{MAINSAIL_DIR}' does not exist. Skipping ...") return @@ -81,12 +80,13 @@ def remove_nginx_logs() -> None: remove_file(Path("/var/log/nginx/mainsail-error.log"), True) im = InstanceManager(Klipper) - if not im.instances: + instances: List[Klipper] = im.instances + if not instances: return - for instance in im.instances: - remove_file(Path(instance.log_dir, "mainsail-access.log")) - remove_file(Path(instance.log_dir, "mainsail-error.log")) + for instance in instances: + remove_file(instance.log_dir.joinpath("mainsail-access.log")) + remove_file(instance.log_dir.joinpath("mainsail-error.log")) except (OSError, subprocess.CalledProcessError) as e: Logger.print_error(f"Unable to NGINX logs:\n{e}") @@ -118,7 +118,7 @@ def remove_updater_section(name: str) -> None: def remove_mainsail_cfg_dir() -> None: Logger.print_status("Removing mainsail-config ...") - if not Path(MAINSAIL_CONFIG_DIR).exists(): + if not MAINSAIL_CONFIG_DIR.exists(): Logger.print_info(f"'{MAINSAIL_CONFIG_DIR}' does not exist. Skipping ...") return @@ -130,11 +130,12 @@ def remove_mainsail_cfg_dir() -> None: def remove_mainsail_cfg_symlink() -> None: Logger.print_status("Removing mainsail.cfg symlinks ...") - im = InstanceManager(Moonraker) - for instance in im.instances: - Logger.print_status(f"Removing symlink from '{instance.cfg_dir}' ...") + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + for instance in instances: + Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") try: - remove_file(Path(instance.cfg_dir, "mainsail.cfg")) + remove_file(instance.cfg_dir.joinpath("mainsail.cfg")) except subprocess.CalledProcessError: Logger.print_error("Failed to remove symlink!") @@ -142,15 +143,16 @@ def remove_mainsail_cfg_symlink() -> None: def remove_printer_cfg_include() -> None: Logger.print_status("Remove mainsail-config include from printer.cfg ...") im = InstanceManager(Klipper) - if not im.instances: + instances: List[Klipper] = im.instances + if not instances: Logger.print_info("Klipper not installed. Skipping ...") return - for instance in im.instances: + for instance in instances: log = f"Removing include from '{instance.cfg_file}' ..." Logger.print_status(log) - if not Path(instance.cfg_file).exists(): + if not instance.cfg_file.is_file(): continue cm = ConfigManager(instance.cfg_file) diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/modules/moonraker/moonraker_utils.py index 01ee7d7..b9c8981 100644 --- a/kiauh/modules/moonraker/moonraker_utils.py +++ b/kiauh/modules/moonraker/moonraker_utils.py @@ -40,12 +40,12 @@ def create_example_moonraker_conf( instance: Moonraker, ports_map: Dict[str, int] ) -> None: Logger.print_status(f"Creating example moonraker.conf in '{instance.cfg_dir}'") - if instance.cfg_file is not None: - Logger.print_info(f"moonraker.conf in '{instance.cfg_dir}' already exists.") + if instance.cfg_file.is_file(): + Logger.print_info(f"'{instance.cfg_file}' already exists.") return source = MODULE_PATH.joinpath("res/moonraker.conf") - target = instance.cfg_dir.joinpath("moonraker.conf") + target = instance.cfg_file try: shutil.copy(source, target) except OSError as e: From 9dedf38079c6ef45dfe2536334677c0e8edc458f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Tue, 26 Dec 2023 23:37:35 +0100 Subject: [PATCH 079/296] refactor(KIAUH): big refactor of instance handling Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/base_instance.py | 10 +- .../core/instance_manager/instance_manager.py | 6 +- kiauh/core/instance_manager/name_scheme.py | 8 ++ kiauh/modules/klipper/klipper.py | 5 +- kiauh/modules/klipper/klipper_setup.py | 134 +++++++++--------- kiauh/modules/klipper/klipper_utils.py | 127 +++++++---------- kiauh/modules/moonraker/moonraker.py | 3 +- kiauh/modules/moonraker/moonraker_setup.py | 14 +- kiauh/modules/moonraker/moonraker_utils.py | 36 ++++- 9 files changed, 179 insertions(+), 164 deletions(-) create mode 100644 kiauh/core/instance_manager/name_scheme.py diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index bccacee..903c76b 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -11,7 +11,7 @@ from abc import abstractmethod, ABC from pathlib import Path -from typing import List, Union, Optional, Type, TypeVar +from typing import List, Type, TypeVar from kiauh.utils.constants import SYSTEMD, CURRENT_USER @@ -25,7 +25,7 @@ class BaseInstance(ABC): def __init__( self, - suffix: Optional[str], + suffix: str, instance_type: B = B, ): self._instance_type = instance_type @@ -52,7 +52,7 @@ class BaseInstance(ABC): return self._suffix @suffix.setter - def suffix(self, value: Union[str, None]) -> None: + def suffix(self, value: str) -> None: self._suffix = value @property @@ -144,7 +144,7 @@ class BaseInstance(ABC): def get_service_file_name(self, extension: bool = False) -> str: name = f"{self.__class__.__name__.lower()}" - if self.suffix is not None: + if self.suffix != "": name += f"-{self.suffix}" return name if not extension else f"{name}.service" @@ -153,7 +153,7 @@ class BaseInstance(ABC): return SYSTEMD.joinpath(self.get_service_file_name(extension=True)) def get_data_dir_name_from_suffix(self) -> str: - if self._suffix is None: + if self._suffix == "": return "printer" elif self._suffix.isdigit(): return f"printer_{self._suffix}" diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 411a25b..4a45e39 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -207,10 +207,8 @@ class InstanceManager: return instance_list - def _get_instance_suffix(self, file_path: Path) -> Union[str, None]: - full_name = file_path.name.split(".")[0] - - return full_name.split("-")[-1] if "-" in full_name else None + def _get_instance_suffix(self, file_path: Path) -> str: + return file_path.stem.split("-")[-1] if "-" in file_path.stem else "" def _sort_instance_list(self, s: Union[int, str, None]): if s is None: diff --git a/kiauh/core/instance_manager/name_scheme.py b/kiauh/core/instance_manager/name_scheme.py new file mode 100644 index 0000000..bfd9e2c --- /dev/null +++ b/kiauh/core/instance_manager/name_scheme.py @@ -0,0 +1,8 @@ +from enum import unique, Enum + + +@unique +class NameScheme(Enum): + SINGLE = "SINGLE" + INDEX = "INDEX" + CUSTOM = "CUSTOM" diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 1f11fa2..6a43452 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -9,10 +9,9 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import shutil import subprocess from pathlib import Path -from typing import List, Union +from typing import List from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH @@ -26,7 +25,7 @@ class Klipper(BaseInstance): def blacklist(cls) -> List[str]: return ["None", "mcu"] - def __init__(self, suffix: str = None): + def __init__(self, suffix: str = ""): super().__init__(instance_type=self, suffix=suffix) self.klipper_dir: Path = KLIPPER_DIR self.env_dir: Path = KLIPPER_ENV_DIR diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index efa78e5..a30c27c 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -10,12 +10,13 @@ # ======================================================================= # from pathlib import Path -from typing import List, Union +from typing import List from kiauh import KIAUH_CFG from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.instance_manager.name_scheme import NameScheme from kiauh.modules.klipper import ( EXIT_KLIPPER_SETUP, DEFAULT_KLIPPER_REPO_URL, @@ -25,20 +26,21 @@ from kiauh.modules.klipper import ( ) from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.klipper.klipper_dialogs import ( - print_instance_overview, - print_select_instance_count_dialog, print_update_warn_dialog, + print_select_custom_name_dialog, ) from kiauh.modules.klipper.klipper_utils import ( - handle_convert_single_to_multi_instance_names, - handle_new_multi_instance_names, - handle_existing_multi_instance_names, handle_disruptive_system_packages, check_user_groups, - handle_single_to_multi_conversion, + handle_to_multi_instance_conversion, create_example_printer_cfg, + detect_name_scheme, + add_to_existing, + get_install_count, + assign_custom_name, ) from kiauh.core.repo_manager.repo_manager import RepoManager +from kiauh.modules.moonraker.moonraker import Moonraker from kiauh.utils.input_utils import get_confirm, get_number_input from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( @@ -50,50 +52,81 @@ from kiauh.utils.system_utils import ( ) +# TODO: this method needs refactoring! (but it works for now) def install_klipper() -> None: im = InstanceManager(Klipper) + kl_instances: List[Klipper] = im.instances - add_additional = handle_existing_instances(im.instances) - if len(im.instances) > 0 and not add_additional: + # ask to add new instances, if there are existing ones + if kl_instances and not add_to_existing(): Logger.print_status(EXIT_KLIPPER_SETUP) return - print_select_instance_count_dialog() - question = f"Number of{' additional' if len(im.instances) > 0 else ''} Klipper instances to set up" - install_count = get_number_input(question, 1, default=1, allow_go_back=True) + install_count = get_install_count() + # install_count = None -> user entered "b" to go back if install_count is None: Logger.print_status(EXIT_KLIPPER_SETUP) return - instance_names = set_instance_suffix(im.instances, install_count) - if instance_names is None: - Logger.print_status(EXIT_KLIPPER_SETUP) - return + # create a dict of the size of the existing instances + install count + name_scheme = NameScheme.SINGLE + single_to_multi = len(kl_instances) == 1 and kl_instances[0].suffix == "" + name_dict = {c: "" for c in range(len(kl_instances) + install_count)} + + if (not kl_instances and install_count > 1) or single_to_multi: + print_select_custom_name_dialog() + if get_confirm("Assign custom names?", False, allow_go_back=True): + name_scheme = NameScheme.CUSTOM + else: + name_scheme = NameScheme.INDEX + + # if there are more moonraker instances installed than klipper, we + # load their names into the name_dict, as we will detect and enforce that naming scheme + mr_instances: List[Moonraker] = InstanceManager(Moonraker).instances + if len(mr_instances) > len(kl_instances): + for k, v in enumerate(mr_instances): + name_dict[k] = v.suffix + name_scheme = detect_name_scheme(mr_instances) + elif len(kl_instances) > 1: + for k, v in enumerate(kl_instances): + name_dict[k] = v.suffix + name_scheme = detect_name_scheme(kl_instances) + + # set instance names if multiple instances will be created + if name_scheme != NameScheme.SINGLE: + for k in name_dict: + if name_dict[k] == "" and name_scheme == NameScheme.INDEX: + name_dict[k] = str(k + 1) + elif name_dict[k] == "" and name_scheme == NameScheme.CUSTOM: + assign_custom_name(k, name_dict) create_example_cfg = get_confirm("Create example printer.cfg?") - if len(im.instances) < 1: + if not kl_instances: setup_klipper_prerequesites() - convert_single_to_multi = ( - len(im.instances) == 1 and im.instances[0].suffix is None and install_count >= 1 - ) - - for name in instance_names: - if convert_single_to_multi: - current_instance = handle_single_to_multi_conversion(im, name) - convert_single_to_multi = False + count = 0 + for name in name_dict: + if name_dict[name] in [n.suffix for n in kl_instances]: + continue else: - current_instance = Klipper(suffix=name) + count += 1 - im.current_instance = current_instance - im.create_instance() - im.enable_instance() + if single_to_multi: + handle_to_multi_instance_conversion(name_dict[name]) + single_to_multi = False + count -= 1 + else: + new_instance = Klipper(suffix=name_dict[name]) + im.current_instance = new_instance + im.create_instance() + im.enable_instance() + if create_example_cfg: + create_example_printer_cfg(new_instance) + im.start_instance() - if create_example_cfg: - create_example_printer_cfg(current_instance) - - im.start_instance() + if count == install_count: + break im.reload_daemon() @@ -137,41 +170,6 @@ def install_klipper_packages(klipper_dir: Path) -> None: install_system_packages(packages) -def handle_existing_instances(instance_list: List[Klipper]) -> bool: - instance_count = len(instance_list) - - if instance_count > 0: - print_instance_overview(instance_list) - if not get_confirm("Add new instances?", allow_go_back=True): - return False - - return True - - -def set_instance_suffix( - instance_list: List[Klipper], install_count: int -) -> List[Union[str, None]]: - instance_count = len(instance_list) - - # new single instance install - if instance_count == 0 and install_count == 1: - return [None] - - # convert single instance install to multi install - elif instance_count == 1 and install_count >= 1 and instance_list[0].suffix is None: - return handle_convert_single_to_multi_instance_names(install_count) - - # new multi instance install - elif instance_count == 0 and install_count > 1: - return handle_new_multi_instance_names(instance_count, install_count) - - # existing multi instance install - elif instance_count > 1: - return handle_existing_multi_instance_names( - instance_count, install_count, instance_list - ) - - def update_klipper() -> None: print_update_warn_dialog() if not get_confirm("Update Klipper now?"): diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index baf1b3a..b6980dc 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -20,16 +20,20 @@ from pathlib import Path from typing import List, Union, Literal, Dict from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.instance_manager.name_scheme import NameScheme from kiauh.modules.klipper import MODULE_PATH, KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.klipper.klipper_dialogs import ( print_missing_usergroup_dialog, - print_select_custom_name_dialog, + print_instance_overview, + print_select_instance_count_dialog, ) +from kiauh.modules.moonraker.moonraker_utils import moonraker_to_multi_conversion from kiauh.utils.common import get_install_status_common, get_repo_name from kiauh.utils.constants import CURRENT_USER -from kiauh.utils.input_utils import get_confirm, get_string_input +from kiauh.utils.input_utils import get_confirm, get_string_input, get_number_input from kiauh.utils.logger import Logger from kiauh.utils.system_utils import mask_system_service @@ -41,84 +45,57 @@ def get_klipper_status() -> Dict[Literal["status", "repo"], str]: } -def assign_custom_names( - instance_count: int, install_count: int, instance_list: List[Klipper] = None -) -> List[str]: - instance_names = [] - exclude = Klipper.blacklist() - - # if an instance_list is provided, exclude all existing instance suffixes - if instance_list is not None: - for instance in instance_list: - exclude.append(instance.suffix) - - for i in range(instance_count + install_count): - question = f"Enter name for instance {i + 1}" - name = get_string_input(question, exclude=exclude) - instance_names.append(name) - exclude.append(name) - - return instance_names +def add_to_existing() -> bool: + kl_instances = InstanceManager(Klipper).instances + print_instance_overview(kl_instances) + return get_confirm("Add new instances?", allow_go_back=True) -def handle_convert_single_to_multi_instance_names( - install_count: int, -) -> Union[List[str], None]: - print_select_custom_name_dialog() - choice = get_confirm("Assign custom names?", False, allow_go_back=True) - if choice is True: - # instance_count = 0 and install_count + 1 as we want to assign a new name to the existing single install - return assign_custom_names(0, install_count + 1) - elif choice is False: - # "install_count + 2" as we need to account for the existing single install - _range = range(1, install_count + 2) - return [str(i) for i in _range] - - return None +def get_install_count() -> Union[int, None]: + kl_instances = InstanceManager(Klipper).instances + print_select_instance_count_dialog() + question = f"Number of{' additional' if len(kl_instances) > 0 else ''} Klipper instances to set up" + return get_number_input(question, 1, default=1, allow_go_back=True) -def handle_new_multi_instance_names( - instance_count: int, install_count: int -) -> Union[List[str], None]: - print_select_custom_name_dialog() - choice = get_confirm("Assign custom names?", False, allow_go_back=True) - if choice is True: - return assign_custom_names(instance_count, install_count) - elif choice is False: - _range = range(1, install_count + 1) - return [str(i) for i in _range] - - return None +def assign_custom_name(key: int, name_dict: Dict[int, str]) -> None: + existing_names = [] + existing_names.extend(Klipper.blacklist()) + existing_names.extend(name_dict[n] for n in name_dict) + question = f"Enter name for instance {key + 1}" + name_dict[key] = get_string_input(question, exclude=existing_names) -def handle_existing_multi_instance_names( - instance_count: int, install_count: int, instance_list: List[Klipper] -) -> List[str]: - if has_custom_names(instance_list): - return assign_custom_names(instance_count, install_count, instance_list) +def handle_to_multi_instance_conversion(new_name: str) -> None: + Logger.print_status("Converting single instance to multi instances ...") + klipper_to_multi_conversion(new_name) + moonraker_to_multi_conversion(new_name) + + +def klipper_to_multi_conversion(new_name: str) -> None: + Logger.print_status("Convert Klipper single to multi instance ...") + im = InstanceManager(Klipper) + im.current_instance = im.instances[0] + # temporarily store the data dir path + old_data_dir = im.instances[0].data_dir + # remove the old single instance + im.stop_instance() + im.disable_instance() + im.delete_instance() + # create a new klipper instance with the new name + im.current_instance = Klipper(suffix=new_name) + new_data_dir: Path = im.current_instance.data_dir + + # rename the old data dir and use it for the new instance + Logger.print_status(f"Rename '{old_data_dir}' to '{new_data_dir}' ...") + if not new_data_dir.is_dir(): + old_data_dir.rename(new_data_dir) else: - start = get_highest_index(instance_list) + 1 - _range = range(start, start + install_count) - return [str(i) for i in _range] + Logger.print_info(f"'{new_data_dir}' already exist. Skipped ...") - -def handle_single_to_multi_conversion( - instance_manager: InstanceManager, name: str -) -> Klipper: - instance_list = instance_manager.instances - instance_manager.current_instance = instance_list[0] - old_data_dir_name = instance_manager.instances[0].data_dir - instance_manager.stop_instance() - instance_manager.disable_instance() - instance_manager.delete_instance() - instance_manager.current_instance = Klipper(suffix=name) - new_data_dir_name = instance_manager.current_instance.data_dir - try: - Path(old_data_dir_name).rename(new_data_dir_name) - return instance_manager.current_instance - except OSError as e: - log = f"Cannot rename {old_data_dir_name} to {new_data_dir_name}:\n{e}" - Logger.print_error(log) + im.create_instance() + im.enable_instance() + im.start_instance() def check_user_groups(): @@ -189,13 +166,13 @@ def handle_disruptive_system_packages() -> None: Logger.print_warn(warn_msg) -def has_custom_names(instance_list: List[Klipper]) -> bool: +def detect_name_scheme(instance_list: List[BaseInstance]) -> NameScheme: pattern = re.compile("^\d+$") for instance in instance_list: if not pattern.match(instance.suffix): - return True + return NameScheme.CUSTOM - return False + return NameScheme.INDEX def get_highest_index(instance_list: List[Klipper]) -> int: diff --git a/kiauh/modules/moonraker/moonraker.py b/kiauh/modules/moonraker/moonraker.py index 78d60c7..904c813 100644 --- a/kiauh/modules/moonraker/moonraker.py +++ b/kiauh/modules/moonraker/moonraker.py @@ -9,7 +9,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import shutil import subprocess from pathlib import Path from typing import List, Union @@ -27,7 +26,7 @@ class Moonraker(BaseInstance): def blacklist(cls) -> List[str]: return ["None", "mcu"] - def __init__(self, suffix: str = None): + def __init__(self, suffix: str = ""): super().__init__(instance_type=self, suffix=suffix) self.moonraker_dir: Path = MOONRAKER_DIR self.env_dir: Path = MOONRAKER_ENV_DIR diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index 430e157..8208d16 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -54,21 +54,23 @@ from kiauh.utils.system_utils import ( def check_moonraker_install_requirements() -> bool: - kl_im = InstanceManager(Klipper) - kl_instance_list = kl_im.instances - kl_instance_count = len(kl_instance_list) - if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7): Logger.print_error("Versioncheck failed!") Logger.print_error("Python 3.7 or newer required to run Moonraker.") return False - is_klipper_installed = kl_instance_count > 0 - if not is_klipper_installed: + kl_instance_count = len(InstanceManager(Klipper).instances) + if kl_instance_count < 1: Logger.print_warn("Klipper not installed!") Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") return False + mr_instance_count = len(InstanceManager(Moonraker).instances) + if mr_instance_count >= kl_instance_count: + Logger.print_warn("Unable to install more Moonraker instances!") + Logger.print_warn("More Klipper instances required.") + return False + return True diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/modules/moonraker/moonraker_utils.py index b9c8981..9045b42 100644 --- a/kiauh/modules/moonraker/moonraker_utils.py +++ b/kiauh/modules/moonraker/moonraker_utils.py @@ -10,9 +10,10 @@ # ======================================================================= # import shutil -from typing import Dict, Literal +from typing import Dict, Literal, List from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.modules.moonraker import ( DEFAULT_MOONRAKER_PORT, MODULE_PATH, @@ -83,3 +84,36 @@ def create_example_moonraker_conf( cm.write_config() Logger.print_ok(f"Example moonraker.conf created in '{instance.cfg_dir}'") + + +def moonraker_to_multi_conversion(new_name: str) -> None: + """ + Converts the first instance in the List of Moonraker instances to an instance + with a new name. This method will be called when converting from a single Klipper + instance install to a multi instance install when Moonraker is also already + installed with a single instance. + :param new_name: new name the previous single instance is renamed to + :return: None + """ + im = InstanceManager(Moonraker) + instances: List[Moonraker] = im.instances + if not instances: + return + + # in case there are multiple Moonraker instances, we don't want to do anything + if len(instances) > 1: + Logger.print_info("More than a single Moonraker instance found. Skipped ...") + return + + Logger.print_status("Convert Moonraker single to multi instance ...") + # remove the old single instance + im.current_instance = im.instances[0] + im.stop_instance() + im.disable_instance() + im.delete_instance() + # create a new klipper instance with the new name + im.current_instance = Moonraker(suffix=new_name) + # create, enable and start the new moonraker instance + im.create_instance() + im.enable_instance() + im.start_instance() From ad0dbf63b82ef872ad507ac17796a7568deea38b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 27 Dec 2023 00:11:11 +0100 Subject: [PATCH 080/296] refactor(Mainsail): enable remote mode if moonraker multi instance Signed-off-by: Dominik Willner --- kiauh/modules/mainsail/mainsail_setup.py | 45 ++++++++++++------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index cb0e85e..4851d50 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -59,21 +59,22 @@ from kiauh.utils.system_utils import ( def run_mainsail_installation() -> None: - im_mr = InstanceManager(Moonraker) - is_moonraker_installed = len(im_mr.instances) > 0 + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances enable_remotemode = False - if not is_moonraker_installed: + if not mr_instances: print_moonraker_not_found_dialog() - do_continue = get_confirm("Continue Mainsail installation?", allow_go_back=True) - if do_continue: - enable_remotemode = True - else: + if not get_confirm("Continue Mainsail installation?", allow_go_back=True): return - is_mainsail_installed = Path.home().joinpath("mainsail").exists() + # if moonraker is not installed or multiple instances + # are installed we enable mainsails remote mode + if not mr_instances or len(mr_instances) > 1: + enable_remotemode = True + do_reinstall = False - if is_mainsail_installed: + if Path.home().joinpath("mainsail").exists(): print_mainsail_already_installed_dialog() do_reinstall = get_confirm("Re-install Mainsail?", allow_go_back=True) if do_reinstall: @@ -81,10 +82,10 @@ def run_mainsail_installation() -> None: else: return - im_kl = InstanceManager(Klipper) - is_klipper_installed = len(im_kl.instances) > 0 + kl_im = InstanceManager(Klipper) + kl_instances = kl_im.instances install_ms_config = False - if is_klipper_installed: + if kl_instances: print_install_mainsail_config_dialog() question = "Download the recommended macros?" install_ms_config = get_confirm(question, allow_go_back=False) @@ -108,31 +109,31 @@ def run_mainsail_installation() -> None: restore_config_json() if enable_remotemode: enable_mainsail_remotemode() - if is_moonraker_installed: + if mr_instances: patch_moonraker_conf( - im_mr.instances, + mr_instances, "Mainsail", "update_manager mainsail", "mainsail-updater.conf", ) - im_mr.restart_all_instance() - if install_ms_config and is_klipper_installed: + mr_im.restart_all_instance() + if install_ms_config and kl_instances: download_mainsail_cfg() - create_mainsail_cfg_symlink(im_kl.instances) + create_mainsail_cfg_symlink(kl_instances) patch_moonraker_conf( - im_mr.instances, + mr_instances, "mainsail-config", "update_manager mainsail-config", "mainsail-config-updater.conf", ) - patch_printer_config(im_kl.instances) - im_kl.restart_all_instance() + patch_printer_config(kl_instances) + kl_im.restart_all_instance() copy_upstream_nginx_cfg() copy_common_vars_nginx_cfg() create_mainsail_nginx_cfg(mainsail_port) - if is_klipper_installed: - symlink_webui_nginx_log(im_kl.instances) + if kl_instances: + symlink_webui_nginx_log(kl_instances) control_systemd_service("nginx", "restart") except Exception as e: From 625a808484d49998031f57082a02ece139f12ef5 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 27 Dec 2023 14:09:51 +0100 Subject: [PATCH 081/296] fix(InstanceManager): return an updated list when getting the instances property Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/instance_manager.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 4a45e39..8bc9e9c 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -82,10 +82,7 @@ class InstanceManager: @property def instances(self) -> List[I]: - if not self._instances: - self._instances = self.find_instances() - - return sorted(self._instances, key=lambda x: self._sort_instance_list(x.suffix)) + return self.find_instances() @instances.setter def instances(self, value: List[I]): @@ -205,7 +202,7 @@ class InstanceManager: for service in service_list ] - return instance_list + return sorted(instance_list, key=lambda x: self._sort_instance_list(x.suffix)) def _get_instance_suffix(self, file_path: Path) -> str: return file_path.stem.split("-")[-1] if "-" in file_path.stem else "" From 8f44187568480162a52d2c6cb2cf9919f31db71b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 27 Dec 2023 15:42:46 +0100 Subject: [PATCH 082/296] feat(Moonraker): enable Mainsail remote mode after multi instance setup Signed-off-by: Dominik Willner --- kiauh/modules/mainsail/mainsail_utils.py | 3 +++ kiauh/modules/moonraker/moonraker_setup.py | 7 +++++++ kiauh/modules/moonraker/moonraker_utils.py | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/kiauh/modules/mainsail/mainsail_utils.py b/kiauh/modules/mainsail/mainsail_utils.py index 196e27a..dafc2f7 100644 --- a/kiauh/modules/mainsail/mainsail_utils.py +++ b/kiauh/modules/mainsail/mainsail_utils.py @@ -51,10 +51,12 @@ def restore_config_json() -> None: def enable_mainsail_remotemode() -> None: + Logger.print_status("Enable Mainsails remote mode ...") with open(MAINSAIL_CONFIG_JSON, "r") as f: config_data = json.load(f) if config_data["instancesDB"] == "browser": + Logger.print_info("Remote mode already configured. Skipped ...") return Logger.print_status("Setting instance storage location to 'browser' ...") @@ -62,6 +64,7 @@ def enable_mainsail_remotemode() -> None: with open(MAINSAIL_CONFIG_JSON, "w") as f: json.dump(config_data, f, indent=4) + Logger.print_ok("Mainsails remote mode enabled!") def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index 8208d16..e7a8506 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -24,6 +24,8 @@ from kiauh.modules.klipper.klipper_dialogs import ( print_update_warn_dialog, ) from kiauh.core.repo_manager.repo_manager import RepoManager +from kiauh.modules.mainsail import MAINSAIL_DIR +from kiauh.modules.mainsail.mainsail_utils import enable_mainsail_remotemode from kiauh.modules.moonraker import ( EXIT_MOONRAKER_SETUP, DEFAULT_MOONRAKER_REPO_URL, @@ -130,6 +132,11 @@ def install_moonraker() -> None: mr_im.reload_daemon() + # if mainsail is installed, and we installed + # multiple moonraker instances, we enable mainsails remote mode + if MAINSAIL_DIR.exists() and len(mr_im.instances) > 1: + enable_mainsail_remotemode() + def setup_moonraker_prerequesites() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/modules/moonraker/moonraker_utils.py index 9045b42..c92721f 100644 --- a/kiauh/modules/moonraker/moonraker_utils.py +++ b/kiauh/modules/moonraker/moonraker_utils.py @@ -14,6 +14,8 @@ from typing import Dict, Literal, List from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.modules.mainsail import MAINSAIL_DIR +from kiauh.modules.mainsail.mainsail_utils import enable_mainsail_remotemode from kiauh.modules.moonraker import ( DEFAULT_MOONRAKER_PORT, MODULE_PATH, @@ -117,3 +119,7 @@ def moonraker_to_multi_conversion(new_name: str) -> None: im.create_instance() im.enable_instance() im.start_instance() + + # if mainsail is installed, we enable mainsails remote mode + if MAINSAIL_DIR.exists() and len(im.instances) > 1: + enable_mainsail_remotemode() From 83e5d9c0d5383f55e9c9ed16c045a602f62c51b5 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 27 Dec 2023 15:58:37 +0100 Subject: [PATCH 083/296] refactor(Klipper/Moonraker): remove obsolete method calls Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_remove.py | 1 - kiauh/modules/moonraker/moonraker_remove.py | 1 - 2 files changed, 2 deletions(-) diff --git a/kiauh/modules/klipper/klipper_remove.py b/kiauh/modules/klipper/klipper_remove.py index 07f2fe9..a62712a 100644 --- a/kiauh/modules/klipper/klipper_remove.py +++ b/kiauh/modules/klipper/klipper_remove.py @@ -37,7 +37,6 @@ def run_klipper_removal( else: Logger.print_info("No Klipper Services installed! Skipped ...") - im.find_instances() if (remove_dir or remove_env) and im.instances: Logger.print_warn("There are still other Klipper services installed!") Logger.print_warn("Therefor the following parts cannot be removed:") diff --git a/kiauh/modules/moonraker/moonraker_remove.py b/kiauh/modules/moonraker/moonraker_remove.py index e9631db..ab7f22c 100644 --- a/kiauh/modules/moonraker/moonraker_remove.py +++ b/kiauh/modules/moonraker/moonraker_remove.py @@ -39,7 +39,6 @@ def run_moonraker_removal( else: Logger.print_info("No Moonraker Services installed! Skipped ...") - im.find_instances() if (remove_polkit or remove_dir or remove_env) and im.instances: Logger.print_warn("There are still other Moonraker services installed!") Logger.print_warn("Therefor the following parts cannot be removed:") From f3b0e45e39fa40b8542b7b765e0b2b6c111af6d0 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 27 Dec 2023 23:49:34 +0100 Subject: [PATCH 084/296] refactor(Klipper): refactor klipper_setup to reduce cognitive complexity Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 93 ++++++++++---------------- kiauh/modules/klipper/klipper_utils.py | 67 +++++++++++++++++++ 2 files changed, 101 insertions(+), 59 deletions(-) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index a30c27c..05aa038 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -10,13 +10,11 @@ # ======================================================================= # from pathlib import Path -from typing import List from kiauh import KIAUH_CFG from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.core.instance_manager.name_scheme import NameScheme from kiauh.modules.klipper import ( EXIT_KLIPPER_SETUP, DEFAULT_KLIPPER_REPO_URL, @@ -25,23 +23,22 @@ from kiauh.modules.klipper import ( KLIPPER_REQUIREMENTS_TXT, ) from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.klipper.klipper_dialogs import ( - print_update_warn_dialog, - print_select_custom_name_dialog, -) +from kiauh.modules.klipper.klipper_dialogs import print_update_warn_dialog from kiauh.modules.klipper.klipper_utils import ( handle_disruptive_system_packages, check_user_groups, handle_to_multi_instance_conversion, create_example_printer_cfg, - detect_name_scheme, add_to_existing, get_install_count, - assign_custom_name, + init_name_scheme, + check_is_single_to_multi_conversion, + update_name_scheme, + handle_instance_naming, ) from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.modules.moonraker.moonraker import Moonraker -from kiauh.utils.input_utils import get_confirm, get_number_input +from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( parse_packages_from_file, @@ -52,83 +49,50 @@ from kiauh.utils.system_utils import ( ) -# TODO: this method needs refactoring! (but it works for now) def install_klipper() -> None: - im = InstanceManager(Klipper) - kl_instances: List[Klipper] = im.instances + kl_im = InstanceManager(Klipper) # ask to add new instances, if there are existing ones - if kl_instances and not add_to_existing(): + if kl_im.instances and not add_to_existing(): Logger.print_status(EXIT_KLIPPER_SETUP) return install_count = get_install_count() - # install_count = None -> user entered "b" to go back if install_count is None: Logger.print_status(EXIT_KLIPPER_SETUP) return # create a dict of the size of the existing instances + install count - name_scheme = NameScheme.SINGLE - single_to_multi = len(kl_instances) == 1 and kl_instances[0].suffix == "" - name_dict = {c: "" for c in range(len(kl_instances) + install_count)} + name_dict = {c: "" for c in range(len(kl_im.instances) + install_count)} + name_scheme = init_name_scheme(kl_im.instances, install_count) + mr_im = InstanceManager(Moonraker) + name_scheme = update_name_scheme( + name_scheme, name_dict, kl_im.instances, mr_im.instances + ) - if (not kl_instances and install_count > 1) or single_to_multi: - print_select_custom_name_dialog() - if get_confirm("Assign custom names?", False, allow_go_back=True): - name_scheme = NameScheme.CUSTOM - else: - name_scheme = NameScheme.INDEX - - # if there are more moonraker instances installed than klipper, we - # load their names into the name_dict, as we will detect and enforce that naming scheme - mr_instances: List[Moonraker] = InstanceManager(Moonraker).instances - if len(mr_instances) > len(kl_instances): - for k, v in enumerate(mr_instances): - name_dict[k] = v.suffix - name_scheme = detect_name_scheme(mr_instances) - elif len(kl_instances) > 1: - for k, v in enumerate(kl_instances): - name_dict[k] = v.suffix - name_scheme = detect_name_scheme(kl_instances) - - # set instance names if multiple instances will be created - if name_scheme != NameScheme.SINGLE: - for k in name_dict: - if name_dict[k] == "" and name_scheme == NameScheme.INDEX: - name_dict[k] = str(k + 1) - elif name_dict[k] == "" and name_scheme == NameScheme.CUSTOM: - assign_custom_name(k, name_dict) + handle_instance_naming(name_dict, name_scheme) create_example_cfg = get_confirm("Create example printer.cfg?") - if not kl_instances: + if not kl_im.instances: setup_klipper_prerequesites() count = 0 for name in name_dict: - if name_dict[name] in [n.suffix for n in kl_instances]: + if name_dict[name] in [n.suffix for n in kl_im.instances]: continue - else: - count += 1 - if single_to_multi: + if check_is_single_to_multi_conversion(kl_im.instances): handle_to_multi_instance_conversion(name_dict[name]) - single_to_multi = False - count -= 1 - else: - new_instance = Klipper(suffix=name_dict[name]) - im.current_instance = new_instance - im.create_instance() - im.enable_instance() - if create_example_cfg: - create_example_printer_cfg(new_instance) - im.start_instance() + continue + + count += 1 + create_klipper_instance(name_dict[name], create_example_cfg) if count == install_count: break - im.reload_daemon() + kl_im.reload_daemon() # step 4: check/handle conflicting packages/services handle_disruptive_system_packages() @@ -194,3 +158,14 @@ def update_klipper() -> None: ) repo_manager.pull_repo() instance_manager.start_all_instance() + + +def create_klipper_instance(name: str, create_example_cfg: bool) -> None: + kl_im = InstanceManager(Klipper) + new_instance = Klipper(suffix=name) + kl_im.current_instance = new_instance + kl_im.create_instance() + kl_im.enable_instance() + if create_example_cfg: + create_example_printer_cfg(new_instance) + kl_im.start_instance() diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index b6980dc..67818dd 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -29,7 +29,9 @@ from kiauh.modules.klipper.klipper_dialogs import ( print_missing_usergroup_dialog, print_instance_overview, print_select_instance_count_dialog, + print_select_custom_name_dialog, ) +from kiauh.modules.moonraker.moonraker import Moonraker from kiauh.modules.moonraker.moonraker_utils import moonraker_to_multi_conversion from kiauh.utils.common import get_install_status_common, get_repo_name from kiauh.utils.constants import CURRENT_USER @@ -45,6 +47,65 @@ def get_klipper_status() -> Dict[Literal["status", "repo"], str]: } +def check_is_multi_install( + existing_instances: List[Klipper], install_count: int +) -> bool: + return not existing_instances and install_count > 1 + + +def check_is_single_to_multi_conversion(existing_instances: List[Klipper]) -> bool: + return len(existing_instances) == 1 and existing_instances[0].suffix == "" + + +def init_name_scheme( + existing_instances: List[Klipper], install_count: int +) -> NameScheme: + if check_is_multi_install( + existing_instances, install_count + ) or check_is_single_to_multi_conversion(existing_instances): + print_select_custom_name_dialog() + if get_confirm("Assign custom names?", False, allow_go_back=True): + return NameScheme.CUSTOM + else: + return NameScheme.INDEX + else: + return NameScheme.SINGLE + + +def update_name_scheme( + name_scheme: NameScheme, + name_dict: Dict[int, str], + klipper_instances: List[Klipper], + moonraker_instances: List[Moonraker], +) -> NameScheme: + # if there are more moonraker instances installed than klipper, we + # load their names into the name_dict, as we will detect and enforce that naming scheme + if len(moonraker_instances) > len(klipper_instances): + update_name_dict(name_dict, moonraker_instances) + return detect_name_scheme(moonraker_instances) + elif len(klipper_instances) > 1: + update_name_dict(name_dict, klipper_instances) + return detect_name_scheme(klipper_instances) + else: + return name_scheme + + +def update_name_dict(name_dict: Dict[int, str], instances: List[BaseInstance]) -> None: + for k, v in enumerate(instances): + name_dict[k] = v.suffix + + +def handle_instance_naming(name_dict: Dict[int, str], name_scheme: NameScheme) -> None: + if name_scheme == NameScheme.SINGLE: + return + + for k in name_dict: + if name_dict[k] == "" and name_scheme == NameScheme.INDEX: + name_dict[k] = str(k + 1) + elif name_dict[k] == "" and name_scheme == NameScheme.CUSTOM: + assign_custom_name(k, name_dict) + + def add_to_existing() -> bool: kl_instances = InstanceManager(Klipper).instances print_instance_overview(kl_instances) @@ -52,6 +113,12 @@ def add_to_existing() -> bool: def get_install_count() -> Union[int, None]: + """ + Print a dialog for selecting the amount of Klipper instances + to set up with an option to navigate back. Returns None if the + user selected to go back, otherwise an integer greater or equal than 1 | + :return: Integer >= 1 or None + """ kl_instances = InstanceManager(Klipper).instances print_select_instance_count_dialog() question = f"Number of{' additional' if len(kl_instances) > 0 else ''} Klipper instances to set up" From dfbce3b489d3c83255f888ccf66e9f9462a36484 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 28 Dec 2023 13:38:24 +0100 Subject: [PATCH 085/296] feat(KIAUH): show commit in UpdateMenu Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 57 +++++++++++----- kiauh/core/menus/update_menu.py | 76 +++++++++++++++------- kiauh/core/repo_manager/repo_manager.py | 40 ++++++++++++ kiauh/modules/klipper/klipper_utils.py | 19 ++++-- kiauh/modules/moonraker/moonraker_utils.py | 23 +++++-- kiauh/utils/common.py | 40 +++++------- 6 files changed, 182 insertions(+), 73 deletions(-) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index b62b312..4f1579f 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -21,7 +21,14 @@ from kiauh.core.menus.update_menu import UpdateMenu from kiauh.modules.klipper.klipper_utils import get_klipper_status from kiauh.modules.mainsail.mainsail_utils import get_mainsail_status from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status -from kiauh.utils.constants import COLOR_MAGENTA, COLOR_CYAN, RESET_FORMAT, COLOR_RED +from kiauh.utils.constants import ( + COLOR_MAGENTA, + COLOR_CYAN, + RESET_FORMAT, + COLOR_RED, + COLOR_GREEN, + COLOR_YELLOW, +) class MainMenu(BaseMenu): @@ -39,18 +46,18 @@ class MainMenu(BaseMenu): }, footer_type=QUIT_FOOTER, ) - self.kl_status = None - self.kl_repo = None - self.mr_status = None - self.mr_repo = None - self.ms_status = None - self.fl_status = None - self.ks_status = None - self.mb_status = None - self.cn_status = None - self.tg_status = None - self.ob_status = None - self.oe_status = None + self.kl_status = "" + self.kl_repo = "" + self.mr_status = "" + self.mr_repo = "" + self.ms_status = "" + self.fl_status = "" + self.ks_status = "" + self.mb_status = "" + self.cn_status = "" + self.tg_status = "" + self.ob_status = "" + self.oe_status = "" self.init_status() def init_status(self) -> None: @@ -60,14 +67,30 @@ class MainMenu(BaseMenu): def fetch_status(self) -> None: # klipper - self.kl_status = get_klipper_status().get("status") - self.kl_repo = get_klipper_status().get("repo") + klipper_status = get_klipper_status() + kl_status = klipper_status.get("status") + kl_code = klipper_status.get("status_code") + kl_instances = f" {klipper_status.get('instances')}" if kl_code == 1 else "" + self.kl_status = self.format_status_by_code(kl_code, kl_status, kl_instances) + self.kl_repo = f"{COLOR_CYAN}{klipper_status.get('repo')}{RESET_FORMAT}" # moonraker - self.mr_status = get_moonraker_status().get("status") - self.mr_repo = get_moonraker_status().get("repo") + moonraker_status = get_moonraker_status() + mr_status = moonraker_status.get("status") + mr_code = moonraker_status.get("status_code") + mr_instances = f" {moonraker_status.get('instances')}" if mr_code == 1 else "" + self.mr_status = self.format_status_by_code(mr_code, mr_status, mr_instances) + self.mr_repo = f"{COLOR_CYAN}{moonraker_status.get('repo')}{RESET_FORMAT}" # mainsail self.ms_status = get_mainsail_status() + def format_status_by_code(self, code: int, status: str, count: str) -> str: + if code == 1: + return f"{COLOR_GREEN}{status}{count}{RESET_FORMAT}" + elif code == 2: + return f"{COLOR_RED}{status}{count}{RESET_FORMAT}" + + return f"{COLOR_YELLOW}{status}{count}{RESET_FORMAT}" + def print_menu(self): self.fetch_status() diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 3413616..21da09b 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -14,7 +14,11 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.modules.klipper.klipper_setup import update_klipper -from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT +from kiauh.modules.klipper.klipper_utils import ( + get_klipper_status, +) +from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status +from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_WHITE # noinspection PyMethodMayBeStatic @@ -39,8 +43,14 @@ class UpdateMenu(BaseMenu): }, footer_type=BACK_FOOTER, ) + self.kl_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.kl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.mr_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.mr_remote = f"{COLOR_WHITE}{RESET_FORMAT}" def print_menu(self): + self.fetch_update_status() + header = " [ Update Menu ] " color = COLOR_GREEN count = 62 - len(color) - len(RESET_FORMAT) @@ -49,28 +59,28 @@ class UpdateMenu(BaseMenu): /=======================================================\\ | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| - | 0) [Update all] | | | - | | Current: | Latest: | - | Klipper & API: |--------------|--------------| - | 1) [Klipper] | | | - | 2) [Moonraker] | | | - | | | | - | Klipper Webinterface: |--------------|--------------| - | 3) [Mainsail] | | | - | 4) [Fluidd] | | | - | | | | - | Touchscreen GUI: |--------------|--------------| - | 5) [KlipperScreen] | | | - | | | | - | Other: |--------------|--------------| - | 6) [PrettyGCode] | | | - | 7) [Telegram Bot] | | | - | 8) [Obico for Klipper] | | | - | 9) [OctoEverywhere] | | | - | 10) [Mobileraker] | | | - | 11) [Crowsnest] | | | - | |-----------------------------| - | 12) [System] | | | + | 0) Update all | | | + | | Current: | Latest: | + | Klipper & API: |---------------|---------------| + | 1) Klipper | {self.kl_local:<22} | {self.kl_remote:<22} | + | 2) Moonraker | {self.mr_local:<22} | {self.mr_remote:<22} | + | | | | + | Klipper Webinterface: |---------------|---------------| + | 3) Mainsail | | | + | 4) Fluidd | | | + | | | | + | Touchscreen GUI: |---------------|---------------| + | 5) KlipperScreen | | | + | | | | + | Other: |---------------|---------------| + | 6) PrettyGCode | | | + | 7) Telegram Bot | | | + | 8) Obico for Klipper | | | + | 9) OctoEverywhere | | | + | 10) Mobileraker | | | + | 11) Crowsnest | | | + | |-------------------------------| + | 12) System | | """ )[1:] print(menu, end="") @@ -113,3 +123,23 @@ class UpdateMenu(BaseMenu): def upgrade_system_packages(self): print("upgrade_system_packages") + + def fetch_update_status(self): + # klipper + kl_status = get_klipper_status() + self.kl_local = kl_status.get("local") + self.kl_remote = kl_status.get("remote") + if self.kl_local == self.kl_remote: + self.kl_local = f"{COLOR_GREEN}{self.kl_local}{RESET_FORMAT}" + else: + self.kl_local = f"{COLOR_YELLOW}{self.kl_local}{RESET_FORMAT}" + self.kl_remote = f"{COLOR_GREEN}{self.kl_remote}{RESET_FORMAT}" + # moonraker + mr_status = get_moonraker_status() + self.mr_local = mr_status.get("local") + self.mr_remote = mr_status.get("remote") + if self.mr_local == self.mr_remote: + self.mr_local = f"{COLOR_GREEN}{self.mr_local}{RESET_FORMAT}" + else: + self.mr_local = f"{COLOR_YELLOW}{self.mr_local}{RESET_FORMAT}" + self.mr_remote = f"{COLOR_GREEN}{self.mr_remote}{RESET_FORMAT}" diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index a3343e8..61250c4 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -62,6 +62,46 @@ class RepoManager: def target_dir(self, value) -> None: self._target_dir = value + @staticmethod + def get_repo_name(repo: Path) -> str: + """ + Helper method to extract the organisation and name of a repository | + :param repo: repository to extract the values from + :return: String in form of "/" + """ + try: + cmd = ["git", "-C", repo, "config", "--get", "remote.origin.url"] + result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) + return "/".join(result.decode().strip().split("/")[-2:]) + except subprocess.CalledProcessError: + return "-" + + @staticmethod + def get_local_commit(repo: Path) -> str: + if not repo.exists() and repo.joinpath(".git").exists(): + return "-" + + try: + cmd = f"cd {repo} && git describe HEAD --always --tags | cut -d '-' -f 1,2" + return subprocess.check_output(cmd, shell=True, text=True).strip() + except subprocess.CalledProcessError: + return "-" + + @staticmethod + def get_remote_commit(repo: Path) -> str: + if not repo.exists() and repo.joinpath(".git").exists(): + return "-" + + try: + # get locally checked out branch + branch_cmd = f"cd {repo} && git branch | grep -E '\*'" + branch = subprocess.check_output(branch_cmd, shell=True, text=True) + branch = branch.split("*")[-1].strip() + cmd = f"cd {repo} && git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2" + return subprocess.check_output(cmd, shell=True, text=True).strip() + except subprocess.CalledProcessError: + return "-" + def clone_repo(self): log = f"Cloning repository from '{self.repo}' with method '{self.method}'" Logger.print_status(log) diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 67818dd..3aae6b9 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -23,6 +23,7 @@ from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.core.instance_manager.name_scheme import NameScheme +from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.modules.klipper import MODULE_PATH, KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.modules.klipper.klipper import Klipper from kiauh.modules.klipper.klipper_dialogs import ( @@ -33,17 +34,27 @@ from kiauh.modules.klipper.klipper_dialogs import ( ) from kiauh.modules.moonraker.moonraker import Moonraker from kiauh.modules.moonraker.moonraker_utils import moonraker_to_multi_conversion -from kiauh.utils.common import get_install_status_common, get_repo_name +from kiauh.utils.common import get_install_status_common from kiauh.utils.constants import CURRENT_USER from kiauh.utils.input_utils import get_confirm, get_string_input, get_number_input from kiauh.utils.logger import Logger from kiauh.utils.system_utils import mask_system_service -def get_klipper_status() -> Dict[Literal["status", "repo"], str]: +def get_klipper_status() -> ( + Dict[ + Literal["status", "status_code", "instances", "repo", "local", "remote"], + Union[str, int], + ] +): + status = get_install_status_common(Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR) return { - "status": get_install_status_common(Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR), - "repo": get_repo_name(KLIPPER_DIR), + "status": status.get("status"), + "status_code": status.get("status_code"), + "instances": status.get("instances"), + "repo": RepoManager.get_repo_name(KLIPPER_DIR), + "local": RepoManager.get_local_commit(KLIPPER_DIR), + "remote": RepoManager.get_remote_commit(KLIPPER_DIR), } diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/modules/moonraker/moonraker_utils.py index c92721f..c0e1e81 100644 --- a/kiauh/modules/moonraker/moonraker_utils.py +++ b/kiauh/modules/moonraker/moonraker_utils.py @@ -10,10 +10,11 @@ # ======================================================================= # import shutil -from typing import Dict, Literal, List +from typing import Dict, Literal, List, Union from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.modules.mainsail import MAINSAIL_DIR from kiauh.modules.mainsail.mainsail_utils import enable_mainsail_remotemode from kiauh.modules.moonraker import ( @@ -23,19 +24,27 @@ from kiauh.modules.moonraker import ( MOONRAKER_ENV_DIR, ) from kiauh.modules.moonraker.moonraker import Moonraker -from kiauh.utils.common import get_install_status_common, get_repo_name +from kiauh.utils.common import get_install_status_common from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( get_ipv4_addr, ) -def get_moonraker_status() -> Dict[Literal["status", "repo"], str]: +def get_moonraker_status() -> ( + Dict[ + Literal["status", "status_code", "instances", "repo", "local", "remote"], + Union[str, int], + ] +): + status = get_install_status_common(Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR) return { - "status": get_install_status_common( - Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR - ), - "repo": get_repo_name(MOONRAKER_DIR), + "status": status.get("status"), + "status_code": status.get("status_code"), + "instances": status.get("instances"), + "repo": RepoManager.get_repo_name(MOONRAKER_DIR), + "local": RepoManager.get_local_commit(MOONRAKER_DIR), + "remote": RepoManager.get_remote_commit(MOONRAKER_DIR), } diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index aebbaf3..ee8739c 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -9,10 +9,9 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import subprocess from datetime import datetime from pathlib import Path -from typing import Dict, Literal, List, Type +from typing import Dict, Literal, List, Type, Union from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.core.instance_manager.instance_manager import InstanceManager @@ -56,24 +55,9 @@ def check_install_dependencies(deps: List[str]) -> None: install_system_packages(requirements) -def get_repo_name(repo_dir: Path) -> str: - """ - Helper method to extract the organisation and name of a repository | - :param repo_dir: repository to extract the values from - :return: String in form of "/" - """ - try: - cmd = ["git", "-C", repo_dir, "config", "--get", "remote.origin.url"] - result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) - result = "/".join(result.decode().strip().split("/")[-2:]) - return f"{COLOR_CYAN}{result}{RESET_FORMAT}" - except subprocess.CalledProcessError: - return f"{COLOR_RED}-{RESET_FORMAT}" - - def get_install_status_common( instance_type: Type[BaseInstance], repo_dir: Path, env_dir: Path -) -> str: +) -> Dict[Literal["status", "status_code", "instances"], Union[str, int]]: """ Helper method to get the installation status of software components, which only consist of 3 major parts and if those parts exist, the @@ -82,17 +66,29 @@ def get_install_status_common( :param instance_type: The component type :param repo_dir: the repository directory :param env_dir: the python environment directory - :return: formatted string, containing the status + :return: Dictionary with status string, statuscode and instance count """ im = InstanceManager(instance_type) instances_exist = len(im.instances) > 0 status = [repo_dir.exists(), env_dir.exists(), instances_exist] if all(status): - return f"{COLOR_GREEN}Installed: {len(im.instances)}{RESET_FORMAT}" + return { + "status": "Installed:", + "status_code": 1, + "instances": len(im.instances), + } elif not any(status): - return f"{COLOR_RED}Not installed!{RESET_FORMAT}" + return { + "status": "Not installed!", + "status_code": 2, + "instances": len(im.instances), + } else: - return f"{COLOR_YELLOW}Incomplete!{RESET_FORMAT}" + return { + "status": "Incomplete!", + "status_code": 3, + "instances": len(im.instances), + } def get_install_status_webui( From 85b4b68f16f66e89405c7c574d4d6e736b8f2ad3 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 28 Dec 2023 13:47:24 +0100 Subject: [PATCH 086/296] refactor(Klipper/Moonraker): install new packages during updates Signed-off-by: Dominik Willner --- kiauh/core/menus/update_menu.py | 3 ++- kiauh/modules/klipper/klipper_setup.py | 6 ++++++ kiauh/modules/moonraker/moonraker_setup.py | 12 +++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 21da09b..50aae17 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -17,6 +17,7 @@ from kiauh.modules.klipper.klipper_setup import update_klipper from kiauh.modules.klipper.klipper_utils import ( get_klipper_status, ) +from kiauh.modules.moonraker.moonraker_setup import update_moonraker from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_WHITE @@ -92,7 +93,7 @@ class UpdateMenu(BaseMenu): update_klipper() def update_moonraker(self): - print("update_moonraker") + update_moonraker() def update_mainsail(self): print("update_mainsail") diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 05aa038..5b0c6f7 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -157,6 +157,12 @@ def update_klipper() -> None: target_dir=KLIPPER_DIR, ) repo_manager.pull_repo() + + # install possible new system packages + install_klipper_packages(KLIPPER_DIR) + # install possible new python dependencies + install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT) + instance_manager.start_all_instance() diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index e7a8506..0b08a54 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -19,10 +19,7 @@ from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.klipper.klipper_dialogs import ( - print_instance_overview, - print_update_warn_dialog, -) +from kiauh.modules.klipper.klipper_dialogs import print_instance_overview from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.modules.mainsail import MAINSAIL_DIR from kiauh.modules.mainsail.mainsail_utils import enable_mainsail_remotemode @@ -204,7 +201,6 @@ def handle_existing_instances(instance_list: List[Klipper]) -> bool: def update_moonraker() -> None: - print_update_warn_dialog() if not get_confirm("Update Moonraker now?"): return @@ -228,4 +224,10 @@ def update_moonraker() -> None: target_dir=MOONRAKER_DIR, ) repo_manager.pull_repo() + + # install possible new system packages + install_moonraker_packages(MOONRAKER_DIR) + # install possible new python dependencies + install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT) + instance_manager.start_all_instance() From 9a657daffddaef715944034e4aa3c067d9441fac Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 28 Dec 2023 18:05:43 +0100 Subject: [PATCH 087/296] feat(KIAUH): show Mainsail in UpdateMenu Signed-off-by: Dominik Willner --- kiauh/core/menus/update_menu.py | 19 +++++++++++++++++-- kiauh/modules/mainsail/mainsail_setup.py | 7 +++++++ kiauh/modules/mainsail/mainsail_utils.py | 17 +++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 50aae17..68fcc90 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -17,6 +17,11 @@ from kiauh.modules.klipper.klipper_setup import update_klipper from kiauh.modules.klipper.klipper_utils import ( get_klipper_status, ) +from kiauh.modules.mainsail.mainsail_setup import update_mainsail +from kiauh.modules.mainsail.mainsail_utils import ( + get_mainsail_local_version, + get_mainsail_remote_version, +) from kiauh.modules.moonraker.moonraker_setup import update_moonraker from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_WHITE @@ -48,6 +53,8 @@ class UpdateMenu(BaseMenu): self.kl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.mr_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.mr_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.ms_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.ms_remote = f"{COLOR_WHITE}{RESET_FORMAT}" def print_menu(self): self.fetch_update_status() @@ -67,7 +74,7 @@ class UpdateMenu(BaseMenu): | 2) Moonraker | {self.mr_local:<22} | {self.mr_remote:<22} | | | | | | Klipper Webinterface: |---------------|---------------| - | 3) Mainsail | | | + | 3) Mainsail | {self.ms_local:<22} | {self.ms_remote:<22} | | 4) Fluidd | | | | | | | | Touchscreen GUI: |---------------|---------------| @@ -96,7 +103,7 @@ class UpdateMenu(BaseMenu): update_moonraker() def update_mainsail(self): - print("update_mainsail") + update_mainsail() def update_fluidd(self): print("update_fluidd") @@ -144,3 +151,11 @@ class UpdateMenu(BaseMenu): else: self.mr_local = f"{COLOR_YELLOW}{self.mr_local}{RESET_FORMAT}" self.mr_remote = f"{COLOR_GREEN}{self.mr_remote}{RESET_FORMAT}" + # mainsail + self.ms_local = get_mainsail_local_version() + self.ms_remote = get_mainsail_remote_version() + if self.ms_local == self.ms_remote: + self.ms_local = f"{COLOR_GREEN}{self.ms_local}{RESET_FORMAT}" + else: + self.ms_local = f"{COLOR_YELLOW}{self.ms_local}{RESET_FORMAT}" + self.ms_remote = f"{COLOR_GREEN}{self.ms_remote}{RESET_FORMAT}" diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index 4851d50..70f7dc4 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -162,6 +162,13 @@ def download_mainsail() -> None: raise +def update_mainsail() -> None: + Logger.print_status("Updating Mainsail ...") + backup_config_json(is_temp=True) + download_mainsail() + restore_config_json() + + def download_mainsail_cfg() -> None: try: Logger.print_status("Downloading mainsail-config ...") diff --git a/kiauh/modules/mainsail/mainsail_utils.py b/kiauh/modules/mainsail/mainsail_utils.py index dafc2f7..329dff6 100644 --- a/kiauh/modules/mainsail/mainsail_utils.py +++ b/kiauh/modules/mainsail/mainsail_utils.py @@ -11,6 +11,7 @@ import json import shutil +import requests from pathlib import Path from typing import List @@ -80,3 +81,19 @@ def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: desti_error = instance.log_dir.joinpath("mainsail-error.log") if not desti_error.exists(): desti_error.symlink_to(error_log) + + +def get_mainsail_local_version() -> str: + relinfo_file = MAINSAIL_DIR.joinpath("release_info.json") + if not relinfo_file.is_file(): + return "-" + + with open(relinfo_file, "r") as f: + return json.load(f)["version"] + + +def get_mainsail_remote_version() -> str: + url = "https://api.github.com/repos/mainsail-crew/mainsail/tags" + response = requests.get(url) + data = json.loads(response.text) + return data[0]["name"] From cda6d31a7c2b772c2213c7c36147f96055fe6580 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 29 Dec 2023 18:51:23 +0100 Subject: [PATCH 088/296] fix(RepoManager): check if git dir exists Signed-off-by: Dominik Willner --- kiauh/core/repo_manager/repo_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index 61250c4..4754a8d 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -69,6 +69,9 @@ class RepoManager: :param repo: repository to extract the values from :return: String in form of "/" """ + if not repo.exists() and not repo.joinpath(".git").exists(): + return "-" + try: cmd = ["git", "-C", repo, "config", "--get", "remote.origin.url"] result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) @@ -78,7 +81,7 @@ class RepoManager: @staticmethod def get_local_commit(repo: Path) -> str: - if not repo.exists() and repo.joinpath(".git").exists(): + if not repo.exists() and not repo.joinpath(".git").exists(): return "-" try: @@ -89,7 +92,7 @@ class RepoManager: @staticmethod def get_remote_commit(repo: Path) -> str: - if not repo.exists() and repo.joinpath(".git").exists(): + if not repo.exists() and not repo.joinpath(".git").exists(): return "-" try: From c28d5c28b9b6a59a306cedf6f190f7c2f230e407 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 29 Dec 2023 19:20:04 +0100 Subject: [PATCH 089/296] refactor(KIAUH): use pythons own venv module to create a venv Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 7 +++---- kiauh/utils/system_utils.py | 11 ++++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 5b0c6f7..10ce452 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -114,10 +114,9 @@ def setup_klipper_prerequesites() -> None: repo_manager.clone_repo() # install klipper dependencies and create python virtualenv - install_klipper_packages(Path(KLIPPER_DIR)) - create_python_venv(Path(KLIPPER_ENV_DIR)) - klipper_py_req = Path(KLIPPER_REQUIREMENTS_TXT) - install_python_requirements(Path(KLIPPER_ENV_DIR), klipper_py_req) + install_klipper_packages(KLIPPER_DIR) + create_python_venv(KLIPPER_ENV_DIR) + install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT) def install_klipper_packages(klipper_dir: Path) -> None: diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 5d7292e..5b15a16 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -17,6 +17,7 @@ import sys import time import urllib.error import urllib.request +import venv from pathlib import Path from typing import List, Literal @@ -68,14 +69,10 @@ def create_python_venv(target: Path) -> None: Logger.print_status("Set up Python virtual environment ...") if not target.exists(): try: - command = ["python3", "-m", "venv", f"{target}"] - result = subprocess.run(command, stderr=subprocess.PIPE, text=True) - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", prefix=False) - Logger.print_error("Setup of virtualenv failed!") - return - + venv.create(target, with_pip=True) Logger.print_ok("Setup of virtualenv successfull!") + except OSError as e: + Logger.print_error(f"Error setting up virtualenv:\n{e}") except subprocess.CalledProcessError as e: Logger.print_error(f"Error setting up virtualenv:\n{e.output.decode()}") else: From 7820155094519879afc3d3175fabf96fe6c8ad35 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 29 Dec 2023 19:47:45 +0100 Subject: [PATCH 090/296] refactor(Klipper): add some exception handling Signed-off-by: Dominik Willner --- kiauh/modules/klipper/klipper_setup.py | 43 ++++++++++++++++---------- kiauh/utils/system_utils.py | 4 +++ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 10ce452..4e3382b 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -74,25 +74,30 @@ def install_klipper() -> None: create_example_cfg = get_confirm("Create example printer.cfg?") - if not kl_im.instances: - setup_klipper_prerequesites() + try: + if not kl_im.instances: + setup_klipper_prerequesites() - count = 0 - for name in name_dict: - if name_dict[name] in [n.suffix for n in kl_im.instances]: - continue + count = 0 + for name in name_dict: + if name_dict[name] in [n.suffix for n in kl_im.instances]: + continue - if check_is_single_to_multi_conversion(kl_im.instances): - handle_to_multi_instance_conversion(name_dict[name]) - continue + if check_is_single_to_multi_conversion(kl_im.instances): + handle_to_multi_instance_conversion(name_dict[name]) + continue - count += 1 - create_klipper_instance(name_dict[name], create_example_cfg) + count += 1 + create_klipper_instance(name_dict[name], create_example_cfg) - if count == install_count: - break + if count == install_count: + break - kl_im.reload_daemon() + kl_im.reload_daemon() + + except Exception: + Logger.print_error("Klipper installation failed!") + return # step 4: check/handle conflicting packages/services handle_disruptive_system_packages() @@ -114,9 +119,13 @@ def setup_klipper_prerequesites() -> None: repo_manager.clone_repo() # install klipper dependencies and create python virtualenv - install_klipper_packages(KLIPPER_DIR) - create_python_venv(KLIPPER_ENV_DIR) - install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT) + try: + install_klipper_packages(KLIPPER_DIR) + create_python_venv(KLIPPER_ENV_DIR) + install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT) + except Exception: + Logger.print_error("Error during installation of Klipper requirements!") + raise def install_klipper_packages(klipper_dir: Path) -> None: diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 5b15a16..cb4b70b 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -73,8 +73,10 @@ def create_python_venv(target: Path) -> None: Logger.print_ok("Setup of virtualenv successfull!") except OSError as e: Logger.print_error(f"Error setting up virtualenv:\n{e}") + raise except subprocess.CalledProcessError as e: Logger.print_error(f"Error setting up virtualenv:\n{e.output.decode()}") + raise else: if get_confirm("Virtualenv already exists. Re-create?", default_choice=False): try: @@ -83,6 +85,7 @@ def create_python_venv(target: Path) -> None: except OSError as e: log = f"Error removing existing virtualenv: {e.strerror}" Logger.print_error(log, False) + raise else: Logger.print_info("Skipping re-creation of virtualenv ...") @@ -128,6 +131,7 @@ def install_python_requirements(target: Path, requirements: Path) -> None: except subprocess.CalledProcessError as e: log = f"Error installing Python requirements:\n{e.output.decode()}" Logger.print_error(log) + raise def update_system_package_lists(silent: bool, rls_info_change=False) -> None: From 77f1089041441d85d0a8f0be9a1e32ab21f8e961 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Jan 2024 12:51:23 +0100 Subject: [PATCH 091/296] chore(kiauh): reformat code / rename method Signed-off-by: Dominik Willner --- kiauh/core/menus/install_menu.py | 2 +- kiauh/modules/mainsail/mainsail_setup.py | 2 +- kiauh/modules/moonraker/moonraker_setup.py | 40 ++++++++++------------ 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 45ecccb..52e6592 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -70,7 +70,7 @@ class InstallMenu(BaseMenu): moonraker_setup.install_moonraker() def install_mainsail(self): - mainsail_setup.run_mainsail_installation() + mainsail_setup.install_mainsail() def install_fluidd(self): print("install_fluidd") diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index 70f7dc4..437c19b 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -58,7 +58,7 @@ from kiauh.utils.system_utils import ( ) -def run_mainsail_installation() -> None: +def install_mainsail() -> None: mr_im = InstanceManager(Moonraker) mr_instances: List[Moonraker] = mr_im.instances diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index 0b08a54..e47b7eb 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -52,27 +52,6 @@ from kiauh.utils.system_utils import ( ) -def check_moonraker_install_requirements() -> bool: - if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7): - Logger.print_error("Versioncheck failed!") - Logger.print_error("Python 3.7 or newer required to run Moonraker.") - return False - - kl_instance_count = len(InstanceManager(Klipper).instances) - if kl_instance_count < 1: - Logger.print_warn("Klipper not installed!") - Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") - return False - - mr_instance_count = len(InstanceManager(Moonraker).instances) - if mr_instance_count >= kl_instance_count: - Logger.print_warn("Unable to install more Moonraker instances!") - Logger.print_warn("More Klipper instances required.") - return False - - return True - - def install_moonraker() -> None: if not check_moonraker_install_requirements(): return @@ -134,6 +113,25 @@ def install_moonraker() -> None: if MAINSAIL_DIR.exists() and len(mr_im.instances) > 1: enable_mainsail_remotemode() +def check_moonraker_install_requirements() -> bool: + if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7): + Logger.print_error("Versioncheck failed!") + Logger.print_error("Python 3.7 or newer required to run Moonraker.") + return False + + kl_instance_count = len(InstanceManager(Klipper).instances) + if kl_instance_count < 1: + Logger.print_warn("Klipper not installed!") + Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") + return False + + mr_instance_count = len(InstanceManager(Moonraker).instances) + if mr_instance_count >= kl_instance_count: + Logger.print_warn("Unable to install more Moonraker instances!") + Logger.print_warn("More Klipper instances required.") + return False + + return True def setup_moonraker_prerequesites() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) From 35911604af82a9f201e1740f259cf1b0af5a236f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Jan 2024 12:54:06 +0100 Subject: [PATCH 092/296] chore(kiauh): update copyright Signed-off-by: Dominik Willner --- kiauh.py | 2 +- kiauh/__init__.py | 2 +- kiauh/core/backup_manager/backup_manager.py | 2 +- kiauh/core/config_manager/config_manager.py | 2 +- kiauh/core/instance_manager/base_instance.py | 2 +- kiauh/core/instance_manager/instance_manager.py | 2 +- kiauh/core/menus/__init__.py | 2 +- kiauh/core/menus/advanced_menu.py | 2 +- kiauh/core/menus/base_menu.py | 2 +- kiauh/core/menus/install_menu.py | 2 +- kiauh/core/menus/main_menu.py | 2 +- kiauh/core/menus/remove_menu.py | 2 +- kiauh/core/menus/settings_menu.py | 2 +- kiauh/core/menus/update_menu.py | 2 +- kiauh/core/repo_manager/repo_manager.py | 2 +- kiauh/main.py | 2 +- kiauh/modules/klipper/__init__.py | 2 +- kiauh/modules/klipper/klipper.py | 2 +- kiauh/modules/klipper/klipper_dialogs.py | 2 +- kiauh/modules/klipper/klipper_remove.py | 2 +- kiauh/modules/klipper/klipper_setup.py | 2 +- kiauh/modules/klipper/klipper_utils.py | 2 +- kiauh/modules/klipper/menus/klipper_remove_menu.py | 2 +- kiauh/modules/mainsail/__init__.py | 2 +- kiauh/modules/mainsail/mainsail_dialogs.py | 2 +- kiauh/modules/mainsail/mainsail_remove.py | 2 +- kiauh/modules/mainsail/mainsail_setup.py | 2 +- kiauh/modules/mainsail/mainsail_utils.py | 2 +- kiauh/modules/mainsail/menus/mainsail_remove_menu.py | 2 +- kiauh/modules/moonraker/__init__.py | 2 +- kiauh/modules/moonraker/menus/moonraker_remove_menu.py | 2 +- kiauh/modules/moonraker/moonraker.py | 2 +- kiauh/modules/moonraker/moonraker_dialogs.py | 2 +- kiauh/modules/moonraker/moonraker_remove.py | 2 +- kiauh/modules/moonraker/moonraker_setup.py | 4 +++- kiauh/modules/moonraker/moonraker_utils.py | 2 +- kiauh/utils/__init__.py | 2 +- kiauh/utils/common.py | 2 +- kiauh/utils/constants.py | 2 +- kiauh/utils/filesystem_utils.py | 2 +- kiauh/utils/input_utils.py | 2 +- kiauh/utils/logger.py | 2 +- kiauh/utils/system_utils.py | 2 +- 43 files changed, 45 insertions(+), 43 deletions(-) diff --git a/kiauh.py b/kiauh.py index 227775f..ff930a4 100644 --- a/kiauh.py +++ b/kiauh.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/__init__.py b/kiauh/__init__.py index e5fd81f..3a58dc0 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 68fa338..556346d 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index 5e56626..63a4490 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index 903c76b..571985e 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 8bc9e9c..d4e2f28 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index 23cd836..afca077 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 038409a..533b5c1 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 1950e4b..434293c 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 52e6592..22bdad2 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 4f1579f..66324bc 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 922c2cf..bb81c31 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 8cb7723..1305ad1 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 68fcc90..f947d84 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index 4754a8d..9134694 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/main.py b/kiauh/main.py index e064aec..0dd1b31 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/klipper/__init__.py b/kiauh/modules/klipper/__init__.py index e4d32b6..6b87989 100644 --- a/kiauh/modules/klipper/__init__.py +++ b/kiauh/modules/klipper/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/modules/klipper/klipper.py index 6a43452..a42eb53 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/modules/klipper/klipper.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/klipper/klipper_dialogs.py b/kiauh/modules/klipper/klipper_dialogs.py index d3b86bd..94e9374 100644 --- a/kiauh/modules/klipper/klipper_dialogs.py +++ b/kiauh/modules/klipper/klipper_dialogs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/klipper/klipper_remove.py b/kiauh/modules/klipper/klipper_remove.py index a62712a..4037b49 100644 --- a/kiauh/modules/klipper/klipper_remove.py +++ b/kiauh/modules/klipper/klipper_remove.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/modules/klipper/klipper_setup.py index 4e3382b..b5b4def 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/modules/klipper/klipper_setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/modules/klipper/klipper_utils.py index 3aae6b9..3169fd6 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/modules/klipper/klipper_utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/klipper/menus/klipper_remove_menu.py b/kiauh/modules/klipper/menus/klipper_remove_menu.py index ed9740f..d5cd0d9 100644 --- a/kiauh/modules/klipper/menus/klipper_remove_menu.py +++ b/kiauh/modules/klipper/menus/klipper_remove_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/mainsail/__init__.py b/kiauh/modules/mainsail/__init__.py index 87dc2f5..9c01f87 100644 --- a/kiauh/modules/mainsail/__init__.py +++ b/kiauh/modules/mainsail/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/mainsail/mainsail_dialogs.py b/kiauh/modules/mainsail/mainsail_dialogs.py index 0c60bbf..6c8ea69 100644 --- a/kiauh/modules/mainsail/mainsail_dialogs.py +++ b/kiauh/modules/mainsail/mainsail_dialogs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/mainsail/mainsail_remove.py b/kiauh/modules/mainsail/mainsail_remove.py index 162e77f..7a10041 100644 --- a/kiauh/modules/mainsail/mainsail_remove.py +++ b/kiauh/modules/mainsail/mainsail_remove.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index 437c19b..ee15812 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/mainsail/mainsail_utils.py b/kiauh/modules/mainsail/mainsail_utils.py index 329dff6..1fdfa5a 100644 --- a/kiauh/modules/mainsail/mainsail_utils.py +++ b/kiauh/modules/mainsail/mainsail_utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/mainsail/menus/mainsail_remove_menu.py b/kiauh/modules/mainsail/menus/mainsail_remove_menu.py index 071a818..bd0abef 100644 --- a/kiauh/modules/mainsail/menus/mainsail_remove_menu.py +++ b/kiauh/modules/mainsail/menus/mainsail_remove_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/moonraker/__init__.py b/kiauh/modules/moonraker/__init__.py index 9e7231e..14cdac0 100644 --- a/kiauh/modules/moonraker/__init__.py +++ b/kiauh/modules/moonraker/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/moonraker/menus/moonraker_remove_menu.py b/kiauh/modules/moonraker/menus/moonraker_remove_menu.py index c6d0a05..348b34d 100644 --- a/kiauh/modules/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/modules/moonraker/menus/moonraker_remove_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/moonraker/moonraker.py b/kiauh/modules/moonraker/moonraker.py index 904c813..89f47b3 100644 --- a/kiauh/modules/moonraker/moonraker.py +++ b/kiauh/modules/moonraker/moonraker.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/moonraker/moonraker_dialogs.py b/kiauh/modules/moonraker/moonraker_dialogs.py index 6e79b49..f1576cd 100644 --- a/kiauh/modules/moonraker/moonraker_dialogs.py +++ b/kiauh/modules/moonraker/moonraker_dialogs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/moonraker/moonraker_remove.py b/kiauh/modules/moonraker/moonraker_remove.py index ab7f22c..4ddc0c9 100644 --- a/kiauh/modules/moonraker/moonraker_remove.py +++ b/kiauh/modules/moonraker/moonraker_remove.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/modules/moonraker/moonraker_setup.py index e47b7eb..faf50ca 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/modules/moonraker/moonraker_setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # @@ -113,6 +113,7 @@ def install_moonraker() -> None: if MAINSAIL_DIR.exists() and len(mr_im.instances) > 1: enable_mainsail_remotemode() + def check_moonraker_install_requirements() -> bool: if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7): Logger.print_error("Versioncheck failed!") @@ -133,6 +134,7 @@ def check_moonraker_install_requirements() -> bool: return True + def setup_moonraker_prerequesites() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) repo = str( diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/modules/moonraker/moonraker_utils.py index c0e1e81..35fe973 100644 --- a/kiauh/modules/moonraker/moonraker_utils.py +++ b/kiauh/modules/moonraker/moonraker_utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index f44410e..f097b5d 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index ee8739c..fbae241 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py index 647b55b..a2dd612 100644 --- a/kiauh/utils/constants.py +++ b/kiauh/utils/constants.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 505c1f1..360461e 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 7f9f1f5..e2daf8e 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index 8732878..fcd0c9b 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index cb4b70b..fce0977 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ======================================================================= # -# Copyright (C) 2020 - 2023 Dominik Willner # +# Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # From ee81ee4c0c62328ad8bd7c61fe2e8f4610db3c75 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Jan 2024 15:35:00 +0100 Subject: [PATCH 093/296] fix(Mainsail): correctly handle invalid config value for default_port Signed-off-by: Dominik Willner --- kiauh/modules/mainsail/mainsail_setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/modules/mainsail/mainsail_setup.py index ee15812..294db45 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/modules/mainsail/mainsail_setup.py @@ -90,10 +90,13 @@ def install_mainsail() -> None: question = "Download the recommended macros?" install_ms_config = get_confirm(question, allow_go_back=False) + # if a default port is configured in the kiauh.cfg, we use that for the port + # otherwise we default to port 80, but show the user a dialog to confirm/change that port cm = ConfigManager(cfg_file=KIAUH_CFG) default_port = cm.get_value("mainsail", "default_port") - mainsail_port = default_port if default_port else "80" - if not default_port: + is_valid_port = default_port and default_port.isdigit() + mainsail_port = default_port if is_valid_port else "80" + if not is_valid_port: print_mainsail_port_select_dialog(mainsail_port) mainsail_port = get_number_input( "Configure Mainsail for port", From bc30cf418bde8d717164e63410d62d59f56de8a5 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Jan 2024 22:25:07 +0100 Subject: [PATCH 094/296] refactor(kiauh): add option index parameter to method calls from menus Signed-off-by: Dominik Willner --- kiauh/core/menus/base_menu.py | 2 +- kiauh/core/menus/install_menu.py | 23 ++++++++-------- kiauh/core/menus/remove_menu.py | 23 ++++++++-------- kiauh/core/menus/update_menu.py | 27 ++++++++++--------- .../klipper/menus/klipper_remove_menu.py | 13 ++++----- .../mainsail/menus/mainsail_remove_menu.py | 15 ++++++----- .../moonraker/menus/moonraker_remove_menu.py | 15 ++++++----- 7 files changed, 62 insertions(+), 56 deletions(-) diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 434293c..59b5d91 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -175,7 +175,7 @@ class BaseMenu(ABC): if isinstance(option, type) and issubclass(option, BaseMenu): self.navigate_to_submenu(option) elif callable(option): - option() + option(opt_index=choice) elif option is None: raise NotImplementedError(f"No implementation for option {choice}") else: diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 22bdad2..83344ab 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -19,6 +19,7 @@ from kiauh.modules.moonraker import moonraker_setup from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT +# noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class InstallMenu(BaseMenu): def __init__(self): @@ -63,35 +64,35 @@ class InstallMenu(BaseMenu): )[1:] print(menu, end="") - def install_klipper(self): + def install_klipper(self, **kwargs): klipper_setup.install_klipper() - def install_moonraker(self): + def install_moonraker(self, **kwargs): moonraker_setup.install_moonraker() - def install_mainsail(self): + def install_mainsail(self, **kwargs): mainsail_setup.install_mainsail() - def install_fluidd(self): + def install_fluidd(self, **kwargs): print("install_fluidd") - def install_klipperscreen(self): + def install_klipperscreen(self, **kwargs): print("install_klipperscreen") - def install_pretty_gcode(self): + def install_pretty_gcode(self, **kwargs): print("install_pretty_gcode") - def install_telegram_bot(self): + def install_telegram_bot(self, **kwargs): print("install_telegram_bot") - def install_obico(self): + def install_obico(self, **kwargs): print("install_obico") - def install_octoeverywhere(self): + def install_octoeverywhere(self, **kwargs): print("install_octoeverywhere") - def install_mobileraker(self): + def install_mobileraker(self, **kwargs): print("install_mobileraker") - def install_crowsnest(self): + def install_crowsnest(self, **kwargs): print("install_crowsnest") diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index bb81c31..e8ddd79 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -19,6 +19,7 @@ from kiauh.modules.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveM from kiauh.utils.constants import COLOR_RED, RESET_FORMAT +# noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class RemoveMenu(BaseMenu): def __init__(self): @@ -69,35 +70,35 @@ class RemoveMenu(BaseMenu): )[1:] print(menu, end="") - def remove_fluidd(self): + def remove_fluidd(self, **kwargs): print("remove_fluidd") - def remove_fluidd_config(self): + def remove_fluidd_config(self, **kwargs): print("remove_fluidd_config") - def remove_klipperscreen(self): + def remove_klipperscreen(self, **kwargs): print("remove_klipperscreen") - def remove_crowsnest(self): + def remove_crowsnest(self, **kwargs): print("remove_crowsnest") - def remove_mjpgstreamer(self): + def remove_mjpgstreamer(self, **kwargs): print("remove_mjpgstreamer") - def remove_pretty_gcode(self): + def remove_pretty_gcode(self, **kwargs): print("remove_pretty_gcode") - def remove_telegram_bot(self): + def remove_telegram_bot(self, **kwargs): print("remove_telegram_bot") - def remove_obico(self): + def remove_obico(self, **kwargs): print("remove_obico") - def remove_octoeverywhere(self): + def remove_octoeverywhere(self, **kwargs): print("remove_octoeverywhere") - def remove_mobileraker(self): + def remove_mobileraker(self, **kwargs): print("remove_mobileraker") - def remove_nginx(self): + def remove_nginx(self, **kwargs): print("remove_nginx") diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index f947d84..5b7c6ef 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -27,6 +27,7 @@ from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_WHITE +# noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class UpdateMenu(BaseMenu): def __init__(self): @@ -93,43 +94,43 @@ class UpdateMenu(BaseMenu): )[1:] print(menu, end="") - def update_all(self): + def update_all(self, **kwargs): print("update_all") - def update_klipper(self): + def update_klipper(self, **kwargs): update_klipper() - def update_moonraker(self): + def update_moonraker(self, **kwargs): update_moonraker() - def update_mainsail(self): + def update_mainsail(self, **kwargs): update_mainsail() - def update_fluidd(self): + def update_fluidd(self, **kwargs): print("update_fluidd") - def update_klipperscreen(self): + def update_klipperscreen(self, **kwargs): print("update_klipperscreen") - def update_pgc_for_klipper(self): + def update_pgc_for_klipper(self, **kwargs): print("update_pgc_for_klipper") - def update_telegram_bot(self): + def update_telegram_bot(self, **kwargs): print("update_telegram_bot") - def update_moonraker_obico(self): + def update_moonraker_obico(self, **kwargs): print("update_moonraker_obico") - def update_octoeverywhere(self): + def update_octoeverywhere(self, **kwargs): print("update_octoeverywhere") - def update_mobileraker(self): + def update_mobileraker(self, **kwargs): print("update_mobileraker") - def update_crowsnest(self): + def update_crowsnest(self, **kwargs): print("update_crowsnest") - def upgrade_system_packages(self): + def upgrade_system_packages(self, **kwargs): print("upgrade_system_packages") def fetch_update_status(self): diff --git a/kiauh/modules/klipper/menus/klipper_remove_menu.py b/kiauh/modules/klipper/menus/klipper_remove_menu.py index d5cd0d9..61fdc94 100644 --- a/kiauh/modules/klipper/menus/klipper_remove_menu.py +++ b/kiauh/modules/klipper/menus/klipper_remove_menu.py @@ -18,6 +18,7 @@ from kiauh.modules.moonraker import moonraker_remove from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN +# noinspection PyUnusedLocal class KlipperRemoveMenu(BaseMenu): def __init__(self): super().__init__( @@ -67,25 +68,25 @@ class KlipperRemoveMenu(BaseMenu): )[1:] print(menu, end="") - def toggle_all(self) -> None: + def toggle_all(self, **kwargs) -> None: self.remove_klipper_service = True self.remove_klipper_dir = True self.remove_klipper_env = True self.delete_klipper_logs = True - def toggle_remove_klipper_service(self) -> None: + def toggle_remove_klipper_service(self, **kwargs) -> None: self.remove_klipper_service = not self.remove_klipper_service - def toggle_remove_klipper_dir(self) -> None: + def toggle_remove_klipper_dir(self, **kwargs) -> None: self.remove_klipper_dir = not self.remove_klipper_dir - def toggle_remove_klipper_env(self) -> None: + def toggle_remove_klipper_env(self, **kwargs) -> None: self.remove_klipper_env = not self.remove_klipper_env - def toggle_delete_klipper_logs(self) -> None: + def toggle_delete_klipper_logs(self, **kwargs) -> None: self.delete_klipper_logs = not self.delete_klipper_logs - def run_removal_process(self) -> None: + def run_removal_process(self, **kwargs) -> None: if ( not self.remove_klipper_service and not self.remove_klipper_dir diff --git a/kiauh/modules/mainsail/menus/mainsail_remove_menu.py b/kiauh/modules/mainsail/menus/mainsail_remove_menu.py index bd0abef..e2ec352 100644 --- a/kiauh/modules/mainsail/menus/mainsail_remove_menu.py +++ b/kiauh/modules/mainsail/menus/mainsail_remove_menu.py @@ -17,6 +17,7 @@ from kiauh.modules.mainsail import mainsail_remove from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN +# noinspection PyUnusedLocal class MainsailRemoveMenu(BaseMenu): def __init__(self): super().__init__( @@ -72,29 +73,29 @@ class MainsailRemoveMenu(BaseMenu): )[1:] print(menu, end="") - def toggle_all(self) -> None: + def toggle_all(self, **kwargs) -> None: self.remove_mainsail = True self.remove_ms_config = True self.backup_config_json = True self.remove_updater_section = True self.remove_printer_cfg_include = True - def toggle_remove_mainsail(self) -> None: + def toggle_remove_mainsail(self, **kwargs) -> None: self.remove_mainsail = not self.remove_mainsail - def toggle_remove_ms_config(self) -> None: + def toggle_remove_ms_config(self, **kwargs) -> None: self.remove_ms_config = not self.remove_ms_config - def toggle_backup_config_json(self) -> None: + def toggle_backup_config_json(self, **kwargs) -> None: self.backup_config_json = not self.backup_config_json - def toggle_remove_updater_section(self) -> None: + def toggle_remove_updater_section(self, **kwargs) -> None: self.remove_updater_section = not self.remove_updater_section - def toggle_remove_printer_cfg_include(self) -> None: + def toggle_remove_printer_cfg_include(self, **kwargs) -> None: self.remove_printer_cfg_include = not self.remove_printer_cfg_include - def run_removal_process(self) -> None: + def run_removal_process(self, **kwargs) -> None: if ( not self.remove_mainsail and not self.remove_ms_config diff --git a/kiauh/modules/moonraker/menus/moonraker_remove_menu.py b/kiauh/modules/moonraker/menus/moonraker_remove_menu.py index 348b34d..c193f85 100644 --- a/kiauh/modules/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/modules/moonraker/menus/moonraker_remove_menu.py @@ -17,6 +17,7 @@ from kiauh.modules.moonraker import moonraker_remove from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN +# noinspection PyUnusedLocal class MoonrakerRemoveMenu(BaseMenu): def __init__(self): super().__init__( @@ -70,29 +71,29 @@ class MoonrakerRemoveMenu(BaseMenu): )[1:] print(menu, end="") - def toggle_all(self) -> None: + def toggle_all(self, **kwargs) -> None: self.remove_moonraker_service = True self.remove_moonraker_dir = True self.remove_moonraker_env = True self.remove_moonraker_polkit = True self.delete_moonraker_logs = True - def toggle_remove_moonraker_service(self) -> None: + def toggle_remove_moonraker_service(self, **kwargs) -> None: self.remove_moonraker_service = not self.remove_moonraker_service - def toggle_remove_moonraker_dir(self) -> None: + def toggle_remove_moonraker_dir(self, **kwargs) -> None: self.remove_moonraker_dir = not self.remove_moonraker_dir - def toggle_remove_moonraker_env(self) -> None: + def toggle_remove_moonraker_env(self, **kwargs) -> None: self.remove_moonraker_env = not self.remove_moonraker_env - def toggle_remove_moonraker_polkit(self) -> None: + def toggle_remove_moonraker_polkit(self, **kwargs) -> None: self.remove_moonraker_polkit = not self.remove_moonraker_polkit - def toggle_delete_moonraker_logs(self) -> None: + def toggle_delete_moonraker_logs(self, **kwargs) -> None: self.delete_moonraker_logs = not self.delete_moonraker_logs - def run_removal_process(self) -> None: + def run_removal_process(self, **kwargs) -> None: if ( not self.remove_moonraker_service and not self.remove_moonraker_dir From c6999f1990785b9b5ba76804d4f9e19baa9f0fc5 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Jan 2024 22:26:54 +0100 Subject: [PATCH 095/296] refactor(kiauh): if self.options is an empty dict, return invalid input error message. Signed-off-by: Dominik Willner --- kiauh/core/menus/base_menu.py | 36 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 59b5d91..35d87fb 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -23,6 +23,7 @@ from kiauh.utils.constants import ( COLOR_CYAN, RESET_FORMAT, ) +from kiauh.utils.logger import Logger def clear(): @@ -92,6 +93,8 @@ def print_back_help_footer(): class BaseMenu(ABC): + NAVI_OPTIONS = {"quit": ["q"], "back": ["b"], "back_help": ["b", "h"]} + def __init__( self, options: Dict[int, Any], @@ -130,29 +133,20 @@ class BaseMenu(ABC): while True: choice = input(f"{COLOR_CYAN}###### Perform action: {RESET_FORMAT}") - error_msg = ( - f"{COLOR_RED}Invalid input.{RESET_FORMAT}" - if choice.isalpha() - else f"{COLOR_RED}Invalid input. Select a number between {min(self.options)} and {max(self.options)}.{RESET_FORMAT}" - ) - if choice.isdigit() and 0 <= int(choice) < len(self.options): return choice - elif choice.isalpha(): - allowed_input = { - "quit": ["q"], - "back": ["b"], - "back_help": ["b", "h"], - } - if ( - self.footer_type in allowed_input - and choice.lower() in allowed_input[self.footer_type] - ): - return choice - else: - print(error_msg) + elif choice.isalpha() and ( + self.footer_type in self.NAVI_OPTIONS + and choice.lower() in self.NAVI_OPTIONS[self.footer_type] + ): + return choice else: - print(error_msg) + error_msg = ( + "Invalid input!" + if choice.isalpha() or (not self.options and len(self.options) < 1) + else f"Invalid input! Select a number between {min(self.options)} and {max(self.options)}." + ) + Logger.print_error(error_msg, False) def start(self): while True: @@ -160,7 +154,7 @@ class BaseMenu(ABC): choice = self.handle_user_input() if choice == "q": - print(f"{COLOR_GREEN}###### Happy printing!{RESET_FORMAT}") + Logger.print_ok("###### Happy printing!", False) sys.exit(0) elif choice == "b": return From ad56b51e707dfc4bffbb642ad20654a995026e18 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Jan 2024 22:27:38 +0100 Subject: [PATCH 096/296] feat(LogUpload): implement log upload feature Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 3 +- kiauh/modules/log_uploads/__init__.py | 16 ++++++ kiauh/modules/log_uploads/log_upload_utils.py | 57 +++++++++++++++++++ .../log_uploads/menus/log_upload_menu.py | 54 ++++++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 kiauh/modules/log_uploads/__init__.py create mode 100644 kiauh/modules/log_uploads/log_upload_utils.py create mode 100644 kiauh/modules/log_uploads/menus/log_upload_menu.py diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 66324bc..68e964e 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -19,6 +19,7 @@ from kiauh.core.menus.remove_menu import RemoveMenu from kiauh.core.menus.settings_menu import SettingsMenu from kiauh.core.menus.update_menu import UpdateMenu from kiauh.modules.klipper.klipper_utils import get_klipper_status +from kiauh.modules.log_uploads.menus.log_upload_menu import LogUploadMenu from kiauh.modules.mainsail.mainsail_utils import get_mainsail_status from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status from kiauh.utils.constants import ( @@ -36,7 +37,7 @@ class MainMenu(BaseMenu): super().__init__( header=True, options={ - 0: None, + 0: LogUploadMenu, 1: InstallMenu, 2: UpdateMenu, 3: RemoveMenu, diff --git a/kiauh/modules/log_uploads/__init__.py b/kiauh/modules/log_uploads/__init__.py new file mode 100644 index 0000000..7672138 --- /dev/null +++ b/kiauh/modules/log_uploads/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path +from typing import Dict, Union, Literal + +FileKey = Literal["filepath", "display_name"] +LogFile = Dict[FileKey, Union[str, Path]] diff --git a/kiauh/modules/log_uploads/log_upload_utils.py b/kiauh/modules/log_uploads/log_upload_utils.py new file mode 100644 index 0000000..c61dbc6 --- /dev/null +++ b/kiauh/modules/log_uploads/log_upload_utils.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 typing import List +from pathlib import Path + +import urllib.request + +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.modules.klipper.klipper import Klipper +from kiauh.modules.log_uploads import LogFile +from kiauh.utils.logger import Logger + + +def get_logfile_list() -> List[LogFile]: + cm = InstanceManager(Klipper) + log_dirs: List[Path] = [instance.log_dir for instance in cm.instances] + + logfiles: List[LogFile] = [] + for _dir in log_dirs: + for f in _dir.iterdir(): + logfiles.append({"filepath": f, "display_name": get_display_name(f)}) + + return logfiles + + +def get_display_name(filepath: Path) -> str: + printer = " ".join(filepath.parts[-3].split("_")[:-1]) + name = filepath.name + + return f"{printer}: {name}" + + +def upload_logfile(logfile: LogFile) -> None: + file = logfile.get("filepath") + name = logfile.get("display_name") + Logger.print_status(f"Uploading the following logfile from {name} ...") + + with open(file, "rb") as f: + headers = {"x-random": ""} + req = urllib.request.Request("http://paste.c-net.org/", headers=headers, data=f) + try: + response = urllib.request.urlopen(req) + link = response.read().decode("utf-8") + Logger.print_ok("Upload successfull! Access it via the following link:") + Logger.print_ok(f">>>> {link}", False) + except Exception as e: + Logger.print_error(f"Uploading logfile failed!") + Logger.print_error(str(e)) diff --git a/kiauh/modules/log_uploads/menus/log_upload_menu.py b/kiauh/modules/log_uploads/menus/log_upload_menu.py new file mode 100644 index 0000000..9c96c5c --- /dev/null +++ b/kiauh/modules/log_uploads/menus/log_upload_menu.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import textwrap + +from kiauh.core.menus import BACK_FOOTER +from kiauh.core.menus.base_menu import BaseMenu +from kiauh.modules.log_uploads.log_upload_utils import upload_logfile +from kiauh.modules.log_uploads.log_upload_utils import get_logfile_list +from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW + + +# noinspection PyMethodMayBeStatic +class LogUploadMenu(BaseMenu): + def __init__(self): + self.logfile_list = get_logfile_list() + options = {index: self.upload for index in range(len(self.logfile_list))} + super().__init__( + header=True, + options=options, + footer_type=BACK_FOOTER, + ) + + def print_menu(self): + header = " [ Log Upload ] " + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | You can select the following logfiles for uploading: | + | | + """ + )[1:] + + logfile_list = get_logfile_list() + for logfile in enumerate(logfile_list): + line = f"{logfile[0]}) {logfile[1].get('display_name')}" + menu += f"| {line:<54}|\n" + + print(menu, end="") + + def upload(self, **kwargs): + upload_logfile(self.logfile_list[kwargs.get("opt_index")]) From 5a3d21c40b8d114ebe9f6d232f001af9ce6574c4 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 29 Jan 2024 21:10:14 +0100 Subject: [PATCH 097/296] chore(kiauh): rename "modules" to "components" Signed-off-by: Dominik Willner --- kiauh/{modules => components}/__init__.py | 0 .../{modules => components}/klipper/__init__.py | 0 kiauh/{modules => components}/klipper/klipper.py | 2 +- .../klipper/klipper_dialogs.py | 0 .../klipper/klipper_remove.py | 6 +++--- .../klipper/klipper_setup.py | 10 +++++----- .../klipper/klipper_utils.py | 10 +++++----- .../klipper/menus/__init__.py | 0 .../klipper/menus/klipper_remove_menu.py | 3 +-- .../klipper/res/klipper.env | 0 .../klipper/res/klipper.service | 0 .../klipper/res/printer.cfg | 0 .../log_uploads/__init__.py | 0 .../log_uploads/log_upload_utils.py | 4 ++-- .../log_uploads/menus/log_upload_menu.py | 4 ++-- .../{modules => components}/mainsail/__init__.py | 0 .../mainsail/mainsail_dialogs.py | 0 .../mainsail/mainsail_remove.py | 8 ++++---- .../mainsail/mainsail_setup.py | 10 +++++----- .../mainsail/mainsail_utils.py | 4 ++-- .../mainsail/menus/__init__.py | 0 .../mainsail/menus/mainsail_remove_menu.py | 2 +- .../mainsail/res/mainsail-config-updater.conf | 0 .../mainsail/res/mainsail-updater.conf | 0 .../moonraker/__init__.py | 0 .../moonraker/menus/__init__.py | 0 .../moonraker/menus/moonraker_remove_menu.py | 2 +- .../moonraker/moonraker.py | 2 +- .../moonraker/moonraker_dialogs.py | 4 ++-- .../moonraker/moonraker_remove.py | 6 +++--- .../moonraker/moonraker_setup.py | 16 ++++++++-------- .../moonraker/moonraker_utils.py | 8 ++++---- .../moonraker/res/moonraker.conf | 0 .../moonraker/res/moonraker.env | 0 .../moonraker/res/moonraker.service | 0 kiauh/core/menus/install_menu.py | 6 +++--- kiauh/core/menus/main_menu.py | 8 ++++---- kiauh/core/menus/remove_menu.py | 6 +++--- kiauh/core/menus/update_menu.py | 12 ++++++------ 39 files changed, 66 insertions(+), 67 deletions(-) rename kiauh/{modules => components}/__init__.py (100%) rename kiauh/{modules => components}/klipper/__init__.py (100%) rename kiauh/{modules => components}/klipper/klipper.py (98%) rename kiauh/{modules => components}/klipper/klipper_dialogs.py (100%) rename kiauh/{modules => components}/klipper/klipper_remove.py (95%) rename kiauh/{modules => components}/klipper/klipper_setup.py (95%) rename kiauh/{modules => components}/klipper/klipper_utils.py (96%) rename kiauh/{modules => components}/klipper/menus/__init__.py (100%) rename kiauh/{modules => components}/klipper/menus/klipper_remove_menu.py (97%) rename kiauh/{modules => components}/klipper/res/klipper.env (100%) rename kiauh/{modules => components}/klipper/res/klipper.service (100%) rename kiauh/{modules => components}/klipper/res/printer.cfg (100%) rename kiauh/{modules => components}/log_uploads/__init__.py (100%) rename kiauh/{modules => components}/log_uploads/log_upload_utils.py (95%) rename kiauh/{modules => components}/log_uploads/menus/log_upload_menu.py (93%) rename kiauh/{modules => components}/mainsail/__init__.py (100%) rename kiauh/{modules => components}/mainsail/mainsail_dialogs.py (100%) rename kiauh/{modules => components}/mainsail/mainsail_remove.py (95%) rename kiauh/{modules => components}/mainsail/mainsail_setup.py (97%) rename kiauh/{modules => components}/mainsail/mainsail_utils.py (96%) rename kiauh/{modules => components}/mainsail/menus/__init__.py (100%) rename kiauh/{modules => components}/mainsail/menus/mainsail_remove_menu.py (98%) rename kiauh/{modules => components}/mainsail/res/mainsail-config-updater.conf (100%) rename kiauh/{modules => components}/mainsail/res/mainsail-updater.conf (100%) rename kiauh/{modules => components}/moonraker/__init__.py (100%) rename kiauh/{modules => components}/moonraker/menus/__init__.py (100%) rename kiauh/{modules => components}/moonraker/menus/moonraker_remove_menu.py (98%) rename kiauh/{modules => components}/moonraker/moonraker.py (98%) rename kiauh/{modules => components}/moonraker/moonraker_dialogs.py (95%) rename kiauh/{modules => components}/moonraker/moonraker_remove.py (96%) rename kiauh/{modules => components}/moonraker/moonraker_setup.py (93%) rename kiauh/{modules => components}/moonraker/moonraker_utils.py (95%) rename kiauh/{modules => components}/moonraker/res/moonraker.conf (100%) rename kiauh/{modules => components}/moonraker/res/moonraker.env (100%) rename kiauh/{modules => components}/moonraker/res/moonraker.service (100%) diff --git a/kiauh/modules/__init__.py b/kiauh/components/__init__.py similarity index 100% rename from kiauh/modules/__init__.py rename to kiauh/components/__init__.py diff --git a/kiauh/modules/klipper/__init__.py b/kiauh/components/klipper/__init__.py similarity index 100% rename from kiauh/modules/klipper/__init__.py rename to kiauh/components/klipper/__init__.py diff --git a/kiauh/modules/klipper/klipper.py b/kiauh/components/klipper/klipper.py similarity index 98% rename from kiauh/modules/klipper/klipper.py rename to kiauh/components/klipper/klipper.py index a42eb53..78dc8f4 100644 --- a/kiauh/modules/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import List from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH +from kiauh.components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger diff --git a/kiauh/modules/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py similarity index 100% rename from kiauh/modules/klipper/klipper_dialogs.py rename to kiauh/components/klipper/klipper_dialogs.py diff --git a/kiauh/modules/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py similarity index 95% rename from kiauh/modules/klipper/klipper_remove.py rename to kiauh/components/klipper/klipper_remove.py index 4037b49..3534094 100644 --- a/kiauh/modules/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -13,9 +13,9 @@ import shutil from typing import List, Union from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR -from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.klipper.klipper_dialogs import print_instance_overview +from kiauh.components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.components.klipper.klipper import Klipper +from kiauh.components.klipper.klipper_dialogs import print_instance_overview from kiauh.utils.filesystem_utils import remove_file from kiauh.utils.input_utils import get_selection_input from kiauh.utils.logger import Logger diff --git a/kiauh/modules/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py similarity index 95% rename from kiauh/modules/klipper/klipper_setup.py rename to kiauh/components/klipper/klipper_setup.py index b5b4def..bed720d 100644 --- a/kiauh/modules/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -15,16 +15,16 @@ from kiauh import KIAUH_CFG from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.modules.klipper import ( +from kiauh.components.klipper import ( EXIT_KLIPPER_SETUP, DEFAULT_KLIPPER_REPO_URL, KLIPPER_DIR, KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT, ) -from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.klipper.klipper_dialogs import print_update_warn_dialog -from kiauh.modules.klipper.klipper_utils import ( +from kiauh.components.klipper.klipper import Klipper +from kiauh.components.klipper.klipper_dialogs import print_update_warn_dialog +from kiauh.components.klipper.klipper_utils import ( handle_disruptive_system_packages, check_user_groups, handle_to_multi_instance_conversion, @@ -37,7 +37,7 @@ from kiauh.modules.klipper.klipper_utils import ( handle_instance_naming, ) from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.components.moonraker.moonraker import Moonraker from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( diff --git a/kiauh/modules/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py similarity index 96% rename from kiauh/modules/klipper/klipper_utils.py rename to kiauh/components/klipper/klipper_utils.py index 3169fd6..c5de7bf 100644 --- a/kiauh/modules/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -24,16 +24,16 @@ from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.core.instance_manager.name_scheme import NameScheme from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.modules.klipper import MODULE_PATH, KLIPPER_DIR, KLIPPER_ENV_DIR -from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.klipper.klipper_dialogs import ( +from kiauh.components.klipper import MODULE_PATH, KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.components.klipper.klipper import Klipper +from kiauh.components.klipper.klipper_dialogs import ( print_missing_usergroup_dialog, print_instance_overview, print_select_instance_count_dialog, print_select_custom_name_dialog, ) -from kiauh.modules.moonraker.moonraker import Moonraker -from kiauh.modules.moonraker.moonraker_utils import moonraker_to_multi_conversion +from kiauh.components.moonraker.moonraker import Moonraker +from kiauh.components.moonraker.moonraker_utils import moonraker_to_multi_conversion from kiauh.utils.common import get_install_status_common from kiauh.utils.constants import CURRENT_USER from kiauh.utils.input_utils import get_confirm, get_string_input, get_number_input diff --git a/kiauh/modules/klipper/menus/__init__.py b/kiauh/components/klipper/menus/__init__.py similarity index 100% rename from kiauh/modules/klipper/menus/__init__.py rename to kiauh/components/klipper/menus/__init__.py diff --git a/kiauh/modules/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py similarity index 97% rename from kiauh/modules/klipper/menus/klipper_remove_menu.py rename to kiauh/components/klipper/menus/klipper_remove_menu.py index 61fdc94..a75d58d 100644 --- a/kiauh/modules/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -13,8 +13,7 @@ import textwrap from kiauh.core.menus import BACK_HELP_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.modules.klipper import klipper_remove -from kiauh.modules.moonraker import moonraker_remove +from kiauh.components.klipper import klipper_remove from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN diff --git a/kiauh/modules/klipper/res/klipper.env b/kiauh/components/klipper/res/klipper.env similarity index 100% rename from kiauh/modules/klipper/res/klipper.env rename to kiauh/components/klipper/res/klipper.env diff --git a/kiauh/modules/klipper/res/klipper.service b/kiauh/components/klipper/res/klipper.service similarity index 100% rename from kiauh/modules/klipper/res/klipper.service rename to kiauh/components/klipper/res/klipper.service diff --git a/kiauh/modules/klipper/res/printer.cfg b/kiauh/components/klipper/res/printer.cfg similarity index 100% rename from kiauh/modules/klipper/res/printer.cfg rename to kiauh/components/klipper/res/printer.cfg diff --git a/kiauh/modules/log_uploads/__init__.py b/kiauh/components/log_uploads/__init__.py similarity index 100% rename from kiauh/modules/log_uploads/__init__.py rename to kiauh/components/log_uploads/__init__.py diff --git a/kiauh/modules/log_uploads/log_upload_utils.py b/kiauh/components/log_uploads/log_upload_utils.py similarity index 95% rename from kiauh/modules/log_uploads/log_upload_utils.py rename to kiauh/components/log_uploads/log_upload_utils.py index c61dbc6..9ce95c8 100644 --- a/kiauh/modules/log_uploads/log_upload_utils.py +++ b/kiauh/components/log_uploads/log_upload_utils.py @@ -15,8 +15,8 @@ from pathlib import Path import urllib.request from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.log_uploads import LogFile +from kiauh.components.klipper.klipper import Klipper +from kiauh.components.log_uploads import LogFile from kiauh.utils.logger import Logger diff --git a/kiauh/modules/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py similarity index 93% rename from kiauh/modules/log_uploads/menus/log_upload_menu.py rename to kiauh/components/log_uploads/menus/log_upload_menu.py index 9c96c5c..c140b2e 100644 --- a/kiauh/modules/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -13,8 +13,8 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.modules.log_uploads.log_upload_utils import upload_logfile -from kiauh.modules.log_uploads.log_upload_utils import get_logfile_list +from kiauh.components.log_uploads.log_upload_utils import upload_logfile +from kiauh.components.log_uploads.log_upload_utils import get_logfile_list from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW diff --git a/kiauh/modules/mainsail/__init__.py b/kiauh/components/mainsail/__init__.py similarity index 100% rename from kiauh/modules/mainsail/__init__.py rename to kiauh/components/mainsail/__init__.py diff --git a/kiauh/modules/mainsail/mainsail_dialogs.py b/kiauh/components/mainsail/mainsail_dialogs.py similarity index 100% rename from kiauh/modules/mainsail/mainsail_dialogs.py rename to kiauh/components/mainsail/mainsail_dialogs.py diff --git a/kiauh/modules/mainsail/mainsail_remove.py b/kiauh/components/mainsail/mainsail_remove.py similarity index 95% rename from kiauh/modules/mainsail/mainsail_remove.py rename to kiauh/components/mainsail/mainsail_remove.py index 7a10041..fc396ba 100644 --- a/kiauh/modules/mainsail/mainsail_remove.py +++ b/kiauh/components/mainsail/mainsail_remove.py @@ -17,10 +17,10 @@ from typing import List from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR -from kiauh.modules.mainsail.mainsail_utils import backup_config_json -from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.components.klipper.klipper import Klipper +from kiauh.components.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR +from kiauh.components.mainsail.mainsail_utils import backup_config_json +from kiauh.components.moonraker.moonraker import Moonraker from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from kiauh.utils.filesystem_utils import remove_file from kiauh.utils.logger import Logger diff --git a/kiauh/modules/mainsail/mainsail_setup.py b/kiauh/components/mainsail/mainsail_setup.py similarity index 97% rename from kiauh/modules/mainsail/mainsail_setup.py rename to kiauh/components/mainsail/mainsail_setup.py index 294db45..0149794 100644 --- a/kiauh/modules/mainsail/mainsail_setup.py +++ b/kiauh/components/mainsail/mainsail_setup.py @@ -17,27 +17,27 @@ from kiauh import KIAUH_CFG from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.mainsail import ( +from kiauh.components.klipper.klipper import Klipper +from kiauh.components.mainsail import ( MAINSAIL_URL, MAINSAIL_DIR, MAINSAIL_CONFIG_DIR, MAINSAIL_CONFIG_REPO_URL, MODULE_PATH, ) -from kiauh.modules.mainsail.mainsail_dialogs import ( +from kiauh.components.mainsail.mainsail_dialogs import ( print_moonraker_not_found_dialog, print_mainsail_already_installed_dialog, print_install_mainsail_config_dialog, print_mainsail_port_select_dialog, ) -from kiauh.modules.mainsail.mainsail_utils import ( +from kiauh.components.mainsail.mainsail_utils import ( restore_config_json, enable_mainsail_remotemode, backup_config_json, symlink_webui_nginx_log, ) -from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.components.moonraker.moonraker import Moonraker from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from kiauh.utils.common import check_install_dependencies from kiauh.utils.filesystem_utils import ( diff --git a/kiauh/modules/mainsail/mainsail_utils.py b/kiauh/components/mainsail/mainsail_utils.py similarity index 96% rename from kiauh/modules/mainsail/mainsail_utils.py rename to kiauh/components/mainsail/mainsail_utils.py index 1fdfa5a..5f08765 100644 --- a/kiauh/modules/mainsail/mainsail_utils.py +++ b/kiauh/components/mainsail/mainsail_utils.py @@ -16,8 +16,8 @@ from pathlib import Path from typing import List from kiauh.core.backup_manager.backup_manager import BackupManager -from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.mainsail import MAINSAIL_CONFIG_JSON, MAINSAIL_DIR +from kiauh.components.klipper.klipper import Klipper +from kiauh.components.mainsail import MAINSAIL_CONFIG_JSON, MAINSAIL_DIR from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_CONFD from kiauh.utils.common import get_install_status_webui from kiauh.utils.logger import Logger diff --git a/kiauh/modules/mainsail/menus/__init__.py b/kiauh/components/mainsail/menus/__init__.py similarity index 100% rename from kiauh/modules/mainsail/menus/__init__.py rename to kiauh/components/mainsail/menus/__init__.py diff --git a/kiauh/modules/mainsail/menus/mainsail_remove_menu.py b/kiauh/components/mainsail/menus/mainsail_remove_menu.py similarity index 98% rename from kiauh/modules/mainsail/menus/mainsail_remove_menu.py rename to kiauh/components/mainsail/menus/mainsail_remove_menu.py index e2ec352..0beca2a 100644 --- a/kiauh/modules/mainsail/menus/mainsail_remove_menu.py +++ b/kiauh/components/mainsail/menus/mainsail_remove_menu.py @@ -13,7 +13,7 @@ import textwrap from kiauh.core.menus import BACK_HELP_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.modules.mainsail import mainsail_remove +from kiauh.components.mainsail import mainsail_remove from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN diff --git a/kiauh/modules/mainsail/res/mainsail-config-updater.conf b/kiauh/components/mainsail/res/mainsail-config-updater.conf similarity index 100% rename from kiauh/modules/mainsail/res/mainsail-config-updater.conf rename to kiauh/components/mainsail/res/mainsail-config-updater.conf diff --git a/kiauh/modules/mainsail/res/mainsail-updater.conf b/kiauh/components/mainsail/res/mainsail-updater.conf similarity index 100% rename from kiauh/modules/mainsail/res/mainsail-updater.conf rename to kiauh/components/mainsail/res/mainsail-updater.conf diff --git a/kiauh/modules/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py similarity index 100% rename from kiauh/modules/moonraker/__init__.py rename to kiauh/components/moonraker/__init__.py diff --git a/kiauh/modules/moonraker/menus/__init__.py b/kiauh/components/moonraker/menus/__init__.py similarity index 100% rename from kiauh/modules/moonraker/menus/__init__.py rename to kiauh/components/moonraker/menus/__init__.py diff --git a/kiauh/modules/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py similarity index 98% rename from kiauh/modules/moonraker/menus/moonraker_remove_menu.py rename to kiauh/components/moonraker/menus/moonraker_remove_menu.py index c193f85..f8a9ae4 100644 --- a/kiauh/modules/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -13,7 +13,7 @@ import textwrap from kiauh.core.menus import BACK_HELP_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.modules.moonraker import moonraker_remove +from kiauh.components.moonraker import moonraker_remove from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN diff --git a/kiauh/modules/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py similarity index 98% rename from kiauh/modules/moonraker/moonraker.py rename to kiauh/components/moonraker/moonraker.py index 89f47b3..688b825 100644 --- a/kiauh/modules/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -15,7 +15,7 @@ from typing import List, Union from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.modules.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH +from kiauh.components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger diff --git a/kiauh/modules/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py similarity index 95% rename from kiauh/modules/moonraker/moonraker_dialogs.py rename to kiauh/components/moonraker/moonraker_dialogs.py index f1576cd..62867bd 100644 --- a/kiauh/modules/moonraker/moonraker_dialogs.py +++ b/kiauh/components/moonraker/moonraker_dialogs.py @@ -13,8 +13,8 @@ import textwrap from typing import List from kiauh.core.menus.base_menu import print_back_footer -from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.components.klipper.klipper import Klipper +from kiauh.components.moonraker.moonraker import Moonraker from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN diff --git a/kiauh/modules/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py similarity index 96% rename from kiauh/modules/moonraker/moonraker_remove.py rename to kiauh/components/moonraker/moonraker_remove.py index 4ddc0c9..cc76be8 100644 --- a/kiauh/modules/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -14,9 +14,9 @@ import subprocess from typing import List, Union from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.modules.klipper.klipper_dialogs import print_instance_overview -from kiauh.modules.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR -from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.components.klipper.klipper_dialogs import print_instance_overview +from kiauh.components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR +from kiauh.components.moonraker.moonraker import Moonraker from kiauh.utils.filesystem_utils import remove_file from kiauh.utils.input_utils import get_selection_input from kiauh.utils.logger import Logger diff --git a/kiauh/modules/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py similarity index 93% rename from kiauh/modules/moonraker/moonraker_setup.py rename to kiauh/components/moonraker/moonraker_setup.py index faf50ca..066d627 100644 --- a/kiauh/modules/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -18,12 +18,12 @@ from kiauh import KIAUH_CFG from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.modules.klipper.klipper import Klipper -from kiauh.modules.klipper.klipper_dialogs import print_instance_overview +from kiauh.components.klipper.klipper import Klipper +from kiauh.components.klipper.klipper_dialogs import print_instance_overview from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.modules.mainsail import MAINSAIL_DIR -from kiauh.modules.mainsail.mainsail_utils import enable_mainsail_remotemode -from kiauh.modules.moonraker import ( +from kiauh.components.mainsail import MAINSAIL_DIR +from kiauh.components.mainsail.mainsail_utils import enable_mainsail_remotemode +from kiauh.components.moonraker import ( EXIT_MOONRAKER_SETUP, DEFAULT_MOONRAKER_REPO_URL, MOONRAKER_DIR, @@ -34,9 +34,9 @@ from kiauh.modules.moonraker import ( POLKIT_USR_FILE, POLKIT_SCRIPT, ) -from kiauh.modules.moonraker.moonraker import Moonraker -from kiauh.modules.moonraker.moonraker_dialogs import print_moonraker_overview -from kiauh.modules.moonraker.moonraker_utils import create_example_moonraker_conf +from kiauh.components.moonraker.moonraker import Moonraker +from kiauh.components.moonraker.moonraker_dialogs import print_moonraker_overview +from kiauh.components.moonraker.moonraker_utils import create_example_moonraker_conf from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.input_utils import ( get_confirm, diff --git a/kiauh/modules/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py similarity index 95% rename from kiauh/modules/moonraker/moonraker_utils.py rename to kiauh/components/moonraker/moonraker_utils.py index 35fe973..2533b4d 100644 --- a/kiauh/modules/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -15,15 +15,15 @@ from typing import Dict, Literal, List, Union from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.modules.mainsail import MAINSAIL_DIR -from kiauh.modules.mainsail.mainsail_utils import enable_mainsail_remotemode -from kiauh.modules.moonraker import ( +from kiauh.components.mainsail import MAINSAIL_DIR +from kiauh.components.mainsail.mainsail_utils import enable_mainsail_remotemode +from kiauh.components.moonraker import ( DEFAULT_MOONRAKER_PORT, MODULE_PATH, MOONRAKER_DIR, MOONRAKER_ENV_DIR, ) -from kiauh.modules.moonraker.moonraker import Moonraker +from kiauh.components.moonraker.moonraker import Moonraker from kiauh.utils.common import get_install_status_common from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( diff --git a/kiauh/modules/moonraker/res/moonraker.conf b/kiauh/components/moonraker/res/moonraker.conf similarity index 100% rename from kiauh/modules/moonraker/res/moonraker.conf rename to kiauh/components/moonraker/res/moonraker.conf diff --git a/kiauh/modules/moonraker/res/moonraker.env b/kiauh/components/moonraker/res/moonraker.env similarity index 100% rename from kiauh/modules/moonraker/res/moonraker.env rename to kiauh/components/moonraker/res/moonraker.env diff --git a/kiauh/modules/moonraker/res/moonraker.service b/kiauh/components/moonraker/res/moonraker.service similarity index 100% rename from kiauh/modules/moonraker/res/moonraker.service rename to kiauh/components/moonraker/res/moonraker.service diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 83344ab..29be6d6 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -13,9 +13,9 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.modules.klipper import klipper_setup -from kiauh.modules.mainsail import mainsail_setup -from kiauh.modules.moonraker import moonraker_setup +from kiauh.components.klipper import klipper_setup +from kiauh.components.mainsail import mainsail_setup +from kiauh.components.moonraker import moonraker_setup from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 68e964e..0464412 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -18,10 +18,10 @@ from kiauh.core.menus.install_menu import InstallMenu from kiauh.core.menus.remove_menu import RemoveMenu from kiauh.core.menus.settings_menu import SettingsMenu from kiauh.core.menus.update_menu import UpdateMenu -from kiauh.modules.klipper.klipper_utils import get_klipper_status -from kiauh.modules.log_uploads.menus.log_upload_menu import LogUploadMenu -from kiauh.modules.mainsail.mainsail_utils import get_mainsail_status -from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status +from kiauh.components.klipper.klipper_utils import get_klipper_status +from kiauh.components.log_uploads.menus.log_upload_menu import LogUploadMenu +from kiauh.components.mainsail.mainsail_utils import get_mainsail_status +from kiauh.components.moonraker.moonraker_utils import get_moonraker_status from kiauh.utils.constants import ( COLOR_MAGENTA, COLOR_CYAN, diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index e8ddd79..ce9c3a2 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -13,9 +13,9 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.modules.klipper.menus.klipper_remove_menu import KlipperRemoveMenu -from kiauh.modules.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu -from kiauh.modules.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu +from kiauh.components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu +from kiauh.components.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu +from kiauh.components.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu from kiauh.utils.constants import COLOR_RED, RESET_FORMAT diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 5b7c6ef..fef8983 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -13,17 +13,17 @@ import textwrap from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.modules.klipper.klipper_setup import update_klipper -from kiauh.modules.klipper.klipper_utils import ( +from kiauh.components.klipper.klipper_setup import update_klipper +from kiauh.components.klipper.klipper_utils import ( get_klipper_status, ) -from kiauh.modules.mainsail.mainsail_setup import update_mainsail -from kiauh.modules.mainsail.mainsail_utils import ( +from kiauh.components.mainsail.mainsail_setup import update_mainsail +from kiauh.components.mainsail.mainsail_utils import ( get_mainsail_local_version, get_mainsail_remote_version, ) -from kiauh.modules.moonraker.moonraker_setup import update_moonraker -from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status +from kiauh.components.moonraker.moonraker_setup import update_moonraker +from kiauh.components.moonraker.moonraker_utils import get_moonraker_status from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_WHITE From 7cb2231584ead920a4914b3177c79239217fec4b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 29 Jan 2024 21:28:01 +0100 Subject: [PATCH 098/296] chore(kiauh): rename "res" to "assets" Signed-off-by: Dominik Willner --- kiauh/components/klipper/{res => assets}/klipper.env | 0 kiauh/components/klipper/{res => assets}/klipper.service | 0 kiauh/components/klipper/{res => assets}/printer.cfg | 0 kiauh/components/klipper/klipper.py | 4 ++-- kiauh/components/klipper/klipper_utils.py | 2 +- .../mainsail/{res => assets}/mainsail-config-updater.conf | 0 .../mainsail/{res => assets}/mainsail-updater.conf | 0 kiauh/components/mainsail/mainsail_setup.py | 2 +- kiauh/components/moonraker/{res => assets}/moonraker.conf | 0 kiauh/components/moonraker/{res => assets}/moonraker.env | 0 .../components/moonraker/{res => assets}/moonraker.service | 0 kiauh/components/moonraker/moonraker.py | 4 ++-- kiauh/components/moonraker/moonraker_utils.py | 2 +- kiauh/utils/{res => assets}/common_vars.conf | 0 kiauh/utils/{res => assets}/nginx_cfg | 0 kiauh/utils/{res => assets}/upstreams.conf | 0 kiauh/utils/filesystem_utils.py | 6 +++--- 17 files changed, 10 insertions(+), 10 deletions(-) rename kiauh/components/klipper/{res => assets}/klipper.env (100%) rename kiauh/components/klipper/{res => assets}/klipper.service (100%) rename kiauh/components/klipper/{res => assets}/printer.cfg (100%) rename kiauh/components/mainsail/{res => assets}/mainsail-config-updater.conf (100%) rename kiauh/components/mainsail/{res => assets}/mainsail-updater.conf (100%) rename kiauh/components/moonraker/{res => assets}/moonraker.conf (100%) rename kiauh/components/moonraker/{res => assets}/moonraker.env (100%) rename kiauh/components/moonraker/{res => assets}/moonraker.service (100%) rename kiauh/utils/{res => assets}/common_vars.conf (100%) rename kiauh/utils/{res => assets}/nginx_cfg (100%) rename kiauh/utils/{res => assets}/upstreams.conf (100%) diff --git a/kiauh/components/klipper/res/klipper.env b/kiauh/components/klipper/assets/klipper.env similarity index 100% rename from kiauh/components/klipper/res/klipper.env rename to kiauh/components/klipper/assets/klipper.env diff --git a/kiauh/components/klipper/res/klipper.service b/kiauh/components/klipper/assets/klipper.service similarity index 100% rename from kiauh/components/klipper/res/klipper.service rename to kiauh/components/klipper/assets/klipper.service diff --git a/kiauh/components/klipper/res/printer.cfg b/kiauh/components/klipper/assets/printer.cfg similarity index 100% rename from kiauh/components/klipper/res/printer.cfg rename to kiauh/components/klipper/assets/printer.cfg diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index 78dc8f4..1f8e6e5 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -52,10 +52,10 @@ class Klipper(BaseInstance): def create(self) -> None: Logger.print_status("Creating new Klipper Instance ...") - service_template_path = MODULE_PATH.joinpath("res/klipper.service") + service_template_path = MODULE_PATH.joinpath("assets/klipper.service") service_file_name = self.get_service_file_name(extension=True) service_file_target = SYSTEMD.joinpath(service_file_name) - env_template_file_path = MODULE_PATH.joinpath("res/klipper.env") + env_template_file_path = MODULE_PATH.joinpath("assets/klipper.env") env_file_target = self.sysd_dir.joinpath("klipper.env") try: diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index c5de7bf..f49cdec 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -264,7 +264,7 @@ def create_example_printer_cfg(instance: Klipper) -> None: Logger.print_info(f"'{instance.cfg_file}' already exists.") return - source = MODULE_PATH.joinpath("res/printer.cfg") + source = MODULE_PATH.joinpath("assets/printer.cfg") target = instance.cfg_file try: shutil.copy(source, target) diff --git a/kiauh/components/mainsail/res/mainsail-config-updater.conf b/kiauh/components/mainsail/assets/mainsail-config-updater.conf similarity index 100% rename from kiauh/components/mainsail/res/mainsail-config-updater.conf rename to kiauh/components/mainsail/assets/mainsail-config-updater.conf diff --git a/kiauh/components/mainsail/res/mainsail-updater.conf b/kiauh/components/mainsail/assets/mainsail-updater.conf similarity index 100% rename from kiauh/components/mainsail/res/mainsail-updater.conf rename to kiauh/components/mainsail/assets/mainsail-updater.conf diff --git a/kiauh/components/mainsail/mainsail_setup.py b/kiauh/components/mainsail/mainsail_setup.py index 0149794..33b764a 100644 --- a/kiauh/components/mainsail/mainsail_setup.py +++ b/kiauh/components/mainsail/mainsail_setup.py @@ -229,7 +229,7 @@ def patch_moonraker_conf( Logger.print_info("Section already exist. Skipped ...") return - template = MODULE_PATH.joinpath("res", template_file) + template = MODULE_PATH.joinpath("assets", template_file) with open(template, "r") as t: template_content = "\n" template_content += t.read() diff --git a/kiauh/components/moonraker/res/moonraker.conf b/kiauh/components/moonraker/assets/moonraker.conf similarity index 100% rename from kiauh/components/moonraker/res/moonraker.conf rename to kiauh/components/moonraker/assets/moonraker.conf diff --git a/kiauh/components/moonraker/res/moonraker.env b/kiauh/components/moonraker/assets/moonraker.env similarity index 100% rename from kiauh/components/moonraker/res/moonraker.env rename to kiauh/components/moonraker/assets/moonraker.env diff --git a/kiauh/components/moonraker/res/moonraker.service b/kiauh/components/moonraker/assets/moonraker.service similarity index 100% rename from kiauh/components/moonraker/res/moonraker.service rename to kiauh/components/moonraker/assets/moonraker.service diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index 688b825..f33d039 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -39,8 +39,8 @@ class Moonraker(BaseInstance): def create(self, create_example_cfg: bool = False) -> None: Logger.print_status("Creating new Moonraker Instance ...") - service_template_path = MODULE_PATH.joinpath("res/moonraker.service") - env_template_file_path = MODULE_PATH.joinpath("res/moonraker.env") + service_template_path = MODULE_PATH.joinpath("assets/moonraker.service") + env_template_file_path = MODULE_PATH.joinpath("assets/moonraker.env") service_file_name = self.get_service_file_name(extension=True) service_file_target = SYSTEMD.joinpath(service_file_name) env_file_target = self.sysd_dir.joinpath("moonraker.env") diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 2533b4d..017e00a 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -56,7 +56,7 @@ def create_example_moonraker_conf( Logger.print_info(f"'{instance.cfg_file}' already exists.") return - source = MODULE_PATH.joinpath("res/moonraker.conf") + source = MODULE_PATH.joinpath("assets/moonraker.conf") target = instance.cfg_file try: shutil.copy(source, target) diff --git a/kiauh/utils/res/common_vars.conf b/kiauh/utils/assets/common_vars.conf similarity index 100% rename from kiauh/utils/res/common_vars.conf rename to kiauh/utils/assets/common_vars.conf diff --git a/kiauh/utils/res/nginx_cfg b/kiauh/utils/assets/nginx_cfg similarity index 100% rename from kiauh/utils/res/nginx_cfg rename to kiauh/utils/assets/nginx_cfg diff --git a/kiauh/utils/res/upstreams.conf b/kiauh/utils/assets/upstreams.conf similarity index 100% rename from kiauh/utils/res/upstreams.conf rename to kiauh/utils/assets/upstreams.conf diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 360461e..68ce432 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -79,7 +79,7 @@ def copy_upstream_nginx_cfg() -> None: Creates an upstream.conf in /etc/nginx/conf.d :return: None """ - source = MODULE_PATH.joinpath("res/upstreams.conf") + source = MODULE_PATH.joinpath("assets/upstreams.conf") target = NGINX_CONFD.joinpath("upstreams.conf") try: command = ["sudo", "cp", source, target] @@ -95,7 +95,7 @@ def copy_common_vars_nginx_cfg() -> None: Creates a common_vars.conf in /etc/nginx/conf.d :return: None """ - source = MODULE_PATH.joinpath("res/common_vars.conf") + source = MODULE_PATH.joinpath("assets/common_vars.conf") target = NGINX_CONFD.joinpath("common_vars.conf") try: command = ["sudo", "cp", source, target] @@ -115,7 +115,7 @@ def create_nginx_cfg(name: str, port: int, root_dir: Path) -> None: :return: None """ tmp = Path.home().joinpath(f"{name}.tmp") - shutil.copy(MODULE_PATH.joinpath("res/nginx_cfg"), tmp) + shutil.copy(MODULE_PATH.joinpath("assets/nginx_cfg"), tmp) with open(tmp, "r+") as f: content = f.read() content = content.replace("%NAME%", name) From 0447bc4405aa8cb765634cf5c34c8b5a25ec0453 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 5 Feb 2024 21:57:19 +0100 Subject: [PATCH 099/296] refactor(kiauh): allow menus to link options to letters Signed-off-by: Dominik Willner --- .../klipper/menus/klipper_remove_menu.py | 12 +++--- .../mainsail/menus/mainsail_remove_menu.py | 14 +++---- .../moonraker/menus/moonraker_remove_menu.py | 14 +++---- kiauh/core/menus/base_menu.py | 40 +++++++++---------- kiauh/core/menus/install_menu.py | 22 +++++----- kiauh/core/menus/main_menu.py | 19 ++++----- kiauh/core/menus/remove_menu.py | 26 ++++++------ kiauh/core/menus/update_menu.py | 26 ++++++------ 8 files changed, 85 insertions(+), 88 deletions(-) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index a75d58d..7ee5461 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -23,12 +23,12 @@ class KlipperRemoveMenu(BaseMenu): super().__init__( header=False, options={ - 0: self.toggle_all, - 1: self.toggle_remove_klipper_service, - 2: self.toggle_remove_klipper_dir, - 3: self.toggle_remove_klipper_env, - 4: self.toggle_delete_klipper_logs, - 5: self.run_removal_process, + "0": self.toggle_all, + "1": self.toggle_remove_klipper_service, + "2": self.toggle_remove_klipper_dir, + "3": self.toggle_remove_klipper_env, + "4": self.toggle_delete_klipper_logs, + "5": self.run_removal_process, }, footer_type=BACK_HELP_FOOTER, ) diff --git a/kiauh/components/mainsail/menus/mainsail_remove_menu.py b/kiauh/components/mainsail/menus/mainsail_remove_menu.py index 0beca2a..47488fc 100644 --- a/kiauh/components/mainsail/menus/mainsail_remove_menu.py +++ b/kiauh/components/mainsail/menus/mainsail_remove_menu.py @@ -23,13 +23,13 @@ class MainsailRemoveMenu(BaseMenu): super().__init__( header=False, options={ - 0: self.toggle_all, - 1: self.toggle_remove_mainsail, - 2: self.toggle_remove_ms_config, - 3: self.toggle_backup_config_json, - 4: self.toggle_remove_updater_section, - 5: self.toggle_remove_printer_cfg_include, - 6: self.run_removal_process, + "0": self.toggle_all, + "1": self.toggle_remove_mainsail, + "2": self.toggle_remove_ms_config, + "3": self.toggle_backup_config_json, + "4": self.toggle_remove_updater_section, + "5": self.toggle_remove_printer_cfg_include, + "6": self.run_removal_process, }, footer_type=BACK_HELP_FOOTER, ) diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index f8a9ae4..f8bc307 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -23,13 +23,13 @@ class MoonrakerRemoveMenu(BaseMenu): super().__init__( header=False, options={ - 0: self.toggle_all, - 1: self.toggle_remove_moonraker_service, - 2: self.toggle_remove_moonraker_dir, - 3: self.toggle_remove_moonraker_env, - 4: self.toggle_remove_moonraker_polkit, - 5: self.toggle_delete_moonraker_logs, - 6: self.run_removal_process, + "0": self.toggle_all, + "1": self.toggle_remove_moonraker_service, + "2": self.toggle_remove_moonraker_dir, + "3": self.toggle_remove_moonraker_env, + "4": self.toggle_remove_moonraker_polkit, + "5": self.toggle_delete_moonraker_logs, + "6": self.run_removal_process, }, footer_type=BACK_HELP_FOOTER, ) diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 35d87fb..38a9873 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -13,7 +13,7 @@ import subprocess import sys import textwrap from abc import abstractmethod, ABC -from typing import Dict, Any, Literal +from typing import Dict, Any, Literal, Union, Callable, Type from kiauh.core.menus import QUIT_FOOTER, BACK_FOOTER, BACK_HELP_FOOTER from kiauh.utils.constants import ( @@ -97,7 +97,7 @@ class BaseMenu(ABC): def __init__( self, - options: Dict[int, Any], + options: Dict[str, Union[Callable, Type["BaseMenu"]]], options_offset: int = 0, header: bool = True, footer_type: Literal[ @@ -110,10 +110,10 @@ class BaseMenu(ABC): self.footer_type = footer_type @abstractmethod - def print_menu(self): + def print_menu(self) -> None: raise NotImplementedError("Subclasses must implement the print_menu method") - def print_footer(self): + def print_footer(self) -> None: footer_type_map = { QUIT_FOOTER: print_quit_footer, BACK_FOOTER: print_back_footer, @@ -122,33 +122,29 @@ class BaseMenu(ABC): footer_function = footer_type_map.get(self.footer_type, print_quit_footer) footer_function() - def display(self): + def display(self) -> None: # clear() if self.header: print_header() self.print_menu() self.print_footer() - def handle_user_input(self): + def handle_user_input(self) -> str: while True: choice = input(f"{COLOR_CYAN}###### Perform action: {RESET_FORMAT}") + option = self.options.get(choice, None) - if choice.isdigit() and 0 <= int(choice) < len(self.options): + has_navi_option = self.footer_type in self.NAVI_OPTIONS + user_navigated = choice.lower() in self.NAVI_OPTIONS[self.footer_type] + if has_navi_option and user_navigated: return choice - elif choice.isalpha() and ( - self.footer_type in self.NAVI_OPTIONS - and choice.lower() in self.NAVI_OPTIONS[self.footer_type] - ): + + if option is not None: return choice else: - error_msg = ( - "Invalid input!" - if choice.isalpha() or (not self.options and len(self.options) < 1) - else f"Invalid input! Select a number between {min(self.options)} and {max(self.options)}." - ) - Logger.print_error(error_msg, False) + Logger.print_error("Invalid input!", False) - def start(self): + def start(self) -> None: while True: self.display() choice = self.handle_user_input() @@ -158,12 +154,12 @@ class BaseMenu(ABC): sys.exit(0) elif choice == "b": return - elif choice == "p": + elif choice == "h": print("help!") else: - self.execute_option(int(choice)) + self.execute_option(choice) - def execute_option(self, choice): + def execute_option(self, choice: str) -> None: option = self.options.get(choice, None) if isinstance(option, type) and issubclass(option, BaseMenu): @@ -177,7 +173,7 @@ class BaseMenu(ABC): f"Type {type(option)} of option {choice} not of type BaseMenu or Method" ) - def navigate_to_submenu(self, submenu_class): + def navigate_to_submenu(self, submenu_class) -> None: submenu = submenu_class() submenu.previous_menu = self submenu.start() diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 29be6d6..239dfaf 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -26,17 +26,17 @@ class InstallMenu(BaseMenu): super().__init__( header=True, options={ - 1: self.install_klipper, - 2: self.install_moonraker, - 3: self.install_mainsail, - 4: self.install_fluidd, - 5: self.install_klipperscreen, - 6: self.install_pretty_gcode, - 7: self.install_telegram_bot, - 8: self.install_obico, - 9: self.install_octoeverywhere, - 10: self.install_mobileraker, - 11: self.install_crowsnest, + "1": self.install_klipper, + "2": self.install_moonraker, + "3": self.install_mainsail, + "4": self.install_fluidd, + "5": self.install_klipperscreen, + "6": self.install_pretty_gcode, + "7": self.install_telegram_bot, + "8": self.install_obico, + "9": self.install_octoeverywhere, + "10": self.install_mobileraker, + "11": self.install_crowsnest, }, footer_type=BACK_FOOTER, ) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 0464412..aa59d79 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -37,13 +37,14 @@ class MainMenu(BaseMenu): super().__init__( header=True, options={ - 0: LogUploadMenu, - 1: InstallMenu, - 2: UpdateMenu, - 3: RemoveMenu, - 4: AdvancedMenu, - 5: None, - 6: SettingsMenu, + "0": LogUploadMenu, + "1": InstallMenu, + "2": UpdateMenu, + "3": RemoveMenu, + "4": AdvancedMenu, + "5": None, + "e": None, + "s": SettingsMenu, }, footer_type=QUIT_FOOTER, ) @@ -113,13 +114,13 @@ class MainMenu(BaseMenu): | 4) [Advanced] |------------------------------------| | 5) [Backup] | Mainsail: {self.ms_status:<26} | | | Fluidd: {self.fl_status:<26} | - | 6) [Settings] | KlipperScreen: {self.ks_status:<26} | + | E) [Extensions] | KlipperScreen: {self.ks_status:<26} | | | Mobileraker: {self.mb_status:<26} | | | | | | Crowsnest: {self.cn_status:<26} | | | Telegram Bot: {self.tg_status:<26} | | | Obico: {self.ob_status:<26} | - | | OctoEverywhere: {self.oe_status:<26} | + | S) [Settings] | OctoEverywhere: {self.oe_status:<26} | |-------------------------------------------------------| | {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} | """ diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index ce9c3a2..060927e 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -26,19 +26,19 @@ class RemoveMenu(BaseMenu): super().__init__( header=True, options={ - 1: KlipperRemoveMenu, - 2: MoonrakerRemoveMenu, - 3: MainsailRemoveMenu, - 5: self.remove_fluidd, - 6: self.remove_klipperscreen, - 7: self.remove_crowsnest, - 8: self.remove_mjpgstreamer, - 9: self.remove_pretty_gcode, - 10: self.remove_telegram_bot, - 11: self.remove_obico, - 12: self.remove_octoeverywhere, - 13: self.remove_mobileraker, - 14: self.remove_nginx, + "1": KlipperRemoveMenu, + "2": MoonrakerRemoveMenu, + "3": MainsailRemoveMenu, + "5": self.remove_fluidd, + "6": self.remove_klipperscreen, + "7": self.remove_crowsnest, + "8": self.remove_mjpgstreamer, + "9": self.remove_pretty_gcode, + "10": self.remove_telegram_bot, + "11": self.remove_obico, + "12": self.remove_octoeverywhere, + "13": self.remove_mobileraker, + "14": self.remove_nginx, }, footer_type=BACK_FOOTER, ) diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index fef8983..c90de1c 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -34,19 +34,19 @@ class UpdateMenu(BaseMenu): super().__init__( header=True, options={ - 0: self.update_all, - 1: self.update_klipper, - 2: self.update_moonraker, - 3: self.update_mainsail, - 4: self.update_fluidd, - 5: self.update_klipperscreen, - 6: self.update_pgc_for_klipper, - 7: self.update_telegram_bot, - 8: self.update_moonraker_obico, - 9: self.update_octoeverywhere, - 10: self.update_mobileraker, - 11: self.update_crowsnest, - 12: self.upgrade_system_packages, + "0": self.update_all, + "1": self.update_klipper, + "2": self.update_moonraker, + "3": self.update_mainsail, + "4": self.update_fluidd, + "5": self.update_klipperscreen, + "6": self.update_pgc_for_klipper, + "7": self.update_telegram_bot, + "8": self.update_moonraker_obico, + "9": self.update_octoeverywhere, + "10": self.update_mobileraker, + "11": self.update_crowsnest, + "12": self.upgrade_system_packages, }, footer_type=BACK_FOOTER, ) From 2f34253badcfde34330d9e441fcc58975fb92bee Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 8 Feb 2024 22:41:52 +0100 Subject: [PATCH 100/296] refactor(kiauh): handle menus based on if they need instantiation or not Signed-off-by: Dominik Willner --- kiauh/core/menus/base_menu.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 38a9873..2cdf379 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -97,13 +97,14 @@ class BaseMenu(ABC): def __init__( self, - options: Dict[str, Union[Callable, Type["BaseMenu"]]], + options: Dict[str, Union[Callable, Any]], options_offset: int = 0, header: bool = True, footer_type: Literal[ "QUIT_FOOTER", "BACK_FOOTER", "BACK_HELP_FOOTER" ] = QUIT_FOOTER, ): + self.previous_menu = None self.options = options self.options_offset = options_offset self.header = header @@ -131,11 +132,11 @@ class BaseMenu(ABC): def handle_user_input(self) -> str: while True: - choice = input(f"{COLOR_CYAN}###### Perform action: {RESET_FORMAT}") + choice = input(f"{COLOR_CYAN}###### Perform action: {RESET_FORMAT}").lower() option = self.options.get(choice, None) has_navi_option = self.footer_type in self.NAVI_OPTIONS - user_navigated = choice.lower() in self.NAVI_OPTIONS[self.footer_type] + user_navigated = choice in self.NAVI_OPTIONS[self.footer_type] if has_navi_option and user_navigated: return choice @@ -163,7 +164,9 @@ class BaseMenu(ABC): option = self.options.get(choice, None) if isinstance(option, type) and issubclass(option, BaseMenu): - self.navigate_to_submenu(option) + self.navigate_to_menu(option, True) + elif isinstance(option, BaseMenu): + self.navigate_to_menu(option, False) elif callable(option): option(opt_index=choice) elif option is None: @@ -173,7 +176,14 @@ class BaseMenu(ABC): f"Type {type(option)} of option {choice} not of type BaseMenu or Method" ) - def navigate_to_submenu(self, submenu_class) -> None: - submenu = submenu_class() - submenu.previous_menu = self - submenu.start() + def navigate_to_menu(self, menu, instantiate: bool) -> None: + """ + Method for handling the actual menu switch. Can either take in a menu type or an already + instantiated menu class. Use instantiated menu classes only if the menu requires specific input parameters + :param menu: A menu type or menu instance + :param instantiate: Specify if the menu requires instantiation + :return: None + """ + menu = menu() if instantiate else menu + menu.previous_menu = self + menu.start() From 5ace920d3edc635de5e1982c9e4738df9155a43c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 9 Feb 2024 15:47:13 +0100 Subject: [PATCH 101/296] feat(extensions): implement initial extension feature and first extension Signed-off-by: Dominik Willner --- kiauh/core/base_extension.py | 32 +++++ kiauh/core/menus/extensions_menu.py | 135 ++++++++++++++++++ kiauh/core/menus/main_menu.py | 3 +- kiauh/extensions/__init__.py | 0 kiauh/extensions/gcode_shell_cmd/__init__.py | 22 +++ .../assets/gcode_shell_command.py | 87 +++++++++++ .../gcode_shell_cmd/assets/shell_command.cfg | 7 + .../gcode_shell_cmd_extension.py | 127 ++++++++++++++++ .../extensions/gcode_shell_cmd/metadata.json | 9 ++ 9 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 kiauh/core/base_extension.py create mode 100644 kiauh/core/menus/extensions_menu.py create mode 100644 kiauh/extensions/__init__.py create mode 100644 kiauh/extensions/gcode_shell_cmd/__init__.py create mode 100644 kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py create mode 100644 kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg create mode 100644 kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py create mode 100644 kiauh/extensions/gcode_shell_cmd/metadata.json diff --git a/kiauh/core/base_extension.py b/kiauh/core/base_extension.py new file mode 100644 index 0000000..09245ee --- /dev/null +++ b/kiauh/core/base_extension.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 abc import abstractmethod, ABC +from typing import Dict + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class BaseExtension(ABC): + def __init__(self, metadata: Dict[str, str]): + self.metadata = metadata + + @abstractmethod + def install_extension(self, **kwargs) -> None: + raise NotImplementedError( + "Subclasses must implement the install_extension method" + ) + + @abstractmethod + def remove_extension(self, **kwargs) -> None: + raise NotImplementedError( + "Subclasses must implement the remove_extension method" + ) diff --git a/kiauh/core/menus/extensions_menu.py b/kiauh/core/menus/extensions_menu.py new file mode 100644 index 0000000..ebbc4c5 --- /dev/null +++ b/kiauh/core/menus/extensions_menu.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import json +import textwrap +import importlib +import inspect +from pathlib import Path +from typing import List, Dict + +from kiauh.core.base_extension import BaseExtension +from kiauh.core.menus import BACK_FOOTER +from kiauh.core.menus.base_menu import BaseMenu +from kiauh.utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class ExtensionsMenu(BaseMenu): + def __init__(self): + self.extensions = self.discover_extensions() + super().__init__( + header=True, + options=self.get_options(), + footer_type=BACK_FOOTER, + ) + + def discover_extensions(self) -> List[BaseExtension]: + extensions = [] + extensions_dir = Path(__file__).resolve().parents[2].joinpath("extensions") + + for extension in extensions_dir.iterdir(): + metadata_json = Path(extension).joinpath("metadata.json") + if not metadata_json.exists(): + continue + + try: + with open(metadata_json, "r") as m: + metadata = json.load(m).get("metadata") + module_name = ( + f"kiauh.extensions.{extension.name}.{metadata.get('module')}" + ) + name, extension = inspect.getmembers( + importlib.import_module(module_name), + predicate=lambda o: inspect.isclass(o) + and issubclass(o, BaseExtension) + and o != BaseExtension, + )[0] + extensions.append(extension(metadata)) + except (IOError, json.JSONDecodeError, ImportError) as e: + print(f"Failed loading extension {extension}: {e}") + + return sorted(extensions, key=lambda ex: ex.metadata.get("index")) + + def get_options(self) -> Dict[str, BaseMenu]: + options = {} + for extension in self.extensions: + index = extension.metadata.get("index") + options[f"{index}"] = ExtensionSubmenu(extension) + + return options + + def print_menu(self): + header = " [ Extensions Menu ] " + color = COLOR_CYAN + line1 = f"{COLOR_YELLOW}Available Extensions:{RESET_FORMAT}" + count = 62 - len(color) - len(RESET_FORMAT) + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {line1:<62} | + | | + """ + )[1:] + print(menu, end="") + + for extension in self.extensions: + index = extension.metadata.get("index") + name = extension.metadata.get("display_name") + row = f"{index}) {name}" + print(f"| {row:<53} |") + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class ExtensionSubmenu(BaseMenu): + def __init__(self, extension: BaseExtension): + self.extension = extension + self.extension_name = extension.metadata.get("display_name") + self.extension_desc = extension.metadata.get("description") + super().__init__( + header=False, + options={ + "1": extension.install_extension, + "2": extension.remove_extension, + }, + footer_type=BACK_FOOTER, + ) + + def print_menu(self) -> None: + header = f" [ {self.extension_name} ] " + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) + + wrapper = textwrap.TextWrapper(55, initial_indent="| ", subsequent_indent="| ") + lines = wrapper.wrap(self.extension_desc) + formatted_lines = [f"{line:<55} |" for line in lines] + description_text = "\n".join(formatted_lines) + + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + """ + )[1:] + menu += f"{description_text}\n" + menu += textwrap.dedent( + """ + |-------------------------------------------------------| + | 1) Install | + | 2) Remove | + """ + )[1:] + print(menu, end="") diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index aa59d79..366b99a 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -14,6 +14,7 @@ import textwrap from kiauh.core.menus import QUIT_FOOTER from kiauh.core.menus.advanced_menu import AdvancedMenu from kiauh.core.menus.base_menu import BaseMenu +from kiauh.core.menus.extensions_menu import ExtensionsMenu from kiauh.core.menus.install_menu import InstallMenu from kiauh.core.menus.remove_menu import RemoveMenu from kiauh.core.menus.settings_menu import SettingsMenu @@ -43,7 +44,7 @@ class MainMenu(BaseMenu): "3": RemoveMenu, "4": AdvancedMenu, "5": None, - "e": None, + "e": ExtensionsMenu, "s": SettingsMenu, }, footer_type=QUIT_FOOTER, diff --git a/kiauh/extensions/__init__.py b/kiauh/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/extensions/gcode_shell_cmd/__init__.py b/kiauh/extensions/gcode_shell_cmd/__init__.py new file mode 100644 index 0000000..c0d921b --- /dev/null +++ b/kiauh/extensions/gcode_shell_cmd/__init__.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path + + +EXT_MODULE_NAME = "gcode_shell_command.py" +MODULE_PATH = Path(__file__).resolve().parent +MODULE_ASSETS = MODULE_PATH.joinpath("assets") +KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") +EXTENSION_SRC = MODULE_ASSETS.joinpath(EXT_MODULE_NAME) +EXTENSION_TARGET_PATH = KLIPPER_EXTRAS.joinpath(EXT_MODULE_NAME) +EXAMPLE_CFG_SRC = MODULE_ASSETS.joinpath("shell_command.cfg") diff --git a/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py b/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py new file mode 100644 index 0000000..bb38ae5 --- /dev/null +++ b/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py @@ -0,0 +1,87 @@ +# Run a shell command via gcode +# +# Copyright (C) 2019 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import os +import shlex +import subprocess +import logging + +class ShellCommand: + def __init__(self, config): + self.name = config.get_name().split()[-1] + self.printer = config.get_printer() + self.gcode = self.printer.lookup_object('gcode') + cmd = config.get('command') + cmd = os.path.expanduser(cmd) + self.command = shlex.split(cmd) + self.timeout = config.getfloat('timeout', 2., above=0.) + self.verbose = config.getboolean('verbose', True) + self.proc_fd = None + self.partial_output = "" + self.gcode.register_mux_command( + "RUN_SHELL_COMMAND", "CMD", self.name, + self.cmd_RUN_SHELL_COMMAND, + desc=self.cmd_RUN_SHELL_COMMAND_help) + + def _process_output(self, eventime): + if self.proc_fd is None: + return + try: + data = os.read(self.proc_fd, 4096) + except Exception: + pass + data = self.partial_output + data.decode() + if '\n' not in data: + self.partial_output = data + return + elif data[-1] != '\n': + split = data.rfind('\n') + 1 + self.partial_output = data[split:] + data = data[:split] + else: + self.partial_output = "" + self.gcode.respond_info(data) + + cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command" + def cmd_RUN_SHELL_COMMAND(self, params): + gcode_params = params.get('PARAMS','') + gcode_params = shlex.split(gcode_params) + reactor = self.printer.get_reactor() + try: + proc = subprocess.Popen( + self.command + gcode_params, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + except Exception: + logging.exception( + "shell_command: Command {%s} failed" % (self.name)) + raise self.gcode.error("Error running command {%s}" % (self.name)) + if self.verbose: + self.proc_fd = proc.stdout.fileno() + self.gcode.respond_info("Running Command {%s}...:" % (self.name)) + hdl = reactor.register_fd(self.proc_fd, self._process_output) + eventtime = reactor.monotonic() + endtime = eventtime + self.timeout + complete = False + while eventtime < endtime: + eventtime = reactor.pause(eventtime + .05) + if proc.poll() is not None: + complete = True + break + if not complete: + proc.terminate() + if self.verbose: + if self.partial_output: + self.gcode.respond_info(self.partial_output) + self.partial_output = "" + if complete: + msg = "Command {%s} finished\n" % (self.name) + else: + msg = "Command {%s} timed out" % (self.name) + self.gcode.respond_info(msg) + reactor.unregister_fd(hdl) + self.proc_fd = None + + +def load_config_prefix(config): + return ShellCommand(config) diff --git a/kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg b/kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg new file mode 100644 index 0000000..34e7581 --- /dev/null +++ b/kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg @@ -0,0 +1,7 @@ +[gcode_shell_command hello_world] +command: echo hello world +timeout: 2. +verbose: True +[gcode_macro HELLO_WORLD] +gcode: + RUN_SHELL_COMMAND CMD=hello_world \ No newline at end of file diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py new file mode 100644 index 0000000..d8ed49b --- /dev/null +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import os +import shutil +from typing import List + +from kiauh.components.klipper.klipper import Klipper +from kiauh.core.backup_manager.backup_manager import BackupManager +from kiauh.core.base_extension import BaseExtension +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.extensions.gcode_shell_cmd import ( + EXTENSION_TARGET_PATH, + EXTENSION_SRC, + KLIPPER_DIR, + EXAMPLE_CFG_SRC, + KLIPPER_EXTRAS, +) +from kiauh.utils.filesystem_utils import check_file_exist +from kiauh.utils.input_utils import get_confirm +from kiauh.utils.logger import Logger + + +# noinspection PyMethodMayBeStatic +class GcodeShellCmdExtension(BaseExtension): + def install_extension(self, **kwargs) -> None: + install_example = get_confirm("Create an example shell command?", False, False) + + klipper_dir_exists = check_file_exist(KLIPPER_DIR) + if not klipper_dir_exists: + Logger.print_warn( + "No Klipper directory found! Unable to install extension." + ) + return + + extension_installed = check_file_exist(EXTENSION_TARGET_PATH) + overwrite = True + if extension_installed: + overwrite = get_confirm( + "Extension seems to be installed already. Overwrite?", True, False + ) + + if not overwrite: + Logger.print_warn("Installation aborted due to user request.") + return + + im = InstanceManager(Klipper) + im.stop_all_instance() + + try: + Logger.print_status(f"Copy extension to '{KLIPPER_EXTRAS}' ...") + shutil.copy(EXTENSION_SRC, EXTENSION_TARGET_PATH) + except OSError as e: + Logger.print_error(f"Unable to install extension: {e}") + return + + if install_example: + self.install_example_cfg(im.instances) + + im.start_all_instance() + + Logger.print_ok("Installing G-Code Shell Command extension successfull!") + + def remove_extension(self, **kwargs) -> None: + extension_installed = check_file_exist(EXTENSION_TARGET_PATH) + if not extension_installed: + Logger.print_info("Extension does not seem to be installed! Skipping ...") + return + + question = "Do you really want to remove the extension?" + if get_confirm(question, True, False): + try: + Logger.print_status(f"Removing '{EXTENSION_TARGET_PATH}' ...") + os.remove(EXTENSION_TARGET_PATH) + Logger.print_ok("Extension successfully removed!") + except OSError as e: + Logger.print_error(f"Unable to remove extension: {e}") + + Logger.print_warn("PLEASE NOTE:") + Logger.print_warn( + "Remaining gcode shell command will cause Klipper to throw an error." + ) + Logger.print_warn("Make sure to remove them from the printer.cfg!") + + def install_example_cfg(self, instances: List[Klipper]): + cfg_dirs = [instance.cfg_dir for instance in instances] + # copy extension to klippy/extras + for cfg_dir in cfg_dirs: + Logger.print_status(f"Create shell_command.cfg in '{cfg_dir}' ...") + if check_file_exist(cfg_dir.joinpath("shell_command.cfg")): + Logger.print_info("File already exists! Skipping ...") + continue + try: + shutil.copy(EXAMPLE_CFG_SRC, cfg_dir) + Logger.print_ok("Done!") + except OSError as e: + Logger.warn(f"Unable to create example config: {e}") + + # backup each printer.cfg before modification + bm = BackupManager() + for instance in instances: + bm.backup_file( + [instance.cfg_file], + custom_filename=f"{instance.suffix}.printer.cfg", + ) + + # add section to printer.cfg if not already defined + section = "include shell_command.cfg" + cfg_files = [instance.cfg_file for instance in instances] + for cfg_file in cfg_files: + Logger.print_status(f"Include shell_command.cfg in '{cfg_file}' ...") + cm = ConfigManager(cfg_file) + if cm.config.has_section(section): + Logger.print_info("Section already defined! Skipping ...") + continue + cm.config.add_section(section) + cm.write_config() + Logger.print_ok("Done!") diff --git a/kiauh/extensions/gcode_shell_cmd/metadata.json b/kiauh/extensions/gcode_shell_cmd/metadata.json new file mode 100644 index 0000000..cfb38b4 --- /dev/null +++ b/kiauh/extensions/gcode_shell_cmd/metadata.json @@ -0,0 +1,9 @@ +{ + "metadata": { + "index": 1, + "module": "gcode_shell_cmd_extension", + "maintained_by": "dw-0", + "display_name": "G-Code Shell Command", + "description": "Allows to run a shell command from gcode." + } +} From 3bef6ecb85cd8f706402a4ce0c942069ddeecf53 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 10 Feb 2024 00:50:45 +0100 Subject: [PATCH 102/296] feat(BackupManager): allow to ignore folders Signed-off-by: Dominik Willner --- kiauh/core/backup_manager/backup_manager.py | 29 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 556346d..d5cfca5 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -13,15 +13,17 @@ import shutil from pathlib import Path from typing import List -from kiauh import KIAUH_BACKUP_DIR +from kiauh.core.backup_manager import BACKUP_ROOT_DIR from kiauh.utils.common import get_current_date from kiauh.utils.logger import Logger +# noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class BackupManager: - def __init__(self, backup_root_dir: Path = KIAUH_BACKUP_DIR): + def __init__(self, backup_root_dir: Path = BACKUP_ROOT_DIR): self._backup_root_dir = backup_root_dir + self._ignore_folders = None @property def backup_root_dir(self) -> Path: @@ -31,6 +33,14 @@ class BackupManager: def backup_root_dir(self, value: Path): self._backup_root_dir = value + @property + def ignore_folders(self) -> List[str]: + return self._ignore_folders + + @ignore_folders.setter + def ignore_folders(self, value: List[str]): + self._ignore_folders = value + def backup_file( self, files: List[Path] = None, target: Path = None, custom_filename=None ): @@ -56,7 +66,7 @@ class BackupManager: def backup_directory(self, name: str, source: Path, target: Path = None) -> None: if source is None or not Path(source).exists(): - raise OSError + raise OSError("Parameter 'source' is None or Path does not exist!") target = self.backup_root_dir if target is None else target try: @@ -64,9 +74,20 @@ class BackupManager: Logger.print_status(log) date = get_current_date().get("date") time = get_current_date().get("time") - shutil.copytree(source, target.joinpath(f"{name}-{date}-{time}")) + shutil.copytree( + source, + target.joinpath(f"{name.lower()}-{date}-{time}"), + ignore=self.ignore_folders_func, + ) except OSError as e: Logger.print_error(f"Unable to backup directory '{source}':\n{e}") return Logger.print_ok("Backup successfull!") + + def ignore_folders_func(self, dirpath, filenames): + return ( + [f for f in filenames if f in self._ignore_folders] + if self._ignore_folders is not None + else [] + ) From 34ebe5d15e6dbac169f8e3f53a2ef518be3d4ff9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 10 Feb 2024 11:38:23 +0100 Subject: [PATCH 103/296] refactor(BackupManager): backup_file method only takes in single files now Signed-off-by: Dominik Willner --- kiauh/components/mainsail/mainsail_utils.py | 4 +- kiauh/core/backup_manager/backup_manager.py | 38 +++++++++---------- .../gcode_shell_cmd_extension.py | 4 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/kiauh/components/mainsail/mainsail_utils.py b/kiauh/components/mainsail/mainsail_utils.py index 5f08765..de54941 100644 --- a/kiauh/components/mainsail/mainsail_utils.py +++ b/kiauh/components/mainsail/mainsail_utils.py @@ -37,9 +37,9 @@ def backup_config_json(is_temp=False) -> None: bm = BackupManager() if is_temp: fn = Path.home().joinpath("config.json.kiauh.bak") - bm.backup_file([MAINSAIL_CONFIG_JSON], custom_filename=fn) + bm.backup_file(MAINSAIL_CONFIG_JSON, custom_filename=fn) else: - bm.backup_file([MAINSAIL_CONFIG_JSON]) + bm.backup_file(MAINSAIL_CONFIG_JSON) def restore_config_json() -> None: diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index d5cfca5..7f5a887 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -42,27 +42,27 @@ class BackupManager: self._ignore_folders = value def backup_file( - self, files: List[Path] = None, target: Path = None, custom_filename=None + self, file: Path = None, target: Path = None, custom_filename=None ): - if not files: - raise ValueError("Parameter 'files' cannot be None or an empty List!") + if not file: + raise ValueError("Parameter 'file' cannot be None!") target = self.backup_root_dir if target is None else target - for file in files: - Logger.print_status(f"Creating backup of {file} ...") - if Path(file).is_file(): - date = get_current_date().get("date") - time = get_current_date().get("time") - filename = f"{file.stem}-{date}-{time}{file.suffix}" - filename = custom_filename if custom_filename is not None else filename - try: - Path(target).mkdir(exist_ok=True) - shutil.copyfile(file, target.joinpath(filename)) - except OSError as e: - Logger.print_error(f"Unable to backup '{file}':\n{e}") - continue - else: - Logger.print_info(f"File '{file}' not found ...") + + Logger.print_status(f"Creating backup of {file} ...") + if Path(file).is_file(): + date = get_current_date().get("date") + time = get_current_date().get("time") + filename = f"{file.stem}-{date}-{time}{file.suffix}" + filename = custom_filename if custom_filename is not None else filename + try: + Path(target).mkdir(exist_ok=True) + shutil.copyfile(file, target.joinpath(filename)) + Logger.print_ok("Backup successfull!") + except OSError as e: + Logger.print_error(f"Unable to backup '{file}':\n{e}") + else: + Logger.print_info(f"File '{file}' not found ...") def backup_directory(self, name: str, source: Path, target: Path = None) -> None: if source is None or not Path(source).exists(): @@ -79,11 +79,11 @@ class BackupManager: target.joinpath(f"{name.lower()}-{date}-{time}"), ignore=self.ignore_folders_func, ) + Logger.print_ok("Backup successfull!") except OSError as e: Logger.print_error(f"Unable to backup directory '{source}':\n{e}") return - Logger.print_ok("Backup successfull!") def ignore_folders_func(self, dirpath, filenames): return ( diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index d8ed49b..c0c28c1 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -24,7 +24,7 @@ from kiauh.extensions.gcode_shell_cmd import ( KLIPPER_DIR, EXAMPLE_CFG_SRC, KLIPPER_EXTRAS, -) + ) from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger @@ -109,7 +109,7 @@ class GcodeShellCmdExtension(BaseExtension): bm = BackupManager() for instance in instances: bm.backup_file( - [instance.cfg_file], + instance.cfg_file, custom_filename=f"{instance.suffix}.printer.cfg", ) From 948927cfd3c9f0a7d010ec6034b4471d29c68269 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 10 Feb 2024 11:47:27 +0100 Subject: [PATCH 104/296] feat: implement backup menu and backup methods for existing components Signed-off-by: Dominik Willner --- kiauh/__init__.py | 1 - kiauh/components/klipper/__init__.py | 3 + kiauh/components/klipper/klipper_setup.py | 18 ++-- kiauh/components/klipper/klipper_utils.py | 29 ++++-- kiauh/components/mainsail/__init__.py | 9 +- kiauh/components/mainsail/mainsail_utils.py | 20 ++++- kiauh/components/moonraker/__init__.py | 4 + kiauh/components/moonraker/moonraker.py | 10 ++- kiauh/components/moonraker/moonraker_setup.py | 22 ++--- kiauh/components/moonraker/moonraker_utils.py | 33 +++++-- kiauh/core/backup_manager/__init__.py | 14 +++ kiauh/core/menus/backup_menu.py | 89 +++++++++++++++++++ kiauh/core/menus/main_menu.py | 14 +-- kiauh/utils/__init__.py | 3 + kiauh/utils/common.py | 21 ++++- 15 files changed, 238 insertions(+), 52 deletions(-) create mode 100644 kiauh/core/menus/backup_menu.py diff --git a/kiauh/__init__.py b/kiauh/__init__.py index 3a58dc0..91fd073 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -13,4 +13,3 @@ from pathlib import Path APPLICATION_ROOT = Path(__file__).resolve().parent.parent KIAUH_CFG = APPLICATION_ROOT.joinpath("kiauh.cfg") -KIAUH_BACKUP_DIR = Path.home().joinpath("kiauh-backups") diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index 6b87989..ff3ae20 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -11,10 +11,13 @@ from pathlib import Path +from kiauh.core.backup_manager import BACKUP_ROOT_DIR + MODULE_PATH = Path(__file__).resolve().parent KLIPPER_DIR = Path.home().joinpath("klipper") KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") +KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups") KLIPPER_REQUIREMENTS_TXT = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") DEFAULT_KLIPPER_REPO_URL = "https://github.com/Klipper3D/klipper" diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index bed720d..05cfb1c 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -12,16 +12,13 @@ from pathlib import Path from kiauh import KIAUH_CFG -from kiauh.core.backup_manager.backup_manager import BackupManager -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.components.klipper import ( EXIT_KLIPPER_SETUP, DEFAULT_KLIPPER_REPO_URL, KLIPPER_DIR, KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT, -) + ) from kiauh.components.klipper.klipper import Klipper from kiauh.components.klipper.klipper_dialogs import print_update_warn_dialog from kiauh.components.klipper.klipper_utils import ( @@ -35,9 +32,12 @@ from kiauh.components.klipper.klipper_utils import ( check_is_single_to_multi_conversion, update_name_scheme, handle_instance_naming, -) -from kiauh.core.repo_manager.repo_manager import RepoManager + backup_klipper_dir, + ) from kiauh.components.moonraker.moonraker import Moonraker +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( @@ -46,7 +46,7 @@ from kiauh.utils.system_utils import ( install_python_requirements, update_system_package_lists, install_system_packages, -) + ) def install_klipper() -> None: @@ -149,9 +149,7 @@ def update_klipper() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) if cm.get_value("kiauh", "backup_before_update"): - bm = BackupManager() - bm.backup_directory("klipper", KLIPPER_DIR) - bm.backup_directory("klippy-env", KLIPPER_ENV_DIR) + backup_klipper_dir() instance_manager = InstanceManager(Klipper) instance_manager.stop_all_instance() diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index f49cdec..373afc3 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -9,31 +9,36 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import grp import os import re -import grp import shutil import subprocess import textwrap from pathlib import Path - from typing import List, Union, Literal, Dict -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.core.instance_manager.name_scheme import NameScheme -from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.components.klipper import MODULE_PATH, KLIPPER_DIR, KLIPPER_ENV_DIR +from kiauh.components.klipper import ( + MODULE_PATH, + KLIPPER_DIR, + KLIPPER_ENV_DIR, + KLIPPER_BACKUP_DIR, + ) from kiauh.components.klipper.klipper import Klipper from kiauh.components.klipper.klipper_dialogs import ( print_missing_usergroup_dialog, print_instance_overview, print_select_instance_count_dialog, print_select_custom_name_dialog, -) + ) from kiauh.components.moonraker.moonraker import Moonraker from kiauh.components.moonraker.moonraker_utils import moonraker_to_multi_conversion +from kiauh.core.backup_manager.backup_manager import BackupManager +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.base_instance import BaseInstance +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.instance_manager.name_scheme import NameScheme +from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.utils.common import get_install_status_common from kiauh.utils.constants import CURRENT_USER from kiauh.utils.input_utils import get_confirm, get_string_input, get_number_input @@ -276,3 +281,9 @@ def create_example_printer_cfg(instance: Klipper) -> None: cm.set_value("virtual_sdcard", "path", str(instance.gcodes_dir)) cm.write_config() Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'") + + +def backup_klipper_dir() -> None: + bm = BackupManager() + bm.backup_directory("klipper", source=KLIPPER_DIR, target=KLIPPER_BACKUP_DIR) + bm.backup_directory("klippy-env", source=KLIPPER_ENV_DIR, target=KLIPPER_BACKUP_DIR) diff --git a/kiauh/components/mainsail/__init__.py b/kiauh/components/mainsail/__init__.py index 9c01f87..0b2d2af 100644 --- a/kiauh/components/mainsail/__init__.py +++ b/kiauh/components/mainsail/__init__.py @@ -11,10 +11,13 @@ from pathlib import Path +from kiauh.core.backup_manager import BACKUP_ROOT_DIR + MODULE_PATH = Path(__file__).resolve().parent -MAINSAIL_DIR = Path(Path.home(), "mainsail") -MAINSAIL_CONFIG_DIR = Path(Path.home(), "mainsail-config") -MAINSAIL_CONFIG_JSON = Path(MAINSAIL_DIR, "config.json") +MAINSAIL_DIR = Path.home().joinpath("mainsail") +MAINSAIL_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mainsail-backups") +MAINSAIL_CONFIG_DIR = Path.home().joinpath("mainsail-config") +MAINSAIL_CONFIG_JSON = MAINSAIL_DIR.joinpath("config.json") MAINSAIL_URL = ( "https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" ) diff --git a/kiauh/components/mainsail/mainsail_utils.py b/kiauh/components/mainsail/mainsail_utils.py index de54941..9a01e9d 100644 --- a/kiauh/components/mainsail/mainsail_utils.py +++ b/kiauh/components/mainsail/mainsail_utils.py @@ -11,13 +11,18 @@ import json import shutil -import requests from pathlib import Path from typing import List -from kiauh.core.backup_manager.backup_manager import BackupManager +import requests + from kiauh.components.klipper.klipper import Klipper -from kiauh.components.mainsail import MAINSAIL_CONFIG_JSON, MAINSAIL_DIR +from kiauh.components.mainsail import ( + MAINSAIL_CONFIG_JSON, + MAINSAIL_DIR, + MAINSAIL_BACKUP_DIR, + ) +from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_CONFD from kiauh.utils.common import get_install_status_webui from kiauh.utils.logger import Logger @@ -97,3 +102,12 @@ def get_mainsail_remote_version() -> str: response = requests.get(url) data = json.loads(response.text) return data[0]["name"] + + +def backup_mainsail_data() -> None: + with open(MAINSAIL_DIR.joinpath(".version"), "r") as v: + version = v.readlines()[0] + bm = BackupManager() + bm.backup_directory(f"mainsail-{version}", MAINSAIL_DIR, MAINSAIL_BACKUP_DIR) + bm.backup_file(MAINSAIL_CONFIG_JSON, MAINSAIL_BACKUP_DIR) + bm.backup_file(NGINX_SITES_AVAILABLE.joinpath("mainsail"), MAINSAIL_BACKUP_DIR) diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index 14cdac0..e42f3e9 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -11,10 +11,14 @@ from pathlib import Path +from kiauh.core.backup_manager import BACKUP_ROOT_DIR + MODULE_PATH = Path(__file__).resolve().parent MOONRAKER_DIR = Path.home().joinpath("moonraker") MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env") +MOONRAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-backups") +MOONRAKER_DB_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-db-backups") MOONRAKER_REQUIREMENTS_TXT = MOONRAKER_DIR.joinpath( "scripts/moonraker-requirements.txt" ) diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index f33d039..c2a9d29 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -13,9 +13,9 @@ import subprocess from pathlib import Path from typing import List, Union +from kiauh.components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger @@ -34,9 +34,13 @@ class Moonraker(BaseInstance): self.port = self._get_port() self.backup_dir = self.data_dir.joinpath("backup") self.certs_dir = self.data_dir.joinpath("certs") - self.db_dir = self.data_dir.joinpath("database") + self._db_dir = self.data_dir.joinpath("database") self.log = self.log_dir.joinpath("moonraker.log") + @property + def db_dir(self) -> Path: + return self._db_dir + def create(self, create_example_cfg: bool = False) -> None: Logger.print_status("Creating new Moonraker Instance ...") service_template_path = MODULE_PATH.joinpath("assets/moonraker.service") @@ -46,7 +50,7 @@ class Moonraker(BaseInstance): env_file_target = self.sysd_dir.joinpath("moonraker.env") try: - self.create_folders([self.backup_dir, self.certs_dir, self.db_dir]) + self.create_folders([self.backup_dir, self.certs_dir, self._db_dir]) self.write_service_file( service_template_path, service_file_target, env_file_target ) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 066d627..9ed8fa0 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -15,12 +15,8 @@ from pathlib import Path from typing import List from kiauh import KIAUH_CFG -from kiauh.core.backup_manager.backup_manager import BackupManager -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.components.klipper.klipper import Klipper from kiauh.components.klipper.klipper_dialogs import print_instance_overview -from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.components.mainsail import MAINSAIL_DIR from kiauh.components.mainsail.mainsail_utils import enable_mainsail_remotemode from kiauh.components.moonraker import ( @@ -33,15 +29,21 @@ from kiauh.components.moonraker import ( POLKIT_FILE, POLKIT_USR_FILE, POLKIT_SCRIPT, -) + ) from kiauh.components.moonraker.moonraker import Moonraker from kiauh.components.moonraker.moonraker_dialogs import print_moonraker_overview -from kiauh.components.moonraker.moonraker_utils import create_example_moonraker_conf +from kiauh.components.moonraker.moonraker_utils import ( + create_example_moonraker_conf, + backup_moonraker_dir, + ) +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.input_utils import ( get_confirm, get_selection_input, -) + ) from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( parse_packages_from_file, @@ -49,7 +51,7 @@ from kiauh.utils.system_utils import ( install_python_requirements, update_system_package_lists, install_system_packages, -) + ) def install_moonraker() -> None: @@ -206,9 +208,7 @@ def update_moonraker() -> None: cm = ConfigManager(cfg_file=KIAUH_CFG) if cm.get_value("kiauh", "backup_before_update"): - bm = BackupManager() - bm.backup_directory("moonraker", MOONRAKER_DIR) - bm.backup_directory("moonraker-env", MOONRAKER_ENV_DIR) + backup_moonraker_dir() instance_manager = InstanceManager(Moonraker) instance_manager.stop_all_instance() diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 017e00a..e3db3c6 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -12,9 +12,6 @@ import shutil from typing import Dict, Literal, List, Union -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.components.mainsail import MAINSAIL_DIR from kiauh.components.mainsail.mainsail_utils import enable_mainsail_remotemode from kiauh.components.moonraker import ( @@ -22,13 +19,19 @@ from kiauh.components.moonraker import ( MODULE_PATH, MOONRAKER_DIR, MOONRAKER_ENV_DIR, -) + MOONRAKER_BACKUP_DIR, + MOONRAKER_DB_BACKUP_DIR, + ) from kiauh.components.moonraker.moonraker import Moonraker +from kiauh.core.backup_manager.backup_manager import BackupManager +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.utils.common import get_install_status_common from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( get_ipv4_addr, -) + ) def get_moonraker_status() -> ( @@ -132,3 +135,23 @@ def moonraker_to_multi_conversion(new_name: str) -> None: # if mainsail is installed, we enable mainsails remote mode if MAINSAIL_DIR.exists() and len(im.instances) > 1: enable_mainsail_remotemode() + + +def backup_moonraker_dir(): + bm = BackupManager() + bm.backup_directory("moonraker", source=MOONRAKER_DIR, target=MOONRAKER_BACKUP_DIR) + bm.backup_directory( + "moonraker-env", source=MOONRAKER_ENV_DIR, target=MOONRAKER_BACKUP_DIR + ) + + +def backup_moonraker_db_dir() -> None: + im = InstanceManager(Moonraker) + instances: List[Moonraker] = im.instances + bm = BackupManager() + + for instance in instances: + name = f"database-{instance.data_dir_name}" + bm.backup_directory( + name, source=instance.db_dir, target=MOONRAKER_DB_BACKUP_DIR + ) diff --git a/kiauh/core/backup_manager/__init__.py b/kiauh/core/backup_manager/__init__.py index e69de29..7bbbe1e 100644 --- a/kiauh/core/backup_manager/__init__.py +++ b/kiauh/core/backup_manager/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path + +BACKUP_ROOT_DIR = Path.home().joinpath("kiauh-backups") diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py new file mode 100644 index 0000000..ffc246b --- /dev/null +++ b/kiauh/core/menus/backup_menu.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import textwrap + +from kiauh.components.klipper.klipper_utils import backup_klipper_dir +from kiauh.components.mainsail.mainsail_utils import backup_mainsail_data +from kiauh.components.moonraker.moonraker_utils import ( + backup_moonraker_dir, + backup_moonraker_db_dir, + ) +from kiauh.core.menus import BACK_FOOTER +from kiauh.core.menus.base_menu import BaseMenu +from kiauh.utils.common import backup_printer_config_dir +from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class BackupMenu(BaseMenu): + def __init__(self): + super().__init__( + header=True, + options={ + "1": self.backup_klipper, + "2": self.backup_moonraker, + "3": self.backup_printer_config, + "4": self.backup_moonraker_db, + "5": self.backup_mainsail + }, + footer_type=BACK_FOOTER, + ) + + def print_menu(self): + header = " [ Backup Menu ] " + line1 = f"{COLOR_YELLOW}INFO: Backups are located in '~/kiauh-backups'{RESET_FORMAT}" + color = COLOR_CYAN + count = 62 - len(color) - len(RESET_FORMAT) + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {line1:^62} | + |-------------------------------------------------------| + | Klipper & Moonraker API: | Touchscreen GUI: | + | 1) [Klipper] | 7) [KlipperScreen] | + | 2) [Moonraker] | | + | 3) [Config Folder] | Other: | + | 4) [Moonraker Database] | 9) [Telegram Bot] | + | | | + | Klipper Webinterface: | | + | 5) [Mainsail] | | + | 6) [Fluidd] | | + """ + )[1:] + print(menu, end="") + + def backup_klipper(self, **kwargs): + backup_klipper_dir() + + def backup_moonraker(self, **kwargs): + backup_moonraker_dir() + + def backup_printer_config(self, **kwargs): + backup_printer_config_dir() + + def backup_moonraker_db(self, **kwargs): + backup_moonraker_db_dir() + + def backup_mainsail(self, **kwargs): + backup_mainsail_data() + + def backup_fluidd(self, **kwargs): + pass + + def backup_klipperscreen(self, **kwargs): + pass + + def backup_telegram_bot(self, **kwargs): + pass diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 366b99a..14c80cc 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -11,18 +11,19 @@ import textwrap +from kiauh.components.klipper.klipper_utils import get_klipper_status +from kiauh.components.log_uploads.menus.log_upload_menu import LogUploadMenu +from kiauh.components.mainsail.mainsail_utils import get_mainsail_status +from kiauh.components.moonraker.moonraker_utils import get_moonraker_status from kiauh.core.menus import QUIT_FOOTER from kiauh.core.menus.advanced_menu import AdvancedMenu +from kiauh.core.menus.backup_menu import BackupMenu from kiauh.core.menus.base_menu import BaseMenu from kiauh.core.menus.extensions_menu import ExtensionsMenu from kiauh.core.menus.install_menu import InstallMenu from kiauh.core.menus.remove_menu import RemoveMenu from kiauh.core.menus.settings_menu import SettingsMenu from kiauh.core.menus.update_menu import UpdateMenu -from kiauh.components.klipper.klipper_utils import get_klipper_status -from kiauh.components.log_uploads.menus.log_upload_menu import LogUploadMenu -from kiauh.components.mainsail.mainsail_utils import get_mainsail_status -from kiauh.components.moonraker.moonraker_utils import get_moonraker_status from kiauh.utils.constants import ( COLOR_MAGENTA, COLOR_CYAN, @@ -30,9 +31,10 @@ from kiauh.utils.constants import ( COLOR_RED, COLOR_GREEN, COLOR_YELLOW, -) + ) +# noinspection PyMethodMayBeStatic class MainMenu(BaseMenu): def __init__(self): super().__init__( @@ -43,7 +45,7 @@ class MainMenu(BaseMenu): "2": UpdateMenu, "3": RemoveMenu, "4": AdvancedMenu, - "5": None, + "5": BackupMenu, "e": ExtensionsMenu, "s": SettingsMenu, }, diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index f097b5d..319fc15 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -11,8 +11,11 @@ from pathlib import Path +from kiauh.core.backup_manager import BACKUP_ROOT_DIR + MODULE_PATH = Path(__file__).resolve().parent INVALID_CHOICE = "Invalid choice. Please select a valid value." +PRINTER_CFG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-cfg-backups") # ================== NGINX =====================# NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index fbae241..222b58d 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -13,15 +13,17 @@ from datetime import datetime from pathlib import Path from typing import Dict, Literal, List, Type, Union +from kiauh.components.klipper.klipper import Klipper from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.utils import PRINTER_CFG_BACKUP_DIR from kiauh.utils.constants import ( COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_GREEN, COLOR_RED, -) + ) from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.logger import Logger from kiauh.utils.system_utils import check_package_install, install_system_packages @@ -116,3 +118,20 @@ def get_install_status_webui( return f"{COLOR_RED}Not installed!{RESET_FORMAT}" else: return f"{COLOR_YELLOW}Incomplete!{RESET_FORMAT}" + + +def backup_printer_config_dir(): + # local import to prevent circular import + from kiauh.core.backup_manager.backup_manager import BackupManager + + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + bm = BackupManager() + + for instance in instances: + name = f"config-{instance.data_dir_name}" + bm.backup_directory( + name, + source=instance.cfg_dir, + target=PRINTER_CFG_BACKUP_DIR, + ) From be5f345a7c8ad0c7d724167acd02da40f1840dfe Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 10 Feb 2024 11:51:43 +0100 Subject: [PATCH 105/296] style: reformat code Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper.py | 2 +- kiauh/components/klipper/klipper_remove.py | 2 +- kiauh/components/klipper/klipper_setup.py | 6 +-- kiauh/components/klipper/klipper_utils.py | 14 +++---- .../klipper/menus/klipper_remove_menu.py | 2 +- .../log_uploads/log_upload_utils.py | 7 ++-- .../log_uploads/menus/log_upload_menu.py | 4 +- kiauh/components/mainsail/mainsail_remove.py | 4 +- kiauh/components/mainsail/mainsail_setup.py | 6 +-- kiauh/components/mainsail/mainsail_utils.py | 2 +- .../mainsail/menus/mainsail_remove_menu.py | 2 +- .../moonraker/menus/moonraker_remove_menu.py | 2 +- .../components/moonraker/moonraker_dialogs.py | 10 ++--- .../components/moonraker/moonraker_remove.py | 2 +- kiauh/components/moonraker/moonraker_setup.py | 8 ++-- kiauh/components/moonraker/moonraker_utils.py | 14 +++---- kiauh/core/backup_manager/backup_manager.py | 5 +-- .../core/instance_manager/instance_manager.py | 1 - kiauh/core/menus/backup_menu.py | 4 +- kiauh/core/menus/base_menu.py | 2 +- kiauh/core/menus/extensions_menu.py | 4 +- kiauh/core/menus/install_menu.py | 4 +- kiauh/core/menus/main_menu.py | 2 +- kiauh/core/menus/remove_menu.py | 4 +- kiauh/core/menus/update_menu.py | 4 +- kiauh/extensions/gcode_shell_cmd/__init__.py | 1 - .../assets/gcode_shell_command.py | 37 +++++++++++-------- .../gcode_shell_cmd_extension.py | 2 +- kiauh/utils/common.py | 2 +- kiauh/utils/system_utils.py | 2 +- 30 files changed, 79 insertions(+), 82 deletions(-) diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index 1f8e6e5..1b442c6 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -13,8 +13,8 @@ import subprocess from pathlib import Path from typing import List -from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH +from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index 3534094..61b007c 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -12,10 +12,10 @@ import shutil from typing import List, Union -from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR from kiauh.components.klipper.klipper import Klipper from kiauh.components.klipper.klipper_dialogs import print_instance_overview +from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.utils.filesystem_utils import remove_file from kiauh.utils.input_utils import get_selection_input from kiauh.utils.logger import Logger diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 05cfb1c..1e553f2 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -18,7 +18,7 @@ from kiauh.components.klipper import ( KLIPPER_DIR, KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT, - ) +) from kiauh.components.klipper.klipper import Klipper from kiauh.components.klipper.klipper_dialogs import print_update_warn_dialog from kiauh.components.klipper.klipper_utils import ( @@ -33,7 +33,7 @@ from kiauh.components.klipper.klipper_utils import ( update_name_scheme, handle_instance_naming, backup_klipper_dir, - ) +) from kiauh.components.moonraker.moonraker import Moonraker from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager @@ -46,7 +46,7 @@ from kiauh.utils.system_utils import ( install_python_requirements, update_system_package_lists, install_system_packages, - ) +) def install_klipper() -> None: diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 373afc3..564d7f7 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -23,14 +23,14 @@ from kiauh.components.klipper import ( KLIPPER_DIR, KLIPPER_ENV_DIR, KLIPPER_BACKUP_DIR, - ) +) from kiauh.components.klipper.klipper import Klipper from kiauh.components.klipper.klipper_dialogs import ( print_missing_usergroup_dialog, print_instance_overview, print_select_instance_count_dialog, print_select_custom_name_dialog, - ) +) from kiauh.components.moonraker.moonraker import Moonraker from kiauh.components.moonraker.moonraker_utils import moonraker_to_multi_conversion from kiauh.core.backup_manager.backup_manager import BackupManager @@ -46,12 +46,10 @@ from kiauh.utils.logger import Logger from kiauh.utils.system_utils import mask_system_service -def get_klipper_status() -> ( - Dict[ - Literal["status", "status_code", "instances", "repo", "local", "remote"], - Union[str, int], - ] -): +def get_klipper_status() -> Dict[ + Literal["status", "status_code", "instances", "repo", "local", "remote"], + Union[str, int], +]: status = get_install_status_common(Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR) return { "status": status.get("status"), diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index 7ee5461..a36d51b 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -11,9 +11,9 @@ import textwrap +from kiauh.components.klipper import klipper_remove from kiauh.core.menus import BACK_HELP_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.components.klipper import klipper_remove from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN diff --git a/kiauh/components/log_uploads/log_upload_utils.py b/kiauh/components/log_uploads/log_upload_utils.py index 9ce95c8..4247a0e 100644 --- a/kiauh/components/log_uploads/log_upload_utils.py +++ b/kiauh/components/log_uploads/log_upload_utils.py @@ -9,14 +9,13 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from typing import List -from pathlib import Path - import urllib.request +from pathlib import Path +from typing import List -from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.components.klipper.klipper import Klipper from kiauh.components.log_uploads import LogFile +from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.utils.logger import Logger diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index c140b2e..11bb65e 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -11,10 +11,10 @@ import textwrap +from kiauh.components.log_uploads.log_upload_utils import get_logfile_list +from kiauh.components.log_uploads.log_upload_utils import upload_logfile from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.components.log_uploads.log_upload_utils import upload_logfile -from kiauh.components.log_uploads.log_upload_utils import get_logfile_list from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW diff --git a/kiauh/components/mainsail/mainsail_remove.py b/kiauh/components/mainsail/mainsail_remove.py index fc396ba..3a4f797 100644 --- a/kiauh/components/mainsail/mainsail_remove.py +++ b/kiauh/components/mainsail/mainsail_remove.py @@ -15,12 +15,12 @@ import subprocess from pathlib import Path from typing import List -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.components.klipper.klipper import Klipper from kiauh.components.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR from kiauh.components.mainsail.mainsail_utils import backup_config_json from kiauh.components.moonraker.moonraker import Moonraker +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from kiauh.utils.filesystem_utils import remove_file from kiauh.utils.logger import Logger diff --git a/kiauh/components/mainsail/mainsail_setup.py b/kiauh/components/mainsail/mainsail_setup.py index 33b764a..e370847 100644 --- a/kiauh/components/mainsail/mainsail_setup.py +++ b/kiauh/components/mainsail/mainsail_setup.py @@ -14,9 +14,6 @@ from pathlib import Path from typing import List from kiauh import KIAUH_CFG -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.components.klipper.klipper import Klipper from kiauh.components.mainsail import ( MAINSAIL_URL, @@ -38,6 +35,9 @@ from kiauh.components.mainsail.mainsail_utils import ( symlink_webui_nginx_log, ) from kiauh.components.moonraker.moonraker import Moonraker +from kiauh.core.config_manager.config_manager import ConfigManager +from kiauh.core.instance_manager.instance_manager import InstanceManager +from kiauh.core.repo_manager.repo_manager import RepoManager from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from kiauh.utils.common import check_install_dependencies from kiauh.utils.filesystem_utils import ( diff --git a/kiauh/components/mainsail/mainsail_utils.py b/kiauh/components/mainsail/mainsail_utils.py index 9a01e9d..3cb7102 100644 --- a/kiauh/components/mainsail/mainsail_utils.py +++ b/kiauh/components/mainsail/mainsail_utils.py @@ -21,7 +21,7 @@ from kiauh.components.mainsail import ( MAINSAIL_CONFIG_JSON, MAINSAIL_DIR, MAINSAIL_BACKUP_DIR, - ) +) from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_CONFD from kiauh.utils.common import get_install_status_webui diff --git a/kiauh/components/mainsail/menus/mainsail_remove_menu.py b/kiauh/components/mainsail/menus/mainsail_remove_menu.py index 47488fc..187d974 100644 --- a/kiauh/components/mainsail/menus/mainsail_remove_menu.py +++ b/kiauh/components/mainsail/menus/mainsail_remove_menu.py @@ -11,9 +11,9 @@ import textwrap +from kiauh.components.mainsail import mainsail_remove from kiauh.core.menus import BACK_HELP_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.components.mainsail import mainsail_remove from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index f8bc307..962e07d 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -11,9 +11,9 @@ import textwrap +from kiauh.components.moonraker import moonraker_remove from kiauh.core.menus import BACK_HELP_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.components.moonraker import moonraker_remove from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN diff --git a/kiauh/components/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py index 62867bd..2bc18b2 100644 --- a/kiauh/components/moonraker/moonraker_dialogs.py +++ b/kiauh/components/moonraker/moonraker_dialogs.py @@ -12,9 +12,9 @@ import textwrap from typing import List -from kiauh.core.menus.base_menu import print_back_footer from kiauh.components.klipper.klipper import Klipper from kiauh.components.moonraker.moonraker import Moonraker +from kiauh.core.menus.base_menu import print_back_footer from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN @@ -39,11 +39,11 @@ def print_moonraker_overview( dialog += "| |\n" instance_map = { - k.get_service_file_name(): k.get_service_file_name().replace( - "klipper", "moonraker" + k.get_service_file_name(): ( + k.get_service_file_name().replace("klipper", "moonraker") + if k.suffix in [m.suffix for m in moonraker_instances] + else "" ) - if k.suffix in [m.suffix for m in moonraker_instances] - else "" for k in klipper_instances } diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index cc76be8..02af9dd 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -13,10 +13,10 @@ import shutil import subprocess from typing import List, Union -from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.components.klipper.klipper_dialogs import print_instance_overview from kiauh.components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR from kiauh.components.moonraker.moonraker import Moonraker +from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.utils.filesystem_utils import remove_file from kiauh.utils.input_utils import get_selection_input from kiauh.utils.logger import Logger diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 9ed8fa0..f041a0e 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -29,13 +29,13 @@ from kiauh.components.moonraker import ( POLKIT_FILE, POLKIT_USR_FILE, POLKIT_SCRIPT, - ) +) from kiauh.components.moonraker.moonraker import Moonraker from kiauh.components.moonraker.moonraker_dialogs import print_moonraker_overview from kiauh.components.moonraker.moonraker_utils import ( create_example_moonraker_conf, backup_moonraker_dir, - ) +) from kiauh.core.config_manager.config_manager import ConfigManager from kiauh.core.instance_manager.instance_manager import InstanceManager from kiauh.core.repo_manager.repo_manager import RepoManager @@ -43,7 +43,7 @@ from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.input_utils import ( get_confirm, get_selection_input, - ) +) from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( parse_packages_from_file, @@ -51,7 +51,7 @@ from kiauh.utils.system_utils import ( install_python_requirements, update_system_package_lists, install_system_packages, - ) +) def install_moonraker() -> None: diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index e3db3c6..0a9ae11 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -21,7 +21,7 @@ from kiauh.components.moonraker import ( MOONRAKER_ENV_DIR, MOONRAKER_BACKUP_DIR, MOONRAKER_DB_BACKUP_DIR, - ) +) from kiauh.components.moonraker.moonraker import Moonraker from kiauh.core.backup_manager.backup_manager import BackupManager from kiauh.core.config_manager.config_manager import ConfigManager @@ -31,15 +31,13 @@ from kiauh.utils.common import get_install_status_common from kiauh.utils.logger import Logger from kiauh.utils.system_utils import ( get_ipv4_addr, - ) +) -def get_moonraker_status() -> ( - Dict[ - Literal["status", "status_code", "instances", "repo", "local", "remote"], - Union[str, int], - ] -): +def get_moonraker_status() -> Dict[ + Literal["status", "status_code", "instances", "repo", "local", "remote"], + Union[str, int], +]: status = get_install_status_common(Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR) return { "status": status.get("status"), diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 7f5a887..8f2dbcf 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -41,9 +41,7 @@ class BackupManager: def ignore_folders(self, value: List[str]): self._ignore_folders = value - def backup_file( - self, file: Path = None, target: Path = None, custom_filename=None - ): + def backup_file(self, file: Path = None, target: Path = None, custom_filename=None): if not file: raise ValueError("Parameter 'file' cannot be None!") @@ -84,7 +82,6 @@ class BackupManager: Logger.print_error(f"Unable to backup directory '{source}':\n{e}") return - def ignore_folders_func(self, dirpath, filenames): return ( [f for f in filenames if f in self._ignore_folders] diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index d4e2f28..0a920d6 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -18,7 +18,6 @@ from kiauh.core.instance_manager.base_instance import BaseInstance from kiauh.utils.constants import SYSTEMD from kiauh.utils.logger import Logger - I = TypeVar(name="I", bound=BaseInstance, covariant=True) diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index ffc246b..184e26c 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -16,7 +16,7 @@ from kiauh.components.mainsail.mainsail_utils import backup_mainsail_data from kiauh.components.moonraker.moonraker_utils import ( backup_moonraker_dir, backup_moonraker_db_dir, - ) +) from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu from kiauh.utils.common import backup_printer_config_dir @@ -34,7 +34,7 @@ class BackupMenu(BaseMenu): "2": self.backup_moonraker, "3": self.backup_printer_config, "4": self.backup_moonraker_db, - "5": self.backup_mainsail + "5": self.backup_mainsail, }, footer_type=BACK_FOOTER, ) diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 2cdf379..89ed09b 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -13,7 +13,7 @@ import subprocess import sys import textwrap from abc import abstractmethod, ABC -from typing import Dict, Any, Literal, Union, Callable, Type +from typing import Dict, Any, Literal, Union, Callable from kiauh.core.menus import QUIT_FOOTER, BACK_FOOTER, BACK_HELP_FOOTER from kiauh.utils.constants import ( diff --git a/kiauh/core/menus/extensions_menu.py b/kiauh/core/menus/extensions_menu.py index ebbc4c5..50fd53b 100644 --- a/kiauh/core/menus/extensions_menu.py +++ b/kiauh/core/menus/extensions_menu.py @@ -9,10 +9,10 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import json -import textwrap import importlib import inspect +import json +import textwrap from pathlib import Path from typing import List, Dict diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 239dfaf..85e1ef3 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -11,11 +11,11 @@ import textwrap -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu from kiauh.components.klipper import klipper_setup from kiauh.components.mainsail import mainsail_setup from kiauh.components.moonraker import moonraker_setup +from kiauh.core.menus import BACK_FOOTER +from kiauh.core.menus.base_menu import BaseMenu from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 14c80cc..60dc9f8 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -31,7 +31,7 @@ from kiauh.utils.constants import ( COLOR_RED, COLOR_GREEN, COLOR_YELLOW, - ) +) # noinspection PyMethodMayBeStatic diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 060927e..1bd2eba 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -11,11 +11,11 @@ import textwrap -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu from kiauh.components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu from kiauh.components.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu from kiauh.components.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu +from kiauh.core.menus import BACK_FOOTER +from kiauh.core.menus.base_menu import BaseMenu from kiauh.utils.constants import COLOR_RED, RESET_FORMAT diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index c90de1c..f2951dd 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -11,8 +11,6 @@ import textwrap -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu from kiauh.components.klipper.klipper_setup import update_klipper from kiauh.components.klipper.klipper_utils import ( get_klipper_status, @@ -24,6 +22,8 @@ from kiauh.components.mainsail.mainsail_utils import ( ) from kiauh.components.moonraker.moonraker_setup import update_moonraker from kiauh.components.moonraker.moonraker_utils import get_moonraker_status +from kiauh.core.menus import BACK_FOOTER +from kiauh.core.menus.base_menu import BaseMenu from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_WHITE diff --git a/kiauh/extensions/gcode_shell_cmd/__init__.py b/kiauh/extensions/gcode_shell_cmd/__init__.py index c0d921b..27ffecb 100644 --- a/kiauh/extensions/gcode_shell_cmd/__init__.py +++ b/kiauh/extensions/gcode_shell_cmd/__init__.py @@ -11,7 +11,6 @@ from pathlib import Path - EXT_MODULE_NAME = "gcode_shell_command.py" MODULE_PATH = Path(__file__).resolve().parent MODULE_ASSETS = MODULE_PATH.joinpath("assets") diff --git a/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py b/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py index bb38ae5..85b664b 100644 --- a/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py +++ b/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py @@ -3,27 +3,31 @@ # Copyright (C) 2019 Eric Callahan # # This file may be distributed under the terms of the GNU GPLv3 license. +import logging import os import shlex import subprocess -import logging + class ShellCommand: def __init__(self, config): self.name = config.get_name().split()[-1] self.printer = config.get_printer() - self.gcode = self.printer.lookup_object('gcode') - cmd = config.get('command') + self.gcode = self.printer.lookup_object("gcode") + cmd = config.get("command") cmd = os.path.expanduser(cmd) self.command = shlex.split(cmd) - self.timeout = config.getfloat('timeout', 2., above=0.) - self.verbose = config.getboolean('verbose', True) + self.timeout = config.getfloat("timeout", 2.0, above=0.0) + self.verbose = config.getboolean("verbose", True) self.proc_fd = None self.partial_output = "" self.gcode.register_mux_command( - "RUN_SHELL_COMMAND", "CMD", self.name, + "RUN_SHELL_COMMAND", + "CMD", + self.name, self.cmd_RUN_SHELL_COMMAND, - desc=self.cmd_RUN_SHELL_COMMAND_help) + desc=self.cmd_RUN_SHELL_COMMAND_help, + ) def _process_output(self, eventime): if self.proc_fd is None: @@ -33,11 +37,11 @@ class ShellCommand: except Exception: pass data = self.partial_output + data.decode() - if '\n' not in data: + if "\n" not in data: self.partial_output = data return - elif data[-1] != '\n': - split = data.rfind('\n') + 1 + elif data[-1] != "\n": + split = data.rfind("\n") + 1 self.partial_output = data[split:] data = data[:split] else: @@ -45,16 +49,19 @@ class ShellCommand: self.gcode.respond_info(data) cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command" + def cmd_RUN_SHELL_COMMAND(self, params): - gcode_params = params.get('PARAMS','') + gcode_params = params.get("PARAMS", "") gcode_params = shlex.split(gcode_params) reactor = self.printer.get_reactor() try: proc = subprocess.Popen( - self.command + gcode_params, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + self.command + gcode_params, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) except Exception: - logging.exception( - "shell_command: Command {%s} failed" % (self.name)) + logging.exception("shell_command: Command {%s} failed" % (self.name)) raise self.gcode.error("Error running command {%s}" % (self.name)) if self.verbose: self.proc_fd = proc.stdout.fileno() @@ -64,7 +71,7 @@ class ShellCommand: endtime = eventtime + self.timeout complete = False while eventtime < endtime: - eventtime = reactor.pause(eventtime + .05) + eventtime = reactor.pause(eventtime + 0.05) if proc.poll() is not None: complete = True break diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index c0c28c1..a9798e2 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -24,7 +24,7 @@ from kiauh.extensions.gcode_shell_cmd import ( KLIPPER_DIR, EXAMPLE_CFG_SRC, KLIPPER_EXTRAS, - ) +) from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 222b58d..6557cd0 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -23,7 +23,7 @@ from kiauh.utils.constants import ( COLOR_YELLOW, COLOR_GREEN, COLOR_RED, - ) +) from kiauh.utils.filesystem_utils import check_file_exist from kiauh.utils.logger import Logger from kiauh.utils.system_utils import check_package_install, install_system_packages diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index fce0977..1f3b018 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -285,7 +285,7 @@ def download_progress(block_num, block_size, total_size) -> None: mb = 1024 * 1024 progress = int(percent / 5) remaining = "-" * (20 - progress) - dl = f"\rDownloading: [{'#' * progress}{remaining}]{percent:.2f}% ({downloaded/mb:.2f}/{total_size/mb:.2f}MB)" + dl = f"\rDownloading: [{'#' * progress}{remaining}]{percent:.2f}% ({downloaded / mb:.2f}/{total_size / mb:.2f}MB)" sys.stdout.write(dl) sys.stdout.flush() From 863c62511c80686e5b761aa405487f7890c7b627 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 11 Feb 2024 20:15:35 +0100 Subject: [PATCH 106/296] fix(klipper): add python3-venv dependency for creating venv Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 1e553f2..2cf5690 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -132,6 +132,7 @@ def install_klipper_packages(klipper_dir: Path) -> None: script = klipper_dir.joinpath("scripts/install-debian.sh") packages = parse_packages_from_file(script) packages = [pkg.replace("python-dev", "python3-dev") for pkg in packages] + packages.append("python3-venv") # Add dfu-util for octopi-images packages.append("dfu-util") # Add dbus requirement for DietPi distro From 05b4ef2d18152dd6c03cdef359ed1bb5be328bf1 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 11 Feb 2024 20:16:21 +0100 Subject: [PATCH 107/296] refactor(utils): raise exception if pip not found in venv Signed-off-by: Dominik Willner --- kiauh/utils/system_utils.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 1f3b018..352d575 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -23,6 +23,7 @@ from typing import List, Literal from kiauh.utils.input_utils import get_confirm from kiauh.utils.logger import Logger +from kiauh.utils.filesystem_utils import check_file_exist def kill(opt_err_msg: str = "") -> None: @@ -98,7 +99,12 @@ def update_python_pip(target: Path) -> None: """ Logger.print_status("Updating pip ...") try: - command = [target.joinpath("bin/pip"), "install", "-U", "pip"] + pip_location = target.joinpath("bin/pip") + pip_exists = check_file_exist(pip_location) + if not pip_exists: + raise FileNotFoundError("Error updating pip! Not found.") + + command = [pip_location, "install", "-U", "pip"] result = subprocess.run(command, stderr=subprocess.PIPE, text=True) if result.returncode != 0 or result.stderr: Logger.print_error(f"{result.stderr}", False) @@ -106,8 +112,12 @@ def update_python_pip(target: Path) -> None: return Logger.print_ok("Updating pip successfull!") + except FileNotFoundError as e: + Logger.print_error(e) + raise except subprocess.CalledProcessError as e: Logger.print_error(f"Error updating pip:\n{e.output.decode()}") + raise def install_python_requirements(target: Path, requirements: Path) -> None: @@ -117,9 +127,9 @@ def install_python_requirements(target: Path, requirements: Path) -> None: :param requirements: Path to the requirements.txt file :return: None """ - update_python_pip(target) Logger.print_status("Installing Python requirements ...") try: + update_python_pip(target) command = [target.joinpath("bin/pip"), "install", "-r", f"{requirements}"] result = subprocess.run(command, stderr=subprocess.PIPE, text=True) if result.returncode != 0 or result.stderr: From 573dc7c3c970a4f395e293001b486817f8599794 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 18 Feb 2024 22:08:21 +0100 Subject: [PATCH 108/296] refactor(Mainsail): use urllib.request instead of requests module requests is actually not part of the python 3.8 standard library, hence we use urllib.request now, which is. Signed-off-by: Dominik Willner --- kiauh/components/mainsail/mainsail_utils.py | 12 ++++++++---- kiauh/core/menus/update_menu.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/kiauh/components/mainsail/mainsail_utils.py b/kiauh/components/mainsail/mainsail_utils.py index 3cb7102..b2aa32b 100644 --- a/kiauh/components/mainsail/mainsail_utils.py +++ b/kiauh/components/mainsail/mainsail_utils.py @@ -11,10 +11,11 @@ import json import shutil +from json import JSONDecodeError from pathlib import Path from typing import List -import requests +import urllib.request from kiauh.components.klipper.klipper import Klipper from kiauh.components.mainsail import ( @@ -99,9 +100,12 @@ def get_mainsail_local_version() -> str: def get_mainsail_remote_version() -> str: url = "https://api.github.com/repos/mainsail-crew/mainsail/tags" - response = requests.get(url) - data = json.loads(response.text) - return data[0]["name"] + try: + with urllib.request.urlopen(url) as response: + data = json.loads(response.read()) + return data[0]["name"] + except (JSONDecodeError, TypeError): + return "ERROR" def backup_mainsail_data() -> None: diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index f2951dd..ba25e07 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -24,7 +24,13 @@ from kiauh.components.moonraker.moonraker_setup import update_moonraker from kiauh.components.moonraker.moonraker_utils import get_moonraker_status from kiauh.core.menus import BACK_FOOTER from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_WHITE +from kiauh.utils.constants import ( + COLOR_GREEN, + RESET_FORMAT, + COLOR_YELLOW, + COLOR_WHITE, + COLOR_RED, +) # noinspection PyUnusedLocal @@ -159,4 +165,4 @@ class UpdateMenu(BaseMenu): self.ms_local = f"{COLOR_GREEN}{self.ms_local}{RESET_FORMAT}" else: self.ms_local = f"{COLOR_YELLOW}{self.ms_local}{RESET_FORMAT}" - self.ms_remote = f"{COLOR_GREEN}{self.ms_remote}{RESET_FORMAT}" + self.ms_remote = f"{COLOR_GREEN if self.ms_remote != 'ERROR' else COLOR_RED}{self.ms_remote}{RESET_FORMAT}" From 2a4fcf3a3a821fbe2078c2ef312d5221f4eefb55 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 18 Feb 2024 22:30:28 +0100 Subject: [PATCH 109/296] refactor(KIAUH): add application root to sys path shortens imports and helps with auto imports from IDE Signed-off-by: Dominik Willner --- kiauh/__init__.py | 8 +++-- kiauh/components/klipper/__init__.py | 2 +- kiauh/components/klipper/klipper.py | 8 ++--- kiauh/components/klipper/klipper_dialogs.py | 6 ++-- kiauh/components/klipper/klipper_remove.py | 14 ++++---- kiauh/components/klipper/klipper_setup.py | 22 ++++++------- kiauh/components/klipper/klipper_utils.py | 32 +++++++++---------- .../klipper/menus/klipper_remove_menu.py | 8 ++--- .../log_uploads/log_upload_utils.py | 8 ++--- .../log_uploads/menus/log_upload_menu.py | 10 +++--- kiauh/components/mainsail/__init__.py | 2 +- kiauh/components/mainsail/mainsail_dialogs.py | 4 +-- kiauh/components/mainsail/mainsail_remove.py | 18 +++++------ kiauh/components/mainsail/mainsail_setup.py | 28 ++++++++-------- kiauh/components/mainsail/mainsail_utils.py | 12 +++---- .../mainsail/menus/mainsail_remove_menu.py | 8 ++--- kiauh/components/moonraker/__init__.py | 2 +- .../moonraker/menus/moonraker_remove_menu.py | 8 ++--- kiauh/components/moonraker/moonraker.py | 10 +++--- .../components/moonraker/moonraker_dialogs.py | 8 ++--- .../components/moonraker/moonraker_remove.py | 14 ++++---- kiauh/components/moonraker/moonraker_setup.py | 30 ++++++++--------- kiauh/components/moonraker/moonraker_utils.py | 22 ++++++------- kiauh/core/backup_manager/backup_manager.py | 6 ++-- kiauh/core/config_manager/config_manager.py | 2 +- kiauh/core/instance_manager/base_instance.py | 2 +- .../core/instance_manager/instance_manager.py | 6 ++-- kiauh/core/menus/advanced_menu.py | 6 ++-- kiauh/core/menus/backup_menu.py | 14 ++++---- kiauh/core/menus/base_menu.py | 6 ++-- kiauh/core/menus/extensions_menu.py | 8 ++--- kiauh/core/menus/install_menu.py | 13 ++++---- kiauh/core/menus/remove_menu.py | 12 +++---- kiauh/core/menus/settings_menu.py | 2 +- kiauh/core/repo_manager/repo_manager.py | 4 +-- .../gcode_shell_cmd_extension.py | 18 +++++------ kiauh/main.py | 4 +-- kiauh/utils/__init__.py | 2 +- kiauh/utils/common.py | 18 +++++------ kiauh/utils/filesystem_utils.py | 4 +-- kiauh/utils/input_utils.py | 6 ++-- kiauh/utils/logger.py | 2 +- kiauh/utils/system_utils.py | 6 ++-- 43 files changed, 215 insertions(+), 210 deletions(-) diff --git a/kiauh/__init__.py b/kiauh/__init__.py index 91fd073..f93f7de 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -9,7 +9,11 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import sys from pathlib import Path -APPLICATION_ROOT = Path(__file__).resolve().parent.parent -KIAUH_CFG = APPLICATION_ROOT.joinpath("kiauh.cfg") +PROJECT_ROOT = Path(__file__).resolve().parent.parent +KIAUH_CFG = PROJECT_ROOT.joinpath("kiauh.cfg") + +APPLICATION_ROOT = Path(__file__).resolve().parent +sys.path.append(str(APPLICATION_ROOT)) diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index ff3ae20..9f46783 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -11,7 +11,7 @@ from pathlib import Path -from kiauh.core.backup_manager import BACKUP_ROOT_DIR +from core.backup_manager import BACKUP_ROOT_DIR MODULE_PATH = Path(__file__).resolve().parent diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index 1b442c6..05eef56 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -13,10 +13,10 @@ import subprocess from pathlib import Path from typing import List -from kiauh.components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH -from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.utils.constants import SYSTEMD -from kiauh.utils.logger import Logger +from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH +from core.instance_manager.base_instance import BaseInstance +from utils.constants import SYSTEMD +from utils.logger import Logger # noinspection PyMethodMayBeStatic diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py index 94e9374..30892c5 100644 --- a/kiauh/components/klipper/klipper_dialogs.py +++ b/kiauh/components/klipper/klipper_dialogs.py @@ -12,9 +12,9 @@ import textwrap from typing import List -from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.core.menus.base_menu import print_back_footer -from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN +from core.instance_manager.base_instance import BaseInstance +from core.menus.base_menu import print_back_footer +from utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN def print_instance_overview( diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index 61b007c..d802d4b 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -12,13 +12,13 @@ import shutil from typing import List, Union -from kiauh.components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR -from kiauh.components.klipper.klipper import Klipper -from kiauh.components.klipper.klipper_dialogs import print_instance_overview -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.utils.filesystem_utils import remove_file -from kiauh.utils.input_utils import get_selection_input -from kiauh.utils.logger import Logger +from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR +from components.klipper.klipper import Klipper +from components.klipper.klipper_dialogs import print_instance_overview +from core.instance_manager.instance_manager import InstanceManager +from utils.filesystem_utils import remove_file +from utils.input_utils import get_selection_input +from utils.logger import Logger def run_klipper_removal( diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 2cf5690..e9761e9 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -12,16 +12,16 @@ from pathlib import Path from kiauh import KIAUH_CFG -from kiauh.components.klipper import ( +from components.klipper import ( EXIT_KLIPPER_SETUP, DEFAULT_KLIPPER_REPO_URL, KLIPPER_DIR, KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT, ) -from kiauh.components.klipper.klipper import Klipper -from kiauh.components.klipper.klipper_dialogs import print_update_warn_dialog -from kiauh.components.klipper.klipper_utils import ( +from components.klipper.klipper import Klipper +from components.klipper.klipper_dialogs import print_update_warn_dialog +from components.klipper.klipper_utils import ( handle_disruptive_system_packages, check_user_groups, handle_to_multi_instance_conversion, @@ -34,13 +34,13 @@ from kiauh.components.klipper.klipper_utils import ( handle_instance_naming, backup_klipper_dir, ) -from kiauh.components.moonraker.moonraker import Moonraker -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.utils.input_utils import get_confirm -from kiauh.utils.logger import Logger -from kiauh.utils.system_utils import ( +from components.moonraker.moonraker import Moonraker +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from core.repo_manager.repo_manager import RepoManager +from utils.input_utils import get_confirm +from utils.logger import Logger +from utils.system_utils import ( parse_packages_from_file, create_python_venv, install_python_requirements, diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 564d7f7..e2ff73a 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -18,32 +18,32 @@ import textwrap from pathlib import Path from typing import List, Union, Literal, Dict -from kiauh.components.klipper import ( +from components.klipper import ( MODULE_PATH, KLIPPER_DIR, KLIPPER_ENV_DIR, KLIPPER_BACKUP_DIR, ) -from kiauh.components.klipper.klipper import Klipper -from kiauh.components.klipper.klipper_dialogs import ( +from components.klipper.klipper import Klipper +from components.klipper.klipper_dialogs import ( print_missing_usergroup_dialog, print_instance_overview, print_select_instance_count_dialog, print_select_custom_name_dialog, ) -from kiauh.components.moonraker.moonraker import Moonraker -from kiauh.components.moonraker.moonraker_utils import moonraker_to_multi_conversion -from kiauh.core.backup_manager.backup_manager import BackupManager -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.core.instance_manager.name_scheme import NameScheme -from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.utils.common import get_install_status_common -from kiauh.utils.constants import CURRENT_USER -from kiauh.utils.input_utils import get_confirm, get_string_input, get_number_input -from kiauh.utils.logger import Logger -from kiauh.utils.system_utils import mask_system_service +from components.moonraker.moonraker import Moonraker +from components.moonraker.moonraker_utils import moonraker_to_multi_conversion +from core.backup_manager.backup_manager import BackupManager +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.base_instance import BaseInstance +from core.instance_manager.instance_manager import InstanceManager +from core.instance_manager.name_scheme import NameScheme +from core.repo_manager.repo_manager import RepoManager +from utils.common import get_install_status_common +from utils.constants import CURRENT_USER +from utils.input_utils import get_confirm, get_string_input, get_number_input +from utils.logger import Logger +from utils.system_utils import mask_system_service def get_klipper_status() -> Dict[ diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index a36d51b..fc953ab 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -11,10 +11,10 @@ import textwrap -from kiauh.components.klipper import klipper_remove -from kiauh.core.menus import BACK_HELP_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN +from components.klipper import klipper_remove +from core.menus import BACK_HELP_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal diff --git a/kiauh/components/log_uploads/log_upload_utils.py b/kiauh/components/log_uploads/log_upload_utils.py index 4247a0e..daa67e4 100644 --- a/kiauh/components/log_uploads/log_upload_utils.py +++ b/kiauh/components/log_uploads/log_upload_utils.py @@ -13,10 +13,10 @@ import urllib.request from pathlib import Path from typing import List -from kiauh.components.klipper.klipper import Klipper -from kiauh.components.log_uploads import LogFile -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.utils.logger import Logger +from components.klipper.klipper import Klipper +from components.log_uploads import LogFile +from core.instance_manager.instance_manager import InstanceManager +from utils.logger import Logger def get_logfile_list() -> List[LogFile]: diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index 11bb65e..baccce5 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -11,11 +11,11 @@ import textwrap -from kiauh.components.log_uploads.log_upload_utils import get_logfile_list -from kiauh.components.log_uploads.log_upload_utils import upload_logfile -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW +from components.log_uploads.log_upload_utils import get_logfile_list +from components.log_uploads.log_upload_utils import upload_logfile +from core.menus import BACK_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import RESET_FORMAT, COLOR_YELLOW # noinspection PyMethodMayBeStatic diff --git a/kiauh/components/mainsail/__init__.py b/kiauh/components/mainsail/__init__.py index 0b2d2af..53a9924 100644 --- a/kiauh/components/mainsail/__init__.py +++ b/kiauh/components/mainsail/__init__.py @@ -11,7 +11,7 @@ from pathlib import Path -from kiauh.core.backup_manager import BACKUP_ROOT_DIR +from core.backup_manager import BACKUP_ROOT_DIR MODULE_PATH = Path(__file__).resolve().parent MAINSAIL_DIR = Path.home().joinpath("mainsail") diff --git a/kiauh/components/mainsail/mainsail_dialogs.py b/kiauh/components/mainsail/mainsail_dialogs.py index 6c8ea69..36453ff 100644 --- a/kiauh/components/mainsail/mainsail_dialogs.py +++ b/kiauh/components/mainsail/mainsail_dialogs.py @@ -11,8 +11,8 @@ import textwrap -from kiauh.core.menus.base_menu import print_back_footer -from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN +from core.menus.base_menu import print_back_footer +from utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN def print_moonraker_not_found_dialog(): diff --git a/kiauh/components/mainsail/mainsail_remove.py b/kiauh/components/mainsail/mainsail_remove.py index 3a4f797..204e8a6 100644 --- a/kiauh/components/mainsail/mainsail_remove.py +++ b/kiauh/components/mainsail/mainsail_remove.py @@ -15,15 +15,15 @@ import subprocess from pathlib import Path from typing import List -from kiauh.components.klipper.klipper import Klipper -from kiauh.components.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR -from kiauh.components.mainsail.mainsail_utils import backup_config_json -from kiauh.components.moonraker.moonraker import Moonraker -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from kiauh.utils.filesystem_utils import remove_file -from kiauh.utils.logger import Logger +from components.klipper.klipper import Klipper +from components.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR +from components.mainsail.mainsail_utils import backup_config_json +from components.moonraker.moonraker import Moonraker +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from utils.filesystem_utils import remove_file +from utils.logger import Logger def run_mainsail_removal( diff --git a/kiauh/components/mainsail/mainsail_setup.py b/kiauh/components/mainsail/mainsail_setup.py index e370847..210dc9e 100644 --- a/kiauh/components/mainsail/mainsail_setup.py +++ b/kiauh/components/mainsail/mainsail_setup.py @@ -14,33 +14,33 @@ from pathlib import Path from typing import List from kiauh import KIAUH_CFG -from kiauh.components.klipper.klipper import Klipper -from kiauh.components.mainsail import ( +from components.klipper.klipper import Klipper +from components.mainsail import ( MAINSAIL_URL, MAINSAIL_DIR, MAINSAIL_CONFIG_DIR, MAINSAIL_CONFIG_REPO_URL, MODULE_PATH, ) -from kiauh.components.mainsail.mainsail_dialogs import ( +from components.mainsail.mainsail_dialogs import ( print_moonraker_not_found_dialog, print_mainsail_already_installed_dialog, print_install_mainsail_config_dialog, print_mainsail_port_select_dialog, ) -from kiauh.components.mainsail.mainsail_utils import ( +from components.mainsail.mainsail_utils import ( restore_config_json, enable_mainsail_remotemode, backup_config_json, symlink_webui_nginx_log, ) -from kiauh.components.moonraker.moonraker import Moonraker -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from kiauh.utils.common import check_install_dependencies -from kiauh.utils.filesystem_utils import ( +from components.moonraker.moonraker import Moonraker +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from core.repo_manager.repo_manager import RepoManager +from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from utils.common import check_install_dependencies +from utils.filesystem_utils import ( unzip, copy_upstream_nginx_cfg, copy_common_vars_nginx_cfg, @@ -48,9 +48,9 @@ from kiauh.utils.filesystem_utils import ( create_symlink, remove_file, ) -from kiauh.utils.input_utils import get_confirm, get_number_input -from kiauh.utils.logger import Logger -from kiauh.utils.system_utils import ( +from utils.input_utils import get_confirm, get_number_input +from utils.logger import Logger +from utils.system_utils import ( download_file, set_nginx_permissions, get_ipv4_addr, diff --git a/kiauh/components/mainsail/mainsail_utils.py b/kiauh/components/mainsail/mainsail_utils.py index b2aa32b..321f326 100644 --- a/kiauh/components/mainsail/mainsail_utils.py +++ b/kiauh/components/mainsail/mainsail_utils.py @@ -17,16 +17,16 @@ from typing import List import urllib.request -from kiauh.components.klipper.klipper import Klipper -from kiauh.components.mainsail import ( +from components.klipper.klipper import Klipper +from components.mainsail import ( MAINSAIL_CONFIG_JSON, MAINSAIL_DIR, MAINSAIL_BACKUP_DIR, ) -from kiauh.core.backup_manager.backup_manager import BackupManager -from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_CONFD -from kiauh.utils.common import get_install_status_webui -from kiauh.utils.logger import Logger +from core.backup_manager.backup_manager import BackupManager +from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD +from utils.common import get_install_status_webui +from utils.logger import Logger def get_mainsail_status() -> str: diff --git a/kiauh/components/mainsail/menus/mainsail_remove_menu.py b/kiauh/components/mainsail/menus/mainsail_remove_menu.py index 187d974..48cfb4b 100644 --- a/kiauh/components/mainsail/menus/mainsail_remove_menu.py +++ b/kiauh/components/mainsail/menus/mainsail_remove_menu.py @@ -11,10 +11,10 @@ import textwrap -from kiauh.components.mainsail import mainsail_remove -from kiauh.core.menus import BACK_HELP_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN +from components.mainsail import mainsail_remove +from core.menus import BACK_HELP_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index e42f3e9..9341992 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -11,7 +11,7 @@ from pathlib import Path -from kiauh.core.backup_manager import BACKUP_ROOT_DIR +from core.backup_manager import BACKUP_ROOT_DIR MODULE_PATH = Path(__file__).resolve().parent diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 962e07d..652d028 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -11,10 +11,10 @@ import textwrap -from kiauh.components.moonraker import moonraker_remove -from kiauh.core.menus import BACK_HELP_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN +from components.moonraker import moonraker_remove +from core.menus import BACK_HELP_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index c2a9d29..aa0e03b 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -13,11 +13,11 @@ import subprocess from pathlib import Path from typing import List, Union -from kiauh.components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.utils.constants import SYSTEMD -from kiauh.utils.logger import Logger +from components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.base_instance import BaseInstance +from utils.constants import SYSTEMD +from utils.logger import Logger # noinspection PyMethodMayBeStatic diff --git a/kiauh/components/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py index 2bc18b2..4f13d10 100644 --- a/kiauh/components/moonraker/moonraker_dialogs.py +++ b/kiauh/components/moonraker/moonraker_dialogs.py @@ -12,10 +12,10 @@ import textwrap from typing import List -from kiauh.components.klipper.klipper import Klipper -from kiauh.components.moonraker.moonraker import Moonraker -from kiauh.core.menus.base_menu import print_back_footer -from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from core.menus.base_menu import print_back_footer +from utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN def print_moonraker_overview( diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index 02af9dd..db10bf4 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -13,13 +13,13 @@ import shutil import subprocess from typing import List, Union -from kiauh.components.klipper.klipper_dialogs import print_instance_overview -from kiauh.components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR -from kiauh.components.moonraker.moonraker import Moonraker -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.utils.filesystem_utils import remove_file -from kiauh.utils.input_utils import get_selection_input -from kiauh.utils.logger import Logger +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 utils.filesystem_utils import remove_file +from utils.input_utils import get_selection_input +from utils.logger import Logger def run_moonraker_removal( diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index f041a0e..fe635f8 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -15,11 +15,11 @@ from pathlib import Path from typing import List from kiauh import KIAUH_CFG -from kiauh.components.klipper.klipper import Klipper -from kiauh.components.klipper.klipper_dialogs import print_instance_overview -from kiauh.components.mainsail import MAINSAIL_DIR -from kiauh.components.mainsail.mainsail_utils import enable_mainsail_remotemode -from kiauh.components.moonraker import ( +from components.klipper.klipper import Klipper +from components.klipper.klipper_dialogs import print_instance_overview +from components.mainsail import MAINSAIL_DIR +from components.mainsail.mainsail_utils import enable_mainsail_remotemode +from components.moonraker import ( EXIT_MOONRAKER_SETUP, DEFAULT_MOONRAKER_REPO_URL, MOONRAKER_DIR, @@ -30,22 +30,22 @@ from kiauh.components.moonraker import ( POLKIT_USR_FILE, POLKIT_SCRIPT, ) -from kiauh.components.moonraker.moonraker import Moonraker -from kiauh.components.moonraker.moonraker_dialogs import print_moonraker_overview -from kiauh.components.moonraker.moonraker_utils import ( +from components.moonraker.moonraker import Moonraker +from components.moonraker.moonraker_dialogs import print_moonraker_overview +from components.moonraker.moonraker_utils import ( create_example_moonraker_conf, backup_moonraker_dir, ) -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.utils.filesystem_utils import check_file_exist -from kiauh.utils.input_utils import ( +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from core.repo_manager.repo_manager import RepoManager +from utils.filesystem_utils import check_file_exist +from utils.input_utils import ( get_confirm, get_selection_input, ) -from kiauh.utils.logger import Logger -from kiauh.utils.system_utils import ( +from utils.logger import Logger +from utils.system_utils import ( parse_packages_from_file, create_python_venv, install_python_requirements, diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 0a9ae11..3321781 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -12,9 +12,9 @@ import shutil from typing import Dict, Literal, List, Union -from kiauh.components.mainsail import MAINSAIL_DIR -from kiauh.components.mainsail.mainsail_utils import enable_mainsail_remotemode -from kiauh.components.moonraker import ( +from components.mainsail import MAINSAIL_DIR +from components.mainsail.mainsail_utils import enable_mainsail_remotemode +from components.moonraker import ( DEFAULT_MOONRAKER_PORT, MODULE_PATH, MOONRAKER_DIR, @@ -22,14 +22,14 @@ from kiauh.components.moonraker import ( MOONRAKER_BACKUP_DIR, MOONRAKER_DB_BACKUP_DIR, ) -from kiauh.components.moonraker.moonraker import Moonraker -from kiauh.core.backup_manager.backup_manager import BackupManager -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.core.repo_manager.repo_manager import RepoManager -from kiauh.utils.common import get_install_status_common -from kiauh.utils.logger import Logger -from kiauh.utils.system_utils import ( +from components.moonraker.moonraker import Moonraker +from core.backup_manager.backup_manager import BackupManager +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from core.repo_manager.repo_manager import RepoManager +from utils.common import get_install_status_common +from utils.logger import Logger +from utils.system_utils import ( get_ipv4_addr, ) diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 8f2dbcf..1c27266 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -13,9 +13,9 @@ import shutil from pathlib import Path from typing import List -from kiauh.core.backup_manager import BACKUP_ROOT_DIR -from kiauh.utils.common import get_current_date -from kiauh.utils.logger import Logger +from core.backup_manager import BACKUP_ROOT_DIR +from utils.common import get_current_date +from utils.logger import Logger # noinspection PyUnusedLocal diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index 63a4490..12d4eb0 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -13,7 +13,7 @@ import configparser from pathlib import Path from typing import Union -from kiauh.utils.logger import Logger +from utils.logger import Logger # noinspection PyMethodMayBeStatic diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index 571985e..95e24dc 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -13,7 +13,7 @@ from abc import abstractmethod, ABC from pathlib import Path from typing import List, Type, TypeVar -from kiauh.utils.constants import SYSTEMD, CURRENT_USER +from utils.constants import SYSTEMD, CURRENT_USER B = TypeVar(name="B", bound="BaseInstance", covariant=True) diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 0a920d6..9750086 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -14,9 +14,9 @@ import subprocess from pathlib import Path from typing import List, Optional, Union, TypeVar -from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.utils.constants import SYSTEMD -from kiauh.utils.logger import Logger +from core.instance_manager.base_instance import BaseInstance +from utils.constants import SYSTEMD +from utils.logger import Logger I = TypeVar(name="I", bound=BaseInstance, covariant=True) diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 533b5c1..27eaacd 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -11,9 +11,9 @@ import textwrap -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import COLOR_YELLOW, RESET_FORMAT +from core.menus import BACK_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import COLOR_YELLOW, RESET_FORMAT class AdvancedMenu(BaseMenu): diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 184e26c..8b92978 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -11,16 +11,16 @@ import textwrap -from kiauh.components.klipper.klipper_utils import backup_klipper_dir -from kiauh.components.mainsail.mainsail_utils import backup_mainsail_data -from kiauh.components.moonraker.moonraker_utils import ( +from components.klipper.klipper_utils import backup_klipper_dir +from components.mainsail.mainsail_utils import backup_mainsail_data +from components.moonraker.moonraker_utils import ( backup_moonraker_dir, backup_moonraker_db_dir, ) -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.common import backup_printer_config_dir -from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW +from core.menus import BACK_FOOTER +from core.menus.base_menu import BaseMenu +from utils.common import backup_printer_config_dir +from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW # noinspection PyUnusedLocal diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 89ed09b..06ad576 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -15,15 +15,15 @@ import textwrap from abc import abstractmethod, ABC from typing import Dict, Any, Literal, Union, Callable -from kiauh.core.menus import QUIT_FOOTER, BACK_FOOTER, BACK_HELP_FOOTER -from kiauh.utils.constants import ( +from core.menus import QUIT_FOOTER, BACK_FOOTER, BACK_HELP_FOOTER +from utils.constants import ( COLOR_GREEN, COLOR_YELLOW, COLOR_RED, COLOR_CYAN, RESET_FORMAT, ) -from kiauh.utils.logger import Logger +from utils.logger import Logger def clear(): diff --git a/kiauh/core/menus/extensions_menu.py b/kiauh/core/menus/extensions_menu.py index 50fd53b..a46b00c 100644 --- a/kiauh/core/menus/extensions_menu.py +++ b/kiauh/core/menus/extensions_menu.py @@ -16,10 +16,10 @@ import textwrap from pathlib import Path from typing import List, Dict -from kiauh.core.base_extension import BaseExtension -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW +from core.base_extension import BaseExtension +from core.menus import BACK_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW # noinspection PyUnusedLocal diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 85e1ef3..8ab2730 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -11,12 +11,13 @@ import textwrap -from kiauh.components.klipper import klipper_setup -from kiauh.components.mainsail import mainsail_setup -from kiauh.components.moonraker import moonraker_setup -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT +from components.fluidd import fluidd_setup +from components.klipper import klipper_setup +from components.mainsail import mainsail_setup +from components.moonraker import moonraker_setup +from core.menus import BACK_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import COLOR_GREEN, RESET_FORMAT # noinspection PyUnusedLocal diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 1bd2eba..3fd94b9 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -11,12 +11,12 @@ import textwrap -from kiauh.components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu -from kiauh.components.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu -from kiauh.components.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import COLOR_RED, RESET_FORMAT +from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu +from components.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu +from components.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu +from core.menus import BACK_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import COLOR_RED, RESET_FORMAT # noinspection PyUnusedLocal diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 1305ad1..66f352c 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -9,7 +9,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from kiauh.core.menus.base_menu import BaseMenu +from core.menus.base_menu import BaseMenu # noinspection PyMethodMayBeStatic diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index 9134694..a8f9eec 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -13,8 +13,8 @@ import shutil import subprocess from pathlib import Path -from kiauh.utils.input_utils import get_confirm -from kiauh.utils.logger import Logger +from utils.input_utils import get_confirm +from utils.logger import Logger # noinspection PyMethodMayBeStatic diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index a9798e2..bf0a977 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -13,21 +13,21 @@ import os import shutil from typing import List -from kiauh.components.klipper.klipper import Klipper -from kiauh.core.backup_manager.backup_manager import BackupManager -from kiauh.core.base_extension import BaseExtension -from kiauh.core.config_manager.config_manager import ConfigManager -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.extensions.gcode_shell_cmd import ( +from components.klipper.klipper import Klipper +from core.backup_manager.backup_manager import BackupManager +from core.base_extension import BaseExtension +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from extensions.gcode_shell_cmd import ( EXTENSION_TARGET_PATH, EXTENSION_SRC, KLIPPER_DIR, EXAMPLE_CFG_SRC, KLIPPER_EXTRAS, ) -from kiauh.utils.filesystem_utils import check_file_exist -from kiauh.utils.input_utils import get_confirm -from kiauh.utils.logger import Logger +from utils.filesystem_utils import check_file_exist +from utils.input_utils import get_confirm +from utils.logger import Logger # noinspection PyMethodMayBeStatic diff --git a/kiauh/main.py b/kiauh/main.py index 0dd1b31..0ca361c 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -9,8 +9,8 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from kiauh.core.menus.main_menu import MainMenu -from kiauh.utils.logger import Logger +from core.menus.main_menu import MainMenu +from utils.logger import Logger def main(): diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index 319fc15..afc69aa 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -11,7 +11,7 @@ from pathlib import Path -from kiauh.core.backup_manager import BACKUP_ROOT_DIR +from core.backup_manager import BACKUP_ROOT_DIR MODULE_PATH = Path(__file__).resolve().parent INVALID_CHOICE = "Invalid choice. Please select a valid value." diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 6557cd0..4e5bbee 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -13,20 +13,20 @@ from datetime import datetime from pathlib import Path from typing import Dict, Literal, List, Type, Union -from kiauh.components.klipper.klipper import Klipper -from kiauh.core.instance_manager.base_instance import BaseInstance -from kiauh.core.instance_manager.instance_manager import InstanceManager -from kiauh.utils import PRINTER_CFG_BACKUP_DIR -from kiauh.utils.constants import ( +from components.klipper.klipper import Klipper +from core.instance_manager.base_instance import BaseInstance +from core.instance_manager.instance_manager import InstanceManager +from utils import PRINTER_CFG_BACKUP_DIR +from utils.constants import ( COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_GREEN, COLOR_RED, ) -from kiauh.utils.filesystem_utils import check_file_exist -from kiauh.utils.logger import Logger -from kiauh.utils.system_utils import check_package_install, install_system_packages +from utils.filesystem_utils import check_file_exist +from utils.logger import Logger +from utils.system_utils import check_package_install, install_system_packages def get_current_date() -> Dict[Literal["date", "time"], str]: @@ -122,7 +122,7 @@ def get_install_status_webui( def backup_printer_config_dir(): # local import to prevent circular import - from kiauh.core.backup_manager.backup_manager import BackupManager + from core.backup_manager.backup_manager import BackupManager im = InstanceManager(Klipper) instances: List[Klipper] = im.instances diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 68ce432..57b414f 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -13,12 +13,12 @@ import subprocess from pathlib import Path from zipfile import ZipFile -from kiauh.utils import ( +from utils import ( NGINX_SITES_AVAILABLE, MODULE_PATH, NGINX_CONFD, ) -from kiauh.utils.logger import Logger +from utils.logger import Logger def check_file_exist(file_path: Path, sudo=False) -> bool: diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index e2daf8e..d0a3b48 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -11,9 +11,9 @@ from typing import Optional, List, Union -from kiauh.utils import INVALID_CHOICE -from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT -from kiauh.utils.logger import Logger +from utils import INVALID_CHOICE +from utils.constants import COLOR_CYAN, RESET_FORMAT +from utils.logger import Logger def get_confirm( diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index fcd0c9b..66f9326 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -9,7 +9,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from kiauh.utils.constants import ( +from utils.constants import ( COLOR_WHITE, COLOR_GREEN, COLOR_YELLOW, diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 352d575..718607a 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -21,9 +21,9 @@ import venv from pathlib import Path from typing import List, Literal -from kiauh.utils.input_utils import get_confirm -from kiauh.utils.logger import Logger -from kiauh.utils.filesystem_utils import check_file_exist +from utils.input_utils import get_confirm +from utils.logger import Logger +from utils.filesystem_utils import check_file_exist def kill(opt_err_msg: str = "") -> None: From 384503c4f58246e3c7189da21aa5715be6b427f9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 24 Feb 2024 15:26:32 +0100 Subject: [PATCH 110/296] feat(Fluidd): add Fluidd Signed-off-by: Dominik Willner --- kiauh.cfg.example | 6 +- kiauh/components/fluidd/__init__.py | 26 ++ .../fluidd/assets/fluidd-config-updater.conf | 6 + .../fluidd/assets/fluidd-updater.conf | 5 + kiauh/components/fluidd/fluidd_dialogs.py | 104 ++++++++ kiauh/components/fluidd/fluidd_remove.py | 160 ++++++++++++ kiauh/components/fluidd/fluidd_setup.py | 242 ++++++++++++++++++ kiauh/components/fluidd/fluidd_utils.py | 79 ++++++ kiauh/components/fluidd/menus/__init__.py | 0 .../fluidd/menus/fluidd_remove_menu.py | 111 ++++++++ kiauh/core/menus/install_menu.py | 2 +- kiauh/core/menus/main_menu.py | 31 ++- kiauh/core/menus/remove_menu.py | 54 +--- kiauh/core/menus/update_menu.py | 37 ++- kiauh/utils/filesystem_utils.py | 35 +++ 15 files changed, 828 insertions(+), 70 deletions(-) create mode 100644 kiauh/components/fluidd/__init__.py create mode 100644 kiauh/components/fluidd/assets/fluidd-config-updater.conf create mode 100644 kiauh/components/fluidd/assets/fluidd-updater.conf create mode 100644 kiauh/components/fluidd/fluidd_dialogs.py create mode 100644 kiauh/components/fluidd/fluidd_remove.py create mode 100644 kiauh/components/fluidd/fluidd_setup.py create mode 100644 kiauh/components/fluidd/fluidd_utils.py create mode 100644 kiauh/components/fluidd/menus/__init__.py create mode 100644 kiauh/components/fluidd/menus/fluidd_remove_menu.py diff --git a/kiauh.cfg.example b/kiauh.cfg.example index e1724d2..0d61335 100644 --- a/kiauh.cfg.example +++ b/kiauh.cfg.example @@ -12,5 +12,9 @@ branch: master method: https [mainsail] -default_port: 80 +port: 80 +unstable_releases: False + +[fluidd] +port: 80 unstable_releases: False diff --git a/kiauh/components/fluidd/__init__.py b/kiauh/components/fluidd/__init__.py new file mode 100644 index 0000000..70bc91f --- /dev/null +++ b/kiauh/components/fluidd/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path + +from core.backup_manager import BACKUP_ROOT_DIR + +MODULE_PATH = Path(__file__).resolve().parent +FLUIDD_DIR = Path.home().joinpath("fluidd") +FLUIDD_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("fluidd-backups") +FLUIDD_CONFIG_DIR = Path.home().joinpath("fluidd-config") +FLUIDD_NGINX_CFG = Path("/etc/nginx/sites-enabled/fluidd") +FLUIDD_URL = "https://github.com/fluidd-core/fluidd/releases/latest/download/fluidd.zip" +FLUIDD_UNSTABLE_URL = ( + "https://github.com/fluidd-core/fluidd/releases/download/%TAG%/fluidd.zip" +) +FLUIDD_CONFIG_REPO_URL = "https://github.com/fluidd-core/fluidd-config.git" + diff --git a/kiauh/components/fluidd/assets/fluidd-config-updater.conf b/kiauh/components/fluidd/assets/fluidd-config-updater.conf new file mode 100644 index 0000000..bc4a5cc --- /dev/null +++ b/kiauh/components/fluidd/assets/fluidd-config-updater.conf @@ -0,0 +1,6 @@ +[update_manager fluidd-config] +type: git_repo +primary_branch: master +path: ~/fluidd-config +origin: https://github.com/fluidd-core/fluidd-config.git +managed_services: klipper diff --git a/kiauh/components/fluidd/assets/fluidd-updater.conf b/kiauh/components/fluidd/assets/fluidd-updater.conf new file mode 100644 index 0000000..d9d1e7d --- /dev/null +++ b/kiauh/components/fluidd/assets/fluidd-updater.conf @@ -0,0 +1,5 @@ +[update_manager fluidd] +type: web +channel: stable +repo: fluidd-core/fluidd +path: ~/fluidd diff --git a/kiauh/components/fluidd/fluidd_dialogs.py b/kiauh/components/fluidd/fluidd_dialogs.py new file mode 100644 index 0000000..deec016 --- /dev/null +++ b/kiauh/components/fluidd/fluidd_dialogs.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import textwrap +from typing import List + +from core.menus.base_menu import print_back_footer +from utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN + + +def print_moonraker_not_found_dialog(): + line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}No local Moonraker installation was found!{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | {line1:<63}| + | {line2:<63}| + |-------------------------------------------------------| + | It is possible to install Fluidd without a local | + | Moonraker installation. If you continue, you need to | + | make sure, that Moonraker is installed on another | + | machine in your network. Otherwise Fluidd will NOT | + | work correctly. | + """ + )[1:] + + print(dialog, end="") + print_back_footer() + + +def print_fluidd_already_installed_dialog(): + line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}Fluidd seems to be already installed!{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | {line1:<63}| + | {line2:<63}| + |-------------------------------------------------------| + | If you continue, your current Fluidd installation | + | will be overwritten. You will not loose any printer | + | configurations and the Moonraker database will remain | + | untouched. | + """ + )[1:] + + print(dialog, end="") + print_back_footer() + + +def print_install_fluidd_config_dialog(): + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | It is recommended to use special macros in order to | + | have Fluidd fully functional and working. | + | | + | The recommended macros for Fluidd can be seen here: | + | https://github.com/fluidd-core/fluidd-config | + | | + | If you already use these macros skip this step. | + | Otherwise you should consider to answer with 'Y' to | + | download the recommended macros. | + \\=======================================================/ + """ + )[1:] + + print(dialog, end="") + + +def print_fluidd_port_select_dialog(port: str, ports_in_use: List[str]): + port = f"{COLOR_CYAN}{port}{RESET_FORMAT}" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | Please select the port, Fluidd should be served on. | + | If you are unsure what to select, hit Enter to apply | + | the suggested value of: {port:38} | + | | + | In case you need Fluidd to be served on a specific | + | port, you can set it now. Make sure the port is not | + | used by any other application on your system! | + """ + )[1:] + + if len(ports_in_use) > 0: + dialog += "|-------------------------------------------------------|\n" + dialog += "| The following ports were found to be in use already: |\n" + for port in ports_in_use: + port = f"{COLOR_CYAN}● {port}{RESET_FORMAT}" + dialog += f"| {port:60} |\n" + + dialog += "\\=======================================================/\n" + + print(dialog, end="") diff --git a/kiauh/components/fluidd/fluidd_remove.py b/kiauh/components/fluidd/fluidd_remove.py new file mode 100644 index 0000000..b617cec --- /dev/null +++ b/kiauh/components/fluidd/fluidd_remove.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + + +import shutil +import subprocess +from pathlib import Path +from typing import List + +from components.klipper.klipper import Klipper +from components.fluidd import FLUIDD_DIR, FLUIDD_CONFIG_DIR +from components.moonraker.moonraker import Moonraker +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from utils.filesystem_utils import remove_file +from utils.logger import Logger + + +def run_fluidd_removal( + remove_fluidd: bool, + remove_fl_config: bool, + remove_mr_updater_section: bool, + remove_flc_printer_cfg_include: bool, +) -> None: + if remove_fluidd: + remove_fluidd_dir() + remove_nginx_config() + remove_nginx_logs() + if remove_mr_updater_section: + remove_updater_section("update_manager fluidd") + if remove_fl_config: + remove_fluidd_cfg_dir() + remove_fluidd_cfg_symlink() + if remove_mr_updater_section: + remove_updater_section("update_manager fluidd-config") + if remove_flc_printer_cfg_include: + remove_printer_cfg_include() + + +def remove_fluidd_dir() -> None: + Logger.print_status("Removing Fluidd ...") + if not FLUIDD_DIR.exists(): + Logger.print_info(f"'{FLUIDD_DIR}' does not exist. Skipping ...") + return + + try: + shutil.rmtree(FLUIDD_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{FLUIDD_DIR}':\n{e}") + + +def remove_nginx_config() -> None: + Logger.print_status("Removing Fluidd NGINX config ...") + try: + remove_file(NGINX_SITES_AVAILABLE.joinpath("fluidd"), True) + remove_file(NGINX_SITES_ENABLED.joinpath("fluidd"), True) + + except subprocess.CalledProcessError as e: + log = f"Unable to remove Fluidd NGINX config:\n{e.stderr.decode()}" + Logger.print_error(log) + + +def remove_nginx_logs() -> None: + Logger.print_status("Removing Fluidd NGINX logs ...") + try: + remove_file(Path("/var/log/nginx/fluidd-access.log"), True) + remove_file(Path("/var/log/nginx/fluidd-error.log"), True) + + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + if not instances: + return + + for instance in instances: + remove_file(instance.log_dir.joinpath("fluidd-access.log")) + remove_file(instance.log_dir.joinpath("fluidd-error.log")) + + except (OSError, subprocess.CalledProcessError) as e: + Logger.print_error(f"Unable to NGINX logs:\n{e}") + + +def remove_updater_section(name: str) -> None: + Logger.print_status("Remove updater section from moonraker.conf ...") + im = InstanceManager(Moonraker) + instances: List[Moonraker] = im.instances + if not instances: + Logger.print_info("Moonraker not installed. Skipped ...") + return + + for instance in instances: + Logger.print_status(f"Remove section '{name}' in '{instance.cfg_file}' ...") + + if not instance.cfg_file.is_file(): + Logger.print_info(f"'{instance.cfg_file}' does not exist. Skipped ...") + continue + + cm = ConfigManager(instance.cfg_file) + if not cm.config.has_section(name): + Logger.print_info("Section not present. Skipped ...") + continue + + cm.config.remove_section(name) + cm.write_config() + + +def remove_fluidd_cfg_dir() -> None: + Logger.print_status("Removing fluidd-config ...") + if not FLUIDD_CONFIG_DIR.exists(): + Logger.print_info(f"'{FLUIDD_CONFIG_DIR}' does not exist. Skipping ...") + return + + try: + shutil.rmtree(FLUIDD_CONFIG_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{FLUIDD_CONFIG_DIR}':\n{e}") + + +def remove_fluidd_cfg_symlink() -> None: + Logger.print_status("Removing fluidd.cfg symlinks ...") + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + for instance in instances: + Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") + try: + remove_file(instance.cfg_dir.joinpath("fluidd.cfg")) + except subprocess.CalledProcessError: + Logger.print_error("Failed to remove symlink!") + + +def remove_printer_cfg_include() -> None: + Logger.print_status("Remove fluidd-config include from printer.cfg ...") + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + if not instances: + Logger.print_info("Klipper not installed. Skipping ...") + return + + for instance in instances: + log = f"Removing include from '{instance.cfg_file}' ..." + Logger.print_status(log) + + if not instance.cfg_file.is_file(): + continue + + cm = ConfigManager(instance.cfg_file) + if not cm.config.has_section("include fluidd.cfg"): + Logger.print_info("Section not present. Skipped ...") + continue + + cm.config.remove_section("include fluidd.cfg") + cm.write_config() diff --git a/kiauh/components/fluidd/fluidd_setup.py b/kiauh/components/fluidd/fluidd_setup.py new file mode 100644 index 0000000..7c23bb5 --- /dev/null +++ b/kiauh/components/fluidd/fluidd_setup.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import subprocess +from pathlib import Path +from typing import List + +from components.fluidd import ( + FLUIDD_URL, + FLUIDD_CONFIG_REPO_URL, + FLUIDD_CONFIG_DIR, + FLUIDD_DIR, + MODULE_PATH, +) +from components.fluidd.fluidd_dialogs import ( + print_fluidd_already_installed_dialog, + print_install_fluidd_config_dialog, + print_fluidd_port_select_dialog, + print_moonraker_not_found_dialog, +) +from components.fluidd.fluidd_utils import symlink_webui_nginx_log +from kiauh import KIAUH_CFG +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from core.repo_manager.repo_manager import RepoManager +from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from utils.common import check_install_dependencies +from utils.filesystem_utils import ( + unzip, + copy_upstream_nginx_cfg, + copy_common_vars_nginx_cfg, + create_nginx_cfg, + create_symlink, + remove_file, + read_ports_from_nginx_configs, + get_next_free_port, is_valid_port, + ) +from utils.input_utils import get_confirm, get_number_input +from utils.logger import Logger +from utils.system_utils import ( + download_file, + set_nginx_permissions, + get_ipv4_addr, + control_systemd_service, +) + + +def install_fluidd() -> None: + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + + if not mr_instances: + print_moonraker_not_found_dialog() + if not get_confirm("Continue Fluidd installation?", allow_go_back=True): + return + + if Path.home().joinpath("fluidd").exists(): + print_fluidd_already_installed_dialog() + do_reinstall = get_confirm("Re-install Fluidd?", allow_go_back=True) + if not do_reinstall: + return + + kl_im = InstanceManager(Klipper) + kl_instances = kl_im.instances + install_fl_config = False + if kl_instances: + print_install_fluidd_config_dialog() + question = "Download the recommended macros?" + install_fl_config = get_confirm(question, allow_go_back=False) + + cm = ConfigManager(cfg_file=KIAUH_CFG) + fluidd_port = cm.get_value("fluidd", "port") + ports_in_use = read_ports_from_nginx_configs() + + # check if configured port is a valid number and not in use already + valid_port = is_valid_port(fluidd_port, ports_in_use) + while not valid_port: + next_port = get_next_free_port(ports_in_use) + print_fluidd_port_select_dialog(next_port, ports_in_use) + fluidd_port = str(get_number_input( + "Configure Fluidd for port", + min_count=int(next_port), + default=next_port, + )) + valid_port = is_valid_port(fluidd_port, ports_in_use) + + check_install_dependencies(["nginx"]) + + try: + download_fluidd() + if mr_instances: + patch_moonraker_conf( + mr_instances, + "Fluidd", + "update_manager fluidd", + "fluidd-updater.conf", + ) + mr_im.restart_all_instance() + if install_fl_config and kl_instances: + download_fluidd_cfg() + create_fluidd_cfg_symlink(kl_instances) + patch_moonraker_conf( + mr_instances, + "fluidd-config", + "update_manager fluidd-config", + "fluidd-config-updater.conf", + ) + patch_printer_config(kl_instances) + kl_im.restart_all_instance() + + copy_upstream_nginx_cfg() + copy_common_vars_nginx_cfg() + create_fluidd_nginx_cfg(fluidd_port) + if kl_instances: + symlink_webui_nginx_log(kl_instances) + control_systemd_service("nginx", "restart") + + except Exception as e: + Logger.print_error(f"Fluidd installation failed!\n{e}") + return + + log = f"Open Fluidd now on: http://{get_ipv4_addr()}:{fluidd_port}" + Logger.print_ok("Fluidd installation complete!", start="\n") + Logger.print_ok(log, prefix=False, end="\n\n") + + +def download_fluidd() -> None: + try: + Logger.print_status("Downloading Fluidd ...") + target = Path.home().joinpath("fluidd.zip") + download_file(FLUIDD_URL, target, True) + Logger.print_ok("Download complete!") + + Logger.print_status("Extracting fluidd.zip ...") + unzip(Path.home().joinpath("fluidd.zip"), FLUIDD_DIR) + target.unlink(missing_ok=True) + Logger.print_ok("OK!") + + except Exception: + Logger.print_error("Downloading Fluidd failed!") + raise + + +def update_fluidd() -> None: + Logger.print_status("Updating Fluidd ...") + download_fluidd() + + +def download_fluidd_cfg() -> None: + try: + Logger.print_status("Downloading fluidd-config ...") + rm = RepoManager(FLUIDD_CONFIG_REPO_URL, target_dir=FLUIDD_CONFIG_DIR) + rm.clone_repo() + except Exception: + Logger.print_error("Downloading fluidd-config failed!") + raise + + +def create_fluidd_cfg_symlink(klipper_instances: List[Klipper]) -> None: + Logger.print_status("Create symlink of fluidd.cfg ...") + source = Path(FLUIDD_CONFIG_DIR, "fluidd.cfg") + for instance in klipper_instances: + target = instance.cfg_dir + Logger.print_status(f"Linking {source} to {target}") + try: + create_symlink(source, target) + except subprocess.CalledProcessError: + Logger.print_error("Creating symlink failed!") + + +def create_fluidd_nginx_cfg(port: int) -> None: + root_dir = FLUIDD_DIR + source = NGINX_SITES_AVAILABLE.joinpath("fluidd") + target = NGINX_SITES_ENABLED.joinpath("fluidd") + try: + Logger.print_status("Creating NGINX config for Fluidd ...") + remove_file(Path("/etc/nginx/sites-enabled/default"), True) + create_nginx_cfg("fluidd", port, root_dir) + create_symlink(source, target, True) + set_nginx_permissions() + Logger.print_ok("NGINX config for Fluidd successfully created.") + except Exception: + Logger.print_error("Creating NGINX config for Fluidd failed!") + raise + + +# TODO: could be fully extracted, its webui agnostic, and used for mainsail and fluidd +def patch_moonraker_conf( + moonraker_instances: List[Moonraker], + name: str, + section_name: str, + template_file: str, +) -> None: + for instance in moonraker_instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Add {name} update section to '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + return + + cm = ConfigManager(cfg_file) + if cm.config.has_section(section_name): + Logger.print_info("Section already exist. Skipped ...") + return + + template = MODULE_PATH.joinpath("assets", template_file) + with open(template, "r") as t: + template_content = "\n" + template_content += t.read() + + with open(cfg_file, "a") as f: + f.write(template_content) + + +# TODO: could be made fully webui agnostic and extracted, and used for mainsail and fluidd +def patch_printer_config(klipper_instances: List[Klipper]) -> None: + for instance in klipper_instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Including fluidd-config in '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + return + + cm = ConfigManager(cfg_file) + if cm.config.has_section("include fluidd.cfg"): + Logger.print_info("Section already exist. Skipped ...") + return + + with open(cfg_file, "a") as f: + f.write("\n[include fluidd.cfg]") diff --git a/kiauh/components/fluidd/fluidd_utils.py b/kiauh/components/fluidd/fluidd_utils.py new file mode 100644 index 0000000..c9c672b --- /dev/null +++ b/kiauh/components/fluidd/fluidd_utils.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import json +import urllib.request +from json import JSONDecodeError +from pathlib import Path +from typing import List + +from components.fluidd import FLUIDD_DIR, FLUIDD_BACKUP_DIR +from components.klipper.klipper import Klipper +from core.backup_manager.backup_manager import BackupManager +from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD +from utils.common import get_install_status_webui +from utils.logger import Logger + + +# TODO: could be extracted and made generic +def get_fluidd_status() -> str: + return get_install_status_webui( + FLUIDD_DIR, + NGINX_SITES_AVAILABLE.joinpath("fluidd"), + NGINX_CONFD.joinpath("upstreams.conf"), + NGINX_CONFD.joinpath("common_vars.conf"), + ) + + +# TODO: could be extracted and made generic +def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: + Logger.print_status("Link NGINX logs into log directory ...") + access_log = Path("/var/log/nginx/fluidd-access.log") + error_log = Path("/var/log/nginx/fluidd-error.log") + + for instance in klipper_instances: + desti_access = instance.log_dir.joinpath("fluidd-access.log") + if not desti_access.exists(): + desti_access.symlink_to(access_log) + + desti_error = instance.log_dir.joinpath("fluidd-error.log") + if not desti_error.exists(): + desti_error.symlink_to(error_log) + + +# TODO: could be extracted and made generic +def get_fluidd_local_version() -> str: + relinfo_file = FLUIDD_DIR.joinpath("release_info.json") + if not relinfo_file.is_file(): + return "-" + + with open(relinfo_file, "r") as f: + return json.load(f)["version"] + + +# TODO: could be extracted and made generic +def get_fluidd_remote_version() -> str: + url = "https://api.github.com/repos/fluidd-core/fluidd/tags" + try: + with urllib.request.urlopen(url) as response: + data = json.loads(response.read()) + return data[0]["name"] + except (JSONDecodeError, TypeError): + return "ERROR" + + +# TODO: could be extracted and made generic +def backup_fluidd_data() -> None: + with open(FLUIDD_DIR.joinpath(".version"), "r") as v: + version = v.readlines()[0] + bm = BackupManager() + bm.backup_directory(f"fluidd-{version}", FLUIDD_DIR, FLUIDD_BACKUP_DIR) + bm.backup_file(NGINX_SITES_AVAILABLE.joinpath("fluidd"), FLUIDD_BACKUP_DIR) diff --git a/kiauh/components/fluidd/menus/__init__.py b/kiauh/components/fluidd/menus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/components/fluidd/menus/fluidd_remove_menu.py b/kiauh/components/fluidd/menus/fluidd_remove_menu.py new file mode 100644 index 0000000..4671799 --- /dev/null +++ b/kiauh/components/fluidd/menus/fluidd_remove_menu.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import textwrap + +from components.fluidd import fluidd_remove +from core.menus import BACK_HELP_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN + + +# noinspection PyUnusedLocal +class FluiddRemoveMenu(BaseMenu): + def __init__(self): + super().__init__( + header=False, + options={ + "0": self.toggle_all, + "1": self.toggle_remove_fluidd, + "2": self.toggle_remove_fl_config, + "3": self.toggle_remove_updater_section, + "4": self.toggle_remove_printer_cfg_include, + "5": self.run_removal_process, + }, + footer_type=BACK_HELP_FOOTER, + ) + self.remove_fluidd = False + self.remove_fl_config = False + self.remove_updater_section = False + self.remove_printer_cfg_include = False + + def print_menu(self) -> None: + header = " [ Remove Fluidd ] " + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" + unchecked = "[ ]" + o1 = checked if self.remove_fluidd else unchecked + o2 = checked if self.remove_fl_config else unchecked + o3 = checked if self.remove_updater_section else unchecked + o4 = checked if self.remove_printer_cfg_include else unchecked + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Enter a number and hit enter to select / deselect | + | the specific option for removal. | + |-------------------------------------------------------| + | 0) Select everything | + |-------------------------------------------------------| + | 1) {o1} Remove Fluidd | + | 2) {o2} Remove fluidd-config | + | | + | printer.cfg & moonraker.conf | + | 3) {o3} Remove Moonraker update section | + | 4) {o4} Remove printer.cfg include | + |-------------------------------------------------------| + | 5) Continue | + """ + )[1:] + print(menu, end="") + + def toggle_all(self, **kwargs) -> None: + self.remove_fluidd = True + self.remove_fl_config = True + self.remove_updater_section = True + self.remove_printer_cfg_include = True + + def toggle_remove_fluidd(self, **kwargs) -> None: + self.remove_fluidd = not self.remove_fluidd + + def toggle_remove_fl_config(self, **kwargs) -> None: + self.remove_fl_config = not self.remove_fl_config + + def toggle_remove_updater_section(self, **kwargs) -> None: + self.remove_updater_section = not self.remove_updater_section + + def toggle_remove_printer_cfg_include(self, **kwargs) -> None: + self.remove_printer_cfg_include = not self.remove_printer_cfg_include + + def run_removal_process(self, **kwargs) -> None: + if ( + not self.remove_fluidd + and not self.remove_fl_config + and not self.remove_updater_section + and not self.remove_printer_cfg_include + ): + error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" + print(error) + return + + fluidd_remove.run_fluidd_removal( + remove_fluidd=self.remove_fluidd, + remove_fl_config=self.remove_fl_config, + remove_mr_updater_section=self.remove_updater_section, + remove_flc_printer_cfg_include=self.remove_printer_cfg_include, + ) + + self.remove_fluidd = False + self.remove_fl_config = False + self.remove_updater_section = False + self.remove_printer_cfg_include = False diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 8ab2730..c466fdf 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -75,7 +75,7 @@ class InstallMenu(BaseMenu): mainsail_setup.install_mainsail() def install_fluidd(self, **kwargs): - print("install_fluidd") + fluidd_setup.install_fluidd() def install_klipperscreen(self, **kwargs): print("install_klipperscreen") diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 60dc9f8..716e046 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -11,20 +11,21 @@ import textwrap -from kiauh.components.klipper.klipper_utils import get_klipper_status -from kiauh.components.log_uploads.menus.log_upload_menu import LogUploadMenu -from kiauh.components.mainsail.mainsail_utils import get_mainsail_status -from kiauh.components.moonraker.moonraker_utils import get_moonraker_status -from kiauh.core.menus import QUIT_FOOTER -from kiauh.core.menus.advanced_menu import AdvancedMenu -from kiauh.core.menus.backup_menu import BackupMenu -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.core.menus.extensions_menu import ExtensionsMenu -from kiauh.core.menus.install_menu import InstallMenu -from kiauh.core.menus.remove_menu import RemoveMenu -from kiauh.core.menus.settings_menu import SettingsMenu -from kiauh.core.menus.update_menu import UpdateMenu -from kiauh.utils.constants import ( +from components.fluidd.fluidd_utils import get_fluidd_status +from components.klipper.klipper_utils import get_klipper_status +from components.log_uploads.menus.log_upload_menu import LogUploadMenu +from components.mainsail.mainsail_utils import get_mainsail_status +from components.moonraker.moonraker_utils import get_moonraker_status +from core.menus import QUIT_FOOTER +from core.menus.advanced_menu import AdvancedMenu +from core.menus.backup_menu import BackupMenu +from core.menus.base_menu import BaseMenu +from core.menus.extensions_menu import ExtensionsMenu +from core.menus.install_menu import InstallMenu +from core.menus.remove_menu import RemoveMenu +from core.menus.settings_menu import SettingsMenu +from core.menus.update_menu import UpdateMenu +from utils.constants import ( COLOR_MAGENTA, COLOR_CYAN, RESET_FORMAT, @@ -87,6 +88,8 @@ class MainMenu(BaseMenu): self.mr_repo = f"{COLOR_CYAN}{moonraker_status.get('repo')}{RESET_FORMAT}" # mainsail self.ms_status = get_mainsail_status() + # fluidd + self.fl_status = get_fluidd_status() def format_status_by_code(self, code: int, status: str, count: str) -> str: if code == 1: diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 3fd94b9..8df2380 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -11,6 +11,7 @@ import textwrap +from components.fluidd.menus.fluidd_remove_menu import FluiddRemoveMenu from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu from components.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu from components.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu @@ -29,16 +30,16 @@ class RemoveMenu(BaseMenu): "1": KlipperRemoveMenu, "2": MoonrakerRemoveMenu, "3": MainsailRemoveMenu, - "5": self.remove_fluidd, - "6": self.remove_klipperscreen, - "7": self.remove_crowsnest, - "8": self.remove_mjpgstreamer, - "9": self.remove_pretty_gcode, - "10": self.remove_telegram_bot, - "11": self.remove_obico, - "12": self.remove_octoeverywhere, - "13": self.remove_mobileraker, - "14": self.remove_nginx, + "4": FluiddRemoveMenu, + "5": None, + "6": None, + "7": None, + "8": None, + "9": None, + "10": None, + "11": None, + "12": None, + "13": None, }, footer_type=BACK_FOOTER, ) @@ -69,36 +70,3 @@ class RemoveMenu(BaseMenu): """ )[1:] print(menu, end="") - - def remove_fluidd(self, **kwargs): - print("remove_fluidd") - - def remove_fluidd_config(self, **kwargs): - print("remove_fluidd_config") - - def remove_klipperscreen(self, **kwargs): - print("remove_klipperscreen") - - def remove_crowsnest(self, **kwargs): - print("remove_crowsnest") - - def remove_mjpgstreamer(self, **kwargs): - print("remove_mjpgstreamer") - - def remove_pretty_gcode(self, **kwargs): - print("remove_pretty_gcode") - - def remove_telegram_bot(self, **kwargs): - print("remove_telegram_bot") - - def remove_obico(self, **kwargs): - print("remove_obico") - - def remove_octoeverywhere(self, **kwargs): - print("remove_octoeverywhere") - - def remove_mobileraker(self, **kwargs): - print("remove_mobileraker") - - def remove_nginx(self, **kwargs): - print("remove_nginx") diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index ba25e07..04d6de5 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -11,20 +11,25 @@ import textwrap -from kiauh.components.klipper.klipper_setup import update_klipper -from kiauh.components.klipper.klipper_utils import ( +from components.fluidd.fluidd_setup import update_fluidd +from components.fluidd.fluidd_utils import ( + get_fluidd_local_version, + get_fluidd_remote_version, +) +from components.klipper.klipper_setup import update_klipper +from components.klipper.klipper_utils import ( get_klipper_status, ) -from kiauh.components.mainsail.mainsail_setup import update_mainsail -from kiauh.components.mainsail.mainsail_utils import ( +from components.mainsail.mainsail_setup import update_mainsail +from components.mainsail.mainsail_utils import ( get_mainsail_local_version, get_mainsail_remote_version, ) -from kiauh.components.moonraker.moonraker_setup import update_moonraker -from kiauh.components.moonraker.moonraker_utils import get_moonraker_status -from kiauh.core.menus import BACK_FOOTER -from kiauh.core.menus.base_menu import BaseMenu -from kiauh.utils.constants import ( +from components.moonraker.moonraker_setup import update_moonraker +from components.moonraker.moonraker_utils import get_moonraker_status +from core.menus import BACK_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import ( COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, @@ -62,6 +67,8 @@ class UpdateMenu(BaseMenu): self.mr_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.ms_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.ms_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.fl_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.fl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" def print_menu(self): self.fetch_update_status() @@ -82,7 +89,7 @@ class UpdateMenu(BaseMenu): | | | | | Klipper Webinterface: |---------------|---------------| | 3) Mainsail | {self.ms_local:<22} | {self.ms_remote:<22} | - | 4) Fluidd | | | + | 4) Fluidd | {self.fl_local:<22} | {self.fl_remote:<22} | | | | | | Touchscreen GUI: |---------------|---------------| | 5) KlipperScreen | | | @@ -113,7 +120,7 @@ class UpdateMenu(BaseMenu): update_mainsail() def update_fluidd(self, **kwargs): - print("update_fluidd") + update_fluidd() def update_klipperscreen(self, **kwargs): print("update_klipperscreen") @@ -166,3 +173,11 @@ class UpdateMenu(BaseMenu): else: self.ms_local = f"{COLOR_YELLOW}{self.ms_local}{RESET_FORMAT}" self.ms_remote = f"{COLOR_GREEN if self.ms_remote != 'ERROR' else COLOR_RED}{self.ms_remote}{RESET_FORMAT}" + # fluidd + self.fl_local = get_fluidd_local_version() + self.fl_remote = get_fluidd_remote_version() + if self.fl_local == self.fl_remote: + self.fl_local = f"{COLOR_GREEN}{self.fl_local}{RESET_FORMAT}" + else: + self.fl_local = f"{COLOR_YELLOW}{self.fl_local}{RESET_FORMAT}" + self.fl_remote = f"{COLOR_GREEN if self.fl_remote != 'ERROR' else COLOR_RED}{self.fl_remote}{RESET_FORMAT}" diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 57b414f..52603d6 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -13,10 +13,13 @@ import subprocess from pathlib import Path from zipfile import ZipFile +from typing import List + from utils import ( NGINX_SITES_AVAILABLE, MODULE_PATH, NGINX_CONFD, + NGINX_SITES_ENABLED, ) from utils.logger import Logger @@ -133,3 +136,35 @@ def create_nginx_cfg(name: str, port: int, root_dir: Path) -> None: log = f"Unable to create '{target}': {e.stderr.decode()}" Logger.print_error(log) raise + + +def read_ports_from_nginx_configs() -> List[str]: + """ + Helper function to iterate over all NGINX configs and read all ports defined for listen + :return: A sorted list of listen ports + """ + if not NGINX_SITES_ENABLED.exists(): + return [] + + port_list = [] + for config in NGINX_SITES_ENABLED.iterdir(): + with open(config, "r") as cfg: + lines = cfg.readlines() + + for line in lines: + line = line.strip().replace(";", "") + if line.startswith("listen"): + port_list.append(line.split()[-1]) + + return sorted(port_list, key=lambda x: int(x)) + + +def is_valid_port(port: str, ports_in_use: List[str]) -> bool: + return port.isdigit() and port not in ports_in_use + + +def get_next_free_port(ports_in_use: List[str]) -> str: + valid_ports = set(range(80, 7125)) + used_ports = set(map(int, ports_in_use)) + + return str(min(valid_ports - used_ports)) From 750cb7b3078bef2c109abf5c596252359307a54a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 24 Feb 2024 15:44:19 +0100 Subject: [PATCH 111/296] refactor(KIAUH): update NGINX config to match mainsails structure Signed-off-by: Dominik Willner --- kiauh/utils/assets/nginx_cfg | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/kiauh/utils/assets/nginx_cfg b/kiauh/utils/assets/nginx_cfg index 6303674..d7aabf4 100644 --- a/kiauh/utils/assets/nginx_cfg +++ b/kiauh/utils/assets/nginx_cfg @@ -1,7 +1,7 @@ -# /etc/nginx/sites-available/%NAME% - server { listen %PORT%; + # uncomment the next line to activate IPv6 + # listen [::]:%PORT%; access_log /var/log/nginx/%NAME%-access.log; error_log /var/log/nginx/%NAME%-error.log; @@ -55,7 +55,6 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Scheme $scheme; - proxy_read_timeout 600; } location /webcam/ { From 7fd91e6cef7df4c166f1fd5858581fe4cae07fc1 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 24 Feb 2024 15:46:02 +0100 Subject: [PATCH 112/296] refactor(KIAUH): allow reading ipv6 configured ports with possible default_server suffixes Signed-off-by: Dominik Willner --- kiauh/utils/filesystem_utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 52603d6..14ad26f 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -7,7 +7,7 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # - +import re import shutil import subprocess from pathlib import Path @@ -152,8 +152,9 @@ def read_ports_from_nginx_configs() -> List[str]: lines = cfg.readlines() for line in lines: - line = line.strip().replace(";", "") - if line.startswith("listen"): + line = line.replace("default_server", "") + line = re.sub(r"[;:\[\]]", "", line.strip()) + if line.startswith("listen") and line.split()[-1] not in port_list: port_list.append(line.split()[-1]) return sorted(port_list, key=lambda x: int(x)) From 1620efe56c79cb0fa965ea10be8f172646e0e798 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 2 Mar 2024 17:22:37 +0100 Subject: [PATCH 113/296] refactor(KIAUH): full refactor of client and client-config installation Signed-off-by: Dominik Willner --- kiauh/components/fluidd/__init__.py | 26 -- .../fluidd/assets/fluidd-config-updater.conf | 6 - .../fluidd/assets/fluidd-updater.conf | 5 - kiauh/components/fluidd/fluidd_remove.py | 160 ----------- kiauh/components/fluidd/fluidd_setup.py | 242 ----------------- kiauh/components/fluidd/fluidd_utils.py | 79 ------ .../fluidd/menus/fluidd_remove_menu.py | 111 -------- .../klipper/menus/klipper_remove_menu.py | 4 +- kiauh/components/mainsail/__init__.py | 27 -- .../assets/mainsail-config-updater.conf | 6 - .../mainsail/assets/mainsail-updater.conf | 5 - kiauh/components/mainsail/mainsail_dialogs.py | 95 ------- kiauh/components/mainsail/mainsail_remove.py | 164 ----------- kiauh/components/mainsail/mainsail_setup.py | 256 ------------------ kiauh/components/mainsail/mainsail_utils.py | 117 -------- .../mainsail/menus/mainsail_remove_menu.py | 122 --------- .../moonraker/menus/moonraker_remove_menu.py | 4 +- kiauh/components/moonraker/moonraker_setup.py | 4 +- kiauh/components/moonraker/moonraker_utils.py | 4 +- kiauh/components/webui_client/__init__.py | 77 ++++++ .../client_config}/__init__.py | 0 .../client_config/client_config_remove.py | 64 +++++ .../client_config/client_config_setup.py | 145 ++++++++++ .../client_dialogs.py} | 69 ++--- .../components/webui_client/client_remove.py | 75 +++++ kiauh/components/webui_client/client_setup.py | 207 ++++++++++++++ kiauh/components/webui_client/client_utils.py | 237 ++++++++++++++++ .../menus/__init__.py | 0 .../webui_client/menus/client_remove_menu.py | 149 ++++++++++ kiauh/core/menus/backup_menu.py | 41 ++- kiauh/core/menus/install_menu.py | 67 ++--- kiauh/core/menus/main_menu.py | 31 ++- kiauh/core/menus/remove_menu.py | 8 +- kiauh/core/menus/update_menu.py | 116 ++++---- kiauh/utils/filesystem_utils.py | 112 +++++++- 35 files changed, 1248 insertions(+), 1587 deletions(-) delete mode 100644 kiauh/components/fluidd/__init__.py delete mode 100644 kiauh/components/fluidd/assets/fluidd-config-updater.conf delete mode 100644 kiauh/components/fluidd/assets/fluidd-updater.conf delete mode 100644 kiauh/components/fluidd/fluidd_remove.py delete mode 100644 kiauh/components/fluidd/fluidd_setup.py delete mode 100644 kiauh/components/fluidd/fluidd_utils.py delete mode 100644 kiauh/components/fluidd/menus/fluidd_remove_menu.py delete mode 100644 kiauh/components/mainsail/__init__.py delete mode 100644 kiauh/components/mainsail/assets/mainsail-config-updater.conf delete mode 100644 kiauh/components/mainsail/assets/mainsail-updater.conf delete mode 100644 kiauh/components/mainsail/mainsail_dialogs.py delete mode 100644 kiauh/components/mainsail/mainsail_remove.py delete mode 100644 kiauh/components/mainsail/mainsail_setup.py delete mode 100644 kiauh/components/mainsail/mainsail_utils.py delete mode 100644 kiauh/components/mainsail/menus/mainsail_remove_menu.py create mode 100644 kiauh/components/webui_client/__init__.py rename kiauh/components/{fluidd/menus => webui_client/client_config}/__init__.py (100%) create mode 100644 kiauh/components/webui_client/client_config/client_config_remove.py create mode 100644 kiauh/components/webui_client/client_config/client_config_setup.py rename kiauh/components/{fluidd/fluidd_dialogs.py => webui_client/client_dialogs.py} (75%) create mode 100644 kiauh/components/webui_client/client_remove.py create mode 100644 kiauh/components/webui_client/client_setup.py create mode 100644 kiauh/components/webui_client/client_utils.py rename kiauh/components/{mainsail => webui_client}/menus/__init__.py (100%) create mode 100644 kiauh/components/webui_client/menus/client_remove_menu.py diff --git a/kiauh/components/fluidd/__init__.py b/kiauh/components/fluidd/__init__.py deleted file mode 100644 index 70bc91f..0000000 --- a/kiauh/components/fluidd/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 pathlib import Path - -from core.backup_manager import BACKUP_ROOT_DIR - -MODULE_PATH = Path(__file__).resolve().parent -FLUIDD_DIR = Path.home().joinpath("fluidd") -FLUIDD_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("fluidd-backups") -FLUIDD_CONFIG_DIR = Path.home().joinpath("fluidd-config") -FLUIDD_NGINX_CFG = Path("/etc/nginx/sites-enabled/fluidd") -FLUIDD_URL = "https://github.com/fluidd-core/fluidd/releases/latest/download/fluidd.zip" -FLUIDD_UNSTABLE_URL = ( - "https://github.com/fluidd-core/fluidd/releases/download/%TAG%/fluidd.zip" -) -FLUIDD_CONFIG_REPO_URL = "https://github.com/fluidd-core/fluidd-config.git" - diff --git a/kiauh/components/fluidd/assets/fluidd-config-updater.conf b/kiauh/components/fluidd/assets/fluidd-config-updater.conf deleted file mode 100644 index bc4a5cc..0000000 --- a/kiauh/components/fluidd/assets/fluidd-config-updater.conf +++ /dev/null @@ -1,6 +0,0 @@ -[update_manager fluidd-config] -type: git_repo -primary_branch: master -path: ~/fluidd-config -origin: https://github.com/fluidd-core/fluidd-config.git -managed_services: klipper diff --git a/kiauh/components/fluidd/assets/fluidd-updater.conf b/kiauh/components/fluidd/assets/fluidd-updater.conf deleted file mode 100644 index d9d1e7d..0000000 --- a/kiauh/components/fluidd/assets/fluidd-updater.conf +++ /dev/null @@ -1,5 +0,0 @@ -[update_manager fluidd] -type: web -channel: stable -repo: fluidd-core/fluidd -path: ~/fluidd diff --git a/kiauh/components/fluidd/fluidd_remove.py b/kiauh/components/fluidd/fluidd_remove.py deleted file mode 100644 index b617cec..0000000 --- a/kiauh/components/fluidd/fluidd_remove.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - - -import shutil -import subprocess -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.fluidd import FLUIDD_DIR, FLUIDD_CONFIG_DIR -from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager -from core.instance_manager.instance_manager import InstanceManager -from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from utils.filesystem_utils import remove_file -from utils.logger import Logger - - -def run_fluidd_removal( - remove_fluidd: bool, - remove_fl_config: bool, - remove_mr_updater_section: bool, - remove_flc_printer_cfg_include: bool, -) -> None: - if remove_fluidd: - remove_fluidd_dir() - remove_nginx_config() - remove_nginx_logs() - if remove_mr_updater_section: - remove_updater_section("update_manager fluidd") - if remove_fl_config: - remove_fluidd_cfg_dir() - remove_fluidd_cfg_symlink() - if remove_mr_updater_section: - remove_updater_section("update_manager fluidd-config") - if remove_flc_printer_cfg_include: - remove_printer_cfg_include() - - -def remove_fluidd_dir() -> None: - Logger.print_status("Removing Fluidd ...") - if not FLUIDD_DIR.exists(): - Logger.print_info(f"'{FLUIDD_DIR}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(FLUIDD_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{FLUIDD_DIR}':\n{e}") - - -def remove_nginx_config() -> None: - Logger.print_status("Removing Fluidd NGINX config ...") - try: - remove_file(NGINX_SITES_AVAILABLE.joinpath("fluidd"), True) - remove_file(NGINX_SITES_ENABLED.joinpath("fluidd"), True) - - except subprocess.CalledProcessError as e: - log = f"Unable to remove Fluidd NGINX config:\n{e.stderr.decode()}" - Logger.print_error(log) - - -def remove_nginx_logs() -> None: - Logger.print_status("Removing Fluidd NGINX logs ...") - try: - remove_file(Path("/var/log/nginx/fluidd-access.log"), True) - remove_file(Path("/var/log/nginx/fluidd-error.log"), True) - - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - if not instances: - return - - for instance in instances: - remove_file(instance.log_dir.joinpath("fluidd-access.log")) - remove_file(instance.log_dir.joinpath("fluidd-error.log")) - - except (OSError, subprocess.CalledProcessError) as e: - Logger.print_error(f"Unable to NGINX logs:\n{e}") - - -def remove_updater_section(name: str) -> None: - Logger.print_status("Remove updater section from moonraker.conf ...") - im = InstanceManager(Moonraker) - instances: List[Moonraker] = im.instances - if not instances: - Logger.print_info("Moonraker not installed. Skipped ...") - return - - for instance in instances: - Logger.print_status(f"Remove section '{name}' in '{instance.cfg_file}' ...") - - if not instance.cfg_file.is_file(): - Logger.print_info(f"'{instance.cfg_file}' does not exist. Skipped ...") - continue - - cm = ConfigManager(instance.cfg_file) - if not cm.config.has_section(name): - Logger.print_info("Section not present. Skipped ...") - continue - - cm.config.remove_section(name) - cm.write_config() - - -def remove_fluidd_cfg_dir() -> None: - Logger.print_status("Removing fluidd-config ...") - if not FLUIDD_CONFIG_DIR.exists(): - Logger.print_info(f"'{FLUIDD_CONFIG_DIR}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(FLUIDD_CONFIG_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{FLUIDD_CONFIG_DIR}':\n{e}") - - -def remove_fluidd_cfg_symlink() -> None: - Logger.print_status("Removing fluidd.cfg symlinks ...") - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - for instance in instances: - Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") - try: - remove_file(instance.cfg_dir.joinpath("fluidd.cfg")) - except subprocess.CalledProcessError: - Logger.print_error("Failed to remove symlink!") - - -def remove_printer_cfg_include() -> None: - Logger.print_status("Remove fluidd-config include from printer.cfg ...") - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - if not instances: - Logger.print_info("Klipper not installed. Skipping ...") - return - - for instance in instances: - log = f"Removing include from '{instance.cfg_file}' ..." - Logger.print_status(log) - - if not instance.cfg_file.is_file(): - continue - - cm = ConfigManager(instance.cfg_file) - if not cm.config.has_section("include fluidd.cfg"): - Logger.print_info("Section not present. Skipped ...") - continue - - cm.config.remove_section("include fluidd.cfg") - cm.write_config() diff --git a/kiauh/components/fluidd/fluidd_setup.py b/kiauh/components/fluidd/fluidd_setup.py deleted file mode 100644 index 7c23bb5..0000000 --- a/kiauh/components/fluidd/fluidd_setup.py +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import subprocess -from pathlib import Path -from typing import List - -from components.fluidd import ( - FLUIDD_URL, - FLUIDD_CONFIG_REPO_URL, - FLUIDD_CONFIG_DIR, - FLUIDD_DIR, - MODULE_PATH, -) -from components.fluidd.fluidd_dialogs import ( - print_fluidd_already_installed_dialog, - print_install_fluidd_config_dialog, - print_fluidd_port_select_dialog, - print_moonraker_not_found_dialog, -) -from components.fluidd.fluidd_utils import symlink_webui_nginx_log -from kiauh import KIAUH_CFG -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager -from core.instance_manager.instance_manager import InstanceManager -from core.repo_manager.repo_manager import RepoManager -from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from utils.common import check_install_dependencies -from utils.filesystem_utils import ( - unzip, - copy_upstream_nginx_cfg, - copy_common_vars_nginx_cfg, - create_nginx_cfg, - create_symlink, - remove_file, - read_ports_from_nginx_configs, - get_next_free_port, is_valid_port, - ) -from utils.input_utils import get_confirm, get_number_input -from utils.logger import Logger -from utils.system_utils import ( - download_file, - set_nginx_permissions, - get_ipv4_addr, - control_systemd_service, -) - - -def install_fluidd() -> None: - mr_im = InstanceManager(Moonraker) - mr_instances: List[Moonraker] = mr_im.instances - - if not mr_instances: - print_moonraker_not_found_dialog() - if not get_confirm("Continue Fluidd installation?", allow_go_back=True): - return - - if Path.home().joinpath("fluidd").exists(): - print_fluidd_already_installed_dialog() - do_reinstall = get_confirm("Re-install Fluidd?", allow_go_back=True) - if not do_reinstall: - return - - kl_im = InstanceManager(Klipper) - kl_instances = kl_im.instances - install_fl_config = False - if kl_instances: - print_install_fluidd_config_dialog() - question = "Download the recommended macros?" - install_fl_config = get_confirm(question, allow_go_back=False) - - cm = ConfigManager(cfg_file=KIAUH_CFG) - fluidd_port = cm.get_value("fluidd", "port") - ports_in_use = read_ports_from_nginx_configs() - - # check if configured port is a valid number and not in use already - valid_port = is_valid_port(fluidd_port, ports_in_use) - while not valid_port: - next_port = get_next_free_port(ports_in_use) - print_fluidd_port_select_dialog(next_port, ports_in_use) - fluidd_port = str(get_number_input( - "Configure Fluidd for port", - min_count=int(next_port), - default=next_port, - )) - valid_port = is_valid_port(fluidd_port, ports_in_use) - - check_install_dependencies(["nginx"]) - - try: - download_fluidd() - if mr_instances: - patch_moonraker_conf( - mr_instances, - "Fluidd", - "update_manager fluidd", - "fluidd-updater.conf", - ) - mr_im.restart_all_instance() - if install_fl_config and kl_instances: - download_fluidd_cfg() - create_fluidd_cfg_symlink(kl_instances) - patch_moonraker_conf( - mr_instances, - "fluidd-config", - "update_manager fluidd-config", - "fluidd-config-updater.conf", - ) - patch_printer_config(kl_instances) - kl_im.restart_all_instance() - - copy_upstream_nginx_cfg() - copy_common_vars_nginx_cfg() - create_fluidd_nginx_cfg(fluidd_port) - if kl_instances: - symlink_webui_nginx_log(kl_instances) - control_systemd_service("nginx", "restart") - - except Exception as e: - Logger.print_error(f"Fluidd installation failed!\n{e}") - return - - log = f"Open Fluidd now on: http://{get_ipv4_addr()}:{fluidd_port}" - Logger.print_ok("Fluidd installation complete!", start="\n") - Logger.print_ok(log, prefix=False, end="\n\n") - - -def download_fluidd() -> None: - try: - Logger.print_status("Downloading Fluidd ...") - target = Path.home().joinpath("fluidd.zip") - download_file(FLUIDD_URL, target, True) - Logger.print_ok("Download complete!") - - Logger.print_status("Extracting fluidd.zip ...") - unzip(Path.home().joinpath("fluidd.zip"), FLUIDD_DIR) - target.unlink(missing_ok=True) - Logger.print_ok("OK!") - - except Exception: - Logger.print_error("Downloading Fluidd failed!") - raise - - -def update_fluidd() -> None: - Logger.print_status("Updating Fluidd ...") - download_fluidd() - - -def download_fluidd_cfg() -> None: - try: - Logger.print_status("Downloading fluidd-config ...") - rm = RepoManager(FLUIDD_CONFIG_REPO_URL, target_dir=FLUIDD_CONFIG_DIR) - rm.clone_repo() - except Exception: - Logger.print_error("Downloading fluidd-config failed!") - raise - - -def create_fluidd_cfg_symlink(klipper_instances: List[Klipper]) -> None: - Logger.print_status("Create symlink of fluidd.cfg ...") - source = Path(FLUIDD_CONFIG_DIR, "fluidd.cfg") - for instance in klipper_instances: - target = instance.cfg_dir - Logger.print_status(f"Linking {source} to {target}") - try: - create_symlink(source, target) - except subprocess.CalledProcessError: - Logger.print_error("Creating symlink failed!") - - -def create_fluidd_nginx_cfg(port: int) -> None: - root_dir = FLUIDD_DIR - source = NGINX_SITES_AVAILABLE.joinpath("fluidd") - target = NGINX_SITES_ENABLED.joinpath("fluidd") - try: - Logger.print_status("Creating NGINX config for Fluidd ...") - remove_file(Path("/etc/nginx/sites-enabled/default"), True) - create_nginx_cfg("fluidd", port, root_dir) - create_symlink(source, target, True) - set_nginx_permissions() - Logger.print_ok("NGINX config for Fluidd successfully created.") - except Exception: - Logger.print_error("Creating NGINX config for Fluidd failed!") - raise - - -# TODO: could be fully extracted, its webui agnostic, and used for mainsail and fluidd -def patch_moonraker_conf( - moonraker_instances: List[Moonraker], - name: str, - section_name: str, - template_file: str, -) -> None: - for instance in moonraker_instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Add {name} update section to '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - return - - cm = ConfigManager(cfg_file) - if cm.config.has_section(section_name): - Logger.print_info("Section already exist. Skipped ...") - return - - template = MODULE_PATH.joinpath("assets", template_file) - with open(template, "r") as t: - template_content = "\n" - template_content += t.read() - - with open(cfg_file, "a") as f: - f.write(template_content) - - -# TODO: could be made fully webui agnostic and extracted, and used for mainsail and fluidd -def patch_printer_config(klipper_instances: List[Klipper]) -> None: - for instance in klipper_instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Including fluidd-config in '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - return - - cm = ConfigManager(cfg_file) - if cm.config.has_section("include fluidd.cfg"): - Logger.print_info("Section already exist. Skipped ...") - return - - with open(cfg_file, "a") as f: - f.write("\n[include fluidd.cfg]") diff --git a/kiauh/components/fluidd/fluidd_utils.py b/kiauh/components/fluidd/fluidd_utils.py deleted file mode 100644 index c9c672b..0000000 --- a/kiauh/components/fluidd/fluidd_utils.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import json -import urllib.request -from json import JSONDecodeError -from pathlib import Path -from typing import List - -from components.fluidd import FLUIDD_DIR, FLUIDD_BACKUP_DIR -from components.klipper.klipper import Klipper -from core.backup_manager.backup_manager import BackupManager -from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD -from utils.common import get_install_status_webui -from utils.logger import Logger - - -# TODO: could be extracted and made generic -def get_fluidd_status() -> str: - return get_install_status_webui( - FLUIDD_DIR, - NGINX_SITES_AVAILABLE.joinpath("fluidd"), - NGINX_CONFD.joinpath("upstreams.conf"), - NGINX_CONFD.joinpath("common_vars.conf"), - ) - - -# TODO: could be extracted and made generic -def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: - Logger.print_status("Link NGINX logs into log directory ...") - access_log = Path("/var/log/nginx/fluidd-access.log") - error_log = Path("/var/log/nginx/fluidd-error.log") - - for instance in klipper_instances: - desti_access = instance.log_dir.joinpath("fluidd-access.log") - if not desti_access.exists(): - desti_access.symlink_to(access_log) - - desti_error = instance.log_dir.joinpath("fluidd-error.log") - if not desti_error.exists(): - desti_error.symlink_to(error_log) - - -# TODO: could be extracted and made generic -def get_fluidd_local_version() -> str: - relinfo_file = FLUIDD_DIR.joinpath("release_info.json") - if not relinfo_file.is_file(): - return "-" - - with open(relinfo_file, "r") as f: - return json.load(f)["version"] - - -# TODO: could be extracted and made generic -def get_fluidd_remote_version() -> str: - url = "https://api.github.com/repos/fluidd-core/fluidd/tags" - try: - with urllib.request.urlopen(url) as response: - data = json.loads(response.read()) - return data[0]["name"] - except (JSONDecodeError, TypeError): - return "ERROR" - - -# TODO: could be extracted and made generic -def backup_fluidd_data() -> None: - with open(FLUIDD_DIR.joinpath(".version"), "r") as v: - version = v.readlines()[0] - bm = BackupManager() - bm.backup_directory(f"fluidd-{version}", FLUIDD_DIR, FLUIDD_BACKUP_DIR) - bm.backup_file(NGINX_SITES_AVAILABLE.joinpath("fluidd"), FLUIDD_BACKUP_DIR) diff --git a/kiauh/components/fluidd/menus/fluidd_remove_menu.py b/kiauh/components/fluidd/menus/fluidd_remove_menu.py deleted file mode 100644 index 4671799..0000000 --- a/kiauh/components/fluidd/menus/fluidd_remove_menu.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import textwrap - -from components.fluidd import fluidd_remove -from core.menus import BACK_HELP_FOOTER -from core.menus.base_menu import BaseMenu -from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN - - -# noinspection PyUnusedLocal -class FluiddRemoveMenu(BaseMenu): - def __init__(self): - super().__init__( - header=False, - options={ - "0": self.toggle_all, - "1": self.toggle_remove_fluidd, - "2": self.toggle_remove_fl_config, - "3": self.toggle_remove_updater_section, - "4": self.toggle_remove_printer_cfg_include, - "5": self.run_removal_process, - }, - footer_type=BACK_HELP_FOOTER, - ) - self.remove_fluidd = False - self.remove_fl_config = False - self.remove_updater_section = False - self.remove_printer_cfg_include = False - - def print_menu(self) -> None: - header = " [ Remove Fluidd ] " - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" - unchecked = "[ ]" - o1 = checked if self.remove_fluidd else unchecked - o2 = checked if self.remove_fl_config else unchecked - o3 = checked if self.remove_updater_section else unchecked - o4 = checked if self.remove_printer_cfg_include else unchecked - menu = textwrap.dedent( - f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Enter a number and hit enter to select / deselect | - | the specific option for removal. | - |-------------------------------------------------------| - | 0) Select everything | - |-------------------------------------------------------| - | 1) {o1} Remove Fluidd | - | 2) {o2} Remove fluidd-config | - | | - | printer.cfg & moonraker.conf | - | 3) {o3} Remove Moonraker update section | - | 4) {o4} Remove printer.cfg include | - |-------------------------------------------------------| - | 5) Continue | - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.remove_fluidd = True - self.remove_fl_config = True - self.remove_updater_section = True - self.remove_printer_cfg_include = True - - def toggle_remove_fluidd(self, **kwargs) -> None: - self.remove_fluidd = not self.remove_fluidd - - def toggle_remove_fl_config(self, **kwargs) -> None: - self.remove_fl_config = not self.remove_fl_config - - def toggle_remove_updater_section(self, **kwargs) -> None: - self.remove_updater_section = not self.remove_updater_section - - def toggle_remove_printer_cfg_include(self, **kwargs) -> None: - self.remove_printer_cfg_include = not self.remove_printer_cfg_include - - def run_removal_process(self, **kwargs) -> None: - if ( - not self.remove_fluidd - and not self.remove_fl_config - and not self.remove_updater_section - and not self.remove_printer_cfg_include - ): - error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" - print(error) - return - - fluidd_remove.run_fluidd_removal( - remove_fluidd=self.remove_fluidd, - remove_fl_config=self.remove_fl_config, - remove_mr_updater_section=self.remove_updater_section, - remove_flc_printer_cfg_include=self.remove_printer_cfg_include, - ) - - self.remove_fluidd = False - self.remove_fl_config = False - self.remove_updater_section = False - self.remove_printer_cfg_include = False diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index fc953ab..dc92b33 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -28,7 +28,7 @@ class KlipperRemoveMenu(BaseMenu): "2": self.toggle_remove_klipper_dir, "3": self.toggle_remove_klipper_env, "4": self.toggle_delete_klipper_logs, - "5": self.run_removal_process, + "c": self.run_removal_process, }, footer_type=BACK_HELP_FOOTER, ) @@ -62,7 +62,7 @@ class KlipperRemoveMenu(BaseMenu): | 3) {o3} Remove Python Environment | | 4) {o4} Delete all Log-Files | |-------------------------------------------------------| - | 5) Continue | + | C) Continue | """ )[1:] print(menu, end="") diff --git a/kiauh/components/mainsail/__init__.py b/kiauh/components/mainsail/__init__.py deleted file mode 100644 index 53a9924..0000000 --- a/kiauh/components/mainsail/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 pathlib import Path - -from core.backup_manager import BACKUP_ROOT_DIR - -MODULE_PATH = Path(__file__).resolve().parent -MAINSAIL_DIR = Path.home().joinpath("mainsail") -MAINSAIL_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mainsail-backups") -MAINSAIL_CONFIG_DIR = Path.home().joinpath("mainsail-config") -MAINSAIL_CONFIG_JSON = MAINSAIL_DIR.joinpath("config.json") -MAINSAIL_URL = ( - "https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" -) -MAINSAIL_UNSTABLE_URL = ( - "https://github.com/mainsail-crew/mainsail/releases/download/%TAG%/mainsail.zip" -) -MAINSAIL_CONFIG_REPO_URL = "https://github.com/mainsail-crew/mainsail-config.git" diff --git a/kiauh/components/mainsail/assets/mainsail-config-updater.conf b/kiauh/components/mainsail/assets/mainsail-config-updater.conf deleted file mode 100644 index 02bb789..0000000 --- a/kiauh/components/mainsail/assets/mainsail-config-updater.conf +++ /dev/null @@ -1,6 +0,0 @@ -[update_manager mainsail-config] -type: git_repo -primary_branch: master -path: ~/mainsail-config -origin: https://github.com/mainsail-crew/mainsail-config.git -managed_services: klipper diff --git a/kiauh/components/mainsail/assets/mainsail-updater.conf b/kiauh/components/mainsail/assets/mainsail-updater.conf deleted file mode 100644 index f668332..0000000 --- a/kiauh/components/mainsail/assets/mainsail-updater.conf +++ /dev/null @@ -1,5 +0,0 @@ -[update_manager mainsail] -type: web -channel: stable -repo: mainsail-crew/mainsail -path: ~/mainsail diff --git a/kiauh/components/mainsail/mainsail_dialogs.py b/kiauh/components/mainsail/mainsail_dialogs.py deleted file mode 100644 index 36453ff..0000000 --- a/kiauh/components/mainsail/mainsail_dialogs.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import textwrap - -from core.menus.base_menu import print_back_footer -from utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN - - -def print_moonraker_not_found_dialog(): - line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" - line2 = f"{COLOR_YELLOW}No local Moonraker installation was found!{RESET_FORMAT}" - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | {line1:<63}| - | {line2:<63}| - |-------------------------------------------------------| - | It is possible to install Mainsail without a local | - | Moonraker installation. If you continue, you need to | - | make sure, that Moonraker is installed on another | - | machine in your network. Otherwise Mainsail will NOT | - | work correctly. | - """ - )[1:] - - print(dialog, end="") - print_back_footer() - - -def print_mainsail_already_installed_dialog(): - line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" - line2 = f"{COLOR_YELLOW}Mainsail seems to be already installed!{RESET_FORMAT}" - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | {line1:<63}| - | {line2:<63}| - |-------------------------------------------------------| - | If you continue, your current Mainsail installation | - | will be overwritten. You will not loose any printer | - | configurations and the Moonraker database will remain | - | untouched. | - """ - )[1:] - - print(dialog, end="") - print_back_footer() - - -def print_install_mainsail_config_dialog(): - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | It is recommended to use special macros in order to | - | have Mainsail fully functional and working. | - | | - | The recommended macros for Mainsail can be seen here: | - | https://github.com/mainsail-crew/mainsail-config | - | | - | If you already use these macros skip this step. | - | Otherwise you should consider to answer with 'Y' to | - | download the recommended macros. | - \\=======================================================/ - """ - )[1:] - - print(dialog, end="") - - -def print_mainsail_port_select_dialog(port: str): - port = f"{COLOR_CYAN}{port}{RESET_FORMAT}" - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | Please select the port, Mainsail should be served on. | - | If you are unsure what to select, hit Enter to apply | - | the suggested value of: {port:38} | - | | - | In case you need Mainsail to be served on a specific | - | port, you can set it now. Make sure the port is not | - | used by any other application on your system! | - \\=======================================================/ - """ - )[1:] - - print(dialog, end="") diff --git a/kiauh/components/mainsail/mainsail_remove.py b/kiauh/components/mainsail/mainsail_remove.py deleted file mode 100644 index 204e8a6..0000000 --- a/kiauh/components/mainsail/mainsail_remove.py +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - - -import shutil -import subprocess -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR -from components.mainsail.mainsail_utils import backup_config_json -from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager -from core.instance_manager.instance_manager import InstanceManager -from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from utils.filesystem_utils import remove_file -from utils.logger import Logger - - -def run_mainsail_removal( - remove_mainsail: bool, - remove_ms_config: bool, - backup_ms_config_json: bool, - remove_mr_updater_section: bool, - remove_msc_printer_cfg_include: bool, -) -> None: - if backup_ms_config_json: - backup_config_json() - if remove_mainsail: - remove_mainsail_dir() - remove_nginx_config() - remove_nginx_logs() - if remove_mr_updater_section: - remove_updater_section("update_manager mainsail") - if remove_ms_config: - remove_mainsail_cfg_dir() - remove_mainsail_cfg_symlink() - if remove_mr_updater_section: - remove_updater_section("update_manager mainsail-config") - if remove_msc_printer_cfg_include: - remove_printer_cfg_include() - - -def remove_mainsail_dir() -> None: - Logger.print_status("Removing Mainsail ...") - if not MAINSAIL_DIR.exists(): - Logger.print_info(f"'{MAINSAIL_DIR}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(MAINSAIL_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{MAINSAIL_DIR}':\n{e}") - - -def remove_nginx_config() -> None: - Logger.print_status("Removing Mainsails NGINX config ...") - try: - remove_file(NGINX_SITES_AVAILABLE.joinpath("mainsail"), True) - remove_file(NGINX_SITES_ENABLED.joinpath("mainsail"), True) - - except subprocess.CalledProcessError as e: - log = f"Unable to remove Mainsail NGINX config:\n{e.stderr.decode()}" - Logger.print_error(log) - - -def remove_nginx_logs() -> None: - Logger.print_status("Removing Mainsails NGINX logs ...") - try: - remove_file(Path("/var/log/nginx/mainsail-access.log"), True) - remove_file(Path("/var/log/nginx/mainsail-error.log"), True) - - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - if not instances: - return - - for instance in instances: - remove_file(instance.log_dir.joinpath("mainsail-access.log")) - remove_file(instance.log_dir.joinpath("mainsail-error.log")) - - except (OSError, subprocess.CalledProcessError) as e: - Logger.print_error(f"Unable to NGINX logs:\n{e}") - - -def remove_updater_section(name: str) -> None: - Logger.print_status("Remove updater section from moonraker.conf ...") - im = InstanceManager(Moonraker) - instances: List[Moonraker] = im.instances - if not instances: - Logger.print_info("Moonraker not installed. Skipped ...") - return - - for instance in instances: - Logger.print_status(f"Remove section '{name}' in '{instance.cfg_file}' ...") - - if not instance.cfg_file.is_file(): - Logger.print_info(f"'{instance.cfg_file}' does not exist. Skipped ...") - continue - - cm = ConfigManager(instance.cfg_file) - if not cm.config.has_section(name): - Logger.print_info("Section not present. Skipped ...") - continue - - cm.config.remove_section(name) - cm.write_config() - - -def remove_mainsail_cfg_dir() -> None: - Logger.print_status("Removing mainsail-config ...") - if not MAINSAIL_CONFIG_DIR.exists(): - Logger.print_info(f"'{MAINSAIL_CONFIG_DIR}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(MAINSAIL_CONFIG_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{MAINSAIL_CONFIG_DIR}':\n{e}") - - -def remove_mainsail_cfg_symlink() -> None: - Logger.print_status("Removing mainsail.cfg symlinks ...") - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - for instance in instances: - Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") - try: - remove_file(instance.cfg_dir.joinpath("mainsail.cfg")) - except subprocess.CalledProcessError: - Logger.print_error("Failed to remove symlink!") - - -def remove_printer_cfg_include() -> None: - Logger.print_status("Remove mainsail-config include from printer.cfg ...") - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances - if not instances: - Logger.print_info("Klipper not installed. Skipping ...") - return - - for instance in instances: - log = f"Removing include from '{instance.cfg_file}' ..." - Logger.print_status(log) - - if not instance.cfg_file.is_file(): - continue - - cm = ConfigManager(instance.cfg_file) - if not cm.config.has_section("include mainsail.cfg"): - Logger.print_info("Section not present. Skipped ...") - continue - - cm.config.remove_section("include mainsail.cfg") - cm.write_config() diff --git a/kiauh/components/mainsail/mainsail_setup.py b/kiauh/components/mainsail/mainsail_setup.py deleted file mode 100644 index 210dc9e..0000000 --- a/kiauh/components/mainsail/mainsail_setup.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import subprocess -from pathlib import Path -from typing import List - -from kiauh import KIAUH_CFG -from components.klipper.klipper import Klipper -from components.mainsail import ( - MAINSAIL_URL, - MAINSAIL_DIR, - MAINSAIL_CONFIG_DIR, - MAINSAIL_CONFIG_REPO_URL, - MODULE_PATH, -) -from components.mainsail.mainsail_dialogs import ( - print_moonraker_not_found_dialog, - print_mainsail_already_installed_dialog, - print_install_mainsail_config_dialog, - print_mainsail_port_select_dialog, -) -from components.mainsail.mainsail_utils import ( - restore_config_json, - enable_mainsail_remotemode, - backup_config_json, - symlink_webui_nginx_log, -) -from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager -from core.instance_manager.instance_manager import InstanceManager -from core.repo_manager.repo_manager import RepoManager -from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from utils.common import check_install_dependencies -from utils.filesystem_utils import ( - unzip, - copy_upstream_nginx_cfg, - copy_common_vars_nginx_cfg, - create_nginx_cfg, - create_symlink, - remove_file, -) -from utils.input_utils import get_confirm, get_number_input -from utils.logger import Logger -from utils.system_utils import ( - download_file, - set_nginx_permissions, - get_ipv4_addr, - control_systemd_service, -) - - -def install_mainsail() -> None: - mr_im = InstanceManager(Moonraker) - mr_instances: List[Moonraker] = mr_im.instances - - enable_remotemode = False - if not mr_instances: - print_moonraker_not_found_dialog() - if not get_confirm("Continue Mainsail installation?", allow_go_back=True): - return - - # if moonraker is not installed or multiple instances - # are installed we enable mainsails remote mode - if not mr_instances or len(mr_instances) > 1: - enable_remotemode = True - - do_reinstall = False - if Path.home().joinpath("mainsail").exists(): - print_mainsail_already_installed_dialog() - do_reinstall = get_confirm("Re-install Mainsail?", allow_go_back=True) - if do_reinstall: - backup_config_json(is_temp=True) - else: - return - - kl_im = InstanceManager(Klipper) - kl_instances = kl_im.instances - install_ms_config = False - if kl_instances: - print_install_mainsail_config_dialog() - question = "Download the recommended macros?" - install_ms_config = get_confirm(question, allow_go_back=False) - - # if a default port is configured in the kiauh.cfg, we use that for the port - # otherwise we default to port 80, but show the user a dialog to confirm/change that port - cm = ConfigManager(cfg_file=KIAUH_CFG) - default_port = cm.get_value("mainsail", "default_port") - is_valid_port = default_port and default_port.isdigit() - mainsail_port = default_port if is_valid_port else "80" - if not is_valid_port: - print_mainsail_port_select_dialog(mainsail_port) - mainsail_port = get_number_input( - "Configure Mainsail for port", - min_count=mainsail_port, - default=mainsail_port, - ) - - check_install_dependencies(["nginx"]) - - try: - download_mainsail() - if do_reinstall: - restore_config_json() - if enable_remotemode: - enable_mainsail_remotemode() - if mr_instances: - patch_moonraker_conf( - mr_instances, - "Mainsail", - "update_manager mainsail", - "mainsail-updater.conf", - ) - mr_im.restart_all_instance() - if install_ms_config and kl_instances: - download_mainsail_cfg() - create_mainsail_cfg_symlink(kl_instances) - patch_moonraker_conf( - mr_instances, - "mainsail-config", - "update_manager mainsail-config", - "mainsail-config-updater.conf", - ) - patch_printer_config(kl_instances) - kl_im.restart_all_instance() - - copy_upstream_nginx_cfg() - copy_common_vars_nginx_cfg() - create_mainsail_nginx_cfg(mainsail_port) - if kl_instances: - symlink_webui_nginx_log(kl_instances) - control_systemd_service("nginx", "restart") - - except Exception as e: - Logger.print_error(f"Mainsail installation failed!\n{e}") - return - - log = f"Open Mainsail now on: http://{get_ipv4_addr()}:{mainsail_port}" - Logger.print_ok("Mainsail installation complete!", start="\n") - Logger.print_ok(log, prefix=False, end="\n\n") - - -def download_mainsail() -> None: - try: - Logger.print_status("Downloading Mainsail ...") - target = Path.home().joinpath("mainsail.zip") - download_file(MAINSAIL_URL, target, True) - Logger.print_ok("Download complete!") - - Logger.print_status("Extracting mainsail.zip ...") - unzip(Path.home().joinpath("mainsail.zip"), MAINSAIL_DIR) - target.unlink(missing_ok=True) - Logger.print_ok("OK!") - - except Exception: - Logger.print_error("Downloading Mainsail failed!") - raise - - -def update_mainsail() -> None: - Logger.print_status("Updating Mainsail ...") - backup_config_json(is_temp=True) - download_mainsail() - restore_config_json() - - -def download_mainsail_cfg() -> None: - try: - Logger.print_status("Downloading mainsail-config ...") - rm = RepoManager(MAINSAIL_CONFIG_REPO_URL, target_dir=MAINSAIL_CONFIG_DIR) - rm.clone_repo() - except Exception: - Logger.print_error("Downloading mainsail-config failed!") - raise - - -def create_mainsail_cfg_symlink(klipper_instances: List[Klipper]) -> None: - Logger.print_status("Create symlink of mainsail.cfg ...") - source = Path(MAINSAIL_CONFIG_DIR, "mainsail.cfg") - for instance in klipper_instances: - target = instance.cfg_dir - Logger.print_status(f"Linking {source} to {target}") - try: - create_symlink(source, target) - except subprocess.CalledProcessError: - Logger.print_error("Creating symlink failed!") - - -def create_mainsail_nginx_cfg(port: int) -> None: - root_dir = MAINSAIL_DIR - source = NGINX_SITES_AVAILABLE.joinpath("mainsail") - target = NGINX_SITES_ENABLED.joinpath("mainsail") - try: - Logger.print_status("Creating NGINX config for Mainsail ...") - remove_file(Path("/etc/nginx/sites-enabled/default"), True) - create_nginx_cfg("mainsail", port, root_dir) - create_symlink(source, target, True) - set_nginx_permissions() - Logger.print_ok("NGINX config for Mainsail successfully created.") - except Exception: - Logger.print_error("Creating NGINX config for Mainsail failed!") - raise - - -def patch_moonraker_conf( - moonraker_instances: List[Moonraker], - name: str, - section_name: str, - template_file: str, -) -> None: - for instance in moonraker_instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Add {name} update section to '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - return - - cm = ConfigManager(cfg_file) - if cm.config.has_section(section_name): - Logger.print_info("Section already exist. Skipped ...") - return - - template = MODULE_PATH.joinpath("assets", template_file) - with open(template, "r") as t: - template_content = "\n" - template_content += t.read() - - with open(cfg_file, "a") as f: - f.write(template_content) - - -def patch_printer_config(klipper_instances: List[Klipper]) -> None: - for instance in klipper_instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Including mainsail-config in '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - return - - cm = ConfigManager(cfg_file) - if cm.config.has_section("include mainsail.cfg"): - Logger.print_info("Section already exist. Skipped ...") - return - - with open(cfg_file, "a") as f: - f.write("\n[include mainsail.cfg]") diff --git a/kiauh/components/mainsail/mainsail_utils.py b/kiauh/components/mainsail/mainsail_utils.py deleted file mode 100644 index 321f326..0000000 --- a/kiauh/components/mainsail/mainsail_utils.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import json -import shutil -from json import JSONDecodeError -from pathlib import Path -from typing import List - -import urllib.request - -from components.klipper.klipper import Klipper -from components.mainsail import ( - MAINSAIL_CONFIG_JSON, - MAINSAIL_DIR, - MAINSAIL_BACKUP_DIR, -) -from core.backup_manager.backup_manager import BackupManager -from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD -from utils.common import get_install_status_webui -from utils.logger import Logger - - -def get_mainsail_status() -> str: - return get_install_status_webui( - MAINSAIL_DIR, - NGINX_SITES_AVAILABLE.joinpath("mainsail"), - NGINX_CONFD.joinpath("upstreams.conf"), - NGINX_CONFD.joinpath("common_vars.conf"), - ) - - -def backup_config_json(is_temp=False) -> None: - Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") - bm = BackupManager() - if is_temp: - fn = Path.home().joinpath("config.json.kiauh.bak") - bm.backup_file(MAINSAIL_CONFIG_JSON, custom_filename=fn) - else: - bm.backup_file(MAINSAIL_CONFIG_JSON) - - -def restore_config_json() -> None: - try: - Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") - source = Path.home().joinpath("config.json.kiauh.bak") - shutil.copy(source, MAINSAIL_CONFIG_JSON) - except OSError: - Logger.print_info("Unable to restore config.json. Skipped ...") - - -def enable_mainsail_remotemode() -> None: - Logger.print_status("Enable Mainsails remote mode ...") - with open(MAINSAIL_CONFIG_JSON, "r") as f: - config_data = json.load(f) - - if config_data["instancesDB"] == "browser": - Logger.print_info("Remote mode already configured. Skipped ...") - return - - Logger.print_status("Setting instance storage location to 'browser' ...") - config_data["instancesDB"] = "browser" - - with open(MAINSAIL_CONFIG_JSON, "w") as f: - json.dump(config_data, f, indent=4) - Logger.print_ok("Mainsails remote mode enabled!") - - -def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: - Logger.print_status("Link NGINX logs into log directory ...") - access_log = Path("/var/log/nginx/mainsail-access.log") - error_log = Path("/var/log/nginx/mainsail-error.log") - - for instance in klipper_instances: - desti_access = instance.log_dir.joinpath("mainsail-access.log") - if not desti_access.exists(): - desti_access.symlink_to(access_log) - - desti_error = instance.log_dir.joinpath("mainsail-error.log") - if not desti_error.exists(): - desti_error.symlink_to(error_log) - - -def get_mainsail_local_version() -> str: - relinfo_file = MAINSAIL_DIR.joinpath("release_info.json") - if not relinfo_file.is_file(): - return "-" - - with open(relinfo_file, "r") as f: - return json.load(f)["version"] - - -def get_mainsail_remote_version() -> str: - url = "https://api.github.com/repos/mainsail-crew/mainsail/tags" - try: - with urllib.request.urlopen(url) as response: - data = json.loads(response.read()) - return data[0]["name"] - except (JSONDecodeError, TypeError): - return "ERROR" - - -def backup_mainsail_data() -> None: - with open(MAINSAIL_DIR.joinpath(".version"), "r") as v: - version = v.readlines()[0] - bm = BackupManager() - bm.backup_directory(f"mainsail-{version}", MAINSAIL_DIR, MAINSAIL_BACKUP_DIR) - bm.backup_file(MAINSAIL_CONFIG_JSON, MAINSAIL_BACKUP_DIR) - bm.backup_file(NGINX_SITES_AVAILABLE.joinpath("mainsail"), MAINSAIL_BACKUP_DIR) diff --git a/kiauh/components/mainsail/menus/mainsail_remove_menu.py b/kiauh/components/mainsail/menus/mainsail_remove_menu.py deleted file mode 100644 index 48cfb4b..0000000 --- a/kiauh/components/mainsail/menus/mainsail_remove_menu.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import textwrap - -from components.mainsail import mainsail_remove -from core.menus import BACK_HELP_FOOTER -from core.menus.base_menu import BaseMenu -from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN - - -# noinspection PyUnusedLocal -class MainsailRemoveMenu(BaseMenu): - def __init__(self): - super().__init__( - header=False, - options={ - "0": self.toggle_all, - "1": self.toggle_remove_mainsail, - "2": self.toggle_remove_ms_config, - "3": self.toggle_backup_config_json, - "4": self.toggle_remove_updater_section, - "5": self.toggle_remove_printer_cfg_include, - "6": self.run_removal_process, - }, - footer_type=BACK_HELP_FOOTER, - ) - self.remove_mainsail = False - self.remove_ms_config = False - self.backup_config_json = False - self.remove_updater_section = False - self.remove_printer_cfg_include = False - - def print_menu(self) -> None: - header = " [ Remove Mainsail ] " - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" - unchecked = "[ ]" - o1 = checked if self.remove_mainsail else unchecked - o2 = checked if self.remove_ms_config else unchecked - o3 = checked if self.backup_config_json else unchecked - o4 = checked if self.remove_updater_section else unchecked - o5 = checked if self.remove_printer_cfg_include else unchecked - menu = textwrap.dedent( - f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Enter a number and hit enter to select / deselect | - | the specific option for removal. | - |-------------------------------------------------------| - | 0) Select everything | - |-------------------------------------------------------| - | 1) {o1} Remove Mainsail | - | 2) {o2} Remove mainsail-config | - | 3) {o3} Backup config.json | - | | - | printer.cfg & moonraker.conf | - | 4) {o4} Remove Moonraker update section | - | 5) {o5} Remove printer.cfg include | - |-------------------------------------------------------| - | 6) Continue | - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.remove_mainsail = True - self.remove_ms_config = True - self.backup_config_json = True - self.remove_updater_section = True - self.remove_printer_cfg_include = True - - def toggle_remove_mainsail(self, **kwargs) -> None: - self.remove_mainsail = not self.remove_mainsail - - def toggle_remove_ms_config(self, **kwargs) -> None: - self.remove_ms_config = not self.remove_ms_config - - def toggle_backup_config_json(self, **kwargs) -> None: - self.backup_config_json = not self.backup_config_json - - def toggle_remove_updater_section(self, **kwargs) -> None: - self.remove_updater_section = not self.remove_updater_section - - def toggle_remove_printer_cfg_include(self, **kwargs) -> None: - self.remove_printer_cfg_include = not self.remove_printer_cfg_include - - def run_removal_process(self, **kwargs) -> None: - if ( - not self.remove_mainsail - and not self.remove_ms_config - and not self.backup_config_json - and not self.remove_updater_section - and not self.remove_printer_cfg_include - ): - error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" - print(error) - return - - mainsail_remove.run_mainsail_removal( - remove_mainsail=self.remove_mainsail, - remove_ms_config=self.remove_ms_config, - backup_ms_config_json=self.backup_config_json, - remove_mr_updater_section=self.remove_updater_section, - remove_msc_printer_cfg_include=self.remove_printer_cfg_include, - ) - - self.remove_mainsail = False - self.remove_ms_config = False - self.backup_config_json = False - self.remove_updater_section = False - self.remove_printer_cfg_include = False diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 652d028..f740b45 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -29,7 +29,7 @@ class MoonrakerRemoveMenu(BaseMenu): "3": self.toggle_remove_moonraker_env, "4": self.toggle_remove_moonraker_polkit, "5": self.toggle_delete_moonraker_logs, - "6": self.run_removal_process, + "c": self.run_removal_process, }, footer_type=BACK_HELP_FOOTER, ) @@ -66,7 +66,7 @@ class MoonrakerRemoveMenu(BaseMenu): | 4) {o4} Remove Policy Kit Rules | | 5) {o5} Delete all Log-Files | |-------------------------------------------------------| - | 6) Continue | + | C) Continue | """ )[1:] print(menu, end="") diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index fe635f8..a4c6cf2 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -14,11 +14,11 @@ import sys from pathlib import Path from typing import List +from components.webui_client import MAINSAIL_DIR +from components.webui_client.client_utils import enable_mainsail_remotemode from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import print_instance_overview -from components.mainsail import MAINSAIL_DIR -from components.mainsail.mainsail_utils import enable_mainsail_remotemode from components.moonraker import ( EXIT_MOONRAKER_SETUP, DEFAULT_MOONRAKER_REPO_URL, diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 3321781..8f89364 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -12,8 +12,6 @@ import shutil from typing import Dict, Literal, List, Union -from components.mainsail import MAINSAIL_DIR -from components.mainsail.mainsail_utils import enable_mainsail_remotemode from components.moonraker import ( DEFAULT_MOONRAKER_PORT, MODULE_PATH, @@ -23,6 +21,8 @@ from components.moonraker import ( MOONRAKER_DB_BACKUP_DIR, ) from components.moonraker.moonraker import Moonraker +from components.webui_client import MAINSAIL_DIR +from components.webui_client.client_utils import enable_mainsail_remotemode from core.backup_manager.backup_manager import BackupManager from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager diff --git a/kiauh/components/webui_client/__init__.py b/kiauh/components/webui_client/__init__.py new file mode 100644 index 0000000..b0d44f3 --- /dev/null +++ b/kiauh/components/webui_client/__init__.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path +from typing import Literal, TypedDict, Set + +from core.backup_manager import BACKUP_ROOT_DIR + +MODULE_PATH = Path(__file__).resolve().parent + +########### +# MAINSAIL +########### +MAINSAIL_DIR = Path.home().joinpath("mainsail") +MAINSAIL_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mainsail-backups") +MAINSAIL_CONFIG_DIR = Path.home().joinpath("mainsail-config") +MAINSAIL_CONFIG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mainsail-config-backups") +MAINSAIL_CONFIG_REPO_URL = "https://github.com/mainsail-crew/mainsail-config.git" +MAINSAIL_CONFIG_JSON = MAINSAIL_DIR.joinpath("config.json") +MAINSAIL_URL = ( + "https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" +) +MAINSAIL_PRE_RLS_URL = ( + "https://github.com/mainsail-crew/mainsail/releases/download/%TAG%/mainsail.zip" +) +MAINSAIL_TAGS_URL = "https://api.github.com/repos/mainsail-crew/mainsail/tags" + +######### +# FLUIDD +######### +FLUIDD_DIR = Path.home().joinpath("fluidd") +FLUIDD_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("fluidd-backups") +FLUIDD_CONFIG_DIR = Path.home().joinpath("fluidd-config") +FLUIDD_CONFIG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("fluidd-config-backups") +FLUIDD_CONFIG_REPO_URL = "https://github.com/fluidd-core/fluidd-config.git" +FLUIDD_URL = "https://github.com/fluidd-core/fluidd/releases/latest/download/fluidd.zip" +FLUIDD_PRE_RLS_URL = ( + "https://github.com/fluidd-core/fluidd/releases/download/%TAG%/fluidd.zip" +) +FLUIDD_TAGS_URL = "https://api.github.com/repos/fluidd-core/fluidd/tags" + +ClientName = Literal["mainsail", "fluidd"] +ClientConfigName = Literal["mainsail-config", "fluidd-config"] + + +class ClientData(TypedDict): + name: ClientName + display_name: str + dir: Path + backup_dir: Path + url: str + pre_release_url: str + tags_url: str + remote_mode: bool # required only for Mainsail + mr_conf_repo: str + mr_conf_path: str + client_config: "ClientConfigData" + + +class ClientConfigData(TypedDict): + name: ClientConfigName + display_name: str + cfg_filename: str + dir: Path + backup_dir: Path + url: str + printer_cfg_section: str + mr_conf_path: str + mr_conf_origin: str diff --git a/kiauh/components/fluidd/menus/__init__.py b/kiauh/components/webui_client/client_config/__init__.py similarity index 100% rename from kiauh/components/fluidd/menus/__init__.py rename to kiauh/components/webui_client/client_config/__init__.py diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py new file mode 100644 index 0000000..41203a3 --- /dev/null +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + + +import shutil +import subprocess +from typing import List + +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from components.webui_client import ClientConfigData +from core.instance_manager.instance_manager import InstanceManager +from utils.filesystem_utils import remove_file, remove_config_section +from utils.logger import Logger + + +def run_client_config_removal( + client_config: ClientConfigData, + remove_moonraker_conf_section: bool, + remove_printer_cfg_include: bool, + kl_instances: List[Klipper], + mr_instances: List[Moonraker], +) -> None: + remove_client_config_dir(client_config) + remove_client_config_symlink(client_config) + if remove_moonraker_conf_section: + remove_config_section( + f"update_manager {client_config.get('name')}", mr_instances + ) + if remove_printer_cfg_include: + remove_config_section(client_config.get("printer_cfg_section"), kl_instances) + + +def remove_client_config_dir(client_config: ClientConfigData) -> None: + Logger.print_status(f"Removing {client_config.get('name')} ...") + client_config_dir = client_config.get("dir") + if not client_config_dir.exists(): + Logger.print_info(f"'{client_config_dir}' does not exist. Skipping ...") + return + + try: + shutil.rmtree(client_config_dir) + except OSError as e: + Logger.print_error(f"Unable to delete '{client_config_dir}':\n{e}") + + +def remove_client_config_symlink(client_config: ClientConfigData) -> None: + Logger.print_status(f"Removing {client_config.get('cfg_filename')} symlinks ...") + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + for instance in instances: + Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") + try: + remove_file(instance.cfg_dir.joinpath(client_config.get("cfg_filename"))) + except subprocess.CalledProcessError: + Logger.print_error("Failed to remove symlink!") diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py new file mode 100644 index 0000000..d893198 --- /dev/null +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import shutil +import subprocess +from pathlib import Path +from typing import List, get_args + +from kiauh import KIAUH_CFG +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from components.webui_client import ClientConfigData, ClientName, ClientData +from components.webui_client.client_dialogs import print_client_already_installed_dialog +from components.webui_client.client_utils import ( + load_client_data, + backup_client_config_data, +) +from core.config_manager.config_manager import ConfigManager + +from core.instance_manager.instance_manager import InstanceManager +from core.repo_manager.repo_manager import RepoManager +from utils.filesystem_utils import ( + create_symlink, + add_config_section, +) +from utils.input_utils import get_confirm +from utils.logger import Logger + + +def install_client_config(client_name: ClientName) -> None: + client: ClientData = load_client_data(client_name) + client_config: ClientConfigData = client.get("client_config") + d_name = client_config.get("display_name") + + if check_existing_client_config_install(client_name): + Logger.print_info("Another Client-Config is already installed! Skipped ...") + return + + if client_config.get("dir").exists(): + print_client_already_installed_dialog(d_name) + if get_confirm(f"Re-install {d_name}?", allow_go_back=True): + shutil.rmtree(client_config.get("dir")) + else: + return + + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + kl_im = InstanceManager(Klipper) + kl_instances = kl_im.instances + + try: + download_client_config(client_config) + create_client_config_symlink(client_config, kl_instances) + add_config_section( + section=f"update_manager {client_config.get('name')}", + instances=mr_instances, + options=[ + ("type", "git_repo"), + ("primary_branch", "master"), + ("path", client_config.get("mr_conf_path")), + ("origin", client_config.get("mr_conf_origin")), + ("managed_services", "klipper"), + ], + ) + add_config_section(client_config.get("printer_cfg_section"), kl_instances) + kl_im.restart_all_instance() + + except Exception as e: + Logger.print_error(f"{d_name} installation failed!\n{e}") + return + + Logger.print_ok(f"{d_name} installation complete!", start="\n") + + +def check_existing_client_config_install(client_name: ClientName) -> bool: + # check if any other client-configs are present + # as they can conflict each other, or are at least + # redundant to have, so we skip the installation + client_list = list(get_args(ClientName)) + client_list.remove(client_name) + for c in client_list: + c_data: ClientData = load_client_data(c) + c_config_data: ClientConfigData = c_data.get("client_config") + if c_config_data.get("dir").exists(): + return True + + return False + + +def download_client_config(client_config: ClientConfigData) -> None: + try: + Logger.print_status(f"Downloading {client_config.get('display_name')} ...") + rm = RepoManager( + client_config.get("url"), target_dir=str(client_config.get("dir")) + ) + rm.clone_repo() + except Exception: + Logger.print_error(f"Downloading {client_config.get('display_name')} failed!") + raise + + +def update_client_config(client: ClientData) -> None: + client_config: ClientConfigData = client.get("client_config") + + Logger.print_status(f"Updating {client_config.get('display_name')} ...") + + cm = ConfigManager(cfg_file=KIAUH_CFG) + if cm.get_value("kiauh", "backup_before_update"): + backup_client_config_data(client) + + repo_manager = RepoManager( + repo=client_config.get("url"), + branch="master", + target_dir=str(client_config.get("dir")), + ) + repo_manager.pull_repo() + + Logger.print_ok(f"Successfully updated {client_config.get('display_name')}.") + Logger.print_warn("Remember to restart Klipper to reload the configurations!") + + +def create_client_config_symlink( + client_config: ClientConfigData, klipper_instances: List[Klipper] = None +) -> None: + if klipper_instances is None: + kl_im = InstanceManager(Klipper) + klipper_instances = kl_im.instances + + Logger.print_status(f"Create symlink for {client_config.get('cfg_filename')} ...") + source = Path(client_config.get("dir"), client_config.get("cfg_filename")) + for instance in klipper_instances: + target = instance.cfg_dir + Logger.print_status(f"Linking {source} to {target}") + try: + create_symlink(source, target) + except subprocess.CalledProcessError: + Logger.print_error("Creating symlink failed!") diff --git a/kiauh/components/fluidd/fluidd_dialogs.py b/kiauh/components/webui_client/client_dialogs.py similarity index 75% rename from kiauh/components/fluidd/fluidd_dialogs.py rename to kiauh/components/webui_client/client_dialogs.py index deec016..9fb98e9 100644 --- a/kiauh/components/fluidd/fluidd_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -12,6 +12,7 @@ import textwrap from typing import List +from components.webui_client import ClientData from core.menus.base_menu import print_back_footer from utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN @@ -25,10 +26,10 @@ def print_moonraker_not_found_dialog(): | {line1:<63}| | {line2:<63}| |-------------------------------------------------------| - | It is possible to install Fluidd without a local | + | It is possible to install Mainsail without a local | | Moonraker installation. If you continue, you need to | | make sure, that Moonraker is installed on another | - | machine in your network. Otherwise Fluidd will NOT | + | machine in your network. Otherwise Mainsail will NOT | | work correctly. | """ )[1:] @@ -37,19 +38,18 @@ def print_moonraker_not_found_dialog(): print_back_footer() -def print_fluidd_already_installed_dialog(): +def print_client_already_installed_dialog(name: str): line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" - line2 = f"{COLOR_YELLOW}Fluidd seems to be already installed!{RESET_FORMAT}" + line2 = f"{COLOR_YELLOW}{name} seems to be already installed!{RESET_FORMAT}" + line3 = f"If you continue, your current {name}" dialog = textwrap.dedent( f""" /=======================================================\\ | {line1:<63}| | {line2:<63}| |-------------------------------------------------------| - | If you continue, your current Fluidd installation | - | will be overwritten. You will not loose any printer | - | configurations and the Moonraker database will remain | - | untouched. | + | {line3:<54}| + | installation will be overwritten. | """ )[1:] @@ -57,38 +57,21 @@ def print_fluidd_already_installed_dialog(): print_back_footer() -def print_install_fluidd_config_dialog(): - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | It is recommended to use special macros in order to | - | have Fluidd fully functional and working. | - | | - | The recommended macros for Fluidd can be seen here: | - | https://github.com/fluidd-core/fluidd-config | - | | - | If you already use these macros skip this step. | - | Otherwise you should consider to answer with 'Y' to | - | download the recommended macros. | - \\=======================================================/ - """ - )[1:] - - print(dialog, end="") - - -def print_fluidd_port_select_dialog(port: str, ports_in_use: List[str]): +def print_client_port_select_dialog(name: str, port: str, ports_in_use: List[str]): port = f"{COLOR_CYAN}{port}{RESET_FORMAT}" + line1 = f"Please select the port, {name} should be served on." + line2 = f"In case you need {name} to be served on a specific" dialog = textwrap.dedent( f""" /=======================================================\\ - | Please select the port, Fluidd should be served on. | + | {line1:<54}| | If you are unsure what to select, hit Enter to apply | | the suggested value of: {port:38} | | | - | In case you need Fluidd to be served on a specific | + | {line2:<54}| | port, you can set it now. Make sure the port is not | | used by any other application on your system! | + \\=======================================================/ """ )[1:] @@ -102,3 +85,27 @@ def print_fluidd_port_select_dialog(port: str, ports_in_use: List[str]): dialog += "\\=======================================================/\n" print(dialog, end="") + + +def print_install_client_config_dialog(client: ClientData): + name = client.get("display_name") + url = client.get("client_config").get("url").replace(".git", "") + line1 = f"have {name} fully functional and working." + line2 = f"The recommended macros for {name} can be seen here:" + dialog = textwrap.dedent( + f""" + /=======================================================\\ + | It is recommended to use special macros in order to | + | {line1:<54}| + | | + | {line2:<54}| + | {url:<54}| + | | + | If you already use these macros skip this step. | + | Otherwise you should consider to answer with 'Y' to | + | download the recommended macros. | + \\=======================================================/ + """ + )[1:] + + print(dialog, end="") diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py new file mode 100644 index 0000000..49bc694 --- /dev/null +++ b/kiauh/components/webui_client/client_remove.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + + +import shutil +from typing import List + +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from components.webui_client import ClientData +from components.webui_client.client_config.client_config_remove import ( + run_client_config_removal, +) +from components.webui_client.client_utils import backup_mainsail_config_json + +from core.instance_manager.instance_manager import InstanceManager +from utils.filesystem_utils import ( + remove_nginx_config, + remove_nginx_logs, + remove_config_section, +) +from utils.logger import Logger + + +def run_client_removal( + client: ClientData, + rm_client: bool, + rm_client_config: bool, + backup_ms_config_json: bool, + rm_moonraker_conf_section: bool, + rm_printer_cfg_section: bool, +) -> None: + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + kl_im = InstanceManager(Klipper) + kl_instances: List[Klipper] = kl_im.instances + if backup_ms_config_json and client.get("name") == "mainsail": + backup_mainsail_config_json() + if rm_client: + client_name = client.get("name") + remove_client_dir(client) + remove_nginx_config(client_name) + remove_nginx_logs(client_name) + if rm_moonraker_conf_section: + section = f"update_manager {client_name}" + remove_config_section(section, mr_instances) + if rm_client_config: + run_client_config_removal( + client.get("client_config"), + rm_moonraker_conf_section, + rm_printer_cfg_section, + kl_instances, + mr_instances, + ) + + +def remove_client_dir(client: ClientData) -> None: + Logger.print_status(f"Removing {client.get('display_name')} ...") + client_dir = client.get("dir") + if not client.get("dir").exists(): + Logger.print_info(f"'{client_dir}' does not exist. Skipping ...") + return + + try: + shutil.rmtree(client_dir) + except OSError as e: + Logger.print_error(f"Unable to delete '{client_dir}':\n{e}") diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py new file mode 100644 index 0000000..a783507 --- /dev/null +++ b/kiauh/components/webui_client/client_setup.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path +from typing import List + +from components.klipper.klipper import Klipper +from components.webui_client import ( + ClientName, + ClientData, +) + +from components.moonraker.moonraker import Moonraker +from components.webui_client.client_config.client_config_setup import ( + install_client_config, + check_existing_client_config_install, +) +from components.webui_client.client_dialogs import ( + print_moonraker_not_found_dialog, + print_client_port_select_dialog, + print_install_client_config_dialog, +) +from components.webui_client.client_utils import ( + backup_mainsail_config_json, + restore_mainsail_config_json, + enable_mainsail_remotemode, + symlink_webui_nginx_log, + load_client_data, +) +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.instance_manager import InstanceManager +from kiauh import KIAUH_CFG +from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from utils.common import check_install_dependencies +from utils.filesystem_utils import ( + unzip, + copy_upstream_nginx_cfg, + copy_common_vars_nginx_cfg, + create_nginx_cfg, + create_symlink, + remove_file, + add_config_section, + read_ports_from_nginx_configs, + is_valid_port, + get_next_free_port, +) +from utils.input_utils import get_confirm, get_number_input +from utils.logger import Logger +from utils.system_utils import ( + download_file, + set_nginx_permissions, + get_ipv4_addr, + control_systemd_service, +) + + +def install_client(client_name: ClientName) -> None: + client: ClientData = load_client_data(client_name) + d_name = client.get("display_name") + + if client is None: + Logger.print_error("Missing parameter client_name!") + return + + if client.get("dir").exists(): + Logger.print_info( + f"{client.get('display_name')} seems to be already installed! Skipped ..." + ) + return + + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + + enable_remotemode = False + if not mr_instances: + print_moonraker_not_found_dialog() + if not get_confirm( + f"Continue {d_name} installation?", + allow_go_back=True, + ): + return + + # if moonraker is not installed or multiple instances + # are installed we enable mainsails remote mode + if client.get("remote_mode") and not mr_instances or len(mr_instances) > 1: + enable_remotemode = True + + kl_im = InstanceManager(Klipper) + kl_instances = kl_im.instances + install_client_cfg = False + client_config = client.get("client_config") + if ( + kl_instances + and not client_config.get("dir").exists() + and not check_existing_client_config_install(client.get("name")) + ): + print_install_client_config_dialog(client) + question = f"Download the recommended {client_config.get('display_name')}?" + install_client_cfg = get_confirm(question, allow_go_back=False) + + cm = ConfigManager(cfg_file=KIAUH_CFG) + client_port = cm.get_value(client.get("name"), "port") + ports_in_use = read_ports_from_nginx_configs() + + # check if configured port is a valid number and not in use already + valid_port = is_valid_port(client_port, ports_in_use) + while not valid_port: + next_port = get_next_free_port(ports_in_use) + print_client_port_select_dialog(d_name, next_port, ports_in_use) + client_port = str( + get_number_input( + f"Configure {d_name} for port", + min_count=int(next_port), + default=next_port, + ) + ) + valid_port = is_valid_port(client_port, ports_in_use) + + check_install_dependencies(["nginx"]) + + try: + download_client(client) + if enable_remotemode and client.get("name") == "mainsail": + enable_mainsail_remotemode() + if mr_instances: + add_config_section( + section=f"update_manager {client.get('name')}", + instances=mr_instances, + options=[ + ("type", "web"), + ("channel", "stable"), + ("repo", client.get("mr_conf_repo")), + ("path", client.get("mr_conf_path")), + ], + ) + mr_im.restart_all_instance() + if install_client_cfg and kl_instances: + install_client_config(client.get("name")) + + copy_upstream_nginx_cfg() + copy_common_vars_nginx_cfg() + create_client_nginx_cfg(client, client_port) + if kl_instances: + symlink_webui_nginx_log(kl_instances) + control_systemd_service("nginx", "restart") + + except Exception as e: + Logger.print_error(f"{d_name} installation failed!\n{e}") + return + + log = f"Open {d_name} now on: http://{get_ipv4_addr()}:{client_port}" + Logger.print_ok(f"{d_name} installation complete!", start="\n") + Logger.print_ok(log, prefix=False, end="\n\n") + + +def download_client(client: ClientData) -> None: + zipfile = f"{client.get('name').lower()}.zip" + target = Path().home().joinpath(zipfile) + try: + Logger.print_status(f"Downloading {zipfile} ...") + download_file(client.get("url"), target, True) + Logger.print_ok("Download complete!") + + Logger.print_status(f"Extracting {zipfile} ...") + unzip(target, client.get("dir")) + target.unlink(missing_ok=True) + Logger.print_ok("OK!") + + except Exception: + Logger.print_error(f"Downloading {zipfile} failed!") + raise + + +def update_client(client: ClientData) -> None: + Logger.print_status(f"Updating {client.get('display_name')} ...") + if client.get("name") == "mainsail": + backup_mainsail_config_json(is_temp=True) + + download_client(client) + + if client.get("name") == "mainsail": + restore_mainsail_config_json() + + +def create_client_nginx_cfg(client: ClientData, port: int) -> None: + d_name = client.get("display_name") + root_dir = client.get("dir") + source = NGINX_SITES_AVAILABLE.joinpath(client.get("name")) + target = NGINX_SITES_ENABLED.joinpath(client.get("name")) + try: + Logger.print_status(f"Creating NGINX config for {d_name} ...") + remove_file(Path("/etc/nginx/sites-enabled/default"), True) + create_nginx_cfg(client.get("name"), port, root_dir) + create_symlink(source, target, True) + set_nginx_permissions() + Logger.print_ok(f"NGINX config for {d_name} successfully created.") + except Exception: + Logger.print_error(f"Creating NGINX config for {d_name} failed!") + raise diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py new file mode 100644 index 0000000..40207e5 --- /dev/null +++ b/kiauh/components/webui_client/client_utils.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import json +import shutil +from json import JSONDecodeError +from pathlib import Path +from typing import List, Optional, Dict, Literal, Union + +import urllib.request + +from components.klipper.klipper import Klipper +from components.webui_client import ( + MAINSAIL_CONFIG_JSON, + MAINSAIL_DIR, + MAINSAIL_BACKUP_DIR, + FLUIDD_PRE_RLS_URL, + FLUIDD_BACKUP_DIR, + FLUIDD_URL, + FLUIDD_DIR, + ClientData, + FLUIDD_CONFIG_REPO_URL, + FLUIDD_CONFIG_DIR, + ClientConfigData, + MAINSAIL_PRE_RLS_URL, + MAINSAIL_URL, + MAINSAIL_CONFIG_REPO_URL, + MAINSAIL_CONFIG_DIR, + ClientName, + MAINSAIL_TAGS_URL, + FLUIDD_TAGS_URL, + FLUIDD_CONFIG_BACKUP_DIR, + MAINSAIL_CONFIG_BACKUP_DIR, +) +from core.backup_manager.backup_manager import BackupManager +from core.repo_manager.repo_manager import RepoManager +from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD +from utils.common import get_install_status_webui, get_install_status_common +from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW +from utils.logger import Logger + + +def load_client_data(client_name: ClientName) -> Optional[ClientData]: + client_data = None + + if client_name == "mainsail": + client_config_data = ClientConfigData( + name="mainsail-config", + display_name="Mainsail-Config", + cfg_filename="mainsail.cfg", + dir=MAINSAIL_CONFIG_DIR, + backup_dir=MAINSAIL_CONFIG_BACKUP_DIR, + url=MAINSAIL_CONFIG_REPO_URL, + printer_cfg_section="include mainsail.cfg", + mr_conf_path="~/mainsail-config", + mr_conf_origin=MAINSAIL_CONFIG_REPO_URL, + ) + client_data = ClientData( + name=client_name, + display_name=client_name.capitalize(), + dir=MAINSAIL_DIR, + backup_dir=MAINSAIL_BACKUP_DIR, + url=MAINSAIL_URL, + pre_release_url=MAINSAIL_PRE_RLS_URL, + tags_url=MAINSAIL_TAGS_URL, + remote_mode=True, + mr_conf_repo="mainsail-crew/mainsail", + mr_conf_path="~/mainsail", + client_config=client_config_data, + ) + elif client_name == "fluidd": + client_config_data = ClientConfigData( + name="fluidd-config", + display_name="Fluidd-Config", + cfg_filename="fluidd.cfg", + dir=FLUIDD_CONFIG_DIR, + backup_dir=FLUIDD_CONFIG_BACKUP_DIR, + url=FLUIDD_CONFIG_REPO_URL, + printer_cfg_section="include fluidd.cfg", + mr_conf_path="~/fluidd-config", + mr_conf_origin=FLUIDD_CONFIG_REPO_URL, + ) + client_data = ClientData( + name=client_name, + display_name=client_name.capitalize(), + dir=FLUIDD_DIR, + backup_dir=FLUIDD_BACKUP_DIR, + url=FLUIDD_URL, + pre_release_url=FLUIDD_PRE_RLS_URL, + tags_url=FLUIDD_TAGS_URL, + remote_mode=False, + mr_conf_repo="fluidd-core/fluidd", + mr_conf_path="~/fluidd", + client_config=client_config_data, + ) + + return client_data + + +def get_client_status(client: ClientData) -> str: + return get_install_status_webui( + client.get("dir"), + NGINX_SITES_AVAILABLE.joinpath(client.get("name")), + NGINX_CONFD.joinpath("upstreams.conf"), + NGINX_CONFD.joinpath("common_vars.conf"), + ) + + +def get_client_config_status(client: ClientData) -> Dict[ + Literal["repo", "local", "remote"], + Union[str, int], +]: + client_config = client.get("client_config") + client_config = client_config.get("dir") + + return { + "repo": RepoManager.get_repo_name(client_config), + "local": RepoManager.get_local_commit(client_config), + "remote": RepoManager.get_remote_commit(client_config), + } + + +def get_current_client_config(clients: List[ClientData]) -> str: + installed = [] + for client in clients: + client_config = client.get("client_config") + if client_config.get("dir").exists(): + installed.append(client) + + if len(installed) > 1: + return f"{COLOR_YELLOW}Conflict!{RESET_FORMAT}" + elif len(installed) == 1: + cfg = installed[0].get("client_config") + return f"{COLOR_CYAN}{cfg.get('display_name')}{RESET_FORMAT}" + + return f"{COLOR_CYAN}-{RESET_FORMAT}" + + +def backup_mainsail_config_json(is_temp=False) -> None: + Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") + bm = BackupManager() + if is_temp: + fn = Path.home().joinpath("config.json.kiauh.bak") + bm.backup_file(MAINSAIL_CONFIG_JSON, custom_filename=fn) + else: + bm.backup_file(MAINSAIL_CONFIG_JSON) + + +def restore_mainsail_config_json() -> None: + try: + Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") + source = Path.home().joinpath("config.json.kiauh.bak") + shutil.copy(source, MAINSAIL_CONFIG_JSON) + except OSError: + Logger.print_info("Unable to restore config.json. Skipped ...") + + +def enable_mainsail_remotemode() -> None: + Logger.print_status("Enable Mainsails remote mode ...") + with open(MAINSAIL_CONFIG_JSON, "r") as f: + config_data = json.load(f) + + if config_data["instancesDB"] == "browser": + Logger.print_info("Remote mode already configured. Skipped ...") + return + + Logger.print_status("Setting instance storage location to 'browser' ...") + config_data["instancesDB"] = "browser" + + with open(MAINSAIL_CONFIG_JSON, "w") as f: + json.dump(config_data, f, indent=4) + Logger.print_ok("Mainsails remote mode enabled!") + + +def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: + Logger.print_status("Link NGINX logs into log directory ...") + access_log = Path("/var/log/nginx/mainsail-access.log") + error_log = Path("/var/log/nginx/mainsail-error.log") + + for instance in klipper_instances: + desti_access = instance.log_dir.joinpath("mainsail-access.log") + if not desti_access.exists(): + desti_access.symlink_to(access_log) + + desti_error = instance.log_dir.joinpath("mainsail-error.log") + if not desti_error.exists(): + desti_error.symlink_to(error_log) + + +def get_local_client_version(client: ClientData) -> str: + relinfo_file = client.get("dir").joinpath("release_info.json") + if not relinfo_file.is_file(): + return "-" + + with open(relinfo_file, "r") as f: + return json.load(f)["version"] + + +def get_remote_client_version(client: ClientData) -> str: + try: + with urllib.request.urlopen(client.get("tags_url")) as response: + data = json.loads(response.read()) + return data[0]["name"] + except (JSONDecodeError, TypeError): + return "ERROR" + + +def backup_client_data(client: ClientData) -> None: + name = client.get("name") + src = client.get("dir") + dest = client.get("backup_dir") + + with open(src.joinpath(".version"), "r") as v: + version = v.readlines()[0] + + bm = BackupManager() + bm.backup_directory(f"{name}-{version}", src, dest) + if name == "mainsail": + bm.backup_file(MAINSAIL_CONFIG_JSON, dest) + bm.backup_file(NGINX_SITES_AVAILABLE.joinpath(name), dest) + + +def backup_client_config_data(client: ClientData) -> None: + client_config = client.get("client_config") + name = client_config.get("name") + source = client_config.get("dir") + target = client_config.get("backup_dir") + bm = BackupManager() + bm.backup_directory(name, source, target) diff --git a/kiauh/components/mainsail/menus/__init__.py b/kiauh/components/webui_client/menus/__init__.py similarity index 100% rename from kiauh/components/mainsail/menus/__init__.py rename to kiauh/components/webui_client/menus/__init__.py diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py new file mode 100644 index 0000000..b4648d6 --- /dev/null +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import textwrap +from typing import Callable, Dict + +from components.webui_client import client_remove, ClientData +from core.menus import BACK_HELP_FOOTER +from core.menus.base_menu import BaseMenu +from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN + + +# noinspection PyUnusedLocal +class ClientRemoveMenu(BaseMenu): + def __init__(self, client: ClientData): + self.client = client + self.rm_client = False + self.rm_client_config = False + self.backup_mainsail_config_json = False + self.rm_moonraker_conf_section = False + self.rm_printer_cfg_section = False + + super().__init__( + header=False, + options=self.get_options(), + footer_type=BACK_HELP_FOOTER, + ) + + def get_options(self) -> Dict[str, Callable]: + options = { + "0": self.toggle_all, + "1": self.toggle_rm_client, + "2": self.toggle_rm_client_config, + "3": self.toggle_rm_printer_cfg_section, + "4": self.toggle_rm_moonraker_conf_section, + "c": self.run_removal_process, + } + if self.client.get("name") == "mainsail": + options["5"] = self.toggle_backup_mainsail_config_json + + return options + + def print_menu(self) -> None: + client_name = self.client.get("display_name") + client_config = self.client.get("client_config") + client_config_name = client_config.get("display_name") + + header = f" [ Remove {client_name} ] " + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" + unchecked = "[ ]" + o1 = checked if self.rm_client else unchecked + o2 = checked if self.rm_client_config else unchecked + o3 = checked if self.rm_printer_cfg_section else unchecked + o4 = checked if self.rm_moonraker_conf_section else unchecked + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Enter a number and hit enter to select / deselect | + | the specific option for removal. | + |-------------------------------------------------------| + | 0) Select everything | + |-------------------------------------------------------| + | 1) {o1} Remove {client_name:16} | + | 2) {o2} Remove {client_config_name:24} | + | | + | printer.cfg & moonraker.conf | + | 3) {o3} Remove printer.cfg include | + | 4) {o4} Remove Moonraker update section | + """ + )[1:] + + if self.client.get("name") == "mainsail": + o5 = checked if self.backup_mainsail_config_json else unchecked + menu += textwrap.dedent( + f""" + | | + | Mainsail config.json | + | 5) {o5} Backup config.json | + """ + )[1:] + + menu += textwrap.dedent( + """ + |-------------------------------------------------------| + | C) Continue | + """ + )[1:] + print(menu, end="") + + def toggle_all(self, **kwargs) -> None: + self.rm_client = True + self.rm_client_config = True + self.backup_mainsail_config_json = True + self.rm_moonraker_conf_section = True + self.rm_printer_cfg_section = True + + def toggle_rm_client(self, **kwargs) -> None: + self.rm_client = not self.rm_client + + def toggle_rm_client_config(self, **kwargs) -> None: + self.rm_client_config = not self.rm_client_config + + def toggle_backup_mainsail_config_json(self, **kwargs) -> None: + self.backup_mainsail_config_json = not self.backup_mainsail_config_json + + def toggle_rm_moonraker_conf_section(self, **kwargs) -> None: + self.rm_moonraker_conf_section = not self.rm_moonraker_conf_section + + def toggle_rm_printer_cfg_section(self, **kwargs) -> None: + self.rm_printer_cfg_section = not self.rm_printer_cfg_section + + def run_removal_process(self, **kwargs) -> None: + if ( + not self.rm_client + and not self.rm_client_config + and not self.backup_mainsail_config_json + and not self.rm_moonraker_conf_section + and not self.rm_printer_cfg_section + ): + error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}" + print(error) + return + + client_remove.run_client_removal( + client=self.client, + rm_client=self.rm_client, + rm_client_config=self.rm_client_config, + backup_ms_config_json=self.backup_mainsail_config_json, + rm_moonraker_conf_section=self.rm_moonraker_conf_section, + rm_printer_cfg_section=self.rm_printer_cfg_section, + ) + + self.rm_client = False + self.rm_client_config = False + self.backup_mainsail_config_json = False + self.rm_moonraker_conf_section = False + self.rm_printer_cfg_section = False diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 8b92978..459461a 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -12,11 +12,15 @@ import textwrap from components.klipper.klipper_utils import backup_klipper_dir -from components.mainsail.mainsail_utils import backup_mainsail_data from components.moonraker.moonraker_utils import ( backup_moonraker_dir, backup_moonraker_db_dir, ) +from components.webui_client.client_utils import ( + backup_client_data, + load_client_data, + backup_client_config_data, +) from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.common import backup_printer_config_dir @@ -35,6 +39,10 @@ class BackupMenu(BaseMenu): "3": self.backup_printer_config, "4": self.backup_moonraker_db, "5": self.backup_mainsail, + "6": self.backup_fluidd, + "7": self.backup_mainsail_config, + "8": self.backup_fluidd_config, + "9": self.backup_klipperscreen, }, footer_type=BACK_FOOTER, ) @@ -51,15 +59,15 @@ class BackupMenu(BaseMenu): |-------------------------------------------------------| | {line1:^62} | |-------------------------------------------------------| - | Klipper & Moonraker API: | Touchscreen GUI: | - | 1) [Klipper] | 7) [KlipperScreen] | - | 2) [Moonraker] | | - | 3) [Config Folder] | Other: | - | 4) [Moonraker Database] | 9) [Telegram Bot] | - | | | - | Klipper Webinterface: | | - | 5) [Mainsail] | | - | 6) [Fluidd] | | + | Klipper & Moonraker API: | Client-Config: | + | 1) [Klipper] | 7) [Mainsail-Config] | + | 2) [Moonraker] | 8) [Fluidd-Config] | + | 3) [Config Folder] | | + | 4) [Moonraker Database] | Touchscreen GUI: | + | | 9) [KlipperScreen] | + | Webinterface: | | + | 5) [Mainsail] | | + | 6) [Fluidd] | | """ )[1:] print(menu, end="") @@ -77,13 +85,16 @@ class BackupMenu(BaseMenu): backup_moonraker_db_dir() def backup_mainsail(self, **kwargs): - backup_mainsail_data() + backup_client_data(load_client_data("mainsail")) def backup_fluidd(self, **kwargs): - pass + backup_client_data(load_client_data("fluidd")) + + def backup_mainsail_config(self, **kwargs): + backup_client_config_data(load_client_data("mainsail")) + + def backup_fluidd_config(self, **kwargs): + backup_client_config_data(load_client_data("fluidd")) def backup_klipperscreen(self, **kwargs): pass - - def backup_telegram_bot(self, **kwargs): - pass diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index c466fdf..a90e235 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -11,10 +11,10 @@ import textwrap -from components.fluidd import fluidd_setup from components.klipper import klipper_setup -from components.mainsail import mainsail_setup from components.moonraker import moonraker_setup +from components.webui_client import client_setup +from components.webui_client.client_config import client_config_setup from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import COLOR_GREEN, RESET_FORMAT @@ -31,13 +31,11 @@ class InstallMenu(BaseMenu): "2": self.install_moonraker, "3": self.install_mainsail, "4": self.install_fluidd, - "5": self.install_klipperscreen, - "6": self.install_pretty_gcode, - "7": self.install_telegram_bot, - "8": self.install_obico, - "9": self.install_octoeverywhere, - "10": self.install_mobileraker, - "11": self.install_crowsnest, + "5": self.install_mainsail_config, + "6": self.install_fluidd_config, + "7": None, + "8": None, + "9": None, }, footer_type=BACK_FOOTER, ) @@ -51,16 +49,18 @@ class InstallMenu(BaseMenu): /=======================================================\\ | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| - | Firmware & API: | Other: | - | 1) [Klipper] | 6) [PrettyGCode] | - | 2) [Moonraker] | 7) [Telegram Bot] | - | | 8) $(obico_install_title) | - | Klipper Webinterface: | 9) [OctoEverywhere] | - | 3) [Mainsail] | 10) [Mobileraker] | - | 4) [Fluidd] | | - | | Webcam Streamer: | - | Touchscreen GUI: | 11) [Crowsnest] | - | 5) [KlipperScreen] | | + | Firmware & API: | Touchscreen GUI: | + | 1) [Klipper] | 7) [KlipperScreen] | + | 2) [Moonraker] | | + | | Android / iOS: | + | Webinterface: | 8) [Mobileraker] | + | 3) [Mainsail] | | + | 4) [Fluidd] | Webcam Streamer: | + | | 9) [Crowsnest] | + | Client-Config: | | + | 5) [Mainsail-Config] | | + | 6) [Fluidd-Config] | | + | | | """ )[1:] print(menu, end="") @@ -72,28 +72,13 @@ class InstallMenu(BaseMenu): moonraker_setup.install_moonraker() def install_mainsail(self, **kwargs): - mainsail_setup.install_mainsail() + client_setup.install_client(client_name="mainsail") + + def install_mainsail_config(self, **kwargs): + client_config_setup.install_client_config(client_name="mainsail") def install_fluidd(self, **kwargs): - fluidd_setup.install_fluidd() + client_setup.install_client(client_name="fluidd") - def install_klipperscreen(self, **kwargs): - print("install_klipperscreen") - - def install_pretty_gcode(self, **kwargs): - print("install_pretty_gcode") - - def install_telegram_bot(self, **kwargs): - print("install_telegram_bot") - - def install_obico(self, **kwargs): - print("install_obico") - - def install_octoeverywhere(self, **kwargs): - print("install_octoeverywhere") - - def install_mobileraker(self, **kwargs): - print("install_mobileraker") - - def install_crowsnest(self, **kwargs): - print("install_crowsnest") + def install_fluidd_config(self, **kwargs): + client_config_setup.install_client_config(client_name="fluidd") diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 716e046..4ab3b31 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -11,11 +11,14 @@ import textwrap -from components.fluidd.fluidd_utils import get_fluidd_status from components.klipper.klipper_utils import get_klipper_status from components.log_uploads.menus.log_upload_menu import LogUploadMenu -from components.mainsail.mainsail_utils import get_mainsail_status from components.moonraker.moonraker_utils import get_moonraker_status +from components.webui_client.client_utils import ( + get_client_status, + load_client_data, + get_current_client_config, +) from core.menus import QUIT_FOOTER from core.menus.advanced_menu import AdvancedMenu from core.menus.backup_menu import BackupMenu @@ -61,13 +64,11 @@ class MainMenu(BaseMenu): self.ks_status = "" self.mb_status = "" self.cn_status = "" - self.tg_status = "" - self.ob_status = "" - self.oe_status = "" + self.cc_status = "" self.init_status() def init_status(self) -> None: - status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn", "tg", "ob", "oe"] + status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn"] for var in status_vars: setattr(self, f"{var}_status", f"{COLOR_RED}Not installed!{RESET_FORMAT}") @@ -87,9 +88,15 @@ class MainMenu(BaseMenu): self.mr_status = self.format_status_by_code(mr_code, mr_status, mr_instances) self.mr_repo = f"{COLOR_CYAN}{moonraker_status.get('repo')}{RESET_FORMAT}" # mainsail - self.ms_status = get_mainsail_status() + mainsail_client_data = load_client_data("mainsail") + self.ms_status = get_client_status(mainsail_client_data) # fluidd - self.fl_status = get_fluidd_status() + fluidd_client_data = load_client_data("fluidd") + self.fl_status = get_client_status(fluidd_client_data) + # client-config + self.cc_status = get_current_client_config( + [mainsail_client_data, fluidd_client_data] + ) def format_status_by_code(self, code: int, status: str, count: str) -> str: if code == 1: @@ -120,13 +127,11 @@ class MainMenu(BaseMenu): | 4) [Advanced] |------------------------------------| | 5) [Backup] | Mainsail: {self.ms_status:<26} | | | Fluidd: {self.fl_status:<26} | - | E) [Extensions] | KlipperScreen: {self.ks_status:<26} | - | | Mobileraker: {self.mb_status:<26} | + | S) [Settings] | Client-Config: {self.cc_status:<26} | | | | + | Community: | KlipperScreen: {self.ks_status:<26} | + | E) [Extensions] | Mobileraker: {self.mb_status:<26} | | | Crowsnest: {self.cn_status:<26} | - | | Telegram Bot: {self.tg_status:<26} | - | | Obico: {self.ob_status:<26} | - | S) [Settings] | OctoEverywhere: {self.oe_status:<26} | |-------------------------------------------------------| | {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} | """ diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 8df2380..ad53c94 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -11,10 +11,10 @@ import textwrap -from components.fluidd.menus.fluidd_remove_menu import FluiddRemoveMenu from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu -from components.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu from components.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu +from components.webui_client.client_utils import load_client_data +from components.webui_client.menus.client_remove_menu import ClientRemoveMenu from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import COLOR_RED, RESET_FORMAT @@ -29,8 +29,8 @@ class RemoveMenu(BaseMenu): options={ "1": KlipperRemoveMenu, "2": MoonrakerRemoveMenu, - "3": MainsailRemoveMenu, - "4": FluiddRemoveMenu, + "3": ClientRemoveMenu(client=load_client_data("mainsail")), + "4": ClientRemoveMenu(client=load_client_data("fluidd")), "5": None, "6": None, "7": None, diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 04d6de5..ffa74ba 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -11,22 +11,22 @@ import textwrap -from components.fluidd.fluidd_setup import update_fluidd -from components.fluidd.fluidd_utils import ( - get_fluidd_local_version, - get_fluidd_remote_version, -) from components.klipper.klipper_setup import update_klipper from components.klipper.klipper_utils import ( get_klipper_status, ) -from components.mainsail.mainsail_setup import update_mainsail -from components.mainsail.mainsail_utils import ( - get_mainsail_local_version, - get_mainsail_remote_version, -) from components.moonraker.moonraker_setup import update_moonraker from components.moonraker.moonraker_utils import get_moonraker_status +from components.webui_client.client_config.client_config_setup import ( + update_client_config, +) +from components.webui_client.client_setup import update_client +from components.webui_client.client_utils import ( + get_local_client_version, + get_remote_client_version, + load_client_data, + get_client_config_status, +) from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import ( @@ -50,14 +50,12 @@ class UpdateMenu(BaseMenu): "2": self.update_moonraker, "3": self.update_mainsail, "4": self.update_fluidd, - "5": self.update_klipperscreen, - "6": self.update_pgc_for_klipper, - "7": self.update_telegram_bot, - "8": self.update_moonraker_obico, - "9": self.update_octoeverywhere, - "10": self.update_mobileraker, - "11": self.update_crowsnest, - "12": self.upgrade_system_packages, + "5": self.update_mainsail_config, + "6": self.update_fluidd_config, + "7": self.update_klipperscreen, + "8": self.update_mobileraker, + "9": self.update_crowsnest, + "10": self.upgrade_system_packages, }, footer_type=BACK_FOOTER, ) @@ -69,6 +67,10 @@ class UpdateMenu(BaseMenu): self.ms_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.fl_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.fl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.mc_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" def print_menu(self): self.fetch_update_status() @@ -87,22 +89,20 @@ class UpdateMenu(BaseMenu): | 1) Klipper | {self.kl_local:<22} | {self.kl_remote:<22} | | 2) Moonraker | {self.mr_local:<22} | {self.mr_remote:<22} | | | | | - | Klipper Webinterface: |---------------|---------------| + | Webinterface: |---------------|---------------| | 3) Mainsail | {self.ms_local:<22} | {self.ms_remote:<22} | | 4) Fluidd | {self.fl_local:<22} | {self.fl_remote:<22} | | | | | - | Touchscreen GUI: |---------------|---------------| - | 5) KlipperScreen | | | + | Client-Config: |---------------|---------------| + | 5) Mainsail-Config | {self.mc_local:<22} | {self.mc_remote:<22} | + | 6) Fluidd-Config | {self.fc_local:<22} | {self.fc_remote:<22} | | | | | | Other: |---------------|---------------| - | 6) PrettyGCode | | | - | 7) Telegram Bot | | | - | 8) Obico for Klipper | | | - | 9) OctoEverywhere | | | - | 10) Mobileraker | | | - | 11) Crowsnest | | | + | 7) KlipperScreen | | | + | 8) Mobileraker | | | + | 9) Crowsnest | | | | |-------------------------------| - | 12) System | | + | 10) System | | """ )[1:] print(menu, end="") @@ -117,34 +117,24 @@ class UpdateMenu(BaseMenu): update_moonraker() def update_mainsail(self, **kwargs): - update_mainsail() + update_client(load_client_data("mainsail")) + + def update_mainsail_config(self, **kwargs): + update_client_config(load_client_data("mainsail")) def update_fluidd(self, **kwargs): - update_fluidd() + update_client(load_client_data("fluidd")) - def update_klipperscreen(self, **kwargs): - print("update_klipperscreen") + def update_fluidd_config(self, **kwargs): + update_client_config(load_client_data("fluidd")) - def update_pgc_for_klipper(self, **kwargs): - print("update_pgc_for_klipper") + def update_klipperscreen(self, **kwargs): ... - def update_telegram_bot(self, **kwargs): - print("update_telegram_bot") + def update_mobileraker(self, **kwargs): ... - def update_moonraker_obico(self, **kwargs): - print("update_moonraker_obico") + def update_crowsnest(self, **kwargs): ... - def update_octoeverywhere(self, **kwargs): - print("update_octoeverywhere") - - def update_mobileraker(self, **kwargs): - print("update_mobileraker") - - def update_crowsnest(self, **kwargs): - print("update_crowsnest") - - def upgrade_system_packages(self, **kwargs): - print("upgrade_system_packages") + def upgrade_system_packages(self, **kwargs): ... def fetch_update_status(self): # klipper @@ -166,18 +156,38 @@ class UpdateMenu(BaseMenu): self.mr_local = f"{COLOR_YELLOW}{self.mr_local}{RESET_FORMAT}" self.mr_remote = f"{COLOR_GREEN}{self.mr_remote}{RESET_FORMAT}" # mainsail - self.ms_local = get_mainsail_local_version() - self.ms_remote = get_mainsail_remote_version() + mainsail_client_data = load_client_data("mainsail") + self.ms_local = get_local_client_version(mainsail_client_data) + self.ms_remote = get_remote_client_version(mainsail_client_data) if self.ms_local == self.ms_remote: self.ms_local = f"{COLOR_GREEN}{self.ms_local}{RESET_FORMAT}" else: self.ms_local = f"{COLOR_YELLOW}{self.ms_local}{RESET_FORMAT}" self.ms_remote = f"{COLOR_GREEN if self.ms_remote != 'ERROR' else COLOR_RED}{self.ms_remote}{RESET_FORMAT}" # fluidd - self.fl_local = get_fluidd_local_version() - self.fl_remote = get_fluidd_remote_version() + fluidd_client_data = load_client_data("fluidd") + self.fl_local = get_local_client_version(fluidd_client_data) + self.fl_remote = get_remote_client_version(fluidd_client_data) if self.fl_local == self.fl_remote: self.fl_local = f"{COLOR_GREEN}{self.fl_local}{RESET_FORMAT}" else: self.fl_local = f"{COLOR_YELLOW}{self.fl_local}{RESET_FORMAT}" self.fl_remote = f"{COLOR_GREEN if self.fl_remote != 'ERROR' else COLOR_RED}{self.fl_remote}{RESET_FORMAT}" + # mainsail-config + mc_status = get_client_config_status(load_client_data("mainsail")) + self.mc_local = mc_status.get("local") + self.mc_remote = mc_status.get("remote") + if self.mc_local == self.mc_remote: + self.mc_local = f"{COLOR_GREEN}{self.mc_local}{RESET_FORMAT}" + else: + self.mc_local = f"{COLOR_YELLOW}{self.mc_local}{RESET_FORMAT}" + self.mc_remote = f"{COLOR_GREEN}{self.mc_remote}{RESET_FORMAT}" + # fluidd-config + fc_status = get_client_config_status(load_client_data("fluidd")) + self.fc_local = fc_status.get("local") + self.fc_remote = fc_status.get("remote") + if self.fc_local == self.mc_remote: + self.fc_local = f"{COLOR_GREEN}{self.fc_local}{RESET_FORMAT}" + else: + self.fc_local = f"{COLOR_YELLOW}{self.fc_local}{RESET_FORMAT}" + self.fc_remote = f"{COLOR_GREEN}{self.fc_remote}{RESET_FORMAT}" diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 14ad26f..b7fed2f 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 + # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # @@ -7,14 +8,20 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # + import re import shutil import subprocess from pathlib import Path from zipfile import ZipFile -from typing import List +from typing import List, Type, TypeVar, Union, Tuple +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from core.config_manager.config_manager import ConfigManager +from core.instance_manager.base_instance import BaseInstance +from core.instance_manager.instance_manager import InstanceManager from utils import ( NGINX_SITES_AVAILABLE, MODULE_PATH, @@ -24,6 +31,10 @@ from utils import ( from utils.logger import Logger +B = TypeVar('B', bound='BaseInstance') +ConfigOption = Tuple[str, str] + + def check_file_exist(file_path: Path, sudo=False) -> bool: """ Helper function for checking the existence of a file | @@ -169,3 +180,102 @@ def get_next_free_port(ports_in_use: List[str]) -> str: used_ports = set(map(int, ports_in_use)) return str(min(valid_ports - used_ports)) + + +def add_config_section(section: str, instances: List[B], options: List[ConfigOption] = None) -> None: + for instance in instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + continue + + cm = ConfigManager(cfg_file) + if cm.config.has_section(section): + Logger.print_info("Section already exist. Skipped ...") + continue + + cm.config.add_section(section) + + if options is not None: + for option in options: + cm.config.set(section, option[0], option[1]) + + cm.write_config() + + +def remove_config_section(section: str, instances: List[B]) -> None: + for instance in instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + continue + + cm = ConfigManager(cfg_file) + if not cm.config.has_section(section): + Logger.print_info("Section does not exist. Skipped ...") + continue + + cm.config.remove_section(section) + cm.write_config() + + +def patch_moonraker_conf( + moonraker_instances: List[Moonraker], + name: str, + section_name: str, + template_file: str, +) -> None: + for instance in moonraker_instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Add {name} update section to '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + return + + cm = ConfigManager(cfg_file) + if cm.config.has_section(section_name): + Logger.print_info("Section already exist. Skipped ...") + return + + template = MODULE_PATH.joinpath("assets", template_file) + with open(template, "r") as t: + template_content = "\n" + template_content += t.read() + + with open(cfg_file, "a") as f: + f.write(template_content) + + +def remove_nginx_config(name: str) -> None: + Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...") + try: + remove_file(NGINX_SITES_AVAILABLE.joinpath(name), True) + remove_file(NGINX_SITES_ENABLED.joinpath(name), True) + + except subprocess.CalledProcessError as e: + log = f"Unable to remove NGINX config '{name}':\n{e.stderr.decode()}" + Logger.print_error(log) + + +def remove_nginx_logs(name: str) -> None: + Logger.print_status(f"Removing NGINX logs for {name.capitalize()} ...") + try: + remove_file(Path(f"/var/log/nginx/{name}-access.log"), True) + remove_file(Path(f"/var/log/nginx/{name}-error.log"), True) + + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + if not instances: + return + + for instance in instances: + remove_file(instance.log_dir.joinpath(f"{name}-access.log")) + remove_file(instance.log_dir.joinpath(f"{name}-error.log")) + + except (OSError, subprocess.CalledProcessError) as e: + Logger.print_error(f"Unable to remove NGINX logs:\n{e}") From 5c37b684638bdfcf51e16396d1e8e6d202a2781e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 2 Mar 2024 21:25:17 +0100 Subject: [PATCH 114/296] fix(webclients): default to port 80 if none/invalid configured in kiauh.cfg Signed-off-by: Dominik Willner --- kiauh/components/webui_client/client_dialogs.py | 1 - kiauh/components/webui_client/client_setup.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py index 9fb98e9..1053a58 100644 --- a/kiauh/components/webui_client/client_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -71,7 +71,6 @@ def print_client_port_select_dialog(name: str, port: str, ports_in_use: List[str | {line2:<54}| | port, you can set it now. Make sure the port is not | | used by any other application on your system! | - \\=======================================================/ """ )[1:] diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index a783507..e2a4ae3 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -107,7 +107,8 @@ def install_client(client_name: ClientName) -> None: install_client_cfg = get_confirm(question, allow_go_back=False) cm = ConfigManager(cfg_file=KIAUH_CFG) - client_port = cm.get_value(client.get("name"), "port") + default_port = cm.get_value(client.get("name"), "port") + client_port = default_port if default_port and default_port.isdigit() else "80" ports_in_use = read_ports_from_nginx_configs() # check if configured port is a valid number and not in use already From 8df75dc8d04aefeea62e2222bd88c44900369ed1 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 2 Mar 2024 21:47:27 +0100 Subject: [PATCH 115/296] fix(webclients): print to screen if symlink does not exist Signed-off-by: Dominik Willner --- .../webui_client/client_config/client_config_remove.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index 41203a3..af5fe27 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -53,12 +53,16 @@ def remove_client_config_dir(client_config: ClientConfigData) -> None: def remove_client_config_symlink(client_config: ClientConfigData) -> None: - Logger.print_status(f"Removing {client_config.get('cfg_filename')} symlinks ...") im = InstanceManager(Klipper) instances: List[Klipper] = im.instances for instance in instances: Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") + symlink = instance.cfg_dir.joinpath(client_config.get("cfg_filename")) + if not symlink.exists(): + Logger.print_info(f"'{symlink}' does not exist. Skipping ...") + continue + try: - remove_file(instance.cfg_dir.joinpath(client_config.get("cfg_filename"))) + remove_file(symlink) except subprocess.CalledProcessError: Logger.print_error("Failed to remove symlink!") From bd1aa1ae2bd7ee24de77a1d464f832545eaef104 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Mar 2024 11:04:06 +0100 Subject: [PATCH 116/296] refactor(klipper): add existing client configs to example config upon creation Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 7 +++- kiauh/components/klipper/klipper_utils.py | 15 ++++++-- .../client_config/client_config_setup.py | 34 +++++++++++++------ kiauh/components/webui_client/client_setup.py | 4 +-- 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index e9761e9..e417c38 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -11,6 +11,9 @@ from pathlib import Path +from components.webui_client.client_config.client_config_setup import ( + get_existing_client_config, +) from kiauh import KIAUH_CFG from components.klipper import ( EXIT_KLIPPER_SETUP, @@ -180,5 +183,7 @@ def create_klipper_instance(name: str, create_example_cfg: bool) -> None: kl_im.create_instance() kl_im.enable_instance() if create_example_cfg: - create_example_printer_cfg(new_instance) + # if a client-config is installed, include it in the new example cfg + client_configs = get_existing_client_config() + create_example_printer_cfg(new_instance, client_configs) kl_im.start_instance() diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index e2ff73a..dfc749b 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -16,7 +16,7 @@ import shutil import subprocess import textwrap from pathlib import Path -from typing import List, Union, Literal, Dict +from typing import List, Union, Literal, Dict, Optional from components.klipper import ( MODULE_PATH, @@ -33,6 +33,7 @@ from components.klipper.klipper_dialogs import ( ) from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker_utils import moonraker_to_multi_conversion +from components.webui_client import ClientData from core.backup_manager.backup_manager import BackupManager from core.config_manager.config_manager import ConfigManager from core.instance_manager.base_instance import BaseInstance @@ -261,7 +262,9 @@ def get_highest_index(instance_list: List[Klipper]) -> int: return max(indices) -def create_example_printer_cfg(instance: Klipper) -> None: +def create_example_printer_cfg( + instance: Klipper, client_configs: Optional[List[ClientData]] = None +) -> None: Logger.print_status(f"Creating example printer.cfg in '{instance.cfg_dir}'") if instance.cfg_file.is_file(): Logger.print_info(f"'{instance.cfg_file}' already exists.") @@ -277,7 +280,15 @@ def create_example_printer_cfg(instance: Klipper) -> None: cm = ConfigManager(target) cm.set_value("virtual_sdcard", "path", str(instance.gcodes_dir)) + + # include existing client configs in the example config + if client_configs is not None and len(client_configs) > 0: + for c in client_configs: + section = c.get("client_config").get("printer_cfg_section") + cm.config.add_section(section=section) + cm.write_config() + Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'") diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index d893198..5f8b17a 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -12,7 +12,7 @@ import shutil import subprocess from pathlib import Path -from typing import List, get_args +from typing import List, get_args, Union, Set from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper @@ -40,7 +40,7 @@ def install_client_config(client_name: ClientName) -> None: client_config: ClientConfigData = client.get("client_config") d_name = client_config.get("display_name") - if check_existing_client_config_install(client_name): + if config_for_other_client_exist(client_name): Logger.print_info("Another Client-Config is already installed! Skipped ...") return @@ -80,19 +80,31 @@ def install_client_config(client_name: ClientName) -> None: Logger.print_ok(f"{d_name} installation complete!", start="\n") -def check_existing_client_config_install(client_name: ClientName) -> bool: - # check if any other client-configs are present - # as they can conflict each other, or are at least - # redundant to have, so we skip the installation - client_list = list(get_args(ClientName)) - client_list.remove(client_name) - for c in client_list: +def config_for_other_client_exist(client_to_ignore: ClientName) -> bool: + """ + Check if any other client configs are present on the system. + It is usually not harmful, but chances are they can conflict each other. + Multiple client configs are, at least, redundant to have them installed + :param client_to_ignore: The client name to ignore for the check + :return: True, if other client configs were found, else False + """ + + clients = set([c["name"] for c in get_existing_client_config()]) + clients = clients - {client_to_ignore} + + return True if len(clients) > 0 else False + + +def get_existing_client_config() -> List[ClientData]: + clients = list(get_args(ClientName)) + installed_client_configs: List[ClientData] = [] + for c in clients: c_data: ClientData = load_client_data(c) c_config_data: ClientConfigData = c_data.get("client_config") if c_config_data.get("dir").exists(): - return True + installed_client_configs.append(c_data) - return False + return installed_client_configs def download_client_config(client_config: ClientConfigData) -> None: diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index e2a4ae3..84762ca 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -21,7 +21,7 @@ from components.webui_client import ( from components.moonraker.moonraker import Moonraker from components.webui_client.client_config.client_config_setup import ( install_client_config, - check_existing_client_config_install, + config_for_other_client_exist, ) from components.webui_client.client_dialogs import ( print_moonraker_not_found_dialog, @@ -100,7 +100,7 @@ def install_client(client_name: ClientName) -> None: if ( kl_instances and not client_config.get("dir").exists() - and not check_existing_client_config_install(client.get("name")) + and not config_for_other_client_exist(client_to_ignore=client.get("name")) ): print_install_client_config_dialog(client) question = f"Download the recommended {client_config.get('display_name')}?" From 14aafd558ae7f9ac76963a4aa187d8e3fa221d76 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Mar 2024 11:33:49 +0100 Subject: [PATCH 117/296] refactor(moonraker): add existing client and client configs to example config upon creation Signed-off-by: Dominik Willner --- kiauh/components/moonraker/moonraker_setup.py | 5 ++- kiauh/components/moonraker/moonraker_utils.py | 38 +++++++++++++++++-- .../client_config/client_config_setup.py | 11 ++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index a4c6cf2..dab18b3 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -15,6 +15,7 @@ from pathlib import Path from typing import List from components.webui_client import MAINSAIL_DIR +from components.webui_client.client_config.client_config_setup import get_existing_client_config, get_existing_clients from components.webui_client.client_utils import enable_mainsail_remotemode from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper @@ -104,7 +105,9 @@ def install_moonraker() -> None: mr_im.enable_instance() if create_example_cfg: - create_example_moonraker_conf(current_instance, used_ports_map) + # 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(current_instance, used_ports_map, clients) mr_im.start_instance() diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 8f89364..e7ea965 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -10,7 +10,7 @@ # ======================================================================= # import shutil -from typing import Dict, Literal, List, Union +from typing import Dict, Literal, List, Union, Optional from components.moonraker import ( DEFAULT_MOONRAKER_PORT, @@ -21,7 +21,7 @@ from components.moonraker import ( MOONRAKER_DB_BACKUP_DIR, ) from components.moonraker.moonraker import Moonraker -from components.webui_client import MAINSAIL_DIR +from components.webui_client import MAINSAIL_DIR, ClientData from components.webui_client.client_utils import enable_mainsail_remotemode from core.backup_manager.backup_manager import BackupManager from core.config_manager.config_manager import ConfigManager @@ -50,7 +50,9 @@ def get_moonraker_status() -> Dict[ def create_example_moonraker_conf( - instance: Moonraker, ports_map: Dict[str, int] + instance: Moonraker, + ports_map: Dict[str, int], + clients: Optional[List[ClientData]] = None, ) -> None: Logger.print_status(f"Creating example moonraker.conf in '{instance.cfg_dir}'") if instance.cfg_file.is_file(): @@ -94,6 +96,36 @@ def create_example_moonraker_conf( cm.set_value("server", "klippy_uds_address", str(uds)) cm.set_value("authorization", "trusted_clients", trusted_clients) + # add existing client and client configs in the update section + if clients is not None and len(clients) > 0: + for c in clients: + # client part + c_section = f"update_manager {c.get('name')}" + c_options = [ + ("type", "web"), + ("channel", "stable"), + ("repo", c.get("mr_conf_repo")), + ("path", c.get("mr_conf_path")), + ] + cm.config.add_section(section=c_section) + for option in c_options: + cm.config.set(c_section, option[0], option[1]) + + # client config part + c_config = c.get("client_config") + if c_config.get("dir").exists(): + c_config_section = f"update_manager {c_config.get('name')}" + c_config_options = [ + ("type", "git_repo"), + ("primary_branch", "master"), + ("path", c_config.get("mr_conf_path")), + ("origin", c_config.get("mr_conf_origin")), + ("managed_services", "klipper"), + ] + cm.config.add_section(section=c_config_section) + for option in c_config_options: + cm.config.set(c_config_section, option[0], option[1]) + cm.write_config() Logger.print_ok(f"Example moonraker.conf created in '{instance.cfg_dir}'") diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 5f8b17a..3dd5de5 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -95,6 +95,17 @@ def config_for_other_client_exist(client_to_ignore: ClientName) -> bool: return True if len(clients) > 0 else False +def get_existing_clients() -> List[ClientData]: + clients = list(get_args(ClientName)) + installed_clients: List[ClientData] = [] + for c in clients: + c_data: ClientData = load_client_data(c) + if c_data.get("dir").exists(): + installed_clients.append(c_data) + + return installed_clients + + def get_existing_client_config() -> List[ClientData]: clients = list(get_args(ClientName)) installed_client_configs: List[ClientData] = [] From 655b781aef42f8d16dac402c6173ac3aae24eef2 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Mar 2024 11:42:27 +0100 Subject: [PATCH 118/296] refactor(KIAUH): move util functions into util modules Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 4 +- kiauh/components/moonraker/moonraker_setup.py | 3 +- .../client_config/client_config_setup.py | 44 ++----------------- kiauh/components/webui_client/client_setup.py | 5 +-- kiauh/components/webui_client/client_utils.py | 42 +++++++++++++++++- 5 files changed, 47 insertions(+), 51 deletions(-) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index e417c38..724d16a 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -11,9 +11,7 @@ from pathlib import Path -from components.webui_client.client_config.client_config_setup import ( - get_existing_client_config, -) +from components.webui_client.client_utils import get_existing_client_config from kiauh import KIAUH_CFG from components.klipper import ( EXIT_KLIPPER_SETUP, diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index dab18b3..c7b80ac 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -15,8 +15,7 @@ from pathlib import Path from typing import List from components.webui_client import MAINSAIL_DIR -from components.webui_client.client_config.client_config_setup import get_existing_client_config, get_existing_clients -from components.webui_client.client_utils import enable_mainsail_remotemode +from components.webui_client.client_utils import enable_mainsail_remotemode, get_existing_clients from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import print_instance_overview diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 3dd5de5..13f1062 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -12,7 +12,7 @@ import shutil import subprocess from pathlib import Path -from typing import List, get_args, Union, Set +from typing import List from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper @@ -21,8 +21,8 @@ from components.webui_client import ClientConfigData, ClientName, ClientData from components.webui_client.client_dialogs import print_client_already_installed_dialog from components.webui_client.client_utils import ( load_client_data, - backup_client_config_data, -) + backup_client_config_data, config_for_other_client_exist, + ) from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager @@ -80,44 +80,6 @@ def install_client_config(client_name: ClientName) -> None: Logger.print_ok(f"{d_name} installation complete!", start="\n") -def config_for_other_client_exist(client_to_ignore: ClientName) -> bool: - """ - Check if any other client configs are present on the system. - It is usually not harmful, but chances are they can conflict each other. - Multiple client configs are, at least, redundant to have them installed - :param client_to_ignore: The client name to ignore for the check - :return: True, if other client configs were found, else False - """ - - clients = set([c["name"] for c in get_existing_client_config()]) - clients = clients - {client_to_ignore} - - return True if len(clients) > 0 else False - - -def get_existing_clients() -> List[ClientData]: - clients = list(get_args(ClientName)) - installed_clients: List[ClientData] = [] - for c in clients: - c_data: ClientData = load_client_data(c) - if c_data.get("dir").exists(): - installed_clients.append(c_data) - - return installed_clients - - -def get_existing_client_config() -> List[ClientData]: - clients = list(get_args(ClientName)) - installed_client_configs: List[ClientData] = [] - for c in clients: - c_data: ClientData = load_client_data(c) - c_config_data: ClientConfigData = c_data.get("client_config") - if c_config_data.get("dir").exists(): - installed_client_configs.append(c_data) - - return installed_client_configs - - def download_client_config(client_config: ClientConfigData) -> None: try: Logger.print_status(f"Downloading {client_config.get('display_name')} ...") diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 84762ca..2744cef 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -21,7 +21,6 @@ from components.webui_client import ( from components.moonraker.moonraker import Moonraker from components.webui_client.client_config.client_config_setup import ( install_client_config, - config_for_other_client_exist, ) from components.webui_client.client_dialogs import ( print_moonraker_not_found_dialog, @@ -33,8 +32,8 @@ from components.webui_client.client_utils import ( restore_mainsail_config_json, enable_mainsail_remotemode, symlink_webui_nginx_log, - load_client_data, -) + load_client_data, config_for_other_client_exist, + ) from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager from kiauh import KIAUH_CFG diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 40207e5..4dd53d0 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -13,7 +13,7 @@ import json import shutil from json import JSONDecodeError from pathlib import Path -from typing import List, Optional, Dict, Literal, Union +from typing import List, Optional, Dict, Literal, Union, get_args import urllib.request @@ -43,7 +43,7 @@ from components.webui_client import ( from core.backup_manager.backup_manager import BackupManager from core.repo_manager.repo_manager import RepoManager from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD -from utils.common import get_install_status_webui, get_install_status_common +from utils.common import get_install_status_webui from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW from utils.logger import Logger @@ -235,3 +235,41 @@ def backup_client_config_data(client: ClientData) -> None: target = client_config.get("backup_dir") bm = BackupManager() bm.backup_directory(name, source, target) + + +def get_existing_clients() -> List[ClientData]: + clients = list(get_args(ClientName)) + installed_clients: List[ClientData] = [] + for c in clients: + c_data: ClientData = load_client_data(c) + if c_data.get("dir").exists(): + installed_clients.append(c_data) + + return installed_clients + + +def get_existing_client_config() -> List[ClientData]: + clients = list(get_args(ClientName)) + installed_client_configs: List[ClientData] = [] + for c in clients: + c_data: ClientData = load_client_data(c) + c_config_data: ClientConfigData = c_data.get("client_config") + if c_config_data.get("dir").exists(): + installed_client_configs.append(c_data) + + return installed_client_configs + + +def config_for_other_client_exist(client_to_ignore: ClientName) -> bool: + """ + Check if any other client configs are present on the system. + It is usually not harmful, but chances are they can conflict each other. + Multiple client configs are, at least, redundant to have them installed + :param client_to_ignore: The client name to ignore for the check + :return: True, if other client configs were found, else False + """ + + clients = set([c["name"] for c in get_existing_client_config()]) + clients = clients - {client_to_ignore} + + return True if len(clients) > 0 else False From 9dc556e7e4fe16789b91823396d820ea321cea56 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Mar 2024 20:26:01 +0100 Subject: [PATCH 119/296] fix(webclients): correctly remove symlink Signed-off-by: Dominik Willner --- .../webui_client/client_config/client_config_remove.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index af5fe27..dc3eb39 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -56,9 +56,9 @@ def remove_client_config_symlink(client_config: ClientConfigData) -> None: im = InstanceManager(Klipper) instances: List[Klipper] = im.instances for instance in instances: - Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...") + Logger.print_status(f"Removing symlink from '{instance.cfg_dir}' ...") symlink = instance.cfg_dir.joinpath(client_config.get("cfg_filename")) - if not symlink.exists(): + if not symlink.is_symlink(): Logger.print_info(f"'{symlink}' does not exist. Skipping ...") continue From 7c754de08e9417c6c1a8e7920c4143a269e0446c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Mar 2024 20:31:30 +0100 Subject: [PATCH 120/296] refactor(utils): re-arrange message printing Signed-off-by: Dominik Willner --- kiauh/utils/system_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 718607a..a79465c 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -127,11 +127,14 @@ def install_python_requirements(target: Path, requirements: Path) -> None: :param requirements: Path to the requirements.txt file :return: None """ - Logger.print_status("Installing Python requirements ...") try: + # always update pip before installing requirements update_python_pip(target) + + Logger.print_status("Installing Python requirements ...") command = [target.joinpath("bin/pip"), "install", "-r", f"{requirements}"] result = subprocess.run(command, stderr=subprocess.PIPE, text=True) + if result.returncode != 0 or result.stderr: Logger.print_error(f"{result.stderr}", False) Logger.print_error("Installing Python requirements failed!") From c0caab13b30fa33a0362d6bd34eb6ed70f46874e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Mar 2024 20:34:21 +0100 Subject: [PATCH 121/296] fix(kiauh): typo Signed-off-by: Dominik Willner --- kiauh/components/log_uploads/log_upload_utils.py | 2 +- kiauh/core/backup_manager/backup_manager.py | 4 ++-- kiauh/core/repo_manager/repo_manager.py | 4 ++-- .../gcode_shell_cmd/gcode_shell_cmd_extension.py | 2 +- kiauh/utils/system_utils.py | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/kiauh/components/log_uploads/log_upload_utils.py b/kiauh/components/log_uploads/log_upload_utils.py index daa67e4..2454443 100644 --- a/kiauh/components/log_uploads/log_upload_utils.py +++ b/kiauh/components/log_uploads/log_upload_utils.py @@ -49,7 +49,7 @@ def upload_logfile(logfile: LogFile) -> None: try: response = urllib.request.urlopen(req) link = response.read().decode("utf-8") - Logger.print_ok("Upload successfull! Access it via the following link:") + Logger.print_ok("Upload successful! Access it via the following link:") Logger.print_ok(f">>>> {link}", False) except Exception as e: Logger.print_error(f"Uploading logfile failed!") diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 1c27266..249bf1e 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -56,7 +56,7 @@ class BackupManager: try: Path(target).mkdir(exist_ok=True) shutil.copyfile(file, target.joinpath(filename)) - Logger.print_ok("Backup successfull!") + Logger.print_ok("Backup successful!") except OSError as e: Logger.print_error(f"Unable to backup '{file}':\n{e}") else: @@ -77,7 +77,7 @@ class BackupManager: target.joinpath(f"{name.lower()}-{date}-{time}"), ignore=self.ignore_folders_func, ) - Logger.print_ok("Backup successfull!") + Logger.print_ok("Backup successful!") except OSError as e: Logger.print_error(f"Unable to backup directory '{source}':\n{e}") return diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index a8f9eec..153deef 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -140,7 +140,7 @@ class RepoManager: command = ["git", "clone", self.repo, self.target_dir] subprocess.run(command, check=True) - Logger.print_ok("Clone successfull!") + Logger.print_ok("Clone successful!") except subprocess.CalledProcessError as e: log = f"Error cloning repository {self.repo}: {e.stderr.decode()}" Logger.print_error(log) @@ -151,7 +151,7 @@ class RepoManager: command = ["git", "checkout", f"{self.branch}"] subprocess.run(command, cwd=self.target_dir, check=True) - Logger.print_ok("Checkout successfull!") + Logger.print_ok("Checkout successful!") except subprocess.CalledProcessError as e: log = f"Error checking out branch {self.branch}: {e.stderr.decode()}" Logger.print_error(log) diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index bf0a977..8d004ec 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -68,7 +68,7 @@ class GcodeShellCmdExtension(BaseExtension): im.start_all_instance() - Logger.print_ok("Installing G-Code Shell Command extension successfull!") + Logger.print_ok("Installing G-Code Shell Command extension successful!") def remove_extension(self, **kwargs) -> None: extension_installed = check_file_exist(EXTENSION_TARGET_PATH) diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index a79465c..d140ead 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -71,7 +71,7 @@ def create_python_venv(target: Path) -> None: if not target.exists(): try: venv.create(target, with_pip=True) - Logger.print_ok("Setup of virtualenv successfull!") + Logger.print_ok("Setup of virtualenv successful!") except OSError as e: Logger.print_error(f"Error setting up virtualenv:\n{e}") raise @@ -111,7 +111,7 @@ def update_python_pip(target: Path) -> None: Logger.print_error("Updating pip failed!") return - Logger.print_ok("Updating pip successfull!") + Logger.print_ok("Updating pip successful!") except FileNotFoundError as e: Logger.print_error(e) raise @@ -140,7 +140,7 @@ def install_python_requirements(target: Path, requirements: Path) -> None: Logger.print_error("Installing Python requirements failed!") return - Logger.print_ok("Installing Python requirements successfull!") + Logger.print_ok("Installing Python requirements successful!") except subprocess.CalledProcessError as e: log = f"Error installing Python requirements:\n{e.output.decode()}" Logger.print_error(log) @@ -183,7 +183,7 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: Logger.print_error("Updating system package list failed!") return - Logger.print_ok("System package list updated successfully!") + Logger.print_ok("System package list update successful!") except subprocess.CalledProcessError as e: kill(f"Error updating system package list:\n{e.stderr.decode()}") From 601ccb219165be1cd6a5bb50ac769400c1919def Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Mar 2024 20:53:49 +0100 Subject: [PATCH 122/296] fix(webclients): add symlink to added klipper instances Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 6 +++--- kiauh/components/klipper/klipper_utils.py | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 724d16a..591cf08 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -11,7 +11,7 @@ from pathlib import Path -from components.webui_client.client_utils import get_existing_client_config +from components.webui_client.client_utils import get_existing_client_config, get_existing_clients from kiauh import KIAUH_CFG from components.klipper import ( EXIT_KLIPPER_SETUP, @@ -182,6 +182,6 @@ def create_klipper_instance(name: str, create_example_cfg: bool) -> None: kl_im.enable_instance() if create_example_cfg: # if a client-config is installed, include it in the new example cfg - client_configs = get_existing_client_config() - create_example_printer_cfg(new_instance, client_configs) + clients = get_existing_clients() + create_example_printer_cfg(new_instance, clients) kl_im.start_instance() diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index dfc749b..6b2bea8 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -34,6 +34,7 @@ from components.klipper.klipper_dialogs import ( from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker_utils import moonraker_to_multi_conversion from components.webui_client import ClientData +from components.webui_client.client_config.client_config_setup import create_client_config_symlink from core.backup_manager.backup_manager import BackupManager from core.config_manager.config_manager import ConfigManager from core.instance_manager.base_instance import BaseInstance @@ -263,7 +264,7 @@ def get_highest_index(instance_list: List[Klipper]) -> int: def create_example_printer_cfg( - instance: Klipper, client_configs: Optional[List[ClientData]] = None + instance: Klipper, clients: Optional[List[ClientData]] = None ) -> None: Logger.print_status(f"Creating example printer.cfg in '{instance.cfg_dir}'") if instance.cfg_file.is_file(): @@ -282,10 +283,12 @@ def create_example_printer_cfg( cm.set_value("virtual_sdcard", "path", str(instance.gcodes_dir)) # include existing client configs in the example config - if client_configs is not None and len(client_configs) > 0: - for c in client_configs: - section = c.get("client_config").get("printer_cfg_section") + if clients is not None and len(clients) > 0: + for c in clients: + client_config = c.get("client_config") + section = client_config.get("printer_cfg_section") cm.config.add_section(section=section) + create_client_config_symlink(client_config,[instance]) cm.write_config() From 682baaa1051b31f1ad432898cb12187be49f1d60 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Mar 2024 20:56:14 +0100 Subject: [PATCH 123/296] refactor(kiauh): remove unused function Signed-off-by: Dominik Willner --- kiauh/components/moonraker/moonraker_setup.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index c7b80ac..f27580b 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -193,17 +193,6 @@ def install_moonraker_polkit() -> None: Logger.print_error(log) -def handle_existing_instances(instance_list: List[Klipper]) -> bool: - instance_count = len(instance_list) - - if instance_count > 0: - print_instance_overview(instance_list) - if not get_confirm("Add new instances?", allow_go_back=True): - return False - - return True - - def update_moonraker() -> None: if not get_confirm("Update Moonraker now?"): return From 758a783ede788fb9ed396a6da810837ef018807c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 3 Mar 2024 21:01:30 +0100 Subject: [PATCH 124/296] refactor(moonraker): allow re-running installer if instance count already matches Signed-off-by: Dominik Willner --- kiauh/components/moonraker/moonraker_setup.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index f27580b..2cfdd0d 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -130,12 +130,6 @@ def check_moonraker_install_requirements() -> bool: Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") return False - mr_instance_count = len(InstanceManager(Moonraker).instances) - if mr_instance_count >= kl_instance_count: - Logger.print_warn("Unable to install more Moonraker instances!") - Logger.print_warn("More Klipper instances required.") - return False - return True From 5f823c2d3a1bedcbb834c6b53a6fc1b9eb1f6eb3 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 18 Mar 2024 22:18:45 +0100 Subject: [PATCH 125/296] refactor(klipper): use correct virtual_sdcard path after multi-conversion Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 8 +++++-- kiauh/components/klipper/klipper_utils.py | 29 +++++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 591cf08..63b5cf9 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -11,7 +11,10 @@ from pathlib import Path -from components.webui_client.client_utils import get_existing_client_config, get_existing_clients +from components.webui_client.client_utils import ( + get_existing_client_config, + get_existing_clients, +) from kiauh import KIAUH_CFG from components.klipper import ( EXIT_KLIPPER_SETUP, @@ -96,7 +99,8 @@ def install_klipper() -> None: kl_im.reload_daemon() - except Exception: + except Exception as e: + Logger.print_error(e) Logger.print_error("Klipper installation failed!") return diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 6b2bea8..fe36979 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -34,13 +34,16 @@ from components.klipper.klipper_dialogs import ( from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker_utils import moonraker_to_multi_conversion from components.webui_client import ClientData -from components.webui_client.client_config.client_config_setup import create_client_config_symlink +from components.webui_client.client_config.client_config_setup import ( + create_client_config_symlink, +) from core.backup_manager.backup_manager import BackupManager from core.config_manager.config_manager import ConfigManager from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.name_scheme import NameScheme from core.repo_manager.repo_manager import RepoManager +from utils import PRINTER_CFG_BACKUP_DIR from utils.common import get_install_status_common from utils.constants import CURRENT_USER from utils.input_utils import get_confirm, get_string_input, get_number_input @@ -161,6 +164,15 @@ def klipper_to_multi_conversion(new_name: str) -> None: im.current_instance = im.instances[0] # temporarily store the data dir path old_data_dir = im.instances[0].data_dir + old_data_dir_name = im.instances[0].data_dir_name + # backup the old data_dir + bm = BackupManager() + name = f"config-{old_data_dir_name}" + bm.backup_directory( + name, + source=im.current_instance.cfg_dir, + target=PRINTER_CFG_BACKUP_DIR, + ) # remove the old single instance im.stop_instance() im.disable_instance() @@ -169,13 +181,20 @@ def klipper_to_multi_conversion(new_name: str) -> None: im.current_instance = Klipper(suffix=new_name) new_data_dir: Path = im.current_instance.data_dir - # rename the old data dir and use it for the new instance - Logger.print_status(f"Rename '{old_data_dir}' to '{new_data_dir}' ...") if not new_data_dir.is_dir(): + # rename the old data dir and use it for the new instance + Logger.print_status(f"Rename '{old_data_dir}' to '{new_data_dir}' ...") old_data_dir.rename(new_data_dir) else: - Logger.print_info(f"'{new_data_dir}' already exist. Skipped ...") + Logger.print_info(f"Existing '{new_data_dir}' found ...") + # patch the virtual_sdcard sections path value to match the new printer_data foldername + cm = ConfigManager(im.current_instance.cfg_file) + if cm.config.has_section("virtual_sdcard"): + cm.set_value("virtual_sdcard", "path", str(im.current_instance.gcodes_dir)) + cm.write_config() + + # finalize creating the new instance im.create_instance() im.enable_instance() im.start_instance() @@ -288,7 +307,7 @@ def create_example_printer_cfg( client_config = c.get("client_config") section = client_config.get("printer_cfg_section") cm.config.add_section(section=section) - create_client_config_symlink(client_config,[instance]) + create_client_config_symlink(client_config, [instance]) cm.write_config() From 84cda99af899ee1d3e9668a91755809b16039459 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 20 Mar 2024 21:39:20 +0100 Subject: [PATCH 126/296] fix(klipper): patch virtual_sdcard section when converting single to multi instance Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index fe36979..5e2314f 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -162,9 +162,11 @@ def klipper_to_multi_conversion(new_name: str) -> None: Logger.print_status("Convert Klipper single to multi instance ...") im = InstanceManager(Klipper) im.current_instance = im.instances[0] + # temporarily store the data dir path old_data_dir = im.instances[0].data_dir old_data_dir_name = im.instances[0].data_dir_name + # backup the old data_dir bm = BackupManager() name = f"config-{old_data_dir_name}" @@ -173,25 +175,27 @@ def klipper_to_multi_conversion(new_name: str) -> None: source=im.current_instance.cfg_dir, target=PRINTER_CFG_BACKUP_DIR, ) + # remove the old single instance im.stop_instance() im.disable_instance() im.delete_instance() - # create a new klipper instance with the new name - im.current_instance = Klipper(suffix=new_name) - new_data_dir: Path = im.current_instance.data_dir - if not new_data_dir.is_dir(): + # create a new klipper instance with the new name + new_instance = Klipper(suffix=new_name) + im.current_instance = new_instance + + if not new_instance.data_dir.is_dir(): # rename the old data dir and use it for the new instance - Logger.print_status(f"Rename '{old_data_dir}' to '{new_data_dir}' ...") - old_data_dir.rename(new_data_dir) + Logger.print_status(f"Rename '{old_data_dir}' to '{new_instance.data_dir}' ...") + old_data_dir.rename(new_instance.data_dir) else: - Logger.print_info(f"Existing '{new_data_dir}' found ...") + Logger.print_info(f"Existing '{new_instance.data_dir}' found ...") # patch the virtual_sdcard sections path value to match the new printer_data foldername - cm = ConfigManager(im.current_instance.cfg_file) + cm = ConfigManager(new_instance.cfg_file) if cm.config.has_section("virtual_sdcard"): - cm.set_value("virtual_sdcard", "path", str(im.current_instance.gcodes_dir)) + cm.set_value("virtual_sdcard", "path", str(new_instance.gcodes_dir)) cm.write_config() # finalize creating the new instance From 9eb0531cdf94b4c7de7b93718c1a374debcee33d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 20 Mar 2024 21:40:03 +0100 Subject: [PATCH 127/296] fix(moonraker): patch klippy_uds_address section when converting single to multi instance Signed-off-by: Dominik Willner --- kiauh/components/moonraker/moonraker.py | 5 +++++ kiauh/components/moonraker/moonraker_utils.py | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index aa0e03b..994cba0 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -35,12 +35,17 @@ class Moonraker(BaseInstance): self.backup_dir = self.data_dir.joinpath("backup") self.certs_dir = self.data_dir.joinpath("certs") self._db_dir = self.data_dir.joinpath("database") + self._comms_dir = self.data_dir.joinpath("comms") self.log = self.log_dir.joinpath("moonraker.log") @property def db_dir(self) -> Path: return self._db_dir + @property + def comms_dir(self) -> Path: + return self._comms_dir + def create(self, create_example_cfg: bool = False) -> None: Logger.print_status("Creating new Moonraker Instance ...") service_template_path = MODULE_PATH.joinpath("assets/moonraker.service") diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index e7ea965..a2a8c22 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -150,13 +150,27 @@ def moonraker_to_multi_conversion(new_name: str) -> None: return Logger.print_status("Convert Moonraker single to multi instance ...") + # remove the old single instance im.current_instance = im.instances[0] im.stop_instance() im.disable_instance() im.delete_instance() - # create a new klipper instance with the new name - im.current_instance = Moonraker(suffix=new_name) + + # create a new moonraker instance with the new name + new_instance = Moonraker(suffix=new_name) + im.current_instance = new_instance + + # patch the server sections klippy_uds_address value to match the new printer_data foldername + cm = ConfigManager(new_instance.cfg_file) + if cm.config.has_section("server"): + cm.set_value( + "server", + "klippy_uds_address", + str(new_instance.comms_dir.joinpath("klippy.sock")), + ) + cm.write_config() + # create, enable and start the new moonraker instance im.create_instance() im.enable_instance() From 7ec055f5628d175b06587a08dce552f884dcf82c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 20 Mar 2024 22:10:51 +0100 Subject: [PATCH 128/296] refactor(webclients): always remove config sections Signed-off-by: Dominik Willner --- .../client_config/client_config_remove.py | 10 ++---- .../components/webui_client/client_remove.py | 13 ++++---- .../webui_client/menus/client_remove_menu.py | 32 ++----------------- 3 files changed, 11 insertions(+), 44 deletions(-) diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index dc3eb39..22d00fb 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -24,19 +24,13 @@ from utils.logger import Logger def run_client_config_removal( client_config: ClientConfigData, - remove_moonraker_conf_section: bool, - remove_printer_cfg_include: bool, kl_instances: List[Klipper], mr_instances: List[Moonraker], ) -> None: remove_client_config_dir(client_config) remove_client_config_symlink(client_config) - if remove_moonraker_conf_section: - remove_config_section( - f"update_manager {client_config.get('name')}", mr_instances - ) - if remove_printer_cfg_include: - remove_config_section(client_config.get("printer_cfg_section"), kl_instances) + remove_config_section(f"update_manager {client_config.get('name')}", mr_instances) + remove_config_section(client_config.get("printer_cfg_section"), kl_instances) def remove_client_config_dir(client_config: ClientConfigData) -> None: diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index 49bc694..7a8648c 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -35,28 +35,27 @@ def run_client_removal( rm_client: bool, rm_client_config: bool, backup_ms_config_json: bool, - rm_moonraker_conf_section: bool, - rm_printer_cfg_section: bool, ) -> None: mr_im = InstanceManager(Moonraker) mr_instances: List[Moonraker] = mr_im.instances kl_im = InstanceManager(Klipper) kl_instances: List[Klipper] = kl_im.instances + if backup_ms_config_json and client.get("name") == "mainsail": backup_mainsail_config_json() + if rm_client: client_name = client.get("name") remove_client_dir(client) remove_nginx_config(client_name) remove_nginx_logs(client_name) - if rm_moonraker_conf_section: - section = f"update_manager {client_name}" - remove_config_section(section, mr_instances) + + section = f"update_manager {client_name}" + remove_config_section(section, mr_instances) + if rm_client_config: run_client_config_removal( client.get("client_config"), - rm_moonraker_conf_section, - rm_printer_cfg_section, kl_instances, mr_instances, ) diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index b4648d6..6db4097 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -25,8 +25,6 @@ class ClientRemoveMenu(BaseMenu): self.rm_client = False self.rm_client_config = False self.backup_mainsail_config_json = False - self.rm_moonraker_conf_section = False - self.rm_printer_cfg_section = False super().__init__( header=False, @@ -39,12 +37,10 @@ class ClientRemoveMenu(BaseMenu): "0": self.toggle_all, "1": self.toggle_rm_client, "2": self.toggle_rm_client_config, - "3": self.toggle_rm_printer_cfg_section, - "4": self.toggle_rm_moonraker_conf_section, "c": self.run_removal_process, } if self.client.get("name") == "mainsail": - options["5"] = self.toggle_backup_mainsail_config_json + options["3"] = self.toggle_backup_mainsail_config_json return options @@ -60,8 +56,6 @@ class ClientRemoveMenu(BaseMenu): unchecked = "[ ]" o1 = checked if self.rm_client else unchecked o2 = checked if self.rm_client_config else unchecked - o3 = checked if self.rm_printer_cfg_section else unchecked - o4 = checked if self.rm_moonraker_conf_section else unchecked menu = textwrap.dedent( f""" /=======================================================\\ @@ -74,20 +68,14 @@ class ClientRemoveMenu(BaseMenu): |-------------------------------------------------------| | 1) {o1} Remove {client_name:16} | | 2) {o2} Remove {client_config_name:24} | - | | - | printer.cfg & moonraker.conf | - | 3) {o3} Remove printer.cfg include | - | 4) {o4} Remove Moonraker update section | """ )[1:] if self.client.get("name") == "mainsail": - o5 = checked if self.backup_mainsail_config_json else unchecked + o3 = checked if self.backup_mainsail_config_json else unchecked menu += textwrap.dedent( f""" - | | - | Mainsail config.json | - | 5) {o5} Backup config.json | + | 3) {o3} Backup config.json | """ )[1:] @@ -103,8 +91,6 @@ class ClientRemoveMenu(BaseMenu): self.rm_client = True self.rm_client_config = True self.backup_mainsail_config_json = True - self.rm_moonraker_conf_section = True - self.rm_printer_cfg_section = True def toggle_rm_client(self, **kwargs) -> None: self.rm_client = not self.rm_client @@ -115,19 +101,11 @@ class ClientRemoveMenu(BaseMenu): def toggle_backup_mainsail_config_json(self, **kwargs) -> None: self.backup_mainsail_config_json = not self.backup_mainsail_config_json - def toggle_rm_moonraker_conf_section(self, **kwargs) -> None: - self.rm_moonraker_conf_section = not self.rm_moonraker_conf_section - - def toggle_rm_printer_cfg_section(self, **kwargs) -> None: - self.rm_printer_cfg_section = not self.rm_printer_cfg_section - def run_removal_process(self, **kwargs) -> None: if ( not self.rm_client and not self.rm_client_config and not self.backup_mainsail_config_json - and not self.rm_moonraker_conf_section - and not self.rm_printer_cfg_section ): error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}" print(error) @@ -138,12 +116,8 @@ class ClientRemoveMenu(BaseMenu): rm_client=self.rm_client, rm_client_config=self.rm_client_config, backup_ms_config_json=self.backup_mainsail_config_json, - rm_moonraker_conf_section=self.rm_moonraker_conf_section, - rm_printer_cfg_section=self.rm_printer_cfg_section, ) self.rm_client = False self.rm_client_config = False self.backup_mainsail_config_json = False - self.rm_moonraker_conf_section = False - self.rm_printer_cfg_section = False From ed35dc9e03a06c3ed61486b12095bd929256f06e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 21 Mar 2024 20:38:14 +0100 Subject: [PATCH 129/296] chore: add mypy config to pyproject.toml Signed-off-by: Dominik Willner --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index be1fddf..40eae37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,3 +11,5 @@ exclude = ''' | scripts/ ) ''' +[tool.mypy] +mypy_path = "./kiauh" From 4ffa05793160ac27e0feaec5cd8c251e917b83d7 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 21 Mar 2024 21:50:10 +0100 Subject: [PATCH 130/296] chore: improve type hinting Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/base_instance.py | 27 +++++++++----------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index 95e24dc..8655258 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # @@ -9,14 +7,13 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations from abc import abstractmethod, ABC from pathlib import Path -from typing import List, Type, TypeVar +from typing import List, Optional from utils.constants import SYSTEMD, CURRENT_USER -B = TypeVar(name="B", bound="BaseInstance", covariant=True) - class BaseInstance(ABC): @classmethod @@ -26,7 +23,7 @@ class BaseInstance(ABC): def __init__( self, suffix: str, - instance_type: B = B, + instance_type: BaseInstance, ): self._instance_type = instance_type self._suffix = suffix @@ -40,11 +37,11 @@ class BaseInstance(ABC): self._gcodes_dir = self.data_dir.joinpath("gcodes") @property - def instance_type(self) -> Type["BaseInstance"]: + def instance_type(self) -> BaseInstance: return self._instance_type @instance_type.setter - def instance_type(self, value: Type["BaseInstance"]) -> None: + def instance_type(self, value: BaseInstance) -> None: self._instance_type = value @property @@ -76,7 +73,7 @@ class BaseInstance(ABC): return self._data_dir @data_dir.setter - def data_dir(self, value: str) -> None: + def data_dir(self, value: Path) -> None: self._data_dir = value @property @@ -84,7 +81,7 @@ class BaseInstance(ABC): return self._cfg_dir @cfg_dir.setter - def cfg_dir(self, value: str) -> None: + def cfg_dir(self, value: Path) -> None: self._cfg_dir = value @property @@ -92,7 +89,7 @@ class BaseInstance(ABC): return self._log_dir @log_dir.setter - def log_dir(self, value: str) -> None: + def log_dir(self, value: Path) -> None: self._log_dir = value @property @@ -100,7 +97,7 @@ class BaseInstance(ABC): return self._comms_dir @comms_dir.setter - def comms_dir(self, value: str) -> None: + def comms_dir(self, value: Path) -> None: self._comms_dir = value @property @@ -108,7 +105,7 @@ class BaseInstance(ABC): return self._sysd_dir @sysd_dir.setter - def sysd_dir(self, value: str) -> None: + def sysd_dir(self, value: Path) -> None: self._sysd_dir = value @property @@ -116,7 +113,7 @@ class BaseInstance(ABC): return self._gcodes_dir @gcodes_dir.setter - def gcodes_dir(self, value: str) -> None: + def gcodes_dir(self, value: Path) -> None: self._gcodes_dir = value @abstractmethod @@ -127,7 +124,7 @@ class BaseInstance(ABC): def delete(self) -> None: raise NotImplementedError("Subclasses must implement the delete method") - def create_folders(self, add_dirs: List[Path] = None) -> None: + def create_folders(self, add_dirs: Optional[List[Path]] = None) -> None: dirs = [ self.data_dir, self.cfg_dir, From a5dce136f342b5e660813faa4de8d70a30be5b62 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 21 Mar 2024 21:55:35 +0100 Subject: [PATCH 131/296] chore: remove shebang from most files because it is not needed Signed-off-by: Dominik Willner --- kiauh/__init__.py | 2 -- kiauh/components/klipper/__init__.py | 2 -- kiauh/components/klipper/klipper.py | 2 -- kiauh/components/klipper/klipper_dialogs.py | 2 -- kiauh/components/klipper/klipper_remove.py | 2 -- kiauh/components/klipper/klipper_setup.py | 3 --- kiauh/components/klipper/klipper_utils.py | 3 --- kiauh/components/klipper/menus/klipper_remove_menu.py | 2 -- kiauh/components/log_uploads/__init__.py | 2 -- kiauh/components/log_uploads/log_upload_utils.py | 2 -- kiauh/components/log_uploads/menus/log_upload_menu.py | 2 -- kiauh/components/moonraker/__init__.py | 2 -- kiauh/components/moonraker/menus/moonraker_remove_menu.py | 2 -- kiauh/components/moonraker/moonraker.py | 2 -- kiauh/components/moonraker/moonraker_dialogs.py | 2 -- kiauh/components/moonraker/moonraker_remove.py | 2 -- kiauh/components/moonraker/moonraker_setup.py | 4 ---- kiauh/components/moonraker/moonraker_utils.py | 2 -- kiauh/components/webui_client/__init__.py | 2 -- .../webui_client/client_config/client_config_remove.py | 2 -- kiauh/components/webui_client/client_dialogs.py | 2 -- kiauh/components/webui_client/client_remove.py | 2 -- kiauh/components/webui_client/client_setup.py | 7 +++---- kiauh/components/webui_client/client_utils.py | 2 -- kiauh/components/webui_client/menus/client_remove_menu.py | 2 -- kiauh/core/backup_manager/__init__.py | 2 -- kiauh/core/backup_manager/backup_manager.py | 2 -- kiauh/core/base_extension.py | 2 -- kiauh/core/config_manager/config_manager.py | 2 -- kiauh/core/instance_manager/instance_manager.py | 2 -- kiauh/core/menus/__init__.py | 2 -- kiauh/core/menus/advanced_menu.py | 2 -- kiauh/core/menus/backup_menu.py | 2 -- kiauh/core/menus/base_menu.py | 2 -- kiauh/core/menus/extensions_menu.py | 2 -- kiauh/core/menus/install_menu.py | 2 -- kiauh/core/menus/main_menu.py | 2 -- kiauh/core/menus/remove_menu.py | 2 -- kiauh/core/menus/settings_menu.py | 2 -- kiauh/core/menus/update_menu.py | 2 -- kiauh/core/repo_manager/repo_manager.py | 2 -- kiauh/extensions/gcode_shell_cmd/__init__.py | 2 -- .../gcode_shell_cmd/gcode_shell_cmd_extension.py | 2 -- kiauh/main.py | 2 -- kiauh/utils/__init__.py | 2 -- kiauh/utils/common.py | 2 -- kiauh/utils/constants.py | 2 -- kiauh/utils/input_utils.py | 2 -- kiauh/utils/logger.py | 2 -- kiauh/utils/system_utils.py | 2 -- 50 files changed, 3 insertions(+), 106 deletions(-) diff --git a/kiauh/__init__.py b/kiauh/__init__.py index f93f7de..105b426 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index 9f46783..aee9c25 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index 05eef56..2847ae5 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py index 30892c5..2cfc672 100644 --- a/kiauh/components/klipper/klipper_dialogs.py +++ b/kiauh/components/klipper/klipper_dialogs.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index d802d4b..7245201 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 63b5cf9..db329cb 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # @@ -12,7 +10,6 @@ from pathlib import Path from components.webui_client.client_utils import ( - get_existing_client_config, get_existing_clients, ) from kiauh import KIAUH_CFG diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 5e2314f..e71cb3b 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # @@ -15,7 +13,6 @@ import re import shutil import subprocess import textwrap -from pathlib import Path from typing import List, Union, Literal, Dict, Optional from components.klipper import ( diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index dc92b33..379005a 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/log_uploads/__init__.py b/kiauh/components/log_uploads/__init__.py index 7672138..1d87ba8 100644 --- a/kiauh/components/log_uploads/__init__.py +++ b/kiauh/components/log_uploads/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/log_uploads/log_upload_utils.py b/kiauh/components/log_uploads/log_upload_utils.py index 2454443..3a8029a 100644 --- a/kiauh/components/log_uploads/log_upload_utils.py +++ b/kiauh/components/log_uploads/log_upload_utils.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index baccce5..df2c268 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index 9341992..6c4ff89 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index f740b45..607d04e 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index 994cba0..8cffeb3 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py index 4f13d10..c047e4a 100644 --- a/kiauh/components/moonraker/moonraker_dialogs.py +++ b/kiauh/components/moonraker/moonraker_dialogs.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index db10bf4..04a9872 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 2cfdd0d..d55fd63 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # @@ -12,13 +10,11 @@ import subprocess import sys from pathlib import Path -from typing import List from components.webui_client import MAINSAIL_DIR from components.webui_client.client_utils import enable_mainsail_remotemode, get_existing_clients from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import print_instance_overview from components.moonraker import ( EXIT_MOONRAKER_SETUP, DEFAULT_MOONRAKER_REPO_URL, diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index a2a8c22..ca1a15c 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/webui_client/__init__.py b/kiauh/components/webui_client/__init__.py index b0d44f3..6f21f4a 100644 --- a/kiauh/components/webui_client/__init__.py +++ b/kiauh/components/webui_client/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index 22d00fb..e61c313 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py index 1053a58..5270c42 100644 --- a/kiauh/components/webui_client/client_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index 7a8648c..a55a25f 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 2744cef..9a148a3 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # @@ -32,8 +30,9 @@ from components.webui_client.client_utils import ( restore_mainsail_config_json, enable_mainsail_remotemode, symlink_webui_nginx_log, - load_client_data, config_for_other_client_exist, - ) + load_client_data, + config_for_other_client_exist, +) from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager from kiauh import KIAUH_CFG diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 4dd53d0..d2c4e1f 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index 6db4097..6cf5638 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/backup_manager/__init__.py b/kiauh/core/backup_manager/__init__.py index 7bbbe1e..642c8aa 100644 --- a/kiauh/core/backup_manager/__init__.py +++ b/kiauh/core/backup_manager/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index 249bf1e..e87710d 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/base_extension.py b/kiauh/core/base_extension.py index 09245ee..af80929 100644 --- a/kiauh/core/base_extension.py +++ b/kiauh/core/base_extension.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py index 12d4eb0..7167cb9 100644 --- a/kiauh/core/config_manager/config_manager.py +++ b/kiauh/core/config_manager/config_manager.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 9750086..0a5750b 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index afca077..14f4f7b 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 27eaacd..8fa89de 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 459461a..4f404a7 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 06ad576..8a27978 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/extensions_menu.py b/kiauh/core/menus/extensions_menu.py index a46b00c..15ed229 100644 --- a/kiauh/core/menus/extensions_menu.py +++ b/kiauh/core/menus/extensions_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index a90e235..6400645 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 4ab3b31..8d20036 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index ad53c94..8c13efe 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 66f352c..c74a384 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index ffa74ba..7cd944b 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index 153deef..6a7cfa0 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/extensions/gcode_shell_cmd/__init__.py b/kiauh/extensions/gcode_shell_cmd/__init__.py index 27ffecb..95336dc 100644 --- a/kiauh/extensions/gcode_shell_cmd/__init__.py +++ b/kiauh/extensions/gcode_shell_cmd/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index 8d004ec..e726565 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/main.py b/kiauh/main.py index 0ca361c..dd6f068 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index afc69aa..2435e5d 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 4e5bbee..89b2ec5 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/utils/constants.py b/kiauh/utils/constants.py index a2dd612..cfb8477 100644 --- a/kiauh/utils/constants.py +++ b/kiauh/utils/constants.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index d0a3b48..25a1b71 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index 66f9326..9bb303b 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index d140ead..3b46913 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # From 00665109c2f4b808dbc52e704a875f0e28ab6935 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 22 Mar 2024 22:11:56 +0100 Subject: [PATCH 132/296] feat: allow sections to be added to the top of a config file Signed-off-by: Dominik Willner --- .../client_config/client_config_setup.py | 15 +++++---- kiauh/utils/filesystem_utils.py | 33 ++++++++++++++++--- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 13f1062..01ca5cc 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # # # @@ -21,16 +19,17 @@ from components.webui_client import ClientConfigData, ClientName, ClientData from components.webui_client.client_dialogs import print_client_already_installed_dialog from components.webui_client.client_utils import ( load_client_data, - backup_client_config_data, config_for_other_client_exist, - ) + backup_client_config_data, + config_for_other_client_exist, +) from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager from core.repo_manager.repo_manager import RepoManager from utils.filesystem_utils import ( create_symlink, - add_config_section, -) + add_config_section, add_config_section_at_top, + ) from utils.input_utils import get_confirm from utils.logger import Logger @@ -70,7 +69,9 @@ def install_client_config(client_name: ClientName) -> None: ("managed_services", "klipper"), ], ) - add_config_section(client_config.get("printer_cfg_section"), kl_instances) + add_config_section_at_top( + client_config.get("printer_cfg_section"), kl_instances + ) kl_im.restart_all_instance() except Exception as e: diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index b7fed2f..0406bc2 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import os # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # @@ -12,15 +13,15 @@ import re import shutil import subprocess +import tempfile from pathlib import Path from zipfile import ZipFile -from typing import List, Type, TypeVar, Union, Tuple +from typing import List, TypeVar, Tuple, Optional from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker from core.config_manager.config_manager import ConfigManager -from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager from utils import ( NGINX_SITES_AVAILABLE, @@ -31,7 +32,7 @@ from utils import ( from utils.logger import Logger -B = TypeVar('B', bound='BaseInstance') +B = TypeVar("B", Klipper, Moonraker) ConfigOption = Tuple[str, str] @@ -182,7 +183,11 @@ def get_next_free_port(ports_in_use: List[str]) -> str: return str(min(valid_ports - used_ports)) -def add_config_section(section: str, instances: List[B], options: List[ConfigOption] = None) -> None: +def add_config_section( + section: str, + instances: List[B], + options: Optional[List[ConfigOption]] = None, +) -> None: for instance in instances: cfg_file = instance.cfg_file Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...") @@ -204,6 +209,26 @@ def add_config_section(section: str, instances: List[B], options: List[ConfigOpt cm.write_config() +def add_config_section_at_top( + section: str, + instances: List[B]): + for instance in instances: + tmp_cfg = tempfile.NamedTemporaryFile(mode="w" ,delete=False) + tmp_cfg_path = Path(tmp_cfg.name) + cmt = ConfigManager(tmp_cfg_path) + cmt.config.add_section(section) + cmt.write_config() + tmp_cfg.close() + + cfg_file = instance.cfg_file + with open(cfg_file, "r") as org: + org_content = org.readlines() + with open(tmp_cfg_path, "a") as tmp: + tmp.writelines(org_content) + + cfg_file.unlink() + tmp_cfg_path.rename(cfg_file) + def remove_config_section(section: str, instances: List[B]) -> None: for instance in instances: From 2acd74cbd9f8dbfccac8590d7a9a8a687842ece2 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 22 Mar 2024 22:20:13 +0100 Subject: [PATCH 133/296] refactor(webclients): make a backup before modification of config files Signed-off-by: Dominik Willner --- .../webui_client/client_config/client_config_setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 01ca5cc..02f25a2 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -26,6 +26,7 @@ from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager from core.repo_manager.repo_manager import RepoManager +from utils.common import backup_printer_config_dir from utils.filesystem_utils import ( create_symlink, add_config_section, add_config_section_at_top, @@ -58,6 +59,9 @@ def install_client_config(client_name: ClientName) -> None: try: download_client_config(client_config) create_client_config_symlink(client_config, kl_instances) + + backup_printer_config_dir() + add_config_section( section=f"update_manager {client_config.get('name')}", instances=mr_instances, From ef13c130e03967e0e915dc44a4fb077f95406d3f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 22 Mar 2024 22:44:07 +0100 Subject: [PATCH 134/296] chore: remove mypy from project Signed-off-by: Dominik Willner --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 40eae37..be1fddf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,5 +11,3 @@ exclude = ''' | scripts/ ) ''' -[tool.mypy] -mypy_path = "./kiauh" From 5c1c98b6b8c5e4a22306e549c5c6090a42692231 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 23 Mar 2024 12:59:26 +0100 Subject: [PATCH 135/296] refactor: update advanced menu layout Signed-off-by: Dominik Willner --- kiauh/core/menus/advanced_menu.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 8fa89de..30f783a 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -27,14 +27,18 @@ class AdvancedMenu(BaseMenu): /=======================================================\\ | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| - | Klipper & API: | Mainsail: | - | 0) [Rollback] | 5) [Theme installer] | - | | | - | Firmware: | System: | - | 1) [Build only] | 6) [Change hostname] | - | 2) [Flash only] | | - | 3) [Build + Flash] | Extras: | - | 4) [Get MCU ID] | 7) [G-Code Shell Command] | + | Repo Rollback: | + | 1) [Klipper] | + | 2) [Moonraker] | + | | + | Klipper Firmware: | + | 3) [Build] | + | 4) [Flash] | + | 5) [Build + Flash] | + | 6) [Get MCU ID] | + | | + | Mainsail: | + | 7) [Theme installer] | """ )[1:] print(menu, end="") From 03c3ed20f3c8d520c061e2e60792beffb40d6e03 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 23 Mar 2024 17:55:25 +0100 Subject: [PATCH 136/296] refactor: disable header printing in extension menu Signed-off-by: Dominik Willner --- kiauh/core/menus/extensions_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/core/menus/extensions_menu.py b/kiauh/core/menus/extensions_menu.py index 15ed229..3db2aea 100644 --- a/kiauh/core/menus/extensions_menu.py +++ b/kiauh/core/menus/extensions_menu.py @@ -26,7 +26,7 @@ class ExtensionsMenu(BaseMenu): def __init__(self): self.extensions = self.discover_extensions() super().__init__( - header=True, + header=False, options=self.get_options(), footer_type=BACK_FOOTER, ) From 0183988d5dedd7f36168f6ac973790a99068797b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 23 Mar 2024 18:40:35 +0100 Subject: [PATCH 137/296] fix(LogUpload): fix bug in menu options Signed-off-by: Dominik Willner --- kiauh/components/log_uploads/menus/log_upload_menu.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index df2c268..fd8f558 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -20,7 +20,7 @@ from utils.constants import RESET_FORMAT, COLOR_YELLOW class LogUploadMenu(BaseMenu): def __init__(self): self.logfile_list = get_logfile_list() - options = {index: self.upload for index in range(len(self.logfile_list))} + options = {f"{index}": self.upload for index in range(len(self.logfile_list))} super().__init__( header=True, options=options, @@ -49,4 +49,5 @@ class LogUploadMenu(BaseMenu): print(menu, end="") def upload(self, **kwargs): - upload_logfile(self.logfile_list[kwargs.get("opt_index")]) + index = int(kwargs.get("opt_index")) + upload_logfile(self.logfile_list[index]) From e3a6d8a0ab4d216dc0d442e42b9cbd5a8b61375e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 23 Mar 2024 21:18:11 +0100 Subject: [PATCH 138/296] README.md: add contributor section Signed-off-by: Dominik Willner --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3f6acc..1c9a6df 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ prompt and confirm by hitting ENTER. -OctoEverywhere Logo +OctoEverywhere Logo OctoEverywhere Logo @@ -174,6 +174,16 @@ prompt and confirm by hitting ENTER.
+

🎖️ Contributors 🎖️

+ + + +
+

✨ Credits ✨

* A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo! From 341ecb325cbd4bb4d852d2a187dd33aecfbb9266 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Mar 2024 00:01:36 +0100 Subject: [PATCH 139/296] refactor(klipper): instance overview dialog can now show printer folder and not only services Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_dialogs.py | 39 +++++++++++++------ kiauh/components/klipper/klipper_remove.py | 2 +- .../components/moonraker/moonraker_remove.py | 2 +- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py index 2cfc672..ada75b6 100644 --- a/kiauh/components/klipper/klipper_dialogs.py +++ b/kiauh/components/klipper/klipper_dialogs.py @@ -8,24 +8,37 @@ # ======================================================================= # import textwrap -from typing import List +from enum import Enum, unique +from typing import List, Union, Literal from core.instance_manager.base_instance import BaseInstance from core.menus.base_menu import print_back_footer from utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN +@unique +class DisplayType(Enum): + SERVICE_NAME = "SERVICE_NAME" + PRINTER_NAME = "PRINTER_NAME" + + def print_instance_overview( - instances: List[BaseInstance], show_index=False, show_select_all=False + instances: List[BaseInstance], + display_type: DisplayType = DisplayType.SERVICE_NAME, + show_headline=True, + show_index=False, + show_select_all=False, ): - headline = f"{COLOR_GREEN}The following Klipper instances were found:{RESET_FORMAT}" - dialog = textwrap.dedent( - f""" - /=======================================================\\ - |{headline:^64}| - |-------------------------------------------------------| - """ - )[1:] + dialog = "/=======================================================\\\n" + if show_headline: + d_type = ( + "Klipper instances" + if display_type is DisplayType.SERVICE_NAME + else "printer directories" + ) + headline = f"{COLOR_GREEN}The following {d_type} were found:{RESET_FORMAT}" + dialog += f"|{headline:^64}|\n" + dialog += "|-------------------------------------------------------|\n" if show_select_all: select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}" @@ -33,7 +46,11 @@ def print_instance_overview( dialog += "| |\n" for i, s in enumerate(instances): - line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {s.get_service_file_name()}{RESET_FORMAT}" + if display_type is DisplayType.SERVICE_NAME: + name = s.get_service_file_name() + else: + name = s.data_dir + line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {name}{RESET_FORMAT}" dialog += f"| {line:<63}|\n" print(dialog, end="") diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index 7245201..2c30e30 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -62,7 +62,7 @@ def run_klipper_removal( def select_instances_to_remove( instances: List[Klipper], ) -> Union[List[Klipper], None]: - print_instance_overview(instances, True, True) + print_instance_overview(instances, show_index=True, show_select_all=True) options = [str(i) for i in range(len(instances))] options.extend(["a", "A", "b", "B"]) diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index 04a9872..7d469fb 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -68,7 +68,7 @@ def run_moonraker_removal( def select_instances_to_remove( instances: List[Moonraker], ) -> Union[List[Moonraker], None]: - print_instance_overview(instances, True, True) + print_instance_overview(instances, show_index=True, show_select_all=True) options = [str(i) for i in range(len(instances))] options.extend(["a", "A", "b", "B"]) From 7104eb078f100eb65641af086088a0b5c5841bf6 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Mar 2024 00:02:10 +0100 Subject: [PATCH 140/296] refactor(RepoManager): if no branch is given, no checkout is done Signed-off-by: Dominik Willner --- kiauh/core/repo_manager/repo_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py index 6a7cfa0..956d6ec 100644 --- a/kiauh/core/repo_manager/repo_manager.py +++ b/kiauh/core/repo_manager/repo_manager.py @@ -24,7 +24,7 @@ class RepoManager: branch: str = None, ): self._repo = repo - self._branch = branch if branch is not None else "master" + self._branch = branch self._method = self._get_method() self._target_dir = target_dir @@ -110,7 +110,7 @@ class RepoManager: if Path(self.target_dir).exists(): question = f"'{self.target_dir}' already exists. Overwrite?" if not get_confirm(question, default_choice=False): - Logger.print_info("Skipping re-clone of repository.") + Logger.print_info("Skip cloning of repository ...") return shutil.rmtree(self.target_dir) @@ -145,6 +145,9 @@ class RepoManager: raise def _checkout(self): + if self.branch is None: + return + try: command = ["git", "checkout", f"{self.branch}"] subprocess.run(command, cwd=self.target_dir, check=True) From 59a83aee129a6dfa2fb5265f561c8639eb4c3e4d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Mar 2024 00:04:29 +0100 Subject: [PATCH 141/296] feat(Mainsail): implement Mainsail Theme-Installer Signed-off-by: Dominik Willner --- .../mainsail_theme_installer/__init__.py | 0 .../mainsail_theme_installer_extension.py | 168 ++++++++++++++++++ .../mainsail_theme_installer/metadata.json | 9 + 3 files changed, 177 insertions(+) create mode 100644 kiauh/extensions/mainsail_theme_installer/__init__.py create mode 100644 kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py create mode 100644 kiauh/extensions/mainsail_theme_installer/metadata.json diff --git a/kiauh/extensions/mainsail_theme_installer/__init__.py b/kiauh/extensions/mainsail_theme_installer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py new file mode 100644 index 0000000..3074c81 --- /dev/null +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -0,0 +1,168 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import csv +import shutil +import textwrap +import urllib.request +from typing import List, Union +from typing import TypedDict + +from components.klipper.klipper import Klipper +from components.klipper.klipper_dialogs import print_instance_overview, DisplayType +from core.base_extension import BaseExtension +from core.instance_manager.base_instance import BaseInstance +from core.instance_manager.instance_manager import InstanceManager +from core.menus import BACK_FOOTER +from core.menus.base_menu import BaseMenu +from core.repo_manager.repo_manager import RepoManager +from utils.constants import COLOR_YELLOW, COLOR_CYAN, RESET_FORMAT +from utils.input_utils import get_selection_input +from utils.logger import Logger + + +class ThemeData(TypedDict): + name: str + short_note: str + author: str + repo: str + + +# noinspection PyMethodMayBeStatic +class MainsailThemeInstallerExtension(BaseExtension): + im = InstanceManager(Klipper) + instances: List[Klipper] = im.instances + + def install_extension(self, **kwargs) -> None: + install_menu = MainsailThemeInstallMenu(self.instances) + install_menu.start() + + def remove_extension(self, **kwargs) -> None: + print_instance_overview( + self.instances, + display_type=DisplayType.PRINTER_NAME, + show_headline=True, + show_index=True, + show_select_all=True, + ) + printer_list = get_printer_selection(self.instances, True) + if printer_list is None: + return + + for printer in printer_list: + Logger.print_status(f"Uninstalling theme from {printer.cfg_dir} ...") + theme_dir = printer.cfg_dir.joinpath(".theme") + if not theme_dir.exists(): + Logger.print_info(f"{theme_dir} not found. Skipping ...") + continue + try: + shutil.rmtree(theme_dir) + Logger.print_ok("Theme successfully uninstalled!") + except OSError as e: + Logger.print_error("Unable to uninstall theme") + Logger.print_error(e) + + +# noinspection PyMethodMayBeStatic +class MainsailThemeInstallMenu(BaseMenu): + THEMES_URL: str = ( + "https://raw.githubusercontent.com/mainsail-crew/gb-docs/main/_data/themes.csv" + ) + + def __init__(self, instances: List[Klipper]): + self.instances = instances + self.themes: List[ThemeData] = self.load_themes() + options = {f"{index}": self.install_theme for index in range(len(self.themes))} + super().__init__( + header=False, + options=options, + footer_type=BACK_FOOTER, + ) + + def print_menu(self) -> None: + header = " [ Mainsail Theme Installer ] " + color = COLOR_YELLOW + line1 = f"{COLOR_CYAN}A preview of each Mainsail theme can be found here:{RESET_FORMAT}" + count = 62 - len(color) - len(RESET_FORMAT) + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {line1:<62} | + | https://docs.mainsail.xyz/theming/themes | + |-------------------------------------------------------| + """ + )[1:] + for i, theme in enumerate(self.themes): + i = f" {i}" if i < 10 else f"{i}" + row = f"{i}) [{theme.get('name')}]" + menu += f"| {row:<53} |\n" + print(menu, end="") + + def load_themes(self) -> List[ThemeData]: + with urllib.request.urlopen(self.THEMES_URL) as response: + themes: List[ThemeData] = [] + csv_data: str = response.read().decode().splitlines() + csv_reader = csv.DictReader(csv_data, delimiter=",") + for row in csv_reader: + row: ThemeData = row + themes.append(row) + + return themes + + def install_theme(self, **kwargs): + index = int(kwargs.get("opt_index")) + theme_data: ThemeData = self.themes[index] + theme_author: str = theme_data.get("author") + theme_repo: str = theme_data.get("repo") + theme_repo_url: str = f"https://github.com/{theme_author}/{theme_repo}" + + print_instance_overview( + self.instances, + display_type=DisplayType.PRINTER_NAME, + show_headline=True, + show_index=True, + show_select_all=True, + ) + + printer_list = get_printer_selection(self.instances, True) + if printer_list is None: + return + + repo_manager = RepoManager(theme_repo_url, "") + for printer in printer_list: + repo_manager.target_dir = printer.cfg_dir.joinpath(".theme") + repo_manager.clone_repo() + + if len(theme_data.get("short_note", "")) > 1: + Logger.print_warn("Info from the creator:", prefix=False, start="\n") + Logger.print_info(theme_data.get("short_note"), prefix=False, end="\n\n") + + +def get_printer_selection(instances: List[BaseInstance], is_install: bool) -> Union[List[BaseInstance], None]: + options = [str(i) for i in range(len(instances))] + options.extend(["a", "A", "b", "B"]) + + if is_install: + q = "Select the printer to install the theme for" + else: + q = "Select the printer to remove the theme from" + selection = get_selection_input(q, options) + + install_for = [] + if selection == "b".lower(): + return None + elif selection == "a".lower(): + install_for.extend(instances) + else: + instance = instances[int(selection)] + install_for.append(instance) + + return install_for diff --git a/kiauh/extensions/mainsail_theme_installer/metadata.json b/kiauh/extensions/mainsail_theme_installer/metadata.json new file mode 100644 index 0000000..05e22d3 --- /dev/null +++ b/kiauh/extensions/mainsail_theme_installer/metadata.json @@ -0,0 +1,9 @@ +{ + "metadata": { + "index": 2, + "module": "mainsail_theme_installer_extension", + "maintained_by": "dw-0", + "display_name": "Mainsail Theme Installer", + "description": "Install Mainsail Themes maintained by the community." + } +} From 58719a4ca0a75049ac5d44f1ed4c49499dd98ec8 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Mar 2024 00:28:28 +0100 Subject: [PATCH 142/296] chore: fix lint issues Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_dialogs.py | 2 +- .../log_uploads/log_upload_utils.py | 2 +- kiauh/components/webui_client/__init__.py | 2 +- .../core/instance_manager/instance_manager.py | 22 +++++++++---------- kiauh/utils/filesystem_utils.py | 8 +++---- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py index ada75b6..9964f66 100644 --- a/kiauh/components/klipper/klipper_dialogs.py +++ b/kiauh/components/klipper/klipper_dialogs.py @@ -9,7 +9,7 @@ import textwrap from enum import Enum, unique -from typing import List, Union, Literal +from typing import List from core.instance_manager.base_instance import BaseInstance from core.menus.base_menu import print_back_footer diff --git a/kiauh/components/log_uploads/log_upload_utils.py b/kiauh/components/log_uploads/log_upload_utils.py index 3a8029a..e54ca3a 100644 --- a/kiauh/components/log_uploads/log_upload_utils.py +++ b/kiauh/components/log_uploads/log_upload_utils.py @@ -50,5 +50,5 @@ def upload_logfile(logfile: LogFile) -> None: Logger.print_ok("Upload successful! Access it via the following link:") Logger.print_ok(f">>>> {link}", False) except Exception as e: - Logger.print_error(f"Uploading logfile failed!") + Logger.print_error("Uploading logfile failed!") Logger.print_error(str(e)) diff --git a/kiauh/components/webui_client/__init__.py b/kiauh/components/webui_client/__init__.py index 6f21f4a..bb87c0e 100644 --- a/kiauh/components/webui_client/__init__.py +++ b/kiauh/components/webui_client/__init__.py @@ -8,7 +8,7 @@ # ======================================================================= # from pathlib import Path -from typing import Literal, TypedDict, Set +from typing import Literal, TypedDict from core.backup_manager import BACKUP_ROOT_DIR diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 0a5750b..07ecdd9 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -16,34 +16,34 @@ from core.instance_manager.base_instance import BaseInstance from utils.constants import SYSTEMD from utils.logger import Logger -I = TypeVar(name="I", bound=BaseInstance, covariant=True) +T = TypeVar(name="T", bound=BaseInstance, covariant=True) # noinspection PyMethodMayBeStatic class InstanceManager: - def __init__(self, instance_type: I) -> None: + def __init__(self, instance_type: T) -> None: self._instance_type = instance_type - self._current_instance: Optional[I] = None + self._current_instance: Optional[T] = None self._instance_suffix: Optional[str] = None self._instance_service: Optional[str] = None self._instance_service_full: Optional[str] = None self._instance_service_path: Optional[str] = None - self._instances: List[I] = [] + self._instances: List[T] = [] @property - def instance_type(self) -> I: + def instance_type(self) -> T: return self._instance_type @instance_type.setter - def instance_type(self, value: I): + def instance_type(self, value: T): self._instance_type = value @property - def current_instance(self) -> I: + def current_instance(self) -> T: return self._current_instance @current_instance.setter - def current_instance(self, value: I) -> None: + def current_instance(self, value: T) -> None: self._current_instance = value self.instance_suffix = value.suffix self.instance_service = value.get_service_file_name() @@ -78,11 +78,11 @@ class InstanceManager: self._instance_service_path = value @property - def instances(self) -> List[I]: + def instances(self) -> List[T]: return self.find_instances() @instances.setter - def instances(self, value: List[I]): + def instances(self, value: List[T]): self._instances = value def create_instance(self) -> None: @@ -182,7 +182,7 @@ class InstanceManager: Logger.print_error(f"{e}") raise - def find_instances(self) -> List[I]: + def find_instances(self) -> List[T]: name = self.instance_type.__name__.lower() pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.service$") excluded = self.instance_type.blacklist() diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 0406bc2..86306bd 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import os # ======================================================================= # # Copyright (C) 2020 - 2024 Dominik Willner # @@ -209,11 +208,10 @@ def add_config_section( cm.write_config() -def add_config_section_at_top( - section: str, - instances: List[B]): + +def add_config_section_at_top(section: str, instances: List[B]): for instance in instances: - tmp_cfg = tempfile.NamedTemporaryFile(mode="w" ,delete=False) + tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False) tmp_cfg_path = Path(tmp_cfg.name) cmt = ConfigManager(tmp_cfg_path) cmt.config.add_section(section) From e64aa94df48f20903e8e666e03914297ce015808 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Mar 2024 00:33:09 +0100 Subject: [PATCH 143/296] chore: format Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 14 +++++--- .../components/moonraker/moonraker_remove.py | 10 ++++-- kiauh/components/moonraker/moonraker_setup.py | 10 ++++-- kiauh/components/moonraker/moonraker_utils.py | 10 +++--- .../client_config/client_config_setup.py | 9 +++-- kiauh/components/webui_client/client_utils.py | 4 ++- .../core/instance_manager/instance_manager.py | 28 ++++++++++++--- kiauh/core/menus/main_menu.py | 6 +++- kiauh/core/menus/remove_menu.py | 4 ++- .../gcode_shell_cmd_extension.py | 4 ++- .../mainsail_theme_installer_extension.py | 13 ++++--- kiauh/utils/input_utils.py | 6 +++- kiauh/utils/system_utils.py | 12 +++++-- resources/gcode_shell_command.py | 35 +++++++++++-------- 14 files changed, 120 insertions(+), 45 deletions(-) diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index e71cb3b..51dd8bd 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -48,10 +48,12 @@ from utils.logger import Logger from utils.system_utils import mask_system_service -def get_klipper_status() -> Dict[ - Literal["status", "status_code", "instances", "repo", "local", "remote"], - Union[str, int], -]: +def get_klipper_status() -> ( + Dict[ + Literal["status", "status_code", "instances", "repo", "local", "remote"], + Union[str, int], + ] +): status = get_install_status_common(Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR) return { "status": status.get("status"), @@ -69,7 +71,9 @@ def check_is_multi_install( return not existing_instances and install_count > 1 -def check_is_single_to_multi_conversion(existing_instances: List[Klipper]) -> bool: +def check_is_single_to_multi_conversion( + existing_instances: List[Klipper], +) -> bool: return len(existing_instances) == 1 and existing_instances[0].suffix == "" diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index 7d469fb..dde6c0e 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -130,9 +130,15 @@ def remove_polkit_rules() -> None: return try: - command = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"] + command = [ + f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", + "--clear", + ] subprocess.run( - command, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, check=True + command, + stderr=subprocess.PIPE, + stdout=subprocess.DEVNULL, + check=True, ) except subprocess.CalledProcessError as e: Logger.print_error(f"Error while removing policykit rules: {e}") diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index d55fd63..ef8c5f6 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -12,7 +12,10 @@ import sys from pathlib import Path from components.webui_client import MAINSAIL_DIR -from components.webui_client.client_utils import enable_mainsail_remotemode, get_existing_clients +from components.webui_client.client_utils import ( + enable_mainsail_remotemode, + get_existing_clients, +) from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper from components.moonraker import ( @@ -170,7 +173,10 @@ def install_moonraker_polkit() -> None: try: command = [POLKIT_SCRIPT, "--disable-systemctl"] result = subprocess.run( - command, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True + command, + stderr=subprocess.PIPE, + stdout=subprocess.DEVNULL, + text=True, ) if result.returncode != 0 or result.stderr: Logger.print_error(f"{result.stderr}", False) diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index ca1a15c..f8be7be 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -32,10 +32,12 @@ from utils.system_utils import ( ) -def get_moonraker_status() -> Dict[ - Literal["status", "status_code", "instances", "repo", "local", "remote"], - Union[str, int], -]: +def get_moonraker_status() -> ( + Dict[ + Literal["status", "status_code", "instances", "repo", "local", "remote"], + Union[str, int], + ] +): status = get_install_status_common(Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR) return { "status": status.get("status"), diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 02f25a2..58d2f3b 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -16,7 +16,9 @@ from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker from components.webui_client import ClientConfigData, ClientName, ClientData -from components.webui_client.client_dialogs import print_client_already_installed_dialog +from components.webui_client.client_dialogs import ( + print_client_already_installed_dialog, +) from components.webui_client.client_utils import ( load_client_data, backup_client_config_data, @@ -29,8 +31,9 @@ from core.repo_manager.repo_manager import RepoManager from utils.common import backup_printer_config_dir from utils.filesystem_utils import ( create_symlink, - add_config_section, add_config_section_at_top, - ) + add_config_section, + add_config_section_at_top, +) from utils.input_utils import get_confirm from utils.logger import Logger diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index d2c4e1f..23d34e3 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -112,7 +112,9 @@ def get_client_status(client: ClientData) -> str: ) -def get_client_config_status(client: ClientData) -> Dict[ +def get_client_config_status( + client: ClientData, +) -> Dict[ Literal["repo", "local", "remote"], Union[str, int], ]: diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 07ecdd9..0879484 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -108,7 +108,12 @@ class InstanceManager: def enable_instance(self) -> None: Logger.print_status(f"Enabling {self.instance_service_full} ...") try: - command = ["sudo", "systemctl", "enable", self.instance_service_full] + command = [ + "sudo", + "systemctl", + "enable", + self.instance_service_full, + ] if subprocess.run(command, check=True): Logger.print_ok(f"{self.instance_service_full} enabled.") except subprocess.CalledProcessError as e: @@ -118,7 +123,12 @@ class InstanceManager: def disable_instance(self) -> None: Logger.print_status(f"Disabling {self.instance_service_full} ...") try: - command = ["sudo", "systemctl", "disable", self.instance_service_full] + command = [ + "sudo", + "systemctl", + "disable", + self.instance_service_full, + ] if subprocess.run(command, check=True): Logger.print_ok(f"{self.instance_service_full} disabled.") except subprocess.CalledProcessError as e: @@ -128,7 +138,12 @@ class InstanceManager: def start_instance(self) -> None: Logger.print_status(f"Starting {self.instance_service_full} ...") try: - command = ["sudo", "systemctl", "start", self.instance_service_full] + command = [ + "sudo", + "systemctl", + "start", + self.instance_service_full, + ] if subprocess.run(command, check=True): Logger.print_ok(f"{self.instance_service_full} started.") except subprocess.CalledProcessError as e: @@ -138,7 +153,12 @@ class InstanceManager: def restart_instance(self) -> None: Logger.print_status(f"Restarting {self.instance_service_full} ...") try: - command = ["sudo", "systemctl", "restart", self.instance_service_full] + command = [ + "sudo", + "systemctl", + "restart", + self.instance_service_full, + ] if subprocess.run(command, check=True): Logger.print_ok(f"{self.instance_service_full} restarted.") except subprocess.CalledProcessError as e: diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 8d20036..5d017e3 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -68,7 +68,11 @@ class MainMenu(BaseMenu): def init_status(self) -> None: status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn"] for var in status_vars: - setattr(self, f"{var}_status", f"{COLOR_RED}Not installed!{RESET_FORMAT}") + setattr( + self, + f"{var}_status", + f"{COLOR_RED}Not installed!{RESET_FORMAT}", + ) def fetch_status(self) -> None: # klipper diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 8c13efe..a7b61a4 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -10,7 +10,9 @@ import textwrap from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu -from components.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu +from components.moonraker.menus.moonraker_remove_menu import ( + MoonrakerRemoveMenu, +) from components.webui_client.client_utils import load_client_data from components.webui_client.menus.client_remove_menu import ClientRemoveMenu from core.menus import BACK_FOOTER diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index e726565..a18488f 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -44,7 +44,9 @@ class GcodeShellCmdExtension(BaseExtension): overwrite = True if extension_installed: overwrite = get_confirm( - "Extension seems to be installed already. Overwrite?", True, False + "Extension seems to be installed already. Overwrite?", + True, + False, ) if not overwrite: diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index 3074c81..546b6ac 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -15,7 +15,10 @@ from typing import List, Union from typing import TypedDict from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import print_instance_overview, DisplayType +from components.klipper.klipper_dialogs import ( + print_instance_overview, + DisplayType, +) from core.base_extension import BaseExtension from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager @@ -50,7 +53,7 @@ class MainsailThemeInstallerExtension(BaseExtension): show_headline=True, show_index=True, show_select_all=True, - ) + ) printer_list = get_printer_selection(self.instances, True) if printer_list is None: return @@ -130,7 +133,7 @@ class MainsailThemeInstallMenu(BaseMenu): show_headline=True, show_index=True, show_select_all=True, - ) + ) printer_list = get_printer_selection(self.instances, True) if printer_list is None: @@ -146,7 +149,9 @@ class MainsailThemeInstallMenu(BaseMenu): Logger.print_info(theme_data.get("short_note"), prefix=False, end="\n\n") -def get_printer_selection(instances: List[BaseInstance], is_install: bool) -> Union[List[BaseInstance], None]: +def get_printer_selection( + instances: List[BaseInstance], is_install: bool +) -> Union[List[BaseInstance], None]: options = [str(i) for i in range(len(instances))] options.extend(["a", "A", "b", "B"]) diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 25a1b71..88b9188 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -51,7 +51,11 @@ def get_confirm( def get_number_input( - question: str, min_count: int, max_count=None, default=None, allow_go_back=False + question: str, + min_count: int, + max_count=None, + default=None, + allow_go_back=False, ) -> Union[int, None]: """ Helper method to get a number input from the user diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 3b46913..991e65e 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -130,7 +130,12 @@ def install_python_requirements(target: Path, requirements: Path) -> None: update_python_pip(target) Logger.print_status("Installing Python requirements ...") - command = [target.joinpath("bin/pip"), "install", "-r", f"{requirements}"] + command = [ + target.joinpath("bin/pip"), + "install", + "-r", + f"{requirements}", + ] result = subprocess.run(command, stderr=subprocess.PIPE, text=True) if result.returncode != 0 or result.stderr: @@ -196,7 +201,10 @@ def check_package_install(packages: List[str]) -> List[str]: for package in packages: command = ["dpkg-query", "-f'${Status}'", "--show", package] result = subprocess.run( - command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True + command, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, ) if "installed" not in result.stdout.strip("'").split(): not_installed.append(package) diff --git a/resources/gcode_shell_command.py b/resources/gcode_shell_command.py index bb38ae5..3f316e6 100755 --- a/resources/gcode_shell_command.py +++ b/resources/gcode_shell_command.py @@ -8,22 +8,26 @@ import shlex import subprocess import logging + class ShellCommand: def __init__(self, config): self.name = config.get_name().split()[-1] self.printer = config.get_printer() - self.gcode = self.printer.lookup_object('gcode') - cmd = config.get('command') + self.gcode = self.printer.lookup_object("gcode") + cmd = config.get("command") cmd = os.path.expanduser(cmd) self.command = shlex.split(cmd) - self.timeout = config.getfloat('timeout', 2., above=0.) - self.verbose = config.getboolean('verbose', True) + self.timeout = config.getfloat("timeout", 2.0, above=0.0) + self.verbose = config.getboolean("verbose", True) self.proc_fd = None self.partial_output = "" self.gcode.register_mux_command( - "RUN_SHELL_COMMAND", "CMD", self.name, + "RUN_SHELL_COMMAND", + "CMD", + self.name, self.cmd_RUN_SHELL_COMMAND, - desc=self.cmd_RUN_SHELL_COMMAND_help) + desc=self.cmd_RUN_SHELL_COMMAND_help, + ) def _process_output(self, eventime): if self.proc_fd is None: @@ -33,11 +37,11 @@ class ShellCommand: except Exception: pass data = self.partial_output + data.decode() - if '\n' not in data: + if "\n" not in data: self.partial_output = data return - elif data[-1] != '\n': - split = data.rfind('\n') + 1 + elif data[-1] != "\n": + split = data.rfind("\n") + 1 self.partial_output = data[split:] data = data[:split] else: @@ -45,16 +49,19 @@ class ShellCommand: self.gcode.respond_info(data) cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command" + def cmd_RUN_SHELL_COMMAND(self, params): - gcode_params = params.get('PARAMS','') + gcode_params = params.get("PARAMS", "") gcode_params = shlex.split(gcode_params) reactor = self.printer.get_reactor() try: proc = subprocess.Popen( - self.command + gcode_params, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + self.command + gcode_params, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) except Exception: - logging.exception( - "shell_command: Command {%s} failed" % (self.name)) + logging.exception("shell_command: Command {%s} failed" % (self.name)) raise self.gcode.error("Error running command {%s}" % (self.name)) if self.verbose: self.proc_fd = proc.stdout.fileno() @@ -64,7 +71,7 @@ class ShellCommand: endtime = eventtime + self.timeout complete = False while eventtime < endtime: - eventtime = reactor.pause(eventtime + .05) + eventtime = reactor.pause(eventtime + 0.05) if proc.poll() is not None: complete = True break From 72e3a56e4f2897c791b4bdf6937be2db8122de0f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 24 Mar 2024 01:06:03 +0100 Subject: [PATCH 144/296] chore: replace black with ruff Signed-off-by: Dominik Willner --- pyproject.toml | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index be1fddf..81a968d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,16 @@ -[tool.black] +[project] +requires-python = ">=3.8" + +[tool.ruff] +required-version = ">=0.3.4" +respect-gitignore = true +exclude = [".git",".github", "./docs"] line-length = 88 -target-version = ['py38'] -include = '\.pyi?$' -exclude = ''' -( - \.git/ - | \.github/ - | docs/ - | resources/ - | scripts/ -) -''' +indent-width = 4 +output-format = "full" + +[tool.ruff.format] +indent-style = "space" +line-ending = "lf" +quote-style = "double" + From fef8b58510d5edc921e25132fdb35b3bf68146f6 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 27 Mar 2024 20:42:59 +0100 Subject: [PATCH 145/296] refactor: help menus need to be an option now Signed-off-by: Dominik Willner --- kiauh/core/menus/base_menu.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 8a27978..90ae959 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -153,8 +153,6 @@ class BaseMenu(ABC): sys.exit(0) elif choice == "b": return - elif choice == "h": - print("help!") else: self.execute_option(choice) From 78dbf3157683764e2d2ecb191a6b10b2e212386e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 27 Mar 2024 20:43:59 +0100 Subject: [PATCH 146/296] refactor: update advanced menu layout Signed-off-by: Dominik Willner --- kiauh/core/menus/advanced_menu.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 30f783a..b76a930 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -9,6 +9,7 @@ import textwrap +from components.klipper_firmware.menus.klipper_flash_menu import KlipperFlashMenu from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import COLOR_YELLOW, RESET_FORMAT @@ -16,7 +17,18 @@ from utils.constants import COLOR_YELLOW, RESET_FORMAT class AdvancedMenu(BaseMenu): def __init__(self): - super().__init__(header=True, options={}, footer_type=BACK_FOOTER) + super().__init__( + header=True, + options={ + "1": None, + "2": None, + "3": None, + "4": KlipperFlashMenu, + "5": None, + "6": None, + }, + footer_type=BACK_FOOTER, + ) def print_menu(self): header = " [ Advanced Menu ] " @@ -35,10 +47,7 @@ class AdvancedMenu(BaseMenu): | 3) [Build] | | 4) [Flash] | | 5) [Build + Flash] | - | 6) [Get MCU ID] | - | | - | Mainsail: | - | 7) [Theme installer] | + | 6) [Get MCU ID] | """ )[1:] print(menu, end="") From ebdfadac07ea4dd309cb1cd41a6c6401dab69f36 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 29 Mar 2024 20:35:00 +0100 Subject: [PATCH 147/296] feat: allow custom input label text in menus Signed-off-by: Dominik Willner --- kiauh/core/menus/base_menu.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 90ae959..78c31cc 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -7,6 +7,8 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations + import subprocess import sys import textwrap @@ -97,14 +99,19 @@ class BaseMenu(ABC): self, options: Dict[str, Union[Callable, Any]], options_offset: int = 0, + default_option: Union[str, None] = None, + input_label_txt: Union[str, None] = None, header: bool = True, + previous_menu: BaseMenu = None, footer_type: Literal[ "QUIT_FOOTER", "BACK_FOOTER", "BACK_HELP_FOOTER" ] = QUIT_FOOTER, ): - self.previous_menu = None + self.previous_menu = previous_menu self.options = options + self.default_option = default_option self.options_offset = options_offset + self.input_label_txt = input_label_txt self.header = header self.footer_type = footer_type @@ -130,7 +137,12 @@ class BaseMenu(ABC): def handle_user_input(self) -> str: while True: - choice = input(f"{COLOR_CYAN}###### Perform action: {RESET_FORMAT}").lower() + label_text = ( + "Perform action" + if self.input_label_txt is None or self.input_label_txt == "" + else self.input_label_txt + ) + choice = input(f"{COLOR_CYAN}###### {label_text}: {RESET_FORMAT}").lower() option = self.options.get(choice, None) has_navi_option = self.footer_type in self.NAVI_OPTIONS @@ -140,6 +152,8 @@ class BaseMenu(ABC): if option is not None: return choice + elif option is None and self.default_option is not None: + return self.default_option else: Logger.print_error("Invalid input!", False) From aaf52162757d81f17142df6899297ac3664bfcbd Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 29 Mar 2024 21:22:27 +0100 Subject: [PATCH 148/296] refactor: remove unnecessary call to get_logfile_list Signed-off-by: Dominik Willner --- kiauh/components/log_uploads/menus/log_upload_menu.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index fd8f558..f477594 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -41,8 +41,7 @@ class LogUploadMenu(BaseMenu): """ )[1:] - logfile_list = get_logfile_list() - for logfile in enumerate(logfile_list): + for logfile in enumerate(self.logfile_list): line = f"{logfile[0]}) {logfile[1].get('display_name')}" menu += f"| {line:<54}|\n" From dc87d30770d4c543ebabfd4b54f268be07af86a5 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 30 Mar 2024 14:33:11 +0100 Subject: [PATCH 149/296] feat: first implementation of firmware flashing via usb and regular flash command Signed-off-by: Dominik Willner --- kiauh/components/klipper_firmware/__init__.py | 0 .../klipper_firmware/flash_options.py | 48 +++ .../klipper_firmware/flash_utils.py | 77 ++++ .../menus/klipper_flash_menu.py | 381 ++++++++++++++++++ kiauh/core/menus/advanced_menu.py | 4 +- kiauh/utils/system_utils.py | 61 ++- 6 files changed, 550 insertions(+), 21 deletions(-) create mode 100644 kiauh/components/klipper_firmware/__init__.py create mode 100644 kiauh/components/klipper_firmware/flash_options.py create mode 100644 kiauh/components/klipper_firmware/flash_utils.py create mode 100644 kiauh/components/klipper_firmware/menus/klipper_flash_menu.py diff --git a/kiauh/components/klipper_firmware/__init__.py b/kiauh/components/klipper_firmware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/components/klipper_firmware/flash_options.py b/kiauh/components/klipper_firmware/flash_options.py new file mode 100644 index 0000000..814e41b --- /dev/null +++ b/kiauh/components/klipper_firmware/flash_options.py @@ -0,0 +1,48 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 dataclasses import field, dataclass +from enum import Enum +from typing import Union, List + + +class FlashMethod(Enum): + REGULAR = "REGULAR" + SD_CARD = "SD_CARD" + + +class FlashCommand(Enum): + FLASH = "flash" + SERIAL_FLASH = "serialflash" + + +class ConnectionType(Enum): + USB = "USB" + USB_DFU = "USB_DFU" + UART = "UART" + + +@dataclass +class FlashOptions: + _instance = None + flash_method: Union[FlashMethod, None] = None + flash_command: Union[FlashCommand, None] = None + connection_type: Union[ConnectionType, None] = None + mcu_list: List[str] = field(default_factory=list) + selected_mcu: str = "" + selected_board: str = "" + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(FlashOptions, cls).__new__(cls, *args, **kwargs) + return cls._instance + + @classmethod + def destroy(cls): + cls._instance = None diff --git a/kiauh/components/klipper_firmware/flash_utils.py b/kiauh/components/klipper_firmware/flash_utils.py new file mode 100644 index 0000000..75b6610 --- /dev/null +++ b/kiauh/components/klipper_firmware/flash_utils.py @@ -0,0 +1,77 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 subprocess import CalledProcessError, check_output, Popen, PIPE, STDOUT +from typing import List + +from components.klipper import KLIPPER_DIR +from components.klipper_firmware.flash_options import FlashOptions, FlashCommand +from utils.logger import Logger +from utils.system_utils import log_process + + +def find_usb_device_by_id() -> List[str]: + try: + command = "find /dev/serial/by-id/* 2>/dev/null" + output = check_output(command, shell=True, text=True) + return output.splitlines() + except CalledProcessError as e: + Logger.print_error("Unable to find a USB device!") + Logger.print_error(e, prefix=False) + return [] + + +def find_uart_device() -> List[str]: + try: + command = '"find /dev -maxdepth 1 -regextype posix-extended -regex "^\/dev\/tty(AMA0|S0)$" 2>/dev/null"' + output = check_output(command, shell=True, text=True) + return output.splitlines() + except CalledProcessError as e: + Logger.print_error("Unable to find a UART device!") + Logger.print_error(e, prefix=False) + return [] + + +def find_usb_dfu_device() -> List[str]: + try: + command = '"lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null"' + output = check_output(command, shell=True, text=True) + return output.splitlines() + except CalledProcessError as e: + Logger.print_error("Unable to find a USB DFU device!") + Logger.print_error(e, prefix=False) + return [] + + +def flash_device(flash_options: FlashOptions) -> None: + try: + if not flash_options.selected_mcu: + raise Exception("Missing value for selected_mcu!") + + if flash_options.flash_command is FlashCommand.FLASH: + command = [ + "make", + flash_options.flash_command.value, + f"FLASH_DEVICE={flash_options.selected_mcu}", + ] + process = Popen( + command, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True + ) + + log_process(process) + + rc = process.returncode + if rc != 0: + raise Exception(f"Flashing failed with returncode: {rc}") + else: + Logger.print_ok("Flashing successfull!", start="\n", end="\n\n") + + except (Exception, CalledProcessError): + Logger.print_error("Flashing failed!", start="\n") + Logger.print_error("See the console output above!", end="\n\n") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py new file mode 100644 index 0000000..afad1b6 --- /dev/null +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -0,0 +1,381 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import textwrap + +from components.klipper_firmware.flash_options import ( + FlashOptions, + FlashMethod, + FlashCommand, + ConnectionType, +) +from components.klipper_firmware.flash_utils import ( + find_usb_device_by_id, + find_uart_device, + find_usb_dfu_device, + flash_device, +) +from core.menus import BACK_HELP_FOOTER, BACK_FOOTER + +from core.menus.base_menu import BaseMenu +from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_RED +from utils.input_utils import get_confirm +from utils.logger import Logger + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperFlashMethodMenu(BaseMenu): + def __init__(self): + self.flash_options = FlashOptions() + super().__init__( + header=False, + options={ + "1": self.select_regular, + "2": self.select_sdcard, + "h": KlipperFlashMethodHelpMenu, + }, + input_label_txt="Select flash method", + footer_type=BACK_HELP_FOOTER, + ) + + def print_menu(self) -> None: + header = " [ Flash MCU ] " + color = COLOR_CYAN + count = 62 - len(color) - len(RESET_FORMAT) + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Please select the flashing method to flash your MCU. | + | Make sure to only select a method your MCU supports. | + | Not all MCUs support both methods! | + |-------------------------------------------------------| + | | + | 1) Regular flashing method | + | 2) Updating via SD-Card Update | + | | + """ + )[1:] + print(menu, end="") + + def select_regular(self, **kwargs): + self.flash_options.flash_method = FlashMethod.REGULAR + self.goto_next_menu() + + def select_sdcard(self, **kwargs): + self.flash_options.flash_method = FlashMethod.SD_CARD + self.goto_next_menu() + + def goto_next_menu(self, **kwargs): + next_menu = KlipperFlashCommandMenu(previous_menu=self) + next_menu.start() + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperFlashCommandMenu(BaseMenu): + def __init__(self, previous_menu: BaseMenu): + self.flash_options = FlashOptions() + super().__init__( + header=False, + options={ + "1": self.select_flash, + "2": self.select_serialflash, + "h": KlipperFlashCommandHelpMenu, + }, + default_option="1", + input_label_txt="Select flash command", + previous_menu=previous_menu, + footer_type=BACK_HELP_FOOTER, + ) + + def print_menu(self) -> None: + menu = textwrap.dedent( + """ + /=======================================================\\ + | | + | Which flash command to use for flashing the MCU? | + | 1) make flash (default) | + | 2) make serialflash (stm32flash) | + | | + """ + )[1:] + print(menu, end="") + + def select_flash(self, **kwargs): + self.flash_options.flash_command = FlashCommand.FLASH + self.goto_next_menu() + + def select_serialflash(self, **kwargs): + self.flash_options.flash_command = FlashCommand.SERIAL_FLASH + self.goto_next_menu() + + def goto_next_menu(self, **kwargs): + next_menu = KlipperSelectMcuConnectionMenu(previous_menu=self) + next_menu.start() + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperSelectMcuConnectionMenu(BaseMenu): + def __init__(self, previous_menu: BaseMenu): + self.flash_options = FlashOptions() + super().__init__( + header=False, + options={ + "1": self.select_usb, + "2": self.select_dfu, + "3": self.select_usb_dfu, + "h": KlipperMcuConnectionHelpMenu, + }, + input_label_txt="Select connection type", + previous_menu=previous_menu, + footer_type=BACK_HELP_FOOTER, + ) + + def print_menu(self) -> None: + header = "Make sure that the controller board is connected now!" + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | | + | How is the controller board connected to the host? | + | 1) USB | + | 2) UART | + | 3) USB (DFU mode) | + | | + """ + )[1:] + print(menu, end="") + + def select_usb(self, **kwargs): + self.flash_options.connection_type = ConnectionType.USB + self.get_mcu_list() + + def select_dfu(self, **kwargs): + self.flash_options.connection_type = ConnectionType.UART + self.get_mcu_list() + + def select_usb_dfu(self, **kwargs): + self.flash_options.connection_type = ConnectionType.USB_DFU + self.get_mcu_list() + + def get_mcu_list(self, **kwargs): + conn_type = self.flash_options.connection_type + + if conn_type is ConnectionType.USB: + Logger.print_status("Identifying MCU connected via USB ...") + self.flash_options.mcu_list = find_usb_device_by_id() + elif conn_type is ConnectionType.UART: + Logger.print_status("Identifying MCU possibly connected via UART ...") + self.flash_options.mcu_list = find_uart_device() + elif conn_type is ConnectionType.USB_DFU: + Logger.print_status("Identifying MCU connected via USB in DFU mode ...") + self.flash_options.mcu_list = find_usb_dfu_device() + + if len(self.flash_options.mcu_list) < 1: + Logger.print_warn("No MCUs found!") + Logger.print_warn("Make sure they are connected and repeat this step.") + else: + self.goto_next_menu() + + def goto_next_menu(self, **kwargs): + next_menu = KlipperSelectMcuIdMenu(previous_menu=self) + next_menu.start() + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperSelectMcuIdMenu(BaseMenu): + def __init__(self, previous_menu: BaseMenu): + self.flash_options = FlashOptions() + self.mcu_list = self.flash_options.mcu_list + options = {f"{index}": self.flash_mcu for index in range(len(self.mcu_list))} + super().__init__( + header=False, + options=options, + input_label_txt="Select MCU to flash", + previous_menu=previous_menu, + footer_type=BACK_FOOTER, + ) + + def print_menu(self) -> None: + header = "!!! ATTENTION !!!" + header2 = f"[{COLOR_CYAN}List of available MCUs{RESET_FORMAT}]" + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Make sure, to select the correct MCU! | + | ONLY flash a firmware created for the respective MCU! | + | | + |{header2:-^64}| + + """ + )[1:] + + for i, mcu in enumerate(self.mcu_list): + mcu = mcu.split("/")[-1] + menu += f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n" + + print(menu, end="\n") + + def flash_mcu(self, **kwargs): + index = int(kwargs.get("opt_index")) + selected_mcu = self.mcu_list[index] + self.flash_options.selected_mcu = selected_mcu + + print(f"{COLOR_CYAN}###### You selected:{RESET_FORMAT}") + print(f"● MCU #{index}: {selected_mcu}\n") + + if get_confirm("Continue", allow_go_back=True): + Logger.print_status(f"Flashing '{selected_mcu}' ...") + flash_device(self.flash_options) + + self.goto_next_menu() + + def goto_next_menu(self, **kwargs): + pass + # TODO: navigate back to advanced menu after flashing + + # from core.menus.main_menu import MainMenu + # from core.menus.advanced_menu import AdvancedMenu + # + # next_menu = AdvancedMenu() + # next_menu.start() + + +class KlipperFlashMethodHelpMenu(BaseMenu): + def __init__(self): + super().__init__( + header=False, + options={}, + footer_type=BACK_FOOTER, + ) + + def print_menu(self) -> None: + header = " < ? > Help: Flash MCU < ? > " + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) + subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}" + subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}" + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {subheader1:<62} | + | The default method to flash controller boards which | + | are connected and updated over USB and not by placing | + | a compiled firmware file onto an internal SD-Card. | + | | + | Common controllers that get flashed that way are: | + | - Arduino Mega 2560 | + | - Fysetc F6 / S6 (used without a Display + SD-Slot) | + | | + | {subheader2:<62} | + | Many popular controller boards ship with a bootloader | + | capable of updating the firmware via SD-Card. | + | Choose this method if your controller board supports | + | this way of updating. This method ONLY works for up- | + | grading firmware. The initial flashing procedure must | + | be done manually per the instructions that apply to | + | your controller board. | + | | + | Common controllers that can be flashed that way are: | + | - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 | + | - Fysetc F6 / S6 (used with a Display + SD-Slot) | + | - Fysetc Spider | + | | + """ + )[1:] + print(menu, end="") + + +class KlipperFlashCommandHelpMenu(BaseMenu): + def __init__(self): + super().__init__( + header=False, + options={}, + footer_type=BACK_FOOTER, + ) + + def print_menu(self) -> None: + header = " < ? > Help: Flash MCU < ? > " + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) + subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}" + subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}" + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {subheader1:<62} | + | The default command to flash controller board, it | + | will detect selected microcontroller and use suitable | + | tool for flashing it. | + | | + | {subheader2:<62} | + | Special command to flash STM32 microcontrollers in | + | DFU mode but connected via serial. stm32flash command | + | will be used internally. | + | | + """ + )[1:] + print(menu, end="") + + +class KlipperMcuConnectionHelpMenu(BaseMenu): + def __init__(self): + super().__init__( + header=False, + options={}, + footer_type=BACK_FOOTER, + ) + + def print_menu(self) -> None: + header = " < ? > Help: Flash MCU < ? > " + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) + subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}" + subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}" + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {subheader1:<62} | + | Selecting USB as the connection method will scan the | + | USB ports for connected controller boards. This will | + | be similar to the 'ls /dev/serial/by-id/*' command | + | suggested by the official Klipper documentation for | + | determining successfull USB connections! | + | | + | {subheader2:<62} | + | Selecting UART as the connection method will list all | + | possible UART serial ports. Note: This method ALWAYS | + | returns something as it seems impossible to determine | + | if a valid Klipper controller board is connected or | + | not. Because of that, you MUST know which UART serial | + | port your controller board is connected to when using | + | this connection method. | + | | + """ + )[1:] + print(menu, end="") diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index b76a930..66d9be1 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -9,7 +9,7 @@ import textwrap -from components.klipper_firmware.menus.klipper_flash_menu import KlipperFlashMenu +from components.klipper_firmware.menus.klipper_flash_menu import KlipperFlashMethodMenu from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import COLOR_YELLOW, RESET_FORMAT @@ -23,7 +23,7 @@ class AdvancedMenu(BaseMenu): "1": None, "2": None, "3": None, - "4": KlipperFlashMenu, + "4": KlipperFlashMethodMenu, "5": None, "6": None, }, diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 991e65e..6ac932f 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -10,7 +10,7 @@ import os import shutil import socket -import subprocess +from subprocess import Popen, PIPE, CalledProcessError, run, DEVNULL import sys import time import urllib.error @@ -19,6 +19,8 @@ import venv from pathlib import Path from typing import List, Literal +import select + from utils.input_utils import get_confirm from utils.logger import Logger from utils.filesystem_utils import check_file_exist @@ -73,7 +75,7 @@ def create_python_venv(target: Path) -> None: except OSError as e: Logger.print_error(f"Error setting up virtualenv:\n{e}") raise - except subprocess.CalledProcessError as e: + except CalledProcessError as e: Logger.print_error(f"Error setting up virtualenv:\n{e.output.decode()}") raise else: @@ -103,7 +105,7 @@ def update_python_pip(target: Path) -> None: raise FileNotFoundError("Error updating pip! Not found.") command = [pip_location, "install", "-U", "pip"] - result = subprocess.run(command, stderr=subprocess.PIPE, text=True) + result = run(command, stderr=PIPE, text=True) if result.returncode != 0 or result.stderr: Logger.print_error(f"{result.stderr}", False) Logger.print_error("Updating pip failed!") @@ -113,7 +115,7 @@ def update_python_pip(target: Path) -> None: except FileNotFoundError as e: Logger.print_error(e) raise - except subprocess.CalledProcessError as e: + except CalledProcessError as e: Logger.print_error(f"Error updating pip:\n{e.output.decode()}") raise @@ -136,7 +138,7 @@ def install_python_requirements(target: Path, requirements: Path) -> None: "-r", f"{requirements}", ] - result = subprocess.run(command, stderr=subprocess.PIPE, text=True) + result = run(command, stderr=PIPE, text=True) if result.returncode != 0 or result.stderr: Logger.print_error(f"{result.stderr}", False) @@ -144,7 +146,7 @@ def install_python_requirements(target: Path, requirements: Path) -> None: return Logger.print_ok("Installing Python requirements successful!") - except subprocess.CalledProcessError as e: + except CalledProcessError as e: log = f"Error installing Python requirements:\n{e.output.decode()}" Logger.print_error(log) raise @@ -180,14 +182,14 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: if rls_info_change: command.append("--allow-releaseinfo-change") - result = subprocess.run(command, stderr=subprocess.PIPE, text=True) + result = run(command, stderr=PIPE, text=True) if result.returncode != 0 or result.stderr: Logger.print_error(f"{result.stderr}", False) Logger.print_error("Updating system package list failed!") return Logger.print_ok("System package list update successful!") - except subprocess.CalledProcessError as e: + except CalledProcessError as e: kill(f"Error updating system package list:\n{e.stderr.decode()}") @@ -200,10 +202,10 @@ def check_package_install(packages: List[str]) -> List[str]: not_installed = [] for package in packages: command = ["dpkg-query", "-f'${Status}'", "--show", package] - result = subprocess.run( + result = run( command, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, + stdout=PIPE, + stderr=DEVNULL, text=True, ) if "installed" not in result.stdout.strip("'").split(): @@ -224,10 +226,10 @@ def install_system_packages(packages: List[str]) -> None: command = ["sudo", "apt-get", "install", "-y"] for pkg in packages: command.append(pkg) - subprocess.run(command, stderr=subprocess.PIPE, check=True) + run(command, stderr=PIPE, check=True) Logger.print_ok("Packages installed successfully.") - except subprocess.CalledProcessError as e: + except CalledProcessError as e: kill(f"Error installing packages:\n{e.stderr.decode()}") @@ -239,8 +241,8 @@ def mask_system_service(service_name: str) -> None: """ try: command = ["sudo", "systemctl", "mask", service_name] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: + run(command, stderr=PIPE, check=True) + except CalledProcessError as e: log = f"Unable to mask system service {service_name}: {e.stderr.decode()}" Logger.print_error(log) raise @@ -318,12 +320,12 @@ def set_nginx_permissions() -> None: :return: None """ cmd = f"ls -ld {Path.home()} | cut -d' ' -f1" - homedir_perm = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, text=True) + homedir_perm = run(cmd, shell=True, stdout=PIPE, text=True) homedir_perm = homedir_perm.stdout if homedir_perm.count("x") < 3: Logger.print_status("Granting NGINX the required permissions ...") - subprocess.run(["chmod", "og+x", Path.home()]) + run(["chmod", "og+x", Path.home()]) Logger.print_ok("Permissions granted.") @@ -339,9 +341,30 @@ def control_systemd_service( try: Logger.print_status(f"{action.capitalize()} {name}.service ...") command = ["sudo", "systemctl", action, f"{name}.service"] - subprocess.run(command, stderr=subprocess.PIPE, check=True) + run(command, stderr=PIPE, check=True) Logger.print_ok("OK!") - except subprocess.CalledProcessError as e: + except CalledProcessError as e: log = f"Failed to {action} {name}.service: {e.stderr.decode()}" Logger.print_error(log) raise + + +def log_process(process: Popen) -> None: + """ + Helper method to print stdout of a process in near realtime to the console. + :param process: Process to log the output from + :return: None + """ + while True: + reads = [process.stdout.fileno()] + ret = select.select(reads, [], []) + for fd in ret[0]: + if fd == process.stdout.fileno(): + line = process.stdout.readline() + if line: + print(line.strip(), flush=True) + else: + break + + if process.poll() is not None: + break From 39f0bd8b0aaf960e3419272e754abf26e60d04bd Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 31 Mar 2024 00:30:44 +0100 Subject: [PATCH 150/296] refactor: menu refactoring Signed-off-by: Dominik Willner --- .../klipper/menus/klipper_remove_menu.py | 26 ++-- .../menus/klipper_flash_menu.py | 110 +++++++-------- .../log_uploads/menus/log_upload_menu.py | 9 +- .../moonraker/menus/moonraker_remove_menu.py | 26 ++-- .../webui_client/menus/client_remove_menu.py | 15 +-- kiauh/core/menus/__init__.py | 17 ++- kiauh/core/menus/advanced_menu.py | 27 ++-- kiauh/core/menus/backup_menu.py | 28 ++-- kiauh/core/menus/base_menu.py | 127 +++++++++--------- kiauh/core/menus/extensions_menu.py | 33 +++-- kiauh/core/menus/install_menu.py | 29 ++-- kiauh/core/menus/main_menu.py | 29 ++-- kiauh/core/menus/remove_menu.py | 36 +++-- kiauh/core/menus/settings_menu.py | 2 +- kiauh/core/menus/update_menu.py | 33 +++-- .../mainsail_theme_installer_extension.py | 14 +- kiauh/main.py | 2 +- 17 files changed, 271 insertions(+), 292 deletions(-) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index 379005a..e6a8845 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -10,7 +10,7 @@ import textwrap from components.klipper import klipper_remove -from core.menus import BACK_HELP_FOOTER +from core.menus import FooterType from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN @@ -18,18 +18,18 @@ from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class KlipperRemoveMenu(BaseMenu): def __init__(self): - super().__init__( - header=False, - options={ - "0": self.toggle_all, - "1": self.toggle_remove_klipper_service, - "2": self.toggle_remove_klipper_dir, - "3": self.toggle_remove_klipper_env, - "4": self.toggle_delete_klipper_logs, - "c": self.run_removal_process, - }, - footer_type=BACK_HELP_FOOTER, - ) + super().__init__() + self.header = False + self.options = { + "0": self.toggle_all, + "1": self.toggle_remove_klipper_service, + "2": self.toggle_remove_klipper_dir, + "3": self.toggle_remove_klipper_env, + "4": self.toggle_delete_klipper_logs, + "c": self.run_removal_process, + } + self.footer_type = FooterType.BACK_HELP + self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index afad1b6..cd8e9f2 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -21,7 +21,7 @@ from components.klipper_firmware.flash_utils import ( find_usb_dfu_device, flash_device, ) -from core.menus import BACK_HELP_FOOTER, BACK_FOOTER +from core.menus import FooterType from core.menus.base_menu import BaseMenu from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_RED @@ -33,17 +33,17 @@ from utils.logger import Logger # noinspection PyMethodMayBeStatic class KlipperFlashMethodMenu(BaseMenu): def __init__(self): + super().__init__() + self.header = False + self.options = { + "1": self.select_regular, + "2": self.select_sdcard, + "h": KlipperFlashMethodHelpMenu, + } + self.input_label_txt = "Select flash method" + self.footer_type = FooterType.BACK_HELP + self.flash_options = FlashOptions() - super().__init__( - header=False, - options={ - "1": self.select_regular, - "2": self.select_sdcard, - "h": KlipperFlashMethodHelpMenu, - }, - input_label_txt="Select flash method", - footer_type=BACK_HELP_FOOTER, - ) def print_menu(self) -> None: header = " [ Flash MCU ] " @@ -76,26 +76,26 @@ class KlipperFlashMethodMenu(BaseMenu): def goto_next_menu(self, **kwargs): next_menu = KlipperFlashCommandMenu(previous_menu=self) - next_menu.start() + next_menu.run() # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperFlashCommandMenu(BaseMenu): def __init__(self, previous_menu: BaseMenu): + super().__init__() + self.header = False + self.options = { + "1": self.select_flash, + "2": self.select_serialflash, + "h": KlipperFlashCommandHelpMenu, + } + self.default_option = self.select_flash + self.input_label_txt = "Select flash command" + self.previous_menu = previous_menu + self.footer_type = FooterType.BACK_HELP + self.flash_options = FlashOptions() - super().__init__( - header=False, - options={ - "1": self.select_flash, - "2": self.select_serialflash, - "h": KlipperFlashCommandHelpMenu, - }, - default_option="1", - input_label_txt="Select flash command", - previous_menu=previous_menu, - footer_type=BACK_HELP_FOOTER, - ) def print_menu(self) -> None: menu = textwrap.dedent( @@ -120,26 +120,26 @@ class KlipperFlashCommandMenu(BaseMenu): def goto_next_menu(self, **kwargs): next_menu = KlipperSelectMcuConnectionMenu(previous_menu=self) - next_menu.start() + next_menu.run() # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperSelectMcuConnectionMenu(BaseMenu): def __init__(self, previous_menu: BaseMenu): + super().__init__() + self.header = False + self.options = { + "1": self.select_usb, + "2": self.select_dfu, + "3": self.select_usb_dfu, + "h": KlipperMcuConnectionHelpMenu, + } + self.input_label_txt = "Select connection type" + self.previous_menu = previous_menu + self.footer_type = FooterType.BACK_HELP + self.flash_options = FlashOptions() - super().__init__( - header=False, - options={ - "1": self.select_usb, - "2": self.select_dfu, - "3": self.select_usb_dfu, - "h": KlipperMcuConnectionHelpMenu, - }, - input_label_txt="Select connection type", - previous_menu=previous_menu, - footer_type=BACK_HELP_FOOTER, - ) def print_menu(self) -> None: header = "Make sure that the controller board is connected now!" @@ -193,23 +193,22 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): def goto_next_menu(self, **kwargs): next_menu = KlipperSelectMcuIdMenu(previous_menu=self) - next_menu.start() + next_menu.run() # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperSelectMcuIdMenu(BaseMenu): def __init__(self, previous_menu: BaseMenu): + super().__init__() + self.header = False self.flash_options = FlashOptions() self.mcu_list = self.flash_options.mcu_list options = {f"{index}": self.flash_mcu for index in range(len(self.mcu_list))} - super().__init__( - header=False, - options=options, - input_label_txt="Select MCU to flash", - previous_menu=previous_menu, - footer_type=BACK_FOOTER, - ) + self.options = options + self.input_label_txt = "Select MCU to flash" + self.previous_menu = previous_menu + self.footer_type = FooterType.BACK_HELP def print_menu(self) -> None: header = "!!! ATTENTION !!!" @@ -262,11 +261,8 @@ class KlipperSelectMcuIdMenu(BaseMenu): class KlipperFlashMethodHelpMenu(BaseMenu): def __init__(self): - super().__init__( - header=False, - options={}, - footer_type=BACK_FOOTER, - ) + super().__init__() + self.header = False def print_menu(self) -> None: header = " < ? > Help: Flash MCU < ? > " @@ -309,11 +305,8 @@ class KlipperFlashMethodHelpMenu(BaseMenu): class KlipperFlashCommandHelpMenu(BaseMenu): def __init__(self): - super().__init__( - header=False, - options={}, - footer_type=BACK_FOOTER, - ) + super().__init__() + self.header = False def print_menu(self) -> None: header = " < ? > Help: Flash MCU < ? > " @@ -343,11 +336,8 @@ class KlipperFlashCommandHelpMenu(BaseMenu): class KlipperMcuConnectionHelpMenu(BaseMenu): def __init__(self): - super().__init__( - header=False, - options={}, - footer_type=BACK_FOOTER, - ) + super().__init__() + self.header = False def print_menu(self) -> None: header = " < ? > Help: Flash MCU < ? > " diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index f477594..d2a1169 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -11,7 +11,6 @@ import textwrap from components.log_uploads.log_upload_utils import get_logfile_list from components.log_uploads.log_upload_utils import upload_logfile -from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_YELLOW @@ -19,13 +18,11 @@ from utils.constants import RESET_FORMAT, COLOR_YELLOW # noinspection PyMethodMayBeStatic class LogUploadMenu(BaseMenu): def __init__(self): + super().__init__() + self.header = True self.logfile_list = get_logfile_list() options = {f"{index}": self.upload for index in range(len(self.logfile_list))} - super().__init__( - header=True, - options=options, - footer_type=BACK_FOOTER, - ) + self.options = options def print_menu(self): header = " [ Log Upload ] " diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 607d04e..4281f15 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -10,7 +10,6 @@ import textwrap from components.moonraker import moonraker_remove -from core.menus import BACK_HELP_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN @@ -18,19 +17,18 @@ from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class MoonrakerRemoveMenu(BaseMenu): def __init__(self): - super().__init__( - header=False, - options={ - "0": self.toggle_all, - "1": self.toggle_remove_moonraker_service, - "2": self.toggle_remove_moonraker_dir, - "3": self.toggle_remove_moonraker_env, - "4": self.toggle_remove_moonraker_polkit, - "5": self.toggle_delete_moonraker_logs, - "c": self.run_removal_process, - }, - footer_type=BACK_HELP_FOOTER, - ) + super().__init__() + self.header = False + self.options = { + "0": self.toggle_all, + "1": self.toggle_remove_moonraker_service, + "2": self.toggle_remove_moonraker_dir, + "3": self.toggle_remove_moonraker_env, + "4": self.toggle_remove_moonraker_polkit, + "5": self.toggle_delete_moonraker_logs, + "c": self.run_removal_process, + } + self.remove_moonraker_service = False self.remove_moonraker_dir = False self.remove_moonraker_env = False diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index 6cf5638..db9198a 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -11,7 +11,6 @@ import textwrap from typing import Callable, Dict from components.webui_client import client_remove, ClientData -from core.menus import BACK_HELP_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN @@ -19,25 +18,23 @@ from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class ClientRemoveMenu(BaseMenu): def __init__(self, client: ClientData): + super().__init__() + self.header = False + self.options = self.get_options(client) + self.client = client self.rm_client = False self.rm_client_config = False self.backup_mainsail_config_json = False - super().__init__( - header=False, - options=self.get_options(), - footer_type=BACK_HELP_FOOTER, - ) - - def get_options(self) -> Dict[str, Callable]: + def get_options(self, client: ClientData) -> Dict[str, Callable]: options = { "0": self.toggle_all, "1": self.toggle_rm_client, "2": self.toggle_rm_client_config, "c": self.run_removal_process, } - if self.client.get("name") == "mainsail": + if client.get("name") == "mainsail": options["3"] = self.toggle_backup_mainsail_config_json return options diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index 14f4f7b..332135d 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -7,6 +7,17 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -QUIT_FOOTER = "quit" -BACK_FOOTER = "back" -BACK_HELP_FOOTER = "back_help" +from enum import Enum + + +class FooterType(Enum): + QUIT = "QUIT" + BACK = "BACK" + BACK_HELP = "BACK_HELP" + + +NAVI_OPTIONS = { + FooterType.QUIT: ["q"], + FooterType.BACK: ["b"], + FooterType.BACK_HELP: ["b", "h"], +} diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 66d9be1..4b3e776 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -9,26 +9,25 @@ import textwrap -from components.klipper_firmware.menus.klipper_flash_menu import KlipperFlashMethodMenu -from core.menus import BACK_FOOTER +from components.klipper_firmware.menus.klipper_flash_menu import ( + KlipperFlashMethodMenu, + KlipperSelectMcuConnectionMenu, +) from core.menus.base_menu import BaseMenu from utils.constants import COLOR_YELLOW, RESET_FORMAT class AdvancedMenu(BaseMenu): def __init__(self): - super().__init__( - header=True, - options={ - "1": None, - "2": None, - "3": None, - "4": KlipperFlashMethodMenu, - "5": None, - "6": None, - }, - footer_type=BACK_FOOTER, - ) + super().__init__() + self.options = { + "1": None, + "2": None, + "3": None, + "4": KlipperFlashMethodMenu, + "5": None, + "6": KlipperSelectMcuConnectionMenu, + } def print_menu(self): header = " [ Advanced Menu ] " diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 4f404a7..ac416fe 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -19,7 +19,6 @@ from components.webui_client.client_utils import ( load_client_data, backup_client_config_data, ) -from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.common import backup_printer_config_dir from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW @@ -29,21 +28,18 @@ from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW # noinspection PyMethodMayBeStatic class BackupMenu(BaseMenu): def __init__(self): - super().__init__( - header=True, - options={ - "1": self.backup_klipper, - "2": self.backup_moonraker, - "3": self.backup_printer_config, - "4": self.backup_moonraker_db, - "5": self.backup_mainsail, - "6": self.backup_fluidd, - "7": self.backup_mainsail_config, - "8": self.backup_fluidd_config, - "9": self.backup_klipperscreen, - }, - footer_type=BACK_FOOTER, - ) + super().__init__() + self.options = { + "1": self.backup_klipper, + "2": self.backup_moonraker, + "3": self.backup_printer_config, + "4": self.backup_moonraker_db, + "5": self.backup_mainsail, + "6": self.backup_fluidd, + "7": self.backup_mainsail_config, + "8": self.backup_fluidd_config, + "9": self.backup_klipperscreen, + } def print_menu(self): header = " [ Backup Menu ] " diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 78c31cc..84916fb 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -13,9 +13,9 @@ import subprocess import sys import textwrap from abc import abstractmethod, ABC -from typing import Dict, Any, Literal, Union, Callable +from typing import Dict, Union, Callable, Type -from core.menus import QUIT_FOOTER, BACK_FOOTER, BACK_HELP_FOOTER +from core.menus import FooterType, NAVI_OPTIONS from utils.constants import ( COLOR_GREEN, COLOR_YELLOW, @@ -92,74 +92,86 @@ def print_back_help_footer(): print(footer, end="") -class BaseMenu(ABC): - NAVI_OPTIONS = {"quit": ["q"], "back": ["b"], "back_help": ["b", "h"]} +Option = Union[Callable, Type["BaseMenu"], "BaseMenu"] +Options = Dict[str, Option] - def __init__( - self, - options: Dict[str, Union[Callable, Any]], - options_offset: int = 0, - default_option: Union[str, None] = None, - input_label_txt: Union[str, None] = None, - header: bool = True, - previous_menu: BaseMenu = None, - footer_type: Literal[ - "QUIT_FOOTER", "BACK_FOOTER", "BACK_HELP_FOOTER" - ] = QUIT_FOOTER, - ): - self.previous_menu = previous_menu - self.options = options - self.default_option = default_option - self.options_offset = options_offset - self.input_label_txt = input_label_txt - self.header = header - self.footer_type = footer_type + +class BaseMenu(ABC): + options: Options = None + options_offset: int = 0 + default_option: Union[Option, None] = None + input_label_txt: str = "Perform action" + header: bool = True + previous_menu: Union[Type[BaseMenu], BaseMenu] = None + footer_type: FooterType = FooterType.BACK + + def __init__(self): + if type(self) is BaseMenu: + raise NotImplementedError("BaseMenu cannot be instantiated directly.") @abstractmethod def print_menu(self) -> None: raise NotImplementedError("Subclasses must implement the print_menu method") def print_footer(self) -> None: - footer_type_map = { - QUIT_FOOTER: print_quit_footer, - BACK_FOOTER: print_back_footer, - BACK_HELP_FOOTER: print_back_help_footer, - } - footer_function = footer_type_map.get(self.footer_type, print_quit_footer) - footer_function() + if self.footer_type is FooterType.QUIT: + print_quit_footer() + elif self.footer_type is FooterType.BACK: + print_back_footer() + elif self.footer_type is FooterType.BACK_HELP: + print_back_help_footer() + else: + raise NotImplementedError("Method for printing footer not implemented.") - def display(self) -> None: + def display_menu(self) -> None: # clear() if self.header: print_header() self.print_menu() self.print_footer() - def handle_user_input(self) -> str: + def validate_user_input(self, usr_input: str) -> Union[Option, str, None]: + """ + Validate the user input and either return an Option, a string or None + :param usr_input: The user input in form of a string + :return: Option, str or None + """ + usr_input = usr_input.lower() + option = self.options.get(usr_input, None) + + # check if usr_input contains a character used for basic navigation, e.g. b, h or q + # and if the current menu has the appropriate footer to allow for that action + is_valid_navigation = self.footer_type in NAVI_OPTIONS + user_navigated = usr_input in NAVI_OPTIONS[self.footer_type] + if is_valid_navigation and user_navigated: + return usr_input + + # if usr_input is None or an empty string, we execute the menues default option if specified + if option is None or option == "" and self.default_option is not None: + return self.default_option + + # user selected a regular option + if option is not None: + return option + + return None + + def handle_user_input(self) -> Union[Option, str]: + """Handle the user input, return the validated input or print an error.""" while True: - label_text = ( - "Perform action" - if self.input_label_txt is None or self.input_label_txt == "" - else self.input_label_txt - ) - choice = input(f"{COLOR_CYAN}###### {label_text}: {RESET_FORMAT}").lower() - option = self.options.get(choice, None) + print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="") + usr_input = input().lower() + validated_input = self.validate_user_input(usr_input) - has_navi_option = self.footer_type in self.NAVI_OPTIONS - user_navigated = choice in self.NAVI_OPTIONS[self.footer_type] - if has_navi_option and user_navigated: - return choice - - if option is not None: - return choice - elif option is None and self.default_option is not None: - return self.default_option + if validated_input is not None: + return validated_input else: Logger.print_error("Invalid input!", False) - def start(self) -> None: + def run(self) -> None: + """Start the menu lifecycle. When this function returns, the lifecycle of the menu ends.""" while True: - self.display() + self.display_menu() choice = self.handle_user_input() if choice == "q": @@ -170,21 +182,16 @@ class BaseMenu(ABC): else: self.execute_option(choice) - def execute_option(self, choice: str) -> None: - option = self.options.get(choice, None) + def execute_option(self, option: Option) -> None: + if option is None: + raise NotImplementedError(f"No implementation for {option}") if isinstance(option, type) and issubclass(option, BaseMenu): self.navigate_to_menu(option, True) elif isinstance(option, BaseMenu): self.navigate_to_menu(option, False) elif callable(option): - option(opt_index=choice) - elif option is None: - raise NotImplementedError(f"No implementation for option {choice}") - else: - raise TypeError( - f"Type {type(option)} of option {choice} not of type BaseMenu or Method" - ) + option() def navigate_to_menu(self, menu, instantiate: bool) -> None: """ @@ -196,4 +203,4 @@ class BaseMenu(ABC): """ menu = menu() if instantiate else menu menu.previous_menu = self - menu.start() + menu.run() diff --git a/kiauh/core/menus/extensions_menu.py b/kiauh/core/menus/extensions_menu.py index 3db2aea..7562cc4 100644 --- a/kiauh/core/menus/extensions_menu.py +++ b/kiauh/core/menus/extensions_menu.py @@ -12,11 +12,10 @@ import inspect import json import textwrap from pathlib import Path -from typing import List, Dict +from typing import List from core.base_extension import BaseExtension -from core.menus import BACK_FOOTER -from core.menus.base_menu import BaseMenu +from core.menus.base_menu import BaseMenu, Options from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW @@ -24,12 +23,11 @@ from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW # noinspection PyMethodMayBeStatic class ExtensionsMenu(BaseMenu): def __init__(self): + super().__init__() + self.header = False + self.options: Options = self.get_options() + self.extensions = self.discover_extensions() - super().__init__( - header=False, - options=self.get_options(), - footer_type=BACK_FOOTER, - ) def discover_extensions(self) -> List[BaseExtension]: extensions = [] @@ -58,8 +56,8 @@ class ExtensionsMenu(BaseMenu): return sorted(extensions, key=lambda ex: ex.metadata.get("index")) - def get_options(self) -> Dict[str, BaseMenu]: - options = {} + def get_options(self) -> Options: + options: Options = {} for extension in self.extensions: index = extension.metadata.get("index") options[f"{index}"] = ExtensionSubmenu(extension) @@ -93,17 +91,16 @@ class ExtensionsMenu(BaseMenu): # noinspection PyMethodMayBeStatic class ExtensionSubmenu(BaseMenu): def __init__(self, extension: BaseExtension): + super().__init__() + self.header = False + self.options = { + "1": extension.install_extension, + "2": extension.remove_extension, + } + self.extension = extension self.extension_name = extension.metadata.get("display_name") self.extension_desc = extension.metadata.get("description") - super().__init__( - header=False, - options={ - "1": extension.install_extension, - "2": extension.remove_extension, - }, - footer_type=BACK_FOOTER, - ) def print_menu(self) -> None: header = f" [ {self.extension_name} ] " diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 6400645..465033f 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -13,7 +13,7 @@ from components.klipper import klipper_setup from components.moonraker import moonraker_setup from components.webui_client import client_setup from components.webui_client.client_config import client_config_setup -from core.menus import BACK_FOOTER + from core.menus.base_menu import BaseMenu from utils.constants import COLOR_GREEN, RESET_FORMAT @@ -22,21 +22,18 @@ from utils.constants import COLOR_GREEN, RESET_FORMAT # noinspection PyMethodMayBeStatic class InstallMenu(BaseMenu): def __init__(self): - super().__init__( - header=True, - options={ - "1": self.install_klipper, - "2": self.install_moonraker, - "3": self.install_mainsail, - "4": self.install_fluidd, - "5": self.install_mainsail_config, - "6": self.install_fluidd_config, - "7": None, - "8": None, - "9": None, - }, - footer_type=BACK_FOOTER, - ) + super().__init__() + self.options = { + "1": self.install_klipper, + "2": self.install_moonraker, + "3": self.install_mainsail, + "4": self.install_fluidd, + "5": self.install_mainsail_config, + "6": self.install_fluidd_config, + "7": None, + "8": None, + "9": None, + } def print_menu(self): header = " [ Installation Menu ] " diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 5d017e3..d11ef81 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -17,7 +17,7 @@ from components.webui_client.client_utils import ( load_client_data, get_current_client_config, ) -from core.menus import QUIT_FOOTER +from core.menus import FooterType from core.menus.advanced_menu import AdvancedMenu from core.menus.backup_menu import BackupMenu from core.menus.base_menu import BaseMenu @@ -39,20 +39,19 @@ from utils.constants import ( # noinspection PyMethodMayBeStatic class MainMenu(BaseMenu): def __init__(self): - super().__init__( - header=True, - options={ - "0": LogUploadMenu, - "1": InstallMenu, - "2": UpdateMenu, - "3": RemoveMenu, - "4": AdvancedMenu, - "5": BackupMenu, - "e": ExtensionsMenu, - "s": SettingsMenu, - }, - footer_type=QUIT_FOOTER, - ) + super().__init__() + self.options = { + "0": LogUploadMenu, + "1": InstallMenu, + "2": UpdateMenu, + "3": RemoveMenu, + "4": AdvancedMenu, + "5": BackupMenu, + "e": ExtensionsMenu, + "s": SettingsMenu, + } + self.footer_type = FooterType.QUIT + self.kl_status = "" self.kl_repo = "" self.mr_status = "" diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index a7b61a4..6ea9810 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -15,7 +15,6 @@ from components.moonraker.menus.moonraker_remove_menu import ( ) from components.webui_client.client_utils import load_client_data from components.webui_client.menus.client_remove_menu import ClientRemoveMenu -from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import COLOR_RED, RESET_FORMAT @@ -24,25 +23,22 @@ from utils.constants import COLOR_RED, RESET_FORMAT # noinspection PyMethodMayBeStatic class RemoveMenu(BaseMenu): def __init__(self): - super().__init__( - header=True, - options={ - "1": KlipperRemoveMenu, - "2": MoonrakerRemoveMenu, - "3": ClientRemoveMenu(client=load_client_data("mainsail")), - "4": ClientRemoveMenu(client=load_client_data("fluidd")), - "5": None, - "6": None, - "7": None, - "8": None, - "9": None, - "10": None, - "11": None, - "12": None, - "13": None, - }, - footer_type=BACK_FOOTER, - ) + super().__init__() + self.options = { + "1": KlipperRemoveMenu, + "2": MoonrakerRemoveMenu, + "3": ClientRemoveMenu(client=load_client_data("mainsail")), + "4": ClientRemoveMenu(client=load_client_data("fluidd")), + "5": None, + "6": None, + "7": None, + "8": None, + "9": None, + "10": None, + "11": None, + "12": None, + "13": None, + } def print_menu(self): header = " [ Remove Menu ] " diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index c74a384..4cfddc7 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -13,7 +13,7 @@ from core.menus.base_menu import BaseMenu # noinspection PyMethodMayBeStatic class SettingsMenu(BaseMenu): def __init__(self): - super().__init__(header=True, options={}) + super().__init__() def print_menu(self): print("self") diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 7cd944b..724a2e3 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -25,7 +25,6 @@ from components.webui_client.client_utils import ( load_client_data, get_client_config_status, ) -from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from utils.constants import ( COLOR_GREEN, @@ -40,23 +39,21 @@ from utils.constants import ( # noinspection PyMethodMayBeStatic class UpdateMenu(BaseMenu): def __init__(self): - super().__init__( - header=True, - options={ - "0": self.update_all, - "1": self.update_klipper, - "2": self.update_moonraker, - "3": self.update_mainsail, - "4": self.update_fluidd, - "5": self.update_mainsail_config, - "6": self.update_fluidd_config, - "7": self.update_klipperscreen, - "8": self.update_mobileraker, - "9": self.update_crowsnest, - "10": self.upgrade_system_packages, - }, - footer_type=BACK_FOOTER, - ) + super().__init__() + self.options = { + "0": self.update_all, + "1": self.update_klipper, + "2": self.update_moonraker, + "3": self.update_mainsail, + "4": self.update_fluidd, + "5": self.update_mainsail_config, + "6": self.update_fluidd_config, + "7": self.update_klipperscreen, + "8": self.update_mobileraker, + "9": self.update_crowsnest, + "10": self.upgrade_system_packages, + } + self.kl_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.kl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.mr_local = f"{COLOR_WHITE}{RESET_FORMAT}" diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index 546b6ac..e3e78c9 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -22,7 +22,6 @@ from components.klipper.klipper_dialogs import ( from core.base_extension import BaseExtension from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager -from core.menus import BACK_FOOTER from core.menus.base_menu import BaseMenu from core.repo_manager.repo_manager import RepoManager from utils.constants import COLOR_YELLOW, COLOR_CYAN, RESET_FORMAT @@ -44,7 +43,7 @@ class MainsailThemeInstallerExtension(BaseExtension): def install_extension(self, **kwargs) -> None: install_menu = MainsailThemeInstallMenu(self.instances) - install_menu.start() + install_menu.run() def remove_extension(self, **kwargs) -> None: print_instance_overview( @@ -79,14 +78,13 @@ class MainsailThemeInstallMenu(BaseMenu): ) def __init__(self, instances: List[Klipper]): - self.instances = instances + super().__init__() + self.header = False self.themes: List[ThemeData] = self.load_themes() options = {f"{index}": self.install_theme for index in range(len(self.themes))} - super().__init__( - header=False, - options=options, - footer_type=BACK_FOOTER, - ) + self.options = options + + self.instances = instances def print_menu(self) -> None: header = " [ Mainsail Theme Installer ] " diff --git a/kiauh/main.py b/kiauh/main.py index dd6f068..3040ec5 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -13,6 +13,6 @@ from utils.logger import Logger def main(): try: - MainMenu().start() + MainMenu().run() except KeyboardInterrupt: Logger.print_ok("\nHappy printing!\n", prefix=False) From 417180f724ecd7d4b401ecf6ec96852c0609fc40 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 31 Mar 2024 17:28:16 +0200 Subject: [PATCH 151/296] refactor: further menu refactoring Signed-off-by: Dominik Willner --- .../klipper/menus/klipper_remove_menu.py | 5 +- .../menus/klipper_flash_menu.py | 60 ++++++++-------- .../log_uploads/menus/log_upload_menu.py | 5 +- .../moonraker/menus/moonraker_remove_menu.py | 5 +- .../webui_client/menus/client_remove_menu.py | 4 +- kiauh/core/menus/__init__.py | 8 +++ kiauh/core/menus/advanced_menu.py | 8 ++- kiauh/core/menus/backup_menu.py | 4 +- kiauh/core/menus/base_menu.py | 69 ++++++------------- kiauh/core/menus/extensions_menu.py | 17 ++--- kiauh/core/menus/install_menu.py | 4 +- kiauh/core/menus/main_menu.py | 19 ++--- kiauh/core/menus/remove_menu.py | 16 +++-- kiauh/core/menus/settings_menu.py | 4 +- kiauh/core/menus/update_menu.py | 4 +- .../mainsail_theme_installer_extension.py | 1 - 16 files changed, 119 insertions(+), 114 deletions(-) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index e6a8845..dca26f9 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -17,9 +17,10 @@ from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class KlipperRemoveMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False + + self.previous_menu: BaseMenu = previous_menu self.options = { "0": self.toggle_all, "1": self.toggle_remove_klipper_service, diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index cd8e9f2..9594de5 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -32,13 +32,14 @@ from utils.logger import Logger # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperFlashMethodMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False + + self.previous_menu: BaseMenu = previous_menu self.options = { "1": self.select_regular, "2": self.select_sdcard, - "h": KlipperFlashMethodHelpMenu, + "h": lambda: KlipperFlashMethodHelpMenu(self).run(), } self.input_label_txt = "Select flash method" self.footer_type = FooterType.BACK_HELP @@ -75,8 +76,7 @@ class KlipperFlashMethodMenu(BaseMenu): self.goto_next_menu() def goto_next_menu(self, **kwargs): - next_menu = KlipperFlashCommandMenu(previous_menu=self) - next_menu.run() + KlipperFlashCommandMenu(previous_menu=self).run() # noinspection PyUnusedLocal @@ -84,15 +84,15 @@ class KlipperFlashMethodMenu(BaseMenu): class KlipperFlashCommandMenu(BaseMenu): def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False + + self.previous_menu: BaseMenu = previous_menu self.options = { "1": self.select_flash, "2": self.select_serialflash, - "h": KlipperFlashCommandHelpMenu, + "h": lambda: KlipperFlashCommandHelpMenu(previous_menu=self).run(), } self.default_option = self.select_flash self.input_label_txt = "Select flash command" - self.previous_menu = previous_menu self.footer_type = FooterType.BACK_HELP self.flash_options = FlashOptions() @@ -119,8 +119,7 @@ class KlipperFlashCommandMenu(BaseMenu): self.goto_next_menu() def goto_next_menu(self, **kwargs): - next_menu = KlipperSelectMcuConnectionMenu(previous_menu=self) - next_menu.run() + KlipperSelectMcuConnectionMenu(previous_menu=self).run() # noinspection PyUnusedLocal @@ -128,15 +127,15 @@ class KlipperFlashCommandMenu(BaseMenu): class KlipperSelectMcuConnectionMenu(BaseMenu): def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False + + self.previous_menu: BaseMenu = previous_menu self.options = { "1": self.select_usb, "2": self.select_dfu, "3": self.select_usb_dfu, - "h": KlipperMcuConnectionHelpMenu, + "h": lambda: KlipperMcuConnectionHelpMenu(previous_menu=self).run(), } self.input_label_txt = "Select connection type" - self.previous_menu = previous_menu self.footer_type = FooterType.BACK_HELP self.flash_options = FlashOptions() @@ -185,6 +184,8 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): Logger.print_status("Identifying MCU connected via USB in DFU mode ...") self.flash_options.mcu_list = find_usb_dfu_device() + print(self.flash_options.mcu_list) + if len(self.flash_options.mcu_list) < 1: Logger.print_warn("No MCUs found!") Logger.print_warn("Make sure they are connected and repeat this step.") @@ -192,8 +193,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): self.goto_next_menu() def goto_next_menu(self, **kwargs): - next_menu = KlipperSelectMcuIdMenu(previous_menu=self) - next_menu.run() + KlipperSelectMcuIdMenu(previous_menu=self).run() # noinspection PyUnusedLocal @@ -201,13 +201,14 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): class KlipperSelectMcuIdMenu(BaseMenu): def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False + + self.previous_menu: BaseMenu = previous_menu self.flash_options = FlashOptions() self.mcu_list = self.flash_options.mcu_list + print(self.mcu_list) options = {f"{index}": self.flash_mcu for index in range(len(self.mcu_list))} self.options = options self.input_label_txt = "Select MCU to flash" - self.previous_menu = previous_menu self.footer_type = FooterType.BACK_HELP def print_menu(self) -> None: @@ -249,20 +250,17 @@ class KlipperSelectMcuIdMenu(BaseMenu): self.goto_next_menu() def goto_next_menu(self, **kwargs): - pass - # TODO: navigate back to advanced menu after flashing + from core.menus.main_menu import MainMenu + from core.menus.advanced_menu import AdvancedMenu - # from core.menus.main_menu import MainMenu - # from core.menus.advanced_menu import AdvancedMenu - # - # next_menu = AdvancedMenu() - # next_menu.start() + AdvancedMenu(previous_menu=MainMenu()).run() class KlipperFlashMethodHelpMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False + + self.previous_menu: BaseMenu = previous_menu def print_menu(self) -> None: header = " < ? > Help: Flash MCU < ? > " @@ -304,9 +302,10 @@ class KlipperFlashMethodHelpMenu(BaseMenu): class KlipperFlashCommandHelpMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False + + self.previous_menu: BaseMenu = previous_menu def print_menu(self) -> None: header = " < ? > Help: Flash MCU < ? > " @@ -335,9 +334,10 @@ class KlipperFlashCommandHelpMenu(BaseMenu): class KlipperMcuConnectionHelpMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False + + self.previous_menu: BaseMenu = previous_menu def print_menu(self) -> None: header = " < ? > Help: Flash MCU < ? > " diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index d2a1169..35ec143 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -17,9 +17,10 @@ from utils.constants import RESET_FORMAT, COLOR_YELLOW # noinspection PyMethodMayBeStatic class LogUploadMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = True + + self.previous_menu: BaseMenu = previous_menu self.logfile_list = get_logfile_list() options = {f"{index}": self.upload for index in range(len(self.logfile_list))} self.options = options diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 4281f15..6a55078 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -16,9 +16,10 @@ from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class MoonrakerRemoveMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False + + self.previous_menu: BaseMenu = previous_menu self.options = { "0": self.toggle_all, "1": self.toggle_remove_moonraker_service, diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index db9198a..ac32298 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -17,9 +17,9 @@ from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class ClientRemoveMenu(BaseMenu): - def __init__(self, client: ClientData): + def __init__(self, previous_menu: BaseMenu, client: ClientData): super().__init__() - self.header = False + self.previous_menu = previous_menu self.options = self.get_options(client) self.client = client diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index 332135d..670d68e 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -21,3 +21,11 @@ NAVI_OPTIONS = { FooterType.BACK: ["b"], FooterType.BACK_HELP: ["b", "h"], } + + +class ExitAppException(Exception): + pass + + +class GoBackException(Exception): + pass diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 4b3e776..293a62e 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -18,15 +18,17 @@ from utils.constants import COLOR_YELLOW, RESET_FORMAT class AdvancedMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() + + self.previous_menu: BaseMenu = previous_menu self.options = { "1": None, "2": None, "3": None, - "4": KlipperFlashMethodMenu, + "4": lambda: KlipperFlashMethodMenu(previous_menu=self).run(), "5": None, - "6": KlipperSelectMcuConnectionMenu, + "6": lambda: KlipperSelectMcuConnectionMenu(previous_menu=self).run(), } def print_menu(self): diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index ac416fe..0c473fc 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -27,8 +27,10 @@ from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class BackupMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() + + self.previous_menu: BaseMenu = previous_menu self.options = { "1": self.backup_klipper, "2": self.backup_moonraker, diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 84916fb..a8cfb8f 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -15,7 +15,7 @@ import textwrap from abc import abstractmethod, ABC from typing import Dict, Union, Callable, Type -from core.menus import FooterType, NAVI_OPTIONS +from core.menus import FooterType, NAVI_OPTIONS, ExitAppException, GoBackException from utils.constants import ( COLOR_GREEN, COLOR_YELLOW, @@ -92,16 +92,15 @@ def print_back_help_footer(): print(footer, end="") -Option = Union[Callable, Type["BaseMenu"], "BaseMenu"] -Options = Dict[str, Option] +Options = Dict[str, Callable] class BaseMenu(ABC): - options: Options = None + options: Options = {} options_offset: int = 0 - default_option: Union[Option, None] = None + default_option: Union[Callable, None] = None input_label_txt: str = "Perform action" - header: bool = True + header: bool = False previous_menu: Union[Type[BaseMenu], BaseMenu] = None footer_type: FooterType = FooterType.BACK @@ -130,7 +129,7 @@ class BaseMenu(ABC): self.print_menu() self.print_footer() - def validate_user_input(self, usr_input: str) -> Union[Option, str, None]: + def validate_user_input(self, usr_input: str) -> Callable: """ Validate the user input and either return an Option, a string or None :param usr_input: The user input in form of a string @@ -144,26 +143,27 @@ class BaseMenu(ABC): is_valid_navigation = self.footer_type in NAVI_OPTIONS user_navigated = usr_input in NAVI_OPTIONS[self.footer_type] if is_valid_navigation and user_navigated: - return usr_input + if usr_input == "q": + raise ExitAppException() + elif usr_input == "b": + raise GoBackException() + elif usr_input == "h": + return option # if usr_input is None or an empty string, we execute the menues default option if specified - if option is None or option == "" and self.default_option is not None: + if usr_input == "" and self.default_option is not None: return self.default_option # user selected a regular option - if option is not None: - return option + return option - return None - - def handle_user_input(self) -> Union[Option, str]: + def handle_user_input(self) -> Callable: """Handle the user input, return the validated input or print an error.""" while True: print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="") usr_input = input().lower() - validated_input = self.validate_user_input(usr_input) - if validated_input is not None: + if (validated_input := self.validate_user_input(usr_input)) is not None: return validated_input else: Logger.print_error("Invalid input!", False) @@ -171,36 +171,11 @@ class BaseMenu(ABC): def run(self) -> None: """Start the menu lifecycle. When this function returns, the lifecycle of the menu ends.""" while True: - self.display_menu() - choice = self.handle_user_input() - - if choice == "q": + try: + self.display_menu() + self.handle_user_input()() + except GoBackException: + return + except ExitAppException: Logger.print_ok("###### Happy printing!", False) sys.exit(0) - elif choice == "b": - return - else: - self.execute_option(choice) - - def execute_option(self, option: Option) -> None: - if option is None: - raise NotImplementedError(f"No implementation for {option}") - - if isinstance(option, type) and issubclass(option, BaseMenu): - self.navigate_to_menu(option, True) - elif isinstance(option, BaseMenu): - self.navigate_to_menu(option, False) - elif callable(option): - option() - - def navigate_to_menu(self, menu, instantiate: bool) -> None: - """ - Method for handling the actual menu switch. Can either take in a menu type or an already - instantiated menu class. Use instantiated menu classes only if the menu requires specific input parameters - :param menu: A menu type or menu instance - :param instantiate: Specify if the menu requires instantiation - :return: None - """ - menu = menu() if instantiate else menu - menu.previous_menu = self - menu.run() diff --git a/kiauh/core/menus/extensions_menu.py b/kiauh/core/menus/extensions_menu.py index 7562cc4..d20171a 100644 --- a/kiauh/core/menus/extensions_menu.py +++ b/kiauh/core/menus/extensions_menu.py @@ -22,12 +22,12 @@ from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class ExtensionsMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() - self.header = False - self.options: Options = self.get_options() + self.previous_menu: BaseMenu = previous_menu self.extensions = self.discover_extensions() + self.options: Options = self.get_options(self.extensions) def discover_extensions(self) -> List[BaseExtension]: extensions = [] @@ -56,11 +56,11 @@ class ExtensionsMenu(BaseMenu): return sorted(extensions, key=lambda ex: ex.metadata.get("index")) - def get_options(self) -> Options: + def get_options(self, extensions: List[BaseExtension]) -> Options: options: Options = {} - for extension in self.extensions: + for extension in extensions: index = extension.metadata.get("index") - options[f"{index}"] = ExtensionSubmenu(extension) + options[f"{index}"] = lambda: ExtensionSubmenu(self, extension).run() return options @@ -90,9 +90,10 @@ class ExtensionsMenu(BaseMenu): # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class ExtensionSubmenu(BaseMenu): - def __init__(self, extension: BaseExtension): + def __init__(self, previous_menu: BaseMenu, extension: BaseExtension): super().__init__() - self.header = False + + self.previous_menu = previous_menu self.options = { "1": extension.install_extension, "2": extension.remove_extension, diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 465033f..ad2024a 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -21,8 +21,10 @@ from utils.constants import COLOR_GREEN, RESET_FORMAT # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class InstallMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() + + self.previous_menu: BaseMenu = previous_menu self.options = { "1": self.install_klipper, "2": self.install_moonraker, diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index d11ef81..300412e 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -40,16 +40,19 @@ from utils.constants import ( class MainMenu(BaseMenu): def __init__(self): super().__init__() + self.options = { - "0": LogUploadMenu, - "1": InstallMenu, - "2": UpdateMenu, - "3": RemoveMenu, - "4": AdvancedMenu, - "5": BackupMenu, - "e": ExtensionsMenu, - "s": SettingsMenu, + "0": lambda: LogUploadMenu(previous_menu=self).run(), + "1": lambda: InstallMenu(previous_menu=self).run(), + "2": lambda: UpdateMenu(previous_menu=self).run(), + "3": lambda: RemoveMenu(previous_menu=self).run(), + "4": lambda: AdvancedMenu(previous_menu=self).run(), + "5": lambda: BackupMenu(previous_menu=self).run(), + "6": None, + "e": lambda: ExtensionsMenu(previous_menu=self).run(), + "s": lambda: SettingsMenu(previous_menu=self).run(), } + self.header = True self.footer_type = FooterType.QUIT self.kl_status = "" diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 6ea9810..8e1e93c 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -22,13 +22,19 @@ from utils.constants import COLOR_RED, RESET_FORMAT # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class RemoveMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() + + self.previous_menu: BaseMenu = previous_menu self.options = { - "1": KlipperRemoveMenu, - "2": MoonrakerRemoveMenu, - "3": ClientRemoveMenu(client=load_client_data("mainsail")), - "4": ClientRemoveMenu(client=load_client_data("fluidd")), + "1": lambda: KlipperRemoveMenu(previous_menu=self).run(), + "2": lambda: MoonrakerRemoveMenu(previous_menu=self).run(), + "3": lambda: ClientRemoveMenu( + previous_menu=self, client=load_client_data("mainsail") + ).run(), + "4": lambda: ClientRemoveMenu( + previous_menu=self, client=load_client_data("fluidd") + ).run(), "5": None, "6": None, "7": None, diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 4cfddc7..9d28b14 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -12,9 +12,11 @@ from core.menus.base_menu import BaseMenu # noinspection PyMethodMayBeStatic class SettingsMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: BaseMenu): super().__init__() + self.previous_menu: BaseMenu = previous_menu + def print_menu(self): print("self") diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 724a2e3..146e1ad 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -38,8 +38,10 @@ from utils.constants import ( # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class UpdateMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu): super().__init__() + + self.previous_menu: BaseMenu = previous_menu self.options = { "0": self.update_all, "1": self.update_klipper, diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index e3e78c9..28e620f 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -79,7 +79,6 @@ class MainsailThemeInstallMenu(BaseMenu): def __init__(self, instances: List[Klipper]): super().__init__() - self.header = False self.themes: List[ThemeData] = self.load_themes() options = {f"{index}": self.install_theme for index in range(len(self.themes))} self.options = options From b2dd5d8ed7b5409837629ac9ba2a6b647640e2ba Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 31 Mar 2024 17:57:01 +0200 Subject: [PATCH 152/296] refactor: using @dataclass actually broke the singleton Signed-off-by: Dominik Willner --- .../klipper_firmware/flash_options.py | 63 ++++++++++++++++--- .../menus/klipper_flash_menu.py | 3 - 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/kiauh/components/klipper_firmware/flash_options.py b/kiauh/components/klipper_firmware/flash_options.py index 814e41b..24564b5 100644 --- a/kiauh/components/klipper_firmware/flash_options.py +++ b/kiauh/components/klipper_firmware/flash_options.py @@ -7,7 +7,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from dataclasses import field, dataclass +from dataclasses import field from enum import Enum from typing import Union, List @@ -28,15 +28,14 @@ class ConnectionType(Enum): UART = "UART" -@dataclass class FlashOptions: _instance = None - flash_method: Union[FlashMethod, None] = None - flash_command: Union[FlashCommand, None] = None - connection_type: Union[ConnectionType, None] = None - mcu_list: List[str] = field(default_factory=list) - selected_mcu: str = "" - selected_board: str = "" + _flash_method: Union[FlashMethod, None] = None + _flash_command: Union[FlashCommand, None] = None + _connection_type: Union[ConnectionType, None] = None + _mcu_list: List[str] = field(default_factory=list) + _selected_mcu: str = "" + _selected_board: str = "" def __new__(cls, *args, **kwargs): if not cls._instance: @@ -46,3 +45,51 @@ class FlashOptions: @classmethod def destroy(cls): cls._instance = None + + @property + def flash_method(self) -> Union[FlashMethod, None]: + return self._flash_method + + @flash_method.setter + def flash_method(self, value: Union[FlashMethod, None]): + self._flash_method = value + + @property + def flash_command(self) -> Union[FlashCommand, None]: + return self._flash_command + + @flash_command.setter + def flash_command(self, value: Union[FlashCommand, None]): + self._flash_command = value + + @property + def connection_type(self) -> Union[ConnectionType, None]: + return self._connection_type + + @connection_type.setter + def connection_type(self, value: Union[ConnectionType, None]): + self._connection_type = value + + @property + def mcu_list(self) -> List[str]: + return self._mcu_list + + @mcu_list.setter + def mcu_list(self, value: List[str]) -> None: + self._mcu_list = value + + @property + def selected_mcu(self) -> str: + return self._selected_mcu + + @selected_mcu.setter + def selected_mcu(self, value: str) -> None: + self._selected_mcu = value + + @property + def selected_board(self) -> str: + return self._selected_board + + @selected_board.setter + def selected_board(self, value: str) -> None: + self._selected_board = value diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index 9594de5..adc3f3c 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -184,8 +184,6 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): Logger.print_status("Identifying MCU connected via USB in DFU mode ...") self.flash_options.mcu_list = find_usb_dfu_device() - print(self.flash_options.mcu_list) - if len(self.flash_options.mcu_list) < 1: Logger.print_warn("No MCUs found!") Logger.print_warn("Make sure they are connected and repeat this step.") @@ -205,7 +203,6 @@ class KlipperSelectMcuIdMenu(BaseMenu): self.previous_menu: BaseMenu = previous_menu self.flash_options = FlashOptions() self.mcu_list = self.flash_options.mcu_list - print(self.mcu_list) options = {f"{index}": self.flash_mcu for index in range(len(self.mcu_list))} self.options = options self.input_label_txt = "Select MCU to flash" From 4547ac571ab4bf9c5e8b23bec75fd3d964bb454e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 1 Apr 2024 00:55:25 +0200 Subject: [PATCH 153/296] fix: use of lambdas breaks the menu refactoring Signed-off-by: Dominik Willner --- .../menus/klipper_flash_menu.py | 15 +++++-- kiauh/core/menus/advanced_menu.py | 11 ++++- kiauh/core/menus/base_menu.py | 15 +++---- kiauh/core/menus/main_menu.py | 42 +++++++++++++++---- kiauh/core/menus/remove_menu.py | 24 +++++++---- 5 files changed, 78 insertions(+), 29 deletions(-) diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index adc3f3c..4c20f2d 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -39,7 +39,7 @@ class KlipperFlashMethodMenu(BaseMenu): self.options = { "1": self.select_regular, "2": self.select_sdcard, - "h": lambda: KlipperFlashMethodHelpMenu(self).run(), + "h": self.help_menu, } self.input_label_txt = "Select flash method" self.footer_type = FooterType.BACK_HELP @@ -78,6 +78,9 @@ class KlipperFlashMethodMenu(BaseMenu): def goto_next_menu(self, **kwargs): KlipperFlashCommandMenu(previous_menu=self).run() + def help_menu(self, **kwargs): + KlipperFlashMethodHelpMenu(previous_menu=self).run() + # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic @@ -89,7 +92,7 @@ class KlipperFlashCommandMenu(BaseMenu): self.options = { "1": self.select_flash, "2": self.select_serialflash, - "h": lambda: KlipperFlashCommandHelpMenu(previous_menu=self).run(), + "h": self.help_menu, } self.default_option = self.select_flash self.input_label_txt = "Select flash command" @@ -121,6 +124,9 @@ class KlipperFlashCommandMenu(BaseMenu): def goto_next_menu(self, **kwargs): KlipperSelectMcuConnectionMenu(previous_menu=self).run() + def help_menu(self, **kwargs): + KlipperFlashCommandHelpMenu(previous_menu=self).run() + # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic @@ -133,7 +139,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): "1": self.select_usb, "2": self.select_dfu, "3": self.select_usb_dfu, - "h": lambda: KlipperMcuConnectionHelpMenu(previous_menu=self).run(), + "h": self.help_menu, } self.input_label_txt = "Select connection type" self.footer_type = FooterType.BACK_HELP @@ -193,6 +199,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): def goto_next_menu(self, **kwargs): KlipperSelectMcuIdMenu(previous_menu=self).run() + def help_menu(self, **kwargs): + KlipperMcuConnectionHelpMenu(previous_menu=self).run() + # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 293a62e..3c07497 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -17,6 +17,7 @@ from core.menus.base_menu import BaseMenu from utils.constants import COLOR_YELLOW, RESET_FORMAT +# noinspection PyUnusedLocal class AdvancedMenu(BaseMenu): def __init__(self, previous_menu: BaseMenu): super().__init__() @@ -26,9 +27,9 @@ class AdvancedMenu(BaseMenu): "1": None, "2": None, "3": None, - "4": lambda: KlipperFlashMethodMenu(previous_menu=self).run(), + "4": self.flash, "5": None, - "6": lambda: KlipperSelectMcuConnectionMenu(previous_menu=self).run(), + "6": self.get_id, } def print_menu(self): @@ -52,3 +53,9 @@ class AdvancedMenu(BaseMenu): """ )[1:] print(menu, end="") + + def flash(self, **kwargs): + KlipperFlashMethodMenu(previous_menu=self).run() + + def get_id(self, **kwargs): + KlipperSelectMcuConnectionMenu(previous_menu=self).run() diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index a8cfb8f..509b46f 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -13,7 +13,7 @@ import subprocess import sys import textwrap from abc import abstractmethod, ABC -from typing import Dict, Union, Callable, Type +from typing import Dict, Union, Callable, Type, Tuple from core.menus import FooterType, NAVI_OPTIONS, ExitAppException, GoBackException from utils.constants import ( @@ -129,7 +129,7 @@ class BaseMenu(ABC): self.print_menu() self.print_footer() - def validate_user_input(self, usr_input: str) -> Callable: + def validate_user_input(self, usr_input: str) -> Tuple[Callable, str]: """ Validate the user input and either return an Option, a string or None :param usr_input: The user input in form of a string @@ -148,16 +148,16 @@ class BaseMenu(ABC): elif usr_input == "b": raise GoBackException() elif usr_input == "h": - return option + return option, usr_input # if usr_input is None or an empty string, we execute the menues default option if specified if usr_input == "" and self.default_option is not None: - return self.default_option + return self.default_option, usr_input # user selected a regular option - return option + return option, usr_input - def handle_user_input(self) -> Callable: + def handle_user_input(self) -> Tuple[Callable, str]: """Handle the user input, return the validated input or print an error.""" while True: print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="") @@ -173,7 +173,8 @@ class BaseMenu(ABC): while True: try: self.display_menu() - self.handle_user_input()() + option = self.handle_user_input() + option[0](opt_index=option[1]) except GoBackException: return except ExitAppException: diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 300412e..66d4cfd 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -36,21 +36,21 @@ from utils.constants import ( ) +# noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class MainMenu(BaseMenu): def __init__(self): super().__init__() self.options = { - "0": lambda: LogUploadMenu(previous_menu=self).run(), - "1": lambda: InstallMenu(previous_menu=self).run(), - "2": lambda: UpdateMenu(previous_menu=self).run(), - "3": lambda: RemoveMenu(previous_menu=self).run(), - "4": lambda: AdvancedMenu(previous_menu=self).run(), - "5": lambda: BackupMenu(previous_menu=self).run(), - "6": None, - "e": lambda: ExtensionsMenu(previous_menu=self).run(), - "s": lambda: SettingsMenu(previous_menu=self).run(), + "0": self.log_upload_menu, + "1": self.install_menu, + "2": self.update_menu, + "3": self.remove_menu, + "4": self.advanced_menu, + "5": self.backup_menu, + "e": self.extension_menu, + "s": self.settings_menu, } self.header = True self.footer_type = FooterType.QUIT @@ -141,3 +141,27 @@ class MainMenu(BaseMenu): """ )[1:] print(menu, end="") + + def log_upload_menu(self, **kwargs): + LogUploadMenu(previous_menu=self).run() + + def install_menu(self, **kwargs): + InstallMenu(previous_menu=self).run() + + def update_menu(self, **kwargs): + UpdateMenu(previous_menu=self).run() + + def remove_menu(self, **kwargs): + RemoveMenu(previous_menu=self).run() + + def advanced_menu(self, **kwargs): + AdvancedMenu(previous_menu=self).run() + + def backup_menu(self, **kwargs): + BackupMenu(previous_menu=self).run() + + def settings_menu(self, **kwargs): + SettingsMenu(previous_menu=self).run() + + def extension_menu(self, **kwargs): + ExtensionsMenu(previous_menu=self).run() diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 8e1e93c..16d1c42 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -27,14 +27,10 @@ class RemoveMenu(BaseMenu): self.previous_menu: BaseMenu = previous_menu self.options = { - "1": lambda: KlipperRemoveMenu(previous_menu=self).run(), - "2": lambda: MoonrakerRemoveMenu(previous_menu=self).run(), - "3": lambda: ClientRemoveMenu( - previous_menu=self, client=load_client_data("mainsail") - ).run(), - "4": lambda: ClientRemoveMenu( - previous_menu=self, client=load_client_data("fluidd") - ).run(), + "1": self.remove_klipper, + "2": self.remove_moonraker, + "3": self.remove_mainsail, + "4": self.remove_fluidd, "5": None, "6": None, "7": None, @@ -72,3 +68,15 @@ class RemoveMenu(BaseMenu): """ )[1:] print(menu, end="") + + def remove_klipper(self, **kwargs): + KlipperRemoveMenu(previous_menu=self).run() + + def remove_moonraker(self, **kwargs): + MoonrakerRemoveMenu(previous_menu=self).run() + + def remove_mainsail(self, **kwargs): + ClientRemoveMenu(previous_menu=self, client=load_client_data("mainsail")).run() + + def remove_fluidd(self, **kwargs): + ClientRemoveMenu(previous_menu=self, client=load_client_data("fluidd")).run() From 1484ebf445980c3c6e7d4803ae70bdff9465f358 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 1 Apr 2024 00:56:14 +0200 Subject: [PATCH 154/296] refactor: use dict instead of list in discover_extensions method Signed-off-by: Dominik Willner --- kiauh/core/menus/extensions_menu.py | 50 +++++++++++++++-------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/kiauh/core/menus/extensions_menu.py b/kiauh/core/menus/extensions_menu.py index d20171a..2d4e862 100644 --- a/kiauh/core/menus/extensions_menu.py +++ b/kiauh/core/menus/extensions_menu.py @@ -12,10 +12,10 @@ import inspect import json import textwrap from pathlib import Path -from typing import List +from typing import Type, Dict from core.base_extension import BaseExtension -from core.menus.base_menu import BaseMenu, Options +from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW @@ -27,42 +27,44 @@ class ExtensionsMenu(BaseMenu): self.previous_menu: BaseMenu = previous_menu self.extensions = self.discover_extensions() - self.options: Options = self.get_options(self.extensions) + self.options = {ext: self.extension_submenu for ext in self.extensions} - def discover_extensions(self) -> List[BaseExtension]: - extensions = [] + def discover_extensions(self) -> Dict[str, BaseExtension]: + ext_dict = {} extensions_dir = Path(__file__).resolve().parents[2].joinpath("extensions") - for extension in extensions_dir.iterdir(): - metadata_json = Path(extension).joinpath("metadata.json") + for ext in extensions_dir.iterdir(): + metadata_json = Path(ext).joinpath("metadata.json") if not metadata_json.exists(): continue try: with open(metadata_json, "r") as m: + # read extension metadata from json metadata = json.load(m).get("metadata") - module_name = ( - f"kiauh.extensions.{extension.name}.{metadata.get('module')}" - ) - name, extension = inspect.getmembers( - importlib.import_module(module_name), + module_name = metadata.get("module") + module_path = f"kiauh.extensions.{ext.name}.{module_name}" + + # get the class name of the extension + ext_class: Type[BaseExtension] = inspect.getmembers( + importlib.import_module(module_path), predicate=lambda o: inspect.isclass(o) and issubclass(o, BaseExtension) and o != BaseExtension, - )[0] - extensions.append(extension(metadata)) + )[0][1] + + # instantiate the extension with its metadata and add to dict + ext_instance: BaseExtension = ext_class(metadata) + ext_dict[f"{metadata.get('index')}"] = ext_instance + except (IOError, json.JSONDecodeError, ImportError) as e: - print(f"Failed loading extension {extension}: {e}") + print(f"Failed loading extension {ext}: {e}") - return sorted(extensions, key=lambda ex: ex.metadata.get("index")) + return ext_dict - def get_options(self, extensions: List[BaseExtension]) -> Options: - options: Options = {} - for extension in extensions: - index = extension.metadata.get("index") - options[f"{index}"] = lambda: ExtensionSubmenu(self, extension).run() - - return options + def extension_submenu(self, **kwargs): + extension = self.extensions.get(kwargs.get("opt_index")) + ExtensionSubmenu(self, extension).run() def print_menu(self): header = " [ Extensions Menu ] " @@ -80,7 +82,7 @@ class ExtensionsMenu(BaseMenu): )[1:] print(menu, end="") - for extension in self.extensions: + for extension in self.extensions.values(): index = extension.metadata.get("index") name = extension.metadata.get("display_name") row = f"{index}) {name}" From 06801a47eb92ed8aed09dd140d162272a10564e2 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 6 Apr 2024 15:09:33 +0200 Subject: [PATCH 155/296] refactor: full refactor of how webclient data is handled Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 8 +- kiauh/components/moonraker/moonraker_setup.py | 4 +- kiauh/components/moonraker/moonraker_utils.py | 23 +- kiauh/components/webui_client/__init__.py | 75 ------- kiauh/components/webui_client/base_data.py | 117 +++++++++++ .../client_config/client_config_remove.py | 18 +- .../client_config/client_config_setup.py | 69 +++--- .../components/webui_client/client_dialogs.py | 8 +- .../components/webui_client/client_remove.py | 21 +- kiauh/components/webui_client/client_setup.py | 100 ++++----- kiauh/components/webui_client/client_utils.py | 196 ++++++------------ kiauh/components/webui_client/fluidd_data.py | 65 ++++++ .../components/webui_client/mainsail_data.py | 65 ++++++ .../webui_client/menus/client_remove_menu.py | 17 +- kiauh/core/menus/backup_menu.py | 11 +- kiauh/core/menus/install_menu.py | 10 +- kiauh/core/menus/main_menu.py | 13 +- kiauh/core/menus/remove_menu.py | 7 +- kiauh/core/menus/update_menu.py | 28 +-- kiauh/utils/git_utils.py | 57 +++++ 20 files changed, 543 insertions(+), 369 deletions(-) create mode 100644 kiauh/components/webui_client/base_data.py create mode 100644 kiauh/components/webui_client/fluidd_data.py create mode 100644 kiauh/components/webui_client/mainsail_data.py create mode 100644 kiauh/utils/git_utils.py diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 51dd8bd..a4423fb 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -30,7 +30,7 @@ from components.klipper.klipper_dialogs import ( ) from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker_utils import moonraker_to_multi_conversion -from components.webui_client import ClientData +from components.webui_client.base_data import BaseWebClient from components.webui_client.client_config.client_config_setup import ( create_client_config_symlink, ) @@ -288,7 +288,7 @@ def get_highest_index(instance_list: List[Klipper]) -> int: def create_example_printer_cfg( - instance: Klipper, clients: Optional[List[ClientData]] = None + instance: Klipper, clients: Optional[List[BaseWebClient]] = None ) -> None: Logger.print_status(f"Creating example printer.cfg in '{instance.cfg_dir}'") if instance.cfg_file.is_file(): @@ -309,8 +309,8 @@ def create_example_printer_cfg( # include existing client configs in the example config if clients is not None and len(clients) > 0: for c in clients: - client_config = c.get("client_config") - section = client_config.get("printer_cfg_section") + client_config = c.client_config + section = client_config.config_section cm.config.add_section(section=section) create_client_config_symlink(client_config, [instance]) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index ef8c5f6..100ae9e 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -11,11 +11,11 @@ import subprocess import sys from pathlib import Path -from components.webui_client import MAINSAIL_DIR from components.webui_client.client_utils import ( enable_mainsail_remotemode, get_existing_clients, ) +from components.webui_client.mainsail_data import MainsailData from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper from components.moonraker import ( @@ -113,7 +113,7 @@ def install_moonraker() -> None: # if mainsail is installed, and we installed # multiple moonraker instances, we enable mainsails remote mode - if MAINSAIL_DIR.exists() and len(mr_im.instances) > 1: + if MainsailData().client_dir.exists() and len(mr_im.instances) > 1: enable_mainsail_remotemode() diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index f8be7be..ab63d49 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -19,8 +19,9 @@ from components.moonraker import ( MOONRAKER_DB_BACKUP_DIR, ) from components.moonraker.moonraker import Moonraker -from components.webui_client import MAINSAIL_DIR, ClientData +from components.webui_client.base_data import BaseWebClient from components.webui_client.client_utils import enable_mainsail_remotemode +from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager @@ -52,7 +53,7 @@ def get_moonraker_status() -> ( def create_example_moonraker_conf( instance: Moonraker, ports_map: Dict[str, int], - clients: Optional[List[ClientData]] = None, + clients: Optional[List[BaseWebClient]] = None, ) -> None: Logger.print_status(f"Creating example moonraker.conf in '{instance.cfg_dir}'") if instance.cfg_file.is_file(): @@ -100,26 +101,26 @@ def create_example_moonraker_conf( if clients is not None and len(clients) > 0: for c in clients: # client part - c_section = f"update_manager {c.get('name')}" + c_section = f"update_manager {c.name}" c_options = [ ("type", "web"), ("channel", "stable"), - ("repo", c.get("mr_conf_repo")), - ("path", c.get("mr_conf_path")), + ("repo", c.repo_path), + ("path", c.client_dir), ] cm.config.add_section(section=c_section) for option in c_options: cm.config.set(c_section, option[0], option[1]) # client config part - c_config = c.get("client_config") - if c_config.get("dir").exists(): - c_config_section = f"update_manager {c_config.get('name')}" + c_config = c.client_config + if c_config.config_dir.exists(): + c_config_section = f"update_manager {c_config.name}" c_config_options = [ ("type", "git_repo"), ("primary_branch", "master"), - ("path", c_config.get("mr_conf_path")), - ("origin", c_config.get("mr_conf_origin")), + ("path", c_config.config_dir), + ("origin", c_config.repo_url), ("managed_services", "klipper"), ] cm.config.add_section(section=c_config_section) @@ -177,7 +178,7 @@ def moonraker_to_multi_conversion(new_name: str) -> None: im.start_instance() # if mainsail is installed, we enable mainsails remote mode - if MAINSAIL_DIR.exists() and len(im.instances) > 1: + if MainsailData().client_dir.exists() and len(im.instances) > 1: enable_mainsail_remotemode() diff --git a/kiauh/components/webui_client/__init__.py b/kiauh/components/webui_client/__init__.py index bb87c0e..e69de29 100644 --- a/kiauh/components/webui_client/__init__.py +++ b/kiauh/components/webui_client/__init__.py @@ -1,75 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 pathlib import Path -from typing import Literal, TypedDict - -from core.backup_manager import BACKUP_ROOT_DIR - -MODULE_PATH = Path(__file__).resolve().parent - -########### -# MAINSAIL -########### -MAINSAIL_DIR = Path.home().joinpath("mainsail") -MAINSAIL_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mainsail-backups") -MAINSAIL_CONFIG_DIR = Path.home().joinpath("mainsail-config") -MAINSAIL_CONFIG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mainsail-config-backups") -MAINSAIL_CONFIG_REPO_URL = "https://github.com/mainsail-crew/mainsail-config.git" -MAINSAIL_CONFIG_JSON = MAINSAIL_DIR.joinpath("config.json") -MAINSAIL_URL = ( - "https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" -) -MAINSAIL_PRE_RLS_URL = ( - "https://github.com/mainsail-crew/mainsail/releases/download/%TAG%/mainsail.zip" -) -MAINSAIL_TAGS_URL = "https://api.github.com/repos/mainsail-crew/mainsail/tags" - -######### -# FLUIDD -######### -FLUIDD_DIR = Path.home().joinpath("fluidd") -FLUIDD_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("fluidd-backups") -FLUIDD_CONFIG_DIR = Path.home().joinpath("fluidd-config") -FLUIDD_CONFIG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("fluidd-config-backups") -FLUIDD_CONFIG_REPO_URL = "https://github.com/fluidd-core/fluidd-config.git" -FLUIDD_URL = "https://github.com/fluidd-core/fluidd/releases/latest/download/fluidd.zip" -FLUIDD_PRE_RLS_URL = ( - "https://github.com/fluidd-core/fluidd/releases/download/%TAG%/fluidd.zip" -) -FLUIDD_TAGS_URL = "https://api.github.com/repos/fluidd-core/fluidd/tags" - -ClientName = Literal["mainsail", "fluidd"] -ClientConfigName = Literal["mainsail-config", "fluidd-config"] - - -class ClientData(TypedDict): - name: ClientName - display_name: str - dir: Path - backup_dir: Path - url: str - pre_release_url: str - tags_url: str - remote_mode: bool # required only for Mainsail - mr_conf_repo: str - mr_conf_path: str - client_config: "ClientConfigData" - - -class ClientConfigData(TypedDict): - name: ClientConfigName - display_name: str - cfg_filename: str - dir: Path - backup_dir: Path - url: str - printer_cfg_section: str - mr_conf_path: str - mr_conf_origin: str diff --git a/kiauh/components/webui_client/base_data.py b/kiauh/components/webui_client/base_data.py new file mode 100644 index 0000000..0fe34da --- /dev/null +++ b/kiauh/components/webui_client/base_data.py @@ -0,0 +1,117 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 abc import ABC, abstractmethod +from enum import Enum +from pathlib import Path + + +class WebClientType(Enum): + MAINSAIL: str = "mainsail" + FLUIDD: str = "fluidd" + + +class WebClientConfigType(Enum): + MAINSAIL: str = "mainsail-config" + FLUIDD: str = "fluidd-config" + + +class BaseWebClient(ABC): + """Base class for webclient data""" + + @property + @abstractmethod + def client(self) -> WebClientType: + raise NotImplementedError + + @property + @abstractmethod + def name(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def display_name(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def client_dir(self) -> Path: + raise NotImplementedError + + @property + @abstractmethod + def backup_dir(self) -> Path: + raise NotImplementedError + + @property + @abstractmethod + def repo_path(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def stable_url(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def unstable_url(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def client_config(self) -> BaseWebClientConfig: + raise NotImplementedError + + +class BaseWebClientConfig(ABC): + """Base class for webclient config data""" + + @property + @abstractmethod + def client_config(self) -> WebClientConfigType: + raise NotImplementedError + + @property + @abstractmethod + def name(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def display_name(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def config_filename(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def config_dir(self) -> Path: + raise NotImplementedError + + @property + @abstractmethod + def backup_dir(self) -> Path: + raise NotImplementedError + + @property + @abstractmethod + def repo_url(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def config_section(self) -> str: + raise NotImplementedError diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index e61c313..6a5a014 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -14,26 +14,26 @@ from typing import List from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker -from components.webui_client import ClientConfigData +from components.webui_client.base_data import BaseWebClientConfig from core.instance_manager.instance_manager import InstanceManager from utils.filesystem_utils import remove_file, remove_config_section from utils.logger import Logger def run_client_config_removal( - client_config: ClientConfigData, + client_config: BaseWebClientConfig, kl_instances: List[Klipper], mr_instances: List[Moonraker], ) -> None: remove_client_config_dir(client_config) remove_client_config_symlink(client_config) - remove_config_section(f"update_manager {client_config.get('name')}", mr_instances) - remove_config_section(client_config.get("printer_cfg_section"), kl_instances) + remove_config_section(f"update_manager {client_config.name}", mr_instances) + remove_config_section(client_config.config_section, kl_instances) -def remove_client_config_dir(client_config: ClientConfigData) -> None: - Logger.print_status(f"Removing {client_config.get('name')} ...") - client_config_dir = client_config.get("dir") +def remove_client_config_dir(client_config: BaseWebClientConfig) -> None: + Logger.print_status(f"Removing {client_config.name} ...") + client_config_dir = client_config.config_dir if not client_config_dir.exists(): Logger.print_info(f"'{client_config_dir}' does not exist. Skipping ...") return @@ -44,12 +44,12 @@ def remove_client_config_dir(client_config: ClientConfigData) -> None: Logger.print_error(f"Unable to delete '{client_config_dir}':\n{e}") -def remove_client_config_symlink(client_config: ClientConfigData) -> None: +def remove_client_config_symlink(client_config: BaseWebClientConfig) -> None: im = InstanceManager(Klipper) instances: List[Klipper] = im.instances for instance in instances: Logger.print_status(f"Removing symlink from '{instance.cfg_dir}' ...") - symlink = instance.cfg_dir.joinpath(client_config.get("cfg_filename")) + symlink = instance.cfg_dir.joinpath(client_config.config_filename) if not symlink.is_symlink(): Logger.print_info(f"'{symlink}' does not exist. Skipping ...") continue diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 58d2f3b..348069d 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -12,15 +12,14 @@ import subprocess from pathlib import Path from typing import List +from components.webui_client.base_data import BaseWebClient, BaseWebClientConfig from kiauh import KIAUH_CFG from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker -from components.webui_client import ClientConfigData, ClientName, ClientData from components.webui_client.client_dialogs import ( print_client_already_installed_dialog, ) from components.webui_client.client_utils import ( - load_client_data, backup_client_config_data, config_for_other_client_exist, ) @@ -38,19 +37,18 @@ from utils.input_utils import get_confirm from utils.logger import Logger -def install_client_config(client_name: ClientName) -> None: - client: ClientData = load_client_data(client_name) - client_config: ClientConfigData = client.get("client_config") - d_name = client_config.get("display_name") +def install_client_config(client_data: BaseWebClient) -> None: + client_config: BaseWebClientConfig = client_data.client_config + display_name = client_config.display_name - if config_for_other_client_exist(client_name): + if config_for_other_client_exist(client_data.client): Logger.print_info("Another Client-Config is already installed! Skipped ...") return - if client_config.get("dir").exists(): - print_client_already_installed_dialog(d_name) - if get_confirm(f"Re-install {d_name}?", allow_go_back=True): - shutil.rmtree(client_config.get("dir")) + if client_config.config_dir.exists(): + print_client_already_installed_dialog(display_name) + if get_confirm(f"Re-install {display_name}?", allow_go_back=True): + shutil.rmtree(client_config.config_dir) else: return @@ -66,69 +64,74 @@ def install_client_config(client_name: ClientName) -> None: backup_printer_config_dir() add_config_section( - section=f"update_manager {client_config.get('name')}", + section=f"update_manager {client_config.name}", instances=mr_instances, options=[ ("type", "git_repo"), ("primary_branch", "master"), - ("path", client_config.get("mr_conf_path")), - ("origin", client_config.get("mr_conf_origin")), + ("path", str(client_config.config_dir)), + ("origin", str(client_config.repo_url)), ("managed_services", "klipper"), ], ) - add_config_section_at_top( - client_config.get("printer_cfg_section"), kl_instances - ) + add_config_section_at_top(client_config.config_section, kl_instances) kl_im.restart_all_instance() except Exception as e: - Logger.print_error(f"{d_name} installation failed!\n{e}") + Logger.print_error(f"{display_name} installation failed!\n{e}") return - Logger.print_ok(f"{d_name} installation complete!", start="\n") + Logger.print_ok(f"{display_name} installation complete!", start="\n") -def download_client_config(client_config: ClientConfigData) -> None: +def download_client_config(client_config: BaseWebClientConfig) -> None: try: - Logger.print_status(f"Downloading {client_config.get('display_name')} ...") + Logger.print_status(f"Downloading {client_config.display_name} ...") rm = RepoManager( - client_config.get("url"), target_dir=str(client_config.get("dir")) + client_config.repo_url, + target_dir=str(client_config.config_dir), ) rm.clone_repo() except Exception: - Logger.print_error(f"Downloading {client_config.get('display_name')} failed!") + Logger.print_error(f"Downloading {client_config.display_name} failed!") raise -def update_client_config(client: ClientData) -> None: - client_config: ClientConfigData = client.get("client_config") +def update_client_config(client: BaseWebClient) -> None: + client_config: BaseWebClientConfig = client.client_config - Logger.print_status(f"Updating {client_config.get('display_name')} ...") + Logger.print_status(f"Updating {client_config.display_name} ...") + + if not client_config.config_dir.exists(): + Logger.print_info( + f"Unable to update {client_config.display_name}. Directory does not exist! Skipping ..." + ) + return cm = ConfigManager(cfg_file=KIAUH_CFG) if cm.get_value("kiauh", "backup_before_update"): backup_client_config_data(client) repo_manager = RepoManager( - repo=client_config.get("url"), + repo=client_config.repo_url, branch="master", - target_dir=str(client_config.get("dir")), + target_dir=str(client_config.config_dir), ) repo_manager.pull_repo() - Logger.print_ok(f"Successfully updated {client_config.get('display_name')}.") - Logger.print_warn("Remember to restart Klipper to reload the configurations!") + Logger.print_ok(f"Successfully updated {client_config.display_name}.") + Logger.print_info("Restart Klipper to reload the configuration!") def create_client_config_symlink( - client_config: ClientConfigData, klipper_instances: List[Klipper] = None + client_config: BaseWebClientConfig, klipper_instances: List[Klipper] = None ) -> None: if klipper_instances is None: kl_im = InstanceManager(Klipper) klipper_instances = kl_im.instances - Logger.print_status(f"Create symlink for {client_config.get('cfg_filename')} ...") - source = Path(client_config.get("dir"), client_config.get("cfg_filename")) + Logger.print_status(f"Create symlink for {client_config.config_filename} ...") + source = Path(client_config.config_dir, client_config.config_filename) for instance in klipper_instances: target = instance.cfg_dir Logger.print_status(f"Linking {source} to {target}") diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py index 5270c42..1fce323 100644 --- a/kiauh/components/webui_client/client_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -10,7 +10,7 @@ import textwrap from typing import List -from components.webui_client import ClientData +from components.webui_client.base_data import BaseWebClient from core.menus.base_menu import print_back_footer from utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN @@ -84,9 +84,9 @@ def print_client_port_select_dialog(name: str, port: str, ports_in_use: List[str print(dialog, end="") -def print_install_client_config_dialog(client: ClientData): - name = client.get("display_name") - url = client.get("client_config").get("url").replace(".git", "") +def print_install_client_config_dialog(client: BaseWebClient): + name = client.display_name + url = client.client_config.repo_url.replace(".git", "") line1 = f"have {name} fully functional and working." line2 = f"The recommended macros for {name} can be seen here:" dialog = textwrap.dedent( diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index a55a25f..4315aa1 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -13,7 +13,10 @@ from typing import List from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker -from components.webui_client import ClientData +from components.webui_client.base_data import ( + BaseWebClient, + WebClientType, +) from components.webui_client.client_config.client_config_remove import ( run_client_config_removal, ) @@ -29,7 +32,7 @@ from utils.logger import Logger def run_client_removal( - client: ClientData, + client: BaseWebClient, rm_client: bool, rm_client_config: bool, backup_ms_config_json: bool, @@ -39,11 +42,11 @@ def run_client_removal( kl_im = InstanceManager(Klipper) kl_instances: List[Klipper] = kl_im.instances - if backup_ms_config_json and client.get("name") == "mainsail": + if backup_ms_config_json and client.client == WebClientType.MAINSAIL: backup_mainsail_config_json() if rm_client: - client_name = client.get("name") + client_name = client.name remove_client_dir(client) remove_nginx_config(client_name) remove_nginx_logs(client_name) @@ -53,16 +56,16 @@ def run_client_removal( if rm_client_config: run_client_config_removal( - client.get("client_config"), + client.client_config, kl_instances, mr_instances, ) -def remove_client_dir(client: ClientData) -> None: - Logger.print_status(f"Removing {client.get('display_name')} ...") - client_dir = client.get("dir") - if not client.get("dir").exists(): +def remove_client_dir(client: BaseWebClient) -> None: + Logger.print_status(f"Removing {client.display_name} ...") + client_dir = client.client_dir + if not client.client_dir.exists(): Logger.print_info(f"'{client_dir}' does not exist. Skipping ...") return diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 9a148a3..41a749b 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -11,12 +11,13 @@ from pathlib import Path from typing import List from components.klipper.klipper import Klipper -from components.webui_client import ( - ClientName, - ClientData, -) from components.moonraker.moonraker import Moonraker +from components.webui_client.base_data import ( + WebClientType, + BaseWebClient, + BaseWebClientConfig, +) from components.webui_client.client_config.client_config_setup import ( install_client_config, ) @@ -30,7 +31,6 @@ from components.webui_client.client_utils import ( restore_mainsail_config_json, enable_mainsail_remotemode, symlink_webui_nginx_log, - load_client_data, config_for_other_client_exist, ) from core.config_manager.config_manager import ConfigManager @@ -60,17 +60,13 @@ from utils.system_utils import ( ) -def install_client(client_name: ClientName) -> None: - client: ClientData = load_client_data(client_name) - d_name = client.get("display_name") - +def install_client(client: BaseWebClient) -> None: if client is None: - Logger.print_error("Missing parameter client_name!") - return + raise ValueError("Missing parameter client_data!") - if client.get("dir").exists(): + if client.client_dir.exists(): Logger.print_info( - f"{client.get('display_name')} seems to be already installed! Skipped ..." + f"{client.display_name} seems to be already installed! Skipped ..." ) return @@ -81,31 +77,35 @@ def install_client(client_name: ClientName) -> None: if not mr_instances: print_moonraker_not_found_dialog() if not get_confirm( - f"Continue {d_name} installation?", + f"Continue {client.display_name} installation?", allow_go_back=True, ): return # if moonraker is not installed or multiple instances # are installed we enable mainsails remote mode - if client.get("remote_mode") and not mr_instances or len(mr_instances) > 1: + if ( + client.client == WebClientType.MAINSAIL + and not mr_instances + or len(mr_instances) > 1 + ): enable_remotemode = True kl_im = InstanceManager(Klipper) kl_instances = kl_im.instances install_client_cfg = False - client_config = client.get("client_config") + client_config: BaseWebClientConfig = client.client_config if ( kl_instances - and not client_config.get("dir").exists() - and not config_for_other_client_exist(client_to_ignore=client.get("name")) + and not client_config.config_dir.exists() + and not config_for_other_client_exist(client_to_ignore=client.client) ): print_install_client_config_dialog(client) - question = f"Download the recommended {client_config.get('display_name')}?" + question = f"Download the recommended {client_config.display_name}?" install_client_cfg = get_confirm(question, allow_go_back=False) cm = ConfigManager(cfg_file=KIAUH_CFG) - default_port = cm.get_value(client.get("name"), "port") + default_port = cm.get_value(client.name, "port") client_port = default_port if default_port and default_port.isdigit() else "80" ports_in_use = read_ports_from_nginx_configs() @@ -113,10 +113,10 @@ def install_client(client_name: ClientName) -> None: valid_port = is_valid_port(client_port, ports_in_use) while not valid_port: next_port = get_next_free_port(ports_in_use) - print_client_port_select_dialog(d_name, next_port, ports_in_use) + print_client_port_select_dialog(client.display_name, next_port, ports_in_use) client_port = str( get_number_input( - f"Configure {d_name} for port", + f"Configure {client.display_name} for port", min_count=int(next_port), default=next_port, ) @@ -127,22 +127,22 @@ def install_client(client_name: ClientName) -> None: try: download_client(client) - if enable_remotemode and client.get("name") == "mainsail": + if enable_remotemode and client.client == WebClientType.MAINSAIL: enable_mainsail_remotemode() if mr_instances: add_config_section( - section=f"update_manager {client.get('name')}", + section=f"update_manager {client.name}", instances=mr_instances, options=[ ("type", "web"), ("channel", "stable"), - ("repo", client.get("mr_conf_repo")), - ("path", client.get("mr_conf_path")), + ("repo", str(client.repo_path)), + ("path", str(client.client_dir)), ], ) mr_im.restart_all_instance() if install_client_cfg and kl_instances: - install_client_config(client.get("name")) + install_client_config(client) copy_upstream_nginx_cfg() copy_common_vars_nginx_cfg() @@ -152,24 +152,24 @@ def install_client(client_name: ClientName) -> None: control_systemd_service("nginx", "restart") except Exception as e: - Logger.print_error(f"{d_name} installation failed!\n{e}") + Logger.print_error(f"{client.display_name} installation failed!\n{e}") return - log = f"Open {d_name} now on: http://{get_ipv4_addr()}:{client_port}" - Logger.print_ok(f"{d_name} installation complete!", start="\n") + log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{client_port}" + Logger.print_ok(f"{client.display_name} installation complete!", start="\n") Logger.print_ok(log, prefix=False, end="\n\n") -def download_client(client: ClientData) -> None: - zipfile = f"{client.get('name').lower()}.zip" +def download_client(client: BaseWebClient) -> None: + zipfile = f"{client.name.lower()}.zip" target = Path().home().joinpath(zipfile) try: Logger.print_status(f"Downloading {zipfile} ...") - download_file(client.get("url"), target, True) + download_file(client.stable_url, target, True) Logger.print_ok("Download complete!") Logger.print_status(f"Extracting {zipfile} ...") - unzip(target, client.get("dir")) + unzip(target, client.client_dir) target.unlink(missing_ok=True) Logger.print_ok("OK!") @@ -178,29 +178,35 @@ def download_client(client: ClientData) -> None: raise -def update_client(client: ClientData) -> None: - Logger.print_status(f"Updating {client.get('display_name')} ...") - if client.get("name") == "mainsail": +def update_client(client: BaseWebClient) -> None: + Logger.print_status(f"Updating {client.display_name} ...") + if not client.client_dir.exists(): + Logger.print_info( + f"Unable to update {client.display_name}. Directory does not exist! Skipping ..." + ) + return + + if client.client == WebClientType.MAINSAIL: backup_mainsail_config_json(is_temp=True) download_client(client) - if client.get("name") == "mainsail": + if client.client == WebClientType.MAINSAIL: restore_mainsail_config_json() -def create_client_nginx_cfg(client: ClientData, port: int) -> None: - d_name = client.get("display_name") - root_dir = client.get("dir") - source = NGINX_SITES_AVAILABLE.joinpath(client.get("name")) - target = NGINX_SITES_ENABLED.joinpath(client.get("name")) +def create_client_nginx_cfg(client: BaseWebClient, port: int) -> None: + display_name = client.display_name + root_dir = client.client_dir + source = NGINX_SITES_AVAILABLE.joinpath(client.name) + target = NGINX_SITES_ENABLED.joinpath(client.name) try: - Logger.print_status(f"Creating NGINX config for {d_name} ...") + Logger.print_status(f"Creating NGINX config for {display_name} ...") remove_file(Path("/etc/nginx/sites-enabled/default"), True) - create_nginx_cfg(client.get("name"), port, root_dir) + create_nginx_cfg(client.name, port, root_dir) create_symlink(source, target, True) set_nginx_permissions() - Logger.print_ok(f"NGINX config for {d_name} successfully created.") + Logger.print_ok(f"NGINX config for {display_name} successfully created.") except Exception: - Logger.print_error(f"Creating NGINX config for {d_name} failed!") + Logger.print_error(f"Creating NGINX config for {display_name} failed!") raise diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 23d34e3..2d1d5bd 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -9,117 +9,43 @@ import json import shutil -from json import JSONDecodeError from pathlib import Path -from typing import List, Optional, Dict, Literal, Union, get_args +from typing import List, Dict, Literal, Union, get_args -import urllib.request from components.klipper.klipper import Klipper -from components.webui_client import ( - MAINSAIL_CONFIG_JSON, - MAINSAIL_DIR, - MAINSAIL_BACKUP_DIR, - FLUIDD_PRE_RLS_URL, - FLUIDD_BACKUP_DIR, - FLUIDD_URL, - FLUIDD_DIR, - ClientData, - FLUIDD_CONFIG_REPO_URL, - FLUIDD_CONFIG_DIR, - ClientConfigData, - MAINSAIL_PRE_RLS_URL, - MAINSAIL_URL, - MAINSAIL_CONFIG_REPO_URL, - MAINSAIL_CONFIG_DIR, - ClientName, - MAINSAIL_TAGS_URL, - FLUIDD_TAGS_URL, - FLUIDD_CONFIG_BACKUP_DIR, - MAINSAIL_CONFIG_BACKUP_DIR, +from components.webui_client.base_data import ( + WebClientType, + BaseWebClient, + BaseWebClientConfig, ) +from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager from core.repo_manager.repo_manager import RepoManager from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD from utils.common import get_install_status_webui from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW +from utils.git_utils import get_latest_tag from utils.logger import Logger -def load_client_data(client_name: ClientName) -> Optional[ClientData]: - client_data = None - - if client_name == "mainsail": - client_config_data = ClientConfigData( - name="mainsail-config", - display_name="Mainsail-Config", - cfg_filename="mainsail.cfg", - dir=MAINSAIL_CONFIG_DIR, - backup_dir=MAINSAIL_CONFIG_BACKUP_DIR, - url=MAINSAIL_CONFIG_REPO_URL, - printer_cfg_section="include mainsail.cfg", - mr_conf_path="~/mainsail-config", - mr_conf_origin=MAINSAIL_CONFIG_REPO_URL, - ) - client_data = ClientData( - name=client_name, - display_name=client_name.capitalize(), - dir=MAINSAIL_DIR, - backup_dir=MAINSAIL_BACKUP_DIR, - url=MAINSAIL_URL, - pre_release_url=MAINSAIL_PRE_RLS_URL, - tags_url=MAINSAIL_TAGS_URL, - remote_mode=True, - mr_conf_repo="mainsail-crew/mainsail", - mr_conf_path="~/mainsail", - client_config=client_config_data, - ) - elif client_name == "fluidd": - client_config_data = ClientConfigData( - name="fluidd-config", - display_name="Fluidd-Config", - cfg_filename="fluidd.cfg", - dir=FLUIDD_CONFIG_DIR, - backup_dir=FLUIDD_CONFIG_BACKUP_DIR, - url=FLUIDD_CONFIG_REPO_URL, - printer_cfg_section="include fluidd.cfg", - mr_conf_path="~/fluidd-config", - mr_conf_origin=FLUIDD_CONFIG_REPO_URL, - ) - client_data = ClientData( - name=client_name, - display_name=client_name.capitalize(), - dir=FLUIDD_DIR, - backup_dir=FLUIDD_BACKUP_DIR, - url=FLUIDD_URL, - pre_release_url=FLUIDD_PRE_RLS_URL, - tags_url=FLUIDD_TAGS_URL, - remote_mode=False, - mr_conf_repo="fluidd-core/fluidd", - mr_conf_path="~/fluidd", - client_config=client_config_data, - ) - - return client_data - - -def get_client_status(client: ClientData) -> str: +def get_client_status(client: BaseWebClient) -> str: return get_install_status_webui( - client.get("dir"), - NGINX_SITES_AVAILABLE.joinpath(client.get("name")), + client.client_dir, + NGINX_SITES_AVAILABLE.joinpath(client.name), NGINX_CONFD.joinpath("upstreams.conf"), NGINX_CONFD.joinpath("common_vars.conf"), ) def get_client_config_status( - client: ClientData, + client: BaseWebClient, ) -> Dict[ Literal["repo", "local", "remote"], Union[str, int], ]: - client_config = client.get("client_config") - client_config = client_config.get("dir") + client_config = client.client_config + client_config = client_config.config_dir return { "repo": RepoManager.get_repo_name(client_config), @@ -128,44 +54,47 @@ def get_client_config_status( } -def get_current_client_config(clients: List[ClientData]) -> str: +def get_current_client_config(clients: List[BaseWebClient]) -> str: installed = [] for client in clients: - client_config = client.get("client_config") - if client_config.get("dir").exists(): + client_config = client.client_config + if client_config.config_dir.exists(): installed.append(client) if len(installed) > 1: return f"{COLOR_YELLOW}Conflict!{RESET_FORMAT}" elif len(installed) == 1: - cfg = installed[0].get("client_config") - return f"{COLOR_CYAN}{cfg.get('display_name')}{RESET_FORMAT}" + cfg = installed[0].client_config + return f"{COLOR_CYAN}{cfg.display_name}{RESET_FORMAT}" return f"{COLOR_CYAN}-{RESET_FORMAT}" def backup_mainsail_config_json(is_temp=False) -> None: - Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...") + c_json = MainsailData().client_dir.joinpath("config.json") + Logger.print_status(f"Backup '{c_json}' ...") bm = BackupManager() if is_temp: fn = Path.home().joinpath("config.json.kiauh.bak") - bm.backup_file(MAINSAIL_CONFIG_JSON, custom_filename=fn) + bm.backup_file(c_json, custom_filename=fn) else: - bm.backup_file(MAINSAIL_CONFIG_JSON) + bm.backup_file(c_json) def restore_mainsail_config_json() -> None: try: - Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...") + c_json = MainsailData().client_dir.joinpath("config.json") + Logger.print_status(f"Restore '{c_json}' ...") source = Path.home().joinpath("config.json.kiauh.bak") - shutil.copy(source, MAINSAIL_CONFIG_JSON) + shutil.copy(source, c_json) except OSError: Logger.print_info("Unable to restore config.json. Skipped ...") def enable_mainsail_remotemode() -> None: Logger.print_status("Enable Mainsails remote mode ...") - with open(MAINSAIL_CONFIG_JSON, "r") as f: + c_json = MainsailData().client_dir.joinpath("config.json") + with open(c_json, "r") as f: config_data = json.load(f) if config_data["instancesDB"] == "browser": @@ -175,7 +104,7 @@ def enable_mainsail_remotemode() -> None: Logger.print_status("Setting instance storage location to 'browser' ...") config_data["instancesDB"] = "browser" - with open(MAINSAIL_CONFIG_JSON, "w") as f: + with open(c_json, "w") as f: json.dump(config_data, f, indent=4) Logger.print_ok("Mainsails remote mode enabled!") @@ -195,8 +124,8 @@ def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: desti_error.symlink_to(error_log) -def get_local_client_version(client: ClientData) -> str: - relinfo_file = client.get("dir").joinpath("release_info.json") +def get_local_client_version(client: BaseWebClient) -> str: + relinfo_file = client.client_dir.joinpath("release_info.json") if not relinfo_file.is_file(): return "-" @@ -204,19 +133,19 @@ def get_local_client_version(client: ClientData) -> str: return json.load(f)["version"] -def get_remote_client_version(client: ClientData) -> str: +def get_remote_client_version(client: BaseWebClient) -> str: try: - with urllib.request.urlopen(client.get("tags_url")) as response: - data = json.loads(response.read()) - return data[0]["name"] - except (JSONDecodeError, TypeError): + if (tag := get_latest_tag(client.repo_path)) != "": + return tag + return "ERROR" + except Exception: return "ERROR" -def backup_client_data(client: ClientData) -> None: - name = client.get("name") - src = client.get("dir") - dest = client.get("backup_dir") +def backup_client_data(client: BaseWebClient) -> None: + name = client.name + src = client.client_dir + dest = client.backup_dir with open(src.joinpath(".version"), "r") as v: version = v.readlines()[0] @@ -224,43 +153,42 @@ def backup_client_data(client: ClientData) -> None: bm = BackupManager() bm.backup_directory(f"{name}-{version}", src, dest) if name == "mainsail": - bm.backup_file(MAINSAIL_CONFIG_JSON, dest) + c_json = MainsailData().client_dir.joinpath("config.json") + bm.backup_file(c_json, dest) bm.backup_file(NGINX_SITES_AVAILABLE.joinpath(name), dest) -def backup_client_config_data(client: ClientData) -> None: - client_config = client.get("client_config") - name = client_config.get("name") - source = client_config.get("dir") - target = client_config.get("backup_dir") +def backup_client_config_data(client: BaseWebClient) -> None: + client_config = client.client_config + name = client_config.name + source = client_config.config_dir + target = client_config.backup_dir bm = BackupManager() bm.backup_directory(name, source, target) -def get_existing_clients() -> List[ClientData]: - clients = list(get_args(ClientName)) - installed_clients: List[ClientData] = [] - for c in clients: - c_data: ClientData = load_client_data(c) - if c_data.get("dir").exists(): - installed_clients.append(c_data) +def get_existing_clients() -> List[BaseWebClient]: + clients = list(get_args(WebClientType)) + installed_clients: List[BaseWebClient] = [] + for client in clients: + if client.client_dir.exists(): + installed_clients.append(client) return installed_clients -def get_existing_client_config() -> List[ClientData]: - clients = list(get_args(ClientName)) - installed_client_configs: List[ClientData] = [] - for c in clients: - c_data: ClientData = load_client_data(c) - c_config_data: ClientConfigData = c_data.get("client_config") - if c_config_data.get("dir").exists(): - installed_client_configs.append(c_data) +def get_existing_client_config() -> List[BaseWebClient]: + clients = list(get_args(WebClientType)) + installed_client_configs: List[BaseWebClient] = [] + for client in clients: + c_config_data: BaseWebClientConfig = client.client_config + if c_config_data.config_dir.exists(): + installed_client_configs.append(client) return installed_client_configs -def config_for_other_client_exist(client_to_ignore: ClientName) -> bool: +def config_for_other_client_exist(client_to_ignore: WebClientType) -> bool: """ Check if any other client configs are present on the system. It is usually not harmful, but chances are they can conflict each other. @@ -269,7 +197,7 @@ def config_for_other_client_exist(client_to_ignore: ClientName) -> bool: :return: True, if other client configs were found, else False """ - clients = set([c["name"] for c in get_existing_client_config()]) - clients = clients - {client_to_ignore} + clients = set([c.name for c in get_existing_client_config()]) + clients = clients - {client_to_ignore.value} return True if len(clients) > 0 else False diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py new file mode 100644 index 0000000..4099eeb --- /dev/null +++ b/kiauh/components/webui_client/fluidd_data.py @@ -0,0 +1,65 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 dataclasses import dataclass +from pathlib import Path + +from components.webui_client.base_data import ( + BaseWebClientConfig, + WebClientConfigType, + WebClientType, + BaseWebClient, +) +from core.backup_manager import BACKUP_ROOT_DIR +from utils.git_utils import get_latest_unstable_tag + + +@dataclass(frozen=True) +class FluiddConfigWeb(BaseWebClientConfig): + client_config: WebClientConfigType = WebClientConfigType.FLUIDD + name: str = client_config.value + display_name: str = name.title() + config_dir: Path = Path.home().joinpath("fluidd-config") + config_filename: str = "fluidd.cfg" + config_section: str = f"include {config_filename}" + backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-config-backups") + repo_url: str = "https://github.com/fluidd-core/fluidd-config.git" + + +@dataclass(frozen=True) +class FluiddData(BaseWebClient): + BASE_DL_URL = "https://github.com/fluidd-core/fluidd/releases" + + client: WebClientType = WebClientType.FLUIDD + name: str = client.value + display_name: str = name.capitalize() + client_dir: Path = Path.home().joinpath("fluidd") + backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups") + repo_path: str = "fluidd-core/fluidd" + + @property + def stable_url(self) -> str: + return f"{self.BASE_DL_URL}/latest/download/fluidd.zip" + + @property + def unstable_url(self) -> str: + try: + unstable_tag = get_latest_unstable_tag(self.repo_path) + if unstable_tag != "": + return f"{self.BASE_DL_URL}/download/{unstable_tag}/fluidd.zip" + else: + raise Exception + except Exception: + return self.stable_url + + @property + def client_config(self) -> BaseWebClientConfig: + return FluiddConfigWeb() diff --git a/kiauh/components/webui_client/mainsail_data.py b/kiauh/components/webui_client/mainsail_data.py new file mode 100644 index 0000000..208ce2c --- /dev/null +++ b/kiauh/components/webui_client/mainsail_data.py @@ -0,0 +1,65 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 dataclasses import dataclass +from pathlib import Path + +from components.webui_client.base_data import ( + BaseWebClientConfig, + WebClientConfigType, + WebClientType, + BaseWebClient, +) +from core.backup_manager import BACKUP_ROOT_DIR +from utils.git_utils import get_latest_unstable_tag + + +@dataclass(frozen=True) +class MainsailConfigWeb(BaseWebClientConfig): + client_config: WebClientConfigType = WebClientConfigType.MAINSAIL + name: str = client_config.value + display_name: str = name.title() + config_dir: Path = Path.home().joinpath("mainsail-config") + config_filename: str = "mainsail.cfg" + config_section: str = f"include {config_filename}" + backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-config-backups") + repo_url: str = "https://github.com/mainsail-crew/mainsail-config.git" + + +@dataclass(frozen=True) +class MainsailData(BaseWebClient): + BASE_DL_URL: str = "https://github.com/mainsail-crew/mainsail/releases" + + client: WebClientType = WebClientType.MAINSAIL + name: str = WebClientType.MAINSAIL.value + display_name: str = name.capitalize() + client_dir: Path = Path.home().joinpath("mainsail") + backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-backups") + repo_path: str = "mainsail-crew/mainsail" + + @property + def stable_url(self) -> str: + return f"{self.BASE_DL_URL}/latest/download/mainsail.zip" + + @property + def unstable_url(self) -> str: + try: + unstable_tag = get_latest_unstable_tag(self.repo_path) + if unstable_tag != "": + return f"{self.BASE_DL_URL}/download/{unstable_tag}/mainsail.zip" + else: + raise Exception + except Exception: + return self.stable_url + + @property + def client_config(self) -> BaseWebClientConfig: + return MainsailConfigWeb() diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index ac32298..f6e99a0 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -10,14 +10,15 @@ import textwrap from typing import Callable, Dict -from components.webui_client import client_remove, ClientData +from components.webui_client import client_remove +from components.webui_client.base_data import BaseWebClient, WebClientType from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class ClientRemoveMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu, client: ClientData): + def __init__(self, previous_menu: BaseMenu, client: BaseWebClient): super().__init__() self.previous_menu = previous_menu self.options = self.get_options(client) @@ -27,22 +28,22 @@ class ClientRemoveMenu(BaseMenu): self.rm_client_config = False self.backup_mainsail_config_json = False - def get_options(self, client: ClientData) -> Dict[str, Callable]: + def get_options(self, client: BaseWebClient) -> Dict[str, Callable]: options = { "0": self.toggle_all, "1": self.toggle_rm_client, "2": self.toggle_rm_client_config, "c": self.run_removal_process, } - if client.get("name") == "mainsail": + if client.client == WebClientType.MAINSAIL: options["3"] = self.toggle_backup_mainsail_config_json return options def print_menu(self) -> None: - client_name = self.client.get("display_name") - client_config = self.client.get("client_config") - client_config_name = client_config.get("display_name") + client_name = self.client.display_name + client_config = self.client.client_config + client_config_name = client_config.display_name header = f" [ Remove {client_name} ] " color = COLOR_RED @@ -66,7 +67,7 @@ class ClientRemoveMenu(BaseMenu): """ )[1:] - if self.client.get("name") == "mainsail": + if self.client.client == WebClientType.MAINSAIL: o3 = checked if self.backup_mainsail_config_json else unchecked menu += textwrap.dedent( f""" diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 0c473fc..72f1735 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -16,9 +16,10 @@ from components.moonraker.moonraker_utils import ( ) from components.webui_client.client_utils import ( backup_client_data, - load_client_data, backup_client_config_data, ) +from components.webui_client.fluidd_data import FluiddData +from components.webui_client.mainsail_data import MainsailData from core.menus.base_menu import BaseMenu from utils.common import backup_printer_config_dir from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW @@ -81,16 +82,16 @@ class BackupMenu(BaseMenu): backup_moonraker_db_dir() def backup_mainsail(self, **kwargs): - backup_client_data(load_client_data("mainsail")) + backup_client_data(MainsailData().get()) def backup_fluidd(self, **kwargs): - backup_client_data(load_client_data("fluidd")) + backup_client_data(FluiddData().get()) def backup_mainsail_config(self, **kwargs): - backup_client_config_data(load_client_data("mainsail")) + backup_client_config_data(MainsailData().get()) def backup_fluidd_config(self, **kwargs): - backup_client_config_data(load_client_data("fluidd")) + backup_client_config_data(FluiddData().get()) def backup_klipperscreen(self, **kwargs): pass diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index ad2024a..5db3846 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -13,6 +13,8 @@ from components.klipper import klipper_setup from components.moonraker import moonraker_setup from components.webui_client import client_setup from components.webui_client.client_config import client_config_setup +from components.webui_client.fluidd_data import FluiddData +from components.webui_client.mainsail_data import MainsailData from core.menus.base_menu import BaseMenu from utils.constants import COLOR_GREEN, RESET_FORMAT @@ -69,13 +71,13 @@ class InstallMenu(BaseMenu): moonraker_setup.install_moonraker() def install_mainsail(self, **kwargs): - client_setup.install_client(client_name="mainsail") + client_setup.install_client(MainsailData()) def install_mainsail_config(self, **kwargs): - client_config_setup.install_client_config(client_name="mainsail") + client_config_setup.install_client_config(MainsailData()) def install_fluidd(self, **kwargs): - client_setup.install_client(client_name="fluidd") + client_setup.install_client(FluiddData()) def install_fluidd_config(self, **kwargs): - client_config_setup.install_client_config(client_name="fluidd") + client_config_setup.install_client_config(FluiddData()) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 66d4cfd..317584b 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -14,9 +14,10 @@ from components.log_uploads.menus.log_upload_menu import LogUploadMenu from components.moonraker.moonraker_utils import get_moonraker_status from components.webui_client.client_utils import ( get_client_status, - load_client_data, get_current_client_config, ) +from components.webui_client.fluidd_data import FluiddData +from components.webui_client.mainsail_data import MainsailData from core.menus import FooterType from core.menus.advanced_menu import AdvancedMenu from core.menus.backup_menu import BackupMenu @@ -92,15 +93,11 @@ class MainMenu(BaseMenu): self.mr_status = self.format_status_by_code(mr_code, mr_status, mr_instances) self.mr_repo = f"{COLOR_CYAN}{moonraker_status.get('repo')}{RESET_FORMAT}" # mainsail - mainsail_client_data = load_client_data("mainsail") - self.ms_status = get_client_status(mainsail_client_data) + self.ms_status = get_client_status(MainsailData()) # fluidd - fluidd_client_data = load_client_data("fluidd") - self.fl_status = get_client_status(fluidd_client_data) + self.fl_status = get_client_status(FluiddData()) # client-config - self.cc_status = get_current_client_config( - [mainsail_client_data, fluidd_client_data] - ) + self.cc_status = get_current_client_config([MainsailData(), FluiddData()]) def format_status_by_code(self, code: int, status: str, count: str) -> str: if code == 1: diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 16d1c42..0ba8992 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -13,7 +13,8 @@ from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu from components.moonraker.menus.moonraker_remove_menu import ( MoonrakerRemoveMenu, ) -from components.webui_client.client_utils import load_client_data +from components.webui_client.fluidd_data import FluiddData +from components.webui_client.mainsail_data import MainsailData from components.webui_client.menus.client_remove_menu import ClientRemoveMenu from core.menus.base_menu import BaseMenu from utils.constants import COLOR_RED, RESET_FORMAT @@ -76,7 +77,7 @@ class RemoveMenu(BaseMenu): MoonrakerRemoveMenu(previous_menu=self).run() def remove_mainsail(self, **kwargs): - ClientRemoveMenu(previous_menu=self, client=load_client_data("mainsail")).run() + ClientRemoveMenu(previous_menu=self, client=MainsailData()).run() def remove_fluidd(self, **kwargs): - ClientRemoveMenu(previous_menu=self, client=load_client_data("fluidd")).run() + ClientRemoveMenu(previous_menu=self, client=FluiddData()).run() diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 146e1ad..4917ebf 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -22,9 +22,10 @@ from components.webui_client.client_setup import update_client from components.webui_client.client_utils import ( get_local_client_version, get_remote_client_version, - load_client_data, get_client_config_status, ) +from components.webui_client.fluidd_data import FluiddData +from components.webui_client.mainsail_data import MainsailData from core.menus.base_menu import BaseMenu from utils.constants import ( COLOR_GREEN, @@ -69,6 +70,9 @@ class UpdateMenu(BaseMenu): self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.mainsail_cient = MainsailData() + self.fluidd_client = FluiddData() + def print_menu(self): self.fetch_update_status() @@ -114,16 +118,16 @@ class UpdateMenu(BaseMenu): update_moonraker() def update_mainsail(self, **kwargs): - update_client(load_client_data("mainsail")) + update_client(self.mainsail_cient) def update_mainsail_config(self, **kwargs): - update_client_config(load_client_data("mainsail")) + update_client_config(self.mainsail_cient) def update_fluidd(self, **kwargs): - update_client(load_client_data("fluidd")) + update_client(self.fluidd_client) def update_fluidd_config(self, **kwargs): - update_client_config(load_client_data("fluidd")) + update_client_config(self.fluidd_client) def update_klipperscreen(self, **kwargs): ... @@ -153,25 +157,23 @@ class UpdateMenu(BaseMenu): self.mr_local = f"{COLOR_YELLOW}{self.mr_local}{RESET_FORMAT}" self.mr_remote = f"{COLOR_GREEN}{self.mr_remote}{RESET_FORMAT}" # mainsail - mainsail_client_data = load_client_data("mainsail") - self.ms_local = get_local_client_version(mainsail_client_data) - self.ms_remote = get_remote_client_version(mainsail_client_data) + self.ms_local = get_local_client_version(self.mainsail_cient) + self.ms_remote = get_remote_client_version(self.mainsail_cient) if self.ms_local == self.ms_remote: self.ms_local = f"{COLOR_GREEN}{self.ms_local}{RESET_FORMAT}" else: self.ms_local = f"{COLOR_YELLOW}{self.ms_local}{RESET_FORMAT}" self.ms_remote = f"{COLOR_GREEN if self.ms_remote != 'ERROR' else COLOR_RED}{self.ms_remote}{RESET_FORMAT}" # fluidd - fluidd_client_data = load_client_data("fluidd") - self.fl_local = get_local_client_version(fluidd_client_data) - self.fl_remote = get_remote_client_version(fluidd_client_data) + self.fl_local = get_local_client_version(self.fluidd_client) + self.fl_remote = get_remote_client_version(self.fluidd_client) if self.fl_local == self.fl_remote: self.fl_local = f"{COLOR_GREEN}{self.fl_local}{RESET_FORMAT}" else: self.fl_local = f"{COLOR_YELLOW}{self.fl_local}{RESET_FORMAT}" self.fl_remote = f"{COLOR_GREEN if self.fl_remote != 'ERROR' else COLOR_RED}{self.fl_remote}{RESET_FORMAT}" # mainsail-config - mc_status = get_client_config_status(load_client_data("mainsail")) + mc_status = get_client_config_status(self.mainsail_cient) self.mc_local = mc_status.get("local") self.mc_remote = mc_status.get("remote") if self.mc_local == self.mc_remote: @@ -180,7 +182,7 @@ class UpdateMenu(BaseMenu): self.mc_local = f"{COLOR_YELLOW}{self.mc_local}{RESET_FORMAT}" self.mc_remote = f"{COLOR_GREEN}{self.mc_remote}{RESET_FORMAT}" # fluidd-config - fc_status = get_client_config_status(load_client_data("fluidd")) + fc_status = get_client_config_status(self.fluidd_client) self.fc_local = fc_status.get("local") self.fc_remote = fc_status.get("remote") if self.fc_local == self.mc_remote: diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py new file mode 100644 index 0000000..2ed9a4d --- /dev/null +++ b/kiauh/utils/git_utils.py @@ -0,0 +1,57 @@ +import json +import urllib.request +from http.client import HTTPResponse +from json import JSONDecodeError +from typing import List + +from utils.logger import Logger + + +def get_tags(repo_path: str) -> List[str]: + try: + url = f"https://api.github.com/repos/{repo_path}/tags" + with urllib.request.urlopen(url) as r: + response: HTTPResponse = r + if response.getcode() != 200: + Logger.print_error( + f"Error retrieving tags: HTTP status code {response.getcode()}" + ) + return [] + + data = json.loads(response.read()) + return [item["name"] for item in data] + except (JSONDecodeError, TypeError) as e: + Logger.print_error(f"Error while processing the response: {e}") + raise + + +def get_latest_tag(repo_path: str) -> str: + """ + Gets the latest stable tag of a GitHub repostiory + :param repo_path: path of the GitHub repository - e.g. `/` + :return: tag or empty string + """ + try: + if len(latest_tag := get_tags(repo_path)) > 0: + return latest_tag[0] + else: + return "" + except Exception: + Logger.print_error("Error while getting the latest tag") + raise + + +def get_latest_unstable_tag(repo_path: str) -> str: + """ + Gets the latest unstable (alpha, beta, rc) tag of a GitHub repository + :param repo_path: path of the GitHub repository - e.g. `/` + :return: tag or empty string + """ + try: + if len(unstable_tags := [t for t in get_tags(repo_path) if "-" in t]) > 0: + return unstable_tags[0] + else: + return "" + except Exception: + Logger.print_error("Error while getting the latest unstable tag") + raise From ace47e2873c97d1838233d0ac26d723170016685 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 6 Apr 2024 22:07:59 +0200 Subject: [PATCH 156/296] refactor: remove code duplication Signed-off-by: Dominik Willner --- kiauh/core/menus/update_menu.py | 83 +++++++++++++++------------------ 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 4917ebf..dcff75b 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -70,7 +70,7 @@ class UpdateMenu(BaseMenu): self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" - self.mainsail_cient = MainsailData() + self.mainsail_client = MainsailData() self.fluidd_client = FluiddData() def print_menu(self): @@ -118,10 +118,10 @@ class UpdateMenu(BaseMenu): update_moonraker() def update_mainsail(self, **kwargs): - update_client(self.mainsail_cient) + update_client(self.mainsail_client) def update_mainsail_config(self, **kwargs): - update_client_config(self.mainsail_cient) + update_client_config(self.mainsail_client) def update_fluidd(self, **kwargs): update_client(self.fluidd_client) @@ -140,53 +140,46 @@ class UpdateMenu(BaseMenu): def fetch_update_status(self): # klipper kl_status = get_klipper_status() - self.kl_local = kl_status.get("local") + self.kl_local = self.format_local_status( + kl_status.get("local"), kl_status.get("remote") + ) self.kl_remote = kl_status.get("remote") - if self.kl_local == self.kl_remote: - self.kl_local = f"{COLOR_GREEN}{self.kl_local}{RESET_FORMAT}" - else: - self.kl_local = f"{COLOR_YELLOW}{self.kl_local}{RESET_FORMAT}" - self.kl_remote = f"{COLOR_GREEN}{self.kl_remote}{RESET_FORMAT}" + self.kl_remote = f"{COLOR_GREEN}{kl_status.get('remote')}{RESET_FORMAT}" + # moonraker mr_status = get_moonraker_status() - self.mr_local = mr_status.get("local") - self.mr_remote = mr_status.get("remote") - if self.mr_local == self.mr_remote: - self.mr_local = f"{COLOR_GREEN}{self.mr_local}{RESET_FORMAT}" - else: - self.mr_local = f"{COLOR_YELLOW}{self.mr_local}{RESET_FORMAT}" - self.mr_remote = f"{COLOR_GREEN}{self.mr_remote}{RESET_FORMAT}" + self.mr_local = self.format_local_status( + mr_status.get("local"), mr_status.get("remote") + ) + self.mr_remote = f"{COLOR_GREEN}{mr_status.get('remote')}{RESET_FORMAT}" + # mainsail - self.ms_local = get_local_client_version(self.mainsail_cient) - self.ms_remote = get_remote_client_version(self.mainsail_cient) - if self.ms_local == self.ms_remote: - self.ms_local = f"{COLOR_GREEN}{self.ms_local}{RESET_FORMAT}" - else: - self.ms_local = f"{COLOR_YELLOW}{self.ms_local}{RESET_FORMAT}" - self.ms_remote = f"{COLOR_GREEN if self.ms_remote != 'ERROR' else COLOR_RED}{self.ms_remote}{RESET_FORMAT}" + ms_local_ver = get_local_client_version(self.mainsail_client) + ms_remote_ver = get_remote_client_version(self.mainsail_client) + self.ms_local = self.format_local_status(ms_local_ver, ms_remote_ver) + self.ms_remote = f"{COLOR_GREEN if ms_remote_ver != 'ERROR' else COLOR_RED}{ms_remote_ver}{RESET_FORMAT}" + # fluidd - self.fl_local = get_local_client_version(self.fluidd_client) - self.fl_remote = get_remote_client_version(self.fluidd_client) - if self.fl_local == self.fl_remote: - self.fl_local = f"{COLOR_GREEN}{self.fl_local}{RESET_FORMAT}" - else: - self.fl_local = f"{COLOR_YELLOW}{self.fl_local}{RESET_FORMAT}" - self.fl_remote = f"{COLOR_GREEN if self.fl_remote != 'ERROR' else COLOR_RED}{self.fl_remote}{RESET_FORMAT}" + fl_local_ver = get_local_client_version(self.fluidd_client) + fl_remote_ver = get_remote_client_version(self.fluidd_client) + self.fl_local = self.format_local_status(fl_local_ver, fl_remote_ver) + self.fl_remote = f"{COLOR_GREEN if fl_remote_ver != 'ERROR' else COLOR_RED}{fl_remote_ver}{RESET_FORMAT}" + # mainsail-config - mc_status = get_client_config_status(self.mainsail_cient) - self.mc_local = mc_status.get("local") - self.mc_remote = mc_status.get("remote") - if self.mc_local == self.mc_remote: - self.mc_local = f"{COLOR_GREEN}{self.mc_local}{RESET_FORMAT}" - else: - self.mc_local = f"{COLOR_YELLOW}{self.mc_local}{RESET_FORMAT}" - self.mc_remote = f"{COLOR_GREEN}{self.mc_remote}{RESET_FORMAT}" + mc_status = get_client_config_status(self.mainsail_client) + self.mc_local = self.format_local_status( + mc_status.get("local"), mc_status.get("remote") + ) + self.mc_remote = f"{COLOR_GREEN}{mc_status.get('remote')}{RESET_FORMAT}" + # fluidd-config fc_status = get_client_config_status(self.fluidd_client) - self.fc_local = fc_status.get("local") - self.fc_remote = fc_status.get("remote") - if self.fc_local == self.mc_remote: - self.fc_local = f"{COLOR_GREEN}{self.fc_local}{RESET_FORMAT}" - else: - self.fc_local = f"{COLOR_YELLOW}{self.fc_local}{RESET_FORMAT}" - self.fc_remote = f"{COLOR_GREEN}{self.fc_remote}{RESET_FORMAT}" + self.fc_local = self.format_local_status( + fc_status.get("local"), fc_status.get("remote") + ) + self.fc_remote = f"{COLOR_GREEN}{fc_status.get('remote')}{RESET_FORMAT}" + + def format_local_status(self, local_version, remote_version) -> str: + if local_version == remote_version: + return f"{COLOR_GREEN}{local_version}{RESET_FORMAT}" + return f"{COLOR_YELLOW}{local_version}{RESET_FORMAT}" From 44301c0c874db0a213899742b0417bf2aa951cf0 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Apr 2024 19:07:42 +0200 Subject: [PATCH 157/296] feat: implement get-id feature Signed-off-by: Dominik Willner --- .../klipper_firmware/menus/klipper_flash_menu.py | 16 +++++++++++++--- kiauh/core/menus/advanced_menu.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index 4c20f2d..cda6301 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -8,6 +8,7 @@ # ======================================================================= # import textwrap +import time from components.klipper_firmware.flash_options import ( FlashOptions, @@ -131,9 +132,10 @@ class KlipperFlashCommandMenu(BaseMenu): # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperSelectMcuConnectionMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: BaseMenu, standalone: bool = False): super().__init__() + self.__standalone = standalone self.previous_menu: BaseMenu = previous_menu self.options = { "1": self.select_usb, @@ -193,8 +195,16 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): if len(self.flash_options.mcu_list) < 1: Logger.print_warn("No MCUs found!") Logger.print_warn("Make sure they are connected and repeat this step.") - else: - self.goto_next_menu() + + # if standalone is True, we only display the MCUs to the user and return + if self.__standalone and len(self.flash_options.mcu_list) > 0: + Logger.print_ok("The following MCUs were found:", prefix=False) + for i, mcu in enumerate(self.flash_options.mcu_list): + print(f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}") + time.sleep(3) + return + + self.goto_next_menu() def goto_next_menu(self, **kwargs): KlipperSelectMcuIdMenu(previous_menu=self).run() diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 3c07497..5d198ff 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -58,4 +58,4 @@ class AdvancedMenu(BaseMenu): KlipperFlashMethodMenu(previous_menu=self).run() def get_id(self, **kwargs): - KlipperSelectMcuConnectionMenu(previous_menu=self).run() + KlipperSelectMcuConnectionMenu(previous_menu=self, standalone=True).run() From 0b41d634967e8c99df3d7331dcb60e72fd29e48b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Apr 2024 19:35:26 +0200 Subject: [PATCH 158/296] feat: implement optional extension update entry point Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 2 +- kiauh/extensions/__init__.py | 12 ++++++++ kiauh/{core => extensions}/base_extension.py | 11 ++++---- .../menus => extensions}/extensions_menu.py | 28 ++++++++++++------- .../gcode_shell_cmd_extension.py | 2 +- .../mainsail_theme_installer_extension.py | 2 +- 6 files changed, 38 insertions(+), 19 deletions(-) rename kiauh/{core => extensions}/base_extension.py (81%) rename kiauh/{core/menus => extensions}/extensions_menu.py (86%) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 317584b..ea18b88 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -22,7 +22,7 @@ from core.menus import FooterType from core.menus.advanced_menu import AdvancedMenu from core.menus.backup_menu import BackupMenu from core.menus.base_menu import BaseMenu -from core.menus.extensions_menu import ExtensionsMenu +from extensions.extensions_menu import ExtensionsMenu from core.menus.install_menu import InstallMenu from core.menus.remove_menu import RemoveMenu from core.menus.settings_menu import SettingsMenu diff --git a/kiauh/extensions/__init__.py b/kiauh/extensions/__init__.py index e69de29..7e995bf 100644 --- a/kiauh/extensions/__init__.py +++ b/kiauh/extensions/__init__.py @@ -0,0 +1,12 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path + +EXTENSION_ROOT = Path(__file__).resolve().parents[1].joinpath("extensions") diff --git a/kiauh/core/base_extension.py b/kiauh/extensions/base_extension.py similarity index 81% rename from kiauh/core/base_extension.py rename to kiauh/extensions/base_extension.py index af80929..a14cbaa 100644 --- a/kiauh/core/base_extension.py +++ b/kiauh/extensions/base_extension.py @@ -19,12 +19,11 @@ class BaseExtension(ABC): @abstractmethod def install_extension(self, **kwargs) -> None: - raise NotImplementedError( - "Subclasses must implement the install_extension method" - ) + raise NotImplementedError + + def update_extension(self, **kwargs) -> None: + raise NotImplementedError @abstractmethod def remove_extension(self, **kwargs) -> None: - raise NotImplementedError( - "Subclasses must implement the remove_extension method" - ) + raise NotImplementedError diff --git a/kiauh/core/menus/extensions_menu.py b/kiauh/extensions/extensions_menu.py similarity index 86% rename from kiauh/core/menus/extensions_menu.py rename to kiauh/extensions/extensions_menu.py index 2d4e862..5e437d2 100644 --- a/kiauh/core/menus/extensions_menu.py +++ b/kiauh/extensions/extensions_menu.py @@ -14,7 +14,8 @@ import textwrap from pathlib import Path from typing import Type, Dict -from core.base_extension import BaseExtension +from extensions import EXTENSION_ROOT +from extensions.base_extension import BaseExtension from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW @@ -31,9 +32,8 @@ class ExtensionsMenu(BaseMenu): def discover_extensions(self) -> Dict[str, BaseExtension]: ext_dict = {} - extensions_dir = Path(__file__).resolve().parents[2].joinpath("extensions") - for ext in extensions_dir.iterdir(): + for ext in EXTENSION_ROOT.iterdir(): metadata_json = Path(ext).joinpath("metadata.json") if not metadata_json.exists(): continue @@ -95,16 +95,18 @@ class ExtensionSubmenu(BaseMenu): def __init__(self, previous_menu: BaseMenu, extension: BaseExtension): super().__init__() - self.previous_menu = previous_menu - self.options = { - "1": extension.install_extension, - "2": extension.remove_extension, - } - self.extension = extension self.extension_name = extension.metadata.get("display_name") self.extension_desc = extension.metadata.get("description") + self.previous_menu = previous_menu + self.options["1"] = extension.install_extension + if self.extension.metadata.get("updates"): + self.options["2"] = extension.update_extension + self.options["3"] = extension.remove_extension + else: + self.options["2"] = extension.remove_extension + def print_menu(self) -> None: header = f" [ {self.extension_name} ] " color = COLOR_YELLOW @@ -127,7 +129,13 @@ class ExtensionSubmenu(BaseMenu): """ |-------------------------------------------------------| | 1) Install | - | 2) Remove | """ )[1:] + + if self.extension.metadata.get("updates"): + menu += "| 2) Update |\n" + menu += "| 3) Remove |\n" + else: + menu += "| 2) Remove |\n" + print(menu, end="") diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index a18488f..a4cc7f7 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -13,7 +13,7 @@ from typing import List from components.klipper.klipper import Klipper from core.backup_manager.backup_manager import BackupManager -from core.base_extension import BaseExtension +from extensions.base_extension import BaseExtension from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager from extensions.gcode_shell_cmd import ( diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index 28e620f..96b68ce 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -19,7 +19,7 @@ from components.klipper.klipper_dialogs import ( print_instance_overview, DisplayType, ) -from core.base_extension import BaseExtension +from extensions.base_extension import BaseExtension from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager from core.menus.base_menu import BaseMenu From 409aa3da25e5e738656435f68a1891290cd23c85 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 10 Apr 2024 21:10:01 +0200 Subject: [PATCH 159/296] refactor: extend firmware flashing functionalities Signed-off-by: Dominik Willner --- kiauh/components/klipper_firmware/__init__.py | 12 + .../klipper_firmware/flash_options.py | 15 +- .../klipper_firmware/flash_utils.py | 80 ++++- .../menus/klipper_flash_error_menu.py | 99 ++++++ .../menus/klipper_flash_help_menu.py | 127 ++++++++ .../menus/klipper_flash_menu.py | 285 ++++++++++-------- kiauh/core/menus/__init__.py | 2 + kiauh/core/menus/advanced_menu.py | 6 +- kiauh/core/menus/base_menu.py | 6 + kiauh/core/menus/main_menu.py | 2 +- 10 files changed, 486 insertions(+), 148 deletions(-) create mode 100644 kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py create mode 100644 kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py diff --git a/kiauh/components/klipper_firmware/__init__.py b/kiauh/components/klipper_firmware/__init__.py index e69de29..f27ce38 100644 --- a/kiauh/components/klipper_firmware/__init__.py +++ b/kiauh/components/klipper_firmware/__init__.py @@ -0,0 +1,12 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 components.klipper import KLIPPER_DIR + +SD_FLASH_SCRIPT = KLIPPER_DIR.joinpath("scripts/flash-sdcard.sh") diff --git a/kiauh/components/klipper_firmware/flash_options.py b/kiauh/components/klipper_firmware/flash_options.py index 24564b5..47e51f9 100644 --- a/kiauh/components/klipper_firmware/flash_options.py +++ b/kiauh/components/klipper_firmware/flash_options.py @@ -13,8 +13,8 @@ from typing import Union, List class FlashMethod(Enum): - REGULAR = "REGULAR" - SD_CARD = "SD_CARD" + REGULAR = "Regular" + SD_CARD = "SD Card" class FlashCommand(Enum): @@ -24,7 +24,7 @@ class FlashCommand(Enum): class ConnectionType(Enum): USB = "USB" - USB_DFU = "USB_DFU" + USB_DFU = "USB (DFU)" UART = "UART" @@ -36,6 +36,7 @@ class FlashOptions: _mcu_list: List[str] = field(default_factory=list) _selected_mcu: str = "" _selected_board: str = "" + _selected_baudrate: int = 250000 def __new__(cls, *args, **kwargs): if not cls._instance: @@ -93,3 +94,11 @@ class FlashOptions: @selected_board.setter def selected_board(self, value: str) -> None: self._selected_board = value + + @property + def selected_baudrate(self) -> int: + return self._selected_baudrate + + @selected_baudrate.setter + def selected_baudrate(self, value: int) -> None: + self._selected_baudrate = value diff --git a/kiauh/components/klipper_firmware/flash_utils.py b/kiauh/components/klipper_firmware/flash_utils.py index 75b6610..b064e46 100644 --- a/kiauh/components/klipper_firmware/flash_utils.py +++ b/kiauh/components/klipper_firmware/flash_utils.py @@ -7,15 +7,35 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import subprocess from subprocess import CalledProcessError, check_output, Popen, PIPE, STDOUT from typing import List from components.klipper import KLIPPER_DIR -from components.klipper_firmware.flash_options import FlashOptions, FlashCommand +from components.klipper_firmware import SD_FLASH_SCRIPT +from components.klipper_firmware.flash_options import ( + FlashOptions, + FlashMethod, +) from utils.logger import Logger from utils.system_utils import log_process +def find_firmware_file(method: FlashMethod) -> bool: + target = KLIPPER_DIR.joinpath("out") + target_exists = target.exists() + if method is FlashMethod.REGULAR: + f1 = "klipper.elf.hex" + f2 = "klipper.elf" + fw_file_exists = target.joinpath(f1).exists() and target.joinpath(f2).exists() + elif method is FlashMethod.SD_CARD: + fw_file_exists = target.joinpath("klipper.bin").exists() + else: + raise Exception("Unknown flash method") + + return target_exists and fw_file_exists + + def find_usb_device_by_id() -> List[str]: try: command = "find /dev/serial/by-id/* 2>/dev/null" @@ -49,28 +69,62 @@ def find_usb_dfu_device() -> List[str]: return [] -def flash_device(flash_options: FlashOptions) -> None: +def get_sd_flash_board_list() -> List[str]: + if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists(): + return [] + try: + cmd = f"{SD_FLASH_SCRIPT} -l" + blist = subprocess.check_output(cmd, shell=True, text=True) + return blist.splitlines()[1:] + except subprocess.CalledProcessError as e: + Logger.print_error(f"An unexpected error occured:\n{e}") + + +def start_flash_process(flash_options: FlashOptions) -> None: + Logger.print_status(f"Flashing '{flash_options.selected_mcu}' ...") + try: + if not flash_options.flash_method: + raise Exception("Missing value for flash_method!") + if not flash_options.flash_command: + raise Exception("Missing value for flash_command!") if not flash_options.selected_mcu: raise Exception("Missing value for selected_mcu!") + if not flash_options.connection_type: + raise Exception("Missing value for connection_type!") + if ( + flash_options.flash_method == FlashMethod.SD_CARD + and not flash_options.selected_board + ): + raise Exception("Missing value for selected_board!") - if flash_options.flash_command is FlashCommand.FLASH: - command = [ + if flash_options.flash_method is FlashMethod.REGULAR: + cmd = [ "make", flash_options.flash_command.value, f"FLASH_DEVICE={flash_options.selected_mcu}", ] - process = Popen( - command, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True - ) + elif flash_options.flash_method is FlashMethod.SD_CARD: + if not SD_FLASH_SCRIPT.exists(): + raise Exception("Unable to find Klippers sdcard flash script!") + cmd = [ + SD_FLASH_SCRIPT, + "-b", + flash_options.selected_baudrate, + flash_options.selected_mcu, + flash_options.selected_board, + ] + else: + raise Exception("Invalid value for flash_method!") - log_process(process) + process = Popen(cmd, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True) + log_process(process) - rc = process.returncode - if rc != 0: - raise Exception(f"Flashing failed with returncode: {rc}") - else: - Logger.print_ok("Flashing successfull!", start="\n", end="\n\n") + rc = process.returncode + if rc != 0: + raise Exception(f"Flashing failed with returncode: {rc}") + else: + Logger.print_ok("Flashing successfull!", start="\n", end="\n\n") except (Exception, CalledProcessError): Logger.print_error("Flashing failed!", start="\n") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py new file mode 100644 index 0000000..beff44a --- /dev/null +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py @@ -0,0 +1,99 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import textwrap + +from components.klipper_firmware.flash_options import FlashOptions, FlashMethod +from core.menus import FooterType +from core.menus.base_menu import BaseMenu +from utils.constants import COLOR_RED, RESET_FORMAT + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperNoFirmwareErrorMenu(BaseMenu): + def __init__(self): + super().__init__() + + self.flash_options = FlashOptions() + self.options = {"": self.go_back} + self.default_options = self.go_back + self.footer_type = FooterType.BLANK + self.input_label_txt = "Press ENTER to go back to [Advanced Menu]" + + def print_menu(self) -> None: + header = "!!! NO FIRMWARE FILE FOUND !!!" + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + line1 = f"{color}Unable to find a compiled firmware file!{RESET_FORMAT}" + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {line1:<62} | + | | + | Make sure, that: | + | ● the folder '~/klipper/out' and its content exist | + | ● the folder contains the following file: | + """ + )[1:] + + if self.flash_options.flash_method is FlashMethod.REGULAR: + menu += "| ● 'klipper.elf' |\n" + menu += "| ● 'klipper.elf.hex' |\n" + else: + menu += "| ● 'klipper.bin' |\n" + + print(menu, end="") + + def go_back(self, **kwargs) -> None: + from core.menus.advanced_menu import AdvancedMenu + + AdvancedMenu().run() + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperNoBoardTypesErrorMenu(BaseMenu): + def __init__(self): + super().__init__() + + self.options = {"": self.go_back} + self.default_options = self.go_back + self.footer_type = FooterType.BLANK + self.input_label_txt = "Press ENTER to go back to [Main Menu]" + + def print_menu(self) -> None: + header = "!!! ERROR GETTING BOARD LIST !!!" + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + line1 = f"{color}Reading the list of supported boards failed!{RESET_FORMAT}" + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {line1:<62} | + | | + | Make sure, that: | + | ● the folder '~/klipper' and all its content exist | + | ● the content of folder '~/klipper' is not currupted | + | ● the file '~/klipper/scripts/flash-sd.py' exist | + | ● your current user has access to those files/folders | + | | + | If in doubt or this process continues to fail, please | + | consider to download Klipper again. | + """ + )[1:] + print(menu, end="") + + def go_back(self, **kwargs) -> None: + from core.menus.main_menu import MainMenu + + MainMenu().run() diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py new file mode 100644 index 0000000..16ebdcb --- /dev/null +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py @@ -0,0 +1,127 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import textwrap + +from core.menus.base_menu import BaseMenu +from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW + + +class KlipperFlashMethodHelpMenu(BaseMenu): + def __init__(self, previous_menu: BaseMenu): + super().__init__() + + self.previous_menu: BaseMenu = previous_menu + + def print_menu(self) -> None: + header = " < ? > Help: Flash MCU < ? > " + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) + subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}" + subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}" + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {subheader1:<62} | + | The default method to flash controller boards which | + | are connected and updated over USB and not by placing | + | a compiled firmware file onto an internal SD-Card. | + | | + | Common controllers that get flashed that way are: | + | - Arduino Mega 2560 | + | - Fysetc F6 / S6 (used without a Display + SD-Slot) | + | | + | {subheader2:<62} | + | Many popular controller boards ship with a bootloader | + | capable of updating the firmware via SD-Card. | + | Choose this method if your controller board supports | + | this way of updating. This method ONLY works for up- | + | grading firmware. The initial flashing procedure must | + | be done manually per the instructions that apply to | + | your controller board. | + | | + | Common controllers that can be flashed that way are: | + | - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 | + | - Fysetc F6 / S6 (used with a Display + SD-Slot) | + | - Fysetc Spider | + | | + """ + )[1:] + print(menu, end="") + + +class KlipperFlashCommandHelpMenu(BaseMenu): + def __init__(self, previous_menu: BaseMenu): + super().__init__() + + self.previous_menu: BaseMenu = previous_menu + + def print_menu(self) -> None: + header = " < ? > Help: Flash MCU < ? > " + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) + subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}" + subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}" + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {subheader1:<62} | + | The default command to flash controller board, it | + | will detect selected microcontroller and use suitable | + | tool for flashing it. | + | | + | {subheader2:<62} | + | Special command to flash STM32 microcontrollers in | + | DFU mode but connected via serial. stm32flash command | + | will be used internally. | + | | + """ + )[1:] + print(menu, end="") + + +class KlipperMcuConnectionHelpMenu(BaseMenu): + def __init__(self, previous_menu: BaseMenu): + super().__init__() + + self.previous_menu: BaseMenu = previous_menu + + def print_menu(self) -> None: + header = " < ? > Help: Flash MCU < ? > " + color = COLOR_YELLOW + count = 62 - len(color) - len(RESET_FORMAT) + subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}" + subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}" + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | {subheader1:<62} | + | Selecting USB as the connection method will scan the | + | USB ports for connected controller boards. This will | + | be similar to the 'ls /dev/serial/by-id/*' command | + | suggested by the official Klipper documentation for | + | determining successfull USB connections! | + | | + | {subheader2:<62} | + | Selecting UART as the connection method will list all | + | possible UART serial ports. Note: This method ALWAYS | + | returns something as it seems impossible to determine | + | if a valid Klipper controller board is connected or | + | not. Because of that, you MUST know which UART serial | + | port your controller board is connected to when using | + | this connection method. | + | | + """ + )[1:] + print(menu, end="") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index cda6301..9b1b7cd 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -20,13 +20,24 @@ from components.klipper_firmware.flash_utils import ( find_usb_device_by_id, find_uart_device, find_usb_dfu_device, - flash_device, + get_sd_flash_board_list, + start_flash_process, + find_firmware_file, +) +from components.klipper_firmware.menus.klipper_flash_error_menu import ( + KlipperNoBoardTypesErrorMenu, + KlipperNoFirmwareErrorMenu, +) +from components.klipper_firmware.menus.klipper_flash_help_menu import ( + KlipperMcuConnectionHelpMenu, + KlipperFlashCommandHelpMenu, + KlipperFlashMethodHelpMenu, ) from core.menus import FooterType from core.menus.base_menu import BaseMenu from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_RED -from utils.input_utils import get_confirm +from utils.input_utils import get_number_input from utils.logger import Logger @@ -48,7 +59,11 @@ class KlipperFlashMethodMenu(BaseMenu): self.flash_options = FlashOptions() def print_menu(self) -> None: - header = " [ Flash MCU ] " + header = " [ MCU Flash Menu ] " + subheader = f"{COLOR_YELLOW}ATTENTION:{RESET_FORMAT}" + subline1 = f"{COLOR_YELLOW}Make sure to select the correct method for the MCU!{RESET_FORMAT}" + subline2 = f"{COLOR_YELLOW}Not all MCUs support both methods!{RESET_FORMAT}" + color = COLOR_CYAN count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( @@ -56,9 +71,11 @@ class KlipperFlashMethodMenu(BaseMenu): /=======================================================\\ | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| - | Please select the flashing method to flash your MCU. | - | Make sure to only select a method your MCU supports. | - | Not all MCUs support both methods! | + | Select the flash method for flashing the MCU. | + | | + | {subheader:<62} | + | {subline1:<62} | + | {subline2:<62} | |-------------------------------------------------------| | | | 1) Regular flashing method | @@ -77,7 +94,10 @@ class KlipperFlashMethodMenu(BaseMenu): self.goto_next_menu() def goto_next_menu(self, **kwargs): - KlipperFlashCommandMenu(previous_menu=self).run() + if find_firmware_file(self.flash_options.flash_method): + KlipperFlashCommandMenu(previous_menu=self).run() + else: + KlipperNoFirmwareErrorMenu().run() def help_menu(self, **kwargs): KlipperFlashMethodHelpMenu(previous_menu=self).run() @@ -256,132 +276,139 @@ class KlipperSelectMcuIdMenu(BaseMenu): selected_mcu = self.mcu_list[index] self.flash_options.selected_mcu = selected_mcu - print(f"{COLOR_CYAN}###### You selected:{RESET_FORMAT}") - print(f"● MCU #{index}: {selected_mcu}\n") + if self.flash_options.flash_method == FlashMethod.SD_CARD: + KlipperSelectSDFlashBoardMenu(previous_menu=self).run() + elif self.flash_options.flash_method == FlashMethod.REGULAR: + KlipperFlashOverviewMenu(previous_menu=self).run() - if get_confirm("Continue", allow_go_back=True): - Logger.print_status(f"Flashing '{selected_mcu}' ...") - flash_device(self.flash_options) - self.goto_next_menu() +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperSelectSDFlashBoardMenu(BaseMenu): + def __init__(self, previous_menu: BaseMenu): + super().__init__() - def goto_next_menu(self, **kwargs): - from core.menus.main_menu import MainMenu + self.previous_menu: BaseMenu = previous_menu + self.flash_options = FlashOptions() + self.available_boards = get_sd_flash_board_list() + self.input_label_txt = "Select board type" + + options = {f"{i}": self.board_select for i in range(len(self.available_boards))} + self.options = options + + def print_menu(self) -> None: + if len(self.available_boards) < 1: + KlipperNoBoardTypesErrorMenu().run() + else: + menu = textwrap.dedent( + """ + /=======================================================\\ + | Please select the type of board that corresponds to | + | the currently selected MCU ID you chose before. | + | | + | The following boards are currently supported: | + |-------------------------------------------------------| + """ + )[1:] + + for i, board in enumerate(self.available_boards): + line = f" {i}) {board}" + menu += f"|{line:<55}|\n" + + print(menu, end="") + + def board_select(self, **kwargs): + board = int(kwargs.get("opt_index")) + self.flash_options.selected_board = self.available_boards[board] + self.baudrate_select() + + def baudrate_select(self, **kwargs): + menu = textwrap.dedent( + """ + /=======================================================\\ + | If your board is flashed with firmware that connects | + | at a custom baud rate, please change it now. | + | | + | If you are unsure, stick to the default 250000! | + \\=======================================================/ + """ + )[1:] + print(menu, end="") + self.flash_options.selected_baudrate = get_number_input( + question="Please set the baud rate", + default=250000, + min_count=0, + allow_go_back=True, + ) + KlipperFlashOverviewMenu(previous_menu=self).run() + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperFlashOverviewMenu(BaseMenu): + def __init__(self, previous_menu: BaseMenu): + super().__init__() + + self.previous_menu: BaseMenu = previous_menu + self.flash_options = FlashOptions() + self.options = {"Y": self.execute_flash, "N": self.abort_process} + self.input_label_txt = "Perform action (default=Y)" + self.default_option = self.execute_flash + + def print_menu(self) -> None: + header = "!!! ATTENTION !!!" + color = COLOR_RED + count = 62 - len(color) - len(RESET_FORMAT) + + method = self.flash_options.flash_method.value + command = self.flash_options.flash_command.value + conn_type = self.flash_options.connection_type.value + mcu = self.flash_options.selected_mcu + board = self.flash_options.selected_board + baudrate = self.flash_options.selected_baudrate + subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]" + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Before contuining the flashing process, please check | + | if all parameters were set correctly! Once you made | + | sure everything is correct, start the process. If any | + | parameter needs to be changed, you can go back (B) | + | step by step or abort and start from the beginning. | + |{subheader:-^64}| + + """ + )[1:] + + menu += f" ● MCU: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n" + menu += f" ● Connection: {COLOR_CYAN}{conn_type}{RESET_FORMAT}\n" + menu += f" ● Flash method: {COLOR_CYAN}{method}{RESET_FORMAT}\n" + menu += f" ● Flash command: {COLOR_CYAN}{command}{RESET_FORMAT}\n" + + if self.flash_options.flash_method is FlashMethod.SD_CARD: + menu += f" ● Board type: {COLOR_CYAN}{board}{RESET_FORMAT}\n" + menu += f" ● Baudrate: {COLOR_CYAN}{baudrate}{RESET_FORMAT}\n" + + menu += textwrap.dedent( + """ + |-------------------------------------------------------| + | Y) Start flash process | + | N) Abort - Return to Advanced Menu | + """ + ) + print(menu, end="") + + def execute_flash(self, **kwargs): from core.menus.advanced_menu import AdvancedMenu - AdvancedMenu(previous_menu=MainMenu()).run() + start_flash_process(self.flash_options) + Logger.print_info("Returning to MCU Flash Menu in 5 seconds ...") + time.sleep(5) + KlipperFlashMethodMenu(previous_menu=AdvancedMenu()).run() + def abort_process(self, **kwargs): + from core.menus.advanced_menu import AdvancedMenu -class KlipperFlashMethodHelpMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): - super().__init__() - - self.previous_menu: BaseMenu = previous_menu - - def print_menu(self) -> None: - header = " < ? > Help: Flash MCU < ? > " - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) - subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}" - subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}" - menu = textwrap.dedent( - f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {subheader1:<62} | - | The default method to flash controller boards which | - | are connected and updated over USB and not by placing | - | a compiled firmware file onto an internal SD-Card. | - | | - | Common controllers that get flashed that way are: | - | - Arduino Mega 2560 | - | - Fysetc F6 / S6 (used without a Display + SD-Slot) | - | | - | {subheader2:<62} | - | Many popular controller boards ship with a bootloader | - | capable of updating the firmware via SD-Card. | - | Choose this method if your controller board supports | - | this way of updating. This method ONLY works for up- | - | grading firmware. The initial flashing procedure must | - | be done manually per the instructions that apply to | - | your controller board. | - | | - | Common controllers that can be flashed that way are: | - | - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 | - | - Fysetc F6 / S6 (used with a Display + SD-Slot) | - | - Fysetc Spider | - | | - """ - )[1:] - print(menu, end="") - - -class KlipperFlashCommandHelpMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): - super().__init__() - - self.previous_menu: BaseMenu = previous_menu - - def print_menu(self) -> None: - header = " < ? > Help: Flash MCU < ? > " - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) - subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}" - subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}" - menu = textwrap.dedent( - f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {subheader1:<62} | - | The default command to flash controller board, it | - | will detect selected microcontroller and use suitable | - | tool for flashing it. | - | | - | {subheader2:<62} | - | Special command to flash STM32 microcontrollers in | - | DFU mode but connected via serial. stm32flash command | - | will be used internally. | - | | - """ - )[1:] - print(menu, end="") - - -class KlipperMcuConnectionHelpMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): - super().__init__() - - self.previous_menu: BaseMenu = previous_menu - - def print_menu(self) -> None: - header = " < ? > Help: Flash MCU < ? > " - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) - subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}" - subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}" - menu = textwrap.dedent( - f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {subheader1:<62} | - | Selecting USB as the connection method will scan the | - | USB ports for connected controller boards. This will | - | be similar to the 'ls /dev/serial/by-id/*' command | - | suggested by the official Klipper documentation for | - | determining successfull USB connections! | - | | - | {subheader2:<62} | - | Selecting UART as the connection method will list all | - | possible UART serial ports. Note: This method ALWAYS | - | returns something as it seems impossible to determine | - | if a valid Klipper controller board is connected or | - | not. Because of that, you MUST know which UART serial | - | port your controller board is connected to when using | - | this connection method. | - | | - """ - )[1:] - print(menu, end="") + AdvancedMenu().run() diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index 670d68e..ed37c68 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -14,12 +14,14 @@ class FooterType(Enum): QUIT = "QUIT" BACK = "BACK" BACK_HELP = "BACK_HELP" + BLANK = "BLANK" NAVI_OPTIONS = { FooterType.QUIT: ["q"], FooterType.BACK: ["b"], FooterType.BACK_HELP: ["b", "h"], + FooterType.BLANK: [], } diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 5d198ff..3000ba9 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -19,10 +19,12 @@ from utils.constants import COLOR_YELLOW, RESET_FORMAT # noinspection PyUnusedLocal class AdvancedMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self): super().__init__() - self.previous_menu: BaseMenu = previous_menu + from core.menus.main_menu import MainMenu + + self.previous_menu: BaseMenu = MainMenu() self.options = { "1": None, "2": None, diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 509b46f..0637f72 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -92,6 +92,10 @@ def print_back_help_footer(): print(footer, end="") +def print_blank_footer(): + print("\=======================================================/") + + Options = Dict[str, Callable] @@ -119,6 +123,8 @@ class BaseMenu(ABC): print_back_footer() elif self.footer_type is FooterType.BACK_HELP: print_back_help_footer() + elif self.footer_type is FooterType.BLANK: + print_blank_footer() else: raise NotImplementedError("Method for printing footer not implemented.") diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index ea18b88..5cf45fd 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -152,7 +152,7 @@ class MainMenu(BaseMenu): RemoveMenu(previous_menu=self).run() def advanced_menu(self, **kwargs): - AdvancedMenu(previous_menu=self).run() + AdvancedMenu().run() def backup_menu(self, **kwargs): BackupMenu(previous_menu=self).run() From bb769fdf6d3c21cf415eb1a556537f0124fd80cd Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 10 Apr 2024 22:41:02 +0200 Subject: [PATCH 160/296] fix: hitting 'b' or 'h' in main menu raises exception Signed-off-by: Dominik Willner --- .../menus/klipper_flash_error_menu.py | 6 +- .../menus/klipper_flash_menu.py | 15 +---- kiauh/core/menus/__init__.py | 8 --- kiauh/core/menus/base_menu.py | 61 ++++++++++++------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py index beff44a..a54040d 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py @@ -21,8 +21,7 @@ class KlipperNoFirmwareErrorMenu(BaseMenu): super().__init__() self.flash_options = FlashOptions() - self.options = {"": self.go_back} - self.default_options = self.go_back + self.default_option = self.go_back self.footer_type = FooterType.BLANK self.input_label_txt = "Press ENTER to go back to [Advanced Menu]" @@ -64,8 +63,7 @@ class KlipperNoBoardTypesErrorMenu(BaseMenu): def __init__(self): super().__init__() - self.options = {"": self.go_back} - self.default_options = self.go_back + self.default_option = self.go_back self.footer_type = FooterType.BLANK self.input_label_txt = "Press ENTER to go back to [Main Menu]" diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index 9b1b7cd..27f1269 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -48,10 +48,10 @@ class KlipperFlashMethodMenu(BaseMenu): super().__init__() self.previous_menu: BaseMenu = previous_menu + self.help_menu = KlipperFlashMethodHelpMenu self.options = { "1": self.select_regular, "2": self.select_sdcard, - "h": self.help_menu, } self.input_label_txt = "Select flash method" self.footer_type = FooterType.BACK_HELP @@ -99,9 +99,6 @@ class KlipperFlashMethodMenu(BaseMenu): else: KlipperNoFirmwareErrorMenu().run() - def help_menu(self, **kwargs): - KlipperFlashMethodHelpMenu(previous_menu=self).run() - # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic @@ -110,10 +107,10 @@ class KlipperFlashCommandMenu(BaseMenu): super().__init__() self.previous_menu: BaseMenu = previous_menu + self.help_menu = KlipperFlashCommandHelpMenu self.options = { "1": self.select_flash, "2": self.select_serialflash, - "h": self.help_menu, } self.default_option = self.select_flash self.input_label_txt = "Select flash command" @@ -145,9 +142,6 @@ class KlipperFlashCommandMenu(BaseMenu): def goto_next_menu(self, **kwargs): KlipperSelectMcuConnectionMenu(previous_menu=self).run() - def help_menu(self, **kwargs): - KlipperFlashCommandHelpMenu(previous_menu=self).run() - # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic @@ -157,11 +151,11 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): self.__standalone = standalone self.previous_menu: BaseMenu = previous_menu + self.help_menu = KlipperMcuConnectionHelpMenu self.options = { "1": self.select_usb, "2": self.select_dfu, "3": self.select_usb_dfu, - "h": self.help_menu, } self.input_label_txt = "Select connection type" self.footer_type = FooterType.BACK_HELP @@ -229,9 +223,6 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): def goto_next_menu(self, **kwargs): KlipperSelectMcuIdMenu(previous_menu=self).run() - def help_menu(self, **kwargs): - KlipperMcuConnectionHelpMenu(previous_menu=self).run() - # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index ed37c68..06c68b2 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -17,14 +17,6 @@ class FooterType(Enum): BLANK = "BLANK" -NAVI_OPTIONS = { - FooterType.QUIT: ["q"], - FooterType.BACK: ["b"], - FooterType.BACK_HELP: ["b", "h"], - FooterType.BLANK: [], -} - - class ExitAppException(Exception): pass diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 0637f72..84f4a13 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -12,10 +12,10 @@ from __future__ import annotations import subprocess import sys import textwrap -from abc import abstractmethod, ABC +from abc import abstractmethod from typing import Dict, Union, Callable, Type, Tuple -from core.menus import FooterType, NAVI_OPTIONS, ExitAppException, GoBackException +from core.menus import FooterType, ExitAppException, GoBackException from utils.constants import ( COLOR_GREEN, COLOR_YELLOW, @@ -96,22 +96,51 @@ def print_blank_footer(): print("\=======================================================/") -Options = Dict[str, Callable] +class PostInitCaller(type): + def __call__(cls, *args, **kwargs): + obj = type.__call__(cls, *args, **kwargs) + obj.__post_init__() + return obj -class BaseMenu(ABC): - options: Options = {} +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class BaseMenu(metaclass=PostInitCaller): + options: Dict[str, Callable] = {} options_offset: int = 0 default_option: Union[Callable, None] = None input_label_txt: str = "Perform action" header: bool = False previous_menu: Union[Type[BaseMenu], BaseMenu] = None + help_menu: Type[BaseMenu] = None footer_type: FooterType = FooterType.BACK - def __init__(self): + def __init__(self, **kwargs): if type(self) is BaseMenu: raise NotImplementedError("BaseMenu cannot be instantiated directly.") + def __post_init__(self): + # conditionally add options based on footer type + if self.footer_type is FooterType.QUIT: + self.options["q"] = self.__exit + if self.footer_type is FooterType.BACK: + self.options["b"] = self.__go_back + if self.footer_type is FooterType.BACK_HELP: + self.options["b"] = self.__go_back + self.options["h"] = self.__go_to_help + # if defined, add the default option to the options dict + if self.default_option is not None: + self.options[""] = self.default_option + + def __go_back(self, **kwargs): + raise GoBackException() + + def __go_to_help(self, **kwargs): + self.help_menu(previous_menu=self).run() + + def __exit(self, **kwargs): + raise ExitAppException() + @abstractmethod def print_menu(self) -> None: raise NotImplementedError("Subclasses must implement the print_menu method") @@ -144,20 +173,8 @@ class BaseMenu(ABC): usr_input = usr_input.lower() option = self.options.get(usr_input, None) - # check if usr_input contains a character used for basic navigation, e.g. b, h or q - # and if the current menu has the appropriate footer to allow for that action - is_valid_navigation = self.footer_type in NAVI_OPTIONS - user_navigated = usr_input in NAVI_OPTIONS[self.footer_type] - if is_valid_navigation and user_navigated: - if usr_input == "q": - raise ExitAppException() - elif usr_input == "b": - raise GoBackException() - elif usr_input == "h": - return option, usr_input - - # if usr_input is None or an empty string, we execute the menues default option if specified - if usr_input == "" and self.default_option is not None: + # if option/usr_input is None/empty string, we execute the menus default option if specified + if (option is None or usr_input == "") and self.default_option is not None: return self.default_option, usr_input # user selected a regular option @@ -168,8 +185,10 @@ class BaseMenu(ABC): while True: print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="") usr_input = input().lower() + validated_input = self.validate_user_input(usr_input) + method_to_call = validated_input[0] - if (validated_input := self.validate_user_input(usr_input)) is not None: + if method_to_call is not None: return validated_input else: Logger.print_error("Invalid input!", False) From da4c5fe1096425b5783bdfdc21db494230307a4a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 14 Apr 2024 22:11:40 +0200 Subject: [PATCH 161/296] refactor: rework of menu lifecycle and option handling Signed-off-by: Dominik Willner --- .../klipper/menus/klipper_remove_menu.py | 33 +++-- .../klipper_firmware/flash_utils.py | 131 ------------------ .../log_uploads/menus/log_upload_menu.py | 21 ++- .../moonraker/menus/moonraker_remove_menu.py | 34 +++-- .../webui_client/menus/client_remove_menu.py | 31 +++-- kiauh/core/menus/__init__.py | 26 ++-- kiauh/core/menus/backup_menu.py | 39 ++++-- kiauh/core/menus/base_menu.py | 71 +++++----- kiauh/core/menus/install_menu.py | 28 ++-- kiauh/core/menus/main_menu.py | 45 +++--- kiauh/core/menus/remove_menu.py | 38 ++--- kiauh/core/menus/settings_menu.py | 13 +- kiauh/core/menus/update_menu.py | 41 +++--- kiauh/extensions/extensions_menu.py | 52 ++++--- 14 files changed, 290 insertions(+), 313 deletions(-) delete mode 100644 kiauh/components/klipper_firmware/flash_utils.py diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index dca26f9..d079265 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -8,34 +8,41 @@ # ======================================================================= # import textwrap +from typing import Type, Optional from components.klipper import klipper_remove -from core.menus import FooterType +from core.menus import FooterType, Option from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class KlipperRemoveMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - - self.previous_menu: BaseMenu = previous_menu - self.options = { - "0": self.toggle_all, - "1": self.toggle_remove_klipper_service, - "2": self.toggle_remove_klipper_dir, - "3": self.toggle_remove_klipper_env, - "4": self.toggle_delete_klipper_logs, - "c": self.run_removal_process, - } self.footer_type = FooterType.BACK_HELP - self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False self.delete_klipper_logs = False + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.remove_menu import RemoveMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else RemoveMenu + ) + + def set_options(self) -> None: + self.options = { + "0": Option(method=self.toggle_all, menu=False), + "1": Option(method=self.toggle_remove_klipper_service, menu=False), + "2": Option(method=self.toggle_remove_klipper_dir, menu=False), + "3": Option(method=self.toggle_remove_klipper_env, menu=False), + "4": Option(method=self.toggle_delete_klipper_logs, menu=False), + "c": Option(method=self.run_removal_process, menu=False), + } + def print_menu(self) -> None: header = " [ Remove Klipper ] " color = COLOR_RED diff --git a/kiauh/components/klipper_firmware/flash_utils.py b/kiauh/components/klipper_firmware/flash_utils.py deleted file mode 100644 index b064e46..0000000 --- a/kiauh/components/klipper_firmware/flash_utils.py +++ /dev/null @@ -1,131 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import subprocess -from subprocess import CalledProcessError, check_output, Popen, PIPE, STDOUT -from typing import List - -from components.klipper import KLIPPER_DIR -from components.klipper_firmware import SD_FLASH_SCRIPT -from components.klipper_firmware.flash_options import ( - FlashOptions, - FlashMethod, -) -from utils.logger import Logger -from utils.system_utils import log_process - - -def find_firmware_file(method: FlashMethod) -> bool: - target = KLIPPER_DIR.joinpath("out") - target_exists = target.exists() - if method is FlashMethod.REGULAR: - f1 = "klipper.elf.hex" - f2 = "klipper.elf" - fw_file_exists = target.joinpath(f1).exists() and target.joinpath(f2).exists() - elif method is FlashMethod.SD_CARD: - fw_file_exists = target.joinpath("klipper.bin").exists() - else: - raise Exception("Unknown flash method") - - return target_exists and fw_file_exists - - -def find_usb_device_by_id() -> List[str]: - try: - command = "find /dev/serial/by-id/* 2>/dev/null" - output = check_output(command, shell=True, text=True) - return output.splitlines() - except CalledProcessError as e: - Logger.print_error("Unable to find a USB device!") - Logger.print_error(e, prefix=False) - return [] - - -def find_uart_device() -> List[str]: - try: - command = '"find /dev -maxdepth 1 -regextype posix-extended -regex "^\/dev\/tty(AMA0|S0)$" 2>/dev/null"' - output = check_output(command, shell=True, text=True) - return output.splitlines() - except CalledProcessError as e: - Logger.print_error("Unable to find a UART device!") - Logger.print_error(e, prefix=False) - return [] - - -def find_usb_dfu_device() -> List[str]: - try: - command = '"lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null"' - output = check_output(command, shell=True, text=True) - return output.splitlines() - except CalledProcessError as e: - Logger.print_error("Unable to find a USB DFU device!") - Logger.print_error(e, prefix=False) - return [] - - -def get_sd_flash_board_list() -> List[str]: - if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists(): - return [] - - try: - cmd = f"{SD_FLASH_SCRIPT} -l" - blist = subprocess.check_output(cmd, shell=True, text=True) - return blist.splitlines()[1:] - except subprocess.CalledProcessError as e: - Logger.print_error(f"An unexpected error occured:\n{e}") - - -def start_flash_process(flash_options: FlashOptions) -> None: - Logger.print_status(f"Flashing '{flash_options.selected_mcu}' ...") - try: - if not flash_options.flash_method: - raise Exception("Missing value for flash_method!") - if not flash_options.flash_command: - raise Exception("Missing value for flash_command!") - if not flash_options.selected_mcu: - raise Exception("Missing value for selected_mcu!") - if not flash_options.connection_type: - raise Exception("Missing value for connection_type!") - if ( - flash_options.flash_method == FlashMethod.SD_CARD - and not flash_options.selected_board - ): - raise Exception("Missing value for selected_board!") - - if flash_options.flash_method is FlashMethod.REGULAR: - cmd = [ - "make", - flash_options.flash_command.value, - f"FLASH_DEVICE={flash_options.selected_mcu}", - ] - elif flash_options.flash_method is FlashMethod.SD_CARD: - if not SD_FLASH_SCRIPT.exists(): - raise Exception("Unable to find Klippers sdcard flash script!") - cmd = [ - SD_FLASH_SCRIPT, - "-b", - flash_options.selected_baudrate, - flash_options.selected_mcu, - flash_options.selected_board, - ] - else: - raise Exception("Invalid value for flash_method!") - - process = Popen(cmd, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True) - log_process(process) - - rc = process.returncode - if rc != 0: - raise Exception(f"Flashing failed with returncode: {rc}") - else: - Logger.print_ok("Flashing successfull!", start="\n", end="\n\n") - - except (Exception, CalledProcessError): - Logger.print_error("Flashing failed!", start="\n") - Logger.print_error("See the console output above!", end="\n\n") diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index 35ec143..3129c52 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -8,22 +8,33 @@ # ======================================================================= # import textwrap +from typing import Type, Optional from components.log_uploads.log_upload_utils import get_logfile_list from components.log_uploads.log_upload_utils import upload_logfile +from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_YELLOW # noinspection PyMethodMayBeStatic class LogUploadMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self): super().__init__() - - self.previous_menu: BaseMenu = previous_menu self.logfile_list = get_logfile_list() - options = {f"{index}": self.upload for index in range(len(self.logfile_list))} - self.options = options + + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.main_menu import MainMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else MainMenu + ) + + def set_options(self) -> None: + self.options = { + f"{index}": Option(self.upload, False, opt_index=f"{index}") + for index in range(len(self.logfile_list)) + } def print_menu(self): header = " [ Log Upload ] " diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 6a55078..a72b61c 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -8,34 +8,42 @@ # ======================================================================= # import textwrap +from typing import Type, Optional from components.moonraker import moonraker_remove +from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class MoonrakerRemoveMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - - self.previous_menu: BaseMenu = previous_menu - self.options = { - "0": self.toggle_all, - "1": self.toggle_remove_moonraker_service, - "2": self.toggle_remove_moonraker_dir, - "3": self.toggle_remove_moonraker_env, - "4": self.toggle_remove_moonraker_polkit, - "5": self.toggle_delete_moonraker_logs, - "c": self.run_removal_process, - } - self.remove_moonraker_service = False self.remove_moonraker_dir = False self.remove_moonraker_env = False self.remove_moonraker_polkit = False self.delete_moonraker_logs = False + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.remove_menu import RemoveMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else RemoveMenu + ) + + def set_options(self) -> None: + self.options = { + "0": Option(method=self.toggle_all, menu=False), + "1": Option(method=self.toggle_remove_moonraker_service, menu=False), + "2": Option(method=self.toggle_remove_moonraker_dir, menu=False), + "3": Option(method=self.toggle_remove_moonraker_env, menu=False), + "4": Option(method=self.toggle_remove_moonraker_polkit, menu=False), + "5": Option(method=self.toggle_delete_moonraker_logs, menu=False), + "c": Option(method=self.run_removal_process, menu=False), + } + def print_menu(self) -> None: header = " [ Remove Moonraker ] " color = COLOR_RED diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index f6e99a0..ed11e0a 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -8,35 +8,42 @@ # ======================================================================= # import textwrap -from typing import Callable, Dict +from typing import Dict, Type, Optional from components.webui_client import client_remove from components.webui_client.base_data import BaseWebClient, WebClientType +from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN # noinspection PyUnusedLocal class ClientRemoveMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu, client: BaseWebClient): + def __init__( + self, client: BaseWebClient, previous_menu: Optional[Type[BaseMenu]] = None + ): super().__init__() - self.previous_menu = previous_menu - self.options = self.get_options(client) - self.client = client self.rm_client = False self.rm_client_config = False self.backup_mainsail_config_json = False - def get_options(self, client: BaseWebClient) -> Dict[str, Callable]: + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.remove_menu import RemoveMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else RemoveMenu + ) + + def set_options(self) -> Dict[str, Option]: options = { - "0": self.toggle_all, - "1": self.toggle_rm_client, - "2": self.toggle_rm_client_config, - "c": self.run_removal_process, + "0": Option(method=self.toggle_all, menu=False), + "1": Option(method=self.toggle_rm_client, menu=False), + "2": Option(method=self.toggle_rm_client_config, menu=False), + "c": Option(method=self.run_removal_process, menu=False), } - if client.client == WebClientType.MAINSAIL: - options["3"] = self.toggle_backup_mainsail_config_json + if self.client.client == WebClientType.MAINSAIL: + options["3"] = Option(self.toggle_backup_mainsail_config_json, False) return options diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index 06c68b2..8102e76 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -7,7 +7,25 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from dataclasses import dataclass from enum import Enum +from typing import Callable, Any, Union + + +@dataclass +class Option: + """ + Represents a menu option. + :param method: Method that will be used to call the menu option + :param menu: Flag for singaling that another menu will be opened + :param opt_index: Can be used to pass the user input to the menu option + :param opt_data: Can be used to pass any additional data to the menu option + """ + + method: Union[Callable, None] = None + menu: bool = False + opt_index: str = "" + opt_data: Any = None class FooterType(Enum): @@ -15,11 +33,3 @@ class FooterType(Enum): BACK = "BACK" BACK_HELP = "BACK_HELP" BLANK = "BLANK" - - -class ExitAppException(Exception): - pass - - -class GoBackException(Exception): - pass diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 72f1735..0eb8698 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -8,6 +8,7 @@ # ======================================================================= # import textwrap +from typing import Type, Optional from components.klipper.klipper_utils import backup_klipper_dir from components.moonraker.moonraker_utils import ( @@ -20,6 +21,7 @@ from components.webui_client.client_utils import ( ) from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData +from core.menus import Option from core.menus.base_menu import BaseMenu from utils.common import backup_printer_config_dir from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW @@ -28,20 +30,27 @@ from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class BackupMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - self.previous_menu: BaseMenu = previous_menu + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.main_menu import MainMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else MainMenu + ) + + def set_options(self) -> None: self.options = { - "1": self.backup_klipper, - "2": self.backup_moonraker, - "3": self.backup_printer_config, - "4": self.backup_moonraker_db, - "5": self.backup_mainsail, - "6": self.backup_fluidd, - "7": self.backup_mainsail_config, - "8": self.backup_fluidd_config, - "9": self.backup_klipperscreen, + "1": Option(method=self.backup_klipper, menu=False), + "2": Option(method=self.backup_moonraker, menu=False), + "3": Option(method=self.backup_printer_config, menu=False), + "4": Option(method=self.backup_moonraker_db, menu=False), + "5": Option(method=self.backup_mainsail, menu=False), + "6": Option(method=self.backup_fluidd, menu=False), + "7": Option(method=self.backup_mainsail_config, menu=False), + "8": Option(method=self.backup_fluidd_config, menu=False), + "9": Option(method=self.backup_klipperscreen, menu=False), } def print_menu(self): @@ -82,16 +91,16 @@ class BackupMenu(BaseMenu): backup_moonraker_db_dir() def backup_mainsail(self, **kwargs): - backup_client_data(MainsailData().get()) + backup_client_data(MainsailData()) def backup_fluidd(self, **kwargs): - backup_client_data(FluiddData().get()) + backup_client_data(FluiddData()) def backup_mainsail_config(self, **kwargs): - backup_client_config_data(MainsailData().get()) + backup_client_config_data(MainsailData()) def backup_fluidd_config(self, **kwargs): - backup_client_config_data(FluiddData().get()) + backup_client_config_data(FluiddData()) def backup_klipperscreen(self, **kwargs): pass diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 84f4a13..2d5e1f8 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -13,9 +13,9 @@ import subprocess import sys import textwrap from abc import abstractmethod -from typing import Dict, Union, Callable, Type, Tuple +from typing import Type, Dict, Optional -from core.menus import FooterType, ExitAppException, GoBackException +from core.menus import FooterType, Option from utils.constants import ( COLOR_GREEN, COLOR_YELLOW, @@ -106,12 +106,12 @@ class PostInitCaller(type): # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class BaseMenu(metaclass=PostInitCaller): - options: Dict[str, Callable] = {} + options: Dict[str, Option] = {} options_offset: int = 0 - default_option: Union[Callable, None] = None + default_option: Option = None input_label_txt: str = "Perform action" header: bool = False - previous_menu: Union[Type[BaseMenu], BaseMenu] = None + previous_menu: Type[BaseMenu] = None help_menu: Type[BaseMenu] = None footer_type: FooterType = FooterType.BACK @@ -120,30 +120,42 @@ class BaseMenu(metaclass=PostInitCaller): raise NotImplementedError("BaseMenu cannot be instantiated directly.") def __post_init__(self): + self.set_previous_menu(self.previous_menu) + self.set_options() + # conditionally add options based on footer type if self.footer_type is FooterType.QUIT: - self.options["q"] = self.__exit + self.options["q"] = Option(method=self.__exit, menu=False) if self.footer_type is FooterType.BACK: - self.options["b"] = self.__go_back + self.options["b"] = Option(method=self.__go_back, menu=False) if self.footer_type is FooterType.BACK_HELP: - self.options["b"] = self.__go_back - self.options["h"] = self.__go_to_help + self.options["b"] = Option(method=self.__go_back, menu=False) + self.options["h"] = Option(method=self.__go_to_help, menu=False) # if defined, add the default option to the options dict if self.default_option is not None: self.options[""] = self.default_option def __go_back(self, **kwargs): - raise GoBackException() + self.previous_menu().run() def __go_to_help(self, **kwargs): self.help_menu(previous_menu=self).run() def __exit(self, **kwargs): - raise ExitAppException() + Logger.print_ok("###### Happy printing!", False) + sys.exit(0) + + @abstractmethod + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + raise NotImplementedError + + @abstractmethod + def set_options(self) -> None: + raise NotImplementedError @abstractmethod def print_menu(self) -> None: - raise NotImplementedError("Subclasses must implement the print_menu method") + raise NotImplementedError def print_footer(self) -> None: if self.footer_type is FooterType.QUIT: @@ -155,53 +167,50 @@ class BaseMenu(metaclass=PostInitCaller): elif self.footer_type is FooterType.BLANK: print_blank_footer() else: - raise NotImplementedError("Method for printing footer not implemented.") + raise NotImplementedError def display_menu(self) -> None: - # clear() if self.header: print_header() self.print_menu() self.print_footer() - def validate_user_input(self, usr_input: str) -> Tuple[Callable, str]: + def validate_user_input(self, usr_input: str) -> Option: """ Validate the user input and either return an Option, a string or None :param usr_input: The user input in form of a string :return: Option, str or None """ usr_input = usr_input.lower() - option = self.options.get(usr_input, None) + option = self.options.get(usr_input, Option(None, False, "", None)) # if option/usr_input is None/empty string, we execute the menus default option if specified if (option is None or usr_input == "") and self.default_option is not None: - return self.default_option, usr_input + self.default_option.opt_index = usr_input + return self.default_option # user selected a regular option - return option, usr_input + option.opt_index = usr_input + return option - def handle_user_input(self) -> Tuple[Callable, str]: + def handle_user_input(self) -> Option: """Handle the user input, return the validated input or print an error.""" while True: print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="") usr_input = input().lower() validated_input = self.validate_user_input(usr_input) - method_to_call = validated_input[0] - if method_to_call is not None: + if validated_input.method is not None: return validated_input else: Logger.print_error("Invalid input!", False) def run(self) -> None: """Start the menu lifecycle. When this function returns, the lifecycle of the menu ends.""" - while True: - try: - self.display_menu() - option = self.handle_user_input() - option[0](opt_index=option[1]) - except GoBackException: - return - except ExitAppException: - Logger.print_ok("###### Happy printing!", False) - sys.exit(0) + try: + self.display_menu() + option = self.handle_user_input() + option.method(opt_index=option.opt_index, opt_data=option.opt_data) + self.run() + except Exception as e: + Logger.print_error(f"An unexpecred error occured:\n{e}") diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 5db3846..ecf4f78 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -8,6 +8,7 @@ # ======================================================================= # import textwrap +from typing import Type, Optional from components.klipper import klipper_setup from components.moonraker import moonraker_setup @@ -15,6 +16,7 @@ from components.webui_client import client_setup from components.webui_client.client_config import client_config_setup from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData +from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import COLOR_GREEN, RESET_FORMAT @@ -23,20 +25,24 @@ from utils.constants import COLOR_GREEN, RESET_FORMAT # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class InstallMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - self.previous_menu: BaseMenu = previous_menu + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.main_menu import MainMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else MainMenu + ) + + def set_options(self) -> None: self.options = { - "1": self.install_klipper, - "2": self.install_moonraker, - "3": self.install_mainsail, - "4": self.install_fluidd, - "5": self.install_mainsail_config, - "6": self.install_fluidd_config, - "7": None, - "8": None, - "9": None, + "1": Option(method=self.install_klipper, menu=False), + "2": Option(method=self.install_moonraker, menu=False), + "3": Option(method=self.install_mainsail, menu=False), + "4": Option(method=self.install_fluidd, menu=False), + "5": Option(method=self.install_mainsail_config, menu=False), + "6": Option(method=self.install_fluidd_config, menu=False), } def print_menu(self): diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 5cf45fd..d347807 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -8,6 +8,7 @@ # ======================================================================= # import textwrap +from typing import Type, Optional from components.klipper.klipper_utils import get_klipper_status from components.log_uploads.menus.log_upload_menu import LogUploadMenu @@ -21,7 +22,7 @@ from components.webui_client.mainsail_data import MainsailData from core.menus import FooterType from core.menus.advanced_menu import AdvancedMenu from core.menus.backup_menu import BackupMenu -from core.menus.base_menu import BaseMenu +from core.menus.base_menu import BaseMenu, Option from extensions.extensions_menu import ExtensionsMenu from core.menus.install_menu import InstallMenu from core.menus.remove_menu import RemoveMenu @@ -43,16 +44,6 @@ class MainMenu(BaseMenu): def __init__(self): super().__init__() - self.options = { - "0": self.log_upload_menu, - "1": self.install_menu, - "2": self.update_menu, - "3": self.remove_menu, - "4": self.advanced_menu, - "5": self.backup_menu, - "e": self.extension_menu, - "s": self.settings_menu, - } self.header = True self.footer_type = FooterType.QUIT @@ -68,6 +59,22 @@ class MainMenu(BaseMenu): self.cc_status = "" self.init_status() + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + """MainMenu does not have a previous menu""" + pass + + def set_options(self) -> None: + self.options = { + "0": Option(method=self.log_upload_menu, menu=True), + "1": Option(method=self.install_menu, menu=True), + "2": Option(method=self.update_menu, menu=True), + "3": Option(method=self.remove_menu, menu=True), + "4": Option(method=self.advanced_menu, menu=True), + "5": Option(method=self.backup_menu, menu=True), + "e": Option(method=self.extension_menu, menu=True), + "s": Option(method=self.settings_menu, menu=True), + } + def init_status(self) -> None: status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn"] for var in status_vars: @@ -140,25 +147,25 @@ class MainMenu(BaseMenu): print(menu, end="") def log_upload_menu(self, **kwargs): - LogUploadMenu(previous_menu=self).run() + LogUploadMenu().run() def install_menu(self, **kwargs): - InstallMenu(previous_menu=self).run() + InstallMenu(previous_menu=self.__class__).run() def update_menu(self, **kwargs): - UpdateMenu(previous_menu=self).run() + UpdateMenu(previous_menu=self.__class__).run() def remove_menu(self, **kwargs): - RemoveMenu(previous_menu=self).run() + RemoveMenu(previous_menu=self.__class__).run() def advanced_menu(self, **kwargs): - AdvancedMenu().run() + AdvancedMenu(previous_menu=self.__class__).run() def backup_menu(self, **kwargs): - BackupMenu(previous_menu=self).run() + BackupMenu(previous_menu=self.__class__).run() def settings_menu(self, **kwargs): - SettingsMenu(previous_menu=self).run() + SettingsMenu().run() def extension_menu(self, **kwargs): - ExtensionsMenu(previous_menu=self).run() + ExtensionsMenu(previous_menu=self.__class__).run() diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 0ba8992..7d8ac75 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -8,6 +8,7 @@ # ======================================================================= # import textwrap +from typing import Type, Optional from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu from components.moonraker.menus.moonraker_remove_menu import ( @@ -16,6 +17,7 @@ from components.moonraker.menus.moonraker_remove_menu import ( from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData from components.webui_client.menus.client_remove_menu import ClientRemoveMenu +from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import COLOR_RED, RESET_FORMAT @@ -23,24 +25,22 @@ from utils.constants import COLOR_RED, RESET_FORMAT # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class RemoveMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - self.previous_menu: BaseMenu = previous_menu + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.main_menu import MainMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else MainMenu + ) + + def set_options(self): self.options = { - "1": self.remove_klipper, - "2": self.remove_moonraker, - "3": self.remove_mainsail, - "4": self.remove_fluidd, - "5": None, - "6": None, - "7": None, - "8": None, - "9": None, - "10": None, - "11": None, - "12": None, - "13": None, + "1": Option(method=self.remove_klipper, menu=True), + "2": Option(method=self.remove_moonraker, menu=True), + "3": Option(method=self.remove_mainsail, menu=True), + "4": Option(method=self.remove_fluidd, menu=True), } def print_menu(self): @@ -71,13 +71,13 @@ class RemoveMenu(BaseMenu): print(menu, end="") def remove_klipper(self, **kwargs): - KlipperRemoveMenu(previous_menu=self).run() + KlipperRemoveMenu(previous_menu=self.__class__).run() def remove_moonraker(self, **kwargs): - MoonrakerRemoveMenu(previous_menu=self).run() + MoonrakerRemoveMenu(previous_menu=self.__class__).run() def remove_mainsail(self, **kwargs): - ClientRemoveMenu(previous_menu=self, client=MainsailData()).run() + ClientRemoveMenu(previous_menu=self.__class__, client=MainsailData()).run() def remove_fluidd(self, **kwargs): - ClientRemoveMenu(previous_menu=self, client=FluiddData()).run() + ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run() diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 9d28b14..d26f49a 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -6,16 +6,25 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from typing import Type, Optional from core.menus.base_menu import BaseMenu # noinspection PyMethodMayBeStatic class SettingsMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self): super().__init__() - self.previous_menu: BaseMenu = previous_menu + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.main_menu import MainMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else MainMenu + ) + + def set_options(self) -> None: + pass def print_menu(self): print("self") diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index dcff75b..ba7d735 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -8,6 +8,7 @@ # ======================================================================= # import textwrap +from typing import Type, Optional from components.klipper.klipper_setup import update_klipper from components.klipper.klipper_utils import ( @@ -26,6 +27,7 @@ from components.webui_client.client_utils import ( ) from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData +from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import ( COLOR_GREEN, @@ -39,24 +41,9 @@ from utils.constants import ( # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class UpdateMenu(BaseMenu): - def __init__(self, previous_menu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - self.previous_menu: BaseMenu = previous_menu - self.options = { - "0": self.update_all, - "1": self.update_klipper, - "2": self.update_moonraker, - "3": self.update_mainsail, - "4": self.update_fluidd, - "5": self.update_mainsail_config, - "6": self.update_fluidd_config, - "7": self.update_klipperscreen, - "8": self.update_mobileraker, - "9": self.update_crowsnest, - "10": self.upgrade_system_packages, - } - self.kl_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.kl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.mr_local = f"{COLOR_WHITE}{RESET_FORMAT}" @@ -73,6 +60,28 @@ class UpdateMenu(BaseMenu): self.mainsail_client = MainsailData() self.fluidd_client = FluiddData() + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.main_menu import MainMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else MainMenu + ) + + def set_options(self) -> None: + self.options = { + "0": Option(self.update_all, menu=False), + "1": Option(self.update_klipper, menu=False), + "2": Option(self.update_moonraker, menu=False), + "3": Option(self.update_mainsail, menu=False), + "4": Option(self.update_fluidd, menu=False), + "5": Option(self.update_mainsail_config, menu=False), + "6": Option(self.update_fluidd_config, menu=False), + "7": Option(self.update_klipperscreen, menu=False), + "8": Option(self.update_mobileraker, menu=False), + "9": Option(self.update_crowsnest, menu=False), + "10": Option(self.upgrade_system_packages, menu=False), + } + def print_menu(self): self.fetch_update_status() diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py index 5e437d2..b9f7e13 100644 --- a/kiauh/extensions/extensions_menu.py +++ b/kiauh/extensions/extensions_menu.py @@ -12,8 +12,9 @@ import inspect import json import textwrap from pathlib import Path -from typing import Type, Dict +from typing import Type, Dict, Optional +from core.menus import Option from extensions import EXTENSION_ROOT from extensions.base_extension import BaseExtension from core.menus.base_menu import BaseMenu @@ -23,12 +24,24 @@ from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class ExtensionsMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.extensions: Dict[str, BaseExtension] = self.discover_extensions() - self.previous_menu: BaseMenu = previous_menu - self.extensions = self.discover_extensions() - self.options = {ext: self.extension_submenu for ext in self.extensions} + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.main_menu import MainMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else MainMenu + ) + + def set_options(self) -> None: + self.options = { + i: Option( + self.extension_submenu, menu=True, opt_data=self.extensions.get(i) + ) + for i in self.extensions + } def discover_extensions(self) -> Dict[str, BaseExtension]: ext_dict = {} @@ -63,8 +76,7 @@ class ExtensionsMenu(BaseMenu): return ext_dict def extension_submenu(self, **kwargs): - extension = self.extensions.get(kwargs.get("opt_index")) - ExtensionSubmenu(self, extension).run() + ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run() def print_menu(self): header = " [ Extensions Menu ] " @@ -92,28 +104,32 @@ class ExtensionsMenu(BaseMenu): # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class ExtensionSubmenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu, extension: BaseExtension): + def __init__( + self, extension: BaseExtension, previous_menu: Optional[Type[BaseMenu]] = None + ): super().__init__() - self.extension = extension - self.extension_name = extension.metadata.get("display_name") - self.extension_desc = extension.metadata.get("description") - self.previous_menu = previous_menu - self.options["1"] = extension.install_extension + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else ExtensionsMenu + ) + + def set_options(self) -> None: + self.options["1"] = Option(self.extension.install_extension, menu=False) if self.extension.metadata.get("updates"): - self.options["2"] = extension.update_extension - self.options["3"] = extension.remove_extension + self.options["2"] = Option(self.extension.update_extension, menu=False) + self.options["3"] = Option(self.extension.remove_extension, menu=False) else: - self.options["2"] = extension.remove_extension + self.options["2"] = Option(self.extension.remove_extension, menu=False) def print_menu(self) -> None: - header = f" [ {self.extension_name} ] " + header = f" [ {self.extension.metadata.get('display_name')} ] " color = COLOR_YELLOW count = 62 - len(color) - len(RESET_FORMAT) wrapper = textwrap.TextWrapper(55, initial_indent="| ", subsequent_indent="| ") - lines = wrapper.wrap(self.extension_desc) + lines = wrapper.wrap(self.extension.metadata.get("description")) formatted_lines = [f"{line:<55} |" for line in lines] description_text = "\n".join(formatted_lines) From ecb673a088a9792abc5d809aa13da38d1069cfb3 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 15 Apr 2024 21:29:13 +0200 Subject: [PATCH 162/296] feat: implement firmware build Signed-off-by: Dominik Willner --- .../klipper_firmware/firmware_utils.py | 169 ++++++++++++++++++ .../menus/klipper_build_menu.py | 111 ++++++++++++ .../menus/klipper_flash_error_menu.py | 21 ++- .../menus/klipper_flash_help_menu.py | 48 ++++- .../menus/klipper_flash_menu.py | 135 ++++++++------ kiauh/core/menus/advanced_menu.py | 33 ++-- kiauh/utils/system_utils.py | 8 +- 7 files changed, 451 insertions(+), 74 deletions(-) create mode 100644 kiauh/components/klipper_firmware/firmware_utils.py create mode 100644 kiauh/components/klipper_firmware/menus/klipper_build_menu.py diff --git a/kiauh/components/klipper_firmware/firmware_utils.py b/kiauh/components/klipper_firmware/firmware_utils.py new file mode 100644 index 0000000..698fdb1 --- /dev/null +++ b/kiauh/components/klipper_firmware/firmware_utils.py @@ -0,0 +1,169 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 subprocess import CalledProcessError, check_output, Popen, PIPE, STDOUT, run +from typing import List + +from components.klipper import KLIPPER_DIR +from components.klipper_firmware import SD_FLASH_SCRIPT +from components.klipper_firmware.flash_options import ( + FlashOptions, + FlashMethod, +) +from utils.logger import Logger +from utils.system_utils import log_process + + +def find_firmware_file(method: FlashMethod) -> bool: + target = KLIPPER_DIR.joinpath("out") + target_exists = target.exists() + if method is FlashMethod.REGULAR: + f1 = "klipper.elf.hex" + f2 = "klipper.elf" + fw_file_exists = target.joinpath(f1).exists() and target.joinpath(f2).exists() + elif method is FlashMethod.SD_CARD: + fw_file_exists = target.joinpath("klipper.bin").exists() + else: + raise Exception("Unknown flash method") + + return target_exists and fw_file_exists + + +def find_usb_device_by_id() -> List[str]: + try: + command = "find /dev/serial/by-id/* 2>/dev/null" + output = check_output(command, shell=True, text=True) + return output.splitlines() + except CalledProcessError as e: + Logger.print_error("Unable to find a USB device!") + Logger.print_error(e, prefix=False) + return [] + + +def find_uart_device() -> List[str]: + try: + command = '"find /dev -maxdepth 1 -regextype posix-extended -regex "^\/dev\/tty(AMA0|S0)$" 2>/dev/null"' + output = check_output(command, shell=True, text=True) + return output.splitlines() + except CalledProcessError as e: + Logger.print_error("Unable to find a UART device!") + Logger.print_error(e, prefix=False) + return [] + + +def find_usb_dfu_device() -> List[str]: + try: + command = '"lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null"' + output = check_output(command, shell=True, text=True) + return output.splitlines() + except CalledProcessError as e: + Logger.print_error("Unable to find a USB DFU device!") + Logger.print_error(e, prefix=False) + return [] + + +def get_sd_flash_board_list() -> List[str]: + if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists(): + return [] + + try: + cmd = f"{SD_FLASH_SCRIPT} -l" + blist = check_output(cmd, shell=True, text=True) + return blist.splitlines()[1:] + except CalledProcessError as e: + Logger.print_error(f"An unexpected error occured:\n{e}") + + +def start_flash_process(flash_options: FlashOptions) -> None: + Logger.print_status(f"Flashing '{flash_options.selected_mcu}' ...") + try: + if not flash_options.flash_method: + raise Exception("Missing value for flash_method!") + if not flash_options.flash_command: + raise Exception("Missing value for flash_command!") + if not flash_options.selected_mcu: + raise Exception("Missing value for selected_mcu!") + if not flash_options.connection_type: + raise Exception("Missing value for connection_type!") + if ( + flash_options.flash_method == FlashMethod.SD_CARD + and not flash_options.selected_board + ): + raise Exception("Missing value for selected_board!") + + if flash_options.flash_method is FlashMethod.REGULAR: + cmd = [ + "make", + flash_options.flash_command.value, + f"FLASH_DEVICE={flash_options.selected_mcu}", + ] + elif flash_options.flash_method is FlashMethod.SD_CARD: + if not SD_FLASH_SCRIPT.exists(): + raise Exception("Unable to find Klippers sdcard flash script!") + cmd = [ + SD_FLASH_SCRIPT, + "-b", + flash_options.selected_baudrate, + flash_options.selected_mcu, + flash_options.selected_board, + ] + else: + raise Exception("Invalid value for flash_method!") + + process = Popen(cmd, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True) + log_process(process) + + rc = process.returncode + if rc != 0: + raise Exception(f"Flashing failed with returncode: {rc}") + else: + Logger.print_ok("Flashing successfull!", start="\n", end="\n\n") + + except (Exception, CalledProcessError): + Logger.print_error("Flashing failed!", start="\n") + Logger.print_error("See the console output above!", end="\n\n") + + +def run_make_clean() -> None: + try: + run( + "make clean", + cwd=KLIPPER_DIR, + shell=True, + check=True, + ) + except CalledProcessError as e: + Logger.print_error(f"Unexpected error:\n{e}") + raise + + +def run_make_menuconfig() -> None: + try: + run( + "make PYTHON=python3 menuconfig", + cwd=KLIPPER_DIR, + shell=True, + check=True, + ) + except CalledProcessError as e: + Logger.print_error(f"Unexpected error:\n{e}") + raise + + +def run_make() -> None: + try: + run( + "make PYTHON=python3", + cwd=KLIPPER_DIR, + shell=True, + check=True, + ) + except CalledProcessError as e: + Logger.print_error(f"Unexpected error:\n{e}") + raise diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py new file mode 100644 index 0000000..6dc4272 --- /dev/null +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -0,0 +1,111 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import textwrap +from typing import Type, Optional + +from components.klipper import KLIPPER_DIR +from components.klipper_firmware.firmware_utils import ( + run_make_clean, + run_make_menuconfig, + run_make, +) +from core.menus import Option +from core.menus.base_menu import BaseMenu +from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_GREEN, COLOR_RED +from utils.logger import Logger +from utils.system_utils import ( + check_package_install, + update_system_package_lists, + install_system_packages, +) + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperBuildFirmwareMenu(BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): + super().__init__() + self.deps = ["build-essential", "dpkg-dev", "make"] + self.missing_deps = check_package_install(self.deps) + + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from core.menus.advanced_menu import AdvancedMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else AdvancedMenu + ) + + def set_options(self) -> None: + if len(self.missing_deps) == 0: + self.input_label_txt = "Press ENTER to continue" + self.default_option = Option(method=self.start_build_process, menu=False) + else: + self.input_label_txt = "Press ENTER to install dependencies" + self.default_option = Option(method=self.install_missing_deps, menu=False) + + def print_menu(self) -> None: + header = " [ Build Firmware Menu ] " + color = COLOR_CYAN + count = 62 - len(color) - len(RESET_FORMAT) + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | The following dependencies are required: | + | | + """ + )[1:] + + for d in self.deps: + status_ok = f"{COLOR_GREEN}*INSTALLED*{RESET_FORMAT}" + status_missing = f"{COLOR_RED}*MISSING*{RESET_FORMAT}" + status = status_missing if d in self.missing_deps else status_ok + padding = 39 - len(d) + len(status) + (len(status_ok) - len(status)) + d = f" {COLOR_CYAN}● {d}{RESET_FORMAT}" + menu += f"| {d}{status:>{padding}} |\n" + + menu += "| |\n" + if len(self.missing_deps) == 0: + line = f"{COLOR_GREEN}All dependencies are met!{RESET_FORMAT}" + else: + line = f"{COLOR_RED}Dependencies are missing!{RESET_FORMAT}" + + menu += f"| {line:<62} |\n" + + print(menu, end="") + + def install_missing_deps(self, **kwargs) -> None: + try: + update_system_package_lists(silent=False) + Logger.print_status("Installing system packages...") + install_system_packages(self.missing_deps) + except Exception as e: + Logger.print_error(e) + Logger.print_error("Installding dependencies failed!") + finally: + # restart this menu + KlipperBuildFirmwareMenu().run() + + def start_build_process(self, **kwargs) -> None: + try: + run_make_clean() + run_make_menuconfig() + run_make() + + Logger.print_ok("Firmware successfully built!") + Logger.print_ok(f"Firmware file located in '{KLIPPER_DIR}/out'!") + + except Exception as e: + Logger.print_error(e) + Logger.print_error("Building Klipper Firmware failed!") + + finally: + self.previous_menu().run() diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py index a54040d..61dea31 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py @@ -7,9 +7,10 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # import textwrap +from typing import Optional, Type from components.klipper_firmware.flash_options import FlashOptions, FlashMethod -from core.menus import FooterType +from core.menus import FooterType, Option from core.menus.base_menu import BaseMenu from utils.constants import COLOR_RED, RESET_FORMAT @@ -20,11 +21,19 @@ class KlipperNoFirmwareErrorMenu(BaseMenu): def __init__(self): super().__init__() + self.set_previous_menu(None) + self.set_options() + self.flash_options = FlashOptions() - self.default_option = self.go_back self.footer_type = FooterType.BLANK self.input_label_txt = "Press ENTER to go back to [Advanced Menu]" + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + pass + + def set_options(self) -> None: + self.default_option = Option(self.go_back, False) + def print_menu(self) -> None: header = "!!! NO FIRMWARE FILE FOUND !!!" color = COLOR_RED @@ -62,11 +71,15 @@ class KlipperNoFirmwareErrorMenu(BaseMenu): class KlipperNoBoardTypesErrorMenu(BaseMenu): def __init__(self): super().__init__() - - self.default_option = self.go_back self.footer_type = FooterType.BLANK self.input_label_txt = "Press ENTER to go back to [Main Menu]" + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + pass + + def set_options(self) -> None: + self.default_option = Option(self.go_back, False) + def print_menu(self) -> None: header = "!!! ERROR GETTING BOARD LIST !!!" color = COLOR_RED diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py index 16ebdcb..ed7d3cf 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py @@ -7,16 +7,28 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # import textwrap +from typing import Type, Optional from core.menus.base_menu import BaseMenu from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW +# noinspection DuplicatedCode class KlipperFlashMethodHelpMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self): super().__init__() - self.previous_menu: BaseMenu = previous_menu + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from components.klipper_firmware.menus.klipper_flash_menu import ( + KlipperFlashMethodMenu, + ) + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else KlipperFlashMethodMenu + ) + + def set_options(self) -> None: + pass def print_menu(self) -> None: header = " < ? > Help: Flash MCU < ? > " @@ -57,11 +69,22 @@ class KlipperFlashMethodHelpMenu(BaseMenu): print(menu, end="") +# noinspection DuplicatedCode class KlipperFlashCommandHelpMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self): super().__init__() - self.previous_menu: BaseMenu = previous_menu + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from components.klipper_firmware.menus.klipper_flash_menu import ( + KlipperFlashCommandMenu, + ) + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else KlipperFlashCommandMenu + ) + + def set_options(self) -> None: + pass def print_menu(self) -> None: header = " < ? > Help: Flash MCU < ? > " @@ -89,11 +112,24 @@ class KlipperFlashCommandHelpMenu(BaseMenu): print(menu, end="") +# noinspection DuplicatedCode class KlipperMcuConnectionHelpMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self): super().__init__() - self.previous_menu: BaseMenu = previous_menu + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from components.klipper_firmware.menus.klipper_flash_menu import ( + KlipperSelectMcuConnectionMenu, + ) + + self.previous_menu: Type[BaseMenu] = ( + previous_menu + if previous_menu is not None + else KlipperSelectMcuConnectionMenu + ) + + def set_options(self) -> None: + pass def print_menu(self) -> None: header = " < ? > Help: Flash MCU < ? > " diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index 27f1269..09b812b 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -9,6 +9,7 @@ import textwrap import time +from typing import Type, Optional from components.klipper_firmware.flash_options import ( FlashOptions, @@ -16,7 +17,7 @@ from components.klipper_firmware.flash_options import ( FlashCommand, ConnectionType, ) -from components.klipper_firmware.flash_utils import ( +from components.klipper_firmware.firmware_utils import ( find_usb_device_by_id, find_uart_device, find_usb_dfu_device, @@ -33,7 +34,7 @@ from components.klipper_firmware.menus.klipper_flash_help_menu import ( KlipperFlashCommandHelpMenu, KlipperFlashMethodHelpMenu, ) -from core.menus import FooterType +from core.menus import FooterType, Option from core.menus.base_menu import BaseMenu from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_RED @@ -44,20 +45,22 @@ from utils.logger import Logger # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperFlashMethodMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - - self.previous_menu: BaseMenu = previous_menu self.help_menu = KlipperFlashMethodHelpMenu - self.options = { - "1": self.select_regular, - "2": self.select_sdcard, - } self.input_label_txt = "Select flash method" self.footer_type = FooterType.BACK_HELP - self.flash_options = FlashOptions() + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + self.previous_menu: Type[BaseMenu] = previous_menu + + def set_options(self) -> None: + self.options = { + "1": Option(self.select_regular, menu=False), + "2": Option(self.select_sdcard, menu=False), + } + def print_menu(self) -> None: header = " [ MCU Flash Menu ] " subheader = f"{COLOR_YELLOW}ATTENTION:{RESET_FORMAT}" @@ -95,7 +98,7 @@ class KlipperFlashMethodMenu(BaseMenu): def goto_next_menu(self, **kwargs): if find_firmware_file(self.flash_options.flash_method): - KlipperFlashCommandMenu(previous_menu=self).run() + KlipperFlashCommandMenu(previous_menu=self.__class__).run() else: KlipperNoFirmwareErrorMenu().run() @@ -103,21 +106,25 @@ class KlipperFlashMethodMenu(BaseMenu): # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperFlashCommandMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - - self.previous_menu: BaseMenu = previous_menu self.help_menu = KlipperFlashCommandHelpMenu - self.options = { - "1": self.select_flash, - "2": self.select_serialflash, - } - self.default_option = self.select_flash self.input_label_txt = "Select flash command" self.footer_type = FooterType.BACK_HELP - self.flash_options = FlashOptions() + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else KlipperFlashMethodMenu + ) + + def set_options(self) -> None: + self.options = { + "1": Option(self.select_flash, menu=False), + "2": Option(self.select_serialflash, menu=False), + } + self.default_option = Option(self.select_flash, menu=False) + def print_menu(self) -> None: menu = textwrap.dedent( """ @@ -140,28 +147,34 @@ class KlipperFlashCommandMenu(BaseMenu): self.goto_next_menu() def goto_next_menu(self, **kwargs): - KlipperSelectMcuConnectionMenu(previous_menu=self).run() + KlipperSelectMcuConnectionMenu(previous_menu=self.__class__).run() # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperSelectMcuConnectionMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu, standalone: bool = False): + def __init__( + self, previous_menu: Optional[Type[BaseMenu]] = None, standalone: bool = False + ): super().__init__() - self.__standalone = standalone - self.previous_menu: BaseMenu = previous_menu self.help_menu = KlipperMcuConnectionHelpMenu - self.options = { - "1": self.select_usb, - "2": self.select_dfu, - "3": self.select_usb_dfu, - } self.input_label_txt = "Select connection type" self.footer_type = FooterType.BACK_HELP - self.flash_options = FlashOptions() + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else KlipperFlashCommandMenu + ) + + def set_options(self) -> None: + self.options = { + "1": Option(method=self.select_usb, menu=False), + "2": Option(method=self.select_dfu, menu=False), + "3": Option(method=self.select_usb_dfu, menu=False), + } + def print_menu(self) -> None: header = "Make sure that the controller board is connected now!" color = COLOR_YELLOW @@ -221,23 +234,32 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): self.goto_next_menu() def goto_next_menu(self, **kwargs): - KlipperSelectMcuIdMenu(previous_menu=self).run() + KlipperSelectMcuIdMenu(previous_menu=self.__class__).run() # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperSelectMcuIdMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - - self.previous_menu: BaseMenu = previous_menu self.flash_options = FlashOptions() self.mcu_list = self.flash_options.mcu_list - options = {f"{index}": self.flash_mcu for index in range(len(self.mcu_list))} - self.options = options self.input_label_txt = "Select MCU to flash" self.footer_type = FooterType.BACK_HELP + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + self.previous_menu: Type[BaseMenu] = ( + previous_menu + if previous_menu is not None + else KlipperSelectMcuConnectionMenu + ) + + def set_options(self) -> None: + self.options = { + f"{i}": Option(self.flash_mcu, False, f"{i}") + for i in range(len(self.mcu_list)) + } + def print_menu(self) -> None: header = "!!! ATTENTION !!!" header2 = f"[{COLOR_CYAN}List of available MCUs{RESET_FORMAT}]" @@ -268,24 +290,30 @@ class KlipperSelectMcuIdMenu(BaseMenu): self.flash_options.selected_mcu = selected_mcu if self.flash_options.flash_method == FlashMethod.SD_CARD: - KlipperSelectSDFlashBoardMenu(previous_menu=self).run() + KlipperSelectSDFlashBoardMenu(previous_menu=self.__class__).run() elif self.flash_options.flash_method == FlashMethod.REGULAR: - KlipperFlashOverviewMenu(previous_menu=self).run() + KlipperFlashOverviewMenu(previous_menu=self.__class__).run() # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperSelectSDFlashBoardMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - - self.previous_menu: BaseMenu = previous_menu self.flash_options = FlashOptions() self.available_boards = get_sd_flash_board_list() self.input_label_txt = "Select board type" - options = {f"{i}": self.board_select for i in range(len(self.available_boards))} - self.options = options + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else KlipperSelectMcuIdMenu + ) + + def set_options(self) -> None: + self.options = { + f"{i}": Option(self.board_select, False, f"{i}") + for i in range(len(self.available_boards)) + } def print_menu(self) -> None: if len(self.available_boards) < 1: @@ -331,20 +359,27 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu): min_count=0, allow_go_back=True, ) - KlipperFlashOverviewMenu(previous_menu=self).run() + KlipperFlashOverviewMenu(previous_menu=self.__class__).run() # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperFlashOverviewMenu(BaseMenu): - def __init__(self, previous_menu: BaseMenu): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - - self.previous_menu: BaseMenu = previous_menu self.flash_options = FlashOptions() - self.options = {"Y": self.execute_flash, "N": self.abort_process} self.input_label_txt = "Perform action (default=Y)" - self.default_option = self.execute_flash + + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + self.previous_menu: Type[BaseMenu] = previous_menu + + def set_options(self) -> None: + self.options = { + "Y": Option(self.execute_flash, menu=False), + "N": Option(self.abort_process, menu=False), + } + + self.default_option = Option(self.execute_flash, menu=False) def print_menu(self) -> None: header = "!!! ATTENTION !!!" @@ -397,7 +432,7 @@ class KlipperFlashOverviewMenu(BaseMenu): start_flash_process(self.flash_options) Logger.print_info("Returning to MCU Flash Menu in 5 seconds ...") time.sleep(5) - KlipperFlashMethodMenu(previous_menu=AdvancedMenu()).run() + KlipperFlashMethodMenu(previous_menu=AdvancedMenu).run() def abort_process(self, **kwargs): from core.menus.advanced_menu import AdvancedMenu diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 3000ba9..959b1ac 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -8,30 +8,37 @@ # ======================================================================= # import textwrap +from typing import Type, Optional +from components.klipper_firmware.menus.klipper_build_menu import ( + KlipperBuildFirmwareMenu, +) from components.klipper_firmware.menus.klipper_flash_menu import ( KlipperFlashMethodMenu, KlipperSelectMcuConnectionMenu, ) +from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import COLOR_YELLOW, RESET_FORMAT # noinspection PyUnusedLocal class AdvancedMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu - self.previous_menu: BaseMenu = MainMenu() + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else MainMenu + ) + + def set_options(self): self.options = { - "1": None, - "2": None, - "3": None, - "4": self.flash, - "5": None, - "6": self.get_id, + "3": Option(method=self.build, menu=True), + "4": Option(method=self.flash, menu=False), + "6": Option(method=self.get_id, menu=False), } def print_menu(self): @@ -56,8 +63,14 @@ class AdvancedMenu(BaseMenu): )[1:] print(menu, end="") + def build(self, **kwargs): + KlipperBuildFirmwareMenu(previous_menu=self.__class__).run() + def flash(self, **kwargs): - KlipperFlashMethodMenu(previous_menu=self).run() + KlipperFlashMethodMenu(previous_menu=self.__class__).run() def get_id(self, **kwargs): - KlipperSelectMcuConnectionMenu(previous_menu=self, standalone=True).run() + KlipperSelectMcuConnectionMenu( + previous_menu=self.__class__, + standalone=True, + ).run() diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 6ac932f..bdd3527 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -190,7 +190,8 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: Logger.print_ok("System package list update successful!") except CalledProcessError as e: - kill(f"Error updating system package list:\n{e.stderr.decode()}") + Logger.print_error(f"Error updating system package list:\n{e.stderr.decode()}") + raise def check_package_install(packages: List[str]) -> List[str]: @@ -210,8 +211,6 @@ def check_package_install(packages: List[str]) -> List[str]: ) if "installed" not in result.stdout.strip("'").split(): not_installed.append(package) - else: - Logger.print_ok(f"{package} already installed.") return not_installed @@ -230,7 +229,8 @@ def install_system_packages(packages: List[str]) -> None: Logger.print_ok("Packages installed successfully.") except CalledProcessError as e: - kill(f"Error installing packages:\n{e.stderr.decode()}") + Logger.print_error(f"Error installing packages:\n{e.stderr.decode()}") + raise def mask_system_service(service_name: str) -> None: From c2b0ca5b193114fbc1990aae17d86c7151ee4f55 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 15 Apr 2024 21:31:54 +0200 Subject: [PATCH 163/296] fix: typo Signed-off-by: Dominik Willner --- kiauh/core/menus/base_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 2d5e1f8..68e27b9 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -213,4 +213,4 @@ class BaseMenu(metaclass=PostInitCaller): option.method(opt_index=option.opt_index, opt_data=option.opt_data) self.run() except Exception as e: - Logger.print_error(f"An unexpecred error occured:\n{e}") + Logger.print_error(f"An unexpected error occured:\n{e}") From 8de7ab7e11f1a391fb91cacf73043d7c305939c3 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 15 Apr 2024 21:37:25 +0200 Subject: [PATCH 164/296] fix: wrong default previous menu for KlipperFlashMethodMenu Signed-off-by: Dominik Willner --- .../components/klipper_firmware/menus/klipper_flash_menu.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index 09b812b..f414924 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -53,7 +53,11 @@ class KlipperFlashMethodMenu(BaseMenu): self.flash_options = FlashOptions() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: - self.previous_menu: Type[BaseMenu] = previous_menu + from core.menus.advanced_menu import AdvancedMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else AdvancedMenu + ) def set_options(self) -> None: self.options = { From cd63034b7409fa3b54afed11742cd4c4ee6f3f69 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 15 Apr 2024 21:52:32 +0200 Subject: [PATCH 165/296] fix: ignore flash method when checking for firmware files Signed-off-by: Dominik Willner --- .../klipper_firmware/firmware_utils.py | 17 ++++++++--------- .../menus/klipper_flash_menu.py | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/kiauh/components/klipper_firmware/firmware_utils.py b/kiauh/components/klipper_firmware/firmware_utils.py index 698fdb1..a75fb22 100644 --- a/kiauh/components/klipper_firmware/firmware_utils.py +++ b/kiauh/components/klipper_firmware/firmware_utils.py @@ -20,17 +20,16 @@ from utils.logger import Logger from utils.system_utils import log_process -def find_firmware_file(method: FlashMethod) -> bool: +def find_firmware_file() -> bool: target = KLIPPER_DIR.joinpath("out") target_exists = target.exists() - if method is FlashMethod.REGULAR: - f1 = "klipper.elf.hex" - f2 = "klipper.elf" - fw_file_exists = target.joinpath(f1).exists() and target.joinpath(f2).exists() - elif method is FlashMethod.SD_CARD: - fw_file_exists = target.joinpath("klipper.bin").exists() - else: - raise Exception("Unknown flash method") + + f1 = "klipper.elf.hex" + f2 = "klipper.elf" + f3 = "klipper.bin" + fw_file_exists = ( + target.joinpath(f1).exists() and target.joinpath(f2).exists() + ) or target.joinpath(f3).exists() return target_exists and fw_file_exists diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index f414924..09b1322 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -101,7 +101,7 @@ class KlipperFlashMethodMenu(BaseMenu): self.goto_next_menu() def goto_next_menu(self, **kwargs): - if find_firmware_file(self.flash_options.flash_method): + if find_firmware_file(): KlipperFlashCommandMenu(previous_menu=self.__class__).run() else: KlipperNoFirmwareErrorMenu().run() From 336414c43ce217669aa264bf9b61256ba35e5cf4 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 15 Apr 2024 22:12:14 +0200 Subject: [PATCH 166/296] fix: init previous_menu in menus Signed-off-by: Dominik Willner --- .../components/klipper/menus/klipper_remove_menu.py | 1 + .../klipper_firmware/menus/klipper_build_menu.py | 1 + .../menus/klipper_flash_error_menu.py | 13 ++++++------- .../menus/klipper_flash_help_menu.py | 9 ++++++--- .../klipper_firmware/menus/klipper_flash_menu.py | 1 + .../components/log_uploads/menus/log_upload_menu.py | 3 ++- .../moonraker/menus/moonraker_remove_menu.py | 1 + .../webui_client/menus/client_remove_menu.py | 1 + kiauh/core/menus/advanced_menu.py | 1 + kiauh/core/menus/backup_menu.py | 1 + kiauh/core/menus/install_menu.py | 1 + kiauh/core/menus/remove_menu.py | 1 + kiauh/core/menus/settings_menu.py | 3 ++- kiauh/core/menus/update_menu.py | 1 + kiauh/extensions/extensions_menu.py | 2 ++ 15 files changed, 28 insertions(+), 12 deletions(-) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index d079265..1190e98 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -20,6 +20,7 @@ from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN class KlipperRemoveMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu self.footer_type = FooterType.BACK_HELP self.remove_klipper_service = False self.remove_klipper_dir = False diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py index 6dc4272..4bba6f4 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -32,6 +32,7 @@ from utils.system_utils import ( class KlipperBuildFirmwareMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu self.deps = ["build-essential", "dpkg-dev", "make"] self.missing_deps = check_package_install(self.deps) diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py index 61dea31..7df7e82 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py @@ -18,18 +18,16 @@ from utils.constants import COLOR_RED, RESET_FORMAT # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperNoFirmwareErrorMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() - - self.set_previous_menu(None) - self.set_options() + self.previous_menu = previous_menu self.flash_options = FlashOptions() self.footer_type = FooterType.BLANK self.input_label_txt = "Press ENTER to go back to [Advanced Menu]" def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: - pass + self.previous_menu = previous_menu def set_options(self) -> None: self.default_option = Option(self.go_back, False) @@ -69,13 +67,14 @@ class KlipperNoFirmwareErrorMenu(BaseMenu): # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KlipperNoBoardTypesErrorMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu self.footer_type = FooterType.BLANK self.input_label_txt = "Press ENTER to go back to [Main Menu]" def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: - pass + self.previous_menu = previous_menu def set_options(self) -> None: self.default_option = Option(self.go_back, False) diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py index ed7d3cf..736d9e9 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py @@ -15,8 +15,9 @@ from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW # noinspection DuplicatedCode class KlipperFlashMethodHelpMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from components.klipper_firmware.menus.klipper_flash_menu import ( @@ -71,8 +72,9 @@ class KlipperFlashMethodHelpMenu(BaseMenu): # noinspection DuplicatedCode class KlipperFlashCommandHelpMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from components.klipper_firmware.menus.klipper_flash_menu import ( @@ -114,8 +116,9 @@ class KlipperFlashCommandHelpMenu(BaseMenu): # noinspection DuplicatedCode class KlipperMcuConnectionHelpMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from components.klipper_firmware.menus.klipper_flash_menu import ( diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index 09b1322..9c2394f 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -161,6 +161,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): self, previous_menu: Optional[Type[BaseMenu]] = None, standalone: bool = False ): super().__init__() + self.previous_menu = previous_menu self.__standalone = standalone self.help_menu = KlipperMcuConnectionHelpMenu self.input_label_txt = "Select connection type" diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index 3129c52..34f9834 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -19,8 +19,9 @@ from utils.constants import RESET_FORMAT, COLOR_YELLOW # noinspection PyMethodMayBeStatic class LogUploadMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu self.logfile_list = get_logfile_list() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index a72b61c..102c369 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -20,6 +20,7 @@ from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN class MoonrakerRemoveMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu self.remove_moonraker_service = False self.remove_moonraker_dir = False self.remove_moonraker_env = False diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index ed11e0a..8cfa50f 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -23,6 +23,7 @@ class ClientRemoveMenu(BaseMenu): self, client: BaseWebClient, previous_menu: Optional[Type[BaseMenu]] = None ): super().__init__() + self.previous_menu = previous_menu self.client = client self.rm_client = False self.rm_client_config = False diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 959b1ac..2235334 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -26,6 +26,7 @@ from utils.constants import COLOR_YELLOW, RESET_FORMAT class AdvancedMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 0eb8698..4bfb2c5 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -32,6 +32,7 @@ from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW class BackupMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index ecf4f78..ce42419 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -27,6 +27,7 @@ from utils.constants import COLOR_GREEN, RESET_FORMAT class InstallMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 7d8ac75..ee6272a 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -27,6 +27,7 @@ from utils.constants import COLOR_RED, RESET_FORMAT class RemoveMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index d26f49a..8ed407b 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -13,8 +13,9 @@ from core.menus.base_menu import BaseMenu # noinspection PyMethodMayBeStatic class SettingsMenu(BaseMenu): - def __init__(self): + def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index ba7d735..3af9085 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -43,6 +43,7 @@ from utils.constants import ( class UpdateMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu self.kl_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.kl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py index b9f7e13..7d7e59e 100644 --- a/kiauh/extensions/extensions_menu.py +++ b/kiauh/extensions/extensions_menu.py @@ -26,6 +26,7 @@ from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW class ExtensionsMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() + self.previous_menu = previous_menu self.extensions: Dict[str, BaseExtension] = self.discover_extensions() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: @@ -109,6 +110,7 @@ class ExtensionSubmenu(BaseMenu): ): super().__init__() self.extension = extension + self.previous_menu = previous_menu def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: self.previous_menu: Type[BaseMenu] = ( From 449317b118fdc7bf97f6678f5aa0774cfdaec196 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 17 Apr 2024 19:48:31 +0200 Subject: [PATCH 167/296] fix: fix sd flash process Signed-off-by: Dominik Willner --- kiauh/components/klipper_firmware/firmware_utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kiauh/components/klipper_firmware/firmware_utils.py b/kiauh/components/klipper_firmware/firmware_utils.py index a75fb22..c8210e1 100644 --- a/kiauh/components/klipper_firmware/firmware_utils.py +++ b/kiauh/components/klipper_firmware/firmware_utils.py @@ -11,11 +11,13 @@ from subprocess import CalledProcessError, check_output, Popen, PIPE, STDOUT, ru from typing import List from components.klipper import KLIPPER_DIR +from components.klipper.klipper import Klipper from components.klipper_firmware import SD_FLASH_SCRIPT from components.klipper_firmware.flash_options import ( FlashOptions, FlashMethod, ) +from core.instance_manager.instance_manager import InstanceManager from utils.logger import Logger from utils.system_utils import log_process @@ -106,18 +108,22 @@ def start_flash_process(flash_options: FlashOptions) -> None: if not SD_FLASH_SCRIPT.exists(): raise Exception("Unable to find Klippers sdcard flash script!") cmd = [ - SD_FLASH_SCRIPT, - "-b", - flash_options.selected_baudrate, + SD_FLASH_SCRIPT.as_posix(), + f"-b {flash_options.selected_baudrate}", flash_options.selected_mcu, flash_options.selected_board, ] else: raise Exception("Invalid value for flash_method!") + instance_manager = InstanceManager(Klipper) + instance_manager.stop_all_instance() + process = Popen(cmd, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True) log_process(process) + instance_manager.start_all_instance() + rc = process.returncode if rc != 0: raise Exception(f"Flashing failed with returncode: {rc}") From aa1b435da5058673d72798d204bcb879df735247 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 17 Apr 2024 19:58:40 +0200 Subject: [PATCH 168/296] feat: implement build + flash process Signed-off-by: Dominik Willner --- .../components/klipper_firmware/menus/klipper_flash_menu.py | 4 +--- kiauh/core/menus/advanced_menu.py | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index 9c2394f..455732e 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -432,12 +432,10 @@ class KlipperFlashOverviewMenu(BaseMenu): print(menu, end="") def execute_flash(self, **kwargs): - from core.menus.advanced_menu import AdvancedMenu - start_flash_process(self.flash_options) Logger.print_info("Returning to MCU Flash Menu in 5 seconds ...") time.sleep(5) - KlipperFlashMethodMenu(previous_menu=AdvancedMenu).run() + KlipperFlashMethodMenu().run() def abort_process(self, **kwargs): from core.menus.advanced_menu import AdvancedMenu diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 2235334..f86788e 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -39,6 +39,7 @@ class AdvancedMenu(BaseMenu): self.options = { "3": Option(method=self.build, menu=True), "4": Option(method=self.flash, menu=False), + "5": Option(method=self.build_flash, menu=False), "6": Option(method=self.get_id, menu=False), } @@ -70,6 +71,10 @@ class AdvancedMenu(BaseMenu): def flash(self, **kwargs): KlipperFlashMethodMenu(previous_menu=self.__class__).run() + def build_flash(self, **kwargs): + KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run() + KlipperFlashMethodMenu(previous_menu=self.__class__).run() + def get_id(self, **kwargs): KlipperSelectMcuConnectionMenu( previous_menu=self.__class__, From b020f1096768983c4fcfd132e4f30a727608a188 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 18 Apr 2024 21:54:34 +0200 Subject: [PATCH 169/296] feat: implement repo rollback feature Signed-off-by: Dominik Willner --- kiauh/core/menus/advanced_menu.py | 14 ++++++++++++ kiauh/utils/git_utils.py | 36 ++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index f86788e..fb4a3ea 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -10,6 +10,8 @@ import textwrap from typing import Type, Optional +from components.klipper import KLIPPER_DIR +from components.klipper.klipper import Klipper from components.klipper_firmware.menus.klipper_build_menu import ( KlipperBuildFirmwareMenu, ) @@ -17,12 +19,16 @@ from components.klipper_firmware.menus.klipper_flash_menu import ( KlipperFlashMethodMenu, KlipperSelectMcuConnectionMenu, ) +from components.moonraker import MOONRAKER_DIR +from components.moonraker.moonraker import Moonraker from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import COLOR_YELLOW, RESET_FORMAT +from utils.git_utils import rollback_repository # noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic class AdvancedMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() @@ -37,6 +43,8 @@ class AdvancedMenu(BaseMenu): def set_options(self): self.options = { + "1": Option(method=self.klipper_rollback, menu=True), + "2": Option(method=self.moonraker_rollback, menu=True), "3": Option(method=self.build, menu=True), "4": Option(method=self.flash, menu=False), "5": Option(method=self.build_flash, menu=False), @@ -65,6 +73,12 @@ class AdvancedMenu(BaseMenu): )[1:] print(menu, end="") + def klipper_rollback(self, **kwargs): + rollback_repository(KLIPPER_DIR, Klipper) + + def moonraker_rollback(self, **kwargs): + rollback_repository(MOONRAKER_DIR, Moonraker) + def build(self, **kwargs): KlipperBuildFirmwareMenu(previous_menu=self.__class__).run() diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py index 2ed9a4d..4feeaf3 100644 --- a/kiauh/utils/git_utils.py +++ b/kiauh/utils/git_utils.py @@ -2,8 +2,12 @@ import json import urllib.request from http.client import HTTPResponse from json import JSONDecodeError -from typing import List +from subprocess import CalledProcessError, PIPE, run +from typing import List, Type +from core.instance_manager.base_instance import BaseInstance +from core.instance_manager.instance_manager import InstanceManager +from utils.input_utils import get_number_input, get_confirm from utils.logger import Logger @@ -55,3 +59,33 @@ def get_latest_unstable_tag(repo_path: str) -> str: except Exception: Logger.print_error("Error while getting the latest unstable tag") raise + + +def rollback_repository(repo_dir: str, instance: Type[BaseInstance]) -> None: + q1 = "How many commits do you want to roll back" + amount = get_number_input(q1, 1, allow_go_back=True) + + im = InstanceManager(instance) + + Logger.print_warn("Do not continue if you have ongoing prints!", start="\n") + Logger.print_warn( + f"All currently running {im.instance_type.__name__} services will be stopped!" + ) + if not get_confirm( + f"Roll back {amount} commit{'s' if amount > 1 else ''}", + default_choice=False, + allow_go_back=True, + ): + Logger.print_info("Aborting roll back ...") + return + + im.stop_all_instance() + + try: + cmd = ["git", "reset", "--hard", f"HEAD~{amount}"] + run(cmd, cwd=repo_dir, check=True, stdout=PIPE, stderr=PIPE) + Logger.print_ok(f"Rolled back {amount} commits!", start="\n") + except CalledProcessError as e: + Logger.print_error(f"An error occured during repo rollback:\n{e}") + + im.start_all_instance() From 2c1c94c9048e0c5c6f21bfbfa4de18fa0b9fe934 Mon Sep 17 00:00:00 2001 From: Staubgeborener Date: Fri, 19 Apr 2024 17:58:41 +0200 Subject: [PATCH 170/296] feat: Add Klipper-Backup to KIAUH (#457) --- kiauh/extensions/klipper_backup/__init__.py | 18 +++ .../klipper_backup_extension.py | 150 ++++++++++++++++++ kiauh/extensions/klipper_backup/metadata.json | 10 ++ 3 files changed, 178 insertions(+) create mode 100644 kiauh/extensions/klipper_backup/__init__.py create mode 100644 kiauh/extensions/klipper_backup/klipper_backup_extension.py create mode 100644 kiauh/extensions/klipper_backup/metadata.json diff --git a/kiauh/extensions/klipper_backup/__init__.py b/kiauh/extensions/klipper_backup/__init__.py new file mode 100644 index 0000000..6597237 --- /dev/null +++ b/kiauh/extensions/klipper_backup/__init__.py @@ -0,0 +1,18 @@ +# ======================================================================= # +# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet # +# https://github.com/Staubgeborener/klipper-backup # +# # +# 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 pathlib import Path + +EXT_MODULE_NAME = "klipper_backup_extension.py" +MODULE_PATH = Path(__file__).resolve().parent +MOONRAKER_CONF = Path.home().joinpath("printer_data", "config", "moonraker.conf") +KLIPPERBACKUP_DIR = Path.home().joinpath("klipper-backup") +KLIPPERBACKUP_CONFIG_DIR = Path.home().joinpath("config_backup") +KLIPPERBACKUP_REPO_URL = "https://github.com/staubgeborener/klipper-backup" diff --git a/kiauh/extensions/klipper_backup/klipper_backup_extension.py b/kiauh/extensions/klipper_backup/klipper_backup_extension.py new file mode 100644 index 0000000..e10b0ce --- /dev/null +++ b/kiauh/extensions/klipper_backup/klipper_backup_extension.py @@ -0,0 +1,150 @@ +# ======================================================================= # +# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet # +# https://github.com/Staubgeborener/klipper-backup # +# # +# 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 # +# ======================================================================= # + +import os +import shutil +import subprocess + +from extensions.base_extension import BaseExtension +from extensions.klipper_backup import ( + KLIPPERBACKUP_REPO_URL, + KLIPPERBACKUP_DIR, + KLIPPERBACKUP_CONFIG_DIR, + MOONRAKER_CONF, +) + +from utils.filesystem_utils import check_file_exist +from utils.input_utils import get_confirm +from utils.logger import Logger + + +# noinspection PyMethodMayBeStatic +class KlipperbackupExtension(BaseExtension): + def install_extension(self, **kwargs) -> None: + if not KLIPPERBACKUP_DIR.exists(): + subprocess.run(["git", "clone", str(KLIPPERBACKUP_REPO_URL), str(KLIPPERBACKUP_DIR)]) + #subprocess.run(["git", "-C", str(KLIPPERBACKUP_DIR), "checkout", "installer-dev"]) # Only for testing + subprocess.run(["chmod", "+x", str(KLIPPERBACKUP_DIR / "install.sh")]) + subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh")]) + + def update_extension(self, **kwargs) -> None: + extension_installed = check_file_exist(KLIPPERBACKUP_DIR) + if not extension_installed: + Logger.print_info("Extension does not seem to be installed! Skipping ...") + return + else: + subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh"), "check_updates"]) + + def remove_extension(self, **kwargs) -> None: + def is_service_installed(service_name): + command = ["systemctl", "status", service_name] + result = subprocess.run(command, capture_output=True, text=True) + # Doesn't matter whether the service is active or not, what matters is whether it is installed. So let's search for "Loaded:" in stdout + if "Loaded:" in result.stdout: + return True + else: + return False + + def uninstall_service(service_name): + try: + subprocess.run(["sudo", "systemctl", "stop", service_name], check=True) + subprocess.run(["sudo", "systemctl", "disable", service_name], check=True) + subprocess.run(["sudo", "systemctl", "daemon-reload"], check=True) + service_path = f'/etc/systemd/system/{service_name}' + os.system(f'sudo rm {service_path}') + return True + except subprocess.CalledProcessError: + return False + + def check_crontab_entry(entry): + try: + crontab_content = subprocess.check_output(["crontab", "-l"], stderr=subprocess.DEVNULL, text=True) + except subprocess.CalledProcessError: + return False + for line in crontab_content.splitlines(): + if entry in line: + return True + return False + + extension_installed = check_file_exist(KLIPPERBACKUP_DIR) + if not extension_installed: + Logger.print_info("Extension does not seem to be installed! Skipping ...") + return + + def remove_moonraker_entry(): + original_file_path = MOONRAKER_CONF + comparison_file_path = os.path.join(str(KLIPPERBACKUP_DIR), 'install-files', 'moonraker.conf') + if not os.path.exists(original_file_path) or not os.path.exists(comparison_file_path): + return False + with open(original_file_path, 'r') as original_file, open(comparison_file_path, 'r') as comparison_file: + original_content = original_file.read() + comparison_content = comparison_file.read() + if comparison_content in original_content: + modified_content = original_content.replace(comparison_content, '').strip() + modified_content = "\n".join(line for line in modified_content.split("\n") if line.strip()) + with open(original_file_path, 'w') as original_file: + original_file.write(modified_content) + return True + else: + return False + + question = "Do you really want to remove the extension?" + if get_confirm(question, True, False): + + # Remove Klipper-Backup services + service_names = ["klipper-backup-on-boot.service", "klipper-backup-filewatch.service"] + for service_name in service_names: + try: + Logger.print_status(f"Check whether the service {service_name} is installed ...") + if is_service_installed(service_name): + Logger.print_info(f"Service {service_name} detected.") + if uninstall_service(service_name): + Logger.print_ok(f"The service {service_name} has been successfully uninstalled.") + else: + Logger.print_error(f"Error uninstalling the service {service_name}.") + else: + Logger.print_info(f"The service {service_name} is not installed. Skipping ...") + except: + Logger.print_error(f"Unable to remove the service {service_name}") + + # Remove Klipper-Backup cron + Logger.print_status("Check for Klipper-Backup cron entry ...") + entry_to_check = "/klipper-backup/script.sh" + try: + if check_crontab_entry(entry_to_check): + crontab_content = subprocess.check_output(["crontab", "-l"], text=True) + modified_content = "\n".join(line for line in crontab_content.splitlines() if entry_to_check not in line) + subprocess.run(["crontab", "-"], input=modified_content, text=True, check=True) + Logger.print_ok("The Klipper-Backup entry has been removed from the crontab.") + else: + Logger.print_info("The Klipper-Backup entry is not present in the crontab. Skipping ...") + except: + Logger.print_error("Unable to remove the Klipper-Backup cron entry") + + # Remove Moonraker entry + Logger.print_status(f"Check for Klipper-Backup moonraker entry ...") + try: + if remove_moonraker_entry(): + Logger.print_ok("Klipper-Backup entry in moonraker.conf removed") + else: + Logger.print_info("Klipper-Backup entry not found in moonraker.conf. Skipping ...") + except: + Logger.print_error("Unknown error, either the moonraker.conf is not found or the Klipper-Backup entry under ~/klipper-backup/install-files/moonraker.conf. Skipping ...") + + # Remove Klipper-Backup + Logger.print_status(f"Removing '{KLIPPERBACKUP_DIR}' ...") + try: + shutil.rmtree(KLIPPERBACKUP_DIR) + config_backup_exists = check_file_exist(KLIPPERBACKUP_CONFIG_DIR) + if config_backup_exists: + shutil.rmtree(KLIPPERBACKUP_CONFIG_DIR) + Logger.print_ok("Extension Klipper-Backup successfully removed!") + except OSError as e: + Logger.print_error(f"Unable to remove extension: {e}") diff --git a/kiauh/extensions/klipper_backup/metadata.json b/kiauh/extensions/klipper_backup/metadata.json new file mode 100644 index 0000000..9e5ef7e --- /dev/null +++ b/kiauh/extensions/klipper_backup/metadata.json @@ -0,0 +1,10 @@ +{ + "metadata": { + "index": 3, + "module": "klipper_backup_extension", + "maintained_by": "Staubgeborener", + "display_name": "Klipper-Backup", + "description": "Backup all your klipper files in GitHub", + "updates": true + } +} From fda99bb70a3f2ca3bf45689fd0d45ce04a0c6222 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 19 Apr 2024 18:05:49 +0200 Subject: [PATCH 171/296] chore: format Signed-off-by: Dominik Willner --- .../klipper_backup_extension.py | 92 ++++++++++++++----- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/kiauh/extensions/klipper_backup/klipper_backup_extension.py b/kiauh/extensions/klipper_backup/klipper_backup_extension.py index e10b0ce..2f739cf 100644 --- a/kiauh/extensions/klipper_backup/klipper_backup_extension.py +++ b/kiauh/extensions/klipper_backup/klipper_backup_extension.py @@ -29,8 +29,10 @@ from utils.logger import Logger class KlipperbackupExtension(BaseExtension): def install_extension(self, **kwargs) -> None: if not KLIPPERBACKUP_DIR.exists(): - subprocess.run(["git", "clone", str(KLIPPERBACKUP_REPO_URL), str(KLIPPERBACKUP_DIR)]) - #subprocess.run(["git", "-C", str(KLIPPERBACKUP_DIR), "checkout", "installer-dev"]) # Only for testing + subprocess.run( + ["git", "clone", str(KLIPPERBACKUP_REPO_URL), str(KLIPPERBACKUP_DIR)] + ) + # subprocess.run(["git", "-C", str(KLIPPERBACKUP_DIR), "checkout", "installer-dev"]) # Only for testing subprocess.run(["chmod", "+x", str(KLIPPERBACKUP_DIR / "install.sh")]) subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh")]) @@ -55,17 +57,21 @@ class KlipperbackupExtension(BaseExtension): def uninstall_service(service_name): try: subprocess.run(["sudo", "systemctl", "stop", service_name], check=True) - subprocess.run(["sudo", "systemctl", "disable", service_name], check=True) + subprocess.run( + ["sudo", "systemctl", "disable", service_name], check=True + ) subprocess.run(["sudo", "systemctl", "daemon-reload"], check=True) - service_path = f'/etc/systemd/system/{service_name}' - os.system(f'sudo rm {service_path}') + service_path = f"/etc/systemd/system/{service_name}" + os.system(f"sudo rm {service_path}") return True except subprocess.CalledProcessError: return False def check_crontab_entry(entry): try: - crontab_content = subprocess.check_output(["crontab", "-l"], stderr=subprocess.DEVNULL, text=True) + crontab_content = subprocess.check_output( + ["crontab", "-l"], stderr=subprocess.DEVNULL, text=True + ) except subprocess.CalledProcessError: return False for line in crontab_content.splitlines(): @@ -80,16 +86,26 @@ class KlipperbackupExtension(BaseExtension): def remove_moonraker_entry(): original_file_path = MOONRAKER_CONF - comparison_file_path = os.path.join(str(KLIPPERBACKUP_DIR), 'install-files', 'moonraker.conf') - if not os.path.exists(original_file_path) or not os.path.exists(comparison_file_path): + comparison_file_path = os.path.join( + str(KLIPPERBACKUP_DIR), "install-files", "moonraker.conf" + ) + if not os.path.exists(original_file_path) or not os.path.exists( + comparison_file_path + ): return False - with open(original_file_path, 'r') as original_file, open(comparison_file_path, 'r') as comparison_file: + with open(original_file_path, "r") as original_file, open( + comparison_file_path, "r" + ) as comparison_file: original_content = original_file.read() comparison_content = comparison_file.read() if comparison_content in original_content: - modified_content = original_content.replace(comparison_content, '').strip() - modified_content = "\n".join(line for line in modified_content.split("\n") if line.strip()) - with open(original_file_path, 'w') as original_file: + modified_content = original_content.replace( + comparison_content, "" + ).strip() + modified_content = "\n".join( + line for line in modified_content.split("\n") if line.strip() + ) + with open(original_file_path, "w") as original_file: original_file.write(modified_content) return True else: @@ -97,20 +113,30 @@ class KlipperbackupExtension(BaseExtension): question = "Do you really want to remove the extension?" if get_confirm(question, True, False): - # Remove Klipper-Backup services - service_names = ["klipper-backup-on-boot.service", "klipper-backup-filewatch.service"] + service_names = [ + "klipper-backup-on-boot.service", + "klipper-backup-filewatch.service", + ] for service_name in service_names: try: - Logger.print_status(f"Check whether the service {service_name} is installed ...") + Logger.print_status( + f"Check whether the service {service_name} is installed ..." + ) if is_service_installed(service_name): Logger.print_info(f"Service {service_name} detected.") if uninstall_service(service_name): - Logger.print_ok(f"The service {service_name} has been successfully uninstalled.") + Logger.print_ok( + f"The service {service_name} has been successfully uninstalled." + ) else: - Logger.print_error(f"Error uninstalling the service {service_name}.") + Logger.print_error( + f"Error uninstalling the service {service_name}." + ) else: - Logger.print_info(f"The service {service_name} is not installed. Skipping ...") + Logger.print_info( + f"The service {service_name} is not installed. Skipping ..." + ) except: Logger.print_error(f"Unable to remove the service {service_name}") @@ -119,12 +145,24 @@ class KlipperbackupExtension(BaseExtension): entry_to_check = "/klipper-backup/script.sh" try: if check_crontab_entry(entry_to_check): - crontab_content = subprocess.check_output(["crontab", "-l"], text=True) - modified_content = "\n".join(line for line in crontab_content.splitlines() if entry_to_check not in line) - subprocess.run(["crontab", "-"], input=modified_content, text=True, check=True) - Logger.print_ok("The Klipper-Backup entry has been removed from the crontab.") + crontab_content = subprocess.check_output( + ["crontab", "-l"], text=True + ) + modified_content = "\n".join( + line + for line in crontab_content.splitlines() + if entry_to_check not in line + ) + subprocess.run( + ["crontab", "-"], input=modified_content, text=True, check=True + ) + Logger.print_ok( + "The Klipper-Backup entry has been removed from the crontab." + ) else: - Logger.print_info("The Klipper-Backup entry is not present in the crontab. Skipping ...") + Logger.print_info( + "The Klipper-Backup entry is not present in the crontab. Skipping ..." + ) except: Logger.print_error("Unable to remove the Klipper-Backup cron entry") @@ -134,9 +172,13 @@ class KlipperbackupExtension(BaseExtension): if remove_moonraker_entry(): Logger.print_ok("Klipper-Backup entry in moonraker.conf removed") else: - Logger.print_info("Klipper-Backup entry not found in moonraker.conf. Skipping ...") + Logger.print_info( + "Klipper-Backup entry not found in moonraker.conf. Skipping ..." + ) except: - Logger.print_error("Unknown error, either the moonraker.conf is not found or the Klipper-Backup entry under ~/klipper-backup/install-files/moonraker.conf. Skipping ...") + Logger.print_error( + "Unknown error, either the moonraker.conf is not found or the Klipper-Backup entry under ~/klipper-backup/install-files/moonraker.conf. Skipping ..." + ) # Remove Klipper-Backup Logger.print_status(f"Removing '{KLIPPERBACKUP_DIR}' ...") From c67ea2245dc407e62cf223bd5542f7fd0e93bf9b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 19 Apr 2024 18:29:39 +0200 Subject: [PATCH 172/296] fix: return sorted extension dict Signed-off-by: Dominik Willner --- kiauh/extensions/extensions_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py index 7d7e59e..80c6a85 100644 --- a/kiauh/extensions/extensions_menu.py +++ b/kiauh/extensions/extensions_menu.py @@ -74,7 +74,7 @@ class ExtensionsMenu(BaseMenu): except (IOError, json.JSONDecodeError, ImportError) as e: print(f"Failed loading extension {ext}: {e}") - return ext_dict + return dict(sorted(ext_dict.items())) def extension_submenu(self, **kwargs): ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run() From 372c9c0b7de97ff6da4a2fc54a2c9d9c136eff28 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 25 Apr 2024 20:57:35 +0200 Subject: [PATCH 173/296] refactor: update remove menu ui Signed-off-by: Dominik Willner --- kiauh/core/menus/remove_menu.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index ee6272a..d7ce2c0 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -55,17 +55,13 @@ class RemoveMenu(BaseMenu): |-------------------------------------------------------| | INFO: Configurations and/or any backups will be kept! | |-------------------------------------------------------| - | Firmware & API: | Webcam Streamer: | - | 1) [Klipper] | 6) [Crowsnest] | - | 2) [Moonraker] | 7) [MJPG-Streamer] | - | | | - | Klipper Webinterface: | Other: | - | 3) [Mainsail] | 8) [PrettyGCode] | - | 4) [Fluidd] | 9) [Telegram Bot] | - | | 10) [Obico for Klipper] | - | Touchscreen GUI: | 11) [OctoEverywhere] | - | 5) [KlipperScreen] | 12) [Mobileraker] | - | | 13) [NGINX] | + | Firmware & API: | Touchscreen GUI: | + | 1) [Klipper] | 5) [KlipperScreen] | + | 2) [Moonraker] | | + | | Webcam Streamer: | + | Klipper Webinterface: | 6) [Crowsnest] | + | 3) [Mainsail] | | + | 4) [Fluidd] | | | | | """ )[1:] From 36b295bd1bd76f60524332a8b3c784642fae6f57 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 26 Apr 2024 17:26:15 +0200 Subject: [PATCH 174/296] refactor: clean up fetch_status code Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 37 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index d347807..67a61ee 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -85,28 +85,29 @@ class MainMenu(BaseMenu): ) def fetch_status(self) -> None: - # klipper - klipper_status = get_klipper_status() - kl_status = klipper_status.get("status") - kl_code = klipper_status.get("status_code") - kl_instances = f" {klipper_status.get('instances')}" if kl_code == 1 else "" - self.kl_status = self.format_status_by_code(kl_code, kl_status, kl_instances) - self.kl_repo = f"{COLOR_CYAN}{klipper_status.get('repo')}{RESET_FORMAT}" - # moonraker - moonraker_status = get_moonraker_status() - mr_status = moonraker_status.get("status") - mr_code = moonraker_status.get("status_code") - mr_instances = f" {moonraker_status.get('instances')}" if mr_code == 1 else "" - self.mr_status = self.format_status_by_code(mr_code, mr_status, mr_instances) - self.mr_repo = f"{COLOR_CYAN}{moonraker_status.get('repo')}{RESET_FORMAT}" - # mainsail + self._update_status("kl", get_klipper_status) + self._update_status("mr", get_moonraker_status) self.ms_status = get_client_status(MainsailData()) - # fluidd self.fl_status = get_client_status(FluiddData()) - # client-config self.cc_status = get_current_client_config([MainsailData(), FluiddData()]) - def format_status_by_code(self, code: int, status: str, count: str) -> str: + def _update_status(self, status_name: str, status_fn: callable) -> None: + status_data = status_fn() + status = status_data.get("status") + code = status_data.get("status_code") + instances = f" {status_data.get('instances')}" if code == 1 else "" + setattr( + self, + f"{status_name}_status", + self._format_status_by_code(code, status, instances), + ) + setattr( + self, + f"{status_name}_repo", + f"{COLOR_CYAN}{status_data.get('repo')}{RESET_FORMAT}", + ) + + def _format_status_by_code(self, code: int, status: str, count: str) -> str: if code == 1: return f"{COLOR_GREEN}{status}{count}{RESET_FORMAT}" elif code == 2: From 02eebff57141f5a195cf0a215548082f478f68d0 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 27 Apr 2024 15:03:29 +0200 Subject: [PATCH 175/296] feat: implement KiauhSettings and use it where appropriate Signed-off-by: Dominik Willner --- default_kiauh.cfg | 18 +++ kiauh/__init__.py | 2 - kiauh/components/klipper/__init__.py | 1 - kiauh/components/klipper/klipper_setup.py | 24 ++-- kiauh/components/moonraker/__init__.py | 1 - kiauh/components/moonraker/moonraker_setup.py | 25 ++-- .../client_config/client_config_setup.py | 7 +- kiauh/components/webui_client/client_setup.py | 18 ++- kiauh/core/settings/__init__.py | 0 kiauh/core/settings/kiauh_settings.py | 117 ++++++++++++++++++ kiauh/main.py | 2 + 11 files changed, 164 insertions(+), 51 deletions(-) create mode 100644 default_kiauh.cfg create mode 100644 kiauh/core/settings/__init__.py create mode 100644 kiauh/core/settings/kiauh_settings.py diff --git a/default_kiauh.cfg b/default_kiauh.cfg new file mode 100644 index 0000000..cd36055 --- /dev/null +++ b/default_kiauh.cfg @@ -0,0 +1,18 @@ +[kiauh] +backup_before_update: False + +[klipper] +repo_url: https://github.com/Klipper3d/klipper +branch: master + +[moonraker] +repo_url: https://github.com/Arksine/moonraker +branch: master + +[mainsail] +port: 80 +unstable_releases: False + +[fluidd] +port: 81 +unstable_releases: False diff --git a/kiauh/__init__.py b/kiauh/__init__.py index 105b426..5c08988 100644 --- a/kiauh/__init__.py +++ b/kiauh/__init__.py @@ -11,7 +11,5 @@ import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent -KIAUH_CFG = PROJECT_ROOT.joinpath("kiauh.cfg") - APPLICATION_ROOT = Path(__file__).resolve().parent sys.path.append(str(APPLICATION_ROOT)) diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index aee9c25..6e8d50c 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -17,6 +17,5 @@ KLIPPER_DIR = Path.home().joinpath("klipper") KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups") KLIPPER_REQUIREMENTS_TXT = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") -DEFAULT_KLIPPER_REPO_URL = "https://github.com/Klipper3D/klipper" EXIT_KLIPPER_SETUP = "Exiting Klipper setup ..." diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index db329cb..c63c607 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -12,10 +12,9 @@ from pathlib import Path from components.webui_client.client_utils import ( get_existing_clients, ) -from kiauh import KIAUH_CFG +from core.settings.kiauh_settings import KiauhSettings from components.klipper import ( EXIT_KLIPPER_SETUP, - DEFAULT_KLIPPER_REPO_URL, KLIPPER_DIR, KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT, @@ -36,7 +35,6 @@ from components.klipper.klipper_utils import ( backup_klipper_dir, ) from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager from core.repo_manager.repo_manager import RepoManager from utils.input_utils import get_confirm @@ -109,13 +107,10 @@ def install_klipper() -> None: def setup_klipper_prerequesites() -> None: - cm = ConfigManager(cfg_file=KIAUH_CFG) - repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL) - branch = str(cm.get_value("klipper", "branch") or "master") - + settings = KiauhSettings() repo_manager = RepoManager( - repo=repo, - branch=branch, + repo=settings.get("klipper", "repo_url"), + branch=settings.get("klipper", "branch"), target_dir=KLIPPER_DIR, ) repo_manager.clone_repo() @@ -150,19 +145,16 @@ def update_klipper() -> None: if not get_confirm("Update Klipper now?"): return - cm = ConfigManager(cfg_file=KIAUH_CFG) - if cm.get_value("kiauh", "backup_before_update"): + settings = KiauhSettings() + if settings.get("kiauh", "backup_before_update"): backup_klipper_dir() instance_manager = InstanceManager(Klipper) instance_manager.stop_all_instance() - repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL) - branch = str(cm.get_value("klipper", "branch") or "master") - repo_manager = RepoManager( - repo=repo, - branch=branch, + repo=settings.get("klipper", "repo_url"), + branch=settings.get("klipper", "branch"), target_dir=KLIPPER_DIR, ) repo_manager.pull_repo() diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index 6c4ff89..e357204 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -20,7 +20,6 @@ MOONRAKER_DB_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-db-backups") MOONRAKER_REQUIREMENTS_TXT = MOONRAKER_DIR.joinpath( "scripts/moonraker-requirements.txt" ) -DEFAULT_MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker" DEFAULT_MOONRAKER_PORT = 7125 # introduced due to diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 100ae9e..adcf522 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -16,11 +16,10 @@ from components.webui_client.client_utils import ( get_existing_clients, ) from components.webui_client.mainsail_data import MainsailData -from kiauh import KIAUH_CFG +from core.settings.kiauh_settings import KiauhSettings from components.klipper.klipper import Klipper from components.moonraker import ( EXIT_MOONRAKER_SETUP, - DEFAULT_MOONRAKER_REPO_URL, MOONRAKER_DIR, MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT, @@ -35,7 +34,6 @@ from components.moonraker.moonraker_utils import ( create_example_moonraker_conf, backup_moonraker_dir, ) -from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager from core.repo_manager.repo_manager import RepoManager from utils.filesystem_utils import check_file_exist @@ -133,11 +131,9 @@ def check_moonraker_install_requirements() -> bool: def setup_moonraker_prerequesites() -> None: - cm = ConfigManager(cfg_file=KIAUH_CFG) - repo = str( - cm.get_value("moonraker", "repository_url") or DEFAULT_MOONRAKER_REPO_URL - ) - branch = str(cm.get_value("moonraker", "branch") or "master") + settings = KiauhSettings() + repo = settings.get("moonraker", "repo_url") + branch = settings.get("moonraker", "branch") repo_manager = RepoManager( repo=repo, @@ -193,21 +189,16 @@ def update_moonraker() -> None: if not get_confirm("Update Moonraker now?"): return - cm = ConfigManager(cfg_file=KIAUH_CFG) - if cm.get_value("kiauh", "backup_before_update"): + settings = KiauhSettings() + if settings.get("kiauh", "backup_before_update"): backup_moonraker_dir() instance_manager = InstanceManager(Moonraker) instance_manager.stop_all_instance() - repo = str( - cm.get_value("moonraker", "repository_url") or DEFAULT_MOONRAKER_REPO_URL - ) - branch = str(cm.get_value("moonraker", "branch") or "master") - repo_manager = RepoManager( - repo=repo, - branch=branch, + repo=settings.get("moonraker", "repo_url"), + branch=settings.get("moonraker", "branch"), target_dir=MOONRAKER_DIR, ) repo_manager.pull_repo() diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 348069d..f54735c 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -13,7 +13,7 @@ from pathlib import Path from typing import List from components.webui_client.base_data import BaseWebClient, BaseWebClientConfig -from kiauh import KIAUH_CFG +from core.settings.kiauh_settings import KiauhSettings from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker from components.webui_client.client_dialogs import ( @@ -23,7 +23,6 @@ from components.webui_client.client_utils import ( backup_client_config_data, config_for_other_client_exist, ) -from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager from core.repo_manager.repo_manager import RepoManager @@ -108,8 +107,8 @@ def update_client_config(client: BaseWebClient) -> None: ) return - cm = ConfigManager(cfg_file=KIAUH_CFG) - if cm.get_value("kiauh", "backup_before_update"): + settings = KiauhSettings() + if settings.get("kiauh", "backup_before_update"): backup_client_config_data(client) repo_manager = RepoManager( diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 41a749b..88f168a 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -33,9 +33,8 @@ from components.webui_client.client_utils import ( symlink_webui_nginx_log, config_for_other_client_exist, ) -from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager -from kiauh import KIAUH_CFG +from core.settings.kiauh_settings import KiauhSettings from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from utils.common import check_install_dependencies from utils.filesystem_utils import ( @@ -104,24 +103,23 @@ def install_client(client: BaseWebClient) -> None: question = f"Download the recommended {client_config.display_name}?" install_client_cfg = get_confirm(question, allow_go_back=False) - cm = ConfigManager(cfg_file=KIAUH_CFG) - default_port = cm.get_value(client.name, "port") - client_port = default_port if default_port and default_port.isdigit() else "80" + settings = KiauhSettings() + port = settings.get(client.name, "port") ports_in_use = read_ports_from_nginx_configs() # check if configured port is a valid number and not in use already - valid_port = is_valid_port(client_port, ports_in_use) + valid_port = is_valid_port(port, ports_in_use) while not valid_port: next_port = get_next_free_port(ports_in_use) print_client_port_select_dialog(client.display_name, next_port, ports_in_use) - client_port = str( + port = str( get_number_input( f"Configure {client.display_name} for port", min_count=int(next_port), default=next_port, ) ) - valid_port = is_valid_port(client_port, ports_in_use) + valid_port = is_valid_port(port, ports_in_use) check_install_dependencies(["nginx"]) @@ -146,7 +144,7 @@ def install_client(client: BaseWebClient) -> None: copy_upstream_nginx_cfg() copy_common_vars_nginx_cfg() - create_client_nginx_cfg(client, client_port) + create_client_nginx_cfg(client, port) if kl_instances: symlink_webui_nginx_log(kl_instances) control_systemd_service("nginx", "restart") @@ -155,7 +153,7 @@ def install_client(client: BaseWebClient) -> None: Logger.print_error(f"{client.display_name} installation failed!\n{e}") return - log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{client_port}" + log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}" Logger.print_ok(f"{client.display_name} installation complete!", start="\n") Logger.print_ok(log, prefix=False, end="\n\n") diff --git a/kiauh/core/settings/__init__.py b/kiauh/core/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py new file mode 100644 index 0000000..7a9ef86 --- /dev/null +++ b/kiauh/core/settings/kiauh_settings.py @@ -0,0 +1,117 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import textwrap +import configparser +from configparser import ConfigParser +from typing import Dict, Union +from kiauh import PROJECT_ROOT +from utils.constants import RESET_FORMAT, COLOR_RED +from utils.logger import Logger +from utils.system_utils import kill + + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KiauhSettings: + _instance = None + _default_cfg = PROJECT_ROOT.joinpath("default_kiauh.cfg") + _custom_cfg = PROJECT_ROOT.joinpath("kiauh.cfg") + + def __new__(cls, *args, **kwargs) -> "KiauhSettings": + if cls._instance is None: + cls._instance = super(KiauhSettings, cls).__new__(cls, *args, **kwargs) + cls._instance.__initialized = False + return cls._instance + + def __init__(self) -> None: + if self.__initialized: + return + self.__initialized = True + self.settings: Dict[str, Dict[str, Union[str, int, bool]]] = {} + + self.config = ConfigParser() + if self._custom_cfg.exists(): + self.config.read(self._custom_cfg) + elif self._default_cfg.exists(): + self.config.read(self._default_cfg) + else: + self._kill() + + sections = self.config.sections() + for s in sections: + self.settings[s] = dict(self.config[s]) + + self._validate_cfg() + + def get(self, key: str, value: Union[str, int, bool]) -> Union[str, int, bool]: + return self.settings[key][value] + + def set(self, key: str, value: Union[str, int, bool]) -> None: + self.settings[key][value] = value + + def save(self) -> None: + for section, option in self.settings.items(): + self.config[section] = option + with open(self._custom_cfg, "w") as configfile: + self.config.write(configfile) + + def _validate_cfg(self) -> None: + try: + self._validate_bool("kiauh", "backup_before_update") + + self._validate_str("klipper", "repo_url") + self._validate_str("klipper", "branch") + + self._validate_int("mainsail", "port") + self._validate_bool("mainsail", "unstable_releases") + + self._validate_int("fluidd", "port") + self._validate_bool("fluidd", "unstable_releases") + + except ValueError: + err = f"Invalid value for option '{self._v_option}' in section '{self._v_section}'" + Logger.print_error(err) + kill() + except configparser.NoSectionError: + err = f"Missing section '{self._v_section}' in config file" + Logger.print_error(err) + kill() + except configparser.NoOptionError: + err = f"Missing option '{self._v_option}' in section '{self._v_section}'" + Logger.print_error(err) + kill() + + def _validate_bool(self, section: str, option: str) -> None: + self._v_section, self._v_option = (section, option) + bool(self.config.getboolean(section, option)) + + def _validate_int(self, section: str, option: str) -> None: + self._v_section, self._v_option = (section, option) + int(self.config.getint(section, option)) + + def _validate_str(self, section: str, option: str) -> None: + self._v_section, self._v_option = (section, option) + v = self.config.get(section, option) + if v.isdigit() or v.lower() == "true" or v.lower() == "false": + raise ValueError + + def _kill(self) -> None: + l1 = "!!! ERROR !!!" + l2 = "No KIAUH configuration file found!" + error = textwrap.dedent( + f""" + {COLOR_RED}/=======================================================\\ + | {l1:^53} | + | {l2:^53} | + \=======================================================/{RESET_FORMAT} + """ + )[1:] + print(error, end="") + kill() diff --git a/kiauh/main.py b/kiauh/main.py index 3040ec5..bd92b8f 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -8,11 +8,13 @@ # ======================================================================= # from core.menus.main_menu import MainMenu +from core.settings.kiauh_settings import KiauhSettings from utils.logger import Logger def main(): try: + KiauhSettings() MainMenu().run() except KeyboardInterrupt: Logger.print_ok("\nHappy printing!\n", prefix=False) From cb62909f41038aaa40a7f94e2aec5feca71079d3 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Apr 2024 12:12:45 +0200 Subject: [PATCH 176/296] feat: implement functions of SettingsMenu Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 2 +- kiauh/core/menus/settings_menu.py | 209 ++++++++++++++++++++++++-- kiauh/core/settings/kiauh_settings.py | 51 ++++--- kiauh/utils/input_utils.py | 26 +++- 4 files changed, 248 insertions(+), 40 deletions(-) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 67a61ee..35291bc 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -166,7 +166,7 @@ class MainMenu(BaseMenu): BackupMenu(previous_menu=self.__class__).run() def settings_menu(self, **kwargs): - SettingsMenu().run() + SettingsMenu(previous_menu=self.__class__).run() def extension_menu(self, **kwargs): ExtensionsMenu(previous_menu=self.__class__).run() diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 8ed407b..f049462 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -6,16 +6,37 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from typing import Type, Optional +import shutil +import textwrap +from pathlib import Path +from typing import Type, Optional, Tuple +from components.klipper import KLIPPER_DIR +from components.klipper.klipper import Klipper +from components.moonraker import MOONRAKER_DIR +from components.moonraker.moonraker import Moonraker +from core.instance_manager.instance_manager import InstanceManager +from core.menus import Option from core.menus.base_menu import BaseMenu +from core.repo_manager.repo_manager import RepoManager +from core.settings.kiauh_settings import KiauhSettings +from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_GREEN, COLOR_YELLOW +from utils.input_utils import get_string_input, get_confirm +from utils.logger import Logger +# noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class SettingsMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() self.previous_menu = previous_menu + self.klipper_repo = None + self.moonraker_repo = None + self.mainsail_unstable = None + self.fluidd_unstable = None + self.auto_backups_enabled = None + self._load_settings() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu @@ -25,19 +46,183 @@ class SettingsMenu(BaseMenu): ) def set_options(self) -> None: - pass + self.options = { + "1": Option(method=self.set_klipper_repo, menu=True), + "2": Option(method=self.set_moonraker_repo, menu=True), + "3": Option(method=self.toggle_mainsail_release, menu=True), + "4": Option(method=self.toggle_fluidd_release, menu=False), + "5": Option(method=self.toggle_backup_before_update, menu=False), + } def print_menu(self): - print("self") + header = " [ KIAUH Settings ] " + color = COLOR_CYAN + count = 62 - len(color) - len(RESET_FORMAT) + checked = f"[{COLOR_GREEN}x{RESET_FORMAT}]" + unchecked = "[ ]" + o1 = checked if self.mainsail_unstable else unchecked + o2 = checked if self.fluidd_unstable else unchecked + o3 = checked if self.auto_backups_enabled else unchecked + menu = textwrap.dedent( + f""" + /=======================================================\\ + | {color}{header:~^{count}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Klipper source repository: | + | ● {self.klipper_repo:<67} | + | | + | Moonraker source repository: | + | ● {self.moonraker_repo:<67} | + | | + | Install unstable Webinterface releases: | + | {o1} Mainsail | + | {o2} Fluidd | + | | + | Auto-Backup: | + | {o3} Automatic backup before update | + | | + |-------------------------------------------------------| + | 1) Set Klipper source repository | + | 2) Set Moonraker source repository | + | | + | 3) Toggle unstable Mainsail releases | + | 4) Toggle unstable Fluidd releases | + | | + | 5) Toggle automatic backups before updates | + """ + )[1:] + print(menu, end="") - def execute_option_p(self): - # Implement the functionality for Option P - print("Executing Option P") + def _load_settings(self): + self.kiauh_settings = KiauhSettings() - def execute_option_q(self): - # Implement the functionality for Option Q - print("Executing Option Q") + self._format_repo_str("klipper") + self._format_repo_str("moonraker") - def execute_option_r(self): - # Implement the functionality for Option R - print("Executing Option R") + self.auto_backups_enabled = self.kiauh_settings.get( + "kiauh", "backup_before_update" + ) + self.mainsail_unstable = self.kiauh_settings.get( + "mainsail", "unstable_releases" + ) + self.fluidd_unstable = self.kiauh_settings.get("fluidd", "unstable_releases") + + def _format_repo_str(self, repo_name: str) -> None: + repo = self.kiauh_settings.get(repo_name, "repo_url") + repo = f"{'/'.join(repo.rsplit('/', 2)[-2:])}" + branch = self.kiauh_settings.get(repo_name, "branch") + branch = f"({COLOR_CYAN}@ {branch}{RESET_FORMAT})" + setattr(self, f"{repo_name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT} {branch}") + + def _gather_input(self) -> Tuple[str, str]: + l2 = "Make sure your input is correct!" + error = textwrap.dedent( + f"""\n + {COLOR_YELLOW}/=======================================================\\ + | ATTENTION: | + | There is no input validation in place! Make sure your | + | input is valid and has no typos! For any change to | + | take effect, the repository must be cloned again. | + | Make sure you don't have any ongoing prints running, | + | as the services will be restarted! | + \=======================================================/{RESET_FORMAT} + """ + )[1:] + print(error, end="\n") + repo = get_string_input( + "Enter new repository URL", + allow_special_chars=True, + ) + branch = get_string_input( + "Enter new branch name", + allow_special_chars=True, + ) + + return repo, branch + + def _display_summary(self, name: str, repo: str, branch: str): + l1 = f"New {name} repository URL:" + l2 = f"● {repo}" + l3 = f"New {name} repository branch:" + l4 = f"● {branch}" + summary = textwrap.dedent( + f"""\n + /=======================================================\\ + | {l1:<52} | + | {l2:<52} | + | {l3:<52} | + | {l4:<52} | + \=======================================================/ + """ + )[1:] + print(summary, end="") + + def _set_repo(self, repo_name: str): + repo, branch = self._gather_input() + self._display_summary(repo_name.capitalize(), repo, branch) + + if get_confirm("Apply changes?", allow_go_back=True): + self.kiauh_settings.set(repo_name, "repo_url", repo) + self.kiauh_settings.set(repo_name, "branch", branch) + self.kiauh_settings.save() + self._load_settings() + Logger.print_ok("Changes saved!") + else: + Logger.print_info( + f"Skipping change of {repo_name.capitalize()} source repository ..." + ) + return + + Logger.print_status( + f"Switching to {repo_name.capitalize()}'s new source repository ..." + ) + self._switch_repo(repo_name) + Logger.print_ok(f"Switched to {repo} at branch {branch}!") + + def _switch_repo(self, name: str) -> None: + target_dir: Path + if name == "klipper": + target_dir = KLIPPER_DIR + _type = Klipper + elif name == "moonraker": + target_dir = MOONRAKER_DIR + _type = Moonraker + else: + Logger.print_error("Invalid repository name!") + return + + if target_dir.exists(): + shutil.rmtree(target_dir) + + im = InstanceManager(_type) + im.stop_all_instance() + + repo = self.kiauh_settings.get(name, "repo_url") + branch = self.kiauh_settings.get(name, "branch") + repman = RepoManager(repo, str(target_dir), branch) + repman.clone_repo() + + im.start_all_instance() + + def set_klipper_repo(self, **kwargs): + self._set_repo("klipper") + + def set_moonraker_repo(self, **kwargs): + self._set_repo("moonraker") + + def toggle_mainsail_release(self, **kwargs): + self.mainsail_unstable = not self.mainsail_unstable + self.kiauh_settings.set("mainsail", "unstable_releases", self.mainsail_unstable) + self.kiauh_settings.save() + + def toggle_fluidd_release(self, **kwargs): + self.fluidd_unstable = not self.fluidd_unstable + self.kiauh_settings.set("fluidd", "unstable_releases", self.fluidd_unstable) + self.kiauh_settings.save() + + def toggle_backup_before_update(self, **kwargs): + self.auto_backups_enabled = not self.auto_backups_enabled + self.kiauh_settings.set( + "kiauh", "backup_before_update", self.auto_backups_enabled + ) + self.kiauh_settings.save() diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 7a9ef86..8eddb12 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -9,8 +9,9 @@ import textwrap import configparser -from configparser import ConfigParser from typing import Dict, Union + +from core.config_manager.config_manager import CustomConfigParser from kiauh import PROJECT_ROOT from utils.constants import RESET_FORMAT, COLOR_RED from utils.logger import Logger @@ -34,33 +35,32 @@ class KiauhSettings: if self.__initialized: return self.__initialized = True + self.config = CustomConfigParser() self.settings: Dict[str, Dict[str, Union[str, int, bool]]] = {} + self._load_settings() - self.config = ConfigParser() - if self._custom_cfg.exists(): - self.config.read(self._custom_cfg) - elif self._default_cfg.exists(): - self.config.read(self._default_cfg) - else: - self._kill() + def get(self, section: str, option: str) -> Union[str, int, bool]: + return self.settings[section][option] - sections = self.config.sections() - for s in sections: - self.settings[s] = dict(self.config[s]) - - self._validate_cfg() - - def get(self, key: str, value: Union[str, int, bool]) -> Union[str, int, bool]: - return self.settings[key][value] - - def set(self, key: str, value: Union[str, int, bool]) -> None: - self.settings[key][value] = value + def set(self, section: str, option: str, value: Union[str, int, bool]) -> None: + self.settings[section][option] = value def save(self) -> None: for section, option in self.settings.items(): self.config[section] = option with open(self._custom_cfg, "w") as configfile: self.config.write(configfile) + self._load_settings() + + def _load_settings(self) -> None: + if self._custom_cfg.exists(): + self.config.read(self._custom_cfg) + elif self._default_cfg.exists(): + self.config.read(self._default_cfg) + else: + self._kill() + self._validate_cfg() + self._parse_settings() def _validate_cfg(self) -> None: try: @@ -102,6 +102,19 @@ class KiauhSettings: if v.isdigit() or v.lower() == "true" or v.lower() == "false": raise ValueError + def _parse_settings(self): + for s in self.config.sections(): + self.settings[s] = {} + for o, v in self.config.items(s): + if v.lower() == "true": + self.settings[s][o] = True + elif v.lower() == "false": + self.settings[s][o] = False + elif v.isdigit(): + self.settings[s][o] = int(v) + else: + self.settings[s][o] = v + def _kill(self) -> None: l1 = "!!! ERROR !!!" l2 = "No KIAUH configuration file found!" diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 88b9188..4738681 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -7,7 +7,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from typing import Optional, List, Union +from typing import List, Union from utils import INVALID_CHOICE from utils.constants import COLOR_CYAN, RESET_FORMAT @@ -82,23 +82,33 @@ def get_number_input( Logger.print_error(INVALID_CHOICE) -def get_string_input(question: str, exclude=Optional[List], default=None) -> str: +def get_string_input( + question: str, + exclude: Union[List, None] = None, + allow_special_chars=False, + default=None, +) -> str: """ Helper method to get a string input from the user :param question: The question to display :param exclude: List of strings which are not allowed + :param allow_special_chars: Wheter to allow special characters in the input :param default: Optional default value :return: The validated string value """ + _exclude = [] if exclude is None else exclude + _question = format_question(question, default) while True: - _input = input(format_question(question, default)).strip() + _input = input(_question) - if _input.isalnum() and _input.lower() not in exclude: - return _input - - Logger.print_error(INVALID_CHOICE) - if _input in exclude: + if _input.lower() in _exclude: Logger.print_error("This value is already in use/reserved.") + elif allow_special_chars: + return _input + elif not allow_special_chars and _input.isalnum(): + return _input + else: + Logger.print_error(INVALID_CHOICE) def get_selection_input(question: str, option_list: List, default=None) -> str: From d420daca263d66d43bf93ceb6af9e773ee590c20 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Apr 2024 12:49:44 +0200 Subject: [PATCH 177/296] fix: options not applied to self.options Signed-off-by: Dominik Willner --- .../webui_client/menus/client_remove_menu.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index 8cfa50f..f38da19 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -8,7 +8,7 @@ # ======================================================================= # import textwrap -from typing import Dict, Type, Optional +from typing import Type, Optional from components.webui_client import client_remove from components.webui_client.base_data import BaseWebClient, WebClientType @@ -36,17 +36,15 @@ class ClientRemoveMenu(BaseMenu): previous_menu if previous_menu is not None else RemoveMenu ) - def set_options(self) -> Dict[str, Option]: - options = { + def set_options(self) -> None: + self.options = { "0": Option(method=self.toggle_all, menu=False), "1": Option(method=self.toggle_rm_client, menu=False), "2": Option(method=self.toggle_rm_client_config, menu=False), "c": Option(method=self.run_removal_process, menu=False), } if self.client.client == WebClientType.MAINSAIL: - options["3"] = Option(self.toggle_backup_mainsail_config_json, False) - - return options + self.options["3"] = Option(self.toggle_backup_mainsail_config_json, False) def print_menu(self) -> None: client_name = self.client.display_name From 51f0713c5addaabc0da3016c91d5168a30efc484 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Apr 2024 19:40:30 +0200 Subject: [PATCH 178/296] refactor: print traceback of exception Signed-off-by: Dominik Willner --- kiauh/core/menus/base_menu.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 68e27b9..ef169b4 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -12,6 +12,7 @@ from __future__ import annotations import subprocess import sys import textwrap +import traceback from abc import abstractmethod from typing import Type, Dict, Optional @@ -213,4 +214,6 @@ class BaseMenu(metaclass=PostInitCaller): option.method(opt_index=option.opt_index, opt_data=option.opt_data) self.run() except Exception as e: - Logger.print_error(f"An unexpected error occured:\n{e}") + Logger.print_error( + f"An unexpected error occured:\n{e}\n{traceback.format_exc()}" + ) From 5225e70e83ec53204a2eb7db4da01b9d6d2ded0e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Apr 2024 19:41:38 +0200 Subject: [PATCH 179/296] refactor: replace two seperate download url properties by only one Signed-off-by: Dominik Willner --- kiauh/components/webui_client/base_data.py | 7 +------ kiauh/components/webui_client/client_utils.py | 20 ++++++++++++++++++- kiauh/components/webui_client/fluidd_data.py | 17 +++------------- .../components/webui_client/mainsail_data.py | 16 +++------------ 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/kiauh/components/webui_client/base_data.py b/kiauh/components/webui_client/base_data.py index 0fe34da..3e7ce42 100644 --- a/kiauh/components/webui_client/base_data.py +++ b/kiauh/components/webui_client/base_data.py @@ -59,12 +59,7 @@ class BaseWebClient(ABC): @property @abstractmethod - def stable_url(self) -> str: - raise NotImplementedError - - @property - @abstractmethod - def unstable_url(self) -> str: + def download_url(self) -> str: raise NotImplementedError @property diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 2d1d5bd..819bcd6 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -22,10 +22,11 @@ from components.webui_client.base_data import ( from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager from core.repo_manager.repo_manager import RepoManager +from core.settings.kiauh_settings import KiauhSettings from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD from utils.common import get_install_status_webui from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW -from utils.git_utils import get_latest_tag +from utils.git_utils import get_latest_tag, get_latest_unstable_tag from utils.logger import Logger @@ -201,3 +202,20 @@ def config_for_other_client_exist(client_to_ignore: WebClientType) -> bool: clients = clients - {client_to_ignore.value} return True if len(clients) > 0 else False + + +def get_download_url(base_url: str, client: BaseWebClient) -> str: + settings = KiauhSettings() + use_unstable = settings.get(client.name, "unstable_releases") + stable_url = f"{base_url}/latest/download/{client.name}.zip" + + if not use_unstable: + return stable_url + + try: + unstable_tag = get_latest_unstable_tag(client.repo_path) + if unstable_tag == "": + raise Exception + return f"{base_url}/download/{unstable_tag}/{client.name}.zip" + except Exception: + return stable_url diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py index 4099eeb..6d34279 100644 --- a/kiauh/components/webui_client/fluidd_data.py +++ b/kiauh/components/webui_client/fluidd_data.py @@ -18,8 +18,8 @@ from components.webui_client.base_data import ( WebClientType, BaseWebClient, ) +from components.webui_client.client_utils import get_download_url from core.backup_manager import BACKUP_ROOT_DIR -from utils.git_utils import get_latest_unstable_tag @dataclass(frozen=True) @@ -46,19 +46,8 @@ class FluiddData(BaseWebClient): repo_path: str = "fluidd-core/fluidd" @property - def stable_url(self) -> str: - return f"{self.BASE_DL_URL}/latest/download/fluidd.zip" - - @property - def unstable_url(self) -> str: - try: - unstable_tag = get_latest_unstable_tag(self.repo_path) - if unstable_tag != "": - return f"{self.BASE_DL_URL}/download/{unstable_tag}/fluidd.zip" - else: - raise Exception - except Exception: - return self.stable_url + def download_url(self) -> str: + return get_download_url(self.BASE_DL_URL, self) @property def client_config(self) -> BaseWebClientConfig: diff --git a/kiauh/components/webui_client/mainsail_data.py b/kiauh/components/webui_client/mainsail_data.py index 208ce2c..91299c2 100644 --- a/kiauh/components/webui_client/mainsail_data.py +++ b/kiauh/components/webui_client/mainsail_data.py @@ -19,7 +19,6 @@ from components.webui_client.base_data import ( BaseWebClient, ) from core.backup_manager import BACKUP_ROOT_DIR -from utils.git_utils import get_latest_unstable_tag @dataclass(frozen=True) @@ -46,19 +45,10 @@ class MainsailData(BaseWebClient): repo_path: str = "mainsail-crew/mainsail" @property - def stable_url(self) -> str: - return f"{self.BASE_DL_URL}/latest/download/mainsail.zip" + def download_url(self) -> str: + from components.webui_client.client_utils import get_download_url - @property - def unstable_url(self) -> str: - try: - unstable_tag = get_latest_unstable_tag(self.repo_path) - if unstable_tag != "": - return f"{self.BASE_DL_URL}/download/{unstable_tag}/mainsail.zip" - else: - raise Exception - except Exception: - return self.stable_url + return get_download_url(self.BASE_DL_URL, self) @property def client_config(self) -> BaseWebClientConfig: From 1a29324e6ae6bf6e39d848da12eaddef765edc6c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Apr 2024 19:42:40 +0200 Subject: [PATCH 180/296] refactor: handle ports as ints as they are coming as ints from the KiauhSettings Signed-off-by: Dominik Willner --- kiauh/components/webui_client/client_dialogs.py | 2 +- kiauh/components/webui_client/client_setup.py | 14 ++++++-------- kiauh/utils/filesystem_utils.py | 13 +++++++------ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py index 1fce323..1932ad6 100644 --- a/kiauh/components/webui_client/client_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -55,7 +55,7 @@ def print_client_already_installed_dialog(name: str): print_back_footer() -def print_client_port_select_dialog(name: str, port: str, ports_in_use: List[str]): +def print_client_port_select_dialog(name: str, port: int, ports_in_use: List[int]): port = f"{COLOR_CYAN}{port}{RESET_FORMAT}" line1 = f"Please select the port, {name} should be served on." line2 = f"In case you need {name} to be served on a specific" diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 88f168a..b400752 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -104,20 +104,18 @@ def install_client(client: BaseWebClient) -> None: install_client_cfg = get_confirm(question, allow_go_back=False) settings = KiauhSettings() - port = settings.get(client.name, "port") - ports_in_use = read_ports_from_nginx_configs() + port: int = settings.get(client.name, "port") + ports_in_use: List[int] = read_ports_from_nginx_configs() # check if configured port is a valid number and not in use already valid_port = is_valid_port(port, ports_in_use) while not valid_port: next_port = get_next_free_port(ports_in_use) print_client_port_select_dialog(client.display_name, next_port, ports_in_use) - port = str( - get_number_input( - f"Configure {client.display_name} for port", - min_count=int(next_port), - default=next_port, - ) + port = get_number_input( + f"Configure {client.display_name} for port", + min_count=int(next_port), + default=next_port, ) valid_port = is_valid_port(port, ports_in_use) diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 86306bd..367e4f7 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -149,7 +149,7 @@ def create_nginx_cfg(name: str, port: int, root_dir: Path) -> None: raise -def read_ports_from_nginx_configs() -> List[str]: +def read_ports_from_nginx_configs() -> List[int]: """ Helper function to iterate over all NGINX configs and read all ports defined for listen :return: A sorted list of listen ports @@ -168,18 +168,19 @@ def read_ports_from_nginx_configs() -> List[str]: if line.startswith("listen") and line.split()[-1] not in port_list: port_list.append(line.split()[-1]) - return sorted(port_list, key=lambda x: int(x)) + ports_to_ints_list = [int(port) for port in port_list] + return sorted(ports_to_ints_list, key=lambda x: int(x)) -def is_valid_port(port: str, ports_in_use: List[str]) -> bool: - return port.isdigit() and port not in ports_in_use +def is_valid_port(port: int, ports_in_use: List[int]) -> bool: + return port not in ports_in_use -def get_next_free_port(ports_in_use: List[str]) -> str: +def get_next_free_port(ports_in_use: List[int]) -> int: valid_ports = set(range(80, 7125)) used_ports = set(map(int, ports_in_use)) - return str(min(valid_ports - used_ports)) + return min(valid_ports - used_ports) def add_config_section( From 1f2d724189fa7551ede455801f0c2e55e17dda94 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 28 Apr 2024 19:43:36 +0200 Subject: [PATCH 181/296] feat: use dynamically created client download URL Signed-off-by: Dominik Willner --- kiauh/components/webui_client/client_setup.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index b400752..e7cef99 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -160,8 +160,10 @@ def download_client(client: BaseWebClient) -> None: zipfile = f"{client.name.lower()}.zip" target = Path().home().joinpath(zipfile) try: - Logger.print_status(f"Downloading {zipfile} ...") - download_file(client.stable_url, target, True) + Logger.print_status( + f"Downloading {client.display_name} from {client.download_url} ..." + ) + download_file(client.download_url, target, True) Logger.print_ok("Download complete!") Logger.print_status(f"Extracting {zipfile} ...") @@ -170,7 +172,7 @@ def download_client(client: BaseWebClient) -> None: Logger.print_ok("OK!") except Exception: - Logger.print_error(f"Downloading {zipfile} failed!") + Logger.print_error(f"Downloading {client.display_name} failed!") raise From 8d343853f10610f06071f0758bb42ed91d2aa61c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 1 May 2024 13:02:42 +0200 Subject: [PATCH 182/296] feat: fall back to .version file if release_info.json not exist Signed-off-by: Dominik Willner --- kiauh/components/webui_client/client_utils.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 819bcd6..3bb224c 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -127,11 +127,19 @@ def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: def get_local_client_version(client: BaseWebClient) -> str: relinfo_file = client.client_dir.joinpath("release_info.json") - if not relinfo_file.is_file(): - return "-" + version_file = client.client_dir.joinpath(".version") - with open(relinfo_file, "r") as f: - return json.load(f)["version"] + if not client.client_dir.exists(): + return "-" + if not relinfo_file.is_file() and not version_file.is_file(): + return "n/a" + + if relinfo_file.is_file(): + with open(relinfo_file, "r") as f: + return json.load(f)["version"] + else: + with open(version_file, "r") as f: + return f.readlines()[0] def get_remote_client_version(client: BaseWebClient) -> str: From 3da7aedd7fd69c3fd72846f8d9eeac1e283b787d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 1 May 2024 13:08:04 +0200 Subject: [PATCH 183/296] refactor: remove redundant variable assignment Signed-off-by: Dominik Willner --- kiauh/components/webui_client/client_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 3bb224c..df8a660 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -45,8 +45,7 @@ def get_client_config_status( Literal["repo", "local", "remote"], Union[str, int], ]: - client_config = client.client_config - client_config = client_config.config_dir + client_config = client.client_config.config_dir return { "repo": RepoManager.get_repo_name(client_config), From 7d3d46ac07f6bce48b18e1939c88f3a6f249d2e0 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 1 May 2024 14:05:46 +0200 Subject: [PATCH 184/296] refactor: replace RepositoryManager by simple util functions Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 19 +- kiauh/components/klipper/klipper_utils.py | 8 +- kiauh/components/moonraker/moonraker_setup.py | 16 +- kiauh/components/moonraker/moonraker_utils.py | 8 +- .../client_config/client_config_setup.py | 17 +- kiauh/components/webui_client/client_utils.py | 15 +- kiauh/core/menus/settings_menu.py | 5 +- kiauh/core/repo_manager/__init__.py | 0 kiauh/core/repo_manager/repo_manager.py | 171 ------------------ .../mainsail_theme_installer_extension.py | 6 +- kiauh/utils/git_utils.py | 135 +++++++++++++- 11 files changed, 169 insertions(+), 231 deletions(-) delete mode 100644 kiauh/core/repo_manager/__init__.py delete mode 100644 kiauh/core/repo_manager/repo_manager.py diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index c63c607..32f1be6 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -36,7 +36,7 @@ from components.klipper.klipper_utils import ( ) from components.moonraker.moonraker import Moonraker from core.instance_manager.instance_manager import InstanceManager -from core.repo_manager.repo_manager import RepoManager +from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm from utils.logger import Logger from utils.system_utils import ( @@ -108,12 +108,10 @@ def install_klipper() -> None: def setup_klipper_prerequesites() -> None: settings = KiauhSettings() - repo_manager = RepoManager( - repo=settings.get("klipper", "repo_url"), - branch=settings.get("klipper", "branch"), - target_dir=KLIPPER_DIR, - ) - repo_manager.clone_repo() + repo = settings.get("klipper", "repo_url") + branch = settings.get("klipper", "branch") + + git_clone_wrapper(repo, branch, KLIPPER_DIR) # install klipper dependencies and create python virtualenv try: @@ -152,12 +150,7 @@ def update_klipper() -> None: instance_manager = InstanceManager(Klipper) instance_manager.stop_all_instance() - repo_manager = RepoManager( - repo=settings.get("klipper", "repo_url"), - branch=settings.get("klipper", "branch"), - target_dir=KLIPPER_DIR, - ) - repo_manager.pull_repo() + git_pull_wrapper(repo=settings.get("klipper", "repo_url"), target_dir=KLIPPER_DIR) # install possible new system packages install_klipper_packages(KLIPPER_DIR) diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index a4423fb..c6693f7 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -39,10 +39,10 @@ from core.config_manager.config_manager import ConfigManager from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.name_scheme import NameScheme -from core.repo_manager.repo_manager import RepoManager from utils import PRINTER_CFG_BACKUP_DIR from utils.common import get_install_status_common from utils.constants import CURRENT_USER +from utils.git_utils import get_repo_name, get_remote_commit, get_local_commit from utils.input_utils import get_confirm, get_string_input, get_number_input from utils.logger import Logger from utils.system_utils import mask_system_service @@ -59,9 +59,9 @@ def get_klipper_status() -> ( "status": status.get("status"), "status_code": status.get("status_code"), "instances": status.get("instances"), - "repo": RepoManager.get_repo_name(KLIPPER_DIR), - "local": RepoManager.get_local_commit(KLIPPER_DIR), - "remote": RepoManager.get_remote_commit(KLIPPER_DIR), + "repo": get_repo_name(KLIPPER_DIR), + "local": get_local_commit(KLIPPER_DIR), + "remote": get_remote_commit(KLIPPER_DIR), } diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index adcf522..8af405c 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -35,8 +35,8 @@ from components.moonraker.moonraker_utils import ( backup_moonraker_dir, ) from core.instance_manager.instance_manager import InstanceManager -from core.repo_manager.repo_manager import RepoManager from utils.filesystem_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, @@ -135,12 +135,7 @@ def setup_moonraker_prerequesites() -> None: repo = settings.get("moonraker", "repo_url") branch = settings.get("moonraker", "branch") - repo_manager = RepoManager( - repo=repo, - branch=branch, - target_dir=MOONRAKER_DIR, - ) - repo_manager.clone_repo() + git_clone_wrapper(repo, branch, MOONRAKER_DIR) # install moonraker dependencies and create python virtualenv install_moonraker_packages(MOONRAKER_DIR) @@ -196,12 +191,9 @@ def update_moonraker() -> None: instance_manager = InstanceManager(Moonraker) instance_manager.stop_all_instance() - repo_manager = RepoManager( - repo=settings.get("moonraker", "repo_url"), - branch=settings.get("moonraker", "branch"), - target_dir=MOONRAKER_DIR, + git_pull_wrapper( + repo=settings.get("moonraker", "repo_url"), target_dir=MOONRAKER_DIR ) - repo_manager.pull_repo() # install possible new system packages install_moonraker_packages(MOONRAKER_DIR) diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index ab63d49..8014420 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -25,8 +25,8 @@ from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager -from core.repo_manager.repo_manager import RepoManager from utils.common import get_install_status_common +from utils.git_utils import get_repo_name, get_local_commit, get_remote_commit from utils.logger import Logger from utils.system_utils import ( get_ipv4_addr, @@ -44,9 +44,9 @@ def get_moonraker_status() -> ( "status": status.get("status"), "status_code": status.get("status_code"), "instances": status.get("instances"), - "repo": RepoManager.get_repo_name(MOONRAKER_DIR), - "local": RepoManager.get_local_commit(MOONRAKER_DIR), - "remote": RepoManager.get_remote_commit(MOONRAKER_DIR), + "repo": get_repo_name(MOONRAKER_DIR), + "local": get_local_commit(MOONRAKER_DIR), + "remote": get_remote_commit(MOONRAKER_DIR), } diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index f54735c..06f1f05 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -25,13 +25,13 @@ from components.webui_client.client_utils import ( ) from core.instance_manager.instance_manager import InstanceManager -from core.repo_manager.repo_manager import RepoManager from utils.common import backup_printer_config_dir from utils.filesystem_utils import ( create_symlink, add_config_section, add_config_section_at_top, ) +from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm from utils.logger import Logger @@ -86,11 +86,9 @@ def install_client_config(client_data: BaseWebClient) -> None: def download_client_config(client_config: BaseWebClientConfig) -> None: try: Logger.print_status(f"Downloading {client_config.display_name} ...") - rm = RepoManager( - client_config.repo_url, - target_dir=str(client_config.config_dir), - ) - rm.clone_repo() + repo = client_config.repo_url + target_dir = client_config.config_dir + git_clone_wrapper(repo, None, target_dir) except Exception: Logger.print_error(f"Downloading {client_config.display_name} failed!") raise @@ -111,12 +109,7 @@ def update_client_config(client: BaseWebClient) -> None: if settings.get("kiauh", "backup_before_update"): backup_client_config_data(client) - repo_manager = RepoManager( - repo=client_config.repo_url, - branch="master", - target_dir=str(client_config.config_dir), - ) - repo_manager.pull_repo() + git_pull_wrapper(client_config.repo_url, client_config.config_dir) Logger.print_ok(f"Successfully updated {client_config.display_name}.") Logger.print_info("Restart Klipper to reload the configuration!") diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index df8a660..32f2ff6 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -21,12 +21,17 @@ from components.webui_client.base_data import ( ) from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager -from core.repo_manager.repo_manager import RepoManager from core.settings.kiauh_settings import KiauhSettings from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD from utils.common import get_install_status_webui from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW -from utils.git_utils import get_latest_tag, get_latest_unstable_tag +from utils.git_utils import ( + get_latest_tag, + get_latest_unstable_tag, + get_repo_name, + get_local_commit, + get_remote_commit, +) from utils.logger import Logger @@ -48,9 +53,9 @@ def get_client_config_status( client_config = client.client_config.config_dir return { - "repo": RepoManager.get_repo_name(client_config), - "local": RepoManager.get_local_commit(client_config), - "remote": RepoManager.get_remote_commit(client_config), + "repo": get_repo_name(client_config), + "local": get_local_commit(client_config), + "remote": get_remote_commit(client_config), } diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index f049462..5a795f8 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -18,9 +18,9 @@ from components.moonraker.moonraker import Moonraker from core.instance_manager.instance_manager import InstanceManager from core.menus import Option from core.menus.base_menu import BaseMenu -from core.repo_manager.repo_manager import RepoManager from core.settings.kiauh_settings import KiauhSettings from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_GREEN, COLOR_YELLOW +from utils.git_utils import git_clone_wrapper from utils.input_utils import get_string_input, get_confirm from utils.logger import Logger @@ -199,8 +199,7 @@ class SettingsMenu(BaseMenu): repo = self.kiauh_settings.get(name, "repo_url") branch = self.kiauh_settings.get(name, "branch") - repman = RepoManager(repo, str(target_dir), branch) - repman.clone_repo() + git_clone_wrapper(repo, branch, target_dir) im.start_all_instance() diff --git a/kiauh/core/repo_manager/__init__.py b/kiauh/core/repo_manager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/repo_manager/repo_manager.py b/kiauh/core/repo_manager/repo_manager.py deleted file mode 100644 index 956d6ec..0000000 --- a/kiauh/core/repo_manager/repo_manager.py +++ /dev/null @@ -1,171 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import shutil -import subprocess -from pathlib import Path - -from utils.input_utils import get_confirm -from utils.logger import Logger - - -# noinspection PyMethodMayBeStatic -class RepoManager: - def __init__( - self, - repo: str, - target_dir: str, - branch: str = None, - ): - self._repo = repo - self._branch = branch - self._method = self._get_method() - self._target_dir = target_dir - - @property - def repo(self) -> str: - return self._repo - - @repo.setter - def repo(self, value) -> None: - self._repo = value - - @property - def branch(self) -> str: - return self._branch - - @branch.setter - def branch(self, value) -> None: - self._branch = value - - @property - def method(self) -> str: - return self._method - - @method.setter - def method(self, value) -> None: - self._method = value - - @property - def target_dir(self) -> str: - return self._target_dir - - @target_dir.setter - def target_dir(self, value) -> None: - self._target_dir = value - - @staticmethod - def get_repo_name(repo: Path) -> str: - """ - Helper method to extract the organisation and name of a repository | - :param repo: repository to extract the values from - :return: String in form of "/" - """ - if not repo.exists() and not repo.joinpath(".git").exists(): - return "-" - - try: - cmd = ["git", "-C", repo, "config", "--get", "remote.origin.url"] - result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) - return "/".join(result.decode().strip().split("/")[-2:]) - except subprocess.CalledProcessError: - return "-" - - @staticmethod - def get_local_commit(repo: Path) -> str: - if not repo.exists() and not repo.joinpath(".git").exists(): - return "-" - - try: - cmd = f"cd {repo} && git describe HEAD --always --tags | cut -d '-' -f 1,2" - return subprocess.check_output(cmd, shell=True, text=True).strip() - except subprocess.CalledProcessError: - return "-" - - @staticmethod - def get_remote_commit(repo: Path) -> str: - if not repo.exists() and not repo.joinpath(".git").exists(): - return "-" - - try: - # get locally checked out branch - branch_cmd = f"cd {repo} && git branch | grep -E '\*'" - branch = subprocess.check_output(branch_cmd, shell=True, text=True) - branch = branch.split("*")[-1].strip() - cmd = f"cd {repo} && git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2" - return subprocess.check_output(cmd, shell=True, text=True).strip() - except subprocess.CalledProcessError: - return "-" - - def clone_repo(self): - log = f"Cloning repository from '{self.repo}' with method '{self.method}'" - Logger.print_status(log) - try: - if Path(self.target_dir).exists(): - question = f"'{self.target_dir}' already exists. Overwrite?" - if not get_confirm(question, default_choice=False): - Logger.print_info("Skip cloning of repository ...") - return - shutil.rmtree(self.target_dir) - - self._clone() - self._checkout() - except subprocess.CalledProcessError: - log = "An unexpected error occured during cloning of the repository." - Logger.print_error(log) - return - except OSError as e: - Logger.print_error(f"Error removing existing repository: {e.strerror}") - return - - def pull_repo(self) -> None: - Logger.print_status(f"Updating repository '{self.repo}' ...") - try: - self._pull() - except subprocess.CalledProcessError: - log = "An unexpected error occured during updating the repository." - Logger.print_error(log) - return - - def _clone(self): - try: - command = ["git", "clone", self.repo, self.target_dir] - subprocess.run(command, check=True) - - Logger.print_ok("Clone successful!") - except subprocess.CalledProcessError as e: - log = f"Error cloning repository {self.repo}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - def _checkout(self): - if self.branch is None: - return - - try: - command = ["git", "checkout", f"{self.branch}"] - subprocess.run(command, cwd=self.target_dir, check=True) - - Logger.print_ok("Checkout successful!") - except subprocess.CalledProcessError as e: - log = f"Error checking out branch {self.branch}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - def _pull(self) -> None: - try: - command = ["git", "pull"] - subprocess.run(command, cwd=self.target_dir, check=True) - except subprocess.CalledProcessError as e: - log = f"Error on git pull: {e.stderr.decode()}" - Logger.print_error(log) - raise - - def _get_method(self) -> str: - return "ssh" if self.repo.startswith("git") else "https" diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index 96b68ce..7964bf7 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -23,8 +23,8 @@ from extensions.base_extension import BaseExtension from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager from core.menus.base_menu import BaseMenu -from core.repo_manager.repo_manager import RepoManager from utils.constants import COLOR_YELLOW, COLOR_CYAN, RESET_FORMAT +from utils.git_utils import git_clone_wrapper from utils.input_utils import get_selection_input from utils.logger import Logger @@ -136,10 +136,8 @@ class MainsailThemeInstallMenu(BaseMenu): if printer_list is None: return - repo_manager = RepoManager(theme_repo_url, "") for printer in printer_list: - repo_manager.target_dir = printer.cfg_dir.joinpath(".theme") - repo_manager.clone_repo() + git_clone_wrapper(theme_repo_url, None, printer.cfg_dir.joinpath(".theme")) if len(theme_data.get("short_note", "")) > 1: Logger.print_warn("Info from the creator:", prefix=False, start="\n") diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py index 4feeaf3..d0e625d 100644 --- a/kiauh/utils/git_utils.py +++ b/kiauh/utils/git_utils.py @@ -1,9 +1,11 @@ import json +import shutil import urllib.request from http.client import HTTPResponse from json import JSONDecodeError -from subprocess import CalledProcessError, PIPE, run -from typing import List, Type +from pathlib import Path +from subprocess import CalledProcessError, PIPE, run, check_output, DEVNULL +from typing import List, Type, Optional from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager @@ -11,6 +13,70 @@ from utils.input_utils import get_number_input, get_confirm from utils.logger import Logger +def git_clone_wrapper(repo: str, branch: Optional[str], target_dir: Path) -> None: + """ + Clones a repository from the given URL and checks out the specified branch if given. + + :param repo: The URL of the repository to clone. + :param branch: The branch to check out. If None, the default branch will be checked out. + :param target_dir: The directory where the repository will be cloned. + :return: None + """ + log = f"Cloning repository from '{repo}'" + Logger.print_status(log) + try: + if Path(target_dir).exists(): + question = f"'{target_dir}' already exists. Overwrite?" + if not get_confirm(question, default_choice=False): + Logger.print_info("Skip cloning of repository ...") + return + shutil.rmtree(target_dir) + + git_cmd_clone(repo, target_dir) + git_cmd_checkout(branch, target_dir) + except CalledProcessError: + log = "An unexpected error occured during cloning of the repository." + Logger.print_error(log) + return + except OSError as e: + Logger.print_error(f"Error removing existing repository: {e.strerror}") + return + + +def git_pull_wrapper(repo: str, target_dir: Path) -> None: + """ + A function that updates a repository using git pull. + + :param repo: The repository to update. + :param target_dir: The directory of the repository. + :return: None + """ + Logger.print_status(f"Updating repository '{repo}' ...") + try: + git_cmd_pull(target_dir) + except CalledProcessError: + log = "An unexpected error occured during updating the repository." + Logger.print_error(log) + return + + +def get_repo_name(repo: Path) -> str: + """ + Helper method to extract the organisation and name of a repository | + :param repo: repository to extract the values from + :return: String in form of "/" + """ + if not repo.exists() and not repo.joinpath(".git").exists(): + return "-" + + try: + cmd = ["git", "-C", repo, "config", "--get", "remote.origin.url"] + result = check_output(cmd, stderr=DEVNULL) + return "/".join(result.decode().strip().split("/")[-2:]) + except CalledProcessError: + return "-" + + def get_tags(repo_path: str) -> List[str]: try: url = f"https://api.github.com/repos/{repo_path}/tags" @@ -61,7 +127,70 @@ def get_latest_unstable_tag(repo_path: str) -> str: raise -def rollback_repository(repo_dir: str, instance: Type[BaseInstance]) -> None: +def get_local_commit(repo: Path) -> str: + if not repo.exists() and not repo.joinpath(".git").exists(): + return "-" + + try: + cmd = f"cd {repo} && git describe HEAD --always --tags | cut -d '-' -f 1,2" + return check_output(cmd, shell=True, text=True).strip() + except CalledProcessError: + return "-" + + +def get_remote_commit(repo: Path) -> str: + if not repo.exists() and not repo.joinpath(".git").exists(): + return "-" + + try: + # get locally checked out branch + branch_cmd = f"cd {repo} && git branch | grep -E '\*'" + branch = check_output(branch_cmd, shell=True, text=True) + branch = branch.split("*")[-1].strip() + cmd = f"cd {repo} && git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2" + return check_output(cmd, shell=True, text=True).strip() + except CalledProcessError: + return "-" + + +def git_cmd_clone(repo: str, target_dir: Path) -> None: + try: + command = ["git", "clone", repo, target_dir] + run(command, check=True) + + Logger.print_ok("Clone successful!") + except CalledProcessError as e: + log = f"Error cloning repository {repo}: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def git_cmd_checkout(branch: str, target_dir: Path) -> None: + if branch is None: + return + + try: + command = ["git", "checkout", f"{branch}"] + run(command, cwd=target_dir, check=True) + + Logger.print_ok("Checkout successful!") + except CalledProcessError as e: + log = f"Error checking out branch {branch}: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def git_cmd_pull(target_dir: Path) -> None: + try: + command = ["git", "pull"] + run(command, cwd=target_dir, check=True) + except CalledProcessError as e: + log = f"Error on git pull: {e.stderr.decode()}" + Logger.print_error(log) + raise + + +def rollback_repository(repo_dir: Path, instance: Type[BaseInstance]) -> None: q1 = "How many commits do you want to roll back" amount = get_number_input(q1, 1, allow_go_back=True) From 8c3397ea7819feeb0d213ac402c21126c630bad9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 1 May 2024 14:12:20 +0200 Subject: [PATCH 185/296] fix: add missing methods to MainsailThemeInstallMenu Signed-off-by: Dominik Willner --- .../mainsail_theme_installer_extension.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index 7964bf7..99c4049 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -11,7 +11,7 @@ import csv import shutil import textwrap import urllib.request -from typing import List, Union +from typing import List, Union, Optional, Type from typing import TypedDict from components.klipper.klipper import Klipper @@ -19,6 +19,7 @@ from components.klipper.klipper_dialogs import ( print_instance_overview, DisplayType, ) +from core.menus import Option from extensions.base_extension import BaseExtension from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager @@ -42,8 +43,7 @@ class MainsailThemeInstallerExtension(BaseExtension): instances: List[Klipper] = im.instances def install_extension(self, **kwargs) -> None: - install_menu = MainsailThemeInstallMenu(self.instances) - install_menu.run() + MainsailThemeInstallMenu(self.instances).run() def remove_extension(self, **kwargs) -> None: print_instance_overview( @@ -80,11 +80,21 @@ class MainsailThemeInstallMenu(BaseMenu): def __init__(self, instances: List[Klipper]): super().__init__() self.themes: List[ThemeData] = self.load_themes() - options = {f"{index}": self.install_theme for index in range(len(self.themes))} - self.options = options - self.instances = instances + def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: + from extensions.extensions_menu import ExtensionsMenu + + self.previous_menu: Type[BaseMenu] = ( + previous_menu if previous_menu is not None else ExtensionsMenu + ) + + def set_options(self) -> None: + self.options = { + f"{index}": Option(self.install_theme, False, opt_index=f"{index}") + for index in range(len(self.themes)) + } + def print_menu(self) -> None: header = " [ Mainsail Theme Installer ] " color = COLOR_YELLOW From 9d2cb72aa475f03d8ca85d54af69cc168a10d869 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 1 May 2024 18:46:58 +0200 Subject: [PATCH 186/296] feat: implement crowsnest (#462) * feat: add crowsnest install/remove Signed-off-by: Dominik Willner * feat: add crowsnest update Signed-off-by: Dominik Willner --------- Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/__init__.py | 13 ++ kiauh/components/crowsnest/crowsnest.py | 172 ++++++++++++++++++++++++ kiauh/core/menus/install_menu.py | 5 + kiauh/core/menus/main_menu.py | 12 +- kiauh/core/menus/remove_menu.py | 5 + kiauh/core/menus/update_menu.py | 15 ++- kiauh/utils/common.py | 16 +++ 7 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 kiauh/components/crowsnest/__init__.py create mode 100644 kiauh/components/crowsnest/crowsnest.py diff --git a/kiauh/components/crowsnest/__init__.py b/kiauh/components/crowsnest/__init__.py new file mode 100644 index 0000000..c68284b --- /dev/null +++ b/kiauh/components/crowsnest/__init__.py @@ -0,0 +1,13 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path + +CROWSNEST_DIR = Path.home().joinpath("crowsnest") +CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git" diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py new file mode 100644 index 0000000..62c51d4 --- /dev/null +++ b/kiauh/components/crowsnest/crowsnest.py @@ -0,0 +1,172 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 shutil +import textwrap +from pathlib import Path +from subprocess import run, CalledProcessError +from typing import List, Dict, Literal, Union + +from components.crowsnest import CROWSNEST_REPO, CROWSNEST_DIR +from components.klipper.klipper import Klipper +from core.instance_manager.instance_manager import InstanceManager +from utils.common import get_install_status +from utils.constants import COLOR_CYAN, RESET_FORMAT, CURRENT_USER +from utils.git_utils import ( + git_clone_wrapper, + get_repo_name, + get_local_commit, + get_remote_commit, + git_pull_wrapper, +) +from utils.input_utils import get_confirm +from utils.logger import Logger +from utils.system_utils import ( + check_package_install, + install_system_packages, + parse_packages_from_file, + update_system_package_lists, +) + + +def install_crowsnest() -> None: + # Step 1: Clone crowsnest repo + git_clone_wrapper(CROWSNEST_REPO, "master", CROWSNEST_DIR) + + # Step 2: Install dependencies + requirements: List[str] = check_package_install(["make"]) + if requirements: + install_system_packages(requirements) + + # Step 3: Check for Multi Instance + im = InstanceManager(Klipper) + instances: List[Klipper] = im.find_instances() + + if len(instances) > 1: + Logger.print_status("Multi instance install detected ...") + info = textwrap.dedent(""" + Crowsnest is NOT designed to support multi instances. + A workaround for this is to choose the most used instance as a 'master' + Use this instance to set up your 'crowsnest.conf' and steering it's service. + Found the following instances: + """)[:-1] + print(info, end="") + for instance in instances: + print(f"● {instance.data_dir_name}") + + Logger.print_status("\nLaunching crowsnest's configuration tool ...") + + if not get_confirm("Continue with configuration?", False, allow_go_back=True): + Logger.print_info("Installation aborted by user ... Exiting!") + return + + config = Path(CROWSNEST_DIR).joinpath("tools/.config") + try: + run( + "make config", + cwd=CROWSNEST_DIR, + shell=True, + check=True, + ) + except CalledProcessError as e: + Logger.print_error(f"Something went wrong! Please try again...\n{e}") + if config.exists(): + Path.unlink(config) + return + + if not config.exists(): + Logger.print_error("Generating .config failed, installation aborted") + return + + # Step 4: Launch crowsnest installer + print(f"{COLOR_CYAN}Installer will prompt you for sudo password!{RESET_FORMAT}") + Logger.print_status("Launching crowsnest installer ...") + try: + run( + f"sudo make install BASE_USER={CURRENT_USER}", + cwd=CROWSNEST_DIR, + shell=True, + check=True, + ) + except CalledProcessError as e: + Logger.print_error(f"Something went wrong! Please try again...\n{e}") + return + + +def update_crowsnest() -> None: + try: + stop_cn = "sudo systemctl stop crowsnest" + restart_cn = "sudo systemctl restart crowsnest" + + Logger.print_status("Stopping Crowsnest service ...") + run(stop_cn, shell=True, check=True) + + if not CROWSNEST_DIR.exists(): + git_clone_wrapper(CROWSNEST_REPO, "master", CROWSNEST_DIR) + else: + Logger.print_status("Updating Crowsnest ...") + + git_pull_wrapper(CROWSNEST_REPO, CROWSNEST_DIR) + + script = CROWSNEST_DIR.joinpath("tools/install.sh") + deps = parse_packages_from_file(script) + packages = check_package_install(deps) + update_system_package_lists(silent=False) + install_system_packages(packages) + + Logger.print_status("Restarting Crowsnest service ...") + run(restart_cn, shell=True, check=True) + + Logger.print_ok("Crowsnest updated successfully.", end="\n\n") + except CalledProcessError as e: + Logger.print_error(f"Something went wrong! Please try again...\n{e}") + return + + +def get_crowsnest_status() -> ( + Dict[ + Literal["status", "status_code", "repo", "local", "remote"], + Union[str, int], + ] +): + files = [ + Path("/usr/local/bin/crowsnest"), + Path("/etc/logrotate.d/crowsnest"), + Path("/etc/systemd/system/crowsnest.service"), + ] + status = get_install_status(CROWSNEST_DIR, files) + return { + "status": status.get("status"), + "status_code": status.get("status_code"), + "repo": get_repo_name(CROWSNEST_DIR), + "local": get_local_commit(CROWSNEST_DIR), + "remote": get_remote_commit(CROWSNEST_DIR), + } + + +def remove_crowsnest() -> None: + if not CROWSNEST_DIR.exists(): + Logger.print_info("Crowsnest does not seem to be installed! Skipping ...") + return + + try: + run( + "make uninstall", + cwd=CROWSNEST_DIR, + shell=True, + check=True, + ) + except CalledProcessError as e: + Logger.print_error(f"Something went wrong! Please try again...\n{e}") + return + + Logger.print_status("Removing crowsnest directory ...") + shutil.rmtree(CROWSNEST_DIR) + Logger.print_ok("Directory removed!") diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index ce42419..4d69b9e 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -10,6 +10,7 @@ import textwrap from typing import Type, Optional +from components.crowsnest.crowsnest import install_crowsnest from components.klipper import klipper_setup from components.moonraker import moonraker_setup from components.webui_client import client_setup @@ -44,6 +45,7 @@ class InstallMenu(BaseMenu): "4": Option(method=self.install_fluidd, menu=False), "5": Option(method=self.install_mainsail_config, menu=False), "6": Option(method=self.install_fluidd_config, menu=False), + "9": Option(method=self.install_crowsnest, menu=False), } def print_menu(self): @@ -88,3 +90,6 @@ class InstallMenu(BaseMenu): def install_fluidd_config(self, **kwargs): client_config_setup.install_client_config(FluiddData()) + + def install_crowsnest(self, **kwargs): + install_crowsnest() diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 35291bc..5e8464c 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -10,6 +10,7 @@ import textwrap from typing import Type, Optional +from components.crowsnest.crowsnest import get_crowsnest_status from components.klipper.klipper_utils import get_klipper_status from components.log_uploads.menus.log_upload_menu import LogUploadMenu from components.moonraker.moonraker_utils import get_moonraker_status @@ -90,16 +91,23 @@ class MainMenu(BaseMenu): self.ms_status = get_client_status(MainsailData()) self.fl_status = get_client_status(FluiddData()) self.cc_status = get_current_client_config([MainsailData(), FluiddData()]) + self._update_status("cn", get_crowsnest_status) def _update_status(self, status_name: str, status_fn: callable) -> None: status_data = status_fn() status = status_data.get("status") code = status_data.get("status_code") - instances = f" {status_data.get('instances')}" if code == 1 else "" + + instance_count = status_data.get("instances") + + count: str = "" + if instance_count and code == 1: + count = f" {instance_count}" + setattr( self, f"{status_name}_status", - self._format_status_by_code(code, status, instances), + self._format_status_by_code(code, status, count), ) setattr( self, diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index d7ce2c0..b6f11b0 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -10,6 +10,7 @@ import textwrap from typing import Type, Optional +from components.crowsnest.crowsnest import remove_crowsnest from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu from components.moonraker.menus.moonraker_remove_menu import ( MoonrakerRemoveMenu, @@ -42,6 +43,7 @@ class RemoveMenu(BaseMenu): "2": Option(method=self.remove_moonraker, menu=True), "3": Option(method=self.remove_mainsail, menu=True), "4": Option(method=self.remove_fluidd, menu=True), + "6": Option(method=self.remove_crowsnest, menu=True), } def print_menu(self): @@ -78,3 +80,6 @@ class RemoveMenu(BaseMenu): def remove_fluidd(self, **kwargs): ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run() + + def remove_crowsnest(self, **kwargs): + remove_crowsnest() diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 3af9085..4a4b236 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -10,6 +10,7 @@ import textwrap from typing import Type, Optional +from components.crowsnest.crowsnest import get_crowsnest_status, update_crowsnest from components.klipper.klipper_setup import update_klipper from components.klipper.klipper_utils import ( get_klipper_status, @@ -57,6 +58,8 @@ class UpdateMenu(BaseMenu): self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.cn_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.cn_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.mainsail_client = MainsailData() self.fluidd_client = FluiddData() @@ -111,7 +114,7 @@ class UpdateMenu(BaseMenu): | Other: |---------------|---------------| | 7) KlipperScreen | | | | 8) Mobileraker | | | - | 9) Crowsnest | | | + | 9) Crowsnest | {self.cn_local:<22} | {self.cn_remote:<22} | | |-------------------------------| | 10) System | | """ @@ -143,7 +146,8 @@ class UpdateMenu(BaseMenu): def update_mobileraker(self, **kwargs): ... - def update_crowsnest(self, **kwargs): ... + def update_crowsnest(self, **kwargs): + update_crowsnest() def upgrade_system_packages(self, **kwargs): ... @@ -189,6 +193,13 @@ class UpdateMenu(BaseMenu): ) self.fc_remote = f"{COLOR_GREEN}{fc_status.get('remote')}{RESET_FORMAT}" + # crowsnest + cn_status = get_crowsnest_status() + self.cn_local = self.format_local_status( + cn_status.get("local"), cn_status.get("remote") + ) + self.cn_remote = f"{COLOR_GREEN}{cn_status.get('remote')}{RESET_FORMAT}" + def format_local_status(self, local_version, remote_version) -> str: if local_version == remote_version: return f"{COLOR_GREEN}{local_version}{RESET_FORMAT}" diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 89b2ec5..307d6a8 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -55,6 +55,22 @@ def check_install_dependencies(deps: List[str]) -> None: install_system_packages(requirements) +def get_install_status( + repo_dir: Path, opt_files: List[Path] +) -> Dict[Literal["status", "status_code", "instances"], Union[str, int]]: + status = [repo_dir.exists()] + + for f in opt_files: + status.append(f.exists()) + + if all(status): + return {"status": "Installed!", "status_code": 1} + elif not any(status): + return {"status": "Not installed!", "status_code": 2} + else: + return {"status": "Incomplete!", "status_code": 3} + + def get_install_status_common( instance_type: Type[BaseInstance], repo_dir: Path, env_dir: Path ) -> Dict[Literal["status", "status_code", "instances"], Union[str, int]]: From af487382216f5b813d866a088e54952c35a37707 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 1 May 2024 19:04:50 +0200 Subject: [PATCH 187/296] refactor: use util function to handle service controls Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index 62c51d4..ee1c428 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -33,6 +33,7 @@ from utils.system_utils import ( install_system_packages, parse_packages_from_file, update_system_package_lists, + control_systemd_service, ) @@ -102,11 +103,7 @@ def install_crowsnest() -> None: def update_crowsnest() -> None: try: - stop_cn = "sudo systemctl stop crowsnest" - restart_cn = "sudo systemctl restart crowsnest" - - Logger.print_status("Stopping Crowsnest service ...") - run(stop_cn, shell=True, check=True) + control_systemd_service("crowsnest", "stop") if not CROWSNEST_DIR.exists(): git_clone_wrapper(CROWSNEST_REPO, "master", CROWSNEST_DIR) @@ -121,8 +118,7 @@ def update_crowsnest() -> None: update_system_package_lists(silent=False) install_system_packages(packages) - Logger.print_status("Restarting Crowsnest service ...") - run(restart_cn, shell=True, check=True) + control_systemd_service("crowsnest", "restart") Logger.print_ok("Crowsnest updated successfully.", end="\n\n") except CalledProcessError as e: From b70ac0dfd7d23035480434984c7b01244f9d1ae3 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 1 May 2024 21:12:37 +0200 Subject: [PATCH 188/296] refactor: move config related helper methods into own util module Signed-off-by: Dominik Willner --- .../client_config/client_config_remove.py | 3 +- .../client_config/client_config_setup.py | 7 +- .../components/webui_client/client_remove.py | 2 +- kiauh/components/webui_client/client_setup.py | 2 +- kiauh/utils/config_utils.py | 83 ++++++++++++++ kiauh/utils/filesystem_utils.py | 101 +----------------- 6 files changed, 90 insertions(+), 108 deletions(-) create mode 100644 kiauh/utils/config_utils.py diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index 6a5a014..3e1dffd 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -16,7 +16,8 @@ from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker from components.webui_client.base_data import BaseWebClientConfig from core.instance_manager.instance_manager import InstanceManager -from utils.filesystem_utils import remove_file, remove_config_section +from utils.config_utils import remove_config_section +from utils.filesystem_utils import remove_file from utils.logger import Logger diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 06f1f05..8b01c48 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -26,11 +26,8 @@ from components.webui_client.client_utils import ( from core.instance_manager.instance_manager import InstanceManager from utils.common import backup_printer_config_dir -from utils.filesystem_utils import ( - create_symlink, - add_config_section, - add_config_section_at_top, -) +from utils.config_utils import add_config_section, add_config_section_at_top +from utils.filesystem_utils import create_symlink from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm from utils.logger import Logger diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index 4315aa1..745954c 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -23,10 +23,10 @@ from components.webui_client.client_config.client_config_remove import ( from components.webui_client.client_utils import backup_mainsail_config_json from core.instance_manager.instance_manager import InstanceManager +from utils.config_utils import remove_config_section from utils.filesystem_utils import ( remove_nginx_config, remove_nginx_logs, - remove_config_section, ) from utils.logger import Logger diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index e7cef99..0afce0b 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -37,6 +37,7 @@ from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from utils.common import check_install_dependencies +from utils.config_utils import add_config_section from utils.filesystem_utils import ( unzip, copy_upstream_nginx_cfg, @@ -44,7 +45,6 @@ from utils.filesystem_utils import ( create_nginx_cfg, create_symlink, remove_file, - add_config_section, read_ports_from_nginx_configs, is_valid_port, get_next_free_port, diff --git a/kiauh/utils/config_utils.py b/kiauh/utils/config_utils.py new file mode 100644 index 0000000..42a2992 --- /dev/null +++ b/kiauh/utils/config_utils.py @@ -0,0 +1,83 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import tempfile +from pathlib import Path +from typing import List, TypeVar, Tuple, Optional + +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from core.config_manager.config_manager import ConfigManager +from utils.logger import Logger + +B = TypeVar("B", Klipper, Moonraker) +ConfigOption = Tuple[str, str] + + +def add_config_section( + section: str, + instances: List[B], + options: Optional[List[ConfigOption]] = None, +) -> None: + for instance in instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + continue + + cm = ConfigManager(cfg_file) + if cm.config.has_section(section): + Logger.print_info("Section already exist. Skipped ...") + continue + + cm.config.add_section(section) + + if options is not None: + for option in options: + cm.config.set(section, option[0], option[1]) + + cm.write_config() + + +def add_config_section_at_top(section: str, instances: List[B]): + for instance in instances: + tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False) + tmp_cfg_path = Path(tmp_cfg.name) + cmt = ConfigManager(tmp_cfg_path) + cmt.config.add_section(section) + cmt.write_config() + tmp_cfg.close() + + cfg_file = instance.cfg_file + with open(cfg_file, "r") as org: + org_content = org.readlines() + with open(tmp_cfg_path, "a") as tmp: + tmp.writelines(org_content) + + cfg_file.unlink() + tmp_cfg_path.rename(cfg_file) + + +def remove_config_section(section: str, instances: List[B]) -> None: + for instance in instances: + cfg_file = instance.cfg_file + Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...") + + if not Path(cfg_file).exists(): + Logger.print_warn(f"'{cfg_file}' not found!") + continue + + cm = ConfigManager(cfg_file) + if not cm.config.has_section(section): + Logger.print_info("Section does not exist. Skipped ...") + continue + + cm.config.remove_section(section) + cm.write_config() diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index 367e4f7..dbf5b51 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -12,15 +12,12 @@ import re import shutil import subprocess -import tempfile from pathlib import Path from zipfile import ZipFile -from typing import List, TypeVar, Tuple, Optional +from typing import List from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager from utils import ( NGINX_SITES_AVAILABLE, @@ -31,10 +28,6 @@ from utils import ( from utils.logger import Logger -B = TypeVar("B", Klipper, Moonraker) -ConfigOption = Tuple[str, str] - - def check_file_exist(file_path: Path, sudo=False) -> bool: """ Helper function for checking the existence of a file | @@ -183,98 +176,6 @@ def get_next_free_port(ports_in_use: List[int]) -> int: return min(valid_ports - used_ports) -def add_config_section( - section: str, - instances: List[B], - options: Optional[List[ConfigOption]] = None, -) -> None: - for instance in instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - continue - - cm = ConfigManager(cfg_file) - if cm.config.has_section(section): - Logger.print_info("Section already exist. Skipped ...") - continue - - cm.config.add_section(section) - - if options is not None: - for option in options: - cm.config.set(section, option[0], option[1]) - - cm.write_config() - - -def add_config_section_at_top(section: str, instances: List[B]): - for instance in instances: - tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False) - tmp_cfg_path = Path(tmp_cfg.name) - cmt = ConfigManager(tmp_cfg_path) - cmt.config.add_section(section) - cmt.write_config() - tmp_cfg.close() - - cfg_file = instance.cfg_file - with open(cfg_file, "r") as org: - org_content = org.readlines() - with open(tmp_cfg_path, "a") as tmp: - tmp.writelines(org_content) - - cfg_file.unlink() - tmp_cfg_path.rename(cfg_file) - - -def remove_config_section(section: str, instances: List[B]) -> None: - for instance in instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - continue - - cm = ConfigManager(cfg_file) - if not cm.config.has_section(section): - Logger.print_info("Section does not exist. Skipped ...") - continue - - cm.config.remove_section(section) - cm.write_config() - - -def patch_moonraker_conf( - moonraker_instances: List[Moonraker], - name: str, - section_name: str, - template_file: str, -) -> None: - for instance in moonraker_instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Add {name} update section to '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - return - - cm = ConfigManager(cfg_file) - if cm.config.has_section(section_name): - Logger.print_info("Section already exist. Skipped ...") - return - - template = MODULE_PATH.joinpath("assets", template_file) - with open(template, "r") as t: - template_content = "\n" - template_content += t.read() - - with open(cfg_file, "a") as f: - f.write(template_content) - - def remove_nginx_config(name: str) -> None: Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...") try: From be228210bd8837a8afbc84aa4ba03e4e64b66d77 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 1 May 2024 21:33:12 +0200 Subject: [PATCH 189/296] refactor: use utils to handle service actions Signed-off-by: Dominik Willner --- .../components/webui_client/client_remove.py | 2 +- .../core/instance_manager/instance_manager.py | 41 +++---------------- kiauh/utils/filesystem_utils.py | 35 ++++++++-------- kiauh/utils/system_utils.py | 13 +++--- 4 files changed, 29 insertions(+), 62 deletions(-) diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index 745954c..b6350f0 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -49,7 +49,7 @@ def run_client_removal( client_name = client.name remove_client_dir(client) remove_nginx_config(client_name) - remove_nginx_logs(client_name) + remove_nginx_logs(client_name, kl_instances) section = f"update_manager {client_name}" remove_config_section(section, mr_instances) diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 0879484..02df91f 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -15,6 +15,7 @@ from typing import List, Optional, Union, TypeVar from core.instance_manager.base_instance import BaseInstance from utils.constants import SYSTEMD from utils.logger import Logger +from utils.system_utils import control_systemd_service T = TypeVar(name="T", bound=BaseInstance, covariant=True) @@ -108,14 +109,7 @@ class InstanceManager: def enable_instance(self) -> None: Logger.print_status(f"Enabling {self.instance_service_full} ...") try: - command = [ - "sudo", - "systemctl", - "enable", - self.instance_service_full, - ] - if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_service_full} enabled.") + control_systemd_service(self.instance_service_full, "enable") except subprocess.CalledProcessError as e: Logger.print_error(f"Error enabling service {self.instance_service_full}:") Logger.print_error(f"{e}") @@ -123,14 +117,7 @@ class InstanceManager: def disable_instance(self) -> None: Logger.print_status(f"Disabling {self.instance_service_full} ...") try: - command = [ - "sudo", - "systemctl", - "disable", - self.instance_service_full, - ] - if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_service_full} disabled.") + control_systemd_service(self.instance_service_full, "disable") except subprocess.CalledProcessError as e: Logger.print_error(f"Error disabling {self.instance_service_full}:") Logger.print_error(f"{e}") @@ -138,14 +125,7 @@ class InstanceManager: def start_instance(self) -> None: Logger.print_status(f"Starting {self.instance_service_full} ...") try: - command = [ - "sudo", - "systemctl", - "start", - self.instance_service_full, - ] - if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_service_full} started.") + control_systemd_service(self.instance_service_full, "start") except subprocess.CalledProcessError as e: Logger.print_error(f"Error starting {self.instance_service_full}:") Logger.print_error(f"{e}") @@ -153,14 +133,7 @@ class InstanceManager: def restart_instance(self) -> None: Logger.print_status(f"Restarting {self.instance_service_full} ...") try: - command = [ - "sudo", - "systemctl", - "restart", - self.instance_service_full, - ] - if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_service_full} restarted.") + control_systemd_service(self.instance_service_full, "restart") except subprocess.CalledProcessError as e: Logger.print_error(f"Error restarting {self.instance_service_full}:") Logger.print_error(f"{e}") @@ -178,9 +151,7 @@ class InstanceManager: def stop_instance(self) -> None: Logger.print_status(f"Stopping {self.instance_service_full} ...") try: - command = ["sudo", "systemctl", "stop", self.instance_service_full] - if subprocess.run(command, check=True): - Logger.print_ok(f"{self.instance_service_full} stopped.") + control_systemd_service(self.instance_service_full, "stop") except subprocess.CalledProcessError as e: Logger.print_error(f"Error stopping {self.instance_service_full}:") Logger.print_error(f"{e}") diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/filesystem_utils.py index dbf5b51..e4ad4e8 100644 --- a/kiauh/utils/filesystem_utils.py +++ b/kiauh/utils/filesystem_utils.py @@ -11,14 +11,13 @@ import re import shutil -import subprocess from pathlib import Path from zipfile import ZipFile +from subprocess import run, check_output, CalledProcessError, PIPE, DEVNULL from typing import List from components.klipper.klipper import Klipper -from core.instance_manager.instance_manager import InstanceManager from utils import ( NGINX_SITES_AVAILABLE, MODULE_PATH, @@ -38,9 +37,9 @@ def check_file_exist(file_path: Path, sudo=False) -> bool: if sudo: try: command = ["sudo", "find", file_path] - subprocess.check_output(command, stderr=subprocess.DEVNULL) + check_output(command, stderr=DEVNULL) return True - except subprocess.CalledProcessError: + except CalledProcessError: return False else: if file_path.exists(): @@ -54,8 +53,8 @@ def create_symlink(source: Path, target: Path, sudo=False) -> None: cmd = ["ln", "-sf", source, target] if sudo: cmd.insert(0, "sudo") - subprocess.run(cmd, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: + run(cmd, stderr=PIPE, check=True) + except CalledProcessError as e: Logger.print_error(f"Failed to create symlink: {e}") raise @@ -63,8 +62,8 @@ def create_symlink(source: Path, target: Path, sudo=False) -> None: def remove_file(file_path: Path, sudo=False) -> None: try: cmd = f"{'sudo ' if sudo else ''}rm -f {file_path}" - subprocess.run(cmd, stderr=subprocess.PIPE, check=True, shell=True) - except subprocess.CalledProcessError as e: + run(cmd, stderr=PIPE, check=True, shell=True) + except CalledProcessError as e: log = f"Cannot remove file {file_path}: {e.stderr.decode()}" Logger.print_error(log) raise @@ -90,8 +89,8 @@ def copy_upstream_nginx_cfg() -> None: target = NGINX_CONFD.joinpath("upstreams.conf") try: command = ["sudo", "cp", source, target] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: + run(command, stderr=PIPE, check=True) + except CalledProcessError as e: log = f"Unable to create upstreams.conf: {e.stderr.decode()}" Logger.print_error(log) raise @@ -106,8 +105,8 @@ def copy_common_vars_nginx_cfg() -> None: target = NGINX_CONFD.joinpath("common_vars.conf") try: command = ["sudo", "cp", source, target] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: + run(command, stderr=PIPE, check=True) + except CalledProcessError as e: log = f"Unable to create upstreams.conf: {e.stderr.decode()}" Logger.print_error(log) raise @@ -135,8 +134,8 @@ def create_nginx_cfg(name: str, port: int, root_dir: Path) -> None: target = NGINX_SITES_AVAILABLE.joinpath(name) try: command = ["sudo", "mv", tmp, target] - subprocess.run(command, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError as e: + run(command, stderr=PIPE, check=True) + except CalledProcessError as e: log = f"Unable to create '{target}': {e.stderr.decode()}" Logger.print_error(log) raise @@ -182,19 +181,17 @@ def remove_nginx_config(name: str) -> None: remove_file(NGINX_SITES_AVAILABLE.joinpath(name), True) remove_file(NGINX_SITES_ENABLED.joinpath(name), True) - except subprocess.CalledProcessError as e: + except CalledProcessError as e: log = f"Unable to remove NGINX config '{name}':\n{e.stderr.decode()}" Logger.print_error(log) -def remove_nginx_logs(name: str) -> None: +def remove_nginx_logs(name: str, instances: List[Klipper]) -> None: Logger.print_status(f"Removing NGINX logs for {name.capitalize()} ...") try: remove_file(Path(f"/var/log/nginx/{name}-access.log"), True) remove_file(Path(f"/var/log/nginx/{name}-error.log"), True) - im = InstanceManager(Klipper) - instances: List[Klipper] = im.instances if not instances: return @@ -202,5 +199,5 @@ def remove_nginx_logs(name: str) -> None: remove_file(instance.log_dir.joinpath(f"{name}-access.log")) remove_file(instance.log_dir.joinpath(f"{name}-error.log")) - except (OSError, subprocess.CalledProcessError) as e: + except (OSError, CalledProcessError) as e: Logger.print_error(f"Unable to remove NGINX logs:\n{e}") diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index bdd3527..3ebdf0d 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -21,9 +21,9 @@ from typing import List, Literal import select +from utils.filesystem_utils import check_file_exist from utils.input_utils import get_confirm from utils.logger import Logger -from utils.filesystem_utils import check_file_exist def kill(opt_err_msg: str = "") -> None: @@ -240,8 +240,7 @@ def mask_system_service(service_name: str) -> None: :return: None """ try: - command = ["sudo", "systemctl", "mask", service_name] - run(command, stderr=PIPE, check=True) + control_systemd_service(service_name, "mask") except CalledProcessError as e: log = f"Unable to mask system service {service_name}: {e.stderr.decode()}" Logger.print_error(log) @@ -330,7 +329,7 @@ def set_nginx_permissions() -> None: def control_systemd_service( - name: str, action: Literal["start", "stop", "restart", "disable"] + name: str, action: Literal["start", "stop", "restart", "enable", "disable", "mask"] ) -> None: """ Helper method to execute several actions for a specific systemd service. | @@ -339,12 +338,12 @@ def control_systemd_service( :return: None """ try: - Logger.print_status(f"{action.capitalize()} {name}.service ...") - command = ["sudo", "systemctl", action, f"{name}.service"] + Logger.print_status(f"{action.capitalize()} {name} ...") + command = ["sudo", "systemctl", action, name] run(command, stderr=PIPE, check=True) Logger.print_ok("OK!") except CalledProcessError as e: - log = f"Failed to {action} {name}.service: {e.stderr.decode()}" + log = f"Failed to {action} {name}: {e.stderr.decode()}" Logger.print_error(log) raise From e05a42630ee3d88cdcafb00ec6e632ceb81bdbad Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 15:51:52 +0200 Subject: [PATCH 190/296] refactor: use utils to handle service masking Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 7 ++----- kiauh/utils/system_utils.py | 14 -------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index c6693f7..75a3df0 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -45,7 +45,7 @@ from utils.constants import CURRENT_USER from utils.git_utils import get_repo_name, get_remote_commit, get_local_commit from utils.input_utils import get_confirm, get_string_input, get_number_input from utils.logger import Logger -from utils.system_utils import mask_system_service +from utils.system_utils import control_systemd_service def get_klipper_status() -> ( @@ -258,10 +258,7 @@ def handle_disruptive_system_packages() -> None: for service in services if services else []: try: - log = f"{service} service detected! Masking {service} service ..." - Logger.print_status(log) - mask_system_service(service) - Logger.print_ok(f"{service} service masked!") + control_systemd_service(service, "mask") except subprocess.CalledProcessError: warn_msg = textwrap.dedent( f""" diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/system_utils.py index 3ebdf0d..69c3a22 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/system_utils.py @@ -233,20 +233,6 @@ def install_system_packages(packages: List[str]) -> None: raise -def mask_system_service(service_name: str) -> None: - """ - Mask a system service to prevent it from starting | - :param service_name: name of the service to mask - :return: None - """ - try: - control_systemd_service(service_name, "mask") - except CalledProcessError as e: - log = f"Unable to mask system service {service_name}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - # this feels hacky and not quite right, but for now it works # see: https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib def get_ipv4_addr() -> str: From 65617ca971b50d0afd16d56596f9ee389bdc6d78 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 15:53:22 +0200 Subject: [PATCH 191/296] refactor: rename filesystem_utils to fs_utils and system_utils to sys_utils Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 2 +- kiauh/components/klipper/klipper_remove.py | 2 +- kiauh/components/klipper/klipper_setup.py | 2 +- kiauh/components/klipper/klipper_utils.py | 2 +- kiauh/components/klipper_firmware/firmware_utils.py | 2 +- kiauh/components/klipper_firmware/menus/klipper_build_menu.py | 2 +- kiauh/components/moonraker/moonraker_remove.py | 2 +- kiauh/components/moonraker/moonraker_setup.py | 4 ++-- kiauh/components/moonraker/moonraker_utils.py | 2 +- .../webui_client/client_config/client_config_remove.py | 2 +- .../webui_client/client_config/client_config_setup.py | 2 +- kiauh/components/webui_client/client_remove.py | 2 +- kiauh/components/webui_client/client_setup.py | 4 ++-- kiauh/core/instance_manager/instance_manager.py | 2 +- kiauh/core/settings/kiauh_settings.py | 2 +- kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py | 2 +- kiauh/extensions/klipper_backup/klipper_backup_extension.py | 4 ++-- kiauh/utils/common.py | 4 ++-- kiauh/utils/{filesystem_utils.py => fs_utils.py} | 0 kiauh/utils/{system_utils.py => sys_utils.py} | 2 +- 20 files changed, 23 insertions(+), 23 deletions(-) rename kiauh/utils/{filesystem_utils.py => fs_utils.py} (100%) rename kiauh/utils/{system_utils.py => sys_utils.py} (99%) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index ee1c428..68d9fcc 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -28,7 +28,7 @@ from utils.git_utils import ( ) from utils.input_utils import get_confirm from utils.logger import Logger -from utils.system_utils import ( +from utils.sys_utils import ( check_package_install, install_system_packages, parse_packages_from_file, diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index 2c30e30..ad5f30b 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -14,7 +14,7 @@ from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import print_instance_overview from core.instance_manager.instance_manager import InstanceManager -from utils.filesystem_utils import remove_file +from utils.fs_utils import remove_file from utils.input_utils import get_selection_input from utils.logger import Logger diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 32f1be6..bcf0634 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -39,7 +39,7 @@ from core.instance_manager.instance_manager import InstanceManager from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm from utils.logger import Logger -from utils.system_utils import ( +from utils.sys_utils import ( parse_packages_from_file, create_python_venv, install_python_requirements, diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 75a3df0..a16fdb0 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -45,7 +45,7 @@ from utils.constants import CURRENT_USER from utils.git_utils import get_repo_name, get_remote_commit, get_local_commit from utils.input_utils import get_confirm, get_string_input, get_number_input from utils.logger import Logger -from utils.system_utils import control_systemd_service +from utils.sys_utils import control_systemd_service def get_klipper_status() -> ( diff --git a/kiauh/components/klipper_firmware/firmware_utils.py b/kiauh/components/klipper_firmware/firmware_utils.py index c8210e1..b2d33d1 100644 --- a/kiauh/components/klipper_firmware/firmware_utils.py +++ b/kiauh/components/klipper_firmware/firmware_utils.py @@ -19,7 +19,7 @@ from components.klipper_firmware.flash_options import ( ) from core.instance_manager.instance_manager import InstanceManager from utils.logger import Logger -from utils.system_utils import log_process +from utils.sys_utils import log_process def find_firmware_file() -> bool: diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py index 4bba6f4..b747778 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -20,7 +20,7 @@ from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_GREEN, COLOR_RED from utils.logger import Logger -from utils.system_utils import ( +from utils.sys_utils import ( check_package_install, update_system_package_lists, install_system_packages, diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index dde6c0e..8a64ca0 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -15,7 +15,7 @@ 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 utils.filesystem_utils import remove_file +from utils.fs_utils import remove_file from utils.input_utils import get_selection_input from utils.logger import Logger diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 8af405c..d186b5c 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -35,14 +35,14 @@ from components.moonraker.moonraker_utils import ( backup_moonraker_dir, ) from core.instance_manager.instance_manager import InstanceManager -from utils.filesystem_utils import check_file_exist +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.logger import Logger -from utils.system_utils import ( +from utils.sys_utils import ( parse_packages_from_file, create_python_venv, install_python_requirements, diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 8014420..966d58a 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -28,7 +28,7 @@ from core.instance_manager.instance_manager import InstanceManager from utils.common import get_install_status_common from utils.git_utils import get_repo_name, get_local_commit, get_remote_commit from utils.logger import Logger -from utils.system_utils import ( +from utils.sys_utils import ( get_ipv4_addr, ) diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index 3e1dffd..cf8add7 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -17,7 +17,7 @@ from components.moonraker.moonraker import Moonraker from components.webui_client.base_data import BaseWebClientConfig from core.instance_manager.instance_manager import InstanceManager from utils.config_utils import remove_config_section -from utils.filesystem_utils import remove_file +from utils.fs_utils import remove_file from utils.logger import Logger diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 8b01c48..f6a2be1 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -27,7 +27,7 @@ from components.webui_client.client_utils import ( from core.instance_manager.instance_manager import InstanceManager from utils.common import backup_printer_config_dir from utils.config_utils import add_config_section, add_config_section_at_top -from utils.filesystem_utils import create_symlink +from utils.fs_utils import create_symlink from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm from utils.logger import Logger diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index b6350f0..a803ffd 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -24,7 +24,7 @@ from components.webui_client.client_utils import backup_mainsail_config_json from core.instance_manager.instance_manager import InstanceManager from utils.config_utils import remove_config_section -from utils.filesystem_utils import ( +from utils.fs_utils import ( remove_nginx_config, remove_nginx_logs, ) diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 0afce0b..123befd 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -38,7 +38,7 @@ from core.settings.kiauh_settings import KiauhSettings from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from utils.common import check_install_dependencies from utils.config_utils import add_config_section -from utils.filesystem_utils import ( +from utils.fs_utils import ( unzip, copy_upstream_nginx_cfg, copy_common_vars_nginx_cfg, @@ -51,7 +51,7 @@ from utils.filesystem_utils import ( ) from utils.input_utils import get_confirm, get_number_input from utils.logger import Logger -from utils.system_utils import ( +from utils.sys_utils import ( download_file, set_nginx_permissions, get_ipv4_addr, diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 02df91f..48f80e2 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -15,7 +15,7 @@ from typing import List, Optional, Union, TypeVar from core.instance_manager.base_instance import BaseInstance from utils.constants import SYSTEMD from utils.logger import Logger -from utils.system_utils import control_systemd_service +from utils.sys_utils import control_systemd_service T = TypeVar(name="T", bound=BaseInstance, covariant=True) diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 8eddb12..6547855 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -15,7 +15,7 @@ from core.config_manager.config_manager import CustomConfigParser from kiauh import PROJECT_ROOT from utils.constants import RESET_FORMAT, COLOR_RED from utils.logger import Logger -from utils.system_utils import kill +from utils.sys_utils import kill # noinspection PyUnusedLocal diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index a4cc7f7..8e90550 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -23,7 +23,7 @@ from extensions.gcode_shell_cmd import ( EXAMPLE_CFG_SRC, KLIPPER_EXTRAS, ) -from utils.filesystem_utils import check_file_exist +from utils.fs_utils import check_file_exist from utils.input_utils import get_confirm from utils.logger import Logger diff --git a/kiauh/extensions/klipper_backup/klipper_backup_extension.py b/kiauh/extensions/klipper_backup/klipper_backup_extension.py index 2f739cf..6aac38c 100644 --- a/kiauh/extensions/klipper_backup/klipper_backup_extension.py +++ b/kiauh/extensions/klipper_backup/klipper_backup_extension.py @@ -20,7 +20,7 @@ from extensions.klipper_backup import ( MOONRAKER_CONF, ) -from utils.filesystem_utils import check_file_exist +from utils.fs_utils import check_file_exist from utils.input_utils import get_confirm from utils.logger import Logger @@ -167,7 +167,7 @@ class KlipperbackupExtension(BaseExtension): Logger.print_error("Unable to remove the Klipper-Backup cron entry") # Remove Moonraker entry - Logger.print_status(f"Check for Klipper-Backup moonraker entry ...") + Logger.print_status("Check for Klipper-Backup moonraker entry ...") try: if remove_moonraker_entry(): Logger.print_ok("Klipper-Backup entry in moonraker.conf removed") diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 307d6a8..c63dc0b 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -22,9 +22,9 @@ from utils.constants import ( COLOR_GREEN, COLOR_RED, ) -from utils.filesystem_utils import check_file_exist +from utils.fs_utils import check_file_exist from utils.logger import Logger -from utils.system_utils import check_package_install, install_system_packages +from utils.sys_utils import check_package_install, install_system_packages def get_current_date() -> Dict[Literal["date", "time"], str]: diff --git a/kiauh/utils/filesystem_utils.py b/kiauh/utils/fs_utils.py similarity index 100% rename from kiauh/utils/filesystem_utils.py rename to kiauh/utils/fs_utils.py diff --git a/kiauh/utils/system_utils.py b/kiauh/utils/sys_utils.py similarity index 99% rename from kiauh/utils/system_utils.py rename to kiauh/utils/sys_utils.py index 69c3a22..80cc569 100644 --- a/kiauh/utils/system_utils.py +++ b/kiauh/utils/sys_utils.py @@ -21,7 +21,7 @@ from typing import List, Literal import select -from utils.filesystem_utils import check_file_exist +from utils.fs_utils import check_file_exist from utils.input_utils import get_confirm from utils.logger import Logger From 6407664e3ec53790bba13dba25f7d4adca040eb5 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 16:02:27 +0200 Subject: [PATCH 192/296] refactor: extract check for python version into function Signed-off-by: Dominik Willner --- kiauh/components/moonraker/moonraker_setup.py | 13 +++++-------- kiauh/utils/sys_utils.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index d186b5c..67350a9 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -8,7 +8,6 @@ # ======================================================================= # import subprocess -import sys from pathlib import Path from components.webui_client.client_utils import ( @@ -48,6 +47,7 @@ from utils.sys_utils import ( install_python_requirements, update_system_package_lists, install_system_packages, + check_python_version, ) @@ -116,18 +116,15 @@ def install_moonraker() -> None: def check_moonraker_install_requirements() -> bool: - if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7): - Logger.print_error("Versioncheck failed!") - Logger.print_error("Python 3.7 or newer required to run Moonraker.") - return False + def check_klipper_instances() -> bool: + if len(InstanceManager(Klipper).instances) >= 1: + return True - kl_instance_count = len(InstanceManager(Klipper).instances) - if kl_instance_count < 1: Logger.print_warn("Klipper not installed!") Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") return False - return True + return check_python_version(3, 7) and check_klipper_instances() def setup_moonraker_prerequesites() -> None: diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 80cc569..24a120e 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -39,6 +39,20 @@ def kill(opt_err_msg: str = "") -> None: sys.exit(1) +def check_python_version(major: int, minor: int) -> bool: + """ + Checks the python version and returns True if it's at least the given version + :param major: the major version to check + :param minor: the minor version to check + :return: bool + """ + if not (sys.version_info.major >= major and sys.version_info.minor >= minor): + Logger.print_error("Versioncheck failed!") + Logger.print_error(f"Python {major}.{minor} or newer required.") + return False + return True + + def parse_packages_from_file(source_file: Path) -> List[str]: """ Read the package names from bash scripts, when defined like: From 4a5d1a971a57ed1b45801114dbfaa5bba22e8f9e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 19:11:34 +0200 Subject: [PATCH 193/296] refactor: rearrange input parameters for git_clone_wrapper Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 4 ++-- kiauh/components/klipper/klipper_setup.py | 2 +- kiauh/components/moonraker/moonraker_setup.py | 2 +- .../webui_client/client_config/client_config_setup.py | 2 +- kiauh/core/menus/settings_menu.py | 2 +- .../mainsail_theme_installer_extension.py | 2 +- kiauh/utils/git_utils.py | 4 +++- 7 files changed, 10 insertions(+), 8 deletions(-) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index 68d9fcc..e2f022b 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -39,7 +39,7 @@ from utils.sys_utils import ( def install_crowsnest() -> None: # Step 1: Clone crowsnest repo - git_clone_wrapper(CROWSNEST_REPO, "master", CROWSNEST_DIR) + git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master") # Step 2: Install dependencies requirements: List[str] = check_package_install(["make"]) @@ -106,7 +106,7 @@ def update_crowsnest() -> None: control_systemd_service("crowsnest", "stop") if not CROWSNEST_DIR.exists(): - git_clone_wrapper(CROWSNEST_REPO, "master", CROWSNEST_DIR) + git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master") else: Logger.print_status("Updating Crowsnest ...") diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index bcf0634..d873762 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -111,7 +111,7 @@ def setup_klipper_prerequesites() -> None: repo = settings.get("klipper", "repo_url") branch = settings.get("klipper", "branch") - git_clone_wrapper(repo, branch, KLIPPER_DIR) + git_clone_wrapper(repo, KLIPPER_DIR, branch) # install klipper dependencies and create python virtualenv try: diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 67350a9..2fd8452 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -132,7 +132,7 @@ def setup_moonraker_prerequesites() -> None: repo = settings.get("moonraker", "repo_url") branch = settings.get("moonraker", "branch") - git_clone_wrapper(repo, branch, MOONRAKER_DIR) + git_clone_wrapper(repo, MOONRAKER_DIR, branch) # install moonraker dependencies and create python virtualenv install_moonraker_packages(MOONRAKER_DIR) diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index f6a2be1..bd37184 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -85,7 +85,7 @@ def download_client_config(client_config: BaseWebClientConfig) -> None: Logger.print_status(f"Downloading {client_config.display_name} ...") repo = client_config.repo_url target_dir = client_config.config_dir - git_clone_wrapper(repo, None, target_dir) + git_clone_wrapper(repo, target_dir) except Exception: Logger.print_error(f"Downloading {client_config.display_name} failed!") raise diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 5a795f8..786f3ed 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -199,7 +199,7 @@ class SettingsMenu(BaseMenu): repo = self.kiauh_settings.get(name, "repo_url") branch = self.kiauh_settings.get(name, "branch") - git_clone_wrapper(repo, branch, target_dir) + git_clone_wrapper(repo, target_dir, branch) im.start_all_instance() diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index 99c4049..fec24d7 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -147,7 +147,7 @@ class MainsailThemeInstallMenu(BaseMenu): return for printer in printer_list: - git_clone_wrapper(theme_repo_url, None, printer.cfg_dir.joinpath(".theme")) + git_clone_wrapper(theme_repo_url, printer.cfg_dir.joinpath(".theme")) if len(theme_data.get("short_note", "")) > 1: Logger.print_warn("Info from the creator:", prefix=False, start="\n") diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py index d0e625d..3ccb0d1 100644 --- a/kiauh/utils/git_utils.py +++ b/kiauh/utils/git_utils.py @@ -13,7 +13,9 @@ from utils.input_utils import get_number_input, get_confirm from utils.logger import Logger -def git_clone_wrapper(repo: str, branch: Optional[str], target_dir: Path) -> None: +def git_clone_wrapper( + repo: str, target_dir: Path, branch: Optional[str] = None +) -> None: """ Clones a repository from the given URL and checks out the specified branch if given. From 067a102b6b369fd52b8ced568a817d2bce5a25f9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 21:51:35 +0200 Subject: [PATCH 194/296] feat: add deprecated decorator Signed-off-by: Dominik Willner --- kiauh/utils/decorators.py | 22 ++++++++++++++++++++++ kiauh/utils/fs_utils.py | 2 ++ 2 files changed, 24 insertions(+) create mode 100644 kiauh/utils/decorators.py diff --git a/kiauh/utils/decorators.py b/kiauh/utils/decorators.py new file mode 100644 index 0000000..a11cb72 --- /dev/null +++ b/kiauh/utils/decorators.py @@ -0,0 +1,22 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import warnings +from typing import Callable + + +def deprecated(info: str = "", replaced_by: Callable = None) -> Callable: + def decorator(func): + def wrapper(*args, **kwargs): + msg = f"{info}{replaced_by.__name__ if replaced_by else ''}" + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index e4ad4e8..4ac3df9 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -24,6 +24,7 @@ from utils import ( NGINX_CONFD, NGINX_SITES_ENABLED, ) +from utils.decorators import deprecated from utils.logger import Logger @@ -59,6 +60,7 @@ def create_symlink(source: Path, target: Path, sudo=False) -> None: raise +@deprecated(info="Use remove_with_sudo instead", replaced_by=remove_with_sudo) def remove_file(file_path: Path, sudo=False) -> None: try: cmd = f"{'sudo ' if sudo else ''}rm -f {file_path}" From 9804411d74c52ae8246098dd922e6100479ccae7 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 21:52:29 +0200 Subject: [PATCH 195/296] feat: add remove_with_sudo function Signed-off-by: Dominik Willner --- kiauh/utils/fs_utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 4ac3df9..57efbaf 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -60,6 +60,15 @@ def create_symlink(source: Path, target: Path, sudo=False) -> None: raise +def remove_with_sudo(file_path: Path) -> None: + try: + cmd = ["sudo", "rm", "-f", file_path] + run(cmd, stderr=PIPE, check=True) + except CalledProcessError as e: + Logger.print_error(f"Failed to remove file: {e}") + raise + + @deprecated(info="Use remove_with_sudo instead", replaced_by=remove_with_sudo) def remove_file(file_path: Path, sudo=False) -> None: try: From 42667ad7928122b60e6af0cbc38ac1bb102dce35 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 22:53:18 +0200 Subject: [PATCH 196/296] refactor(backups): print info message when file or directory does not exist Signed-off-by: Dominik Willner --- kiauh/core/backup_manager/backup_manager.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index e87710d..c440f80 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -39,13 +39,15 @@ class BackupManager: def ignore_folders(self, value: List[str]): self._ignore_folders = value - def backup_file(self, file: Path = None, target: Path = None, custom_filename=None): - if not file: - raise ValueError("Parameter 'file' cannot be None!") + def backup_file(self, file: Path, target: Path = None, custom_filename=None): + Logger.print_status(f"Creating backup of {file} ...") + + if not file.exists(): + Logger.print_info("File does not exist! Skipping ...") + return target = self.backup_root_dir if target is None else target - Logger.print_status(f"Creating backup of {file} ...") if Path(file).is_file(): date = get_current_date().get("date") time = get_current_date().get("time") @@ -61,13 +63,14 @@ class BackupManager: Logger.print_info(f"File '{file}' not found ...") def backup_directory(self, name: str, source: Path, target: Path = None) -> None: + Logger.print_status(f"Creating backup of {name} in {target} ...") + if source is None or not Path(source).exists(): - raise OSError("Parameter 'source' is None or Path does not exist!") + Logger.print_info("Source directory does not exist! Skipping ...") + return target = self.backup_root_dir if target is None else target try: - log = f"Creating backup of {name} in {target} ..." - Logger.print_status(log) date = get_current_date().get("date") time = get_current_date().get("time") shutil.copytree( From 074344cf7cd6a75091b80e2aa1be692fbddcbc60 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 22:53:57 +0200 Subject: [PATCH 197/296] refactor: unneccessary use of check_file_exist Signed-off-by: Dominik Willner --- kiauh/utils/common.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index c63dc0b..8b26f61 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -22,7 +22,6 @@ from utils.constants import ( COLOR_GREEN, COLOR_RED, ) -from utils.fs_utils import check_file_exist from utils.logger import Logger from utils.sys_utils import check_package_install, install_system_packages @@ -119,12 +118,8 @@ def get_install_status_webui( :param common_cfg: the required common_vars.conf :return: formatted string, containing the status """ - dir_exist = install_dir.exists() - nginx_cfg_exist = check_file_exist(nginx_cfg) - upstreams_cfg_exist = check_file_exist(upstreams_cfg) - common_cfg_exist = check_file_exist(common_cfg) - status = [dir_exist, nginx_cfg_exist] - general_nginx_status = [upstreams_cfg_exist, common_cfg_exist] + status = [install_dir.exists(), nginx_cfg.exists()] + general_nginx_status = [upstreams_cfg.exists(), common_cfg.exists()] if all(status) and all(general_nginx_status): return f"{COLOR_GREEN}Installed!{RESET_FORMAT}" From c17c3e9bd421c06c54f55c70cb36622d49265385 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 22:54:49 +0200 Subject: [PATCH 198/296] feat: add KlipperScreen Signed-off-by: Dominik Willner --- kiauh/components/klipperscreen/__init__.py | 16 ++ .../components/klipperscreen/klipperscreen.py | 217 ++++++++++++++++++ kiauh/core/menus/backup_menu.py | 3 +- kiauh/core/menus/install_menu.py | 5 + kiauh/core/menus/main_menu.py | 2 + kiauh/core/menus/remove_menu.py | 5 + kiauh/core/menus/update_menu.py | 18 +- 7 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 kiauh/components/klipperscreen/__init__.py create mode 100644 kiauh/components/klipperscreen/klipperscreen.py diff --git a/kiauh/components/klipperscreen/__init__.py b/kiauh/components/klipperscreen/__init__.py new file mode 100644 index 0000000..c79b463 --- /dev/null +++ b/kiauh/components/klipperscreen/__init__.py @@ -0,0 +1,16 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path + +from core.backup_manager import BACKUP_ROOT_DIR + +KLIPPERSCREEN_REPO = "https://github.com/KlipperScreen/KlipperScreen.git" +KLIPPERSCREEN_DIR = Path.home().joinpath("KlipperScreen") +KLIPPERSCREEN_ENV = Path.home().joinpath(".KlipperScreen-env") +KLIPPERSCREEN_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipperscreen-backups") diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py new file mode 100644 index 0000000..5e3b44b --- /dev/null +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -0,0 +1,217 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import shutil +from pathlib import Path +from subprocess import run, CalledProcessError +from typing import List, Dict, Literal, Union + +from components.klipper.klipper import Klipper +from components.klipperscreen import ( + KLIPPERSCREEN_DIR, + KLIPPERSCREEN_REPO, + KLIPPERSCREEN_ENV, + KLIPPERSCREEN_BACKUP_DIR, +) +from components.moonraker.moonraker import Moonraker +from core.backup_manager.backup_manager import BackupManager +from core.instance_manager.instance_manager import InstanceManager +from core.settings.kiauh_settings import KiauhSettings +from utils.common import get_install_status +from utils.config_utils import add_config_section, remove_config_section +from utils.constants import SYSTEMD +from utils.fs_utils import remove_with_sudo +from utils.git_utils import ( + git_clone_wrapper, + git_pull_wrapper, + get_repo_name, + get_local_commit, + get_remote_commit, +) +from utils.input_utils import get_confirm +from utils.logger import Logger +from utils.sys_utils import ( + check_python_version, + check_package_install, + install_system_packages, + control_systemd_service, + install_python_requirements, +) + + +def install_klipperscreen() -> None: + Logger.print_status("Installing KlipperScreen ...") + + if not check_python_version(3, 7): + return + + mr_im = InstanceManager(Moonraker) + mr_instances = mr_im.instances + if not mr_instances: + # TODO: add moonraker not found dialog + print("Moonraker not found!") + if not get_confirm( + "Continue KlipperScreen installation?", + allow_go_back=True, + ): + return + + package_list = ["wget", "curl", "unzip", "dfu-util"] + packages = check_package_install(package_list) + if packages: + install_system_packages(packages) + + git_clone_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR) + + try: + script = f"{KLIPPERSCREEN_DIR}/scripts/KlipperScreen-install.sh" + run(script, shell=True, check=True) + if mr_instances: + patch_klipperscreen_update_manager(mr_instances) + mr_im.restart_all_instance() + else: + Logger.print_info( + "Moonraker is not installed! Cannot add KlipperScreen to update manager!" + ) + Logger.print_ok("KlipperScreen successfully installed!") + except CalledProcessError as e: + Logger.print_error(f"Error installing KlipperScreen:\n{e}") + return + + +def patch_klipperscreen_update_manager(instances: List[Moonraker]) -> None: + env_py = f"{KLIPPERSCREEN_ENV}/bin/python" + add_config_section( + section="update_manager KlipperScreen", + instances=instances, + options=[ + ("type", "git_repo"), + ("path", str(KLIPPERSCREEN_DIR)), + ("orgin", KLIPPERSCREEN_REPO), + ("env", env_py), + ("requirements", "scripts/KlipperScreen-requirements.txt"), + ("install_script", "scripts/KlipperScreen-install.sh"), + ], + ) + + +def update_klipperscreen() -> None: + try: + control_systemd_service("KlipperScreen", "stop") + + if not KLIPPERSCREEN_DIR.exists(): + Logger.print_info( + "KlipperScreen does not seem to be installed! Skipping ..." + ) + return + + Logger.print_status("Updating KlipperScreen ...") + + control_systemd_service("KlipperScreen", "stop") + + settings = KiauhSettings() + if settings.get("kiauh", "backup_before_update"): + backup_klipperscreen_dir() + + git_pull_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR) + + requirements = KLIPPERSCREEN_DIR.joinpath( + "/scripts/KlipperScreen-requirements.txt" + ) + install_python_requirements(KLIPPERSCREEN_ENV, requirements) + + control_systemd_service("KlipperScreen", "start") + + Logger.print_ok("KlipperScreen updated successfully.", end="\n\n") + except CalledProcessError as e: + Logger.print_error(f"Something went wrong! Please try again...\n{e}") + return + + +def get_klipperscreen_status() -> ( + Dict[ + Literal["status", "status_code", "repo", "local", "remote"], + Union[str, int], + ] +): + files = [ + KLIPPERSCREEN_DIR, + KLIPPERSCREEN_ENV, + SYSTEMD.joinpath("KlipperScreen.service"), + ] + status = get_install_status(KLIPPERSCREEN_DIR, files) + return { + "status": status.get("status"), + "status_code": status.get("status_code"), + "repo": get_repo_name(KLIPPERSCREEN_DIR), + "local": get_local_commit(KLIPPERSCREEN_DIR), + "remote": get_remote_commit(KLIPPERSCREEN_DIR), + } + + +def remove_klipperscreen() -> None: + Logger.print_status("Removing KlipperScreen ...") + try: + if KLIPPERSCREEN_DIR.exists(): + Logger.print_status("Removing KlipperScreen directory ...") + shutil.rmtree(KLIPPERSCREEN_DIR) + Logger.print_ok("KlipperScreen directory successfully removed!") + else: + Logger.print_warn("KlipperScreen directory not found!") + + if KLIPPERSCREEN_ENV.exists(): + Logger.print_status("Removing KlipperScreen environment ...") + shutil.rmtree(KLIPPERSCREEN_ENV) + Logger.print_ok("KlipperScreen environment successfully removed!") + else: + Logger.print_warn("KlipperScreen environment not found!") + + service = SYSTEMD.joinpath("KlipperScreen.service") + if service.exists(): + Logger.print_status("Removing KlipperScreen service ...") + control_systemd_service(service, "stop") + control_systemd_service(service, "disable") + remove_with_sudo(service) + Logger.print_ok("KlipperScreen service successfully removed!") + + logfile = Path("/tmp/KlipperScreen.log") + if logfile.exists(): + Logger.print_status("Removing KlipperScreen log file ...") + remove_with_sudo(logfile) + Logger.print_ok("KlipperScreen log file successfully removed!") + + kl_im = InstanceManager(Klipper) + kl_instances: List[Klipper] = kl_im.instances + for instance in kl_instances: + logfile = instance.log_dir.joinpath("KlipperScreen.log") + if logfile.exists(): + Logger.print_status(f"Removing {logfile} ...") + Path(logfile).unlink() + Logger.print_ok(f"{logfile} successfully removed!") + + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + if mr_instances: + Logger.print_status("Removing KlipperScreen from update manager ...") + remove_config_section("update_manager KlipperScreen", mr_instances) + Logger.print_ok("KlipperScreen successfully removed from update manager!") + + Logger.print_ok("KlipperScreen successfully removed!") + + except Exception as e: + Logger.print_error(f"Error removing KlipperScreen:\n{e}") + + +def backup_klipperscreen_dir() -> None: + bm = BackupManager() + bm.backup_directory( + "KlipperScreen", source=KLIPPERSCREEN_DIR, target=KLIPPERSCREEN_BACKUP_DIR + ) + bm.backup_directory( + "KlipperScreen-env", source=KLIPPERSCREEN_ENV, target=KLIPPERSCREEN_BACKUP_DIR + ) diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 4bfb2c5..a7b28d6 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -11,6 +11,7 @@ import textwrap from typing import Type, Optional from components.klipper.klipper_utils import backup_klipper_dir +from components.klipperscreen.klipperscreen import backup_klipperscreen_dir from components.moonraker.moonraker_utils import ( backup_moonraker_dir, backup_moonraker_db_dir, @@ -104,4 +105,4 @@ class BackupMenu(BaseMenu): backup_client_config_data(FluiddData()) def backup_klipperscreen(self, **kwargs): - pass + backup_klipperscreen_dir() diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 4d69b9e..3906f79 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -12,6 +12,7 @@ from typing import Type, Optional from components.crowsnest.crowsnest import install_crowsnest from components.klipper import klipper_setup +from components.klipperscreen.klipperscreen import install_klipperscreen from components.moonraker import moonraker_setup from components.webui_client import client_setup from components.webui_client.client_config import client_config_setup @@ -45,6 +46,7 @@ class InstallMenu(BaseMenu): "4": Option(method=self.install_fluidd, menu=False), "5": Option(method=self.install_mainsail_config, menu=False), "6": Option(method=self.install_fluidd_config, menu=False), + "7": Option(method=self.install_klipperscreen, menu=False), "9": Option(method=self.install_crowsnest, menu=False), } @@ -91,5 +93,8 @@ class InstallMenu(BaseMenu): def install_fluidd_config(self, **kwargs): client_config_setup.install_client_config(FluiddData()) + def install_klipperscreen(self, **kwargs): + install_klipperscreen() + def install_crowsnest(self, **kwargs): install_crowsnest() diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 5e8464c..1c6541a 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -12,6 +12,7 @@ from typing import Type, Optional from components.crowsnest.crowsnest import get_crowsnest_status from components.klipper.klipper_utils import get_klipper_status +from components.klipperscreen.klipperscreen import get_klipperscreen_status from components.log_uploads.menus.log_upload_menu import LogUploadMenu from components.moonraker.moonraker_utils import get_moonraker_status from components.webui_client.client_utils import ( @@ -91,6 +92,7 @@ class MainMenu(BaseMenu): self.ms_status = get_client_status(MainsailData()) self.fl_status = get_client_status(FluiddData()) self.cc_status = get_current_client_config([MainsailData(), FluiddData()]) + self._update_status("ks", get_klipperscreen_status) self._update_status("cn", get_crowsnest_status) def _update_status(self, status_name: str, status_fn: callable) -> None: diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index b6f11b0..7339823 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -12,6 +12,7 @@ from typing import Type, Optional from components.crowsnest.crowsnest import remove_crowsnest from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu +from components.klipperscreen.klipperscreen import remove_klipperscreen from components.moonraker.menus.moonraker_remove_menu import ( MoonrakerRemoveMenu, ) @@ -43,6 +44,7 @@ class RemoveMenu(BaseMenu): "2": Option(method=self.remove_moonraker, menu=True), "3": Option(method=self.remove_mainsail, menu=True), "4": Option(method=self.remove_fluidd, menu=True), + "5": Option(method=self.remove_klipperscreen, menu=True), "6": Option(method=self.remove_crowsnest, menu=True), } @@ -81,5 +83,8 @@ class RemoveMenu(BaseMenu): def remove_fluidd(self, **kwargs): ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run() + def remove_klipperscreen(self, **kwargs): + remove_klipperscreen() + def remove_crowsnest(self, **kwargs): remove_crowsnest() diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 4a4b236..9d0661c 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -15,6 +15,10 @@ from components.klipper.klipper_setup import update_klipper from components.klipper.klipper_utils import ( get_klipper_status, ) +from components.klipperscreen.klipperscreen import ( + update_klipperscreen, + get_klipperscreen_status, +) from components.moonraker.moonraker_setup import update_moonraker from components.moonraker.moonraker_utils import get_moonraker_status from components.webui_client.client_config.client_config_setup import ( @@ -58,6 +62,8 @@ class UpdateMenu(BaseMenu): self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.ks_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.ks_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.cn_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.cn_remote = f"{COLOR_WHITE}{RESET_FORMAT}" @@ -112,7 +118,7 @@ class UpdateMenu(BaseMenu): | 6) Fluidd-Config | {self.fc_local:<22} | {self.fc_remote:<22} | | | | | | Other: |---------------|---------------| - | 7) KlipperScreen | | | + | 7) KlipperScreen | {self.ks_local:<22} | {self.ks_remote:<22} | | 8) Mobileraker | | | | 9) Crowsnest | {self.cn_local:<22} | {self.cn_remote:<22} | | |-------------------------------| @@ -142,7 +148,8 @@ class UpdateMenu(BaseMenu): def update_fluidd_config(self, **kwargs): update_client_config(self.fluidd_client) - def update_klipperscreen(self, **kwargs): ... + def update_klipperscreen(self, **kwargs): + update_klipperscreen() def update_mobileraker(self, **kwargs): ... @@ -193,6 +200,13 @@ class UpdateMenu(BaseMenu): ) self.fc_remote = f"{COLOR_GREEN}{fc_status.get('remote')}{RESET_FORMAT}" + # klipperscreen + ks_status = get_klipperscreen_status() + self.ks_local = self.format_local_status( + ks_status.get("local"), ks_status.get("remote") + ) + self.ks_remote = f"{COLOR_GREEN}{ks_status.get('remote')}{RESET_FORMAT}" + # crowsnest cn_status = get_crowsnest_status() self.cn_local = self.format_local_status( From d84adee7f93a80317b9dc5e64b60d01ccb9a883a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 23:26:00 +0200 Subject: [PATCH 199/296] fix: typo Signed-off-by: Dominik Willner --- kiauh/components/klipper_firmware/menus/klipper_build_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py index b747778..b4e643e 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -90,7 +90,7 @@ class KlipperBuildFirmwareMenu(BaseMenu): install_system_packages(self.missing_deps) except Exception as e: Logger.print_error(e) - Logger.print_error("Installding dependencies failed!") + Logger.print_error("Installing dependencies failed!") finally: # restart this menu KlipperBuildFirmwareMenu().run() From 9864dd0c7f7c900740c0ce22457a261f50807b51 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 2 May 2024 23:26:47 +0200 Subject: [PATCH 200/296] refactor: use check_install_dependencies at more places where appropriate Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 13 +++---------- kiauh/components/klipper/klipper_setup.py | 6 ++---- kiauh/components/klipperscreen/klipperscreen.py | 8 ++------ kiauh/components/moonraker/moonraker_setup.py | 6 ++---- kiauh/utils/common.py | 7 ++++++- 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index e2f022b..a57f3f9 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -17,7 +17,7 @@ from typing import List, Dict, Literal, Union from components.crowsnest import CROWSNEST_REPO, CROWSNEST_DIR from components.klipper.klipper import Klipper from core.instance_manager.instance_manager import InstanceManager -from utils.common import get_install_status +from utils.common import get_install_status, check_install_dependencies from utils.constants import COLOR_CYAN, RESET_FORMAT, CURRENT_USER from utils.git_utils import ( git_clone_wrapper, @@ -29,10 +29,7 @@ from utils.git_utils import ( from utils.input_utils import get_confirm from utils.logger import Logger from utils.sys_utils import ( - check_package_install, - install_system_packages, parse_packages_from_file, - update_system_package_lists, control_systemd_service, ) @@ -42,9 +39,7 @@ def install_crowsnest() -> None: git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master") # Step 2: Install dependencies - requirements: List[str] = check_package_install(["make"]) - if requirements: - install_system_packages(requirements) + check_install_dependencies(["make"]) # Step 3: Check for Multi Instance im = InstanceManager(Klipper) @@ -114,9 +109,7 @@ def update_crowsnest() -> None: script = CROWSNEST_DIR.joinpath("tools/install.sh") deps = parse_packages_from_file(script) - packages = check_package_install(deps) - update_system_package_lists(silent=False) - install_system_packages(packages) + check_install_dependencies(deps) control_systemd_service("crowsnest", "restart") diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index d873762..eb280e4 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -36,6 +36,7 @@ from components.klipper.klipper_utils import ( ) from components.moonraker.moonraker import Moonraker from core.instance_manager.instance_manager import InstanceManager +from utils.common import check_install_dependencies from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm from utils.logger import Logger @@ -43,8 +44,6 @@ from utils.sys_utils import ( parse_packages_from_file, create_python_venv, install_python_requirements, - update_system_package_lists, - install_system_packages, ) @@ -134,8 +133,7 @@ def install_klipper_packages(klipper_dir: Path) -> None: if Path("/boot/dietpi/.version").exists(): packages.append("dbus") - update_system_package_lists(silent=False) - install_system_packages(packages) + check_install_dependencies(packages) def update_klipper() -> None: diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index 5e3b44b..bc5ab1f 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -22,7 +22,7 @@ from components.moonraker.moonraker import Moonraker from core.backup_manager.backup_manager import BackupManager from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings -from utils.common import get_install_status +from utils.common import get_install_status, check_install_dependencies from utils.config_utils import add_config_section, remove_config_section from utils.constants import SYSTEMD from utils.fs_utils import remove_with_sudo @@ -37,8 +37,6 @@ from utils.input_utils import get_confirm from utils.logger import Logger from utils.sys_utils import ( check_python_version, - check_package_install, - install_system_packages, control_systemd_service, install_python_requirements, ) @@ -62,9 +60,7 @@ def install_klipperscreen() -> None: return package_list = ["wget", "curl", "unzip", "dfu-util"] - packages = check_package_install(package_list) - if packages: - install_system_packages(packages) + check_install_dependencies(package_list) git_clone_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 2fd8452..1bc0092 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -34,6 +34,7 @@ from components.moonraker.moonraker_utils import ( backup_moonraker_dir, ) from core.instance_manager.instance_manager import InstanceManager +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 ( @@ -45,8 +46,6 @@ from utils.sys_utils import ( parse_packages_from_file, create_python_venv, install_python_requirements, - update_system_package_lists, - install_system_packages, check_python_version, ) @@ -143,8 +142,7 @@ def setup_moonraker_prerequesites() -> None: def install_moonraker_packages(moonraker_dir: Path) -> None: script = moonraker_dir.joinpath("scripts/install-moonraker.sh") packages = parse_packages_from_file(script) - update_system_package_lists(silent=False) - install_system_packages(packages) + check_install_dependencies(packages) def install_moonraker_polkit() -> None: diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 8b26f61..aae434e 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -23,7 +23,11 @@ from utils.constants import ( COLOR_RED, ) from utils.logger import Logger -from utils.sys_utils import check_package_install, install_system_packages +from utils.sys_utils import ( + check_package_install, + install_system_packages, + update_system_package_lists, +) def get_current_date() -> Dict[Literal["date", "time"], str]: @@ -51,6 +55,7 @@ def check_install_dependencies(deps: List[str]) -> None: Logger.print_info("The following packages need installation:") for _ in requirements: print(f"{COLOR_CYAN}● {_}{RESET_FORMAT}") + update_system_package_lists(silent=False) install_system_packages(requirements) From 40e382c9a114a9b97134bee4950fd1002ec5a8aa Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 3 May 2024 19:13:21 +0200 Subject: [PATCH 201/296] feat: implement method for printing formatted dialogs Signed-off-by: Dominik Willner --- kiauh/utils/logger.py | 90 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index 9bb303b..ae0bb4e 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -6,6 +6,8 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import textwrap +from enum import Enum from utils.constants import ( COLOR_WHITE, @@ -14,9 +16,31 @@ from utils.constants import ( COLOR_RED, COLOR_MAGENTA, RESET_FORMAT, + COLOR_CYAN, ) +class DialogType(Enum): + INFO = ("INFO", COLOR_WHITE) + SUCCESS = ("SUCCESS", COLOR_GREEN) + ATTENTION = ("ATTENTION", COLOR_YELLOW) + WARNING = ("WARNING", COLOR_YELLOW) + ERROR = ("ERROR", COLOR_RED) + CUSTOM = (None, None) + + +class DialogCustomColor(Enum): + WHITE = COLOR_WHITE + GREEN = COLOR_GREEN + YELLOW = COLOR_YELLOW + RED = COLOR_RED + CYAN = COLOR_CYAN + MAGENTA = COLOR_MAGENTA + + +LINE_WIDTH = 53 + + class Logger: @staticmethod def info(msg): @@ -57,3 +81,69 @@ class Logger: def print_status(msg, prefix=True, start="", end="\n") -> None: message = f"\n###### {msg}" if prefix else msg print(f"{COLOR_MAGENTA}{start}{message}{RESET_FORMAT}", end=end) + + @staticmethod + def print_dialog( + title: DialogType, + content: str, + custom_title: str = None, + custom_color: DialogCustomColor = None, + ) -> None: + dialog_color = Logger._get_dialog_color(title, custom_color) + dialog_title = Logger._get_dialog_title(title, custom_title) + dialog_title_formatted = Logger._format_dialog_title(dialog_title) + dialog_content = Logger._format_dialog_content(content, LINE_WIDTH) + top = Logger._format_top_border(dialog_color) + bottom = Logger._format_bottom_border() + + print( + f"{top}{dialog_title_formatted}{dialog_content}{bottom}", + end="", + ) + + @staticmethod + def _get_dialog_title(title: DialogType, custom_title: str = None) -> str: + if title == DialogType.CUSTOM and custom_title: + return f"[ {custom_title} ]" + return f"[ {title.value[0]} ]" if title.value[0] else None + + @staticmethod + def _get_dialog_color( + title: DialogType, custom_color: DialogCustomColor = None + ) -> str: + if title == DialogType.CUSTOM and custom_color: + return str(custom_color.value) + return title.value[1] if title.value[1] else DialogCustomColor.WHITE.value + + @staticmethod + def _format_top_border(color: str) -> str: + return textwrap.dedent(f""" + {color}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + """)[:-1] + + @staticmethod + def _format_bottom_border() -> str: + return textwrap.dedent(f""" + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + {RESET_FORMAT}""") + + @staticmethod + def _format_dialog_title(title: str) -> str: + if title is not None: + return textwrap.dedent(f""" + ┃ {title:^{LINE_WIDTH}} ┃ + ┠───────────────────────────────────────────────────────┨ + """) + else: + return "\n" + + @staticmethod + def _format_dialog_content(content: str, line_width: int) -> str: + border_left = "┃" + border_right = "┃" + wrapper = textwrap.TextWrapper(line_width) + lines = wrapper.wrap(content) + formatted_lines = [ + f"{border_left} {line:<{line_width}} {border_right}" for line in lines + ] + return "\n".join(formatted_lines) From 09dc9616464a0a3e7038a424d38591c3fbb36936 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 3 May 2024 20:00:48 +0200 Subject: [PATCH 202/296] refactor: allow content to consist of paragraphs Signed-off-by: Dominik Willner --- kiauh/utils/logger.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index ae0bb4e..6a02310 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -8,6 +8,7 @@ # ======================================================================= # import textwrap from enum import Enum +from typing import List from utils.constants import ( COLOR_WHITE, @@ -85,7 +86,7 @@ class Logger: @staticmethod def print_dialog( title: DialogType, - content: str, + content: List[str], custom_title: str = None, custom_color: DialogCustomColor = None, ) -> None: @@ -138,11 +139,18 @@ class Logger: return "\n" @staticmethod - def _format_dialog_content(content: str, line_width: int) -> str: + def _format_dialog_content(content: List[str], line_width: int) -> str: border_left = "┃" border_right = "┃" wrapper = textwrap.TextWrapper(line_width) - lines = wrapper.wrap(content) + + lines = [] + for i, c in enumerate(content): + paragraph = wrapper.wrap(c) + lines.extend(paragraph) + if i < len(content) - 1: + lines.append(" " * line_width) + formatted_lines = [ f"{border_left} {line:<{line_width}} {border_right}" for line in lines ] From 5f1e42b88bd19a62591b8a048e08e5931bd0b432 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 3 May 2024 20:03:59 +0200 Subject: [PATCH 203/296] refactor(KlipperScreen): add proper warning message Signed-off-by: Dominik Willner --- kiauh/components/klipperscreen/klipperscreen.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index bc5ab1f..ed28439 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -34,7 +34,7 @@ from utils.git_utils import ( get_remote_commit, ) from utils.input_utils import get_confirm -from utils.logger import Logger +from utils.logger import Logger, DialogType from utils.sys_utils import ( check_python_version, control_systemd_service, @@ -51,10 +51,16 @@ def install_klipperscreen() -> None: mr_im = InstanceManager(Moonraker) mr_instances = mr_im.instances if not mr_instances: - # TODO: add moonraker not found dialog - print("Moonraker not found!") + warn_msg = [ + "Moonraker not found! KlipperScreen will not properly work " + "without a working Moonraker installation.", + "KlipperScreens update manager configuration for Moonraker " + "will not be added to any moonraker.conf.", + ] + Logger.print_dialog(DialogType.WARNING, warn_msg) if not get_confirm( "Continue KlipperScreen installation?", + default_choice=False, allow_go_back=True, ): return From 799892500ab61c1b04e7fc0adf2729eebb5c1816 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 3 May 2024 22:20:00 +0200 Subject: [PATCH 204/296] refactor(sys_utils): rename systemctl method and add new one Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 6 ++-- kiauh/components/klipper/klipper_utils.py | 4 +-- .../components/klipperscreen/klipperscreen.py | 12 ++++---- kiauh/components/webui_client/client_setup.py | 4 +-- .../core/instance_manager/instance_manager.py | 12 ++++---- kiauh/utils/sys_utils.py | 29 +++++++++++++++---- 6 files changed, 43 insertions(+), 24 deletions(-) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index a57f3f9..89ba1b8 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -30,7 +30,7 @@ from utils.input_utils import get_confirm from utils.logger import Logger from utils.sys_utils import ( parse_packages_from_file, - control_systemd_service, + cmd_sysctl_service, ) @@ -98,7 +98,7 @@ def install_crowsnest() -> None: def update_crowsnest() -> None: try: - control_systemd_service("crowsnest", "stop") + cmd_sysctl_service("crowsnest", "stop") if not CROWSNEST_DIR.exists(): git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master") @@ -111,7 +111,7 @@ def update_crowsnest() -> None: deps = parse_packages_from_file(script) check_install_dependencies(deps) - control_systemd_service("crowsnest", "restart") + cmd_sysctl_service("crowsnest", "restart") Logger.print_ok("Crowsnest updated successfully.", end="\n\n") except CalledProcessError as e: diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index a16fdb0..6054c23 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -45,7 +45,7 @@ from utils.constants import CURRENT_USER from utils.git_utils import get_repo_name, get_remote_commit, get_local_commit from utils.input_utils import get_confirm, get_string_input, get_number_input from utils.logger import Logger -from utils.sys_utils import control_systemd_service +from utils.sys_utils import cmd_sysctl_service def get_klipper_status() -> ( @@ -258,7 +258,7 @@ def handle_disruptive_system_packages() -> None: for service in services if services else []: try: - control_systemd_service(service, "mask") + cmd_sysctl_service(service, "mask") except subprocess.CalledProcessError: warn_msg = textwrap.dedent( f""" diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index ed28439..f174a19 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -37,7 +37,7 @@ from utils.input_utils import get_confirm from utils.logger import Logger, DialogType from utils.sys_utils import ( check_python_version, - control_systemd_service, + cmd_sysctl_service, install_python_requirements, ) @@ -104,7 +104,7 @@ def patch_klipperscreen_update_manager(instances: List[Moonraker]) -> None: def update_klipperscreen() -> None: try: - control_systemd_service("KlipperScreen", "stop") + cmd_sysctl_service("KlipperScreen", "stop") if not KLIPPERSCREEN_DIR.exists(): Logger.print_info( @@ -114,7 +114,7 @@ def update_klipperscreen() -> None: Logger.print_status("Updating KlipperScreen ...") - control_systemd_service("KlipperScreen", "stop") + cmd_sysctl_service("KlipperScreen", "stop") settings = KiauhSettings() if settings.get("kiauh", "backup_before_update"): @@ -127,7 +127,7 @@ def update_klipperscreen() -> None: ) install_python_requirements(KLIPPERSCREEN_ENV, requirements) - control_systemd_service("KlipperScreen", "start") + cmd_sysctl_service("KlipperScreen", "start") Logger.print_ok("KlipperScreen updated successfully.", end="\n\n") except CalledProcessError as e: @@ -176,8 +176,8 @@ def remove_klipperscreen() -> None: service = SYSTEMD.joinpath("KlipperScreen.service") if service.exists(): Logger.print_status("Removing KlipperScreen service ...") - control_systemd_service(service, "stop") - control_systemd_service(service, "disable") + cmd_sysctl_service(service, "stop") + cmd_sysctl_service(service, "disable") remove_with_sudo(service) Logger.print_ok("KlipperScreen service successfully removed!") diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 123befd..ef5ed99 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -55,7 +55,7 @@ from utils.sys_utils import ( download_file, set_nginx_permissions, get_ipv4_addr, - control_systemd_service, + cmd_sysctl_service, ) @@ -145,7 +145,7 @@ def install_client(client: BaseWebClient) -> None: create_client_nginx_cfg(client, port) if kl_instances: symlink_webui_nginx_log(kl_instances) - control_systemd_service("nginx", "restart") + cmd_sysctl_service("nginx", "restart") except Exception as e: Logger.print_error(f"{client.display_name} installation failed!\n{e}") diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 48f80e2..a4cf768 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -15,7 +15,7 @@ from typing import List, Optional, Union, TypeVar from core.instance_manager.base_instance import BaseInstance from utils.constants import SYSTEMD from utils.logger import Logger -from utils.sys_utils import control_systemd_service +from utils.sys_utils import cmd_sysctl_service T = TypeVar(name="T", bound=BaseInstance, covariant=True) @@ -109,7 +109,7 @@ class InstanceManager: def enable_instance(self) -> None: Logger.print_status(f"Enabling {self.instance_service_full} ...") try: - control_systemd_service(self.instance_service_full, "enable") + cmd_sysctl_service(self.instance_service_full, "enable") except subprocess.CalledProcessError as e: Logger.print_error(f"Error enabling service {self.instance_service_full}:") Logger.print_error(f"{e}") @@ -117,7 +117,7 @@ class InstanceManager: def disable_instance(self) -> None: Logger.print_status(f"Disabling {self.instance_service_full} ...") try: - control_systemd_service(self.instance_service_full, "disable") + cmd_sysctl_service(self.instance_service_full, "disable") except subprocess.CalledProcessError as e: Logger.print_error(f"Error disabling {self.instance_service_full}:") Logger.print_error(f"{e}") @@ -125,7 +125,7 @@ class InstanceManager: def start_instance(self) -> None: Logger.print_status(f"Starting {self.instance_service_full} ...") try: - control_systemd_service(self.instance_service_full, "start") + cmd_sysctl_service(self.instance_service_full, "start") except subprocess.CalledProcessError as e: Logger.print_error(f"Error starting {self.instance_service_full}:") Logger.print_error(f"{e}") @@ -133,7 +133,7 @@ class InstanceManager: def restart_instance(self) -> None: Logger.print_status(f"Restarting {self.instance_service_full} ...") try: - control_systemd_service(self.instance_service_full, "restart") + cmd_sysctl_service(self.instance_service_full, "restart") except subprocess.CalledProcessError as e: Logger.print_error(f"Error restarting {self.instance_service_full}:") Logger.print_error(f"{e}") @@ -151,7 +151,7 @@ class InstanceManager: def stop_instance(self) -> None: Logger.print_status(f"Stopping {self.instance_service_full} ...") try: - control_systemd_service(self.instance_service_full, "stop") + cmd_sysctl_service(self.instance_service_full, "stop") except subprocess.CalledProcessError as e: Logger.print_error(f"Error stopping {self.instance_service_full}:") Logger.print_error(f"{e}") diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 24a120e..bfeb3f5 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -26,6 +26,19 @@ from utils.input_utils import get_confirm from utils.logger import Logger +SysCtlServiceAction = Literal[ + "start", + "stop", + "restart", + "reload", + "enable", + "disable", + "mask", + "unmask", +] +SysCtlManageAction = Literal["deamon-reload", "reset-failed"] + + def kill(opt_err_msg: str = "") -> None: """ Kills the application | @@ -328,9 +341,7 @@ def set_nginx_permissions() -> None: Logger.print_ok("Permissions granted.") -def control_systemd_service( - name: str, action: Literal["start", "stop", "restart", "enable", "disable", "mask"] -) -> None: +def cmd_sysctl_service(name: str, action: SysCtlServiceAction) -> None: """ Helper method to execute several actions for a specific systemd service. | :param name: the service name @@ -339,8 +350,7 @@ def control_systemd_service( """ try: Logger.print_status(f"{action.capitalize()} {name} ...") - command = ["sudo", "systemctl", action, name] - run(command, stderr=PIPE, check=True) + run(["sudo", "systemctl", action, name], stderr=PIPE, check=True) Logger.print_ok("OK!") except CalledProcessError as e: log = f"Failed to {action} {name}: {e.stderr.decode()}" @@ -348,6 +358,15 @@ def control_systemd_service( raise +def cmd_sysctl_manage(action: SysCtlManageAction) -> None: + try: + run(["sudo", "systemctl", action], stderr=PIPE, check=True) + except CalledProcessError as e: + log = f"Failed to run {action}: {e.stderr.decode()}" + Logger.print_error(log) + raise + + def log_process(process: Popen) -> None: """ Helper method to print stdout of a process in near realtime to the console. From e5d0e97b825727ae5cf631709aacac192510d847 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 3 May 2024 22:21:38 +0200 Subject: [PATCH 205/296] refactor(KlipperScreen): reload manager config and reset failed Signed-off-by: Dominik Willner --- kiauh/components/klipperscreen/klipperscreen.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index f174a19..687c61b 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -39,6 +39,7 @@ from utils.sys_utils import ( check_python_version, cmd_sysctl_service, install_python_requirements, + cmd_sysctl_manage, ) @@ -179,6 +180,8 @@ def remove_klipperscreen() -> None: cmd_sysctl_service(service, "stop") cmd_sysctl_service(service, "disable") remove_with_sudo(service) + cmd_sysctl_manage("deamon-reload") + cmd_sysctl_manage("reset-failed") Logger.print_ok("KlipperScreen service successfully removed!") logfile = Path("/tmp/KlipperScreen.log") From 940f7cfbf10fce4ef6900beaf327ece6454c3530 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 3 May 2024 22:37:37 +0200 Subject: [PATCH 206/296] refactor(KlipperScreen): improve error message Signed-off-by: Dominik Willner --- kiauh/components/klipperscreen/klipperscreen.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index 687c61b..2e8c8b2 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -132,7 +132,7 @@ def update_klipperscreen() -> None: Logger.print_ok("KlipperScreen updated successfully.", end="\n\n") except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") + Logger.print_error(f"Error updating KlipperScreen:\n{e}") return @@ -215,8 +215,12 @@ def remove_klipperscreen() -> None: def backup_klipperscreen_dir() -> None: bm = BackupManager() bm.backup_directory( - "KlipperScreen", source=KLIPPERSCREEN_DIR, target=KLIPPERSCREEN_BACKUP_DIR + "KlipperScreen", + source=KLIPPERSCREEN_DIR, + target=KLIPPERSCREEN_BACKUP_DIR, ) bm.backup_directory( - "KlipperScreen-env", source=KLIPPERSCREEN_ENV, target=KLIPPERSCREEN_BACKUP_DIR + "KlipperScreen-env", + source=KLIPPERSCREEN_ENV, + target=KLIPPERSCREEN_BACKUP_DIR, ) From 27455dfc648484451846978e31dbe9e9b610d0f2 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 3 May 2024 23:21:23 +0200 Subject: [PATCH 207/296] feat: add mobileraker support Signed-off-by: Dominik Willner --- kiauh/components/mobileraker/__init__.py | 16 ++ kiauh/components/mobileraker/mobileraker.py | 224 ++++++++++++++++++++ kiauh/core/menus/install_menu.py | 5 + kiauh/core/menus/remove_menu.py | 15 +- kiauh/core/menus/update_menu.py | 18 +- 5 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 kiauh/components/mobileraker/__init__.py create mode 100644 kiauh/components/mobileraker/mobileraker.py diff --git a/kiauh/components/mobileraker/__init__.py b/kiauh/components/mobileraker/__init__.py new file mode 100644 index 0000000..f5fd1b2 --- /dev/null +++ b/kiauh/components/mobileraker/__init__.py @@ -0,0 +1,16 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path + +from core.backup_manager import BACKUP_ROOT_DIR + +MOBILERAKER_REPO = "https://github.com/Clon1998/mobileraker_companion.git" +MOBILERAKER_DIR = Path.home().joinpath("mobileraker_companion") +MOBILERAKER_ENV = Path.home().joinpath("mobileraker-env") +MOBILERAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mobileraker-backups") diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py new file mode 100644 index 0000000..7604772 --- /dev/null +++ b/kiauh/components/mobileraker/mobileraker.py @@ -0,0 +1,224 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import shutil +from pathlib import Path +from subprocess import run, CalledProcessError +from typing import List, Dict, Literal, Union + +from components.klipper.klipper import Klipper +from components.mobileraker import ( + MOBILERAKER_REPO, + MOBILERAKER_DIR, + MOBILERAKER_ENV, + MOBILERAKER_BACKUP_DIR, +) +from components.moonraker.moonraker import Moonraker +from core.backup_manager.backup_manager import BackupManager +from core.instance_manager.instance_manager import InstanceManager +from core.settings.kiauh_settings import KiauhSettings +from utils.common import get_install_status, check_install_dependencies +from utils.config_utils import add_config_section, remove_config_section +from utils.constants import SYSTEMD +from utils.fs_utils import remove_with_sudo +from utils.git_utils import ( + git_clone_wrapper, + git_pull_wrapper, + get_repo_name, + get_local_commit, + get_remote_commit, +) +from utils.input_utils import get_confirm +from utils.logger import Logger, DialogType +from utils.sys_utils import ( + check_python_version, + cmd_sysctl_service, + install_python_requirements, + cmd_sysctl_manage, +) + + +def install_mobileraker() -> None: + Logger.print_status("Installing Mobileraker's companion ...") + + if not check_python_version(3, 7): + return + + mr_im = InstanceManager(Moonraker) + mr_instances = mr_im.instances + if not mr_instances: + warn_msg = [ + "Moonraker not found! Mobileraker's companion will not properly work " + "without a working Moonraker installation.", + "Mobileraker's companion's update manager configuration for Moonraker " + "will not be added to any moonraker.conf.", + ] + Logger.print_dialog(DialogType.WARNING, warn_msg) + if not get_confirm( + "Continue Mobileraker's companion installation?", + default_choice=False, + allow_go_back=True, + ): + return + + package_list = ["wget", "curl", "unzip", "dfu-util"] + check_install_dependencies(package_list) + + git_clone_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR) + + try: + script = f"{MOBILERAKER_DIR}/scripts/install.sh" + run(script, shell=True, check=True) + if mr_instances: + patch_mobileraker_update_manager(mr_instances) + mr_im.restart_all_instance() + else: + Logger.print_info( + "Moonraker is not installed! Cannot add Mobileraker's companion to update manager!" + ) + Logger.print_ok("Mobileraker's companion successfully installed!") + except CalledProcessError as e: + Logger.print_error(f"Error installing Mobileraker's companion:\n{e}") + return + + +def patch_mobileraker_update_manager(instances: List[Moonraker]) -> None: + env_py = f"{MOBILERAKER_ENV}/bin/python" + add_config_section( + section="update_manager mobileraker", + instances=instances, + options=[ + ("type", "git_repo"), + ("path", "mobileraker_companion"), + ("orgin", MOBILERAKER_REPO), + ("primary_branch", "main"), + ("managed_services", "mobileraker"), + ("env", env_py), + ("requirements", "scripts/mobileraker-requirements.txt"), + ("install_script", "scripts/install.sh"), + ], + ) + + +def update_mobileraker() -> None: + try: + cmd_sysctl_service("KlipperScreen", "stop") + + if not MOBILERAKER_DIR.exists(): + Logger.print_info( + "Mobileraker's companion does not seem to be installed! Skipping ..." + ) + return + + Logger.print_status("Updating Mobileraker's companion ...") + + cmd_sysctl_service("mobileraker", "stop") + + settings = KiauhSettings() + if settings.get("kiauh", "backup_before_update"): + backup_mobileraker_dir() + + git_pull_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR) + + requirements = MOBILERAKER_DIR.joinpath("/scripts/mobileraker-requirements.txt") + install_python_requirements(MOBILERAKER_ENV, requirements) + + cmd_sysctl_service("mobileraker", "start") + + Logger.print_ok("Mobileraker's companion updated successfully.", end="\n\n") + except CalledProcessError as e: + Logger.print_error(f"Error updating Mobileraker's companion:\n{e}") + return + + +def get_mobileraker_status() -> ( + Dict[ + Literal["status", "status_code", "repo", "local", "remote"], + Union[str, int], + ] +): + files = [ + MOBILERAKER_DIR, + MOBILERAKER_ENV, + SYSTEMD.joinpath("mobileraker.service"), + ] + status = get_install_status(MOBILERAKER_DIR, files) + return { + "status": status.get("status"), + "status_code": status.get("status_code"), + "repo": get_repo_name(MOBILERAKER_DIR), + "local": get_local_commit(MOBILERAKER_DIR), + "remote": get_remote_commit(MOBILERAKER_DIR), + } + + +def remove_mobileraker() -> None: + Logger.print_status("Removing Mobileraker's companion ...") + try: + if MOBILERAKER_DIR.exists(): + Logger.print_status("Removing Mobileraker's companion directory ...") + shutil.rmtree(MOBILERAKER_DIR) + Logger.print_ok("Mobileraker's companion directory successfully removed!") + else: + Logger.print_warn("Mobileraker's companion directory not found!") + + if MOBILERAKER_ENV.exists(): + Logger.print_status("Removing Mobileraker's companion environment ...") + shutil.rmtree(MOBILERAKER_ENV) + Logger.print_ok("Mobileraker's companion environment successfully removed!") + else: + Logger.print_warn("Mobileraker's companion environment not found!") + + service = SYSTEMD.joinpath("mobileraker.service") + if service.exists(): + Logger.print_status("Removing mobileraker service ...") + cmd_sysctl_service(service, "stop") + cmd_sysctl_service(service, "disable") + remove_with_sudo(service) + cmd_sysctl_manage("deamon-reload") + cmd_sysctl_manage("reset-failed") + Logger.print_ok("Mobileraker's companion service successfully removed!") + + kl_im = InstanceManager(Klipper) + kl_instances: List[Klipper] = kl_im.instances + for instance in kl_instances: + logfile = instance.log_dir.joinpath("mobileraker.log") + if logfile.exists(): + Logger.print_status(f"Removing {logfile} ...") + Path(logfile).unlink() + Logger.print_ok(f"{logfile} successfully removed!") + + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + if mr_instances: + Logger.print_status( + "Removing Mobileraker's companion from update manager ..." + ) + remove_config_section("update_manager mobileraker", mr_instances) + Logger.print_ok( + "Mobileraker's companion successfully removed from update manager!" + ) + + Logger.print_ok("Mobileraker's companion successfully removed!") + + except Exception as e: + Logger.print_error(f"Error removing Mobileraker's companion:\n{e}") + + +def backup_mobileraker_dir() -> None: + bm = BackupManager() + bm.backup_directory( + "mobileraker_companion", + source=MOBILERAKER_DIR, + target=MOBILERAKER_BACKUP_DIR, + ) + bm.backup_directory( + "mobileraker-env", + source=MOBILERAKER_ENV, + target=MOBILERAKER_BACKUP_DIR, + ) diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 3906f79..d1c9d27 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -13,6 +13,7 @@ from typing import Type, Optional from components.crowsnest.crowsnest import install_crowsnest from components.klipper import klipper_setup from components.klipperscreen.klipperscreen import install_klipperscreen +from components.mobileraker.mobileraker import install_mobileraker from components.moonraker import moonraker_setup from components.webui_client import client_setup from components.webui_client.client_config import client_config_setup @@ -47,6 +48,7 @@ class InstallMenu(BaseMenu): "5": Option(method=self.install_mainsail_config, menu=False), "6": Option(method=self.install_fluidd_config, menu=False), "7": Option(method=self.install_klipperscreen, menu=False), + "8": Option(method=self.install_mobileraker, menu=False), "9": Option(method=self.install_crowsnest, menu=False), } @@ -96,5 +98,8 @@ class InstallMenu(BaseMenu): def install_klipperscreen(self, **kwargs): install_klipperscreen() + def install_mobileraker(self, **kwargs): + install_mobileraker() + def install_crowsnest(self, **kwargs): install_crowsnest() diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 7339823..55714fd 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -13,6 +13,7 @@ from typing import Type, Optional from components.crowsnest.crowsnest import remove_crowsnest from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu from components.klipperscreen.klipperscreen import remove_klipperscreen +from components.mobileraker.mobileraker import remove_mobileraker from components.moonraker.menus.moonraker_remove_menu import ( MoonrakerRemoveMenu, ) @@ -45,7 +46,8 @@ class RemoveMenu(BaseMenu): "3": Option(method=self.remove_mainsail, menu=True), "4": Option(method=self.remove_fluidd, menu=True), "5": Option(method=self.remove_klipperscreen, menu=True), - "6": Option(method=self.remove_crowsnest, menu=True), + "6": Option(method=self.remove_mobileraker, menu=True), + "7": Option(method=self.remove_crowsnest, menu=True), } def print_menu(self): @@ -62,11 +64,11 @@ class RemoveMenu(BaseMenu): | Firmware & API: | Touchscreen GUI: | | 1) [Klipper] | 5) [KlipperScreen] | | 2) [Moonraker] | | - | | Webcam Streamer: | - | Klipper Webinterface: | 6) [Crowsnest] | + | | Android / iOS: | + | Klipper Webinterface: | 6) [Mobileraker] | | 3) [Mainsail] | | - | 4) [Fluidd] | | - | | | + | 4) [Fluidd] | Webcam Streamer: | + | | 7) [Crowsnest] | """ )[1:] print(menu, end="") @@ -86,5 +88,8 @@ class RemoveMenu(BaseMenu): def remove_klipperscreen(self, **kwargs): remove_klipperscreen() + def remove_mobileraker(self, **kwargs): + remove_mobileraker() + def remove_crowsnest(self, **kwargs): remove_crowsnest() diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 9d0661c..528de53 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -19,6 +19,10 @@ from components.klipperscreen.klipperscreen import ( update_klipperscreen, get_klipperscreen_status, ) +from components.mobileraker.mobileraker import ( + update_mobileraker, + get_mobileraker_status, +) from components.moonraker.moonraker_setup import update_moonraker from components.moonraker.moonraker_utils import get_moonraker_status from components.webui_client.client_config.client_config_setup import ( @@ -64,6 +68,8 @@ class UpdateMenu(BaseMenu): self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.ks_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.ks_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.mb_local = f"{COLOR_WHITE}{RESET_FORMAT}" + self.mb_remote = f"{COLOR_WHITE}{RESET_FORMAT}" self.cn_local = f"{COLOR_WHITE}{RESET_FORMAT}" self.cn_remote = f"{COLOR_WHITE}{RESET_FORMAT}" @@ -119,7 +125,7 @@ class UpdateMenu(BaseMenu): | | | | | Other: |---------------|---------------| | 7) KlipperScreen | {self.ks_local:<22} | {self.ks_remote:<22} | - | 8) Mobileraker | | | + | 8) Mobileraker | {self.mb_local:<22} | {self.mb_remote:<22} | | 9) Crowsnest | {self.cn_local:<22} | {self.cn_remote:<22} | | |-------------------------------| | 10) System | | @@ -151,7 +157,8 @@ class UpdateMenu(BaseMenu): def update_klipperscreen(self, **kwargs): update_klipperscreen() - def update_mobileraker(self, **kwargs): ... + def update_mobileraker(self, **kwargs): + update_mobileraker() def update_crowsnest(self, **kwargs): update_crowsnest() @@ -207,6 +214,13 @@ class UpdateMenu(BaseMenu): ) self.ks_remote = f"{COLOR_GREEN}{ks_status.get('remote')}{RESET_FORMAT}" + # mobileraker + mb_status = get_mobileraker_status() + self.mb_local = self.format_local_status( + mb_status.get("local"), mb_status.get("remote") + ) + self.mb_remote = f"{COLOR_GREEN}{mb_status.get('remote')}{RESET_FORMAT}" + # crowsnest cn_status = get_crowsnest_status() self.cn_local = self.format_local_status( From 750bf1caaf7b6bdf616ea2e1355d2ac4989370b9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 4 May 2024 00:20:53 +0200 Subject: [PATCH 208/296] refactor: rework status fetching to make it more readable Signed-off-by: Dominik Willner --- kiauh/components/webui_client/client_utils.py | 9 +- kiauh/core/menus/main_menu.py | 48 +++---- kiauh/core/menus/update_menu.py | 117 +++++------------- 3 files changed, 58 insertions(+), 116 deletions(-) diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 32f2ff6..f6c2746 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -35,13 +35,18 @@ from utils.git_utils import ( from utils.logger import Logger -def get_client_status(client: BaseWebClient) -> str: - return get_install_status_webui( +def get_client_status( + client: BaseWebClient, fetch_remote: bool = False +) -> Dict[Literal["status", "local", "remote"], str]: + status = get_install_status_webui( client.client_dir, NGINX_SITES_AVAILABLE.joinpath(client.name), NGINX_CONFD.joinpath("upstreams.conf"), NGINX_CONFD.joinpath("common_vars.conf"), ) + local = get_local_client_version(client) + remote = get_remote_client_version(client) if fetch_remote else None + return {"status": status, "local": local, "remote": remote} def get_client_config_status( diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 1c6541a..a59551d 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -49,16 +49,9 @@ class MainMenu(BaseMenu): self.header = True self.footer_type = FooterType.QUIT - self.kl_status = "" - self.kl_repo = "" - self.mr_status = "" - self.mr_repo = "" - self.ms_status = "" - self.fl_status = "" - self.ks_status = "" - self.mb_status = "" - self.cn_status = "" - self.cc_status = "" + self.kl_status = self.kl_repo = self.mr_status = self.mr_repo = "" + self.ms_status = self.fl_status = self.ks_status = self.mb_status = "" + self.cn_status = self.cc_status = "" self.init_status() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: @@ -87,18 +80,19 @@ class MainMenu(BaseMenu): ) def fetch_status(self) -> None: - self._update_status("kl", get_klipper_status) - self._update_status("mr", get_moonraker_status) - self.ms_status = get_client_status(MainsailData()) - self.fl_status = get_client_status(FluiddData()) + self._get_component_status("kl", get_klipper_status) + self._get_component_status("mr", get_moonraker_status) + self._get_component_status("ms", get_client_status, MainsailData()) + self._get_component_status("fl", get_client_status, FluiddData()) self.cc_status = get_current_client_config([MainsailData(), FluiddData()]) - self._update_status("ks", get_klipperscreen_status) - self._update_status("cn", get_crowsnest_status) + self._get_component_status("ks", get_klipperscreen_status) + self._get_component_status("cn", get_crowsnest_status) - def _update_status(self, status_name: str, status_fn: callable) -> None: - status_data = status_fn() + def _get_component_status(self, name: str, status_fn: callable, *args) -> None: + status_data = status_fn(*args) status = status_data.get("status") code = status_data.get("status_code") + repo = status_data.get("repo") instance_count = status_data.get("instances") @@ -106,18 +100,10 @@ class MainMenu(BaseMenu): if instance_count and code == 1: count = f" {instance_count}" - setattr( - self, - f"{status_name}_status", - self._format_status_by_code(code, status, count), - ) - setattr( - self, - f"{status_name}_repo", - f"{COLOR_CYAN}{status_data.get('repo')}{RESET_FORMAT}", - ) + setattr(self, f"{name}_status", self._format_by_code(code, status, count)) + setattr(self, f"{name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT}") - def _format_status_by_code(self, code: int, status: str, count: str) -> str: + def _format_by_code(self, code: int, status: str, count: str) -> str: if code == 1: return f"{COLOR_GREEN}{status}{count}{RESET_FORMAT}" elif code == 2: @@ -144,8 +130,8 @@ class MainMenu(BaseMenu): | 2) [Update] | Moonraker: {self.mr_status:<32} | | 3) [Remove] | Repo: {self.mr_repo:<32} | | 4) [Advanced] |------------------------------------| - | 5) [Backup] | Mainsail: {self.ms_status:<26} | - | | Fluidd: {self.fl_status:<26} | + | 5) [Backup] | Mainsail: {self.ms_status:<35} | + | | Fluidd: {self.fl_status:<35} | | S) [Settings] | Client-Config: {self.cc_status:<26} | | | | | Community: | KlipperScreen: {self.ks_status:<26} | diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 528de53..7632cf3 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -30,9 +30,8 @@ from components.webui_client.client_config.client_config_setup import ( ) from components.webui_client.client_setup import update_client from components.webui_client.client_utils import ( - get_local_client_version, - get_remote_client_version, get_client_config_status, + get_client_status, ) from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData @@ -42,7 +41,6 @@ from utils.constants import ( COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, - COLOR_WHITE, COLOR_RED, ) @@ -54,27 +52,15 @@ class UpdateMenu(BaseMenu): super().__init__() self.previous_menu = previous_menu - self.kl_local = f"{COLOR_WHITE}{RESET_FORMAT}" - self.kl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" - self.mr_local = f"{COLOR_WHITE}{RESET_FORMAT}" - self.mr_remote = f"{COLOR_WHITE}{RESET_FORMAT}" - self.ms_local = f"{COLOR_WHITE}{RESET_FORMAT}" - self.ms_remote = f"{COLOR_WHITE}{RESET_FORMAT}" - self.fl_local = f"{COLOR_WHITE}{RESET_FORMAT}" - self.fl_remote = f"{COLOR_WHITE}{RESET_FORMAT}" - self.mc_local = f"{COLOR_WHITE}{RESET_FORMAT}" - self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" - self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}" - self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}" - self.ks_local = f"{COLOR_WHITE}{RESET_FORMAT}" - self.ks_remote = f"{COLOR_WHITE}{RESET_FORMAT}" - self.mb_local = f"{COLOR_WHITE}{RESET_FORMAT}" - self.mb_remote = f"{COLOR_WHITE}{RESET_FORMAT}" - self.cn_local = f"{COLOR_WHITE}{RESET_FORMAT}" - self.cn_remote = f"{COLOR_WHITE}{RESET_FORMAT}" + self.kl_local = self.kl_remote = self.mr_local = self.mr_remote = "" + self.ms_local = self.ms_remote = self.fl_local = self.fl_remote = "" + self.mc_local = self.mc_remote = self.fc_local = self.fc_remote = "" + self.ks_local = self.ks_remote = self.mb_local = self.mb_remote = "" + self.cn_local = self.cn_remote = "" - self.mainsail_client = MainsailData() - self.fluidd_client = FluiddData() + self.mainsail_data = MainsailData() + self.fluidd_data = FluiddData() + self._fetch_update_status() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu @@ -99,7 +85,7 @@ class UpdateMenu(BaseMenu): } def print_menu(self): - self.fetch_update_status() + self._fetch_update_status() header = " [ Update Menu ] " color = COLOR_GREEN @@ -143,16 +129,16 @@ class UpdateMenu(BaseMenu): update_moonraker() def update_mainsail(self, **kwargs): - update_client(self.mainsail_client) + update_client(self.mainsail_data) def update_mainsail_config(self, **kwargs): - update_client_config(self.mainsail_client) + update_client_config(self.mainsail_data) def update_fluidd(self, **kwargs): - update_client(self.fluidd_client) + update_client(self.fluidd_data) def update_fluidd_config(self, **kwargs): - update_client_config(self.fluidd_client) + update_client_config(self.fluidd_data) def update_klipperscreen(self, **kwargs): update_klipperscreen() @@ -165,70 +151,35 @@ class UpdateMenu(BaseMenu): def upgrade_system_packages(self, **kwargs): ... - def fetch_update_status(self): + def _fetch_update_status(self): # klipper - kl_status = get_klipper_status() - self.kl_local = self.format_local_status( - kl_status.get("local"), kl_status.get("remote") - ) - self.kl_remote = kl_status.get("remote") - self.kl_remote = f"{COLOR_GREEN}{kl_status.get('remote')}{RESET_FORMAT}" - + self._get_update_status("kl", get_klipper_status) # moonraker - mr_status = get_moonraker_status() - self.mr_local = self.format_local_status( - mr_status.get("local"), mr_status.get("remote") - ) - self.mr_remote = f"{COLOR_GREEN}{mr_status.get('remote')}{RESET_FORMAT}" - + self._get_update_status("mr", get_moonraker_status) # mainsail - ms_local_ver = get_local_client_version(self.mainsail_client) - ms_remote_ver = get_remote_client_version(self.mainsail_client) - self.ms_local = self.format_local_status(ms_local_ver, ms_remote_ver) - self.ms_remote = f"{COLOR_GREEN if ms_remote_ver != 'ERROR' else COLOR_RED}{ms_remote_ver}{RESET_FORMAT}" - - # fluidd - fl_local_ver = get_local_client_version(self.fluidd_client) - fl_remote_ver = get_remote_client_version(self.fluidd_client) - self.fl_local = self.format_local_status(fl_local_ver, fl_remote_ver) - self.fl_remote = f"{COLOR_GREEN if fl_remote_ver != 'ERROR' else COLOR_RED}{fl_remote_ver}{RESET_FORMAT}" - + self._get_update_status("ms", get_client_status, self.mainsail_data, True) # mainsail-config - mc_status = get_client_config_status(self.mainsail_client) - self.mc_local = self.format_local_status( - mc_status.get("local"), mc_status.get("remote") - ) - self.mc_remote = f"{COLOR_GREEN}{mc_status.get('remote')}{RESET_FORMAT}" - + self._get_update_status("mc", get_client_config_status, self.mainsail_data) + # fluidd + self._get_update_status("fl", get_client_status, self.fluidd_data, True) # fluidd-config - fc_status = get_client_config_status(self.fluidd_client) - self.fc_local = self.format_local_status( - fc_status.get("local"), fc_status.get("remote") - ) - self.fc_remote = f"{COLOR_GREEN}{fc_status.get('remote')}{RESET_FORMAT}" - + self._get_update_status("fc", get_client_config_status, self.fluidd_data) # klipperscreen - ks_status = get_klipperscreen_status() - self.ks_local = self.format_local_status( - ks_status.get("local"), ks_status.get("remote") - ) - self.ks_remote = f"{COLOR_GREEN}{ks_status.get('remote')}{RESET_FORMAT}" - + self._get_update_status("ks", get_klipperscreen_status) # mobileraker - mb_status = get_mobileraker_status() - self.mb_local = self.format_local_status( - mb_status.get("local"), mb_status.get("remote") - ) - self.mb_remote = f"{COLOR_GREEN}{mb_status.get('remote')}{RESET_FORMAT}" - + self._get_update_status("mb", get_mobileraker_status) # crowsnest - cn_status = get_crowsnest_status() - self.cn_local = self.format_local_status( - cn_status.get("local"), cn_status.get("remote") - ) - self.cn_remote = f"{COLOR_GREEN}{cn_status.get('remote')}{RESET_FORMAT}" + self._get_update_status("cn", get_crowsnest_status) - def format_local_status(self, local_version, remote_version) -> str: + def _format_local_status(self, local_version, remote_version) -> str: if local_version == remote_version: return f"{COLOR_GREEN}{local_version}{RESET_FORMAT}" return f"{COLOR_YELLOW}{local_version}{RESET_FORMAT}" + + def _get_update_status(self, name: str, status_fn: callable, *args) -> None: + status_data = status_fn(*args) + local_ver = status_data.get("local") + remote_ver = status_data.get("remote") + color = COLOR_GREEN if remote_ver != "ERROR" else COLOR_RED + setattr(self, f"{name}_local", self._format_local_status(local_ver, remote_ver)) + setattr(self, f"{name}_remote", f"{color}{remote_ver}{RESET_FORMAT}") From bf0385e3c92b4efaa15df9417aca283ff06e20b7 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 4 May 2024 00:28:12 +0200 Subject: [PATCH 209/296] fix: add missing mobileraker status getter Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index a59551d..98bb916 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -14,6 +14,7 @@ from components.crowsnest.crowsnest import get_crowsnest_status from components.klipper.klipper_utils import get_klipper_status from components.klipperscreen.klipperscreen import get_klipperscreen_status from components.log_uploads.menus.log_upload_menu import LogUploadMenu +from components.mobileraker.mobileraker import get_mobileraker_status from components.moonraker.moonraker_utils import get_moonraker_status from components.webui_client.client_utils import ( get_client_status, @@ -86,6 +87,7 @@ class MainMenu(BaseMenu): self._get_component_status("fl", get_client_status, FluiddData()) self.cc_status = get_current_client_config([MainsailData(), FluiddData()]) self._get_component_status("ks", get_klipperscreen_status) + self._get_component_status("mb", get_mobileraker_status) self._get_component_status("cn", get_crowsnest_status) def _get_component_status(self, name: str, status_fn: callable, *args) -> None: From 79b4f3eefe6e5d340857a15db29996b71f21efe0 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 4 May 2024 20:41:01 +0200 Subject: [PATCH 210/296] refactor(logger): double newline as content allows for a full blank line Signed-off-by: Dominik Willner --- kiauh/components/klipperscreen/klipperscreen.py | 1 + kiauh/utils/logger.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index 2e8c8b2..64e5d98 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -55,6 +55,7 @@ def install_klipperscreen() -> None: warn_msg = [ "Moonraker not found! KlipperScreen will not properly work " "without a working Moonraker installation.", + "\n\n", "KlipperScreens update manager configuration for Moonraker " "will not be added to any moonraker.conf.", ] diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index 6a02310..8c7d372 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -148,7 +148,10 @@ class Logger: for i, c in enumerate(content): paragraph = wrapper.wrap(c) lines.extend(paragraph) - if i < len(content) - 1: + + # add a full blank line if we have a double newline + # character unless we are at the end of the list + if c == "\n\n" and i < len(content) - 1: lines.append(" " * line_width) formatted_lines = [ From e986dfbf4c178568ec3e37786fa03cb4cd787629 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 5 May 2024 14:15:11 +0200 Subject: [PATCH 211/296] fix: fix typo in systemctl command Signed-off-by: Dominik Willner --- kiauh/components/klipperscreen/klipperscreen.py | 2 +- kiauh/components/mobileraker/mobileraker.py | 2 +- kiauh/utils/sys_utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index 64e5d98..0979cf7 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -181,7 +181,7 @@ def remove_klipperscreen() -> None: cmd_sysctl_service(service, "stop") cmd_sysctl_service(service, "disable") remove_with_sudo(service) - cmd_sysctl_manage("deamon-reload") + cmd_sysctl_manage("daemon-reload") cmd_sysctl_manage("reset-failed") Logger.print_ok("KlipperScreen service successfully removed!") diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py index 7604772..d9cb2b5 100644 --- a/kiauh/components/mobileraker/mobileraker.py +++ b/kiauh/components/mobileraker/mobileraker.py @@ -180,7 +180,7 @@ def remove_mobileraker() -> None: cmd_sysctl_service(service, "stop") cmd_sysctl_service(service, "disable") remove_with_sudo(service) - cmd_sysctl_manage("deamon-reload") + cmd_sysctl_manage("daemon-reload") cmd_sysctl_manage("reset-failed") Logger.print_ok("Mobileraker's companion service successfully removed!") diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index bfeb3f5..f6b12fa 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -36,7 +36,7 @@ SysCtlServiceAction = Literal[ "mask", "unmask", ] -SysCtlManageAction = Literal["deamon-reload", "reset-failed"] +SysCtlManageAction = Literal["daemon-reload", "reset-failed"] def kill(opt_err_msg: str = "") -> None: From 3885405366a1788043a6da7f8c284d1773247177 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 5 May 2024 15:08:24 +0200 Subject: [PATCH 212/296] feat: implement conversion of camel case to kebab case Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/base_instance.py | 4 +++- kiauh/core/instance_manager/instance_manager.py | 4 +++- kiauh/utils/common.py | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index 8655258..a2ab9a5 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -140,7 +140,9 @@ class BaseInstance(ABC): _dir.mkdir(exist_ok=True) def get_service_file_name(self, extension: bool = False) -> str: - name = f"{self.__class__.__name__.lower()}" + from utils.common import convert_camelcase_to_kebabcase + + name = convert_camelcase_to_kebabcase(self.__class__.__name__) if self.suffix != "": name += f"-{self.suffix}" diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index a4cf768..05d8197 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -174,7 +174,9 @@ class InstanceManager: raise def find_instances(self) -> List[T]: - name = self.instance_type.__name__.lower() + from utils.common import convert_camelcase_to_kebabcase + + name = convert_camelcase_to_kebabcase(self.instance_type.__name__) pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.service$") excluded = self.instance_type.blacklist() diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index aae434e..1d567a3 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -6,7 +6,7 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # - +import re from datetime import datetime from pathlib import Path from typing import Dict, Literal, List, Type, Union @@ -30,6 +30,10 @@ from utils.sys_utils import ( ) +def convert_camelcase_to_kebabcase(name: str) -> str: + return re.sub(r"(? Dict[Literal["date", "time"], str]: """ Get the current date | From 8730fc395e7ba41f257c05b8718704a456fb9601 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 5 May 2024 19:15:25 +0200 Subject: [PATCH 213/296] refactor: be able to specify last character after printing a dialog Signed-off-by: Dominik Willner --- kiauh/utils/logger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index 8c7d372..3a357ba 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -89,6 +89,7 @@ class Logger: content: List[str], custom_title: str = None, custom_color: DialogCustomColor = None, + end: str = "\n", ) -> None: dialog_color = Logger._get_dialog_color(title, custom_color) dialog_title = Logger._get_dialog_title(title, custom_title) @@ -99,7 +100,7 @@ class Logger: print( f"{top}{dialog_title_formatted}{dialog_content}{bottom}", - end="", + end=end, ) @staticmethod From 72663ef71c1619d997db21a4440aa611b19b718a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 5 May 2024 19:16:03 +0200 Subject: [PATCH 214/296] feat: implement moonraker telegram bot extension Signed-off-by: Dominik Willner --- kiauh/extensions/telegram_bot/__init__.py | 0 .../assets/moonraker-telegram-bot.env | 1 + .../assets/moonraker-telegram-bot.service | 16 ++ kiauh/extensions/telegram_bot/metadata.json | 11 + .../telegram_bot/moonraker_telegram_bot.py | 154 ++++++++++++ .../moonraker_telegram_bot_extension.py | 230 ++++++++++++++++++ 6 files changed, 412 insertions(+) create mode 100644 kiauh/extensions/telegram_bot/__init__.py create mode 100644 kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env create mode 100644 kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service create mode 100644 kiauh/extensions/telegram_bot/metadata.json create mode 100644 kiauh/extensions/telegram_bot/moonraker_telegram_bot.py create mode 100644 kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py diff --git a/kiauh/extensions/telegram_bot/__init__.py b/kiauh/extensions/telegram_bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env b/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env new file mode 100644 index 0000000..280f165 --- /dev/null +++ b/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env @@ -0,0 +1 @@ +TELEGRAM_BOT_ARGS="%TELEGRAM_BOT_DIR%/bot/main.py -c %CFG% -l %LOG%" \ No newline at end of file diff --git a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service b/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service new file mode 100644 index 0000000..567481d --- /dev/null +++ b/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service @@ -0,0 +1,16 @@ +[Unit] +Description=Moonraker Telegram Bot SV1 %INST% +Documentation=https://github.com/nlef/moonraker-telegram-bot/wiki +After=network-online.target + +[Install] +WantedBy=multi-user.target + +[Service] +Type=simple +User=%USER% +WorkingDirectory=%TELEGRAM_BOT_DIR% +EnvironmentFile=%ENV_FILE% +ExecStart=%ENV%/bin/python $TELEGRAM_BOT_ARGS +Restart=always +RestartSec=10 diff --git a/kiauh/extensions/telegram_bot/metadata.json b/kiauh/extensions/telegram_bot/metadata.json new file mode 100644 index 0000000..b3b4764 --- /dev/null +++ b/kiauh/extensions/telegram_bot/metadata.json @@ -0,0 +1,11 @@ +{ + "metadata": { + "index": 4, + "module": "moonraker_telegram_bot_extension", + "maintained_by": "nlef", + "display_name": "Moonraker Telegram Bot", + "description": "Allows to control your printer with the Telegram messenger app.", + "project_url": "https://github.com/nlef/moonraker-telegram-bot", + "updates": true + } +} diff --git a/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py b/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py new file mode 100644 index 0000000..316b01c --- /dev/null +++ b/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py @@ -0,0 +1,154 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # + +import subprocess +from pathlib import Path +from typing import List + +from core.instance_manager.base_instance import BaseInstance +from utils.constants import SYSTEMD +from utils.logger import Logger + + +MODULE_PATH = Path(__file__).resolve().parent + +TELEGRAM_BOT_DIR = Path.home().joinpath("moonraker-telegram-bot") +TELEGRAM_BOT_ENV = Path.home().joinpath("moonraker-telegram-bot-env") +TELEGRAM_BOT_REPO = "https://github.com/nlef/moonraker-telegram-bot.git" + + +# noinspection PyMethodMayBeStatic +class MoonrakerTelegramBot(BaseInstance): + @classmethod + def blacklist(cls) -> List[str]: + return ["None", "mcu"] + + def __init__(self, suffix: str = ""): + super().__init__(instance_type=self, suffix=suffix) + self.bot_dir: Path = TELEGRAM_BOT_DIR + self.env_dir: Path = TELEGRAM_BOT_ENV + self._cfg_file = self.cfg_dir.joinpath("telegram.conf") + self._log = self.log_dir.joinpath("telegram.log") + self._assets_dir = MODULE_PATH.joinpath("assets") + + @property + def cfg_file(self) -> Path: + return self._cfg_file + + @property + def log(self) -> Path: + return self._log + + def create(self) -> None: + Logger.print_status("Creating new Moonraker Telegram Bot Instance ...") + service_template_path = MODULE_PATH.joinpath( + "assets/moonraker-telegram-bot.service" + ) + service_file_name = self.get_service_file_name(extension=True) + service_file_target = SYSTEMD.joinpath(service_file_name) + env_template_file_path = MODULE_PATH.joinpath( + "assets/moonraker-telegram-bot.env" + ) + env_file_target = self.sysd_dir.joinpath("moonraker-telegram-bot.env") + + try: + self.create_folders() + self.write_service_file( + service_template_path, service_file_target, env_file_target + ) + self.write_env_file(env_template_file_path, env_file_target) + + except subprocess.CalledProcessError as e: + Logger.print_error( + f"Error creating service file {service_file_target}: {e}" + ) + raise + except OSError as e: + Logger.print_error(f"Error creating env file {env_file_target}: {e}") + raise + + def delete(self) -> None: + service_file = self.get_service_file_name(extension=True) + service_file_path = self.get_service_file_path() + + Logger.print_status(f"Deleting Moonraker Telegram Bot Instance: {service_file}") + + try: + command = ["sudo", "rm", "-f", service_file_path] + subprocess.run(command, check=True) + Logger.print_ok(f"Service file deleted: {service_file_path}") + except subprocess.CalledProcessError as e: + Logger.print_error(f"Error deleting service file: {e}") + raise + + def write_service_file( + self, + service_template_path: Path, + service_file_target: Path, + env_file_target: Path, + ) -> None: + service_content = self._prep_service_file( + service_template_path, env_file_target + ) + command = ["sudo", "tee", service_file_target] + subprocess.run( + command, + input=service_content.encode(), + stdout=subprocess.DEVNULL, + check=True, + ) + Logger.print_ok(f"Service file created: {service_file_target}") + + def write_env_file( + self, env_template_file_path: Path, env_file_target: Path + ) -> None: + env_file_content = self._prep_env_file(env_template_file_path) + with open(env_file_target, "w") as env_file: + env_file.write(env_file_content) + Logger.print_ok(f"Env file created: {env_file_target}") + + def _prep_service_file( + self, service_template_path: Path, env_file_path: Path + ) -> str: + try: + with open(service_template_path, "r") as template_file: + template_content = template_file.read() + except FileNotFoundError: + Logger.print_error( + f"Unable to open {service_template_path} - File not found" + ) + raise + service_content = template_content.replace("%USER%", self.user) + service_content = service_content.replace( + "%TELEGRAM_BOT_DIR%", + str(self.bot_dir), + ) + service_content = service_content.replace("%ENV%", str(self.env_dir)) + service_content = service_content.replace("%ENV_FILE%", str(env_file_path)) + return service_content + + def _prep_env_file(self, env_template_file_path: Path) -> str: + try: + with open(env_template_file_path, "r") as env_file: + env_template_file_content = env_file.read() + except FileNotFoundError: + Logger.print_error( + f"Unable to open {env_template_file_path} - File not found" + ) + raise + env_file_content = env_template_file_content.replace( + "%TELEGRAM_BOT_DIR%", + str(self.bot_dir), + ) + env_file_content = env_file_content.replace( + "%CFG%", + f"{self.cfg_dir}/printer.cfg", + ) + env_file_content = env_file_content.replace("%LOG%", str(self.log)) + return env_file_content diff --git a/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py b/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py new file mode 100644 index 0000000..90143c2 --- /dev/null +++ b/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py @@ -0,0 +1,230 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import shutil +from subprocess import run +from typing import List + +from components.moonraker.moonraker import Moonraker +from core.instance_manager.instance_manager import InstanceManager +from extensions.base_extension import BaseExtension +from extensions.telegram_bot.moonraker_telegram_bot import ( + MoonrakerTelegramBot, + TELEGRAM_BOT_REPO, + TELEGRAM_BOT_DIR, + TELEGRAM_BOT_ENV, +) +from utils.common import check_install_dependencies +from utils.config_utils import add_config_section, remove_config_section +from utils.fs_utils import remove_file +from utils.git_utils import git_clone_wrapper, git_pull_wrapper +from utils.input_utils import get_confirm +from utils.logger import Logger, DialogType +from utils.sys_utils import ( + parse_packages_from_file, + create_python_venv, + install_python_requirements, + cmd_sysctl_manage, +) + + +# noinspection PyMethodMayBeStatic +class TelegramBotExtension(BaseExtension): + def install_extension(self, **kwargs) -> None: + Logger.print_status("Installing Moonraker Telegram Bot ...") + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + if not mr_instances: + Logger.print_dialog( + DialogType.WARNING, + [ + "No Moonraker instances found!", + "Moonraker Telegram Bot requires Moonraker to be installed. Please install Moonraker first!", + ], + ) + return + + instance_names = [f"● {instance.data_dir_name}" for instance in mr_instances] + Logger.print_dialog( + DialogType.INFO, + [ + "The following Moonraker instances were found:", + *instance_names, + "\n\n", + "The setup will apply the same names to Telegram Bot!", + ], + ) + if not get_confirm( + "Continue Moonraker Telegram Bot installation?", + default_choice=True, + allow_go_back=True, + ): + return + + create_example_cfg = get_confirm("Create example telegram.conf?") + + try: + git_clone_wrapper(TELEGRAM_BOT_REPO, TELEGRAM_BOT_DIR) + self._install_dependencies() + + # create and start services / create bot configs + show_config_dialog = False + tb_im = InstanceManager(MoonrakerTelegramBot) + tb_names = [mr_i.suffix for mr_i in mr_instances] + for name in tb_names: + current_instance = MoonrakerTelegramBot(suffix=name) + + tb_im.current_instance = current_instance + tb_im.create_instance() + tb_im.enable_instance() + + if create_example_cfg: + Logger.print_status( + f"Creating Telegram Bot config in {current_instance.cfg_dir} ..." + ) + template = TELEGRAM_BOT_DIR.joinpath( + "scripts/base_install_template" + ) + target_file = current_instance.cfg_file + if not target_file.exists(): + show_config_dialog = True + run(["cp", template, target_file], check=True) + else: + Logger.print_info( + f"Telegram Bot config in {current_instance.cfg_dir} already exists! Skipped ..." + ) + + tb_im.start_instance() + + cmd_sysctl_manage("daemon-reload") + + # add to moonraker update manager + self._patch_bot_update_manager(mr_instances) + + # restart moonraker + mr_im.restart_all_instance() + + if show_config_dialog: + Logger.print_dialog( + DialogType.ATTENTION, + [ + "During the installation of the Moonraker Telegram Bot, " + "a basic config was created per instance. You need to edit the " + "config file to set up your Telegram Bot. Please refer to the " + "following wiki page for further information:", + "https://github.com/nlef/moonraker-telegram-bot/wiki", + ], + ) + + Logger.print_ok("Telegram Bot installation complete!") + except Exception as e: + Logger.print_error( + f"Error during installation of Moonraker Telegram Bot:\n{e}" + ) + + def update_extension(self, **kwargs) -> None: + Logger.print_status("Updating Moonraker Telegram Bot ...") + tb_im = InstanceManager(MoonrakerTelegramBot) + tb_im.stop_all_instance() + + git_pull_wrapper(TELEGRAM_BOT_REPO, TELEGRAM_BOT_DIR) + self._install_dependencies() + + tb_im.start_all_instance() + + def remove_extension(self, **kwargs) -> None: + Logger.print_status("Removing Moonraker Telegram Bot ...") + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + tb_im = InstanceManager(MoonrakerTelegramBot) + tb_instances: List[MoonrakerTelegramBot] = tb_im.instances + + try: + self._remove_bot_instances(tb_im, tb_instances) + self._remove_bot_dir() + self._remove_bot_env() + remove_config_section("update_manager moonraker-telegram-bot", mr_instances) + self._delete_bot_logs(tb_instances) + except Exception as e: + Logger.print_error(f"Error during removal of Moonraker Telegram Bot:\n{e}") + + Logger.print_ok("Moonraker Telegram Bot removed!") + + def _install_dependencies(self) -> None: + # install dependencies + script = TELEGRAM_BOT_DIR.joinpath("scripts/install.sh") + package_list = parse_packages_from_file(script) + check_install_dependencies(package_list) + + # create virtualenv + create_python_venv(TELEGRAM_BOT_ENV) + requirements = TELEGRAM_BOT_DIR.joinpath("scripts/requirements.txt") + install_python_requirements(TELEGRAM_BOT_ENV, requirements) + + def _patch_bot_update_manager(self, instances: List[Moonraker]) -> None: + env_py = f"{TELEGRAM_BOT_ENV}/bin/python" + add_config_section( + section="update_manager moonraker-telegram-bot", + instances=instances, + options=[ + ("type", "git_repo"), + ("path", str(TELEGRAM_BOT_DIR)), + ("orgin", TELEGRAM_BOT_REPO), + ("env", env_py), + ("requirements", "scripts/requirements.txt"), + ("install_script", "scripts/install.sh"), + ], + ) + + def _remove_bot_instances( + self, + instance_manager: InstanceManager, + instance_list: List[MoonrakerTelegramBot], + ) -> None: + for instance in instance_list: + Logger.print_status( + f"Removing instance {instance.get_service_file_name()} ..." + ) + instance_manager.current_instance = instance + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance() + + instance_manager.reload_daemon() + + def _remove_bot_dir(self) -> None: + if not TELEGRAM_BOT_DIR.exists(): + Logger.print_info(f"'{TELEGRAM_BOT_DIR}' does not exist. Skipped ...") + return + + try: + shutil.rmtree(TELEGRAM_BOT_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{TELEGRAM_BOT_DIR}':\n{e}") + + def _remove_bot_env(self) -> None: + if not TELEGRAM_BOT_ENV.exists(): + Logger.print_info(f"'{TELEGRAM_BOT_ENV}' does not exist. Skipped ...") + return + + try: + shutil.rmtree(TELEGRAM_BOT_ENV) + except OSError as e: + Logger.print_error(f"Unable to delete '{TELEGRAM_BOT_ENV}':\n{e}") + + def _delete_bot_logs(self, instances: List[MoonrakerTelegramBot]) -> None: + all_logfiles = [] + for instance in instances: + all_logfiles = list(instance.log_dir.glob("telegram_bot.log*")) + if not all_logfiles: + Logger.print_info("No Moonraker Telegram Bot logs found. Skipped ...") + return + + for log in all_logfiles: + Logger.print_status(f"Remove '{log}'") + remove_file(log) From 3b2bc05746d61ad404d21682a7a98d9cd1c80d2c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 5 May 2024 19:23:09 +0200 Subject: [PATCH 215/296] refactor(crowsnest): allow backup before update for crowsnest Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/__init__.py | 3 +++ kiauh/components/crowsnest/crowsnest.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/kiauh/components/crowsnest/__init__.py b/kiauh/components/crowsnest/__init__.py index c68284b..bc1ae8a 100644 --- a/kiauh/components/crowsnest/__init__.py +++ b/kiauh/components/crowsnest/__init__.py @@ -9,5 +9,8 @@ from pathlib import Path +from core.backup_manager import BACKUP_ROOT_DIR + CROWSNEST_DIR = Path.home().joinpath("crowsnest") CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git" +CROWSNEST_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("crowsnest-backups") diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index 89ba1b8..b2ca8d0 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -14,9 +14,11 @@ from pathlib import Path from subprocess import run, CalledProcessError from typing import List, Dict, Literal, Union -from components.crowsnest import CROWSNEST_REPO, CROWSNEST_DIR +from components.crowsnest import CROWSNEST_REPO, CROWSNEST_DIR, CROWSNEST_BACKUP_DIR from components.klipper.klipper import Klipper +from core.backup_manager.backup_manager import BackupManager from core.instance_manager.instance_manager import InstanceManager +from core.settings.kiauh_settings import KiauhSettings from utils.common import get_install_status, check_install_dependencies from utils.constants import COLOR_CYAN, RESET_FORMAT, CURRENT_USER from utils.git_utils import ( @@ -105,6 +107,15 @@ def update_crowsnest() -> None: else: Logger.print_status("Updating Crowsnest ...") + settings = KiauhSettings() + if settings.get("kiauh", "backup_before_update"): + bm = BackupManager() + bm.backup_directory( + "crowsnest", + source=CROWSNEST_DIR, + target=CROWSNEST_BACKUP_DIR, + ) + git_pull_wrapper(CROWSNEST_REPO, CROWSNEST_DIR) script = CROWSNEST_DIR.joinpath("tools/install.sh") From a4b149c11a857654a3fb71fe06d8ac1c7ab601b5 Mon Sep 17 00:00:00 2001 From: Staubgeborener Date: Wed, 8 May 2024 18:44:26 +0200 Subject: [PATCH 216/296] chore: remove test section and add new klipperbackup url in header (#467) The old test branch will be deleted anyway --- kiauh/extensions/klipper_backup/klipper_backup_extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/extensions/klipper_backup/klipper_backup_extension.py b/kiauh/extensions/klipper_backup/klipper_backup_extension.py index 6aac38c..94d1570 100644 --- a/kiauh/extensions/klipper_backup/klipper_backup_extension.py +++ b/kiauh/extensions/klipper_backup/klipper_backup_extension.py @@ -1,6 +1,7 @@ # ======================================================================= # # Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet # # https://github.com/Staubgeborener/klipper-backup # +# https://klipperbackup.xyz # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # @@ -32,7 +33,6 @@ class KlipperbackupExtension(BaseExtension): subprocess.run( ["git", "clone", str(KLIPPERBACKUP_REPO_URL), str(KLIPPERBACKUP_DIR)] ) - # subprocess.run(["git", "-C", str(KLIPPERBACKUP_DIR), "checkout", "installer-dev"]) # Only for testing subprocess.run(["chmod", "+x", str(KLIPPERBACKUP_DIR / "install.sh")]) subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh")]) From 05b5664062e8ac671a08bedfad344f07b1ea3ac4 Mon Sep 17 00:00:00 2001 From: Patrick Gehrsitz Date: Fri, 10 May 2024 21:28:15 +0200 Subject: [PATCH 217/296] fix: fix crowsnest installer (#470) * fix: fix crowsnest installer Signed-off-by: mryel00 * chore: remove unnecessary code This check is made inside the called tool too. Therefore removing it here. Signed-off-by: mryel00 --------- Signed-off-by: mryel00 --- kiauh/components/crowsnest/crowsnest.py | 57 ++++++++++++------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index b2ca8d0..29f0c94 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -28,7 +28,6 @@ from utils.git_utils import ( get_remote_commit, git_pull_wrapper, ) -from utils.input_utils import get_confirm from utils.logger import Logger from utils.sys_utils import ( parse_packages_from_file, @@ -48,23 +47,37 @@ def install_crowsnest() -> None: instances: List[Klipper] = im.find_instances() if len(instances) > 1: - Logger.print_status("Multi instance install detected ...") - info = textwrap.dedent(""" - Crowsnest is NOT designed to support multi instances. - A workaround for this is to choose the most used instance as a 'master' - Use this instance to set up your 'crowsnest.conf' and steering it's service. - Found the following instances: - """)[:-1] - print(info, end="") - for instance in instances: - print(f"● {instance.data_dir_name}") + configure_multi_instance(instances) - Logger.print_status("\nLaunching crowsnest's configuration tool ...") - - if not get_confirm("Continue with configuration?", False, allow_go_back=True): - Logger.print_info("Installation aborted by user ... Exiting!") + # Step 4: Launch crowsnest installer + print(f"{COLOR_CYAN}Installer will prompt you for sudo password!{RESET_FORMAT}") + Logger.print_status("Launching crowsnest installer ...") + try: + run( + f"sudo make install BASE_USER={CURRENT_USER}", + cwd=CROWSNEST_DIR, + shell=True, + check=True, + ) + except CalledProcessError as e: + Logger.print_error(f"Something went wrong! Please try again...\n{e}") return + +def configure_multi_instance(instances: List[Klipper]) -> None: + Logger.print_status("Multi instance install detected ...") + info = textwrap.dedent(""" + Crowsnest is NOT designed to support multi instances. + A workaround for this is to choose the most used instance as a 'master' + Use this instance to set up your 'crowsnest.conf' and steering it's service. + Found the following instances: + """)[:-1] + print(info, end="") + for instance in instances: + print(f"● {instance.data_dir_name}") + + Logger.print_status("\nLaunching crowsnest's configuration tool ...") + config = Path(CROWSNEST_DIR).joinpath("tools/.config") try: run( @@ -83,20 +96,6 @@ def install_crowsnest() -> None: Logger.print_error("Generating .config failed, installation aborted") return - # Step 4: Launch crowsnest installer - print(f"{COLOR_CYAN}Installer will prompt you for sudo password!{RESET_FORMAT}") - Logger.print_status("Launching crowsnest installer ...") - try: - run( - f"sudo make install BASE_USER={CURRENT_USER}", - cwd=CROWSNEST_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") - return - def update_crowsnest() -> None: try: From 63cae491f39324817e5162334ae02305440bb3fd Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 May 2024 12:27:54 +0200 Subject: [PATCH 218/296] refactor: update .gitignore Signed-off-by: Dominik Willner --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index efabb67..bff7f5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .idea .vscode -.idea .pytest_cache +__pycache__ .kiauh-env *.code-workspace *.iml From ea78ba25e6aa7d8fc0ac73987c9394182ec5ba2a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 May 2024 19:45:42 +0200 Subject: [PATCH 219/296] fix(crowsnest): fix multi instance steps Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 65 +++++++++++++++---------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index 29f0c94..f6a0e09 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -9,29 +9,30 @@ from __future__ import annotations import shutil -import textwrap +import time from pathlib import Path -from subprocess import run, CalledProcessError -from typing import List, Dict, Literal, Union +from subprocess import CalledProcessError, run +from typing import Dict, List, Literal, Union -from components.crowsnest import CROWSNEST_REPO, CROWSNEST_DIR, CROWSNEST_BACKUP_DIR +from components.crowsnest import CROWSNEST_BACKUP_DIR, CROWSNEST_DIR, CROWSNEST_REPO from components.klipper.klipper import Klipper from core.backup_manager.backup_manager import BackupManager from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings -from utils.common import get_install_status, check_install_dependencies -from utils.constants import COLOR_CYAN, RESET_FORMAT, CURRENT_USER +from utils.common import check_install_dependencies, get_install_status +from utils.constants import CURRENT_USER from utils.git_utils import ( - git_clone_wrapper, - get_repo_name, get_local_commit, get_remote_commit, + get_repo_name, + git_clone_wrapper, git_pull_wrapper, ) -from utils.logger import Logger +from utils.input_utils import get_confirm +from utils.logger import DialogType, Logger from utils.sys_utils import ( - parse_packages_from_file, cmd_sysctl_service, + parse_packages_from_file, ) @@ -44,14 +45,22 @@ def install_crowsnest() -> None: # Step 3: Check for Multi Instance im = InstanceManager(Klipper) - instances: List[Klipper] = im.find_instances() + instances: List[Klipper] = im.instances if len(instances) > 1: - configure_multi_instance(instances) + print_multi_instance_warning(instances) + + if not get_confirm("Do you want to continue with the installation?"): + Logger.print_info("Crowsnest installation aborted!") + return + + Logger.print_status("Launching crowsnest's install configurator ...") + time.sleep(3) + configure_multi_instance() # Step 4: Launch crowsnest installer - print(f"{COLOR_CYAN}Installer will prompt you for sudo password!{RESET_FORMAT}") Logger.print_status("Launching crowsnest installer ...") + Logger.print_info("Installer will prompt you for sudo password!") try: run( f"sudo make install BASE_USER={CURRENT_USER}", @@ -64,20 +73,25 @@ def install_crowsnest() -> None: return -def configure_multi_instance(instances: List[Klipper]) -> None: - Logger.print_status("Multi instance install detected ...") - info = textwrap.dedent(""" - Crowsnest is NOT designed to support multi instances. - A workaround for this is to choose the most used instance as a 'master' - Use this instance to set up your 'crowsnest.conf' and steering it's service. - Found the following instances: - """)[:-1] - print(info, end="") - for instance in instances: - print(f"● {instance.data_dir_name}") +def print_multi_instance_warning(instances: List[Klipper]) -> None: + _instances = [f"● {instance.data_dir_name}" for instance in instances] + Logger.print_dialog( + DialogType.WARNING, + [ + "Multi instance install detected!", + "\n\n", + "Crowsnest is NOT designed to support multi instances. A workaround " + "for this is to choose the most used instance as a 'master' and use " + "this instance to set up your 'crowsnest.conf' and steering it's service.", + "\n\n", + "The following instances were found:", + *_instances, + ], + end="", + ) - Logger.print_status("\nLaunching crowsnest's configuration tool ...") +def configure_multi_instance() -> None: config = Path(CROWSNEST_DIR).joinpath("tools/.config") try: run( @@ -94,7 +108,6 @@ def configure_multi_instance(instances: List[Klipper]) -> None: if not config.exists(): Logger.print_error("Generating .config failed, installation aborted") - return def update_crowsnest() -> None: From 9342c940969a8365cff84316b8e3d3259d22e689 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 May 2024 20:18:09 +0200 Subject: [PATCH 220/296] chore: cleanup and update toml, create editorconfig Signed-off-by: Dominik Willner --- .editorconfig | 11 +++++++++++ kiauh.cfg.example | 20 -------------------- pyproject.toml | 3 +++ 3 files changed, 14 insertions(+), 20 deletions(-) create mode 100644 .editorconfig delete mode 100644 kiauh.cfg.example diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..50a806a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.py] +max_line_length = 88 diff --git a/kiauh.cfg.example b/kiauh.cfg.example deleted file mode 100644 index 0d61335..0000000 --- a/kiauh.cfg.example +++ /dev/null @@ -1,20 +0,0 @@ -[kiauh] -backup_before_update: False - -[klipper] -repository_url: https://github.com/Klipper3d/klipper -branch: master -method: https - -[moonraker] -repository_url: https://github.com/Arksine/moonraker -branch: master -method: https - -[mainsail] -port: 80 -unstable_releases: False - -[fluidd] -port: 80 -unstable_releases: False diff --git a/pyproject.toml b/pyproject.toml index 81a968d..364b1a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,9 @@ [project] requires-python = ">=3.8" +[project.optional-dependencies] +dev=["ruff"] + [tool.ruff] required-version = ">=0.3.4" respect-gitignore = true From a44508ead5bfd14f8628860b101be9b39db44829 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 May 2024 22:16:36 +0200 Subject: [PATCH 221/296] refactor: update dependency management Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 1 + kiauh/components/klipper/klipper_utils.py | 5 +- .../components/klipperscreen/klipperscreen.py | 24 ++++--- kiauh/components/mobileraker/mobileraker.py | 22 ++++--- kiauh/components/moonraker/moonraker_setup.py | 66 ++++++++++++------- kiauh/components/webui_client/client_setup.py | 2 +- 6 files changed, 74 insertions(+), 46 deletions(-) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index eb280e4..1745d25 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -74,6 +74,7 @@ def install_klipper() -> None: try: if not kl_im.instances: + check_install_dependencies(["git"]) setup_klipper_prerequesites() count = 0 diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 6054c23..10d7d95 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -260,10 +260,11 @@ def handle_disruptive_system_packages() -> None: try: cmd_sysctl_service(service, "mask") except subprocess.CalledProcessError: + # todo: replace with Logger.print_dialog warn_msg = textwrap.dedent( f""" - KIAUH was unable to mask the {service} system service. - Please fix the problem manually. Otherwise, this may have + KIAUH was unable to mask the {service} system service. + Please fix the problem manually. Otherwise, this may have undesirable effects on the operation of Klipper. """ )[1:] diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index 0979cf7..87dcc03 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -52,14 +52,17 @@ def install_klipperscreen() -> None: mr_im = InstanceManager(Moonraker) mr_instances = mr_im.instances if not mr_instances: - warn_msg = [ - "Moonraker not found! KlipperScreen will not properly work " - "without a working Moonraker installation.", - "\n\n", - "KlipperScreens update manager configuration for Moonraker " - "will not be added to any moonraker.conf.", - ] - Logger.print_dialog(DialogType.WARNING, warn_msg) + Logger.print_dialog( + DialogType.WARNING, + [ + "Moonraker not found! KlipperScreen will not properly work " + "without a working Moonraker installation.", + "\n\n", + "KlipperScreens update manager configuration for Moonraker " + "will not be added to any moonraker.conf.", + ], + end="", + ) if not get_confirm( "Continue KlipperScreen installation?", default_choice=False, @@ -67,7 +70,7 @@ def install_klipperscreen() -> None: ): return - package_list = ["wget", "curl", "unzip", "dfu-util"] + package_list = ["git", "wget", "curl", "unzip", "dfu-util"] check_install_dependencies(package_list) git_clone_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR) @@ -80,7 +83,8 @@ def install_klipperscreen() -> None: mr_im.restart_all_instance() else: Logger.print_info( - "Moonraker is not installed! Cannot add KlipperScreen to update manager!" + "Moonraker is not installed! Cannot add " + "KlipperScreen to update manager!" ) Logger.print_ok("KlipperScreen successfully installed!") except CalledProcessError as e: diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py index d9cb2b5..a432653 100644 --- a/kiauh/components/mobileraker/mobileraker.py +++ b/kiauh/components/mobileraker/mobileraker.py @@ -52,13 +52,16 @@ def install_mobileraker() -> None: mr_im = InstanceManager(Moonraker) mr_instances = mr_im.instances if not mr_instances: - warn_msg = [ - "Moonraker not found! Mobileraker's companion will not properly work " - "without a working Moonraker installation.", - "Mobileraker's companion's update manager configuration for Moonraker " - "will not be added to any moonraker.conf.", - ] - Logger.print_dialog(DialogType.WARNING, warn_msg) + Logger.print_dialog( + DialogType.WARNING, + [ + "Moonraker not found! Mobileraker's companion will not properly work " + "without a working Moonraker installation.", + "Mobileraker's companion's update manager configuration for Moonraker " + "will not be added to any moonraker.conf.", + ], + end="", + ) if not get_confirm( "Continue Mobileraker's companion installation?", default_choice=False, @@ -66,7 +69,7 @@ def install_mobileraker() -> None: ): return - package_list = ["wget", "curl", "unzip", "dfu-util"] + package_list = ["git", "wget", "curl", "unzip", "dfu-util"] check_install_dependencies(package_list) git_clone_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR) @@ -79,7 +82,8 @@ def install_mobileraker() -> None: mr_im.restart_all_instance() else: Logger.print_info( - "Moonraker is not installed! Cannot add Mobileraker's companion to update manager!" + "Moonraker is not installed! Cannot add Mobileraker's " + "companion to update manager!" ) Logger.print_ok("Mobileraker's companion successfully installed!") except CalledProcessError as e: diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 1bc0092..8885b34 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -6,7 +6,7 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # - +import json import subprocess from pathlib import Path @@ -86,32 +86,40 @@ def install_moonraker() -> None: instance_names.append(klipper_instances[index].suffix) create_example_cfg = get_confirm("Create example moonraker.conf?") - setup_moonraker_prerequesites() - install_moonraker_polkit() - used_ports_map = { - instance.suffix: instance.port for instance in moonraker_instances - } - for name in instance_names: - current_instance = Moonraker(suffix=name) + try: + check_install_dependencies(["git"]) + setup_moonraker_prerequesites() + install_moonraker_polkit() - mr_im.current_instance = current_instance - mr_im.create_instance() - mr_im.enable_instance() + used_ports_map = { + instance.suffix: instance.port for instance in moonraker_instances + } + for name in instance_names: + current_instance = Moonraker(suffix=name) - 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(current_instance, used_ports_map, clients) + mr_im.current_instance = current_instance + mr_im.create_instance() + mr_im.enable_instance() - mr_im.start_instance() + 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(current_instance, used_ports_map, clients) - mr_im.reload_daemon() + mr_im.start_instance() - # if mainsail is installed, and we installed - # multiple moonraker instances, we enable mainsails remote mode - if MainsailData().client_dir.exists() and len(mr_im.instances) > 1: - enable_mainsail_remotemode() + mr_im.reload_daemon() + + # if mainsail is installed, and we installed + # multiple moonraker instances, we enable mainsails remote mode + if MainsailData().client_dir.exists() and len(mr_im.instances) > 1: + enable_mainsail_remotemode() + + except Exception as e: + Logger.print_error(f"Error while installing Moonraker: {e}") + return def check_moonraker_install_requirements() -> bool: @@ -140,9 +148,19 @@ def setup_moonraker_prerequesites() -> None: def install_moonraker_packages(moonraker_dir: Path) -> None: - script = moonraker_dir.joinpath("scripts/install-moonraker.sh") - packages = parse_packages_from_file(script) - check_install_dependencies(packages) + install_script = moonraker_dir.joinpath("scripts/install-moonraker.sh") + deps_json = MOONRAKER_DIR.joinpath("scripts/system-dependencies.json") + moonraker_deps = [] + + if deps_json.exists(): + moonraker_deps = json.load(deps_json).get("debian", []) + elif install_script.exists(): + moonraker_deps = parse_packages_from_file(install_script) + + if not moonraker_deps: + raise ValueError("Error reading Moonraker dependencies!") + + check_install_dependencies(moonraker_deps) def install_moonraker_polkit() -> None: diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index ef5ed99..f4e1066 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -119,7 +119,7 @@ def install_client(client: BaseWebClient) -> None: ) valid_port = is_valid_port(port, ports_in_use) - check_install_dependencies(["nginx"]) + check_install_dependencies(["nginx", "unzip"]) try: download_client(client) From 7f79f68209c66dd6a08278b9c0f40179baea0414 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 May 2024 22:29:53 +0200 Subject: [PATCH 222/296] refactor(Klipper): use warn dialog Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 35 +++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 10d7d95..be6ede4 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -12,7 +12,6 @@ import os import re import shutil import subprocess -import textwrap from typing import List, Union, Literal, Dict, Optional from components.klipper import ( @@ -44,7 +43,7 @@ from utils.common import get_install_status_common from utils.constants import CURRENT_USER from utils.git_utils import get_repo_name, get_remote_commit, get_local_commit from utils.input_utils import get_confirm, get_string_input, get_number_input -from utils.logger import Logger +from utils.logger import Logger, DialogType from utils.sys_utils import cmd_sysctl_service @@ -98,8 +97,9 @@ def update_name_scheme( klipper_instances: List[Klipper], moonraker_instances: List[Moonraker], ) -> NameScheme: - # if there are more moonraker instances installed than klipper, we - # load their names into the name_dict, as we will detect and enforce that naming scheme + # if there are more moonraker instances installed + # than klipper, we load their names into the name_dict, + # as we will detect and enforce that naming scheme if len(moonraker_instances) > len(klipper_instances): update_name_dict(name_dict, moonraker_instances) return detect_name_scheme(moonraker_instances) @@ -141,7 +141,11 @@ def get_install_count() -> Union[int, None]: """ kl_instances = InstanceManager(Klipper).instances print_select_instance_count_dialog() - question = f"Number of{' additional' if len(kl_instances) > 0 else ''} Klipper instances to set up" + question = ( + f"Number of" + f"{' additional' if len(kl_instances) > 0 else ''} " + f"Klipper instances to set up" + ) return get_number_input(question, 1, default=1, allow_go_back=True) @@ -193,7 +197,8 @@ def klipper_to_multi_conversion(new_name: str) -> None: else: Logger.print_info(f"Existing '{new_instance.data_dir}' found ...") - # patch the virtual_sdcard sections path value to match the new printer_data foldername + # patch the virtual_sdcard sections path + # value to match the new printer_data foldername cm = ConfigManager(new_instance.cfg_file) if cm.config.has_section("virtual_sdcard"): cm.set_value("virtual_sdcard", "path", str(new_instance.gcodes_dir)) @@ -260,15 +265,15 @@ def handle_disruptive_system_packages() -> None: try: cmd_sysctl_service(service, "mask") except subprocess.CalledProcessError: - # todo: replace with Logger.print_dialog - warn_msg = textwrap.dedent( - f""" - KIAUH was unable to mask the {service} system service. - Please fix the problem manually. Otherwise, this may have - undesirable effects on the operation of Klipper. - """ - )[1:] - Logger.print_warn(warn_msg) + Logger.print_dialog( + DialogType.WARNING, + [ + f"KIAUH was unable to mask the {service} system service. " + "Please fix the problem manually. Otherwise, this may have " + "undesirable effects on the operation of Klipper." + ], + end="", + ) def detect_name_scheme(instance_list: List[BaseInstance]) -> NameScheme: From fc8fedc9f6fc014f5e7e64179c5f216eb949a3fb Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 May 2024 22:49:26 +0200 Subject: [PATCH 223/296] refactor(Klipper): change subprocess imports Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 33 +++++++++++------------ 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index be6ede4..691c790 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -11,21 +11,21 @@ import grp import os import re import shutil -import subprocess -from typing import List, Union, Literal, Dict, Optional +from subprocess import CalledProcessError, run +from typing import Dict, List, Literal, Optional, Union from components.klipper import ( - MODULE_PATH, + KLIPPER_BACKUP_DIR, KLIPPER_DIR, KLIPPER_ENV_DIR, - KLIPPER_BACKUP_DIR, + MODULE_PATH, ) from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import ( - print_missing_usergroup_dialog, print_instance_overview, - print_select_instance_count_dialog, + print_missing_usergroup_dialog, print_select_custom_name_dialog, + print_select_instance_count_dialog, ) from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker_utils import moonraker_to_multi_conversion @@ -41,9 +41,9 @@ from core.instance_manager.name_scheme import NameScheme from utils import PRINTER_CFG_BACKUP_DIR from utils.common import get_install_status_common from utils.constants import CURRENT_USER -from utils.git_utils import get_repo_name, get_remote_commit, get_local_commit -from utils.input_utils import get_confirm, get_string_input, get_number_input -from utils.logger import Logger, DialogType +from utils.git_utils import get_local_commit, get_remote_commit, get_repo_name +from utils.input_utils import get_confirm, get_number_input, get_string_input +from utils.logger import DialogType, Logger from utils.sys_utils import cmd_sysctl_service @@ -232,9 +232,9 @@ def check_user_groups(): for group in missing_groups: Logger.print_status(f"Adding user '{CURRENT_USER}' to group {group} ...") command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER] - subprocess.run(command, check=True) + run(command, check=True) Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.") - except subprocess.CalledProcessError as e: + except CalledProcessError as e: Logger.print_error(f"Unable to add user to usergroups: {e}") raise @@ -246,13 +246,13 @@ def handle_disruptive_system_packages() -> None: services = [] command = ["systemctl", "is-enabled", "brltty"] - brltty_status = subprocess.run(command, capture_output=True, text=True) + brltty_status = run(command, capture_output=True, text=True) command = ["systemctl", "is-enabled", "brltty-udev"] - brltty_udev_status = subprocess.run(command, capture_output=True, text=True) + brltty_udev_status = run(command, capture_output=True, text=True) command = ["systemctl", "is-enabled", "ModemManager"] - modem_manager_status = subprocess.run(command, capture_output=True, text=True) + modem_manager_status = run(command, capture_output=True, text=True) if "enabled" in brltty_status.stdout: services.append("brltty") @@ -264,7 +264,7 @@ def handle_disruptive_system_packages() -> None: for service in services if services else []: try: cmd_sysctl_service(service, "mask") - except subprocess.CalledProcessError: + except CalledProcessError: Logger.print_dialog( DialogType.WARNING, [ @@ -286,8 +286,7 @@ def detect_name_scheme(instance_list: List[BaseInstance]) -> NameScheme: def get_highest_index(instance_list: List[Klipper]) -> int: - indices = [int(instance.suffix.split("-")[-1]) for instance in instance_list] - return max(indices) + return max([int(instance.suffix.split("-")[-1]) for instance in instance_list]) def create_example_printer_cfg( From a03e943ebfba2b79e492b540d15a680119e0e140 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 May 2024 22:52:25 +0200 Subject: [PATCH 224/296] chore: check import sorting when linting Signed-off-by: Dominik Willner --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 364b1a6..26204eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,3 +17,5 @@ indent-style = "space" line-ending = "lf" quote-style = "double" +[tool.ruff.lint] +extend-select = ["I"] From 51993e367d4b4aa313eccb7fc24f0394aacfb3b2 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 May 2024 23:07:54 +0200 Subject: [PATCH 225/296] chore: cleanup settings_menu Signed-off-by: Dominik Willner --- kiauh/core/menus/settings_menu.py | 102 +++++++++++++++--------------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 786f3ed..8870022 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -9,7 +9,7 @@ import shutil import textwrap from pathlib import Path -from typing import Type, Optional, Tuple +from typing import Optional, Tuple, Type from components.klipper import KLIPPER_DIR from components.klipper.klipper import Klipper @@ -19,10 +19,10 @@ from core.instance_manager.instance_manager import InstanceManager from core.menus import Option from core.menus.base_menu import BaseMenu from core.settings.kiauh_settings import KiauhSettings -from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_GREEN, COLOR_YELLOW +from utils.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT from utils.git_utils import git_clone_wrapper -from utils.input_utils import get_string_input, get_confirm -from utils.logger import Logger +from utils.input_utils import get_confirm, get_string_input +from utils.logger import DialogType, Logger # noinspection PyUnusedLocal @@ -88,7 +88,7 @@ class SettingsMenu(BaseMenu): | 3) Toggle unstable Mainsail releases | | 4) Toggle unstable Fluidd releases | | | - | 5) Toggle automatic backups before updates | + | 5) Toggle automatic backups before updates | """ )[1:] print(menu, end="") @@ -100,12 +100,17 @@ class SettingsMenu(BaseMenu): self._format_repo_str("moonraker") self.auto_backups_enabled = self.kiauh_settings.get( - "kiauh", "backup_before_update" + "kiauh", + "backup_before_update", ) self.mainsail_unstable = self.kiauh_settings.get( - "mainsail", "unstable_releases" + "mainsail", + "unstable_releases", + ) + self.fluidd_unstable = self.kiauh_settings.get( + "fluidd", + "unstable_releases", ) - self.fluidd_unstable = self.kiauh_settings.get("fluidd", "unstable_releases") def _format_repo_str(self, repo_name: str) -> None: repo = self.kiauh_settings.get(repo_name, "repo_url") @@ -115,20 +120,16 @@ class SettingsMenu(BaseMenu): setattr(self, f"{repo_name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT} {branch}") def _gather_input(self) -> Tuple[str, str]: - l2 = "Make sure your input is correct!" - error = textwrap.dedent( - f"""\n - {COLOR_YELLOW}/=======================================================\\ - | ATTENTION: | - | There is no input validation in place! Make sure your | - | input is valid and has no typos! For any change to | - | take effect, the repository must be cloned again. | - | Make sure you don't have any ongoing prints running, | - | as the services will be restarted! | - \=======================================================/{RESET_FORMAT} - """ - )[1:] - print(error, end="\n") + Logger.print_dialog( + DialogType.ATTENTION, + [ + "There is no input validation in place! Make sure your" + " input is valid and has no typos! For any change to" + " take effect, the repository must be cloned again. " + "Make sure you don't have any ongoing prints running, " + "as the services will be restarted!" + ], + ) repo = get_string_input( "Enter new repository URL", allow_special_chars=True, @@ -140,44 +141,35 @@ class SettingsMenu(BaseMenu): return repo, branch - def _display_summary(self, name: str, repo: str, branch: str): - l1 = f"New {name} repository URL:" - l2 = f"● {repo}" - l3 = f"New {name} repository branch:" - l4 = f"● {branch}" - summary = textwrap.dedent( - f"""\n - /=======================================================\\ - | {l1:<52} | - | {l2:<52} | - | {l3:<52} | - | {l4:<52} | - \=======================================================/ - """ - )[1:] - print(summary, end="") - def _set_repo(self, repo_name: str): - repo, branch = self._gather_input() - self._display_summary(repo_name.capitalize(), repo, branch) + repo_url, branch = self._gather_input() + display_name = repo_name.capitalize() + Logger.print_dialog( + DialogType.CUSTOM, + [ + f"New {display_name} repository URL:", + f"● {repo_url}", + f"New {display_name} repository branch:", + f"● {branch}", + ], + end="", + ) if get_confirm("Apply changes?", allow_go_back=True): - self.kiauh_settings.set(repo_name, "repo_url", repo) + self.kiauh_settings.set(repo_name, "repo_url", repo_url) self.kiauh_settings.set(repo_name, "branch", branch) self.kiauh_settings.save() self._load_settings() Logger.print_ok("Changes saved!") else: Logger.print_info( - f"Skipping change of {repo_name.capitalize()} source repository ..." + f"Skipping change of {display_name} source repository ..." ) return - Logger.print_status( - f"Switching to {repo_name.capitalize()}'s new source repository ..." - ) + Logger.print_status(f"Switching to {display_name}'s new source repository ...") self._switch_repo(repo_name) - Logger.print_ok(f"Switched to {repo} at branch {branch}!") + Logger.print_ok(f"Switched to {repo_url} at branch {branch}!") def _switch_repo(self, name: str) -> None: target_dir: Path @@ -211,17 +203,27 @@ class SettingsMenu(BaseMenu): def toggle_mainsail_release(self, **kwargs): self.mainsail_unstable = not self.mainsail_unstable - self.kiauh_settings.set("mainsail", "unstable_releases", self.mainsail_unstable) + self.kiauh_settings.set( + "mainsail", + "unstable_releases", + self.mainsail_unstable, + ) self.kiauh_settings.save() def toggle_fluidd_release(self, **kwargs): self.fluidd_unstable = not self.fluidd_unstable - self.kiauh_settings.set("fluidd", "unstable_releases", self.fluidd_unstable) + self.kiauh_settings.set( + "fluidd", + "unstable_releases", + self.fluidd_unstable, + ) self.kiauh_settings.save() def toggle_backup_before_update(self, **kwargs): self.auto_backups_enabled = not self.auto_backups_enabled self.kiauh_settings.set( - "kiauh", "backup_before_update", self.auto_backups_enabled + "kiauh", + "backup_before_update", + self.auto_backups_enabled, ) self.kiauh_settings.save() From e28869be1a0f21a86ac696f3ba78b117df85d394 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 18 May 2024 22:39:17 +0200 Subject: [PATCH 226/296] fix(mobileraker): remove copy paste error Signed-off-by: Dominik Willner --- kiauh/components/mobileraker/mobileraker.py | 22 ++++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py index a432653..f08ad05 100644 --- a/kiauh/components/mobileraker/mobileraker.py +++ b/kiauh/components/mobileraker/mobileraker.py @@ -8,38 +8,38 @@ # ======================================================================= # import shutil from pathlib import Path -from subprocess import run, CalledProcessError -from typing import List, Dict, Literal, Union +from subprocess import CalledProcessError, run +from typing import Dict, List, Literal, Union from components.klipper.klipper import Klipper from components.mobileraker import ( - MOBILERAKER_REPO, + MOBILERAKER_BACKUP_DIR, MOBILERAKER_DIR, MOBILERAKER_ENV, - MOBILERAKER_BACKUP_DIR, + MOBILERAKER_REPO, ) from components.moonraker.moonraker import Moonraker from core.backup_manager.backup_manager import BackupManager from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings -from utils.common import get_install_status, check_install_dependencies +from utils.common import check_install_dependencies, get_install_status from utils.config_utils import add_config_section, remove_config_section from utils.constants import SYSTEMD from utils.fs_utils import remove_with_sudo from utils.git_utils import ( - git_clone_wrapper, - git_pull_wrapper, - get_repo_name, get_local_commit, get_remote_commit, + get_repo_name, + git_clone_wrapper, + git_pull_wrapper, ) from utils.input_utils import get_confirm -from utils.logger import Logger, DialogType +from utils.logger import DialogType, Logger from utils.sys_utils import ( check_python_version, + cmd_sysctl_manage, cmd_sysctl_service, install_python_requirements, - cmd_sysctl_manage, ) @@ -111,8 +111,6 @@ def patch_mobileraker_update_manager(instances: List[Moonraker]) -> None: def update_mobileraker() -> None: try: - cmd_sysctl_service("KlipperScreen", "stop") - if not MOBILERAKER_DIR.exists(): Logger.print_info( "Mobileraker's companion does not seem to be installed! Skipping ..." From d6317ad439d8b6020c669497197a86a92e10395f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 19 May 2024 16:05:31 +0200 Subject: [PATCH 227/296] chore: cleanup moonraker telegram bot Signed-off-by: Dominik Willner --- .../telegram_bot/moonraker_telegram_bot.py | 13 ++++++------- .../moonraker_telegram_bot_extension.py | 13 +++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py b/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py index 316b01c..9e5c3fe 100644 --- a/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py +++ b/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py @@ -7,15 +7,14 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import subprocess from pathlib import Path +from subprocess import DEVNULL, CalledProcessError, run from typing import List from core.instance_manager.base_instance import BaseInstance from utils.constants import SYSTEMD from utils.logger import Logger - MODULE_PATH = Path(__file__).resolve().parent TELEGRAM_BOT_DIR = Path.home().joinpath("moonraker-telegram-bot") @@ -64,7 +63,7 @@ class MoonrakerTelegramBot(BaseInstance): ) self.write_env_file(env_template_file_path, env_file_target) - except subprocess.CalledProcessError as e: + except CalledProcessError as e: Logger.print_error( f"Error creating service file {service_file_target}: {e}" ) @@ -81,9 +80,9 @@ class MoonrakerTelegramBot(BaseInstance): try: command = ["sudo", "rm", "-f", service_file_path] - subprocess.run(command, check=True) + run(command, check=True) Logger.print_ok(f"Service file deleted: {service_file_path}") - except subprocess.CalledProcessError as e: + except CalledProcessError as e: Logger.print_error(f"Error deleting service file: {e}") raise @@ -97,10 +96,10 @@ class MoonrakerTelegramBot(BaseInstance): service_template_path, env_file_target ) command = ["sudo", "tee", service_file_target] - subprocess.run( + run( command, input=service_content.encode(), - stdout=subprocess.DEVNULL, + stdout=DEVNULL, check=True, ) Logger.print_ok(f"Service file created: {service_file_target}") diff --git a/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py b/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py index 90143c2..e6c5e74 100644 --- a/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py +++ b/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py @@ -14,22 +14,22 @@ from components.moonraker.moonraker import Moonraker from core.instance_manager.instance_manager import InstanceManager from extensions.base_extension import BaseExtension from extensions.telegram_bot.moonraker_telegram_bot import ( - MoonrakerTelegramBot, - TELEGRAM_BOT_REPO, TELEGRAM_BOT_DIR, TELEGRAM_BOT_ENV, + TELEGRAM_BOT_REPO, + MoonrakerTelegramBot, ) from utils.common import check_install_dependencies from utils.config_utils import add_config_section, remove_config_section from utils.fs_utils import remove_file from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm -from utils.logger import Logger, DialogType +from utils.logger import DialogType, Logger from utils.sys_utils import ( - parse_packages_from_file, + cmd_sysctl_manage, create_python_venv, install_python_requirements, - cmd_sysctl_manage, + parse_packages_from_file, ) @@ -44,7 +44,8 @@ class TelegramBotExtension(BaseExtension): DialogType.WARNING, [ "No Moonraker instances found!", - "Moonraker Telegram Bot requires Moonraker to be installed. Please install Moonraker first!", + "Moonraker Telegram Bot requires Moonraker to be installed. " + "Please install Moonraker first!", ], ) return From 6eb06772b4d384f5e1eb2d9391029b1f03726536 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 19 May 2024 18:39:31 +0200 Subject: [PATCH 228/296] fix(utils): fix condition Signed-off-by: Dominik Willner --- kiauh/utils/git_utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py index 3ccb0d1..d06a2d8 100644 --- a/kiauh/utils/git_utils.py +++ b/kiauh/utils/git_utils.py @@ -4,12 +4,12 @@ import urllib.request from http.client import HTTPResponse from json import JSONDecodeError from pathlib import Path -from subprocess import CalledProcessError, PIPE, run, check_output, DEVNULL -from typing import List, Type, Optional +from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run +from typing import List, Optional, Type from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager -from utils.input_utils import get_number_input, get_confirm +from utils.input_utils import get_confirm, get_number_input from utils.logger import Logger @@ -68,7 +68,7 @@ def get_repo_name(repo: Path) -> str: :param repo: repository to extract the values from :return: String in form of "/" """ - if not repo.exists() and not repo.joinpath(".git").exists(): + if not repo.exists() or not repo.joinpath(".git").exists(): return "-" try: @@ -130,7 +130,7 @@ def get_latest_unstable_tag(repo_path: str) -> str: def get_local_commit(repo: Path) -> str: - if not repo.exists() and not repo.joinpath(".git").exists(): + if not repo.exists() or not repo.joinpath(".git").exists(): return "-" try: @@ -141,7 +141,7 @@ def get_local_commit(repo: Path) -> str: def get_remote_commit(repo: Path) -> str: - if not repo.exists() and not repo.joinpath(".git").exists(): + if not repo.exists() or not repo.joinpath(".git").exists(): return "-" try: From ac0478b062a07df758a9aae13aab93f094cf6dff Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 19 May 2024 18:41:29 +0200 Subject: [PATCH 229/296] refactor: more robust type hinting Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 27 ++--- kiauh/components/klipper/klipper_utils.py | 23 +--- .../components/klipperscreen/klipperscreen.py | 44 +++---- kiauh/components/mobileraker/mobileraker.py | 27 +---- kiauh/components/moonraker/moonraker_utils.py | 27 ++--- kiauh/components/webui_client/client_utils.py | 52 ++++----- kiauh/core/menus/main_menu.py | 52 +++++---- kiauh/core/menus/update_menu.py | 13 ++- kiauh/utils/common.py | 107 +++++++----------- kiauh/utils/types.py | 30 +++++ 10 files changed, 167 insertions(+), 235 deletions(-) create mode 100644 kiauh/utils/types.py diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index f6a0e09..7d4b789 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -12,19 +12,19 @@ import shutil import time from pathlib import Path from subprocess import CalledProcessError, run -from typing import Dict, List, Literal, Union +from typing import List from components.crowsnest import CROWSNEST_BACKUP_DIR, CROWSNEST_DIR, CROWSNEST_REPO from components.klipper.klipper import Klipper from core.backup_manager.backup_manager import BackupManager from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings -from utils.common import check_install_dependencies, get_install_status +from utils.common import ( + check_install_dependencies, + get_install_status, +) from utils.constants import CURRENT_USER from utils.git_utils import ( - get_local_commit, - get_remote_commit, - get_repo_name, git_clone_wrapper, git_pull_wrapper, ) @@ -34,6 +34,7 @@ from utils.sys_utils import ( cmd_sysctl_service, parse_packages_from_file, ) +from utils.types import ComponentStatus def install_crowsnest() -> None: @@ -142,25 +143,13 @@ def update_crowsnest() -> None: return -def get_crowsnest_status() -> ( - Dict[ - Literal["status", "status_code", "repo", "local", "remote"], - Union[str, int], - ] -): +def get_crowsnest_status() -> ComponentStatus: files = [ Path("/usr/local/bin/crowsnest"), Path("/etc/logrotate.d/crowsnest"), Path("/etc/systemd/system/crowsnest.service"), ] - status = get_install_status(CROWSNEST_DIR, files) - return { - "status": status.get("status"), - "status_code": status.get("status_code"), - "repo": get_repo_name(CROWSNEST_DIR), - "local": get_local_commit(CROWSNEST_DIR), - "remote": get_remote_commit(CROWSNEST_DIR), - } + return get_install_status(CROWSNEST_DIR, files=files) def remove_crowsnest() -> None: diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 691c790..2689951 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -12,7 +12,7 @@ import os import re import shutil from subprocess import CalledProcessError, run -from typing import Dict, List, Literal, Optional, Union +from typing import Dict, List, Optional, Union from components.klipper import ( KLIPPER_BACKUP_DIR, @@ -39,29 +39,16 @@ from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.name_scheme import NameScheme from utils import PRINTER_CFG_BACKUP_DIR -from utils.common import get_install_status_common +from utils.common import get_install_status from utils.constants import CURRENT_USER -from utils.git_utils import get_local_commit, get_remote_commit, get_repo_name from utils.input_utils import get_confirm, get_number_input, get_string_input from utils.logger import DialogType, Logger from utils.sys_utils import cmd_sysctl_service +from utils.types import ComponentStatus -def get_klipper_status() -> ( - Dict[ - Literal["status", "status_code", "instances", "repo", "local", "remote"], - Union[str, int], - ] -): - status = get_install_status_common(Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR) - return { - "status": status.get("status"), - "status_code": status.get("status_code"), - "instances": status.get("instances"), - "repo": get_repo_name(KLIPPER_DIR), - "local": get_local_commit(KLIPPER_DIR), - "remote": get_remote_commit(KLIPPER_DIR), - } +def get_klipper_status() -> ComponentStatus: + return get_install_status(KLIPPER_DIR, KLIPPER_ENV_DIR, Klipper) def check_is_multi_install( diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index 87dcc03..1b2ce89 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -8,39 +8,40 @@ # ======================================================================= # import shutil from pathlib import Path -from subprocess import run, CalledProcessError -from typing import List, Dict, Literal, Union +from subprocess import CalledProcessError, run +from typing import List from components.klipper.klipper import Klipper from components.klipperscreen import ( - KLIPPERSCREEN_DIR, - KLIPPERSCREEN_REPO, - KLIPPERSCREEN_ENV, KLIPPERSCREEN_BACKUP_DIR, + KLIPPERSCREEN_DIR, + KLIPPERSCREEN_ENV, + KLIPPERSCREEN_REPO, ) from components.moonraker.moonraker import Moonraker from core.backup_manager.backup_manager import BackupManager from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings -from utils.common import get_install_status, check_install_dependencies +from utils.common import ( + check_install_dependencies, + get_install_status, +) from utils.config_utils import add_config_section, remove_config_section from utils.constants import SYSTEMD from utils.fs_utils import remove_with_sudo from utils.git_utils import ( git_clone_wrapper, git_pull_wrapper, - get_repo_name, - get_local_commit, - get_remote_commit, ) from utils.input_utils import get_confirm -from utils.logger import Logger, DialogType +from utils.logger import DialogType, Logger from utils.sys_utils import ( check_python_version, + cmd_sysctl_manage, cmd_sysctl_service, install_python_requirements, - cmd_sysctl_manage, ) +from utils.types import ComponentStatus def install_klipperscreen() -> None: @@ -141,25 +142,12 @@ def update_klipperscreen() -> None: return -def get_klipperscreen_status() -> ( - Dict[ - Literal["status", "status_code", "repo", "local", "remote"], - Union[str, int], - ] -): - files = [ +def get_klipperscreen_status() -> ComponentStatus: + return get_install_status( KLIPPERSCREEN_DIR, KLIPPERSCREEN_ENV, - SYSTEMD.joinpath("KlipperScreen.service"), - ] - status = get_install_status(KLIPPERSCREEN_DIR, files) - return { - "status": status.get("status"), - "status_code": status.get("status_code"), - "repo": get_repo_name(KLIPPERSCREEN_DIR), - "local": get_local_commit(KLIPPERSCREEN_DIR), - "remote": get_remote_commit(KLIPPERSCREEN_DIR), - } + files=[SYSTEMD.joinpath("KlipperScreen.service")], + ) def remove_klipperscreen() -> None: diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py index f08ad05..5f911de 100644 --- a/kiauh/components/mobileraker/mobileraker.py +++ b/kiauh/components/mobileraker/mobileraker.py @@ -9,7 +9,7 @@ import shutil from pathlib import Path from subprocess import CalledProcessError, run -from typing import Dict, List, Literal, Union +from typing import List from components.klipper.klipper import Klipper from components.mobileraker import ( @@ -27,9 +27,6 @@ from utils.config_utils import add_config_section, remove_config_section from utils.constants import SYSTEMD from utils.fs_utils import remove_with_sudo from utils.git_utils import ( - get_local_commit, - get_remote_commit, - get_repo_name, git_clone_wrapper, git_pull_wrapper, ) @@ -41,6 +38,7 @@ from utils.sys_utils import ( cmd_sysctl_service, install_python_requirements, ) +from utils.types import ComponentStatus def install_mobileraker() -> None: @@ -138,25 +136,12 @@ def update_mobileraker() -> None: return -def get_mobileraker_status() -> ( - Dict[ - Literal["status", "status_code", "repo", "local", "remote"], - Union[str, int], - ] -): - files = [ +def get_mobileraker_status() -> ComponentStatus: + return get_install_status( MOBILERAKER_DIR, MOBILERAKER_ENV, - SYSTEMD.joinpath("mobileraker.service"), - ] - status = get_install_status(MOBILERAKER_DIR, files) - return { - "status": status.get("status"), - "status_code": status.get("status_code"), - "repo": get_repo_name(MOBILERAKER_DIR), - "local": get_local_commit(MOBILERAKER_DIR), - "remote": get_remote_commit(MOBILERAKER_DIR), - } + files=[SYSTEMD.joinpath("mobileraker.service")], + ) def remove_mobileraker() -> None: diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 966d58a..7312bbd 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -8,15 +8,15 @@ # ======================================================================= # import shutil -from typing import Dict, Literal, List, Union, Optional +from typing import Dict, List, Optional from components.moonraker import ( DEFAULT_MOONRAKER_PORT, MODULE_PATH, - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, MOONRAKER_BACKUP_DIR, MOONRAKER_DB_BACKUP_DIR, + MOONRAKER_DIR, + MOONRAKER_ENV_DIR, ) from components.moonraker.moonraker import Moonraker from components.webui_client.base_data import BaseWebClient @@ -25,29 +25,16 @@ from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager -from utils.common import get_install_status_common -from utils.git_utils import get_repo_name, get_local_commit, get_remote_commit +from utils.common import get_install_status from utils.logger import Logger from utils.sys_utils import ( get_ipv4_addr, ) +from utils.types import ComponentStatus -def get_moonraker_status() -> ( - Dict[ - Literal["status", "status_code", "instances", "repo", "local", "remote"], - Union[str, int], - ] -): - status = get_install_status_common(Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR) - return { - "status": status.get("status"), - "status_code": status.get("status_code"), - "instances": status.get("instances"), - "repo": get_repo_name(MOONRAKER_DIR), - "local": get_local_commit(MOONRAKER_DIR), - "remote": get_remote_commit(MOONRAKER_DIR), - } +def get_moonraker_status() -> ComponentStatus: + return get_install_status(MOONRAKER_DIR, MOONRAKER_ENV_DIR, Moonraker) def create_example_moonraker_conf( diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index f6c2746..7b42ae2 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -7,61 +7,53 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import json +import json # noqa: I001 import shutil from pathlib import Path -from typing import List, Dict, Literal, Union, get_args - +from typing import List, get_args from components.klipper.klipper import Klipper from components.webui_client.base_data import ( - WebClientType, BaseWebClient, BaseWebClientConfig, + WebClientType, ) from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager from core.settings.kiauh_settings import KiauhSettings -from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD -from utils.common import get_install_status_webui -from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW +from utils import NGINX_CONFD, NGINX_SITES_AVAILABLE +from utils.common import get_install_status +from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT from utils.git_utils import ( get_latest_tag, get_latest_unstable_tag, - get_repo_name, - get_local_commit, - get_remote_commit, ) from utils.logger import Logger +from utils.types import ComponentStatus, InstallStatus def get_client_status( client: BaseWebClient, fetch_remote: bool = False -) -> Dict[Literal["status", "local", "remote"], str]: - status = get_install_status_webui( - client.client_dir, +) -> ComponentStatus: + files = [ NGINX_SITES_AVAILABLE.joinpath(client.name), NGINX_CONFD.joinpath("upstreams.conf"), NGINX_CONFD.joinpath("common_vars.conf"), - ) - local = get_local_client_version(client) - remote = get_remote_client_version(client) if fetch_remote else None - return {"status": status, "local": local, "remote": remote} + ] + status = get_install_status(client.client_dir, files=files) + + # if the client dir does not exist, set the status to not + # installed even if the other files are present + if not client.client_dir.exists(): + status["status"] = InstallStatus.NOT_INSTALLED + + status["local"] = get_local_client_version(client) + status["remote"] = get_remote_client_version(client) if fetch_remote else None + return status -def get_client_config_status( - client: BaseWebClient, -) -> Dict[ - Literal["repo", "local", "remote"], - Union[str, int], -]: - client_config = client.client_config.config_dir - - return { - "repo": get_repo_name(client_config), - "local": get_local_commit(client_config), - "remote": get_remote_commit(client_config), - } +def get_client_config_status(client: BaseWebClient) -> ComponentStatus: + return get_install_status(client.client_config.config_dir) def get_current_client_config(clients: List[BaseWebClient]) -> str: diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 98bb916..9fef36a 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -8,7 +8,7 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.crowsnest.crowsnest import get_crowsnest_status from components.klipper.klipper_utils import get_klipper_status @@ -26,19 +26,20 @@ from core.menus import FooterType from core.menus.advanced_menu import AdvancedMenu from core.menus.backup_menu import BackupMenu from core.menus.base_menu import BaseMenu, Option -from extensions.extensions_menu import ExtensionsMenu from core.menus.install_menu import InstallMenu from core.menus.remove_menu import RemoveMenu from core.menus.settings_menu import SettingsMenu from core.menus.update_menu import UpdateMenu +from extensions.extensions_menu import ExtensionsMenu from utils.constants import ( - COLOR_MAGENTA, COLOR_CYAN, - RESET_FORMAT, - COLOR_RED, COLOR_GREEN, + COLOR_MAGENTA, + COLOR_RED, COLOR_YELLOW, + RESET_FORMAT, ) +from utils.types import ComponentStatus # noinspection PyUnusedLocal @@ -91,18 +92,17 @@ class MainMenu(BaseMenu): self._get_component_status("cn", get_crowsnest_status) def _get_component_status(self, name: str, status_fn: callable, *args) -> None: - status_data = status_fn(*args) - status = status_data.get("status") - code = status_data.get("status_code") - repo = status_data.get("repo") + status_data: ComponentStatus = status_fn(*args) + code: int = status_data.get("status").value.code + status: str = status_data.get("status").value.txt + repo: str = status_data.get("repo") + instance_count: int = status_data.get("instances") - instance_count = status_data.get("instances") + count_txt: str = "" + if instance_count > 0 and code == 1: + count_txt = f": {instance_count}" - count: str = "" - if instance_count and code == 1: - count = f" {instance_count}" - - setattr(self, f"{name}_status", self._format_by_code(code, status, count)) + setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt)) setattr(self, f"{name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT}") def _format_by_code(self, code: int, status: str, count: str) -> str: @@ -121,24 +121,26 @@ class MainMenu(BaseMenu): footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}" color = COLOR_CYAN count = 62 - len(color) - len(RESET_FORMAT) + pad1 = 32 + pad2 = 26 menu = textwrap.dedent( f""" /=======================================================\\ | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| - | 0) [Log-Upload] | Klipper: {self.kl_status:<32} | - | | Repo: {self.kl_repo:<32} | + | 0) [Log-Upload] | Klipper: {self.kl_status:<{pad1}} | + | | Repo: {self.kl_repo:<{pad1}} | | 1) [Install] |------------------------------------| - | 2) [Update] | Moonraker: {self.mr_status:<32} | - | 3) [Remove] | Repo: {self.mr_repo:<32} | + | 2) [Update] | Moonraker: {self.mr_status:<{pad1}} | + | 3) [Remove] | Repo: {self.mr_repo:<{pad1}} | | 4) [Advanced] |------------------------------------| - | 5) [Backup] | Mainsail: {self.ms_status:<35} | - | | Fluidd: {self.fl_status:<35} | - | S) [Settings] | Client-Config: {self.cc_status:<26} | + | 5) [Backup] | Mainsail: {self.ms_status:<{pad2}} | + | | Fluidd: {self.fl_status:<{pad2}} | + | S) [Settings] | Client-Config: {self.cc_status:<{pad2}} | | | | - | Community: | KlipperScreen: {self.ks_status:<26} | - | E) [Extensions] | Mobileraker: {self.mb_status:<26} | - | | Crowsnest: {self.cn_status:<26} | + | Community: | KlipperScreen: {self.ks_status:<{pad2}} | + | E) [Extensions] | Mobileraker: {self.mb_status:<{pad2}} | + | | Crowsnest: {self.cn_status:<{pad2}} | |-------------------------------------------------------| | {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} | """ diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 7632cf3..2efe153 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -8,7 +8,7 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.crowsnest.crowsnest import get_crowsnest_status, update_crowsnest from components.klipper.klipper_setup import update_klipper @@ -16,12 +16,12 @@ from components.klipper.klipper_utils import ( get_klipper_status, ) from components.klipperscreen.klipperscreen import ( - update_klipperscreen, get_klipperscreen_status, + update_klipperscreen, ) from components.mobileraker.mobileraker import ( - update_mobileraker, get_mobileraker_status, + update_mobileraker, ) from components.moonraker.moonraker_setup import update_moonraker from components.moonraker.moonraker_utils import get_moonraker_status @@ -39,10 +39,11 @@ from core.menus import Option from core.menus.base_menu import BaseMenu from utils.constants import ( COLOR_GREEN, - RESET_FORMAT, - COLOR_YELLOW, COLOR_RED, + COLOR_YELLOW, + RESET_FORMAT, ) +from utils.types import ComponentStatus # noinspection PyUnusedLocal @@ -177,7 +178,7 @@ class UpdateMenu(BaseMenu): return f"{COLOR_YELLOW}{local_version}{RESET_FORMAT}" def _get_update_status(self, name: str, status_fn: callable, *args) -> None: - status_data = status_fn(*args) + status_data: ComponentStatus = status_fn(*args) local_ver = status_data.get("local") remote_ver = status_data.get("remote") color = COLOR_GREEN if remote_ver != "ERROR" else COLOR_RED diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 1d567a3..8ca8051 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -9,7 +9,7 @@ import re from datetime import datetime from pathlib import Path -from typing import Dict, Literal, List, Type, Union +from typing import Dict, List, Literal, Optional, Type from components.klipper.klipper import Klipper from core.instance_manager.base_instance import BaseInstance @@ -18,16 +18,15 @@ from utils import PRINTER_CFG_BACKUP_DIR from utils.constants import ( COLOR_CYAN, RESET_FORMAT, - COLOR_YELLOW, - COLOR_GREEN, - COLOR_RED, ) +from utils.git_utils import get_local_commit, get_remote_commit, get_repo_name from utils.logger import Logger from utils.sys_utils import ( check_package_install, install_system_packages, update_system_package_lists, ) +from utils.types import ComponentStatus, InstallStatus def convert_camelcase_to_kebabcase(name: str) -> str: @@ -64,78 +63,50 @@ def check_install_dependencies(deps: List[str]) -> None: def get_install_status( - repo_dir: Path, opt_files: List[Path] -) -> Dict[Literal["status", "status_code", "instances"], Union[str, int]]: - status = [repo_dir.exists()] - - for f in opt_files: - status.append(f.exists()) - - if all(status): - return {"status": "Installed!", "status_code": 1} - elif not any(status): - return {"status": "Not installed!", "status_code": 2} - else: - return {"status": "Incomplete!", "status_code": 3} - - -def get_install_status_common( - instance_type: Type[BaseInstance], repo_dir: Path, env_dir: Path -) -> Dict[Literal["status", "status_code", "instances"], Union[str, int]]: + repo_dir: Path, + env_dir: Optional[Path] = None, + instance_type: Optional[Type[BaseInstance]] = None, + files: Optional[List[Path]] = None, +) -> ComponentStatus: """ - Helper method to get the installation status of software components, - which only consist of 3 major parts and if those parts exist, the - component can be considered as "installed". Typically, Klipper or - Moonraker match that criteria. - :param instance_type: The component type + Helper method to get the installation status of software components :param repo_dir: the repository directory :param env_dir: the python environment directory + :param instance_type: The component type + :param files: List of optional files to check for existence :return: Dictionary with status string, statuscode and instance count """ + checks = [repo_dir.exists()] + + if env_dir is not None: + checks.append(env_dir.exists()) + im = InstanceManager(instance_type) - instances_exist = len(im.instances) > 0 - status = [repo_dir.exists(), env_dir.exists(), instances_exist] - if all(status): - return { - "status": "Installed:", - "status_code": 1, - "instances": len(im.instances), - } - elif not any(status): - return { - "status": "Not installed!", - "status_code": 2, - "instances": len(im.instances), - } + instances = 0 + if instance_type is not None: + instances = len(im.instances) + checks.append(instances > 0) + + if files is not None: + for f in files: + checks.append(f.exists()) + + if all(checks): + status = InstallStatus.INSTALLED + + elif not any(checks): + status = InstallStatus.NOT_INSTALLED + else: - return { - "status": "Incomplete!", - "status_code": 3, - "instances": len(im.instances), - } + status = InstallStatus.INCOMPLETE - -def get_install_status_webui( - install_dir: Path, nginx_cfg: Path, upstreams_cfg: Path, common_cfg: Path -) -> str: - """ - Helper method to get the installation status of webuis - like Mainsail or Fluidd | - :param install_dir: folder of the static webui files - :param nginx_cfg: the webuis NGINX config - :param upstreams_cfg: the required upstreams.conf - :param common_cfg: the required common_vars.conf - :return: formatted string, containing the status - """ - status = [install_dir.exists(), nginx_cfg.exists()] - general_nginx_status = [upstreams_cfg.exists(), common_cfg.exists()] - - if all(status) and all(general_nginx_status): - return f"{COLOR_GREEN}Installed!{RESET_FORMAT}" - elif not all(status): - return f"{COLOR_RED}Not installed!{RESET_FORMAT}" - else: - return f"{COLOR_YELLOW}Incomplete!{RESET_FORMAT}" + return ComponentStatus( + status=status, + instances=instances, + repo=get_repo_name(repo_dir), + local=get_local_commit(repo_dir), + remote=get_remote_commit(repo_dir), + ) def backup_printer_config_dir(): diff --git a/kiauh/utils/types.py b/kiauh/utils/types.py new file mode 100644 index 0000000..c49a1c9 --- /dev/null +++ b/kiauh/utils/types.py @@ -0,0 +1,30 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 enum import Enum +from typing import Optional, TypedDict + + +class StatusInfo: + def __init__(self, txt: str, code: int): + self.txt: str = txt + self.code: int = code + + +class InstallStatus(Enum): + INSTALLED = StatusInfo("Installed", 1) + NOT_INSTALLED = StatusInfo("Not installed", 2) + INCOMPLETE = StatusInfo("Incomplete", 3) + + +class ComponentStatus(TypedDict): + status: InstallStatus + repo: Optional[str] + local: Optional[str] + remote: Optional[str] + instances: Optional[int] From 01afe1fe773c6d380fc15996a15df77514f143a9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 20 May 2024 10:52:18 +0200 Subject: [PATCH 230/296] chore: ruff organize imports Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_dialogs.py | 2 +- kiauh/components/klipper/klipper_setup.py | 28 +++++++++---------- .../klipper/menus/klipper_remove_menu.py | 4 +-- .../klipper_firmware/firmware_utils.py | 4 +-- .../klipper_firmware/flash_options.py | 2 +- .../menus/klipper_build_menu.py | 8 +++--- .../menus/klipper_flash_error_menu.py | 2 +- .../menus/klipper_flash_help_menu.py | 4 +-- .../menus/klipper_flash_menu.py | 23 ++++++++------- kiauh/components/log_uploads/__init__.py | 2 +- .../log_uploads/menus/log_upload_menu.py | 7 ++--- .../moonraker/menus/moonraker_remove_menu.py | 4 +-- kiauh/components/moonraker/moonraker.py | 2 +- .../components/moonraker/moonraker_dialogs.py | 2 +- kiauh/components/moonraker/moonraker_setup.py | 22 +++++++-------- .../client_config/client_config_setup.py | 5 ++-- .../components/webui_client/client_dialogs.py | 2 +- .../components/webui_client/client_remove.py | 1 - kiauh/components/webui_client/client_setup.py | 27 +++++++++--------- kiauh/components/webui_client/fluidd_data.py | 2 +- .../components/webui_client/mainsail_data.py | 2 +- .../webui_client/menus/client_remove_menu.py | 4 +-- kiauh/core/instance_manager/base_instance.py | 5 ++-- .../core/instance_manager/instance_manager.py | 2 +- kiauh/core/instance_manager/name_scheme.py | 2 +- kiauh/core/menus/__init__.py | 2 +- kiauh/core/menus/advanced_menu.py | 2 +- kiauh/core/menus/backup_menu.py | 8 +++--- kiauh/core/menus/base_menu.py | 8 +++--- kiauh/core/menus/install_menu.py | 3 +- kiauh/core/menus/remove_menu.py | 2 +- kiauh/core/settings/kiauh_settings.py | 7 +++-- kiauh/extensions/base_extension.py | 2 +- kiauh/extensions/extensions_menu.py | 6 ++-- .../gcode_shell_cmd_extension.py | 8 +++--- .../klipper_backup_extension.py | 5 ++-- .../mainsail_theme_installer_extension.py | 13 ++++----- kiauh/utils/config_utils.py | 2 +- kiauh/utils/fs_utils.py | 7 ++--- kiauh/utils/logger.py | 12 ++++---- kiauh/utils/sys_utils.py | 6 ++-- 41 files changed, 126 insertions(+), 135 deletions(-) diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py index 9964f66..7218c43 100644 --- a/kiauh/components/klipper/klipper_dialogs.py +++ b/kiauh/components/klipper/klipper_dialogs.py @@ -13,7 +13,7 @@ from typing import List from core.instance_manager.base_instance import BaseInstance from core.menus.base_menu import print_back_footer -from utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN +from utils.constants import COLOR_CYAN, COLOR_GREEN, COLOR_YELLOW, RESET_FORMAT @unique diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 1745d25..21e013a 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -9,10 +9,6 @@ from pathlib import Path -from components.webui_client.client_utils import ( - get_existing_clients, -) -from core.settings.kiauh_settings import KiauhSettings from components.klipper import ( EXIT_KLIPPER_SETUP, KLIPPER_DIR, @@ -22,28 +18,32 @@ from components.klipper import ( from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import print_update_warn_dialog from components.klipper.klipper_utils import ( - handle_disruptive_system_packages, - check_user_groups, - handle_to_multi_instance_conversion, - create_example_printer_cfg, add_to_existing, - get_install_count, - init_name_scheme, - check_is_single_to_multi_conversion, - update_name_scheme, - handle_instance_naming, backup_klipper_dir, + check_is_single_to_multi_conversion, + check_user_groups, + create_example_printer_cfg, + get_install_count, + handle_disruptive_system_packages, + handle_instance_naming, + handle_to_multi_instance_conversion, + init_name_scheme, + update_name_scheme, ) from components.moonraker.moonraker import Moonraker +from components.webui_client.client_utils import ( + get_existing_clients, +) from core.instance_manager.instance_manager import InstanceManager +from core.settings.kiauh_settings import KiauhSettings from utils.common import check_install_dependencies from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm from utils.logger import Logger from utils.sys_utils import ( - parse_packages_from_file, create_python_venv, install_python_requirements, + parse_packages_from_file, ) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index 1190e98..07e24a3 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -8,12 +8,12 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.klipper import klipper_remove from core.menus import FooterType, Option from core.menus.base_menu import BaseMenu -from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN +from utils.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT # noinspection PyUnusedLocal diff --git a/kiauh/components/klipper_firmware/firmware_utils.py b/kiauh/components/klipper_firmware/firmware_utils.py index b2d33d1..b2ac682 100644 --- a/kiauh/components/klipper_firmware/firmware_utils.py +++ b/kiauh/components/klipper_firmware/firmware_utils.py @@ -7,15 +7,15 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from subprocess import CalledProcessError, check_output, Popen, PIPE, STDOUT, run +from subprocess import PIPE, STDOUT, CalledProcessError, Popen, check_output, run from typing import List from components.klipper import KLIPPER_DIR from components.klipper.klipper import Klipper from components.klipper_firmware import SD_FLASH_SCRIPT from components.klipper_firmware.flash_options import ( - FlashOptions, FlashMethod, + FlashOptions, ) from core.instance_manager.instance_manager import InstanceManager from utils.logger import Logger diff --git a/kiauh/components/klipper_firmware/flash_options.py b/kiauh/components/klipper_firmware/flash_options.py index 47e51f9..775fa9c 100644 --- a/kiauh/components/klipper_firmware/flash_options.py +++ b/kiauh/components/klipper_firmware/flash_options.py @@ -9,7 +9,7 @@ from dataclasses import field from enum import Enum -from typing import Union, List +from typing import List, Union class FlashMethod(Enum): diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py index b4e643e..a9768b9 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -8,22 +8,22 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.klipper import KLIPPER_DIR from components.klipper_firmware.firmware_utils import ( + run_make, run_make_clean, run_make_menuconfig, - run_make, ) from core.menus import Option from core.menus.base_menu import BaseMenu -from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_GREEN, COLOR_RED +from utils.constants import COLOR_CYAN, COLOR_GREEN, COLOR_RED, RESET_FORMAT from utils.logger import Logger from utils.sys_utils import ( check_package_install, - update_system_package_lists, install_system_packages, + update_system_package_lists, ) diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py index 7df7e82..0077d39 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py @@ -9,7 +9,7 @@ import textwrap from typing import Optional, Type -from components.klipper_firmware.flash_options import FlashOptions, FlashMethod +from components.klipper_firmware.flash_options import FlashMethod, FlashOptions from core.menus import FooterType, Option from core.menus.base_menu import BaseMenu from utils.constants import COLOR_RED, RESET_FORMAT diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py index 736d9e9..4cebafd 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py @@ -7,10 +7,10 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from core.menus.base_menu import BaseMenu -from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW +from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT # noinspection DuplicatedCode diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index 455732e..07c1ca5 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -9,35 +9,34 @@ import textwrap import time -from typing import Type, Optional +from typing import Optional, Type -from components.klipper_firmware.flash_options import ( - FlashOptions, - FlashMethod, - FlashCommand, - ConnectionType, -) from components.klipper_firmware.firmware_utils import ( - find_usb_device_by_id, + find_firmware_file, find_uart_device, + find_usb_device_by_id, find_usb_dfu_device, get_sd_flash_board_list, start_flash_process, - find_firmware_file, +) +from components.klipper_firmware.flash_options import ( + ConnectionType, + FlashCommand, + FlashMethod, + FlashOptions, ) from components.klipper_firmware.menus.klipper_flash_error_menu import ( KlipperNoBoardTypesErrorMenu, KlipperNoFirmwareErrorMenu, ) from components.klipper_firmware.menus.klipper_flash_help_menu import ( - KlipperMcuConnectionHelpMenu, KlipperFlashCommandHelpMenu, KlipperFlashMethodHelpMenu, + KlipperMcuConnectionHelpMenu, ) from core.menus import FooterType, Option - from core.menus.base_menu import BaseMenu -from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_RED +from utils.constants import COLOR_CYAN, COLOR_RED, COLOR_YELLOW, RESET_FORMAT from utils.input_utils import get_number_input from utils.logger import Logger diff --git a/kiauh/components/log_uploads/__init__.py b/kiauh/components/log_uploads/__init__.py index 1d87ba8..0303dee 100644 --- a/kiauh/components/log_uploads/__init__.py +++ b/kiauh/components/log_uploads/__init__.py @@ -8,7 +8,7 @@ # ======================================================================= # from pathlib import Path -from typing import Dict, Union, Literal +from typing import Dict, Literal, Union FileKey = Literal["filepath", "display_name"] LogFile = Dict[FileKey, Union[str, Path]] diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index 34f9834..01f1675 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -8,13 +8,12 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type -from components.log_uploads.log_upload_utils import get_logfile_list -from components.log_uploads.log_upload_utils import upload_logfile +from components.log_uploads.log_upload_utils import get_logfile_list, upload_logfile from core.menus import Option from core.menus.base_menu import BaseMenu -from utils.constants import RESET_FORMAT, COLOR_YELLOW +from utils.constants import COLOR_YELLOW, RESET_FORMAT # noinspection PyMethodMayBeStatic diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 102c369..0ae48bc 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -8,12 +8,12 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.moonraker import moonraker_remove from core.menus import Option from core.menus.base_menu import BaseMenu -from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN +from utils.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT # noinspection PyUnusedLocal diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index 8cffeb3..9f92b46 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -11,7 +11,7 @@ import subprocess from pathlib import Path from typing import List, Union -from components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH +from components.moonraker import MODULE_PATH, MOONRAKER_DIR, MOONRAKER_ENV_DIR from core.config_manager.config_manager import ConfigManager from core.instance_manager.base_instance import BaseInstance from utils.constants import SYSTEMD diff --git a/kiauh/components/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py index c047e4a..f49d0a7 100644 --- a/kiauh/components/moonraker/moonraker_dialogs.py +++ b/kiauh/components/moonraker/moonraker_dialogs.py @@ -13,7 +13,7 @@ from typing import List from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker from core.menus.base_menu import print_back_footer -from utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN +from utils.constants import COLOR_CYAN, COLOR_GREEN, COLOR_YELLOW, RESET_FORMAT def print_moonraker_overview( diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 8885b34..b14e658 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -10,30 +10,30 @@ import json import subprocess from pathlib import Path -from components.webui_client.client_utils import ( - enable_mainsail_remotemode, - get_existing_clients, -) -from components.webui_client.mainsail_data import MainsailData -from core.settings.kiauh_settings import KiauhSettings from components.klipper.klipper import Klipper from components.moonraker import ( EXIT_MOONRAKER_SETUP, MOONRAKER_DIR, MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT, - POLKIT_LEGACY_FILE, POLKIT_FILE, - POLKIT_USR_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.moonraker_utils import ( - create_example_moonraker_conf, backup_moonraker_dir, + create_example_moonraker_conf, ) +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.settings.kiauh_settings import KiauhSettings 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 @@ -43,10 +43,10 @@ from utils.input_utils import ( ) from utils.logger import Logger from utils.sys_utils import ( - parse_packages_from_file, + check_python_version, create_python_venv, install_python_requirements, - check_python_version, + parse_packages_from_file, ) diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index bd37184..c5abf7f 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -12,10 +12,9 @@ import subprocess from pathlib import Path from typing import List -from components.webui_client.base_data import BaseWebClient, BaseWebClientConfig -from core.settings.kiauh_settings import KiauhSettings from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker +from components.webui_client.base_data import BaseWebClient, BaseWebClientConfig from components.webui_client.client_dialogs import ( print_client_already_installed_dialog, ) @@ -23,8 +22,8 @@ from components.webui_client.client_utils import ( backup_client_config_data, config_for_other_client_exist, ) - from core.instance_manager.instance_manager import InstanceManager +from core.settings.kiauh_settings import KiauhSettings from utils.common import backup_printer_config_dir from utils.config_utils import add_config_section, add_config_section_at_top from utils.fs_utils import create_symlink diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py index 1932ad6..525aece 100644 --- a/kiauh/components/webui_client/client_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -12,7 +12,7 @@ from typing import List from components.webui_client.base_data import BaseWebClient from core.menus.base_menu import print_back_footer -from utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN +from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT def print_moonraker_not_found_dialog(): diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index a803ffd..a4a0b82 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -21,7 +21,6 @@ from components.webui_client.client_config.client_config_remove import ( run_client_config_removal, ) from components.webui_client.client_utils import backup_mainsail_config_json - from core.instance_manager.instance_manager import InstanceManager from utils.config_utils import remove_config_section from utils.fs_utils import ( diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index f4e1066..cb27075 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -11,27 +11,26 @@ from pathlib import Path from typing import List from components.klipper.klipper import Klipper - from components.moonraker.moonraker import Moonraker from components.webui_client.base_data import ( - WebClientType, BaseWebClient, BaseWebClientConfig, + WebClientType, ) from components.webui_client.client_config.client_config_setup import ( install_client_config, ) from components.webui_client.client_dialogs import ( - print_moonraker_not_found_dialog, print_client_port_select_dialog, print_install_client_config_dialog, + print_moonraker_not_found_dialog, ) from components.webui_client.client_utils import ( backup_mainsail_config_json, - restore_mainsail_config_json, - enable_mainsail_remotemode, - symlink_webui_nginx_log, config_for_other_client_exist, + enable_mainsail_remotemode, + restore_mainsail_config_json, + symlink_webui_nginx_log, ) from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings @@ -39,23 +38,23 @@ from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from utils.common import check_install_dependencies from utils.config_utils import add_config_section from utils.fs_utils import ( - unzip, - copy_upstream_nginx_cfg, copy_common_vars_nginx_cfg, + copy_upstream_nginx_cfg, create_nginx_cfg, create_symlink, - remove_file, - read_ports_from_nginx_configs, - is_valid_port, get_next_free_port, + is_valid_port, + read_ports_from_nginx_configs, + remove_file, + unzip, ) from utils.input_utils import get_confirm, get_number_input from utils.logger import Logger from utils.sys_utils import ( - download_file, - set_nginx_permissions, - get_ipv4_addr, cmd_sysctl_service, + download_file, + get_ipv4_addr, + set_nginx_permissions, ) diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py index 6d34279..59e75d6 100644 --- a/kiauh/components/webui_client/fluidd_data.py +++ b/kiauh/components/webui_client/fluidd_data.py @@ -13,10 +13,10 @@ from dataclasses import dataclass from pathlib import Path from components.webui_client.base_data import ( + BaseWebClient, BaseWebClientConfig, WebClientConfigType, WebClientType, - BaseWebClient, ) from components.webui_client.client_utils import get_download_url from core.backup_manager import BACKUP_ROOT_DIR diff --git a/kiauh/components/webui_client/mainsail_data.py b/kiauh/components/webui_client/mainsail_data.py index 91299c2..efab649 100644 --- a/kiauh/components/webui_client/mainsail_data.py +++ b/kiauh/components/webui_client/mainsail_data.py @@ -13,10 +13,10 @@ from dataclasses import dataclass from pathlib import Path from components.webui_client.base_data import ( + BaseWebClient, BaseWebClientConfig, WebClientConfigType, WebClientType, - BaseWebClient, ) from core.backup_manager import BACKUP_ROOT_DIR diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index f38da19..f8f64ff 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -8,13 +8,13 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.webui_client import client_remove from components.webui_client.base_data import BaseWebClient, WebClientType from core.menus import Option from core.menus.base_menu import BaseMenu -from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN +from utils.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT # noinspection PyUnusedLocal diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index a2ab9a5..f99db2a 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -8,11 +8,12 @@ # ======================================================================= # from __future__ import annotations -from abc import abstractmethod, ABC + +from abc import ABC, abstractmethod from pathlib import Path from typing import List, Optional -from utils.constants import SYSTEMD, CURRENT_USER +from utils.constants import CURRENT_USER, SYSTEMD class BaseInstance(ABC): diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 05d8197..ed3d306 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -10,7 +10,7 @@ import re import subprocess from pathlib import Path -from typing import List, Optional, Union, TypeVar +from typing import List, Optional, TypeVar, Union from core.instance_manager.base_instance import BaseInstance from utils.constants import SYSTEMD diff --git a/kiauh/core/instance_manager/name_scheme.py b/kiauh/core/instance_manager/name_scheme.py index bfd9e2c..492cd6b 100644 --- a/kiauh/core/instance_manager/name_scheme.py +++ b/kiauh/core/instance_manager/name_scheme.py @@ -1,4 +1,4 @@ -from enum import unique, Enum +from enum import Enum, unique @unique diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py index 8102e76..f5a6731 100644 --- a/kiauh/core/menus/__init__.py +++ b/kiauh/core/menus/__init__.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from enum import Enum -from typing import Callable, Any, Union +from typing import Any, Callable, Union @dataclass diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index fb4a3ea..30b8ef3 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -8,7 +8,7 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.klipper import KLIPPER_DIR from components.klipper.klipper import Klipper diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index a7b28d6..2f9f39f 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -8,24 +8,24 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.klipper.klipper_utils import backup_klipper_dir from components.klipperscreen.klipperscreen import backup_klipperscreen_dir from components.moonraker.moonraker_utils import ( - backup_moonraker_dir, backup_moonraker_db_dir, + backup_moonraker_dir, ) from components.webui_client.client_utils import ( - backup_client_data, backup_client_config_data, + backup_client_data, ) from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData from core.menus import Option from core.menus.base_menu import BaseMenu from utils.common import backup_printer_config_dir -from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW +from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT # noinspection PyUnusedLocal diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index ef169b4..6c0b9e3 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -14,14 +14,14 @@ import sys import textwrap import traceback from abc import abstractmethod -from typing import Type, Dict, Optional +from typing import Dict, Optional, Type from core.menus import FooterType, Option from utils.constants import ( - COLOR_GREEN, - COLOR_YELLOW, - COLOR_RED, COLOR_CYAN, + COLOR_GREEN, + COLOR_RED, + COLOR_YELLOW, RESET_FORMAT, ) from utils.logger import Logger diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index d1c9d27..c739da7 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -8,7 +8,7 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.crowsnest.crowsnest import install_crowsnest from components.klipper import klipper_setup @@ -20,7 +20,6 @@ from components.webui_client.client_config import client_config_setup from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData from core.menus import Option - from core.menus.base_menu import BaseMenu from utils.constants import COLOR_GREEN, RESET_FORMAT diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 55714fd..82d4f30 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -8,7 +8,7 @@ # ======================================================================= # import textwrap -from typing import Type, Optional +from typing import Optional, Type from components.crowsnest.crowsnest import remove_crowsnest from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 6547855..aacf3db 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -7,16 +7,17 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import textwrap import configparser +import textwrap from typing import Dict, Union from core.config_manager.config_manager import CustomConfigParser -from kiauh import PROJECT_ROOT -from utils.constants import RESET_FORMAT, COLOR_RED +from utils.constants import COLOR_RED, RESET_FORMAT from utils.logger import Logger from utils.sys_utils import kill +from kiauh import PROJECT_ROOT + # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic diff --git a/kiauh/extensions/base_extension.py b/kiauh/extensions/base_extension.py index a14cbaa..008c520 100644 --- a/kiauh/extensions/base_extension.py +++ b/kiauh/extensions/base_extension.py @@ -7,7 +7,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from abc import abstractmethod, ABC +from abc import ABC, abstractmethod from typing import Dict diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py index 80c6a85..781589b 100644 --- a/kiauh/extensions/extensions_menu.py +++ b/kiauh/extensions/extensions_menu.py @@ -12,13 +12,13 @@ import inspect import json import textwrap from pathlib import Path -from typing import Type, Dict, Optional +from typing import Dict, Optional, Type from core.menus import Option +from core.menus.base_menu import BaseMenu from extensions import EXTENSION_ROOT from extensions.base_extension import BaseExtension -from core.menus.base_menu import BaseMenu -from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW +from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT # noinspection PyUnusedLocal diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index 8e90550..54106b8 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -13,14 +13,14 @@ from typing import List from components.klipper.klipper import Klipper from core.backup_manager.backup_manager import BackupManager -from extensions.base_extension import BaseExtension from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager +from extensions.base_extension import BaseExtension from extensions.gcode_shell_cmd import ( - EXTENSION_TARGET_PATH, - EXTENSION_SRC, - KLIPPER_DIR, EXAMPLE_CFG_SRC, + EXTENSION_SRC, + EXTENSION_TARGET_PATH, + KLIPPER_DIR, KLIPPER_EXTRAS, ) from utils.fs_utils import check_file_exist diff --git a/kiauh/extensions/klipper_backup/klipper_backup_extension.py b/kiauh/extensions/klipper_backup/klipper_backup_extension.py index 94d1570..0d545ba 100644 --- a/kiauh/extensions/klipper_backup/klipper_backup_extension.py +++ b/kiauh/extensions/klipper_backup/klipper_backup_extension.py @@ -15,12 +15,11 @@ import subprocess from extensions.base_extension import BaseExtension from extensions.klipper_backup import ( - KLIPPERBACKUP_REPO_URL, - KLIPPERBACKUP_DIR, KLIPPERBACKUP_CONFIG_DIR, + KLIPPERBACKUP_DIR, + KLIPPERBACKUP_REPO_URL, MOONRAKER_CONF, ) - from utils.fs_utils import check_file_exist from utils.input_utils import get_confirm from utils.logger import Logger diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index fec24d7..1807af1 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -11,20 +11,19 @@ import csv import shutil import textwrap import urllib.request -from typing import List, Union, Optional, Type -from typing import TypedDict +from typing import List, Optional, Type, TypedDict, Union from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import ( - print_instance_overview, DisplayType, + print_instance_overview, ) -from core.menus import Option -from extensions.base_extension import BaseExtension from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager +from core.menus import Option from core.menus.base_menu import BaseMenu -from utils.constants import COLOR_YELLOW, COLOR_CYAN, RESET_FORMAT +from extensions.base_extension import BaseExtension +from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT from utils.git_utils import git_clone_wrapper from utils.input_utils import get_selection_input from utils.logger import Logger @@ -106,7 +105,7 @@ class MainsailThemeInstallMenu(BaseMenu): | {color}{header:~^{count}}{RESET_FORMAT} | |-------------------------------------------------------| | {line1:<62} | - | https://docs.mainsail.xyz/theming/themes | + | https://docs.mainsail.xyz/theming/themes | |-------------------------------------------------------| """ )[1:] diff --git a/kiauh/utils/config_utils.py b/kiauh/utils/config_utils.py index 42a2992..52e1c7e 100644 --- a/kiauh/utils/config_utils.py +++ b/kiauh/utils/config_utils.py @@ -8,7 +8,7 @@ # ======================================================================= # import tempfile from pathlib import Path -from typing import List, TypeVar, Tuple, Optional +from typing import List, Optional, Tuple, TypeVar from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 57efbaf..92f461e 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -12,16 +12,15 @@ import re import shutil from pathlib import Path -from zipfile import ZipFile -from subprocess import run, check_output, CalledProcessError, PIPE, DEVNULL - +from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run from typing import List +from zipfile import ZipFile from components.klipper.klipper import Klipper from utils import ( - NGINX_SITES_AVAILABLE, MODULE_PATH, NGINX_CONFD, + NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED, ) from utils.decorators import deprecated diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index 3a357ba..1b6c0fa 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -11,13 +11,13 @@ from enum import Enum from typing import List from utils.constants import ( - COLOR_WHITE, - COLOR_GREEN, - COLOR_YELLOW, - COLOR_RED, - COLOR_MAGENTA, - RESET_FORMAT, COLOR_CYAN, + COLOR_GREEN, + COLOR_MAGENTA, + COLOR_RED, + COLOR_WHITE, + COLOR_YELLOW, + RESET_FORMAT, ) diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index f6b12fa..e93531d 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -8,24 +8,22 @@ # ======================================================================= # import os +import select import shutil import socket -from subprocess import Popen, PIPE, CalledProcessError, run, DEVNULL import sys import time import urllib.error import urllib.request import venv from pathlib import Path +from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, run from typing import List, Literal -import select - from utils.fs_utils import check_file_exist from utils.input_utils import get_confirm from utils.logger import Logger - SysCtlServiceAction = Literal[ "start", "stop", From b3df3e7b5cf93c8e02b948e17e4fada668a41422 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 20 May 2024 11:35:43 +0200 Subject: [PATCH 231/296] refactor: improve nginx config generation Signed-off-by: Dominik Willner --- kiauh/components/webui_client/__init__.py | 12 ++++++ .../webui_client}/assets/nginx_cfg | 0 kiauh/components/webui_client/client_setup.py | 32 +++++--------- kiauh/utils/fs_utils.py | 42 +++++++++++++++---- 4 files changed, 56 insertions(+), 30 deletions(-) rename kiauh/{utils => components/webui_client}/assets/nginx_cfg (100%) diff --git a/kiauh/components/webui_client/__init__.py b/kiauh/components/webui_client/__init__.py index e69de29..371c365 100644 --- a/kiauh/components/webui_client/__init__.py +++ b/kiauh/components/webui_client/__init__.py @@ -0,0 +1,12 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path + +MODULE_PATH = Path(__file__).resolve().parent diff --git a/kiauh/utils/assets/nginx_cfg b/kiauh/components/webui_client/assets/nginx_cfg similarity index 100% rename from kiauh/utils/assets/nginx_cfg rename to kiauh/components/webui_client/assets/nginx_cfg diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index cb27075..bb6e10e 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -12,6 +12,7 @@ from typing import List from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker +from components.webui_client import MODULE_PATH from components.webui_client.base_data import ( BaseWebClient, BaseWebClientConfig, @@ -34,18 +35,15 @@ from components.webui_client.client_utils import ( ) from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings -from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from utils.common import check_install_dependencies from utils.config_utils import add_config_section from utils.fs_utils import ( copy_common_vars_nginx_cfg, copy_upstream_nginx_cfg, create_nginx_cfg, - create_symlink, get_next_free_port, is_valid_port, read_ports_from_nginx_configs, - remove_file, unzip, ) from utils.input_utils import get_confirm, get_number_input @@ -54,7 +52,6 @@ from utils.sys_utils import ( cmd_sysctl_service, download_file, get_ipv4_addr, - set_nginx_permissions, ) @@ -141,7 +138,15 @@ def install_client(client: BaseWebClient) -> None: copy_upstream_nginx_cfg() copy_common_vars_nginx_cfg() - create_client_nginx_cfg(client, port) + create_nginx_cfg( + display_name=client.display_name, + cfg_name=client.name, + template_src=MODULE_PATH.joinpath("assets/nginx_cfg"), + PORT=port, + ROOT_DIR=client.client_dir, + NAME=client.name, + ) + if kl_instances: symlink_webui_nginx_log(kl_instances) cmd_sysctl_service("nginx", "restart") @@ -190,20 +195,3 @@ def update_client(client: BaseWebClient) -> None: if client.client == WebClientType.MAINSAIL: restore_mainsail_config_json() - - -def create_client_nginx_cfg(client: BaseWebClient, port: int) -> None: - display_name = client.display_name - root_dir = client.client_dir - source = NGINX_SITES_AVAILABLE.joinpath(client.name) - target = NGINX_SITES_ENABLED.joinpath(client.name) - try: - Logger.print_status(f"Creating NGINX config for {display_name} ...") - remove_file(Path("/etc/nginx/sites-enabled/default"), True) - create_nginx_cfg(client.name, port, root_dir) - create_symlink(source, target, True) - set_nginx_permissions() - Logger.print_ok(f"NGINX config for {display_name} successfully created.") - except Exception: - Logger.print_error(f"Creating NGINX config for {display_name} failed!") - raise diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 92f461e..111294b 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -122,21 +122,23 @@ def copy_common_vars_nginx_cfg() -> None: raise -def create_nginx_cfg(name: str, port: int, root_dir: Path) -> None: +def generate_nginx_cfg_from_template(name: str, template_src: Path, **kwargs) -> None: """ - Creates an NGINX config from a template file and replaces all placeholders + Creates an NGINX config from a template file and + replaces all placeholders passed as kwargs. A placeholder must be defined + in the template file as %{placeholder}%. :param name: name of the config to create - :param port: listen port - :param root_dir: directory of the static files + :param template_src: the path to the template file :return: None """ tmp = Path.home().joinpath(f"{name}.tmp") - shutil.copy(MODULE_PATH.joinpath("assets/nginx_cfg"), tmp) + shutil.copy(template_src, tmp) with open(tmp, "r+") as f: content = f.read() - content = content.replace("%NAME%", name) - content = content.replace("%PORT%", str(port)) - content = content.replace("%ROOT_DIR%", str(root_dir)) + + for key, value in kwargs.items(): + content = content.replace(f"%{key}%", str(value)) + f.seek(0) f.write(content) f.truncate() @@ -151,6 +153,30 @@ def create_nginx_cfg(name: str, port: int, root_dir: Path) -> None: raise +def create_nginx_cfg( + display_name: str, + cfg_name: str, + template_src: Path, + **kwargs, +) -> None: + from utils.sys_utils import set_nginx_permissions + + try: + Logger.print_status(f"Creating NGINX config for {display_name} ...") + + source = NGINX_SITES_AVAILABLE.joinpath(cfg_name) + target = NGINX_SITES_ENABLED.joinpath(cfg_name) + remove_file(Path("/etc/nginx/sites-enabled/default"), True) + generate_nginx_cfg_from_template(cfg_name, template_src=template_src, **kwargs) + create_symlink(source, target, True) + set_nginx_permissions() + + Logger.print_ok(f"NGINX config for {display_name} successfully created.") + except Exception: + Logger.print_error(f"Creating NGINX config for {display_name} failed!") + raise + + def read_ports_from_nginx_configs() -> List[int]: """ Helper function to iterate over all NGINX configs and read all ports defined for listen From 0dfe7672b846e13cdb98fa184a5021c43441c1ec Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 20 May 2024 12:15:33 +0200 Subject: [PATCH 232/296] feat(extension): implement PrettyGCode for Klipper extension Signed-off-by: Dominik Willner --- kiauh/extensions/pretty_gcode/__init__.py | 0 .../pretty_gcode/assets/pgcode.local.conf | 19 ++++ kiauh/extensions/pretty_gcode/metadata.json | 10 ++ .../pretty_gcode/pretty_gcode_extension.py | 104 ++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 kiauh/extensions/pretty_gcode/__init__.py create mode 100644 kiauh/extensions/pretty_gcode/assets/pgcode.local.conf create mode 100644 kiauh/extensions/pretty_gcode/metadata.json create mode 100644 kiauh/extensions/pretty_gcode/pretty_gcode_extension.py diff --git a/kiauh/extensions/pretty_gcode/__init__.py b/kiauh/extensions/pretty_gcode/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/extensions/pretty_gcode/assets/pgcode.local.conf b/kiauh/extensions/pretty_gcode/assets/pgcode.local.conf new file mode 100644 index 0000000..eab6162 --- /dev/null +++ b/kiauh/extensions/pretty_gcode/assets/pgcode.local.conf @@ -0,0 +1,19 @@ +# PrettyGCode website configuration +# copy this file to /etc/nginx/sites-available/pgcode.local.conf +# then to enable: +# sudo ln -s /etc/nginx/sites-available/pgcode.local.conf /etc/nginx/sites-enabled/pgcode.local.conf +# then restart ngninx: +# sudo systemctl reload nginx +server { + listen %PORT%; + listen [::]:%PORT%; + server_name pgcode.local; + + root %ROOT_DIR%; + + index pgcode.html; + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/kiauh/extensions/pretty_gcode/metadata.json b/kiauh/extensions/pretty_gcode/metadata.json new file mode 100644 index 0000000..946c046 --- /dev/null +++ b/kiauh/extensions/pretty_gcode/metadata.json @@ -0,0 +1,10 @@ +{ + "metadata": { + "index": 5, + "module": "pretty_gcode_extension", + "maintained_by": "Kragrathea", + "display_name": "PrettyGCode for Klipper", + "description": "3D G-Code viewer for Klipper", + "updates": true + } +} diff --git a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py new file mode 100644 index 0000000..0a1c723 --- /dev/null +++ b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py @@ -0,0 +1,104 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import shutil +from pathlib import Path + +from extensions.base_extension import BaseExtension +from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from utils.common import check_install_dependencies +from utils.fs_utils import ( + create_nginx_cfg, + remove_file, +) +from utils.git_utils import git_clone_wrapper, git_pull_wrapper +from utils.input_utils import get_number_input +from utils.logger import DialogType, Logger +from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr + +MODULE_PATH = Path(__file__).resolve().parent +PGC_DIR = Path.home().joinpath("pgcode") +PGC_REPO = "https://github.com/Kragrathea/pgcode" +PGC_CONF = "pgcode.local.conf" + + +# noinspection PyMethodMayBeStatic +class PrettyGcodeExtension(BaseExtension): + def install_extension(self, **kwargs) -> None: + Logger.print_status("Installing PrettyGCode for Klipper ...") + Logger.print_dialog( + DialogType.ATTENTION, + [ + "Make sure you don't select a port which is already in use by " + "another application. Your input will not be validated! Choosing a port " + "which is already in use by another application may cause issues!", + "The default port is 7136.", + ], + ) + + port = get_number_input( + "On which port should PrettyGCode run", + min_count=0, + default=7136, + allow_go_back=True, + ) + + check_install_dependencies(["nginx"]) + + try: + # remove any existing pgc dir + if PGC_DIR.exists(): + shutil.rmtree(PGC_DIR) + + # clone pgc repo + git_clone_wrapper(PGC_REPO, PGC_DIR) + + # copy pgc conf + create_nginx_cfg( + "PrettyGCode for Klipper", + cfg_name=PGC_CONF, + template_src=MODULE_PATH.joinpath(f"assets/{PGC_CONF}"), + ROOT_DIR=PGC_DIR, + PORT=port, + ) + + cmd_sysctl_service("nginx", "restart") + + log = f"Open PrettyGCode now on: http://{get_ipv4_addr()}:{port}" + Logger.print_ok("PrettyGCode installation complete!", start="\n") + Logger.print_ok(log, prefix=False, end="\n\n") + + except Exception as e: + Logger.print_error( + f"Error during PrettyGCode for Klipper installation: {e}" + ) + + def update_extension(self, **kwargs) -> None: + Logger.print_status("Updating PrettyGCode for Klipper ...") + try: + git_pull_wrapper(PGC_REPO, PGC_DIR) + + except Exception as e: + Logger.print_error(f"Error during PrettyGCode for Klipper update: {e}") + + def remove_extension(self, **kwargs) -> None: + try: + Logger.print_status("Removing PrettyGCode for Klipper ...") + + # remove pgc dir + shutil.rmtree(PGC_DIR) + # remove nginx config + remove_file(NGINX_SITES_AVAILABLE.joinpath(PGC_CONF), True) + remove_file(NGINX_SITES_ENABLED.joinpath(PGC_CONF), True) + # restart nginx + cmd_sysctl_service("nginx", "restart") + + Logger.print_ok("PrettyGCode for Klipper removed!") + + except Exception as e: + Logger.print_error(f"Error during PrettyGCode for Klipper removal: {e}") From 017f1d4597a84e01217a67448034dd71f8080a40 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 20 May 2024 19:27:35 +0200 Subject: [PATCH 233/296] refactor: make format_dialog_content method public, use it in the extensions menu Signed-off-by: Dominik Willner --- kiauh/extensions/extensions_menu.py | 16 ++++++++++------ kiauh/extensions/gcode_shell_cmd/metadata.json | 2 +- kiauh/extensions/klipper_backup/metadata.json | 2 +- .../mainsail_theme_installer/metadata.json | 2 +- kiauh/extensions/pretty_gcode/metadata.json | 2 +- kiauh/extensions/telegram_bot/metadata.json | 2 +- kiauh/utils/logger.py | 11 +++++++---- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py index 781589b..301b8bc 100644 --- a/kiauh/extensions/extensions_menu.py +++ b/kiauh/extensions/extensions_menu.py @@ -12,13 +12,14 @@ import inspect import json import textwrap from pathlib import Path -from typing import Dict, Optional, Type +from typing import Dict, List, Optional, Type from core.menus import Option from core.menus.base_menu import BaseMenu from extensions import EXTENSION_ROOT from extensions.base_extension import BaseExtension from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT +from utils.logger import Logger # noinspection PyUnusedLocal @@ -129,11 +130,14 @@ class ExtensionSubmenu(BaseMenu): header = f" [ {self.extension.metadata.get('display_name')} ] " color = COLOR_YELLOW count = 62 - len(color) - len(RESET_FORMAT) - - wrapper = textwrap.TextWrapper(55, initial_indent="| ", subsequent_indent="| ") - lines = wrapper.wrap(self.extension.metadata.get("description")) - formatted_lines = [f"{line:<55} |" for line in lines] - description_text = "\n".join(formatted_lines) + line_width = 53 + description: List[str] = self.extension.metadata.get("description", []) + description_text = Logger.format_content( + description, + line_width, + border_left="|", + border_right="|", + ) menu = textwrap.dedent( f""" diff --git a/kiauh/extensions/gcode_shell_cmd/metadata.json b/kiauh/extensions/gcode_shell_cmd/metadata.json index cfb38b4..7d7ccdc 100644 --- a/kiauh/extensions/gcode_shell_cmd/metadata.json +++ b/kiauh/extensions/gcode_shell_cmd/metadata.json @@ -4,6 +4,6 @@ "module": "gcode_shell_cmd_extension", "maintained_by": "dw-0", "display_name": "G-Code Shell Command", - "description": "Allows to run a shell command from gcode." + "description": ["Run a shell commands from gcode."] } } diff --git a/kiauh/extensions/klipper_backup/metadata.json b/kiauh/extensions/klipper_backup/metadata.json index 9e5ef7e..ac09323 100644 --- a/kiauh/extensions/klipper_backup/metadata.json +++ b/kiauh/extensions/klipper_backup/metadata.json @@ -4,7 +4,7 @@ "module": "klipper_backup_extension", "maintained_by": "Staubgeborener", "display_name": "Klipper-Backup", - "description": "Backup all your klipper files in GitHub", + "description": ["Backup all your Klipper files to GitHub"], "updates": true } } diff --git a/kiauh/extensions/mainsail_theme_installer/metadata.json b/kiauh/extensions/mainsail_theme_installer/metadata.json index 05e22d3..ffb802a 100644 --- a/kiauh/extensions/mainsail_theme_installer/metadata.json +++ b/kiauh/extensions/mainsail_theme_installer/metadata.json @@ -4,6 +4,6 @@ "module": "mainsail_theme_installer_extension", "maintained_by": "dw-0", "display_name": "Mainsail Theme Installer", - "description": "Install Mainsail Themes maintained by the community." + "description": ["Install Mainsail Themes maintained by the Mainsail community."] } } diff --git a/kiauh/extensions/pretty_gcode/metadata.json b/kiauh/extensions/pretty_gcode/metadata.json index 946c046..187a429 100644 --- a/kiauh/extensions/pretty_gcode/metadata.json +++ b/kiauh/extensions/pretty_gcode/metadata.json @@ -4,7 +4,7 @@ "module": "pretty_gcode_extension", "maintained_by": "Kragrathea", "display_name": "PrettyGCode for Klipper", - "description": "3D G-Code viewer for Klipper", + "description": ["3D G-Code viewer for Klipper"], "updates": true } } diff --git a/kiauh/extensions/telegram_bot/metadata.json b/kiauh/extensions/telegram_bot/metadata.json index b3b4764..35b72ae 100644 --- a/kiauh/extensions/telegram_bot/metadata.json +++ b/kiauh/extensions/telegram_bot/metadata.json @@ -4,7 +4,7 @@ "module": "moonraker_telegram_bot_extension", "maintained_by": "nlef", "display_name": "Moonraker Telegram Bot", - "description": "Allows to control your printer with the Telegram messenger app.", + "description": ["Control your printer with the Telegram messenger app."], "project_url": "https://github.com/nlef/moonraker-telegram-bot", "updates": true } diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index 1b6c0fa..c6fe6d8 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -94,7 +94,7 @@ class Logger: dialog_color = Logger._get_dialog_color(title, custom_color) dialog_title = Logger._get_dialog_title(title, custom_title) dialog_title_formatted = Logger._format_dialog_title(dialog_title) - dialog_content = Logger._format_dialog_content(content, LINE_WIDTH) + dialog_content = Logger.format_content(content, LINE_WIDTH) top = Logger._format_top_border(dialog_color) bottom = Logger._format_bottom_border() @@ -140,9 +140,12 @@ class Logger: return "\n" @staticmethod - def _format_dialog_content(content: List[str], line_width: int) -> str: - border_left = "┃" - border_right = "┃" + def format_content( + content: List[str], + line_width: int, + border_left: str = "┃", + border_right: str = "┃", + ) -> str: wrapper = textwrap.TextWrapper(line_width) lines = [] From 74c70189af2d817cda200e9f8d9a6990fc66e2a1 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 25 May 2024 17:09:41 +0200 Subject: [PATCH 234/296] feat: implement option to center content in dialogs Signed-off-by: Dominik Willner --- kiauh/utils/logger.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index c6fe6d8..e8ba0a4 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -87,6 +87,7 @@ class Logger: def print_dialog( title: DialogType, content: List[str], + center_content: bool = False, custom_title: str = None, custom_color: DialogCustomColor = None, end: str = "\n", @@ -94,7 +95,7 @@ class Logger: dialog_color = Logger._get_dialog_color(title, custom_color) dialog_title = Logger._get_dialog_title(title, custom_title) dialog_title_formatted = Logger._format_dialog_title(dialog_title) - dialog_content = Logger.format_content(content, LINE_WIDTH) + dialog_content = Logger.format_content(content, LINE_WIDTH, center_content) top = Logger._format_top_border(dialog_color) bottom = Logger._format_bottom_border() @@ -143,6 +144,7 @@ class Logger: def format_content( content: List[str], line_width: int, + center_content: bool = False, border_left: str = "┃", border_right: str = "┃", ) -> str: @@ -158,7 +160,13 @@ class Logger: if c == "\n\n" and i < len(content) - 1: lines.append(" " * line_width) - formatted_lines = [ - f"{border_left} {line:<{line_width}} {border_right}" for line in lines - ] + if not center_content: + formatted_lines = [ + f"{border_left} {line:<{line_width}} {border_right}" for line in lines + ] + else: + formatted_lines = [ + f"{border_left} {line:^{line_width}} {border_right}" for line in lines + ] + return "\n".join(formatted_lines) From 91162a7070f347f11330d35abdfd2d4a16d592ff Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 25 May 2024 18:37:42 +0200 Subject: [PATCH 235/296] refactor: remove redundant printing of status messages Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/instance_manager.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index ed3d306..4cf3ea4 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -107,7 +107,6 @@ class InstanceManager: raise ValueError("current_instance cannot be None") def enable_instance(self) -> None: - Logger.print_status(f"Enabling {self.instance_service_full} ...") try: cmd_sysctl_service(self.instance_service_full, "enable") except subprocess.CalledProcessError as e: @@ -115,7 +114,6 @@ class InstanceManager: Logger.print_error(f"{e}") def disable_instance(self) -> None: - Logger.print_status(f"Disabling {self.instance_service_full} ...") try: cmd_sysctl_service(self.instance_service_full, "disable") except subprocess.CalledProcessError as e: @@ -123,7 +121,6 @@ class InstanceManager: Logger.print_error(f"{e}") def start_instance(self) -> None: - Logger.print_status(f"Starting {self.instance_service_full} ...") try: cmd_sysctl_service(self.instance_service_full, "start") except subprocess.CalledProcessError as e: @@ -131,7 +128,6 @@ class InstanceManager: Logger.print_error(f"{e}") def restart_instance(self) -> None: - Logger.print_status(f"Restarting {self.instance_service_full} ...") try: cmd_sysctl_service(self.instance_service_full, "restart") except subprocess.CalledProcessError as e: @@ -149,7 +145,6 @@ class InstanceManager: self.restart_instance() def stop_instance(self) -> None: - Logger.print_status(f"Stopping {self.instance_service_full} ...") try: cmd_sysctl_service(self.instance_service_full, "stop") except subprocess.CalledProcessError as e: From aafcba9f40b1b71e50b88949cce3b269e551714d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 25 May 2024 18:42:20 +0200 Subject: [PATCH 236/296] refactor: replace usage of instance manager method with cmd_sysctl_manage function Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_remove.py | 3 ++- kiauh/components/klipper/klipper_setup.py | 3 ++- kiauh/components/moonraker/moonraker_remove.py | 3 ++- kiauh/components/moonraker/moonraker_setup.py | 3 ++- kiauh/core/instance_manager/instance_manager.py | 11 ----------- .../telegram_bot/moonraker_telegram_bot_extension.py | 2 +- 6 files changed, 9 insertions(+), 16 deletions(-) diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index ad5f30b..8e8eca8 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -17,6 +17,7 @@ from core.instance_manager.instance_manager import InstanceManager from utils.fs_utils import remove_file from utils.input_utils import get_selection_input from utils.logger import Logger +from utils.sys_utils import cmd_sysctl_manage def run_klipper_removal( @@ -92,7 +93,7 @@ def remove_instances( instance_manager.disable_instance() instance_manager.delete_instance() - instance_manager.reload_daemon() + cmd_sysctl_manage("daemon-reload") def remove_klipper_dir() -> None: diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 21e013a..9f23259 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -41,6 +41,7 @@ from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm from utils.logger import Logger from utils.sys_utils import ( + cmd_sysctl_manage, create_python_venv, install_python_requirements, parse_packages_from_file, @@ -92,7 +93,7 @@ def install_klipper() -> None: if count == install_count: break - kl_im.reload_daemon() + cmd_sysctl_manage("daemon-reload") except Exception as e: Logger.print_error(e) diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index 8a64ca0..f3733a8 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -18,6 +18,7 @@ from core.instance_manager.instance_manager import InstanceManager from utils.fs_utils import remove_file from utils.input_utils import get_selection_input from utils.logger import Logger +from utils.sys_utils import cmd_sysctl_manage def run_moonraker_removal( @@ -98,7 +99,7 @@ def remove_instances( instance_manager.disable_instance() instance_manager.delete_instance() - instance_manager.reload_daemon() + cmd_sysctl_manage("daemon-reload") def remove_moonraker_dir() -> None: diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index b14e658..9546cf6 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -44,6 +44,7 @@ from utils.input_utils import ( from utils.logger import Logger from utils.sys_utils import ( check_python_version, + cmd_sysctl_manage, create_python_venv, install_python_requirements, parse_packages_from_file, @@ -110,7 +111,7 @@ def install_moonraker() -> None: mr_im.start_instance() - mr_im.reload_daemon() + cmd_sysctl_manage("daemon-reload") # if mainsail is installed, and we installed # multiple moonraker instances, we enable mainsails remote mode diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 4cf3ea4..205d280 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -157,17 +157,6 @@ class InstanceManager: self.current_instance = instance self.stop_instance() - def reload_daemon(self) -> None: - Logger.print_status("Reloading systemd manager configuration ...") - try: - command = ["sudo", "systemctl", "daemon-reload"] - if subprocess.run(command, check=True): - Logger.print_ok("Systemd manager configuration reloaded") - except subprocess.CalledProcessError as e: - Logger.print_error("Error reloading systemd manager configuration:") - Logger.print_error(f"{e}") - raise - def find_instances(self) -> List[T]: from utils.common import convert_camelcase_to_kebabcase diff --git a/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py b/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py index e6c5e74..f2a900d 100644 --- a/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py +++ b/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py @@ -196,7 +196,7 @@ class TelegramBotExtension(BaseExtension): instance_manager.disable_instance() instance_manager.delete_instance() - instance_manager.reload_daemon() + cmd_sysctl_manage("daemon-reload") def _remove_bot_dir(self) -> None: if not TELEGRAM_BOT_DIR.exists(): From 6570400f9e14d8d3b5ddbfab91a346f44761da24 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 25 May 2024 19:45:05 +0200 Subject: [PATCH 237/296] fix(moonraker): correctly loading dependencies from system-dependencies.json Signed-off-by: Dominik Willner --- kiauh/components/moonraker/moonraker_setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 9546cf6..e7db113 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -154,7 +154,8 @@ def install_moonraker_packages(moonraker_dir: Path) -> None: moonraker_deps = [] if deps_json.exists(): - moonraker_deps = json.load(deps_json).get("debian", []) + with open(deps_json, "r") as deps: + moonraker_deps = json.load(deps).get("debian", []) elif install_script.exists(): moonraker_deps = parse_packages_from_file(install_script) From 70ad635e3d17dc0cbb5c25069c55143cd62da011 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 25 May 2024 21:31:26 +0200 Subject: [PATCH 238/296] feat: add util function to check if moonraker is installed Signed-off-by: Dominik Willner --- kiauh/utils/common.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 8ca8051..28b0a6f 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -20,7 +20,7 @@ from utils.constants import ( RESET_FORMAT, ) from utils.git_utils import get_local_commit, get_remote_commit, get_repo_name -from utils.logger import Logger +from utils.logger import DialogType, Logger from utils.sys_utils import ( check_package_install, install_system_packages, @@ -124,3 +124,33 @@ def backup_printer_config_dir(): source=instance.cfg_dir, target=PRINTER_CFG_BACKUP_DIR, ) + + +def moonraker_exists(name: str = "") -> bool: + """ + Helper method to check if a Moonraker instance exists + :param name: Optional name of an installer where the check is performed + :return: True if at least one Moonraker instance exists, False otherwise + """ + from components.moonraker.moonraker import Moonraker + + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + + info = ( + f"{name} requires Moonraker to be installed" + if name + else "A Moonraker installation is required" + ) + + if not mr_instances: + Logger.print_dialog( + DialogType.WARNING, + [ + "No Moonraker instances found!", + f"{info}. Please install Moonraker first!", + ], + end="", + ) + return False + return True From df45c5955e2c6262cbd0aee90accc6f5ae189a41 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 25 May 2024 21:32:15 +0200 Subject: [PATCH 239/296] refactor: add regex pattern as parameter to get_string_input for validating input Signed-off-by: Dominik Willner --- kiauh/utils/input_utils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 4738681..4c6ca3c 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -6,7 +6,7 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # - +import re from typing import List, Union from utils import INVALID_CHOICE @@ -84,6 +84,7 @@ def get_number_input( def get_string_input( question: str, + regex: Union[str, None] = None, exclude: Union[List, None] = None, allow_special_chars=False, default=None, @@ -91,6 +92,7 @@ def get_string_input( """ Helper method to get a string input from the user :param question: The question to display + :param regex: An optional regex pattern to validate the input against :param exclude: List of strings which are not allowed :param allow_special_chars: Wheter to allow special characters in the input :param default: Optional default value @@ -98,11 +100,18 @@ def get_string_input( """ _exclude = [] if exclude is None else exclude _question = format_question(question, default) + _pattern = re.compile(regex) if regex is not None else None while True: _input = input(_question) + print(_input) + if _input.lower() in _exclude: Logger.print_error("This value is already in use/reserved.") + elif default is not None and _input == "": + return default + elif _pattern is not None and _pattern.match(_input): + return _input elif allow_special_chars: return _input elif not allow_special_chars and _input.isalnum(): From d414be609a6275ee7a691e4eb51f5d25f0db227d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 25 May 2024 21:32:59 +0200 Subject: [PATCH 240/296] feat: add utils function to check for a specific service instance Signed-off-by: Dominik Willner --- kiauh/utils/sys_utils.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index e93531d..d3d3b0d 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -8,6 +8,7 @@ # ======================================================================= # import os +import re import select import shutil import socket @@ -20,6 +21,7 @@ from pathlib import Path from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, run from typing import List, Literal +from utils.constants import SYSTEMD from utils.fs_utils import check_file_exist from utils.input_utils import get_confirm from utils.logger import Logger @@ -73,7 +75,6 @@ def parse_packages_from_file(source_file: Path) -> List[str]: """ packages = [] - print("Reading dependencies...") with open(source_file, "r") as file: for line in file: line = line.strip() @@ -365,6 +366,23 @@ def cmd_sysctl_manage(action: SysCtlManageAction) -> None: raise +def service_instance_exists(name: str, exclude: List[str] = None) -> bool: + """ + Checks if a systemd service instance exists. + :param name: the service name + :param exclude: List of strings of service names to exclude + :return: True if the service exists, False otherwise + """ + exclude = exclude or [] + pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.service$") + service_list = [ + Path(SYSTEMD, service) + for service in SYSTEMD.iterdir() + if pattern.search(service.name) and not any(s in service.name for s in exclude) + ] + return any(service_list) + + def log_process(process: Popen) -> None: """ Helper method to print stdout of a process in near realtime to the console. From 60f8aef69ba552ce8d31066d2e09829d1cca9df2 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 16 Jun 2024 18:14:55 +0200 Subject: [PATCH 241/296] Squashed 'kiauh/core/submodules/simple_config_parser/' content from commit 188dd1f git-subtree-dir: kiauh/core/submodules/simple_config_parser git-subtree-split: 188dd1ffd80bf72a2dc6075147ddc9339b059c4b --- .editorconfig | 13 + .gitignore | 13 + LICENSE | 674 ++++++++++++++++++ README.md | 6 + pyproject.toml | 66 ++ requirements-dev.txt | 3 + src/simple_config_parser/__init__.py | 0 .../simple_config_parser.py | 528 ++++++++++++++ tests/Test SimpleConfigParser.run.xml | 21 + tests/__init__.py | 0 .../internal_state/test_content_handling.py | 95 +++ .../test_internal_state_changes.py | 83 +++ .../line_parsing/data/case_parse_comment.py | 6 + .../line_parsing/data/case_parse_option.py | 19 + .../line_parsing/data/case_parse_section.py | 6 + .../line_parsing/test_line_parsing.py | 92 +++ .../data/case_line_is_comment.py | 9 + .../data/case_line_is_empty.py | 9 + .../data/case_line_is_multiline_option.py | 17 + .../data/case_line_is_option.py | 30 + .../data/case_line_is_section.py | 6 + .../test_line_type_detection.py | 37 + tests/features/public_api/test_public_api.py | 196 +++++ 23 files changed, 1929 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt create mode 100644 src/simple_config_parser/__init__.py create mode 100644 src/simple_config_parser/simple_config_parser.py create mode 100644 tests/Test SimpleConfigParser.run.xml create mode 100644 tests/__init__.py create mode 100644 tests/features/internal_state/test_content_handling.py create mode 100644 tests/features/internal_state/test_internal_state_changes.py create mode 100644 tests/features/line_parsing/data/case_parse_comment.py create mode 100644 tests/features/line_parsing/data/case_parse_option.py create mode 100644 tests/features/line_parsing/data/case_parse_section.py create mode 100644 tests/features/line_parsing/test_line_parsing.py create mode 100644 tests/features/line_type_detection/data/case_line_is_comment.py create mode 100644 tests/features/line_type_detection/data/case_line_is_empty.py create mode 100644 tests/features/line_type_detection/data/case_line_is_multiline_option.py create mode 100644 tests/features/line_type_detection/data/case_line_is_option.py create mode 100644 tests/features/line_type_detection/data/case_line_is_section.py create mode 100644 tests/features/line_type_detection/test_line_type_detection.py create mode 100644 tests/features/public_api/test_public_api.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2546a60 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# see https://editorconfig.org/ +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +indent_style = space +insert_final_newline = true +indent_size = 4 +charset = utf-8 + +[*.py] +max_line_length = 88 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5d5089 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.py[cod] +*.pyc +__pycache__ +.pytest_cache/ + +.idea/ +.vscode/ + +.venv*/ +venv*/ + +.coverage +htmlcov/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dda49fa --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Simple Config Parser + +A custom config parser inspired by Python's configparser module. +Specialized for handling Klipper style config files. + + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a3bca47 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[project] +name = "simple-config-parser" +version = "0.0.1" +description = "A simple config parser for Python" +authors = [ + {name = "Dominik Willner", email = "th33xitus@gmail.com"}, +] +readme = "README.md" +license = {text = "GPL-3.0-only"} +requires-python = ">=3.8" + +[project.urls] +homepage = "https://github.com/dw-0/simple-config-parser" +repository = "https://github.com/dw-0/simple-config-parser" +documentation = "https://github.com/dw-0/simple-config-parser" + +[project.optional-dependencies] +dev=["ruff"] + +[tool.ruff] +required-version = ">=0.3.4" +respect-gitignore = true +exclude = [".git",".github", "./docs"] +line-length = 88 +indent-width = 4 +output-format = "full" + +[tool.ruff.format] +indent-style = "space" +line-ending = "lf" +quote-style = "double" + +[tool.ruff.lint] +extend-select = ["I"] + +[tool.pytest.ini_options] +minversion = "8.2.1" +testpaths = ["tests/**/*.py"] +addopts = "--cov --cov-config=pyproject.toml --cov-report=html" + +[tool.coverage.run] +branch = true +source = ["src.simple_config_parser"] + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_also = [ + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + + # Don't complain about abstract methods, they aren't run: + "@(abc\\.)?abstractmethod", + ] + +[tool.coverage.html] +title = "SimpleConfigParser Coverage Report" +directory = "htmlcov" diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..7e73e5f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +ruff >= 0.3.4 +pytest >= 8.2.1 +pytest-cov >= 5.0.0 diff --git a/src/simple_config_parser/__init__.py b/src/simple_config_parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/simple_config_parser/simple_config_parser.py b/src/simple_config_parser/simple_config_parser.py new file mode 100644 index 0000000..208a630 --- /dev/null +++ b/src/simple_config_parser/simple_config_parser.py @@ -0,0 +1,528 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 Dominik Willner # +# # +# https://github.com/dw-0/simple-config-parser # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # + +from __future__ import annotations + +import re +from pathlib import Path +from typing import Callable, Dict, List, Match, Tuple, TypedDict + +_UNSET = object() + + +class Section(TypedDict): + """ + A single section in the config file + + - _raw: The raw representation of the section name + - options: A list of options in the section + """ + + _raw: str + options: List[Option] + + +class Option(TypedDict, total=False): + """ + A single option in a section in the config file + + - is_multiline: Whether the option is a multiline option + - option: The name of the option + - value: The value of the option + - _raw: The raw representation of the option + - _raw_value: The raw value of the option + + A multinline option is an option that contains multiple lines of text following + the option name in the next line. The value of a multiline option is a list of + strings, where each string represents a single line of text. + """ + + is_multiline: bool + option: str + value: str | List[str] + _raw: str + _raw_value: str | List[str] + + +class NoSectionError(Exception): + """Raised when a section is not defined""" + + def __init__(self, section: str): + msg = f"Section '{section}' is not defined" + super().__init__(msg) + + +class NoOptionError(Exception): + """Raised when an option is not defined in a section""" + + def __init__(self, option: str, section: str): + msg = f"Option '{option}' in section '{section}' is not defined" + super().__init__(msg) + + +class DuplicateSectionError(Exception): + """Raised when a section is defined more than once""" + + def __init__(self, section: str): + msg = f"Section '{section}' is defined more than once" + super().__init__(msg) + + +class DuplicateOptionError(Exception): + """Raised when an option is defined more than once""" + + def __init__(self, option: str, section: str): + msg = f"Option '{option}' in section '{section}' is defined more than once" + super().__init__(msg) + + +# noinspection PyMethodMayBeStatic +class SimpleConfigParser: + """A customized config parser targeted at handling Klipper style config files""" + + _SECTION_RE = re.compile(r"\s*\[(\w+ ?\w+)]\s*([#;].*)?$") + _OPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([^=:].*)\s*([#;].*)?$") + _MLOPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([#;].*)?$") + _COMMENT_RE = re.compile(r"^\s*([#;].*)?$") + _EMPTY_LINE_RE = re.compile(r"^\s*$") + + BOOLEAN_STATES = { + "1": True, + "yes": True, + "true": True, + "on": True, + "0": False, + "no": False, + "false": False, + "off": False, + } + + def __init__(self): + self._config: Dict = {} + self._header: List[str] = [] + self._all_sections: List[str] = [] + self._all_options: Dict = {} + self.section_name: str = "" + self.in_option_block: bool = False # whether we are in a multiline option block + + def read(self, file: Path) -> None: + """Read the given file and store the result in the internal state""" + + try: + with open(file, "r") as f: + self._parse_config(f.readlines()) + + except OSError: + raise + + def write(self, filename): + """Write the internal state to the given file""" + + content = self._construct_content() + + with open(filename, "w") as f: + f.write(content) + + def _construct_content(self) -> str: + """ + Constructs the content of the configuration file based on the internal state of + the _config object by iterating over the sections and their options. It starts + by checking if a header is present and extends the content list with its elements. + Then, for each section, it appends the raw representation of the section to the + content list. If the section has a body, it iterates over its options and extends + the content list with their raw representations. If an option is multiline, it + also extends the content list with its raw value. Finally, the content list is + joined into a single string and returned. + + :return: The content of the configuration file as a string + """ + content: List[str] = [] + if self._header is not None: + content.extend(self._header) + for section in self._config: + content.append(self._config[section]["_raw"]) + + if (sec_body := self._config[section].get("body")) is not None: + for option in sec_body: + content.extend(option["_raw"]) + if option["is_multiline"]: + content.extend(option["_raw_value"]) + content: str = "".join(content) + + return content + + def sections(self) -> List[str]: + """Return a list of section names""" + + return self._all_sections + + def add_section(self, section: str) -> None: + """Add a new section to the internal state""" + + if section in self._all_sections: + raise DuplicateSectionError(section) + self._all_sections.append(section) + self._all_options[section] = {} + self._config[section] = {"_raw": f"\n[{section}]\n", "body": []} + + def remove_section(self, section: str) -> None: + """Remove the given section""" + + if section not in self._all_sections: + raise NoSectionError(section) + + del self._all_sections[self._all_sections.index(section)] + del self._all_options[section] + del self._config[section] + + def options(self, section) -> List[str]: + """Return a list of option names for the given section name""" + + return self._all_options.get(section) + + def get(self, section: str, option: str, fallback: str | _UNSET = _UNSET) -> str: + """ + Return the value of the given option in the given section + + If the key is not found and 'fallback' is provided, it is used as + a fallback value. + """ + + try: + if section not in self._all_sections: + raise NoSectionError(section) + + if option not in self._all_options.get(section): + raise NoOptionError(option, section) + + return self._all_options[section][option] + except (NoSectionError, NoOptionError): + if fallback is _UNSET: + raise + return fallback + + def getint(self, section: str, option: str, fallback: int | _UNSET = _UNSET) -> int: + """Return the value of the given option in the given section as an int""" + + return self._get_conv(section, option, int, fallback=fallback) + + def getfloat( + self, section: str, option: str, fallback: float | _UNSET = _UNSET + ) -> float: + return self._get_conv(section, option, float, fallback=fallback) + + def getboolean( + self, section: str, option: str, fallback: bool | _UNSET = _UNSET + ) -> bool: + return self._get_conv( + section, option, self._convert_to_boolean, fallback=fallback + ) + + def _convert_to_boolean(self, value) -> bool: + if value.lower() not in self.BOOLEAN_STATES: + raise ValueError("Not a boolean: %s" % value) + return self.BOOLEAN_STATES[value.lower()] + + def _get_conv( + self, + section: str, + option: str, + conv: Callable[[str], int | float | bool], + fallback: _UNSET = _UNSET, + ) -> int | float | bool: + try: + return conv(self.get(section, option, fallback)) + except: + if fallback is not _UNSET: + return fallback + raise + + def items(self, section: str) -> List[Tuple[str, str]]: + """Return a list of (option, value) tuples for a specific section""" + + if section not in self._all_sections: + raise NoSectionError(section) + + result = [] + for _option in self._all_options[section]: + result.append((_option, self._all_options[section][_option])) + + return result + + def set( + self, + section: str, + option: str, + value: str, + multiline: bool = False, + indent: int = 2, + ) -> None: + """Set the given option to the given value in the given section + + If the option is already defined, it will be overwritten. If the option + is not defined yet, it will be added to the section body. + + The multiline parameter can be used to specify whether the value is + multiline or not. If it is not specified, the value will be considered + as multiline if it contains a newline character. The value will then be split + into multiple lines. If the value does not contain a newline character, it + will be considered as a single line value. The indent parameter can be used + to specify the indentation of the multiline value. Indentations are with spaces. + + :param section: The section to set the option in + :param option: The option to set + :param value: The value to set + :param multiline: Whether the value is multiline or not + :param indent: The indentation for multiline values + """ + + if section not in self._all_sections: + raise NoSectionError(section) + + # prepare the options value and raw value depending on the multiline flag + _raw_value: List[str] | None = None + if multiline or "\n" in value: + _multiline = True + _raw: str = f"{option}:\n" + _value: List[str] = value.split("\n") + _raw_value: List[str] = [f"{' ' * indent}{v}\n" for v in _value] + else: + _multiline = False + _raw: str = f"{option}: {value}\n" + _value: str = value + + # the option does not exist yet + if option not in self._all_options.get(section): + _option: Option = { + "is_multiline": _multiline, + "option": option, + "value": _value, + "_raw": _raw, + } + if _raw_value is not None: + _option["_raw_value"] = _raw_value + self._config[section]["body"].insert(0, _option) + + # the option exists and we need to update it + else: + for _option in self._config[section]["body"]: + if _option["option"] == option: + # we preserve inline comments by replacing the old value with the new one + _option["_raw"] = _option["_raw"].replace(_option["value"], _value) + _option["value"] = _value + if _raw_value is not None: + _option["_raw_value"] = _raw_value + break + + self._all_options[section][option] = _value + + def remove_option(self, section: str, option: str) -> None: + """Remove the given option from the given section""" + + if section not in self._all_sections: + raise NoSectionError(section) + + if option not in self._all_options.get(section): + raise NoOptionError(option, section) + + for _option in self._config[section]["body"]: + if _option["option"] == option: + del self._all_options[section][option] + self._config[section]["body"].remove(_option) + break + + def has_section(self, section: str) -> bool: + """Return True if the given section exists, False otherwise""" + return section in self._all_sections + + def has_option(self, section: str, option: str) -> bool: + """Return True if the given option exists in the given section, False otherwise""" + return option in self._all_options.get(section) + + def _is_section(self, line: str) -> bool: + """Check if the given line contains a section definition""" + return self._SECTION_RE.match(line) is not None + + def _is_option(self, line: str) -> bool: + """Check if the given line contains an option definition""" + + match: Match[str] | None = self._OPTION_RE.match(line) + + if not match: + return False + + # if there is no value, it's not a regular option but a multiline option + if match.group(2).strip() == "": + return False + + if not match.group(1).strip() == "": + return True + + return False + + def _is_comment(self, line: str) -> bool: + """Check if the given line is a comment""" + return self._COMMENT_RE.match(line) is not None + + def _is_empty_line(self, line: str) -> bool: + """Check if the given line is an empty line""" + return self._EMPTY_LINE_RE.match(line) is not None + + def _is_multiline_option(self, line: str) -> bool: + """Check if the given line starts a multiline option block""" + + match: Match[str] | None = self._MLOPTION_RE.match(line) + + if not match: + return False + + return True + + def _parse_config(self, content: List[str]) -> None: + """Parse the given content and store the result in the internal state""" + + _curr_multi_opt = "" + + # THE ORDER MATTERS, DO NOT REORDER THE CONDITIONS! + for line in content: + if self._is_section(line): + self._parse_section(line) + + elif self._is_option(line): + self._parse_option(line) + + # if it's not a regular option with the value inline, + # it might be a might be a multiline option block + elif self._is_multiline_option(line): + self.in_option_block = True + _curr_multi_opt = self._OPTION_RE.match(line).group(1).strip() + self._add_option_to_section_body(_curr_multi_opt, "", line) + + elif self.in_option_block: + self._parse_multiline_option(_curr_multi_opt, line) + + # if it's nothing from above, it's probably a comment or an empty line + elif self._is_comment(line) or self._is_empty_line(line): + self._parse_comment(line) + + def _parse_section(self, line: str) -> None: + """Parse a section line and store the result in the internal state""" + + match: Match[str] | None = self._SECTION_RE.match(line) + if not match: + return + + self.in_option_block = False + + section_name: str = match.group(1).strip() + self._store_internal_state_section(section_name, line) + + def _store_internal_state_section(self, section: str, raw_value: str) -> None: + """Store the given section and its raw value in the internal state""" + + if section in self._all_sections: + raise DuplicateSectionError(section) + + self.section_name = section + self._all_sections.append(section) + self._config[section]: Section = {"_raw": raw_value, "body": []} + + def _parse_option(self, line: str) -> None: + """Parse an option line and store the result in the internal state""" + + self.in_option_block = False + + match: Match[str] | None = self._OPTION_RE.match(line) + if not match: + return + + option: str = match.group(1).strip() + value: str = match.group(2).strip() + + if ";" in value: + i = value.index(";") + value = value[:i].strip() + elif "#" in value: + i = value.index("#") + value = value[:i].strip() + + self._store_internal_state_option(option, value, line) + + def _store_internal_state_option( + self, option: str, value: str, raw_value: str + ) -> None: + """Store the given option and its raw value in the internal state""" + + section_options = self._all_options.setdefault(self.section_name, {}) + + if option in section_options: + raise DuplicateOptionError(option, self.section_name) + + section_options[option] = value + self._add_option_to_section_body(option, value, raw_value) + + def _parse_multiline_option(self, curr_ml_opt: str, line: str) -> None: + """Parse a multiline option line and store the result in the internal state""" + + section_options = self._all_options.setdefault(self.section_name, {}) + multiline_options = section_options.setdefault(curr_ml_opt, []) + + _cleaned_line = line.strip().strip("\n") + if _cleaned_line and not self._is_comment(line): + multiline_options.append(_cleaned_line) + + # add the option to the internal multiline option value state + self._ensure_section_body_exists() + for _option in self._config[self.section_name]["body"]: + if _option.get("option") == curr_ml_opt: + _option.update( + is_multiline=True, + _raw_value=_option.get("_raw_value", []) + [line], + value=multiline_options, + ) + + def _parse_comment(self, line: str) -> None: + """ + Parse a comment line and store the result in the internal state + + If the there was no previous section parsed, the lines are handled as + the file header and added to the internal header list as it means, that + we are at the very top of the file. + """ + + self.in_option_block = False + + if not self.section_name: + self._header.append(line) + else: + self._add_option_to_section_body("", "", line) + + def _ensure_section_body_exists(self) -> None: + """ + Ensure that the section body exists in the internal state. + If the section body does not exist, it is created as an empty list + """ + if self.section_name not in self._config: + self._config.setdefault(self.section_name, {}).setdefault("body", []) + + def _add_option_to_section_body( + self, option: str, value: str, line: str, is_multiline: bool = False + ) -> None: + """Add a raw option line to the internal state""" + + self._ensure_section_body_exists() + + new_option: Option = { + "is_multiline": is_multiline, + "option": option, + "value": value, + "_raw": line, + } + + option_body = self._config[self.section_name]["body"] + option_body.append(new_option) diff --git a/tests/Test SimpleConfigParser.run.xml b/tests/Test SimpleConfigParser.run.xml new file mode 100644 index 0000000..bc62c5c --- /dev/null +++ b/tests/Test SimpleConfigParser.run.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/features/internal_state/test_content_handling.py b/tests/features/internal_state/test_content_handling.py new file mode 100644 index 0000000..d54681e --- /dev/null +++ b/tests/features/internal_state/test_content_handling.py @@ -0,0 +1,95 @@ +import pytest + +from src.simple_config_parser.simple_config_parser import SimpleConfigParser + + +@pytest.fixture +def parser(): + parser = SimpleConfigParser() + parser._header = ["header1\n", "header2\n"] + parser._config = { + "section1": { + "_raw": "[section1]\n", + "body": [ + { + "_raw": "option1: value1\n", + "_raw_value": "value1\n", + "is_multiline": False, + "option": "option1", + "value": "value1", + }, + { + "_raw": "option2: value2\n", + "_raw_value": "value2\n", + "is_multiline": False, + "option": "option2", + "value": "value2", + }, + ], + }, + "section2": { + "_raw": "[section2]\n", + "body": [ + { + "_raw": "option3: value3\n", + "_raw_value": "value3\n", + "is_multiline": False, + "option": "option3", + "value": "value3", + }, + ], + }, + "section3": { + "_raw": "[section3]\n", + "body": [ + { + "_raw": "option4:\n", + "_raw_value": [" value4\n", " value5\n", " value6\n"], + "is_multiline": True, + "option": "option4", + "value": ["value4", "value5", "value6"], + }, + ], + }, + } + return parser + + +def test_construct_content(parser): + content = parser._construct_content() + assert ( + content == "header1\nheader2\n" + "[section1]\n" + "option1: value1\n" + "option2: value2\n" + "[section2]\n" + "option3: value3\n" + "[section3]\n" + "option4:\n" + " value4\n" + " value5\n" + " value6\n" + ) + + +def test_construct_content_no_header(parser): + parser._header = None + content = parser._construct_content() + assert ( + content == "[section1]\n" + "option1: value1\n" + "option2: value2\n" + "[section2]\n" + "option3: value3\n" + "[section3]\n" + "option4:\n" + " value4\n" + " value5\n" + " value6\n" + ) + + +def test_construct_content_no_sections(parser): + parser._config = {} + content = parser._construct_content() + assert content == "".join(parser._header) diff --git a/tests/features/internal_state/test_internal_state_changes.py b/tests/features/internal_state/test_internal_state_changes.py new file mode 100644 index 0000000..af32e1e --- /dev/null +++ b/tests/features/internal_state/test_internal_state_changes.py @@ -0,0 +1,83 @@ +import pytest + +from src.simple_config_parser.simple_config_parser import ( + DuplicateOptionError, + DuplicateSectionError, + SimpleConfigParser, +) + + +@pytest.fixture +def parser(): + return SimpleConfigParser() + + +class TestInternalStateChanges: + @pytest.mark.parametrize( + "given", ["dummy_section", "dummy_section 2", "another_section"] + ) + def test_ensure_section_body_exists(self, parser, given): + parser._config = {} + parser.section_name = given + parser._ensure_section_body_exists() + + assert parser._config[given] is not None + assert parser._config[given]["body"] == [] + + def test_add_option_to_section_body(self): + pass + + @pytest.mark.parametrize( + "given", ["dummy_section", "dummy_section 2", "another_section\n"] + ) + def test_store_internal_state_section(self, parser, given): + parser._store_internal_state_section(given, given) + + assert parser._all_sections == [given] + assert parser._config[given]["body"] == [] + assert parser._config[given]["_raw"] == given + + def test_duplicate_section_error(self, parser): + section_name = "dummy_section" + parser._all_sections = [section_name] + + with pytest.raises(DuplicateSectionError) as excinfo: + parser._store_internal_state_section(section_name, section_name) + message = f"Section '{section_name}' is defined more than once" + assert message in str(excinfo.value) + + # Check that the internal state of the parser is correct + assert parser.in_option_block is False + assert parser.section_name == "" + assert parser._all_sections == [section_name] + + @pytest.mark.parametrize( + "given_name, given_value, given_raw_value", + [("dummyoption", "dummyvalue", "dummyvalue\n")], + ) + def test_store_internal_state_option( + self, parser, given_name, given_value, given_raw_value + ): + parser.section_name = "dummy_section" + parser._store_internal_state_option(given_name, given_value, given_raw_value) + + assert parser._all_options[parser.section_name] == {given_name: given_value} + + new_option = { + "is_multiline": False, + "option": given_name, + "value": given_value, + "_raw": given_raw_value, + } + assert parser._config[parser.section_name]["body"] == [new_option] + + def test_duplicate_option_error(self, parser): + option_name = "dummyoption" + value = "dummyvalue" + parser.section_name = "dummy_section" + parser._all_options = {parser.section_name: {option_name: value}} + + with pytest.raises(DuplicateOptionError) as excinfo: + parser._store_internal_state_option(option_name, value, value) + message = f"Option '{option_name}' in section '{parser.section_name}' is defined more than once" + assert message in str(excinfo.value) diff --git a/tests/features/line_parsing/data/case_parse_comment.py b/tests/features/line_parsing/data/case_parse_comment.py new file mode 100644 index 0000000..d84b40f --- /dev/null +++ b/tests/features/line_parsing/data/case_parse_comment.py @@ -0,0 +1,6 @@ +testcases = [ + "# comment # 1", + "; comment # 2", + " ; indented comment", + " # another indented comment", +] diff --git a/tests/features/line_parsing/data/case_parse_option.py b/tests/features/line_parsing/data/case_parse_option.py new file mode 100644 index 0000000..e4901f5 --- /dev/null +++ b/tests/features/line_parsing/data/case_parse_option.py @@ -0,0 +1,19 @@ +testcases = [ + ("option: value", "option", "value"), + ("option : value", "option", "value"), + ("option :value", "option", "value"), + ("option= value", "option", "value"), + ("option = value", "option", "value"), + ("option =value", "option", "value"), + ("option: value\n", "option", "value"), + ("option: value # inline comment", "option", "value"), + ("option: value # inline comment\n", "option", "value"), + ("description: homing!", "description", "homing!"), + ("description: inline macro :-)", "description", "inline macro :-)"), + ("path: %GCODES_DIR%", "path", "%GCODES_DIR%"), + ( + "serial = /dev/serial/by-id/", + "serial", + "/dev/serial/by-id/", + ), +] diff --git a/tests/features/line_parsing/data/case_parse_section.py b/tests/features/line_parsing/data/case_parse_section.py new file mode 100644 index 0000000..ea1536d --- /dev/null +++ b/tests/features/line_parsing/data/case_parse_section.py @@ -0,0 +1,6 @@ +testcases = [ + ("[test_section]", "test_section"), + ("[test_section two]", "test_section two"), + ("[section1] # inline comment", "section1"), + ("[section2] ; second comment", "section2"), +] diff --git a/tests/features/line_parsing/test_line_parsing.py b/tests/features/line_parsing/test_line_parsing.py new file mode 100644 index 0000000..96366e3 --- /dev/null +++ b/tests/features/line_parsing/test_line_parsing.py @@ -0,0 +1,92 @@ +import pytest +from data.case_parse_comment import testcases as case_parse_comment +from data.case_parse_option import testcases as case_parse_option +from data.case_parse_section import testcases as case_parse_section + +from src.simple_config_parser.simple_config_parser import ( + Option, + SimpleConfigParser, +) + + +@pytest.fixture +def parser(): + return SimpleConfigParser() + + +class TestLineParsing: + @pytest.mark.parametrize("given, expected", [*case_parse_section]) + def test_parse_section(self, parser, given, expected): + parser._parse_section(given) + + # Check that the internal state of the parser is correct + assert parser.section_name == expected + assert parser.in_option_block is False + assert parser._all_sections == [expected] + assert parser._config[expected]["_raw"] == given + assert parser._config[expected]["body"] == [] + + @pytest.mark.parametrize( + "given, expected_option, expected_value", [*case_parse_option] + ) + def test_parse_option(self, parser, given, expected_option, expected_value): + section_name = "test_section" + parser.section_name = section_name + parser._parse_option(given) + + # Check that the internal state of the parser is correct + assert parser.section_name == section_name + assert parser.in_option_block is False + assert parser._all_options[section_name][expected_option] == expected_value + + section_option = parser._config[section_name]["body"][0] + assert section_option["option"] == expected_option + assert section_option["value"] == expected_value + assert section_option["_raw"] == given + + @pytest.mark.parametrize( + "option, next_line", + [("gcode", "next line"), ("gcode", " {{% some jinja template %}}")], + ) + def test_parse_multiline_option(self, parser, option, next_line): + parser.section_name = "dummy_section" + parser.in_option_block = True + parser._add_option_to_section_body(option, "", option) + parser._parse_multiline_option(option, next_line) + cleaned_next_line = next_line.strip().strip("\n") + + assert parser._all_options[parser.section_name] is not None + assert parser._all_options[parser.section_name][option] == [cleaned_next_line] + + expected_option: Option = { + "is_multiline": True, + "option": option, + "value": [cleaned_next_line], + "_raw": option, + "_raw_value": [next_line], + } + assert parser._config[parser.section_name]["body"] == [expected_option] + + @pytest.mark.parametrize("given", [*case_parse_comment]) + def test_parse_comment(self, parser, given): + parser.section_name = "dummy_section" + parser._parse_comment(given) + + # internal state checks after parsing + assert parser.in_option_block is False + + expected_option = { + "is_multiline": False, + "_raw": given, + "option": "", + "value": "", + } + assert parser._config[parser.section_name]["body"] == [expected_option] + + @pytest.mark.parametrize("given", ["# header line", "; another header line"]) + def test_parse_header_comment(self, parser, given): + parser.section_name = "" + parser._parse_comment(given) + + assert parser.in_option_block is False + assert parser._header == [given] diff --git a/tests/features/line_type_detection/data/case_line_is_comment.py b/tests/features/line_type_detection/data/case_line_is_comment.py new file mode 100644 index 0000000..107745c --- /dev/null +++ b/tests/features/line_type_detection/data/case_line_is_comment.py @@ -0,0 +1,9 @@ +testcases = [ + ("# an arbitrary comment", True), + ("; another arbitrary comment", True), + (" ; indented comment", True), + (" # indented comment", True), + ("not_a: comment", False), + ("also_not_a= comment", False), + ("[definitely_not_a_comment]", False), +] diff --git a/tests/features/line_type_detection/data/case_line_is_empty.py b/tests/features/line_type_detection/data/case_line_is_empty.py new file mode 100644 index 0000000..7fe6afc --- /dev/null +++ b/tests/features/line_type_detection/data/case_line_is_empty.py @@ -0,0 +1,9 @@ +testcases = [ + ("", True), + (" ", True), + ("not empty", False), + (" # indented comment", False), + ("not: empty", False), + ("also_not= empty", False), + ("[definitely_not_empty]", False), +] diff --git a/tests/features/line_type_detection/data/case_line_is_multiline_option.py b/tests/features/line_type_detection/data/case_line_is_multiline_option.py new file mode 100644 index 0000000..ed93f87 --- /dev/null +++ b/tests/features/line_type_detection/data/case_line_is_multiline_option.py @@ -0,0 +1,17 @@ +testcases = [ + ("valid_option:", True), + ("valid_option:\n", True), + ("valid_option: ; inline comment", True), + ("valid_option: # inline comment", True), + ("valid_option :", True), + ("valid_option=", True), + ("valid_option= ", True), + ("valid_option =", True), + ("valid_option = ", True), + ("invalid_option ==", False), + ("invalid_option :=", False), + ("not_a_valid_option", False), + ("", False), + ("# that's a comment", False), + ("; that's a comment", False), +] diff --git a/tests/features/line_type_detection/data/case_line_is_option.py b/tests/features/line_type_detection/data/case_line_is_option.py new file mode 100644 index 0000000..280e851 --- /dev/null +++ b/tests/features/line_type_detection/data/case_line_is_option.py @@ -0,0 +1,30 @@ +testcases = [ + ("valid_option: value", True), + ("valid_option: value\n", True), + ("valid_option: value ; inline comment", True), + ("valid_option: value # inline comment", True), + ("valid_option: value # inline comment\n", True), + ("valid_option : value", True), + ("valid_option :value", True), + ("valid_option= value", True), + ("valid_option = value", True), + ("valid_option =value", True), + ("invalid_option:", False), + ("invalid_option=", False), + ("invalid_option:: value", False), + ("invalid_option :: value", False), + ("invalid_option ::value", False), + ("invalid_option== value", False), + ("invalid_option == value", False), + ("invalid_option ==value", False), + ("invalid_option:= value", False), + ("invalid_option := value", False), + ("invalid_option :=value", False), + ("[that_is_a_section]", False), + ("[that_is_section two]", False), + ("not_a_valid_option", False), + ("description: homing!", True), + ("description: inline macro :-)", True), + ("path: %GCODES_DIR%", True), + ("serial = /dev/serial/by-id/", True), +] diff --git a/tests/features/line_type_detection/data/case_line_is_section.py b/tests/features/line_type_detection/data/case_line_is_section.py new file mode 100644 index 0000000..663eabe --- /dev/null +++ b/tests/features/line_type_detection/data/case_line_is_section.py @@ -0,0 +1,6 @@ +testcases = [ + ("[example_section]", True), + ("[example_section two]", True), + ("not_a_valid_section", False), + ("section: invalid", False), +] diff --git a/tests/features/line_type_detection/test_line_type_detection.py b/tests/features/line_type_detection/test_line_type_detection.py new file mode 100644 index 0000000..854fde7 --- /dev/null +++ b/tests/features/line_type_detection/test_line_type_detection.py @@ -0,0 +1,37 @@ +import pytest +from data.case_line_is_comment import testcases as case_line_is_comment +from data.case_line_is_empty import testcases as case_line_is_empty +from data.case_line_is_multiline_option import ( + testcases as case_line_is_multiline_option, +) +from data.case_line_is_option import testcases as case_line_is_option +from data.case_line_is_section import testcases as case_line_is_section + +from src.simple_config_parser.simple_config_parser import SimpleConfigParser + + +@pytest.fixture +def parser(): + return SimpleConfigParser() + + +class TestLineTypeDetection: + @pytest.mark.parametrize("given, expected", [*case_line_is_section]) + def test_line_is_section(self, parser, given, expected): + assert parser._is_section(given) is expected + + @pytest.mark.parametrize("given, expected", [*case_line_is_option]) + def test_line_is_option(self, parser, given, expected): + assert parser._is_option(given) is expected + + @pytest.mark.parametrize("given, expected", [*case_line_is_multiline_option]) + def test_line_is_multiline_option(self, parser, given, expected): + assert parser._is_multiline_option(given) is expected + + @pytest.mark.parametrize("given, expected", [*case_line_is_comment]) + def test_line_is_comment(self, parser, given, expected): + assert parser._is_comment(given) is expected + + @pytest.mark.parametrize("given, expected", [*case_line_is_empty]) + def test_line_is_empty(self, parser, given, expected): + assert parser._is_empty_line(given) is expected diff --git a/tests/features/public_api/test_public_api.py b/tests/features/public_api/test_public_api.py new file mode 100644 index 0000000..10d86b1 --- /dev/null +++ b/tests/features/public_api/test_public_api.py @@ -0,0 +1,196 @@ +import pytest + +from src.simple_config_parser.simple_config_parser import ( + DuplicateSectionError, + NoOptionError, + NoSectionError, + SimpleConfigParser, +) + + +@pytest.fixture +def parser(): + return SimpleConfigParser() + + +class TestPublicAPI: + def test_has_section(self, parser): + parser._all_sections = ["section1"] + assert parser.has_section("section1") is True + + @pytest.mark.parametrize("section", ["section1", "section2", "section three"]) + def test_add_section(self, parser, section): + parser.add_section(section) + + assert section in parser._all_sections + assert parser._all_options[section] == {} + + cfg_section = {"_raw": f"\n[{section}]\n", "body": []} + assert parser._config[section] == cfg_section + + @pytest.mark.parametrize("section", ["section1", "section2", "section three"]) + def test_add_existing_section(self, parser, section): + parser._all_sections = [section] + + with pytest.raises(DuplicateSectionError): + parser.add_section(section) + + assert parser._all_sections == [section] + + @pytest.mark.parametrize("section", ["section1", "section2", "section three"]) + def test_remove_section(self, parser, section): + parser.add_section(section) + parser.remove_section(section) + + assert section not in parser._all_sections + assert section not in parser._all_options + assert section not in parser._config + + @pytest.mark.parametrize("section", ["section1", "section2", "section three"]) + def test_remove_non_existing_section(self, parser, section): + with pytest.raises(NoSectionError): + parser.remove_section(section) + + def test_get_all_sections(self, parser): + parser.add_section("section1") + parser.add_section("section2") + parser.add_section("section three") + + assert parser.sections() == ["section1", "section2", "section three"] + + def test_has_option(self, parser): + parser.add_section("section1") + parser.set("section1", "option1", "value1") + + assert parser.has_option("section1", "option1") is True + + @pytest.mark.parametrize( + "section, option, value", + [ + ("section1", "option1", "value1"), + ("section2", "option2", "value2"), + ("section three", "option3", "value three"), + ], + ) + def test_set_new_option(self, parser, section, option, value): + parser.add_section(section) + parser.set(section, option, value) + + assert section in parser._all_sections + assert option in parser._all_options[section] + assert parser._all_options[section][option] == value + + assert parser._config[section]["body"][0]["is_multiline"] is False + assert parser._config[section]["body"][0]["option"] == option + assert parser._config[section]["body"][0]["value"] == value + assert parser._config[section]["body"][0]["_raw"] == f"{option}: {value}\n" + + def test_set_existing_option(self, parser): + section, option, value1, value2 = "section1", "option1", "value1", "value2" + + parser.add_section(section) + parser.set(section, option, value1) + parser.set(section, option, value2) + + assert parser._all_options[section][option] == value2 + assert parser._config[section]["body"][0]["is_multiline"] is False + assert parser._config[section]["body"][0]["option"] == option + assert parser._config[section]["body"][0]["value"] == value2 + assert parser._config[section]["body"][0]["_raw"] == f"{option}: {value2}\n" + + def test_set_new_multiline_option(self, parser): + section, option, value = "section1", "option1", "value1\nvalue2\nvalue3" + + parser.add_section(section) + parser.set(section, option, value) + + assert parser._config[section]["body"][0]["is_multiline"] is True + assert parser._config[section]["body"][0]["option"] == option + + values = ["value1", "value2", "value3"] + raw_values = [" value1\n", " value2\n", " value3\n"] + assert parser._config[section]["body"][0]["value"] == values + assert parser._config[section]["body"][0]["_raw"] == f"{option}:\n" + assert parser._config[section]["body"][0]["_raw_value"] == raw_values + assert parser._all_options[section][option] == values + + def test_set_option_of_non_existing_section(self, parser): + with pytest.raises(NoSectionError): + parser.set("section1", "option1", "value1") + + def test_remove_option(self, parser): + section, option, value = "section1", "option1", "value1" + + parser.add_section(section) + parser.set(section, option, value) + parser.remove_option(section, option) + + assert option not in parser._all_options[section] + assert option not in parser._config[section]["body"] + + def test_remove_non_existing_option(self, parser): + parser.add_section("section1") + with pytest.raises(NoOptionError): + parser.remove_option("section1", "option1") + + def test_remove_option_of_non_existing_section(self, parser): + with pytest.raises(NoSectionError): + parser.remove_option("section1", "option1") + + def test_get_option(self, parser): + parser.add_section("section1") + parser.add_section("section2") + parser.set("section1", "option1", "value1") + parser.set("section2", "option2", "value2") + parser.set("section2", "option3", "value two") + + assert parser.get("section1", "option1") == "value1" + assert parser.get("section2", "option2") == "value2" + assert parser.get("section2", "option3") == "value two" + + def test_get_option_of_non_existing_section(self, parser): + with pytest.raises(NoSectionError): + parser.get("section1", "option1") + + def test_get_option_of_non_existing_option(self, parser): + parser.add_section("section1") + with pytest.raises(NoOptionError): + parser.get("section1", "option1") + + def test_get_option_fallback(self, parser): + parser.add_section("section1") + assert parser.get("section1", "option1", "fallback_value") == "fallback_value" + + def test_get_options(self, parser): + parser.add_section("section1") + parser.set("section1", "option1", "value1") + parser.set("section1", "option2", "value2") + parser.set("section1", "option3", "value3") + + options = {"option1": "value1", "option2": "value2", "option3": "value3"} + assert parser.options("section1") == options + + def test_get_option_as_int(self, parser): + parser.add_section("section1") + parser.set("section1", "option1", "1") + + option = parser.getint("section1", "option1") + assert isinstance(option, int) is True + + def test_get_option_as_float(self, parser): + parser.add_section("section1") + parser.set("section1", "option1", "1.234") + + option = parser.getfloat("section1", "option1") + assert isinstance(option, float) is True + + @pytest.mark.parametrize( + "value", + ["True", "true", "on", "1", "yes", "False", "false", "off", "0", "no"], + ) + def test_get_option_as_boolean(self, parser, value): + parser.add_section("section1") + parser.set("section1", "option1", value) + + option = parser.getboolean("section1", "option1") + assert isinstance(option, bool) is True From fbab9a769a57e1b0927def13adcdc47fddc263ad Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 16 Jun 2024 18:17:05 +0200 Subject: [PATCH 242/296] feat(scp): add new config parser Signed-off-by: Dominik Willner --- kiauh/core/submodules/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 kiauh/core/submodules/__init__.py diff --git a/kiauh/core/submodules/__init__.py b/kiauh/core/submodules/__init__.py new file mode 100644 index 0000000..e69de29 From c6cc3fc0f6e6400714c67d316f2abf08527ad24e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 16 Jun 2024 18:57:15 +0200 Subject: [PATCH 243/296] Squashed 'kiauh/core/submodules/simple_config_parser/' changes from 188dd1f..4d60d30 4d60d30 refactor: in multiline options we do not replace the option name 0a1fba5 refactor: set default indent to 4 spaces ab522bf refactor: the value of an option can be a list of strings git-subtree-dir: kiauh/core/submodules/simple_config_parser git-subtree-split: 4d60d30a75e7151be7b38b7cdbb2c133711b0091 --- src/simple_config_parser/simple_config_parser.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/simple_config_parser/simple_config_parser.py b/src/simple_config_parser/simple_config_parser.py index 208a630..38cf2d9 100644 --- a/src/simple_config_parser/simple_config_parser.py +++ b/src/simple_config_parser/simple_config_parser.py @@ -185,7 +185,9 @@ class SimpleConfigParser: return self._all_options.get(section) - def get(self, section: str, option: str, fallback: str | _UNSET = _UNSET) -> str: + def get( + self, section: str, option: str, fallback: str | _UNSET = _UNSET + ) -> str | List[str]: """ Return the value of the given option in the given section @@ -260,7 +262,7 @@ class SimpleConfigParser: option: str, value: str, multiline: bool = False, - indent: int = 2, + indent: int = 4, ) -> None: """Set the given option to the given value in the given section @@ -312,8 +314,13 @@ class SimpleConfigParser: else: for _option in self._config[section]["body"]: if _option["option"] == option: - # we preserve inline comments by replacing the old value with the new one - _option["_raw"] = _option["_raw"].replace(_option["value"], _value) + if multiline: + _option["_raw"] = _raw + else: + # we preserve inline comments by replacing the old value with the new one + _option["_raw"] = _option["_raw"].replace( + _option["value"], _value + ) _option["value"] = _value if _raw_value is not None: _option["_raw_value"] = _raw_value From 802eaccf57c19a39e735867f6fe53178da62012d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 16 Jun 2024 21:46:21 +0200 Subject: [PATCH 244/296] refactor(scp): replace old config parser with new one, remove ConfigManager Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 22 +++-- kiauh/components/moonraker/moonraker.py | 16 ++-- kiauh/components/moonraker/moonraker_utils.py | 43 ++++++---- kiauh/core/config_manager/__init__.py | 0 kiauh/core/config_manager/config_manager.py | 83 ------------------- .../gcode_shell_cmd_extension.py | 13 +-- kiauh/utils/config_utils.py | 32 ++++--- 7 files changed, 77 insertions(+), 132 deletions(-) delete mode 100644 kiauh/core/config_manager/__init__.py delete mode 100644 kiauh/core/config_manager/config_manager.py diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 2689951..3f97d09 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -34,10 +34,12 @@ from components.webui_client.client_config.client_config_setup import ( create_client_config_symlink, ) from core.backup_manager.backup_manager import BackupManager -from core.config_manager.config_manager import ConfigManager from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.name_scheme import NameScheme +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + SimpleConfigParser, +) from utils import PRINTER_CFG_BACKUP_DIR from utils.common import get_install_status from utils.constants import CURRENT_USER @@ -186,10 +188,11 @@ def klipper_to_multi_conversion(new_name: str) -> None: # patch the virtual_sdcard sections path # value to match the new printer_data foldername - cm = ConfigManager(new_instance.cfg_file) - if cm.config.has_section("virtual_sdcard"): - cm.set_value("virtual_sdcard", "path", str(new_instance.gcodes_dir)) - cm.write_config() + scp = SimpleConfigParser() + scp.read(new_instance.cfg_file) + if scp.has_section("virtual_sdcard"): + scp.set("virtual_sdcard", "path", str(new_instance.gcodes_dir)) + scp.write(new_instance.cfg_file) # finalize creating the new instance im.create_instance() @@ -292,18 +295,19 @@ def create_example_printer_cfg( Logger.print_error(f"Unable to create example printer.cfg:\n{e}") return - cm = ConfigManager(target) - cm.set_value("virtual_sdcard", "path", str(instance.gcodes_dir)) + scp = SimpleConfigParser() + scp.read(target) + scp.set("virtual_sdcard", "path", str(instance.gcodes_dir)) # include existing client configs in the example config if clients is not None and len(clients) > 0: for c in clients: client_config = c.client_config section = client_config.config_section - cm.config.add_section(section=section) + scp.add_section(section=section) create_client_config_symlink(client_config, [instance]) - cm.write_config() + scp.write(target) Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'") diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index 9f92b46..761d729 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -6,14 +6,17 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations import subprocess from pathlib import Path -from typing import List, Union +from typing import List from components.moonraker import MODULE_PATH, MOONRAKER_DIR, MOONRAKER_ENV_DIR -from core.config_manager.config_manager import ConfigManager from core.instance_manager.base_instance import BaseInstance +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + SimpleConfigParser, +) from utils.constants import SYSTEMD from utils.logger import Logger @@ -144,11 +147,12 @@ class Moonraker(BaseInstance): ) return env_file_content - def _get_port(self) -> Union[int, None]: + def _get_port(self) -> int | None: if not self.cfg_file.is_file(): return None - cm = ConfigManager(cfg_file=self.cfg_file) - port = cm.get_value("server", "port") + scp = SimpleConfigParser() + scp.read(self.cfg_file) + port = scp.getint("server", "port", fallback=None) - return int(port) if port is not None else port + return port diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 7312bbd..a16ef82 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -23,8 +23,10 @@ from components.webui_client.base_data import BaseWebClient from components.webui_client.client_utils import enable_mainsail_remotemode from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager -from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + SimpleConfigParser, +) from utils.common import get_install_status from utils.logger import Logger from utils.sys_utils import ( @@ -76,13 +78,21 @@ def create_example_moonraker_conf( ip.extend(["0", "0/16"]) uds = instance.comms_dir.joinpath("klippy.sock") - cm = ConfigManager(target) - trusted_clients = f"\n{'.'.join(ip)}" - trusted_clients += cm.get_value("authorization", "trusted_clients") + scp = SimpleConfigParser() + scp.read(target) + trusted_clients: List[str] = [ + ".".join(ip), + *scp.get("authorization", "trusted_clients"), + ] - cm.set_value("server", "port", str(port)) - cm.set_value("server", "klippy_uds_address", str(uds)) - cm.set_value("authorization", "trusted_clients", trusted_clients) + scp.set("server", "port", str(port)) + scp.set("server", "klippy_uds_address", str(uds)) + scp.set( + "authorization", + "trusted_clients", + "\n".join(trusted_clients), + True, + ) # add existing client and client configs in the update section if clients is not None and len(clients) > 0: @@ -95,9 +105,9 @@ def create_example_moonraker_conf( ("repo", c.repo_path), ("path", c.client_dir), ] - cm.config.add_section(section=c_section) + scp.add_section(section=c_section) for option in c_options: - cm.config.set(c_section, option[0], option[1]) + scp.set(c_section, option[0], option[1]) # client config part c_config = c.client_config @@ -110,11 +120,11 @@ def create_example_moonraker_conf( ("origin", c_config.repo_url), ("managed_services", "klipper"), ] - cm.config.add_section(section=c_config_section) + scp.add_section(section=c_config_section) for option in c_config_options: - cm.config.set(c_config_section, option[0], option[1]) + scp.set(c_config_section, option[0], option[1]) - cm.write_config() + scp.write(target) Logger.print_ok(f"Example moonraker.conf created in '{instance.cfg_dir}'") @@ -150,14 +160,15 @@ def moonraker_to_multi_conversion(new_name: str) -> None: im.current_instance = new_instance # patch the server sections klippy_uds_address value to match the new printer_data foldername - cm = ConfigManager(new_instance.cfg_file) - if cm.config.has_section("server"): - cm.set_value( + scp = SimpleConfigParser() + scp.read(new_instance.cfg_file) + if scp.has_section("server"): + scp.set( "server", "klippy_uds_address", str(new_instance.comms_dir.joinpath("klippy.sock")), ) - cm.write_config() + scp.write(new_instance.cfg_file) # create, enable and start the new moonraker instance im.create_instance() diff --git a/kiauh/core/config_manager/__init__.py b/kiauh/core/config_manager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/config_manager/config_manager.py b/kiauh/core/config_manager/config_manager.py deleted file mode 100644 index 7167cb9..0000000 --- a/kiauh/core/config_manager/config_manager.py +++ /dev/null @@ -1,83 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2024 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 # -# ======================================================================= # - -import configparser -from pathlib import Path -from typing import Union - -from utils.logger import Logger - - -# noinspection PyMethodMayBeStatic -class ConfigManager: - def __init__(self, cfg_file: Path): - self.config_file = cfg_file - self.config = CustomConfigParser() - - if cfg_file.is_file(): - self.read_config() - - def read_config(self) -> None: - if not self.config_file: - Logger.print_error("Unable to read config file. File not found.") - return - - self.config.read_file(open(self.config_file, "r")) - - def write_config(self) -> None: - with open(self.config_file, "w") as cfg: - self.config.write(cfg) - - def get_value(self, section: str, key: str, silent=True) -> Union[str, bool, None]: - if not self.config.has_section(section): - if not silent: - log = f"Section not defined. Unable to read section: [{section}]." - Logger.print_error(log) - return None - - if not self.config.has_option(section, key): - if not silent: - log = f"Option not defined in section [{section}]. Unable to read option: '{key}'." - Logger.print_error(log) - return None - - value = self.config.get(section, key) - if value == "True" or value == "true": - return True - elif value == "False" or value == "false": - return False - else: - return value - - def set_value(self, section: str, key: str, value: str): - self.config.set(section, key, value) - - -class CustomConfigParser(configparser.ConfigParser): - """ - A custom ConfigParser class overwriting the write() method of configparser.Configparser. - Key and value will be delimited by a ": ". - Note the whitespace AFTER the colon, which is the whole reason for that overwrite. - """ - - def write(self, fp, space_around_delimiters=False): - if self._defaults: - fp.write("[%s]\n" % configparser.DEFAULTSECT) - for key, value in self._defaults.items(): - fp.write("%s: %s\n" % (key, str(value).replace("\n", "\n\t"))) - fp.write("\n") - for section in self._sections: - fp.write("[%s]\n" % section) - for key, value in self._sections[section].items(): - if key == "__name__": - continue - if (value is not None) or (self._optcre == self.OPTCRE): - key = ": ".join((key, str(value).replace("\n", "\n\t"))) - fp.write("%s\n" % key) - fp.write("\n") diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py index 54106b8..3bfeb54 100644 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py @@ -13,8 +13,10 @@ from typing import List from components.klipper.klipper import Klipper from core.backup_manager.backup_manager import BackupManager -from core.config_manager.config_manager import ConfigManager from core.instance_manager.instance_manager import InstanceManager +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + SimpleConfigParser, +) from extensions.base_extension import BaseExtension from extensions.gcode_shell_cmd import ( EXAMPLE_CFG_SRC, @@ -118,10 +120,11 @@ class GcodeShellCmdExtension(BaseExtension): cfg_files = [instance.cfg_file for instance in instances] for cfg_file in cfg_files: Logger.print_status(f"Include shell_command.cfg in '{cfg_file}' ...") - cm = ConfigManager(cfg_file) - if cm.config.has_section(section): + scp = SimpleConfigParser() + scp.read(cfg_file) + if scp.has_section(section): Logger.print_info("Section already defined! Skipping ...") continue - cm.config.add_section(section) - cm.write_config() + scp.add_section(section) + scp.write(cfg_file) Logger.print_ok("Done!") diff --git a/kiauh/utils/config_utils.py b/kiauh/utils/config_utils.py index 52e1c7e..2a9d829 100644 --- a/kiauh/utils/config_utils.py +++ b/kiauh/utils/config_utils.py @@ -12,7 +12,9 @@ from typing import List, Optional, Tuple, TypeVar from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker -from core.config_manager.config_manager import ConfigManager +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + SimpleConfigParser, +) from utils.logger import Logger B = TypeVar("B", Klipper, Moonraker) @@ -32,27 +34,30 @@ def add_config_section( Logger.print_warn(f"'{cfg_file}' not found!") continue - cm = ConfigManager(cfg_file) - if cm.config.has_section(section): + scp = SimpleConfigParser() + scp.read(cfg_file) + if scp.has_section(section): Logger.print_info("Section already exist. Skipped ...") continue - cm.config.add_section(section) + scp.add_section(section) if options is not None: for option in options: - cm.config.set(section, option[0], option[1]) + scp.set(section, option[0], option[1]) - cm.write_config() + scp.write(cfg_file) def add_config_section_at_top(section: str, instances: List[B]): + # TODO: this could be implemented natively in SimpleConfigParser for instance in instances: tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False) tmp_cfg_path = Path(tmp_cfg.name) - cmt = ConfigManager(tmp_cfg_path) - cmt.config.add_section(section) - cmt.write_config() + scp = SimpleConfigParser() + scp.read(tmp_cfg_path) + scp.add_section(section) + scp.write(tmp_cfg_path) tmp_cfg.close() cfg_file = instance.cfg_file @@ -74,10 +79,11 @@ def remove_config_section(section: str, instances: List[B]) -> None: Logger.print_warn(f"'{cfg_file}' not found!") continue - cm = ConfigManager(cfg_file) - if not cm.config.has_section(section): + scp = SimpleConfigParser() + scp.read(cfg_file) + if not scp.has_section(section): Logger.print_info("Section does not exist. Skipped ...") continue - cm.config.remove_section(section) - cm.write_config() + scp.remove_section(section) + scp.write(cfg_file) From 08640e5b177595f5e84d4bf9194311282b1a91e2 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 19 Jun 2024 20:06:45 +0200 Subject: [PATCH 245/296] Squashed 'kiauh/core/submodules/simple_config_parser/' changes from 4d60d30..2698f60 2698f60 refactor: reset state on read method call git-subtree-dir: kiauh/core/submodules/simple_config_parser git-subtree-split: 2698f600e4bef3197d696a798f2c3436dabe836a --- .../simple_config_parser.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/simple_config_parser/simple_config_parser.py b/src/simple_config_parser/simple_config_parser.py index 38cf2d9..409161c 100644 --- a/src/simple_config_parser/simple_config_parser.py +++ b/src/simple_config_parser/simple_config_parser.py @@ -111,7 +111,13 @@ class SimpleConfigParser: self.in_option_block: bool = False # whether we are in a multiline option block def read(self, file: Path) -> None: - """Read the given file and store the result in the internal state""" + """ + Read the given file and store the result in the internal state. + Call this method before using any other methods. Calling this method + multiple times will reset the internal state on each call. + """ + + self._reset_state() try: with open(file, "r") as f: @@ -120,6 +126,16 @@ class SimpleConfigParser: except OSError: raise + def _reset_state(self): + """Reset the internal state.""" + + self._config.clear() + self._header.clear() + self._all_sections.clear() + self._all_options.clear() + self.section_name = "" + self.in_option_block = False + def write(self, filename): """Write the internal state to the given file""" From 5c090e88c3b78f7c292f5edfbb18b4fa340f73a7 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Wed, 19 Jun 2024 20:12:39 +0200 Subject: [PATCH 246/296] refactor(settings): use SimpleConfigParser for KiauhSettings Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 2 +- kiauh/components/klipper/klipper_setup.py | 8 +- .../components/klipperscreen/klipperscreen.py | 2 +- kiauh/components/mobileraker/mobileraker.py | 2 +- kiauh/components/moonraker/moonraker_setup.py | 8 +- .../client_config/client_config_setup.py | 2 +- kiauh/core/menus/settings_menu.py | 55 ++---- kiauh/core/settings/kiauh_settings.py | 163 ++++++++++++++---- 8 files changed, 158 insertions(+), 84 deletions(-) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index 7d4b789..99a30db 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -121,7 +121,7 @@ def update_crowsnest() -> None: Logger.print_status("Updating Crowsnest ...") settings = KiauhSettings() - if settings.get("kiauh", "backup_before_update"): + if settings.kiauh.backup_before_update: bm = BackupManager() bm.backup_directory( "crowsnest", diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 9f23259..55ad42b 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -109,8 +109,8 @@ def install_klipper() -> None: def setup_klipper_prerequesites() -> None: settings = KiauhSettings() - repo = settings.get("klipper", "repo_url") - branch = settings.get("klipper", "branch") + repo = settings.klipper.repo_url + branch = settings.klipper.branch git_clone_wrapper(repo, KLIPPER_DIR, branch) @@ -144,13 +144,13 @@ def update_klipper() -> None: return settings = KiauhSettings() - if settings.get("kiauh", "backup_before_update"): + if settings.kiauh.backup_before_update: backup_klipper_dir() instance_manager = InstanceManager(Klipper) instance_manager.stop_all_instance() - git_pull_wrapper(repo=settings.get("klipper", "repo_url"), target_dir=KLIPPER_DIR) + git_pull_wrapper(repo=settings.klipper.repo_url, target_dir=KLIPPER_DIR) # install possible new system packages install_klipper_packages(KLIPPER_DIR) diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index 1b2ce89..ae522c5 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -124,7 +124,7 @@ def update_klipperscreen() -> None: cmd_sysctl_service("KlipperScreen", "stop") settings = KiauhSettings() - if settings.get("kiauh", "backup_before_update"): + if settings.kiauh.backup_before_update: backup_klipperscreen_dir() git_pull_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR) diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py index 5f911de..17416a2 100644 --- a/kiauh/components/mobileraker/mobileraker.py +++ b/kiauh/components/mobileraker/mobileraker.py @@ -120,7 +120,7 @@ def update_mobileraker() -> None: cmd_sysctl_service("mobileraker", "stop") settings = KiauhSettings() - if settings.get("kiauh", "backup_before_update"): + if settings.kiauh.backup_before_update: backup_mobileraker_dir() git_pull_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index e7db113..0a05a3f 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -137,8 +137,8 @@ def check_moonraker_install_requirements() -> bool: def setup_moonraker_prerequesites() -> None: settings = KiauhSettings() - repo = settings.get("moonraker", "repo_url") - branch = settings.get("moonraker", "branch") + repo = settings.moonraker.repo_url + branch = settings.moonraker.branch git_clone_wrapper(repo, MOONRAKER_DIR, branch) @@ -200,14 +200,14 @@ def update_moonraker() -> None: return settings = KiauhSettings() - if settings.get("kiauh", "backup_before_update"): + if settings.kiauh.backup_before_update: backup_moonraker_dir() instance_manager = InstanceManager(Moonraker) instance_manager.stop_all_instance() git_pull_wrapper( - repo=settings.get("moonraker", "repo_url"), target_dir=MOONRAKER_DIR + repo=settings.moonraker.repo_url, target_dir=MOONRAKER_DIR ) # install possible new system packages diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index c5abf7f..05c26bb 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -102,7 +102,7 @@ def update_client_config(client: BaseWebClient) -> None: return settings = KiauhSettings() - if settings.get("kiauh", "backup_before_update"): + if settings.kiauh.backup_before_update: backup_client_config_data(client) git_pull_wrapper(client_config.repo_url, client_config.config_dir) diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 8870022..b2ba753 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -94,28 +94,19 @@ class SettingsMenu(BaseMenu): print(menu, end="") def _load_settings(self): - self.kiauh_settings = KiauhSettings() + self.settings = KiauhSettings() self._format_repo_str("klipper") self._format_repo_str("moonraker") - self.auto_backups_enabled = self.kiauh_settings.get( - "kiauh", - "backup_before_update", - ) - self.mainsail_unstable = self.kiauh_settings.get( - "mainsail", - "unstable_releases", - ) - self.fluidd_unstable = self.kiauh_settings.get( - "fluidd", - "unstable_releases", - ) + self.auto_backups_enabled = self.settings.kiauh.backup_before_update + self.mainsail_unstable = self.settings.mainsail.unstable_releases + self.fluidd_unstable = self.settings.fluidd.unstable_releases def _format_repo_str(self, repo_name: str) -> None: - repo = self.kiauh_settings.get(repo_name, "repo_url") + repo = self.settings.get(repo_name, "repo_url") repo = f"{'/'.join(repo.rsplit('/', 2)[-2:])}" - branch = self.kiauh_settings.get(repo_name, "branch") + branch = self.settings.get(repo_name, "branch") branch = f"({COLOR_CYAN}@ {branch}{RESET_FORMAT})" setattr(self, f"{repo_name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT} {branch}") @@ -156,9 +147,9 @@ class SettingsMenu(BaseMenu): ) if get_confirm("Apply changes?", allow_go_back=True): - self.kiauh_settings.set(repo_name, "repo_url", repo_url) - self.kiauh_settings.set(repo_name, "branch", branch) - self.kiauh_settings.save() + self.settings.set(repo_name, "repo_url", repo_url) + self.settings.set(repo_name, "branch", branch) + self.settings.save() self._load_settings() Logger.print_ok("Changes saved!") else: @@ -189,8 +180,8 @@ class SettingsMenu(BaseMenu): im = InstanceManager(_type) im.stop_all_instance() - repo = self.kiauh_settings.get(name, "repo_url") - branch = self.kiauh_settings.get(name, "branch") + repo = self.settings.get(name, "repo_url") + branch = self.settings.get(name, "branch") git_clone_wrapper(repo, target_dir, branch) im.start_all_instance() @@ -203,27 +194,15 @@ class SettingsMenu(BaseMenu): def toggle_mainsail_release(self, **kwargs): self.mainsail_unstable = not self.mainsail_unstable - self.kiauh_settings.set( - "mainsail", - "unstable_releases", - self.mainsail_unstable, - ) - self.kiauh_settings.save() + self.settings.mainsail.unstable_releases = self.mainsail_unstable + self.settings.save() def toggle_fluidd_release(self, **kwargs): self.fluidd_unstable = not self.fluidd_unstable - self.kiauh_settings.set( - "fluidd", - "unstable_releases", - self.fluidd_unstable, - ) - self.kiauh_settings.save() + self.settings.fluidd.unstable_releases = self.fluidd_unstable + self.settings.save() def toggle_backup_before_update(self, **kwargs): self.auto_backups_enabled = not self.auto_backups_enabled - self.kiauh_settings.set( - "kiauh", - "backup_before_update", - self.auto_backups_enabled, - ) - self.kiauh_settings.save() + self.settings.kiauh.backup_before_update = self.auto_backups_enabled + self.settings.save() diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index aacf3db..904dd69 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -6,12 +6,16 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations -import configparser import textwrap -from typing import Dict, Union +from typing import Union -from core.config_manager.config_manager import CustomConfigParser +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + NoOptionError, + NoSectionError, + SimpleConfigParser, +) from utils.constants import COLOR_RED, RESET_FORMAT from utils.logger import Logger from utils.sys_utils import kill @@ -19,6 +23,35 @@ from utils.sys_utils import kill from kiauh import PROJECT_ROOT +class AppSettings: + def __init__(self) -> None: + self.backup_before_update = None + + +class KlipperSettings: + def __init__(self) -> None: + self.repo_url = None + self.branch = None + + +class MoonrakerSettings: + def __init__(self) -> None: + self.repo_url = None + self.branch = None + + +class MainsailSettings: + def __init__(self) -> None: + self.port = None + self.unstable_releases = None + + +class FluiddSettings: + def __init__(self) -> None: + self.port = None + self.unstable_releases = None + + # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class KiauhSettings: @@ -36,32 +69,69 @@ class KiauhSettings: if self.__initialized: return self.__initialized = True - self.config = CustomConfigParser() - self.settings: Dict[str, Dict[str, Union[str, int, bool]]] = {} - self._load_settings() + self.config = SimpleConfigParser() + self.kiauh = AppSettings() + self.klipper = KlipperSettings() + self.moonraker = MoonrakerSettings() + self.mainsail = MainsailSettings() + self.fluidd = FluiddSettings() + + self.kiauh.backup_before_update = None + self.klipper.repo_url = None + self.klipper.branch = None + self.moonraker.repo_url = None + self.moonraker.branch = None + self.mainsail.port = None + self.mainsail.unstable_releases = None + self.fluidd.port = None + self.fluidd.unstable_releases = None + + self._load_config() def get(self, section: str, option: str) -> Union[str, int, bool]: - return self.settings[section][option] + """ + Get a value from the settings state by providing the section and option name as strings. + Prefer direct access to the properties, as it is usually safer! + :param section: The section name as string. + :param option: The option name as string. + :return: The value of the option as string, int or bool. + """ + + try: + section = getattr(self, section) + value = getattr(section, option) + return value + except AttributeError: + raise def set(self, section: str, option: str, value: Union[str, int, bool]) -> None: - self.settings[section][option] = value + """ + Set a value in the settings state by providing the section and option name as strings. + Prefer direct access to the properties, as it is usually safer! + :param section: The section name as string. + :param option: The option name as string. + :param value: The value to set as string, int or bool. + """ + try: + section = getattr(self, section) + section.option = value + except AttributeError: + raise def save(self) -> None: - for section, option in self.settings.items(): - self.config[section] = option - with open(self._custom_cfg, "w") as configfile: - self.config.write(configfile) - self._load_settings() + self._set_config_options() + self.config.write(self._custom_cfg) + self._load_config() - def _load_settings(self) -> None: - if self._custom_cfg.exists(): - self.config.read(self._custom_cfg) - elif self._default_cfg.exists(): - self.config.read(self._default_cfg) - else: + def _load_config(self) -> None: + if not self._custom_cfg.exists() or not self._default_cfg.exists(): self._kill() + + cfg = self._custom_cfg if self._custom_cfg.exists() else self._default_cfg + self.config.read(cfg) + self._validate_cfg() - self._parse_settings() + self._read_settings() def _validate_cfg(self) -> None: try: @@ -80,11 +150,11 @@ class KiauhSettings: err = f"Invalid value for option '{self._v_option}' in section '{self._v_section}'" Logger.print_error(err) kill() - except configparser.NoSectionError: + except NoSectionError: err = f"Missing section '{self._v_section}' in config file" Logger.print_error(err) kill() - except configparser.NoOptionError: + except NoOptionError: err = f"Missing option '{self._v_option}' in section '{self._v_section}'" Logger.print_error(err) kill() @@ -103,18 +173,43 @@ class KiauhSettings: if v.isdigit() or v.lower() == "true" or v.lower() == "false": raise ValueError - def _parse_settings(self): - for s in self.config.sections(): - self.settings[s] = {} - for o, v in self.config.items(s): - if v.lower() == "true": - self.settings[s][o] = True - elif v.lower() == "false": - self.settings[s][o] = False - elif v.isdigit(): - self.settings[s][o] = int(v) - else: - self.settings[s][o] = v + def _read_settings(self): + self.kiauh.backup_before_update = self.config.getboolean( + "kiauh", "backup_before_update" + ) + self.klipper.repo_url = self.config.get("klipper", "repo_url") + self.klipper.branch = self.config.get("klipper", "branch") + self.moonraker.repo_url = self.config.get("moonraker", "repo_url") + self.moonraker.branch = self.config.get("moonraker", "branch") + self.mainsail.port = self.config.getint("mainsail", "port") + self.mainsail.unstable_releases = self.config.getboolean( + "mainsail", "unstable_releases" + ) + self.fluidd.port = self.config.getint("fluidd", "port") + self.fluidd.unstable_releases = self.config.getboolean( + "fluidd", "unstable_releases" + ) + + def _set_config_options(self): + self.config.set( + "kiauh", + "backup_before_update", + str(self.kiauh.backup_before_update), + ) + self.config.set("klipper", "repo_url", self.klipper.repo_url) + self.config.set("klipper", "branch", self.klipper.branch) + self.config.set("moonraker", "repo_url", self.moonraker.repo_url) + self.config.set("moonraker", "branch", self.moonraker.branch) + self.config.set("mainsail", "port", str(self.mainsail.port)) + self.config.set( + "mainsail", + "unstable_releases", + str(self.mainsail.unstable_releases), + ) + self.config.set("fluidd", "port", str(self.fluidd.port)) + self.config.set( + "fluidd", "unstable_releases", str(self.fluidd.unstable_releases) + ) def _kill(self) -> None: l1 = "!!! ERROR !!!" From 93ba57923298288abec37908bc87e5a4456b6369 Mon Sep 17 00:00:00 2001 From: Staubgeborener Date: Wed, 19 Jun 2024 20:17:52 +0200 Subject: [PATCH 247/296] refactor(klipper_backup): replace is_service_installed with service_instance_exists (#481) * use service_instance_exists function * change header in __init__.py * remove is_service_installed function * small fix --- kiauh/extensions/klipper_backup/__init__.py | 1 + .../klipper_backup_extension.py | 31 +++++++------------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/kiauh/extensions/klipper_backup/__init__.py b/kiauh/extensions/klipper_backup/__init__.py index 6597237..e65e0f5 100644 --- a/kiauh/extensions/klipper_backup/__init__.py +++ b/kiauh/extensions/klipper_backup/__init__.py @@ -1,6 +1,7 @@ # ======================================================================= # # Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet # # https://github.com/Staubgeborener/klipper-backup # +# https://klipperbackup.xyz # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # diff --git a/kiauh/extensions/klipper_backup/klipper_backup_extension.py b/kiauh/extensions/klipper_backup/klipper_backup_extension.py index 0d545ba..c53471b 100644 --- a/kiauh/extensions/klipper_backup/klipper_backup_extension.py +++ b/kiauh/extensions/klipper_backup/klipper_backup_extension.py @@ -23,6 +23,7 @@ from extensions.klipper_backup import ( from utils.fs_utils import check_file_exist from utils.input_utils import get_confirm from utils.logger import Logger +from utils.sys_utils import service_instance_exists # noinspection PyMethodMayBeStatic @@ -44,14 +45,6 @@ class KlipperbackupExtension(BaseExtension): subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh"), "check_updates"]) def remove_extension(self, **kwargs) -> None: - def is_service_installed(service_name): - command = ["systemctl", "status", service_name] - result = subprocess.run(command, capture_output=True, text=True) - # Doesn't matter whether the service is active or not, what matters is whether it is installed. So let's search for "Loaded:" in stdout - if "Loaded:" in result.stdout: - return True - else: - return False def uninstall_service(service_name): try: @@ -60,7 +53,7 @@ class KlipperbackupExtension(BaseExtension): ["sudo", "systemctl", "disable", service_name], check=True ) subprocess.run(["sudo", "systemctl", "daemon-reload"], check=True) - service_path = f"/etc/systemd/system/{service_name}" + service_path = f"/etc/systemd/system/{service_name}.service" os.system(f"sudo rm {service_path}") return True except subprocess.CalledProcessError: @@ -114,30 +107,28 @@ class KlipperbackupExtension(BaseExtension): if get_confirm(question, True, False): # Remove Klipper-Backup services service_names = [ - "klipper-backup-on-boot.service", - "klipper-backup-filewatch.service", + "klipper-backup-on-boot", + "klipper-backup-filewatch", ] for service_name in service_names: try: Logger.print_status( - f"Check whether the service {service_name} is installed ..." + f"Check whether the {service_name} service is installed ..." ) - if is_service_installed(service_name): + if service_instance_exists(service_name): Logger.print_info(f"Service {service_name} detected.") if uninstall_service(service_name): Logger.print_ok( - f"The service {service_name} has been successfully uninstalled." + f"The {service_name} service has been successfully uninstalled." ) else: Logger.print_error( - f"Error uninstalling the service {service_name}." - ) + f"Error uninstalling the {service_name} service." + ) else: - Logger.print_info( - f"The service {service_name} is not installed. Skipping ..." - ) + Logger.print_info(f"Service {service_name} NOT detected.") except: - Logger.print_error(f"Unable to remove the service {service_name}") + Logger.print_error(f"Unable to remove the {service_name} service") # Remove Klipper-Backup cron Logger.print_status("Check for Klipper-Backup cron entry ...") From b758b3887b37d53c80c6c13243c941c0870cb7ed Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 22 Jun 2024 10:54:11 +0200 Subject: [PATCH 248/296] refactor: improve error logging on missing kiauh config file Signed-off-by: Dominik Willner --- kiauh/core/settings/kiauh_settings.py | 21 ++++++++++----------- kiauh/utils/logger.py | 12 ++++++++---- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 904dd69..9aa84d3 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -212,15 +212,14 @@ class KiauhSettings: ) def _kill(self) -> None: - l1 = "!!! ERROR !!!" - l2 = "No KIAUH configuration file found!" - error = textwrap.dedent( - f""" - {COLOR_RED}/=======================================================\\ - | {l1:^53} | - | {l2:^53} | - \=======================================================/{RESET_FORMAT} - """ - )[1:] - print(error, end="") + Logger.print_dialog( + DialogType.ERROR, + [ + "No KIAUH configuration file found! Please make sure you have at least " + "one of the following configuration files in KIAUH's root directory:", + "● default.kiauh.cfg", + "● kiauh.cfg", + ], + end="", + ) kill() diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index e8ba0a4..ef25ee5 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -120,15 +120,19 @@ class Logger: @staticmethod def _format_top_border(color: str) -> str: - return textwrap.dedent(f""" + return textwrap.dedent( + f""" {color}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - """)[:-1] + """ + )[1:-1] @staticmethod def _format_bottom_border() -> str: - return textwrap.dedent(f""" + return textwrap.dedent( + f""" ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - {RESET_FORMAT}""") + {RESET_FORMAT}""" + ) @staticmethod def _format_dialog_title(title: str) -> str: From af57b9670da972af90e175c53cdce75f17dac34e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 20 Jun 2024 21:04:57 +0200 Subject: [PATCH 249/296] fix: wrong condition in _load_config Signed-off-by: Dominik Willner --- kiauh/core/settings/kiauh_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 9aa84d3..eada99d 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -124,7 +124,7 @@ class KiauhSettings: self._load_config() def _load_config(self) -> None: - if not self._custom_cfg.exists() or not self._default_cfg.exists(): + if not self._custom_cfg.exists() and not self._default_cfg.exists(): self._kill() cfg = self._custom_cfg if self._custom_cfg.exists() else self._default_cfg From e63eb47ee93f93c5785cb7712aab835222b87a01 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 22 Jun 2024 10:58:43 +0200 Subject: [PATCH 250/296] refactor: extract config filenames into constants Signed-off-by: Dominik Willner --- default_kiauh.cfg => default.kiauh.cfg | 0 kiauh/core/settings/kiauh_settings.py | 15 +++++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) rename default_kiauh.cfg => default.kiauh.cfg (100%) diff --git a/default_kiauh.cfg b/default.kiauh.cfg similarity index 100% rename from default_kiauh.cfg rename to default.kiauh.cfg diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index eada99d..a00996d 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -8,7 +8,6 @@ # ======================================================================= # from __future__ import annotations -import textwrap from typing import Union from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( @@ -16,12 +15,14 @@ from core.submodules.simple_config_parser.src.simple_config_parser.simple_config NoSectionError, SimpleConfigParser, ) -from utils.constants import COLOR_RED, RESET_FORMAT -from utils.logger import Logger +from utils.logger import DialogType, Logger from utils.sys_utils import kill from kiauh import PROJECT_ROOT +DEFAULT_CFG = PROJECT_ROOT.joinpath("default.kiauh.cfg") +CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg") + class AppSettings: def __init__(self) -> None: @@ -56,8 +57,6 @@ class FluiddSettings: # noinspection PyMethodMayBeStatic class KiauhSettings: _instance = None - _default_cfg = PROJECT_ROOT.joinpath("default_kiauh.cfg") - _custom_cfg = PROJECT_ROOT.joinpath("kiauh.cfg") def __new__(cls, *args, **kwargs) -> "KiauhSettings": if cls._instance is None: @@ -120,14 +119,14 @@ class KiauhSettings: def save(self) -> None: self._set_config_options() - self.config.write(self._custom_cfg) + self.config.write(CUSTOM_CFG) self._load_config() def _load_config(self) -> None: - if not self._custom_cfg.exists() and not self._default_cfg.exists(): + if not CUSTOM_CFG.exists() or not DEFAULT_CFG.exists(): self._kill() - cfg = self._custom_cfg if self._custom_cfg.exists() else self._default_cfg + cfg = CUSTOM_CFG if CUSTOM_CFG.exists() else DEFAULT_CFG self.config.read(cfg) self._validate_cfg() From 205c84b3c31358c96a56b0f979b1f273ea874285 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 22 Jun 2024 12:30:29 +0200 Subject: [PATCH 251/296] refactor: make menus more visually appealing Signed-off-by: Dominik Willner --- .../klipper/menus/klipper_remove_menu.py | 29 ++-- .../menus/klipper_build_menu.py | 16 +-- .../menus/klipper_flash_error_menu.py | 48 +++---- .../menus/klipper_flash_help_menu.py | 118 ++++++++-------- .../menus/klipper_flash_menu.py | 131 +++++++++--------- .../log_uploads/menus/log_upload_menu.py | 13 +- .../moonraker/menus/moonraker_remove_menu.py | 31 +++-- .../webui_client/menus/client_remove_menu.py | 27 ++-- kiauh/core/menus/advanced_menu.py | 33 ++--- kiauh/core/menus/backup_menu.py | 29 ++-- kiauh/core/menus/base_menu.py | 29 ++-- kiauh/core/menus/install_menu.py | 31 +++-- kiauh/core/menus/main_menu.py | 46 +++--- kiauh/core/menus/remove_menu.py | 27 ++-- kiauh/core/menus/settings_menu.py | 49 +++---- kiauh/core/menus/update_menu.py | 47 ++++--- kiauh/extensions/extensions_menu.py | 34 ++--- 17 files changed, 374 insertions(+), 364 deletions(-) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index 07e24a3..b80983b 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -56,20 +56,21 @@ class KlipperRemoveMenu(BaseMenu): o4 = checked if self.delete_klipper_logs else unchecked menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Enter a number and hit enter to select / deselect | - | the specific option for removal. | - |-------------------------------------------------------| - | 0) Select everything | - |-------------------------------------------------------| - | 1) {o1} Remove Service | - | 2) {o2} Remove Local Repository | - | 3) {o3} Remove Python Environment | - | 4) {o4} Delete all Log-Files | - |-------------------------------------------------------| - | C) Continue | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ Enter a number and hit enter to select / deselect ║ + ║ the specific option for removal. ║ + ╟───────────────────────────────────────────────────────╢ + ║ 0) Select everything ║ + ╟───────────────────────────────────────────────────────╢ + ║ 1) {o1} Remove Service ║ + ║ 2) {o2} Remove Local Repository ║ + ║ 3) {o3} Remove Python Environment ║ + ║ 4) {o4} Delete all Log-Files ║ + ╟───────────────────────────────────────────────────────╢ + ║ C) Continue ║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py index a9768b9..f878047 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -57,11 +57,11 @@ class KlipperBuildFirmwareMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | The following dependencies are required: | - | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ The following dependencies are required: ║ + ║ ║ """ )[1:] @@ -71,15 +71,15 @@ class KlipperBuildFirmwareMenu(BaseMenu): status = status_missing if d in self.missing_deps else status_ok padding = 39 - len(d) + len(status) + (len(status_ok) - len(status)) d = f" {COLOR_CYAN}● {d}{RESET_FORMAT}" - menu += f"| {d}{status:>{padding}} |\n" + menu += f"║ {d}{status:>{padding}} ║\n" + menu += "║ ║\n" - menu += "| |\n" if len(self.missing_deps) == 0: line = f"{COLOR_GREEN}All dependencies are met!{RESET_FORMAT}" else: line = f"{COLOR_RED}Dependencies are missing!{RESET_FORMAT}" - menu += f"| {line:<62} |\n" + menu += f"║ {line:<62} ║\n" print(menu, end="") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py index 0077d39..1fae726 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py @@ -39,22 +39,22 @@ class KlipperNoFirmwareErrorMenu(BaseMenu): line1 = f"{color}Unable to find a compiled firmware file!{RESET_FORMAT}" menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {line1:<62} | - | | - | Make sure, that: | - | ● the folder '~/klipper/out' and its content exist | - | ● the folder contains the following file: | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ {line1:<62} ║ + ║ ║ + ║ Make sure, that: ║ + ║ ● the folder '~/klipper/out' and its content exist ║ + ║ ● the folder contains the following file: ║ """ )[1:] if self.flash_options.flash_method is FlashMethod.REGULAR: - menu += "| ● 'klipper.elf' |\n" - menu += "| ● 'klipper.elf.hex' |\n" + menu += "║ ● 'klipper.elf' ║\n" + menu += "║ ● 'klipper.elf.hex' ║\n" else: - menu += "| ● 'klipper.bin' |\n" + menu += "║ ● 'klipper.bin' ║\n" print(menu, end="") @@ -86,19 +86,19 @@ class KlipperNoBoardTypesErrorMenu(BaseMenu): line1 = f"{color}Reading the list of supported boards failed!{RESET_FORMAT}" menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {line1:<62} | - | | - | Make sure, that: | - | ● the folder '~/klipper' and all its content exist | - | ● the content of folder '~/klipper' is not currupted | - | ● the file '~/klipper/scripts/flash-sd.py' exist | - | ● your current user has access to those files/folders | - | | - | If in doubt or this process continues to fail, please | - | consider to download Klipper again. | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ {line1:<62} ║ + ║ ║ + ║ Make sure, that: ║ + ║ ● the folder '~/klipper' and all its content exist ║ + ║ ● the content of folder '~/klipper' is not currupted ║ + ║ ● the file '~/klipper/scripts/flash-sd.py' exist ║ + ║ ● your current user has access to those files/folders ║ + ║ ║ + ║ If in doubt or this process continues to fail, please ║ + ║ consider to download Klipper again. ║ """ )[1:] print(menu, end="") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py index 4cebafd..bee7a34 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py @@ -39,32 +39,33 @@ class KlipperFlashMethodHelpMenu(BaseMenu): subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}" menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {subheader1:<62} | - | The default method to flash controller boards which | - | are connected and updated over USB and not by placing | - | a compiled firmware file onto an internal SD-Card. | - | | - | Common controllers that get flashed that way are: | - | - Arduino Mega 2560 | - | - Fysetc F6 / S6 (used without a Display + SD-Slot) | - | | - | {subheader2:<62} | - | Many popular controller boards ship with a bootloader | - | capable of updating the firmware via SD-Card. | - | Choose this method if your controller board supports | - | this way of updating. This method ONLY works for up- | - | grading firmware. The initial flashing procedure must | - | be done manually per the instructions that apply to | - | your controller board. | - | | - | Common controllers that can be flashed that way are: | - | - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 | - | - Fysetc F6 / S6 (used with a Display + SD-Slot) | - | - Fysetc Spider | - | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ {subheader1:<62} ║ + ║ The default method to flash controller boards which ║ + ║ are connected and updated over USB and not by placing ║ + ║ a compiled firmware file onto an internal SD-Card. ║ + ║ ║ + ║ Common controllers that get flashed that way are: ║ + ║ - Arduino Mega 2560 ║ + ║ - Fysetc F6 / S6 (used without a Display + SD-Slot) ║ + ║ ║ + ║ {subheader2:<62} ║ + ║ Many popular controller boards ship with a bootloader ║ + ║ capable of updating the firmware via SD-Card. ║ + ║ Choose this method if your controller board supports ║ + ║ this way of updating. This method ONLY works for up- ║ + ║ grading firmware. The initial flashing procedure must ║ + ║ be done manually per the instructions that apply to ║ + ║ your controller board. ║ + ║ ║ + ║ Common controllers that can be flashed that way are: ║ + ║ - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 ║ + ║ - Fysetc F6 / S6 (used with a Display + SD-Slot) ║ + ║ - Fysetc Spider ║ + ║ ║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] print(menu, end="") @@ -96,19 +97,19 @@ class KlipperFlashCommandHelpMenu(BaseMenu): subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}" menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {subheader1:<62} | - | The default command to flash controller board, it | - | will detect selected microcontroller and use suitable | - | tool for flashing it. | - | | - | {subheader2:<62} | - | Special command to flash STM32 microcontrollers in | - | DFU mode but connected via serial. stm32flash command | - | will be used internally. | - | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ {subheader1:<62} ║ + ║ The default command to flash controller board, it ║ + ║ will detect selected microcontroller and use suitable ║ + ║ tool for flashing it. ║ + ║ ║ + ║ {subheader2:<62} ║ + ║ Special command to flash STM32 microcontrollers in ║ + ║ DFU mode but connected via serial. stm32flash command ║ + ║ will be used internally. ║ + ║ ║ """ )[1:] print(menu, end="") @@ -142,25 +143,26 @@ class KlipperMcuConnectionHelpMenu(BaseMenu): subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}" menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {subheader1:<62} | - | Selecting USB as the connection method will scan the | - | USB ports for connected controller boards. This will | - | be similar to the 'ls /dev/serial/by-id/*' command | - | suggested by the official Klipper documentation for | - | determining successfull USB connections! | - | | - | {subheader2:<62} | - | Selecting UART as the connection method will list all | - | possible UART serial ports. Note: This method ALWAYS | - | returns something as it seems impossible to determine | - | if a valid Klipper controller board is connected or | - | not. Because of that, you MUST know which UART serial | - | port your controller board is connected to when using | - | this connection method. | - | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ {subheader1:<62} ║ + ║ Selecting USB as the connection method will scan the ║ + ║ USB ports for connected controller boards. This will ║ + ║ be similar to the 'ls /dev/serial/by-id/*' command ║ + ║ suggested by the official Klipper documentation for ║ + ║ determining successfull USB connections! ║ + ║ ║ + ║ {subheader2:<62} ║ + ║ Selecting UART as the connection method will list all ║ + ║ possible UART serial ports. Note: This method ALWAYS ║ + ║ returns something as it seems impossible to determine ║ + ║ if a valid Klipper controller board is connected or ║ + ║ not. Because of that, you MUST know which UART serial ║ + ║ port your controller board is connected to when using ║ + ║ this connection method. ║ + ║ ║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index 07c1ca5..eccd772 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -38,7 +38,7 @@ from core.menus import FooterType, Option from core.menus.base_menu import BaseMenu from utils.constants import COLOR_CYAN, COLOR_RED, COLOR_YELLOW, RESET_FORMAT from utils.input_utils import get_number_input -from utils.logger import Logger +from utils.logger import DialogType, Logger # noinspection PyUnusedLocal @@ -74,19 +74,18 @@ class KlipperFlashMethodMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Select the flash method for flashing the MCU. | - | | - | {subheader:<62} | - | {subline1:<62} | - | {subline2:<62} | - |-------------------------------------------------------| - | | - | 1) Regular flashing method | - | 2) Updating via SD-Card Update | - | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ Select the flash method for flashing the MCU. ║ + ║ ║ + ║ {subheader:<62} ║ + ║ {subline1:<62} ║ + ║ {subline2:<62} ║ + ╟───────────────────────────────────────────────────────╢ + ║ 1) Regular flashing method ║ + ║ 2) Updating via SD-Card Update ║ + ╟───────────────────────────┬───────────────────────────╢ """ )[1:] print(menu, end="") @@ -131,12 +130,12 @@ class KlipperFlashCommandMenu(BaseMenu): def print_menu(self) -> None: menu = textwrap.dedent( """ - /=======================================================\\ - | | - | Which flash command to use for flashing the MCU? | - | 1) make flash (default) | - | 2) make serialflash (stm32flash) | - | | + ╔═══════════════════════════════════════════════════════╗ + ║ Which flash command to use for flashing the MCU? ║ + ╟───────────────────────────────────────────────────────╢ + ║ 1) make flash (default) ║ + ║ 2) make serialflash (stm32flash) ║ + ╟───────────────────────────┬───────────────────────────╢ """ )[1:] print(menu, end="") @@ -185,15 +184,15 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | | - | How is the controller board connected to the host? | - | 1) USB | - | 2) UART | - | 3) USB (DFU mode) | - | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ How is the controller board connected to the host? ║ + ╟───────────────────────────────────────────────────────╢ + ║ 1) USB ║ + ║ 2) UART ║ + ║ 3) USB (DFU mode) ║ + ╟───────────────────────────┬───────────────────────────╢ """ )[1:] print(menu, end="") @@ -271,20 +270,20 @@ class KlipperSelectMcuIdMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Make sure, to select the correct MCU! | - | ONLY flash a firmware created for the respective MCU! | - | | - |{header2:-^64}| - + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ Make sure, to select the correct MCU! ║ + ║ ONLY flash a firmware created for the respective MCU! ║ + ║ ║ + ╟{header2:─^64}╢ """ )[1:] for i, mcu in enumerate(self.mcu_list): mcu = mcu.split("/")[-1] menu += f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n" + menu += "╟───────────────────────────┬───────────────────────────╢" print(menu, end="\n") @@ -325,12 +324,12 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu): else: menu = textwrap.dedent( """ - /=======================================================\\ - | Please select the type of board that corresponds to | - | the currently selected MCU ID you chose before. | - | | - | The following boards are currently supported: | - |-------------------------------------------------------| + ╔═══════════════════════════════════════════════════════╗ + ║ Please select the type of board that corresponds to ║ + ║ the currently selected MCU ID you chose before. ║ + ║ ║ + ║ The following boards are currently supported: ║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] @@ -346,17 +345,16 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu): self.baudrate_select() def baudrate_select(self, **kwargs): - menu = textwrap.dedent( - """ - /=======================================================\\ - | If your board is flashed with firmware that connects | - | at a custom baud rate, please change it now. | - | | - | If you are unsure, stick to the default 250000! | - \\=======================================================/ - """ - )[1:] - print(menu, end="") + Logger.print_dialog( + DialogType.CUSTOM, + [ + "If your board is flashed with firmware that connects " + "at a custom baud rate, please change it now.", + "\n\n", + "If you are unsure, stick to the default 250000!", + ], + end="", + ) self.flash_options.selected_baudrate = get_number_input( question="Please set the baud rate", default=250000, @@ -399,16 +397,15 @@ class KlipperFlashOverviewMenu(BaseMenu): subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]" menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Before contuining the flashing process, please check | - | if all parameters were set correctly! Once you made | - | sure everything is correct, start the process. If any | - | parameter needs to be changed, you can go back (B) | - | step by step or abort and start from the beginning. | - |{subheader:-^64}| - + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ Before contuining the flashing process, please check ║ + ║ if all parameters were set correctly! Once you made ║ + ║ sure everything is correct, start the process. If any ║ + ║ parameter needs to be changed, you can go back (B) ║ + ║ step by step or abort and start from the beginning. ║ + ║{subheader:-^64}║ """ )[1:] @@ -423,9 +420,9 @@ class KlipperFlashOverviewMenu(BaseMenu): menu += textwrap.dedent( """ - |-------------------------------------------------------| - | Y) Start flash process | - | N) Abort - Return to Advanced Menu | + ╟───────────────────────────────────────────────────────╢ + ║ Y) Start flash process ║ + ║ N) Abort - Return to Advanced Menu ║ """ ) print(menu, end="") diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index 01f1675..c448533 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -42,17 +42,18 @@ class LogUploadMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | You can select the following logfiles for uploading: | - | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ You can select the following logfiles for uploading: ║ + ║ ║ """ )[1:] for logfile in enumerate(self.logfile_list): line = f"{logfile[0]}) {logfile[1].get('display_name')}" - menu += f"| {line:<54}|\n" + menu += f"║ {line:<54}║\n" + menu += "╟───────────────────────────────────────────────────────╢\n" print(menu, end="") diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 0ae48bc..85054dd 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -58,21 +58,22 @@ class MoonrakerRemoveMenu(BaseMenu): o5 = checked if self.delete_moonraker_logs else unchecked menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Enter a number and hit enter to select / deselect | - | the specific option for removal. | - |-------------------------------------------------------| - | 0) Select everything | - |-------------------------------------------------------| - | 1) {o1} Remove Service | - | 2) {o2} Remove Local Repository | - | 3) {o3} Remove Python Environment | - | 4) {o4} Remove Policy Kit Rules | - | 5) {o5} Delete all Log-Files | - |-------------------------------------------------------| - | C) Continue | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ Enter a number and hit enter to select / deselect ║ + ║ the specific option for removal. ║ + ╟───────────────────────────────────────────────────────╢ + ║ 0) Select everything ║ + ╟───────────────────────────────────────────────────────╢ + ║ 1) {o1} Remove Service ║ + ║ 2) {o2} Remove Local Repository ║ + ║ 3) {o3} Remove Python Environment ║ + ║ 4) {o4} Remove Policy Kit Rules ║ + ║ 5) {o5} Delete all Log-Files ║ + ╟───────────────────────────────────────────────────────╢ + ║ C) Continue ║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index f8f64ff..9374177 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -60,16 +60,16 @@ class ClientRemoveMenu(BaseMenu): o2 = checked if self.rm_client_config else unchecked menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Enter a number and hit enter to select / deselect | - | the specific option for removal. | - |-------------------------------------------------------| - | 0) Select everything | - |-------------------------------------------------------| - | 1) {o1} Remove {client_name:16} | - | 2) {o2} Remove {client_config_name:24} | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ Enter a number and hit enter to select / deselect ║ + ║ the specific option for removal. ║ + ╟───────────────────────────────────────────────────────╢ + ║ 0) Select everything ║ + ╟───────────────────────────────────────────────────────╢ + ║ 1) {o1} Remove {client_name:16} ║ + ║ 2) {o2} Remove {client_config_name:24} ║ """ )[1:] @@ -77,14 +77,15 @@ class ClientRemoveMenu(BaseMenu): o3 = checked if self.backup_mainsail_config_json else unchecked menu += textwrap.dedent( f""" - | 3) {o3} Backup config.json | + ║ 3) {o3} Backup config.json ║ """ )[1:] menu += textwrap.dedent( """ - |-------------------------------------------------------| - | C) Continue | + ╟───────────────────────────────────────────────────────╢ + ║ C) Continue ║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 30b8ef3..12eb537 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -43,12 +43,12 @@ class AdvancedMenu(BaseMenu): def set_options(self): self.options = { - "1": Option(method=self.klipper_rollback, menu=True), - "2": Option(method=self.moonraker_rollback, menu=True), - "3": Option(method=self.build, menu=True), - "4": Option(method=self.flash, menu=False), - "5": Option(method=self.build_flash, menu=False), - "6": Option(method=self.get_id, menu=False), + "1": Option(method=self.build, menu=True), + "2": Option(method=self.flash, menu=False), + "3": Option(method=self.build_flash, menu=False), + "4": Option(method=self.get_id, menu=False), + "5": Option(method=self.klipper_rollback, menu=True), + "6": Option(method=self.moonraker_rollback, menu=True), } def print_menu(self): @@ -57,18 +57,15 @@ class AdvancedMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Repo Rollback: | - | 1) [Klipper] | - | 2) [Moonraker] | - | | - | Klipper Firmware: | - | 3) [Build] | - | 4) [Flash] | - | 5) [Build + Flash] | - | 6) [Get MCU ID] | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────┬───────────────────────────╢ + ║ Klipper Firmware: │ Repository Rollback: ║ + ║ 1) [Build] │ 5) [Klipper] ║ + ║ 2) [Flash] │ 6) [Moonraker] ║ + ║ 3) [Build + Flash] │ ║ + ║ 4) [Get MCU ID] │ ║ + ╟───────────────────────────┴───────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index 2f9f39f..fffdea1 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -62,20 +62,21 @@ class BackupMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {line1:^62} | - |-------------------------------------------------------| - | Klipper & Moonraker API: | Client-Config: | - | 1) [Klipper] | 7) [Mainsail-Config] | - | 2) [Moonraker] | 8) [Fluidd-Config] | - | 3) [Config Folder] | | - | 4) [Moonraker Database] | Touchscreen GUI: | - | | 9) [KlipperScreen] | - | Webinterface: | | - | 5) [Mainsail] | | - | 6) [Fluidd] | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ {line1:^62} ║ + ╟───────────────────────────┬───────────────────────────╢ + ║ Klipper & Moonraker API: │ Client-Config: ║ + ║ 1) [Klipper] │ 7) [Mainsail-Config] ║ + ║ 2) [Moonraker] │ 8) [Fluidd-Config] ║ + ║ 3) [Config Folder] │ ║ + ║ 4) [Moonraker Database] │ Touchscreen GUI: ║ + ║ │ 9) [KlipperScreen] ║ + ║ Webinterface: │ ║ + ║ 5) [Mainsail] │ ║ + ║ 6) [Fluidd] │ ║ + ╟───────────────────────────┴───────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 6c0b9e3..2a0281d 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -39,11 +39,11 @@ def print_header(): count = 62 - len(color) - len(RESET_FORMAT) header = textwrap.dedent( f""" - /=======================================================\\ - | {color}{line1:~^{count}}{RESET_FORMAT} | - | {color}{line2:^{count}}{RESET_FORMAT} | - | {color}{line3:~^{count}}{RESET_FORMAT} | - \=======================================================/ + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{line1:~^{count}}{RESET_FORMAT} ║ + ║ {color}{line2:^{count}}{RESET_FORMAT} ║ + ║ {color}{line3:~^{count}}{RESET_FORMAT} ║ + ╚═══════════════════════════════════════════════════════╝ """ )[1:] print(header, end="") @@ -55,9 +55,8 @@ def print_quit_footer(): count = 62 - len(color) - len(RESET_FORMAT) footer = textwrap.dedent( f""" - |-------------------------------------------------------| - | {color}{text:^{count}}{RESET_FORMAT} | - \=======================================================/ + ║ {color}{text:^{count}}{RESET_FORMAT} ║ + ╚═══════════════════════════════════════════════════════╝ """ )[1:] print(footer, end="") @@ -69,9 +68,8 @@ def print_back_footer(): count = 62 - len(color) - len(RESET_FORMAT) footer = textwrap.dedent( f""" - |-------------------------------------------------------| - | {color}{text:^{count}}{RESET_FORMAT} | - \=======================================================/ + ║ {color}{text:^{count}}{RESET_FORMAT} ║ + ╚═══════════════════════════════════════════════════════╝ """ )[1:] print(footer, end="") @@ -85,16 +83,15 @@ def print_back_help_footer(): count = 34 - len(color1) - len(RESET_FORMAT) footer = textwrap.dedent( f""" - |-------------------------------------------------------| - | {color1}{text1:^{count}}{RESET_FORMAT} | {color2}{text2:^{count}}{RESET_FORMAT} | - \=======================================================/ + ║ {color1}{text1:^{count}}{RESET_FORMAT} │ {color2}{text2:^{count}}{RESET_FORMAT} ║ + ╚═══════════════════════════╧═══════════════════════════╝ """ )[1:] print(footer, end="") def print_blank_footer(): - print("\=======================================================/") + print("╚═══════════════════════════════════════════════════════╝") class PostInitCaller(type): @@ -168,7 +165,7 @@ class BaseMenu(metaclass=PostInitCaller): elif self.footer_type is FooterType.BLANK: print_blank_footer() else: - raise NotImplementedError + raise NotImplementedError("FooterType not correctly implemented!") def display_menu(self) -> None: if self.header: diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index c739da7..9458465 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -57,21 +57,22 @@ class InstallMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Firmware & API: | Touchscreen GUI: | - | 1) [Klipper] | 7) [KlipperScreen] | - | 2) [Moonraker] | | - | | Android / iOS: | - | Webinterface: | 8) [Mobileraker] | - | 3) [Mainsail] | | - | 4) [Fluidd] | Webcam Streamer: | - | | 9) [Crowsnest] | - | Client-Config: | | - | 5) [Mainsail-Config] | | - | 6) [Fluidd-Config] | | - | | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────┬───────────────────────────╢ + ║ Firmware & API: │ Touchscreen GUI: ║ + ║ 1) [Klipper] │ 7) [KlipperScreen] ║ + ║ 2) [Moonraker] │ ║ + ║ │ Android / iOS: ║ + ║ Webinterface: │ 8) [Mobileraker] ║ + ║ 3) [Mainsail] │ ║ + ║ 4) [Fluidd] │ Webcam Streamer: ║ + ║ │ 9) [Crowsnest] ║ + ║ Client-Config: │ ║ + ║ 5) [Mainsail-Config] │ ║ + ║ 6) [Fluidd-Config] │ ║ + ║ │ ║ + ╟───────────────────────────┴───────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 9fef36a..8de3a12 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -6,7 +6,7 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # - +import sys import textwrap from typing import Optional, Type @@ -39,6 +39,7 @@ from utils.constants import ( COLOR_YELLOW, RESET_FORMAT, ) +from utils.logger import Logger from utils.types import ComponentStatus @@ -117,7 +118,7 @@ class MainMenu(BaseMenu): self.fetch_status() header = " [ Main Menu ] " - footer1 = "KIAUH v6.0.0" + footer1 = f"{COLOR_CYAN}KIAUH v6.0.0{RESET_FORMAT}" footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}" color = COLOR_CYAN count = 62 - len(color) - len(RESET_FORMAT) @@ -125,28 +126,33 @@ class MainMenu(BaseMenu): pad2 = 26 menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | 0) [Log-Upload] | Klipper: {self.kl_status:<{pad1}} | - | | Repo: {self.kl_repo:<{pad1}} | - | 1) [Install] |------------------------------------| - | 2) [Update] | Moonraker: {self.mr_status:<{pad1}} | - | 3) [Remove] | Repo: {self.mr_repo:<{pad1}} | - | 4) [Advanced] |------------------------------------| - | 5) [Backup] | Mainsail: {self.ms_status:<{pad2}} | - | | Fluidd: {self.fl_status:<{pad2}} | - | S) [Settings] | Client-Config: {self.cc_status:<{pad2}} | - | | | - | Community: | KlipperScreen: {self.ks_status:<{pad2}} | - | E) [Extensions] | Mobileraker: {self.mb_status:<{pad2}} | - | | Crowsnest: {self.cn_status:<{pad2}} | - |-------------------------------------------------------| - | {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟──────────────────┬────────────────────────────────────╢ + ║ 0) [Log-Upload] │ Klipper: {self.kl_status:<{pad1}} ║ + ║ │ Repo: {self.kl_repo:<{pad1}} ║ + ║ 1) [Install] ├────────────────────────────────────╢ + ║ 2) [Update] │ Moonraker: {self.mr_status:<{pad1}} ║ + ║ 3) [Remove] │ Repo: {self.mr_repo:<{pad1}} ║ + ║ 4) [Advanced] ├────────────────────────────────────╢ + ║ 5) [Backup] │ Mainsail: {self.ms_status:<{pad2}} ║ + ║ │ Fluidd: {self.fl_status:<{pad2}} ║ + ║ S) [Settings] │ Client-Config: {self.cc_status:<{pad2}} ║ + ║ │ ║ + ║ Community: │ KlipperScreen: {self.ks_status:<{pad2}} ║ + ║ E) [Extensions] │ Mobileraker: {self.mb_status:<{pad2}} ║ + ║ │ Crowsnest: {self.cn_status:<{pad2}} ║ + ╟──────────────────┼────────────────────────────────────╢ + ║ {footer1:^25} │ {footer2:^43} ║ + ╟──────────────────┴────────────────────────────────────╢ """ )[1:] print(menu, end="") + def exit(self, **kwargs): + Logger.print_ok("###### Happy printing!", False) + sys.exit(0) + def log_upload_menu(self, **kwargs): LogUploadMenu().run() diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 82d4f30..d43f998 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -56,19 +56,20 @@ class RemoveMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | INFO: Configurations and/or any backups will be kept! | - |-------------------------------------------------------| - | Firmware & API: | Touchscreen GUI: | - | 1) [Klipper] | 5) [KlipperScreen] | - | 2) [Moonraker] | | - | | Android / iOS: | - | Klipper Webinterface: | 6) [Mobileraker] | - | 3) [Mainsail] | | - | 4) [Fluidd] | Webcam Streamer: | - | | 7) [Crowsnest] | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ INFO: Configurations and/or any backups will be kept! ║ + ╟───────────────────────────┬───────────────────────────╢ + ║ Firmware & API: │ Touchscreen GUI: ║ + ║ 1) [Klipper] │ 5) [KlipperScreen] ║ + ║ 2) [Moonraker] │ ║ + ║ │ Android / iOS: ║ + ║ Klipper Webinterface: │ 6) [Mobileraker] ║ + ║ 3) [Mainsail] │ ║ + ║ 4) [Fluidd] │ Webcam Streamer: ║ + ║ │ 7) [Crowsnest] ║ + ╟───────────────────────────┴───────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index b2ba753..579c098 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -65,30 +65,31 @@ class SettingsMenu(BaseMenu): o3 = checked if self.auto_backups_enabled else unchecked menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | Klipper source repository: | - | ● {self.klipper_repo:<67} | - | | - | Moonraker source repository: | - | ● {self.moonraker_repo:<67} | - | | - | Install unstable Webinterface releases: | - | {o1} Mainsail | - | {o2} Fluidd | - | | - | Auto-Backup: | - | {o3} Automatic backup before update | - | | - |-------------------------------------------------------| - | 1) Set Klipper source repository | - | 2) Set Moonraker source repository | - | | - | 3) Toggle unstable Mainsail releases | - | 4) Toggle unstable Fluidd releases | - | | - | 5) Toggle automatic backups before updates | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ Klipper source repository: ║ + ║ ● {self.klipper_repo:<67} ║ + ║ ║ + ║ Moonraker source repository: ║ + ║ ● {self.moonraker_repo:<67} ║ + ║ ║ + ║ Install unstable Webinterface releases: ║ + ║ {o1} Mainsail ║ + ║ {o2} Fluidd ║ + ║ ║ + ║ Auto-Backup: ║ + ║ {o3} Automatic backup before update ║ + ║ ║ + ╟───────────────────────────────────────────────────────╢ + ║ 1) Set Klipper source repository ║ + ║ 2) Set Moonraker source repository ║ + ║ ║ + ║ 3) Toggle unstable Mainsail releases ║ + ║ 4) Toggle unstable Fluidd releases ║ + ║ ║ + ║ 5) Toggle automatic backups before updates ║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 2efe153..b3cd783 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -93,29 +93,30 @@ class UpdateMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | 0) Update all | | | - | | Current: | Latest: | - | Klipper & API: |---------------|---------------| - | 1) Klipper | {self.kl_local:<22} | {self.kl_remote:<22} | - | 2) Moonraker | {self.mr_local:<22} | {self.mr_remote:<22} | - | | | | - | Webinterface: |---------------|---------------| - | 3) Mainsail | {self.ms_local:<22} | {self.ms_remote:<22} | - | 4) Fluidd | {self.fl_local:<22} | {self.fl_remote:<22} | - | | | | - | Client-Config: |---------------|---------------| - | 5) Mainsail-Config | {self.mc_local:<22} | {self.mc_remote:<22} | - | 6) Fluidd-Config | {self.fc_local:<22} | {self.fc_remote:<22} | - | | | | - | Other: |---------------|---------------| - | 7) KlipperScreen | {self.ks_local:<22} | {self.ks_remote:<22} | - | 8) Mobileraker | {self.mb_local:<22} | {self.mb_remote:<22} | - | 9) Crowsnest | {self.cn_local:<22} | {self.cn_remote:<22} | - | |-------------------------------| - | 10) System | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────┬───────────────┬───────────────╢ + ║ 0) Update all │ │ ║ + ║ │ Current: │ Latest: ║ + ║ Klipper & API: ├───────────────┼───────────────╢ + ║ 1) Klipper │ {self.kl_local:<22} │ {self.kl_remote:<22} ║ + ║ 2) Moonraker │ {self.mr_local:<22} │ {self.mr_remote:<22} ║ + ║ │ │ ║ + ║ Webinterface: ├───────────────┼───────────────╢ + ║ 3) Mainsail │ {self.ms_local:<22} │ {self.ms_remote:<22} ║ + ║ 4) Fluidd │ {self.fl_local:<22} │ {self.fl_remote:<22} ║ + ║ │ │ ║ + ║ Client-Config: ├───────────────┼───────────────╢ + ║ 5) Mainsail-Config │ {self.mc_local:<22} │ {self.mc_remote:<22} ║ + ║ 6) Fluidd-Config │ {self.fc_local:<22} │ {self.fc_remote:<22} ║ + ║ │ │ ║ + ║ Other: ├───────────────┼───────────────╢ + ║ 7) KlipperScreen │ {self.ks_local:<22} │ {self.ks_remote:<22} ║ + ║ 8) Mobileraker │ {self.mb_local:<22} │ {self.mb_remote:<22} ║ + ║ 9) Crowsnest │ {self.cn_local:<22} │ {self.cn_remote:<22} ║ + ║ ├───────────────┴───────────────╢ + ║ 10) System │ ║ + ╟───────────────────────┴───────────────────────────────╢ """ )[1:] print(menu, end="") diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py index 301b8bc..ac2678a 100644 --- a/kiauh/extensions/extensions_menu.py +++ b/kiauh/extensions/extensions_menu.py @@ -87,11 +87,11 @@ class ExtensionsMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| - | {line1:<62} | - | | + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ {line1:<62} ║ + ║ ║ """ )[1:] print(menu, end="") @@ -100,7 +100,8 @@ class ExtensionsMenu(BaseMenu): index = extension.metadata.get("index") name = extension.metadata.get("display_name") row = f"{index}) {name}" - print(f"| {row:<53} |") + print(f"║ {row:<53} ║") + print("╟───────────────────────────────────────────────────────╢") # noinspection PyUnusedLocal @@ -135,29 +136,30 @@ class ExtensionSubmenu(BaseMenu): description_text = Logger.format_content( description, line_width, - border_left="|", - border_right="|", + border_left="║", + border_right="║", ) menu = textwrap.dedent( f""" - /=======================================================\\ - | {color}{header:~^{count}}{RESET_FORMAT} | - |-------------------------------------------------------| + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] menu += f"{description_text}\n" menu += textwrap.dedent( """ - |-------------------------------------------------------| - | 1) Install | + ╟───────────────────────────────────────────────────────╢ + ║ 1) Install ║ """ )[1:] if self.extension.metadata.get("updates"): - menu += "| 2) Update |\n" - menu += "| 3) Remove |\n" + menu += "║ 2) Update ║\n" + menu += "║ 3) Remove ║\n" else: - menu += "| 2) Remove |\n" + menu += "║ 2) Remove ║\n" + menu += "╟───────────────────────────────────────────────────────╢\n" print(menu, end="") From cfc45a9746b19dbf0080c77635d6db12f348e520 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 22 Jun 2024 15:50:31 +0200 Subject: [PATCH 252/296] refactor: rework some klipper dialogs Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_dialogs.py | 106 ++++++-------------- kiauh/components/klipper/klipper_setup.py | 14 ++- kiauh/components/klipper/klipper_utils.py | 28 ++++-- 3 files changed, 59 insertions(+), 89 deletions(-) diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py index 7218c43..fc43c7a 100644 --- a/kiauh/components/klipper/klipper_dialogs.py +++ b/kiauh/components/klipper/klipper_dialogs.py @@ -13,7 +13,12 @@ from typing import List from core.instance_manager.base_instance import BaseInstance from core.menus.base_menu import print_back_footer -from utils.constants import COLOR_CYAN, COLOR_GREEN, COLOR_YELLOW, RESET_FORMAT +from utils.constants import ( + COLOR_CYAN, + COLOR_GREEN, + COLOR_YELLOW, + RESET_FORMAT, +) @unique @@ -29,7 +34,7 @@ def print_instance_overview( show_index=False, show_select_all=False, ): - dialog = "/=======================================================\\\n" + dialog = "╔═══════════════════════════════════════════════════════╗\n" if show_headline: d_type = ( "Klipper instances" @@ -37,13 +42,13 @@ def print_instance_overview( else "printer directories" ) headline = f"{COLOR_GREEN}The following {d_type} were found:{RESET_FORMAT}" - dialog += f"|{headline:^64}|\n" - dialog += "|-------------------------------------------------------|\n" + dialog += f"║{headline:^64}║\n" + dialog += "╟───────────────────────────────────────────────────────╢\n" if show_select_all: select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}" - dialog += f"| {select_all:<63}|\n" - dialog += "| |\n" + dialog += f"║ {select_all:<63}║\n" + dialog += "║ ║\n" for i, s in enumerate(instances): if display_type is DisplayType.SERVICE_NAME: @@ -51,7 +56,8 @@ def print_instance_overview( else: name = s.data_dir line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {name}{RESET_FORMAT}" - dialog += f"| {line:<63}|\n" + dialog += f"║ {line:<63}║\n" + dialog += "╟───────────────────────────────────────────────────────╢\n" print(dialog, end="") print_back_footer() @@ -62,13 +68,14 @@ def print_select_instance_count_dialog(): line2 = f"{COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT}" dialog = textwrap.dedent( f""" - /=======================================================\\ - | Please select the number of Klipper instances to set | - | up. The number of Klipper instances will determine | - | the amount of printers you can run from this host. | - | | - | {line1:<63}| - | {line2:<63}| + ╔═══════════════════════════════════════════════════════╗ + ║ Please select the number of Klipper instances to set ║ + ║ up. The number of Klipper instances will determine ║ + ║ the amount of printers you can run from this host. ║ + ║ ║ + ║ {line1:<63}║ + ║ {line2:<63}║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] @@ -81,71 +88,16 @@ def print_select_custom_name_dialog(): line2 = f"{COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT}" dialog = textwrap.dedent( f""" - /=======================================================\\ - | You can now assign a custom name to each instance. | - | If skipped, each instance will get an index assigned | - | in ascending order, starting at index '1'. | - | | - | {line1:<63}| - | {line2:<63}| + ╔═══════════════════════════════════════════════════════╗ + ║ You can now assign a custom name to each instance. ║ + ║ If skipped, each instance will get an index assigned ║ + ║ in ascending order, starting at index '1'. ║ + ║ ║ + ║ {line1:<63}║ + ║ {line2:<63}║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] print(dialog, end="") print_back_footer() - - -def print_missing_usergroup_dialog(missing_groups) -> None: - line1 = f"{COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT}" - line2 = f"{COLOR_CYAN}● tty{RESET_FORMAT}" - line3 = f"{COLOR_CYAN}● dialout{RESET_FORMAT}" - line4 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}" - line5 = f"{COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT}" - - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | {line1:<63}| - """ - )[1:] - - if "tty" in missing_groups: - dialog += f"| {line2:<63}|\n" - if "dialout" in missing_groups: - dialog += f"| {line3:<63}|\n" - - dialog += textwrap.dedent( - f""" - | | - | It is possible that you won't be able to successfully | - | connect and/or flash the controller board without | - | your user being a member of that group. | - | If you want to add the current user to the group(s) | - | listed above, answer with 'Y'. Else skip with 'n'. | - | | - | {line4:<63}| - | {line5:<63}| - \\=======================================================/ - """ - )[1:] - - print(dialog, end="") - - -def print_update_warn_dialog() -> None: - line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" - line2 = f"{COLOR_YELLOW}Do NOT continue if there are ongoing prints running!{RESET_FORMAT}" - line3 = f"{COLOR_YELLOW}All Klipper instances will be restarted during the {RESET_FORMAT}" - line4 = f"{COLOR_YELLOW}update process and ongoing prints WILL FAIL.{RESET_FORMAT}" - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | {line1:<63}| - | {line2:<63}| - | {line3:<63}| - | {line4:<63}| - \\=======================================================/ - """ - )[1:] - - print(dialog, end="") diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 55ad42b..4196f1f 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -16,7 +16,6 @@ from components.klipper import ( KLIPPER_REQUIREMENTS_TXT, ) from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import print_update_warn_dialog from components.klipper.klipper_utils import ( add_to_existing, backup_klipper_dir, @@ -39,7 +38,7 @@ from core.settings.kiauh_settings import KiauhSettings from utils.common import check_install_dependencies from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.input_utils import get_confirm -from utils.logger import Logger +from utils.logger import DialogType, Logger from utils.sys_utils import ( cmd_sysctl_manage, create_python_venv, @@ -139,7 +138,16 @@ def install_klipper_packages(klipper_dir: Path) -> None: def update_klipper() -> None: - print_update_warn_dialog() + Logger.print_dialog( + DialogType.WARNING, + [ + "Do NOT continue if there are ongoing prints running!", + "All Klipper instances will be restarted during the update process and " + "ongoing prints WILL FAIL.", + ], + end="", + ) + if not get_confirm("Update Klipper now?"): return diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 3f97d09..19633c4 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -23,7 +23,6 @@ from components.klipper import ( from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import ( print_instance_overview, - print_missing_usergroup_dialog, print_select_custom_name_dialog, print_select_instance_count_dialog, ) @@ -201,18 +200,29 @@ def klipper_to_multi_conversion(new_name: str) -> None: def check_user_groups(): - current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] - - missing_groups = [] - if "tty" not in current_groups: - missing_groups.append("tty") - if "dialout" not in current_groups: - missing_groups.append("dialout") + user_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] + missing_groups = [g for g in user_groups if g == "tty" or g == "dialout"] if not missing_groups: return - print_missing_usergroup_dialog(missing_groups) + Logger.print_dialog( + DialogType.ATTENTION, + [ + "Your current user is not in group:", + *[f"● {g}" for g in missing_groups], + "\n\n", + "It is possible that you won't be able to successfully connect and/or " + "flash the controller board without your user being a member of that " + "group. If you want to add the current user to the group(s) listed above, " + "answer with 'Y'. Else skip with 'n'.", + "\n\n", + "INFO:", + "Relog required for group assignments to take effect!", + ], + end="", + ) + if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): log = "Skipped adding user to required groups. You might encounter issues." Logger.warn(log) From 08c10fdded145300a19d9b03270f153be20aacad Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 22 Jun 2024 15:57:13 +0200 Subject: [PATCH 253/296] refactor: rework some moonraker dialogs Signed-off-by: Dominik Willner --- .../components/moonraker/moonraker_dialogs.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/kiauh/components/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py index f49d0a7..e425c21 100644 --- a/kiauh/components/moonraker/moonraker_dialogs.py +++ b/kiauh/components/moonraker/moonraker_dialogs.py @@ -25,16 +25,16 @@ def print_moonraker_overview( headline = f"{COLOR_GREEN}The following instances were found:{RESET_FORMAT}" dialog = textwrap.dedent( f""" - /=======================================================\\ - |{headline:^64}| - |-------------------------------------------------------| + ╔═══════════════════════════════════════════════════════╗ + ║{headline:^64}║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] if show_select_all: select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}" - dialog += f"| {select_all:<63}|\n" - dialog += "| |\n" + dialog += f"║ {select_all:<63}║\n" + dialog += "║ ║\n" instance_map = { k.get_service_file_name(): ( @@ -49,18 +49,19 @@ def print_moonraker_overview( mr_name = instance_map.get(k) m = f"<-> {mr_name}" if mr_name != "" else "" line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {k} {m} {RESET_FORMAT}" - dialog += f"| {line:<63}|\n" + dialog += f"║ {line:<63}║\n" warn_l1 = f"{COLOR_YELLOW}PLEASE NOTE: {RESET_FORMAT}" warn_l2 = f"{COLOR_YELLOW}If you select an instance with an existing Moonraker{RESET_FORMAT}" warn_l3 = f"{COLOR_YELLOW}instance, that Moonraker instance will be re-created!{RESET_FORMAT}" warning = textwrap.dedent( f""" - | | - |-------------------------------------------------------| - | {warn_l1:<63}| - | {warn_l2:<63}| - | {warn_l3:<63}| + ║ ║ + ╟───────────────────────────────────────────────────────╢ + ║ {warn_l1:<63}║ + ║ {warn_l2:<63}║ + ║ {warn_l3:<63}║ + ╟───────────────────────────────────────────────────────╢ """ )[1:] From 3734ef056836e8dfab59bb20dbb93605fc1e54c0 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 22 Jun 2024 18:08:00 +0200 Subject: [PATCH 254/296] feat(obico): add obico extension (#474) * feat(obico): add obico extension Signed-off-by: Dominik Willner * refactor: add obico to moonraker suffix blacklist Signed-off-by: Dominik Willner * fix: correctly recognize the suffix of the instance Signed-off-by: Dominik Willner * fix: fix logic of asking for linking Signed-off-by: Dominik Willner * Squashed 'kiauh/core/submodules/simple_config_parser/' changes from 2698f60..7aa6586 7aa6586 fix: sections can have hyphens in their second word 44cedf5 fix(tests): fix whitespaces in expected output git-subtree-dir: kiauh/core/submodules/simple_config_parser git-subtree-split: 7aa658654eeb08fd53831effbfba4503a61e0eff * refactor: use SimpleConfigParser and finalize the code Signed-off-by: Dominik Willner * fix: wrong condition in _load_config Signed-off-by: Dominik Willner * Squashed 'kiauh/core/submodules/simple_config_parser/' changes from 7aa6586..47c353f 47c353f refactor: improve section regex dd904bc test: add more test cases git-subtree-dir: kiauh/core/submodules/simple_config_parser git-subtree-split: 47c353f4e91e6be9605394b174834e1f34c9cfdf * Squashed 'kiauh/core/submodules/simple_config_parser/' changes from 47c353f..3655330 3655330 refactor: use pop() for removing elements from lists and dicts 99733f1 refactor: add empty options dict to _all_options on section parsing git-subtree-dir: kiauh/core/submodules/simple_config_parser git-subtree-split: 3655330d2156e13acffc56fac070ab8716444c85 * refactor: improve config creations and patching Signed-off-by: Dominik Willner --------- Signed-off-by: Dominik Willner --- kiauh/components/moonraker/moonraker.py | 2 +- .../core/instance_manager/instance_manager.py | 10 +- kiauh/core/settings/kiauh_settings.py | 2 +- .../simple_config_parser.py | 9 +- .../test_internal_state_changes.py | 1 + .../line_parsing/data/case_parse_option.py | 5 + .../line_parsing/data/case_parse_section.py | 2 + .../data/case_line_is_section.py | 6 + .../features/public_api/test_public_api.py | 2 +- kiauh/extensions/obico/__init__.py | 0 .../obico/assets/moonraker-obico.env | 1 + .../obico/assets/moonraker-obico.service | 16 + kiauh/extensions/obico/metadata.json | 16 + kiauh/extensions/obico/moonraker_obico.py | 178 ++++++++ .../obico/moonraker_obico_extension.py | 386 ++++++++++++++++++ kiauh/utils/config_utils.py | 2 +- 16 files changed, 627 insertions(+), 11 deletions(-) create mode 100644 kiauh/extensions/obico/__init__.py create mode 100644 kiauh/extensions/obico/assets/moonraker-obico.env create mode 100644 kiauh/extensions/obico/assets/moonraker-obico.service create mode 100644 kiauh/extensions/obico/metadata.json create mode 100644 kiauh/extensions/obico/moonraker_obico.py create mode 100644 kiauh/extensions/obico/moonraker_obico_extension.py diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index 761d729..e333e71 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -25,7 +25,7 @@ from utils.logger import Logger class Moonraker(BaseInstance): @classmethod def blacklist(cls) -> List[str]: - return ["None", "mcu"] + return ["None", "mcu", "obico"] def __init__(self, suffix: str = ""): super().__init__(instance_type=self, suffix=suffix) diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 205d280..7cfd857 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -172,14 +172,18 @@ class InstanceManager: ] instance_list = [ - self.instance_type(suffix=self._get_instance_suffix(service)) + self.instance_type(suffix=self._get_instance_suffix(name, service)) for service in service_list ] return sorted(instance_list, key=lambda x: self._sort_instance_list(x.suffix)) - def _get_instance_suffix(self, file_path: Path) -> str: - return file_path.stem.split("-")[-1] if "-" in file_path.stem else "" + def _get_instance_suffix(self, name: str, file_path: Path) -> str: + # to get the suffix of the instance, we remove the name of the instance from + # the file name, if the remaining part an empty string we return it + # otherwise there is and hyphen left, and we return the part after the hyphen + suffix = file_path.stem[len(name) :] + return suffix[1:] if suffix else "" def _sort_instance_list(self, s: Union[int, str, None]): if s is None: diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index a00996d..7aff799 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -123,7 +123,7 @@ class KiauhSettings: self._load_config() def _load_config(self) -> None: - if not CUSTOM_CFG.exists() or not DEFAULT_CFG.exists(): + if not CUSTOM_CFG.exists() and not DEFAULT_CFG.exists(): self._kill() cfg = CUSTOM_CFG if CUSTOM_CFG.exists() else DEFAULT_CFG diff --git a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py index 409161c..b3f10c6 100644 --- a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py +++ b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py @@ -85,7 +85,7 @@ class DuplicateOptionError(Exception): class SimpleConfigParser: """A customized config parser targeted at handling Klipper style config files""" - _SECTION_RE = re.compile(r"\s*\[(\w+ ?\w+)]\s*([#;].*)?$") + _SECTION_RE = re.compile(r"\s*\[(\w+\s?.+)]\s*([#;].*)?$") _OPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([^=:].*)\s*([#;].*)?$") _MLOPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([#;].*)?$") _COMMENT_RE = re.compile(r"^\s*([#;].*)?$") @@ -192,9 +192,9 @@ class SimpleConfigParser: if section not in self._all_sections: raise NoSectionError(section) - del self._all_sections[self._all_sections.index(section)] - del self._all_options[section] - del self._config[section] + self._all_sections.pop(self._all_sections.index(section)) + self._all_options.pop(section) + self._config.pop(section) def options(self, section) -> List[str]: """Return a list of option names for the given section name""" @@ -453,6 +453,7 @@ class SimpleConfigParser: self.section_name = section self._all_sections.append(section) + self._all_options[section] = {} self._config[section]: Section = {"_raw": raw_value, "body": []} def _parse_option(self, line: str) -> None: diff --git a/kiauh/core/submodules/simple_config_parser/tests/features/internal_state/test_internal_state_changes.py b/kiauh/core/submodules/simple_config_parser/tests/features/internal_state/test_internal_state_changes.py index af32e1e..789368c 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/features/internal_state/test_internal_state_changes.py +++ b/kiauh/core/submodules/simple_config_parser/tests/features/internal_state/test_internal_state_changes.py @@ -34,6 +34,7 @@ class TestInternalStateChanges: parser._store_internal_state_section(given, given) assert parser._all_sections == [given] + assert parser._all_options[given] == {} assert parser._config[given]["body"] == [] assert parser._config[given]["_raw"] == given diff --git a/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_option.py b/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_option.py index e4901f5..fbe9001 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_option.py +++ b/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_option.py @@ -8,6 +8,11 @@ testcases = [ ("option: value\n", "option", "value"), ("option: value # inline comment", "option", "value"), ("option: value # inline comment\n", "option", "value"), + ( + "description: Helper: park toolhead used in PAUSE and CANCEL_PRINT", + "description", + "Helper: park toolhead used in PAUSE and CANCEL_PRINT", + ), ("description: homing!", "description", "homing!"), ("description: inline macro :-)", "description", "inline macro :-)"), ("path: %GCODES_DIR%", "path", "%GCODES_DIR%"), diff --git a/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_section.py b/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_section.py index ea1536d..bab0f69 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_section.py +++ b/kiauh/core/submodules/simple_config_parser/tests/features/line_parsing/data/case_parse_section.py @@ -3,4 +3,6 @@ testcases = [ ("[test_section two]", "test_section two"), ("[section1] # inline comment", "section1"), ("[section2] ; second comment", "section2"), + ("[include moonraker-obico-update.cfg]", "include moonraker-obico-update.cfg"), + ("[include moonraker_obico_macros.cfg]", "include moonraker_obico_macros.cfg"), ] diff --git a/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_section.py b/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_section.py index 663eabe..42b93d0 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_section.py +++ b/kiauh/core/submodules/simple_config_parser/tests/features/line_type_detection/data/case_line_is_section.py @@ -1,5 +1,11 @@ testcases = [ ("[example_section]", True), + ("[gcode_macro CANCEL_PRINT]", True), + ("[gcode_macro SET_PAUSE_NEXT_LAYER]", True), + ("[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]", True), + ("[update_manager moonraker-obico]", True), + ("[include moonraker_obico_macros.cfg]", True), + ("[include moonraker-obico-update.cfg]", True), ("[example_section two]", True), ("not_a_valid_section", False), ("section: invalid", False), diff --git a/kiauh/core/submodules/simple_config_parser/tests/features/public_api/test_public_api.py b/kiauh/core/submodules/simple_config_parser/tests/features/public_api/test_public_api.py index 10d86b1..0e59b5a 100644 --- a/kiauh/core/submodules/simple_config_parser/tests/features/public_api/test_public_api.py +++ b/kiauh/core/submodules/simple_config_parser/tests/features/public_api/test_public_api.py @@ -108,7 +108,7 @@ class TestPublicAPI: assert parser._config[section]["body"][0]["option"] == option values = ["value1", "value2", "value3"] - raw_values = [" value1\n", " value2\n", " value3\n"] + raw_values = [" value1\n", " value2\n", " value3\n"] assert parser._config[section]["body"][0]["value"] == values assert parser._config[section]["body"][0]["_raw"] == f"{option}:\n" assert parser._config[section]["body"][0]["_raw_value"] == raw_values diff --git a/kiauh/extensions/obico/__init__.py b/kiauh/extensions/obico/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/extensions/obico/assets/moonraker-obico.env b/kiauh/extensions/obico/assets/moonraker-obico.env new file mode 100644 index 0000000..3c3d32b --- /dev/null +++ b/kiauh/extensions/obico/assets/moonraker-obico.env @@ -0,0 +1 @@ +OBICO_ARGS="-m moonraker_obico.app -c %CFG%" diff --git a/kiauh/extensions/obico/assets/moonraker-obico.service b/kiauh/extensions/obico/assets/moonraker-obico.service new file mode 100644 index 0000000..e6bed45 --- /dev/null +++ b/kiauh/extensions/obico/assets/moonraker-obico.service @@ -0,0 +1,16 @@ +#Systemd service file for moonraker-obico +[Unit] +Description=Moonraker-Obico +After=network-online.target moonraker.service + +[Install] +WantedBy=multi-user.target + +[Service] +Type=simple +User=%USER% +WorkingDirectory=%OBICO_DIR% +EnvironmentFile=%ENV_FILE% +ExecStart=%ENV%/bin/python3 $OBICO_ARGS +Restart=always +RestartSec=5 diff --git a/kiauh/extensions/obico/metadata.json b/kiauh/extensions/obico/metadata.json new file mode 100644 index 0000000..cdf5753 --- /dev/null +++ b/kiauh/extensions/obico/metadata.json @@ -0,0 +1,16 @@ +{ + "metadata": { + "index": 6, + "module": "moonraker_obico_extension", + "maintained_by": "Obico", + "display_name": "Obico for Klipper", + "description": [ + "Open source 3D Printing cloud and AI", + "- AI-Powered Failure Detection", + "- Free Remote Monitoring and Access", + "- 25FPS High-Def Webcam Streaming", + "- Free 4.9-Star Mobile App" + ], + "updates": true + } +} diff --git a/kiauh/extensions/obico/moonraker_obico.py b/kiauh/extensions/obico/moonraker_obico.py new file mode 100644 index 0000000..1ad397b --- /dev/null +++ b/kiauh/extensions/obico/moonraker_obico.py @@ -0,0 +1,178 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path +from subprocess import DEVNULL, CalledProcessError, run +from typing import List + +from core.instance_manager.base_instance import BaseInstance +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + SimpleConfigParser, +) +from utils.constants import SYSTEMD +from utils.logger import Logger + +MODULE_PATH = Path(__file__).resolve().parent + +OBICO_DIR = Path.home().joinpath("moonraker-obico") +OBICO_ENV = Path.home().joinpath("moonraker-obico-env") +OBICO_REPO = "https://github.com/TheSpaghettiDetective/moonraker-obico.git" + +OBICO_CFG = "moonraker-obico.cfg" +OBICO_CFG_SAMPLE = "moonraker-obico.cfg.sample" +OBICO_LOG = "moonraker-obico.log" +OBICO_UPDATE_CFG = "moonraker-obico-update.cfg" +OBICO_UPDATE_CFG_SAMPLE = "moonraker-obico-update.cfg.sample" +OBICO_MACROS_CFG = "moonraker_obico_macros.cfg" + + +# noinspection PyMethodMayBeStatic +class MoonrakerObico(BaseInstance): + @classmethod + def blacklist(cls) -> List[str]: + return ["None", "mcu"] + + def __init__(self, suffix: str = ""): + super().__init__(instance_type=self, suffix=suffix) + self.dir: Path = OBICO_DIR + self.env_dir: Path = OBICO_ENV + self._cfg_file = self.cfg_dir.joinpath("moonraker-obico.cfg") + self._log = self.log_dir.joinpath("moonraker-obico.log") + self._is_linked: bool = self._check_link_status() + self._assets_dir = MODULE_PATH.joinpath("assets") + + @property + def cfg_file(self) -> Path: + return self._cfg_file + + @property + def log(self) -> Path: + return self._log + + @property + def is_linked(self) -> bool: + return self._is_linked + + def create(self) -> None: + Logger.print_status("Creating new Obico for Klipper Instance ...") + service_template_path = MODULE_PATH.joinpath("assets/moonraker-obico.service") + service_file_name = self.get_service_file_name(extension=True) + service_file_target = SYSTEMD.joinpath(service_file_name) + env_template_file_path = MODULE_PATH.joinpath("assets/moonraker-obico.env") + env_file_target = self.sysd_dir.joinpath("moonraker-obico.env") + + try: + self.create_folders() + self.write_service_file( + service_template_path, service_file_target, env_file_target + ) + self.write_env_file(env_template_file_path, env_file_target) + + except CalledProcessError as e: + Logger.print_error( + f"Error creating service file {service_file_target}: {e}" + ) + raise + except OSError as e: + Logger.print_error(f"Error creating env file {env_file_target}: {e}") + raise + + def delete(self) -> None: + service_file = self.get_service_file_name(extension=True) + service_file_path = self.get_service_file_path() + + Logger.print_status(f"Deleting Obico for Klipper Instance: {service_file}") + + try: + command = ["sudo", "rm", "-f", service_file_path] + run(command, check=True) + Logger.print_ok(f"Service file deleted: {service_file_path}") + except CalledProcessError as e: + Logger.print_error(f"Error deleting service file: {e}") + raise + + def write_service_file( + self, + service_template_path: Path, + service_file_target: Path, + env_file_target: Path, + ) -> None: + service_content = self._prep_service_file( + service_template_path, env_file_target + ) + command = ["sudo", "tee", service_file_target] + run( + command, + input=service_content.encode(), + stdout=DEVNULL, + check=True, + ) + Logger.print_ok(f"Service file created: {service_file_target}") + + def write_env_file( + self, env_template_file_path: Path, env_file_target: Path + ) -> None: + env_file_content = self._prep_env_file(env_template_file_path) + with open(env_file_target, "w") as env_file: + env_file.write(env_file_content) + Logger.print_ok(f"Env file created: {env_file_target}") + + def link(self) -> None: + Logger.print_status( + f"Linking instance for printer {self.data_dir_name} to the Obico server ..." + ) + try: + script = OBICO_DIR.joinpath("scripts/link.sh") + cmd = [f"{script} -q -c {self.cfg_file}"] + if self.suffix: + cmd.append(f"-n {self.suffix}") + run(cmd, check=True, shell=True) + except CalledProcessError as e: + Logger.print_error(f"Error during Obico linking: {e}") + raise + + def _prep_service_file( + self, service_template_path: Path, env_file_path: Path + ) -> str: + try: + with open(service_template_path, "r") as template_file: + template_content = template_file.read() + except FileNotFoundError: + Logger.print_error( + f"Unable to open {service_template_path} - File not found" + ) + raise + service_content = template_content.replace("%USER%", self.user) + service_content = service_content.replace("%OBICO_DIR%", str(self.dir)) + service_content = service_content.replace("%ENV%", str(self.env_dir)) + service_content = service_content.replace("%ENV_FILE%", str(env_file_path)) + return service_content + + def _prep_env_file(self, env_template_file_path: Path) -> str: + try: + with open(env_template_file_path, "r") as env_file: + env_template_file_content = env_file.read() + except FileNotFoundError: + Logger.print_error( + f"Unable to open {env_template_file_path} - File not found" + ) + raise + env_file_content = env_template_file_content.replace( + "%CFG%", + f"{self.cfg_dir}/{self.cfg_file}", + ) + return env_file_content + + def _check_link_status(self) -> bool: + if not self.cfg_file.exists(): + return False + + scp = SimpleConfigParser() + scp.read(self.cfg_file) + return scp.get("server", "auth_token", None) is not None diff --git a/kiauh/extensions/obico/moonraker_obico_extension.py b/kiauh/extensions/obico/moonraker_obico_extension.py new file mode 100644 index 0000000..bd70d79 --- /dev/null +++ b/kiauh/extensions/obico/moonraker_obico_extension.py @@ -0,0 +1,386 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import shutil +from typing import List + +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from core.instance_manager.instance_manager import InstanceManager +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + SimpleConfigParser, +) +from extensions.base_extension import BaseExtension +from extensions.obico.moonraker_obico import ( + OBICO_CFG_SAMPLE, + OBICO_DIR, + OBICO_ENV, + OBICO_LOG, + OBICO_MACROS_CFG, + OBICO_REPO, + OBICO_UPDATE_CFG, + OBICO_UPDATE_CFG_SAMPLE, + MoonrakerObico, +) +from utils.common import check_install_dependencies, moonraker_exists +from utils.config_utils import ( + add_config_section, + remove_config_section, +) +from utils.fs_utils import remove_file +from utils.git_utils import git_clone_wrapper, git_pull_wrapper +from utils.input_utils import get_confirm, get_selection_input, get_string_input +from utils.logger import DialogType, Logger +from utils.sys_utils import ( + cmd_sysctl_manage, + create_python_venv, + install_python_requirements, + parse_packages_from_file, +) + + +# noinspection PyMethodMayBeStatic +class ObicoExtension(BaseExtension): + server_url: str + + def install_extension(self, **kwargs) -> None: + Logger.print_status("Installing Obico for Klipper ...") + + # check if moonraker is installed. if not, notify the user and exit + if not moonraker_exists(): + return + + # if obico is already installed, ask if the user wants to repair an + # incomplete installation or link to the obico server + obico_im = InstanceManager(MoonrakerObico) + obico_instances: List[MoonrakerObico] = obico_im.instances + if obico_instances: + self._print_is_already_installed() + options = ["l", "L", "r", "R", "b", "B"] + action = get_selection_input("Perform action", option_list=options) + if action.lower() == "b": + Logger.print_info("Exiting Obico for Klipper installation ...") + return + elif action.lower() == "l": + unlinked_instances: List[MoonrakerObico] = [ + obico for obico in obico_instances if not obico.is_linked + ] + self._link_obico_instances(unlinked_instances) + return + else: + Logger.print_status("Re-Installing Obico for Klipper ...") + + # let the user confirm installation + kl_im = InstanceManager(Klipper) + kl_instances: List[Klipper] = kl_im.instances + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + self._print_moonraker_instances(mr_instances) + if not get_confirm( + "Continue Obico for Klipper installation?", + default_choice=True, + allow_go_back=True, + ): + return + + try: + git_clone_wrapper(OBICO_REPO, OBICO_DIR) + self._install_dependencies() + + # ask the user for the obico server url + self._get_server_url() + + # create obico instances + for moonraker in mr_instances: + current_instance = MoonrakerObico(suffix=moonraker.suffix) + + obico_im.current_instance = current_instance + obico_im.create_instance() + obico_im.enable_instance() + + # create obico config + self._create_obico_cfg(current_instance, moonraker) + + # create obico macros + self._create_obico_macros_cfg(moonraker) + + # create obico update manager + self._create_obico_update_manager_cfg(moonraker) + + obico_im.start_instance() + + cmd_sysctl_manage("daemon-reload") + + # add to klippers config + self._patch_printer_cfg(kl_instances) + kl_im.restart_all_instance() + + # add to moonraker update manager + self._patch_moonraker_conf(mr_instances) + mr_im.restart_all_instance() + + # check linking of / ask for linking instances + self._check_and_opt_link_instances() + + Logger.print_dialog( + DialogType.SUCCESS, + ["Obico for Klipper successfully installed!"], + center_content=True, + ) + + except Exception as e: + Logger.print_error(f"Error during Obico for Klipper installation:\n{e}") + + def update_extension(self, **kwargs) -> None: + Logger.print_status("Updating Obico for Klipper ...") + try: + tb_im = InstanceManager(MoonrakerObico) + tb_im.stop_all_instance() + + git_pull_wrapper(OBICO_REPO, OBICO_DIR) + self._install_dependencies() + + tb_im.start_all_instance() + Logger.print_ok("Obico for Klipper successfully updated!") + + except Exception as e: + Logger.print_error(f"Error during Obico for Klipper update:\n{e}") + + def remove_extension(self, **kwargs) -> None: + Logger.print_status("Removing Obico for Klipper ...") + kl_im = InstanceManager(Klipper) + kl_instances: List[Klipper] = kl_im.instances + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + ob_im = InstanceManager(MoonrakerObico) + ob_instances: List[MoonrakerObico] = ob_im.instances + + try: + self._remove_obico_instances(ob_im, ob_instances) + self._remove_obico_dir() + self._remove_obico_env() + remove_config_section(f"include {OBICO_MACROS_CFG}", kl_instances) + remove_config_section(f"include {OBICO_UPDATE_CFG}", mr_instances) + self._delete_obico_logs(ob_instances) + Logger.print_dialog( + DialogType.SUCCESS, + ["Obico for Klipper successfully removed!"], + center_content=True, + ) + + except Exception as e: + Logger.print_error(f"Error during Obico for Klipper removal:\n{e}") + + def _obico_server_url_prompt(self) -> None: + Logger.print_dialog( + DialogType.CUSTOM, + custom_title="Obico Server URL", + content=[ + "You can use a self-hosted Obico Server or the Obico Cloud. " + "For more information, please visit:", + "https://obico.io.", + "\n\n", + "For the Obico Cloud, leave it as the default:", + "https://app.obico.io.", + "\n\n", + "For self-hosted server, specify:", + "http://server_ip:port", + "For instance, 'http://192.168.0.5:3334'.", + ], + end="", + ) + + def _print_moonraker_instances(self, mr_instances) -> None: + mr_names = [f"● {moonraker.data_dir_name}" for moonraker in mr_instances] + if len(mr_names) > 1: + Logger.print_dialog( + DialogType.INFO, + [ + "The following Moonraker instances were found:", + *mr_names, + "\n\n", + "The setup will apply the same names to Obico!", + ], + end="", + ) + + def _print_is_already_installed(self) -> None: + Logger.print_dialog( + DialogType.INFO, + [ + "Obico is already installed!", + "It is save to run the installer again to link your " + "printer or repair any issues.", + "\n\n", + "You can perform the following actions:", + "L) Link printer to the Obico server", + "R) Repair installation", + ], + end="", + ) + + def _get_server_url(self) -> None: + self._obico_server_url_prompt() + pattern = r"^(http|https)://[a-zA-Z0-9./?=_%:-]*$" + self.server_url = get_string_input( + "Obico Server URL", + regex=pattern, + default="https://app.obico.io", + ) + + def _install_dependencies(self) -> None: + # install dependencies + script = OBICO_DIR.joinpath("install.sh") + package_list = parse_packages_from_file(script) + check_install_dependencies(package_list) + + # create virtualenv + create_python_venv(OBICO_ENV) + requirements = OBICO_DIR.joinpath("requirements.txt") + install_python_requirements(OBICO_ENV, requirements) + + def _create_obico_macros_cfg(self, moonraker) -> None: + macros_cfg = OBICO_DIR.joinpath(f"include_cfgs/{OBICO_MACROS_CFG}") + macros_target = moonraker.cfg_dir.joinpath(OBICO_MACROS_CFG) + if not macros_target.exists(): + shutil.copy(macros_cfg, macros_target) + else: + Logger.print_info( + f"Obico's '{OBICO_MACROS_CFG}' in {moonraker.cfg_dir} already exists! Skipped ..." + ) + + def _create_obico_update_manager_cfg(self, moonraker) -> None: + update_cfg = OBICO_DIR.joinpath(OBICO_UPDATE_CFG_SAMPLE) + update_cfg_target = moonraker.cfg_dir.joinpath(OBICO_UPDATE_CFG) + if not update_cfg_target.exists(): + shutil.copy(update_cfg, update_cfg_target) + else: + Logger.print_info( + f"Obico's '{OBICO_UPDATE_CFG}' in {moonraker.cfg_dir} already exists! Skipped ..." + ) + + def _create_obico_cfg( + self, current_instance: MoonrakerObico, moonraker: Moonraker + ) -> None: + cfg_template = OBICO_DIR.joinpath(OBICO_CFG_SAMPLE) + cfg_target_file = current_instance.cfg_file + + if not cfg_template.exists(): + Logger.print_error( + f"Obico config template file {cfg_target_file} does not exist!" + ) + return + + if not cfg_target_file.exists(): + shutil.copy(cfg_template, cfg_target_file) + self._patch_obico_cfg(moonraker, current_instance) + else: + Logger.print_info( + f"Obico config in {current_instance.cfg_dir} already exists! Skipped ..." + ) + + def _patch_obico_cfg(self, moonraker: Moonraker, obico: MoonrakerObico) -> None: + scp = SimpleConfigParser() + scp.read(obico.cfg_file) + scp.set("server", "url", self.server_url) + scp.set("moonraker", "port", str(moonraker.port)) + scp.set("logging", "path", str(obico.log)) + scp.write(obico.cfg_file) + + def _patch_printer_cfg(self, klipper: List[Klipper]) -> None: + add_config_section(section=f"include {OBICO_MACROS_CFG}", instances=klipper) + + def _patch_moonraker_conf(self, instances: List[Moonraker]) -> None: + add_config_section(section=f"include {OBICO_UPDATE_CFG}", instances=instances) + + def _link_obico_instances(self, unlinked_instances): + for obico in unlinked_instances: + obico.link() + + def _check_and_opt_link_instances(self): + Logger.print_status("Checking link status of Obico instances ...") + ob_im = InstanceManager(MoonrakerObico) + ob_instances: List[MoonrakerObico] = ob_im.instances + unlinked_instances: List[MoonrakerObico] = [ + obico for obico in ob_instances if not obico.is_linked + ] + if unlinked_instances: + Logger.print_dialog( + DialogType.INFO, + [ + "The Obico instances for the following printers are not " + "linked to the server:", + *[f"● {obico.data_dir_name}" for obico in unlinked_instances], + "\n\n", + "It will take only 10 seconds to link the printer to the Obico server.", + "For more information visit:", + "https://www.obico.io/docs/user-guides/klipper-setup/", + "\n\n", + "If you don't want to link the printer now, you can restart the " + "linking process later by running this installer again.", + ], + end="", + ) + if not get_confirm("Do you want to link the printers now?"): + Logger.print_info("Linking to Obico server skipped ...") + return + + self._link_obico_instances(unlinked_instances) + + def _remove_obico_instances( + self, + instance_manager: InstanceManager, + instance_list: List[MoonrakerObico], + ) -> None: + if not instance_list: + Logger.print_info("No Obico instances found. Skipped ...") + return + + for instance in instance_list: + Logger.print_status( + f"Removing instance {instance.get_service_file_name()} ..." + ) + instance_manager.current_instance = instance + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance() + + cmd_sysctl_manage("daemon-reload") + + def _remove_obico_dir(self) -> None: + if not OBICO_DIR.exists(): + Logger.print_info(f"'{OBICO_DIR}' does not exist. Skipped ...") + return + + try: + shutil.rmtree(OBICO_DIR) + except OSError as e: + Logger.print_error(f"Unable to delete '{OBICO_DIR}':\n{e}") + + def _remove_obico_env(self) -> None: + if not OBICO_ENV.exists(): + Logger.print_info(f"'{OBICO_ENV}' does not exist. Skipped ...") + return + + try: + shutil.rmtree(OBICO_ENV) + except OSError as e: + Logger.print_error(f"Unable to delete '{OBICO_ENV}':\n{e}") + + def _delete_obico_logs(self, instances: List[MoonrakerObico]) -> None: + Logger.print_status("Removing Obico logs ...") + all_logfiles = [] + for instance in instances: + all_logfiles = list(instance.log_dir.glob(f"{OBICO_LOG}*")) + if not all_logfiles: + Logger.print_info("No Obico logs found. Skipped ...") + return + + for log in all_logfiles: + Logger.print_status(f"Remove '{log}'") + remove_file(log) diff --git a/kiauh/utils/config_utils.py b/kiauh/utils/config_utils.py index 2a9d829..dddbcac 100644 --- a/kiauh/utils/config_utils.py +++ b/kiauh/utils/config_utils.py @@ -43,7 +43,7 @@ def add_config_section( scp.add_section(section) if options is not None: - for option in options: + for option in reversed(options): scp.set(section, option[0], option[1]) scp.write(cfg_file) From e421a12daf07b2b898a881452330c86015873aa9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 22 Jun 2024 23:21:34 +0200 Subject: [PATCH 255/296] fix: logical error in list comprehension Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 19633c4..d5c16ac 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -201,7 +201,7 @@ def klipper_to_multi_conversion(new_name: str) -> None: def check_user_groups(): user_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] - missing_groups = [g for g in user_groups if g == "tty" or g == "dialout"] + missing_groups = [g for g in ["tty", "dialout"] if g not in user_groups] if not missing_groups: return From dbe15e3a3221e92b7dd54e1328868eff8ab55744 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 27 Jun 2024 17:47:15 +0200 Subject: [PATCH 256/296] feat: add ipv6 check before installing webclients Signed-off-by: Dominik Willner --- .../components/webui_client/client_dialogs.py | 49 ++++++++++++------- kiauh/components/webui_client/client_setup.py | 9 ++++ kiauh/utils/sys_utils.py | 31 ++++++++++++ 3 files changed, 70 insertions(+), 19 deletions(-) diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py index 525aece..f737410 100644 --- a/kiauh/components/webui_client/client_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -13,6 +13,7 @@ from typing import List from components.webui_client.base_data import BaseWebClient from core.menus.base_menu import print_back_footer from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT +from utils.logger import DialogType, Logger def print_moonraker_not_found_dialog(): @@ -84,25 +85,35 @@ def print_client_port_select_dialog(name: str, port: int, ports_in_use: List[int print(dialog, end="") -def print_install_client_config_dialog(client: BaseWebClient): +def print_install_client_config_dialog(client: BaseWebClient) -> None: name = client.display_name url = client.client_config.repo_url.replace(".git", "") - line1 = f"have {name} fully functional and working." - line2 = f"The recommended macros for {name} can be seen here:" - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | It is recommended to use special macros in order to | - | {line1:<54}| - | | - | {line2:<54}| - | {url:<54}| - | | - | If you already use these macros skip this step. | - | Otherwise you should consider to answer with 'Y' to | - | download the recommended macros. | - \\=======================================================/ - """ - )[1:] + Logger.print_dialog( + DialogType.INFO, + [ + f"It is recommended to use special macros in order to have {name} fully " + f"functional and working.", + "\n\n", + f"The recommended macros for {name} can be seen here:", + url, + "\n\n", + "If you already use these macros skip this step. Otherwise you should " + "consider to answer with 'Y' to download the recommended macros.", + ], + end="", + ) - print(dialog, end="") + +def print_ipv6_warning_dialog() -> None: + Logger.print_dialog( + DialogType.WARNING, + [ + "It looks like IPv6 is enabled on this system!", + "This may cause issues with the installation of NGINX in the following " + "steps! It is recommended to disable IPv6 on your system to avoid this issue.", + "\n\n", + "If you think this warning is a false alarm, and you are sure that " + "IPv6 is disabled, you can continue with the installation.", + ], + end="", + ) diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index bb6e10e..b1eda56 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -24,6 +24,7 @@ from components.webui_client.client_config.client_config_setup import ( from components.webui_client.client_dialogs import ( print_client_port_select_dialog, print_install_client_config_dialog, + print_ipv6_warning_dialog, print_moonraker_not_found_dialog, ) from components.webui_client.client_utils import ( @@ -49,6 +50,7 @@ from utils.fs_utils import ( from utils.input_utils import get_confirm, get_number_input from utils.logger import Logger from utils.sys_utils import ( + check_ipv6, cmd_sysctl_service, download_file, get_ipv4_addr, @@ -115,6 +117,13 @@ def install_client(client: BaseWebClient) -> None: ) valid_port = is_valid_port(port, ports_in_use) + # check if ipv6 is enabled, as this may cause issues with nginx + if check_ipv6(): + print_ipv6_warning_dialog() + if not get_confirm(f"Continue with {client.display_name} installation?"): + Logger.print_info(f"Exiting {client.display_name} installation ...") + return + check_install_dependencies(["nginx", "unzip"]) try: diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index d3d3b0d..8f2ec5e 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -402,3 +402,34 @@ def log_process(process: Popen) -> None: if process.poll() is not None: break + + +def check_ipv6() -> bool: + """ + Check if IPv6 is enabled + :return: True if IPv6 is enabled, False otherwise + """ + try: + file1 = Path("/proc/sys/net/ipv6/conf/all/disable_ipv6") + if file1.exists(): + with open(file1, "r") as file: + if file.read().strip() == "0": + return True + elif file.read().strip() == "1": + return False + + file3 = Path("/etc/sysctl.conf") + if file3.exists(): + with open(file3, "r") as file: + for line in file.readlines(): + if ( + "net.ipv6.conf.all.disable_ipv6" in line + and not line.startswith("#") + and line.split("=")[1].strip() == "0" + ): + return True + return False + + except Exception as e: + Logger.print_error(f"Error checking IPv6: {e}") + return True From 103a7b61b36a849008c896dca5d8f67d2d706990 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 28 Jun 2024 23:26:27 +0200 Subject: [PATCH 257/296] feat: OctoEverywhere for KIAUH v6 (#485) * feat: scaffold OE installer Signed-off-by: Dominik Willner * refactor: remove redundant steps ocoeverywhere already takes care of Signed-off-by: Dominik Willner * refactor: add padding option to dialog Signed-off-by: Dominik Willner * refactor: oe uninstaller Signed-off-by: Dominik Willner * fix: add recursive removal of files Signed-off-by: Dominik Willner * refactor: implement octoeverywhere update Signed-off-by: Dominik Willner * chore: cleanup Signed-off-by: Dominik Willner * chore: remove unused argument Signed-off-by: Dominik Willner * fix: add instance names to blacklist Signed-off-by: Dominik Willner * refactor: use update.sh script of OctoEverywhere for updating Signed-off-by: Dominik Willner * fix: typo Signed-off-by: Dominik Willner * refactor: add force flag to git_clone_wrapper Signed-off-by: Dominik Willner --------- Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 1 - kiauh/components/klipper/klipper_setup.py | 1 - kiauh/components/klipper/klipper_utils.py | 2 - .../menus/klipper_flash_menu.py | 1 - .../components/klipperscreen/klipperscreen.py | 1 - kiauh/components/mobileraker/mobileraker.py | 1 - kiauh/components/octoeverywhere/__init__.py | 28 +++ .../octoeverywhere/octoeverywhere.py | 88 +++++++ .../octoeverywhere/octoeverywhere_setup.py | 231 ++++++++++++++++++ .../components/webui_client/client_dialogs.py | 2 - kiauh/core/menus/install_menu.py | 9 +- kiauh/core/menus/main_menu.py | 9 +- kiauh/core/menus/remove_menu.py | 19 +- kiauh/core/menus/settings_menu.py | 1 - kiauh/core/menus/update_menu.py | 17 +- kiauh/core/settings/kiauh_settings.py | 1 - .../obico/moonraker_obico_extension.py | 10 +- kiauh/utils/common.py | 1 - kiauh/utils/fs_utils.py | 28 ++- kiauh/utils/git_utils.py | 5 +- kiauh/utils/logger.py | 19 +- 21 files changed, 428 insertions(+), 47 deletions(-) create mode 100644 kiauh/components/octoeverywhere/__init__.py create mode 100644 kiauh/components/octoeverywhere/octoeverywhere.py create mode 100644 kiauh/components/octoeverywhere/octoeverywhere_setup.py diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index 99a30db..ed774c0 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -88,7 +88,6 @@ def print_multi_instance_warning(instances: List[Klipper]) -> None: "The following instances were found:", *_instances, ], - end="", ) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 4196f1f..85d7469 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -145,7 +145,6 @@ def update_klipper() -> None: "All Klipper instances will be restarted during the update process and " "ongoing prints WILL FAIL.", ], - end="", ) if not get_confirm("Update Klipper now?"): diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index d5c16ac..c4b852f 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -220,7 +220,6 @@ def check_user_groups(): "INFO:", "Relog required for group assignments to take effect!", ], - end="", ) if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): @@ -272,7 +271,6 @@ def handle_disruptive_system_packages() -> None: "Please fix the problem manually. Otherwise, this may have " "undesirable effects on the operation of Klipper." ], - end="", ) diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index eccd772..8af866c 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -353,7 +353,6 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu): "\n\n", "If you are unsure, stick to the default 250000!", ], - end="", ) self.flash_options.selected_baudrate = get_number_input( question="Please set the baud rate", diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index ae522c5..d64d500 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -62,7 +62,6 @@ def install_klipperscreen() -> None: "KlipperScreens update manager configuration for Moonraker " "will not be added to any moonraker.conf.", ], - end="", ) if not get_confirm( "Continue KlipperScreen installation?", diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py index 17416a2..5fee326 100644 --- a/kiauh/components/mobileraker/mobileraker.py +++ b/kiauh/components/mobileraker/mobileraker.py @@ -58,7 +58,6 @@ def install_mobileraker() -> None: "Mobileraker's companion's update manager configuration for Moonraker " "will not be added to any moonraker.conf.", ], - end="", ) if not get_confirm( "Continue Mobileraker's companion installation?", diff --git a/kiauh/components/octoeverywhere/__init__.py b/kiauh/components/octoeverywhere/__init__.py new file mode 100644 index 0000000..a816526 --- /dev/null +++ b/kiauh/components/octoeverywhere/__init__.py @@ -0,0 +1,28 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path + +# repo +OE_REPO = "https://github.com/QuinnDamerell/OctoPrint-OctoEverywhere.git" + +# directories +OE_DIR = Path.home().joinpath("octoeverywhere") +OE_ENV_DIR = Path.home().joinpath("octoeverywhere-env") +OE_STORE_DIR = OE_DIR.joinpath("octoeverywhere-store") + +# files +OE_REQ_FILE = OE_DIR.joinpath("requirements.txt") +OE_DEPS_JSON_FILE = OE_DIR.joinpath("moonraker-system-dependencies.json") +OE_INSTALL_SCRIPT = OE_DIR.joinpath("install.sh") +OE_UPDATE_SCRIPT = OE_DIR.joinpath("update.sh") + +# filenames +OE_CFG_NAME = "octoeverywhere.conf" +OE_LOG_NAME = "octoeverywhere.log" +OE_SYS_CFG_NAME = "octoeverywhere-system.cfg" diff --git a/kiauh/components/octoeverywhere/octoeverywhere.py b/kiauh/components/octoeverywhere/octoeverywhere.py new file mode 100644 index 0000000..5c8ad53 --- /dev/null +++ b/kiauh/components/octoeverywhere/octoeverywhere.py @@ -0,0 +1,88 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 pathlib import Path +from subprocess import CalledProcessError, run +from typing import List + +from components.octoeverywhere import ( + OE_CFG_NAME, + OE_DIR, + OE_ENV_DIR, + OE_INSTALL_SCRIPT, + OE_LOG_NAME, + OE_STORE_DIR, + OE_SYS_CFG_NAME, + OE_UPDATE_SCRIPT, +) +from core.instance_manager.base_instance import BaseInstance +from utils.logger import Logger + + +class Octoeverywhere(BaseInstance): + @classmethod + def blacklist(cls) -> List[str]: + return ["None", "mcu", "bambu", "companion"] + + def __init__(self, suffix: str = ""): + super().__init__(instance_type=self, suffix=suffix) + self.dir: Path = OE_DIR + self.env_dir: Path = OE_ENV_DIR + self.store_dir: Path = OE_STORE_DIR + self._cfg_file = self.cfg_dir.joinpath(OE_CFG_NAME) + self._sys_cfg_file = self.cfg_dir.joinpath(OE_SYS_CFG_NAME) + self._log = self.log_dir.joinpath(OE_LOG_NAME) + + @property + def cfg_file(self) -> Path: + return self._cfg_file + + @property + def sys_cfg_file(self) -> Path: + return self._sys_cfg_file + + @property + def log(self) -> Path: + return self._log + + def create(self) -> None: + Logger.print_status("Creating OctoEverywhere for Klipper Instance ...") + + try: + cmd = f"{OE_INSTALL_SCRIPT} {self.cfg_dir}/moonraker.conf" + run(cmd, check=True, shell=True) + + except CalledProcessError as e: + Logger.print_error(f"Error creating instance: {e}") + raise + + @staticmethod + def update(): + try: + run(str(OE_UPDATE_SCRIPT), check=True, shell=True, cwd=OE_DIR) + + except CalledProcessError as e: + Logger.print_error(f"Error updating OctoEverywhere for Klipper: {e}") + raise + + def delete(self) -> None: + service_file = self.get_service_file_name(extension=True) + service_file_path = self.get_service_file_path() + + Logger.print_status( + f"Deleting OctoEverywhere for Klipper Instance: {service_file}" + ) + + try: + command = ["sudo", "rm", "-f", service_file_path] + run(command, check=True) + Logger.print_ok(f"Service file deleted: {service_file_path}") + except CalledProcessError as e: + Logger.print_error(f"Error deleting service file: {e}") + raise diff --git a/kiauh/components/octoeverywhere/octoeverywhere_setup.py b/kiauh/components/octoeverywhere/octoeverywhere_setup.py new file mode 100644 index 0000000..13634d1 --- /dev/null +++ b/kiauh/components/octoeverywhere/octoeverywhere_setup.py @@ -0,0 +1,231 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 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 # +# ======================================================================= # +import json +from pathlib import Path +from typing import List + +from components.moonraker.moonraker import Moonraker +from components.octoeverywhere import ( + OE_DEPS_JSON_FILE, + OE_DIR, + OE_ENV_DIR, + OE_INSTALL_SCRIPT, + OE_LOG_NAME, + OE_REPO, + OE_REQ_FILE, + OE_SYS_CFG_NAME, +) +from components.octoeverywhere.octoeverywhere import Octoeverywhere +from core.instance_manager.instance_manager import InstanceManager +from utils.common import ( + check_install_dependencies, + get_install_status, + moonraker_exists, +) +from utils.config_utils import ( + remove_config_section, +) +from utils.fs_utils import run_remove_routines +from utils.git_utils import git_clone_wrapper +from utils.input_utils import get_confirm +from utils.logger import DialogType, Logger +from utils.sys_utils import ( + cmd_sysctl_manage, + install_python_requirements, + parse_packages_from_file, +) +from utils.types import ComponentStatus + + +def get_octoeverywhere_status() -> ComponentStatus: + return get_install_status(OE_DIR, OE_ENV_DIR, Octoeverywhere) + + +def install_octoeverywhere() -> None: + Logger.print_status("Installing OctoEverywhere for Klipper ...") + + # check if moonraker is installed. if not, notify the user and exit + if not moonraker_exists(): + return + + force_clone = False + oe_im = InstanceManager(Octoeverywhere) + oe_instances: List[Octoeverywhere] = oe_im.instances + if oe_instances: + Logger.print_dialog( + DialogType.INFO, + [ + "OctoEverywhere is already installed!", + "It is safe to run the installer again to link your " + "printer or repair any issues.", + ], + padding_top=0, + padding_bottom=0, + ) + if not get_confirm("Re-run OctoEverywhere installation?"): + Logger.print_info("Exiting OctoEverywhere for Klipper installation ...") + return + else: + Logger.print_status("Re-Installing OctoEverywhere for Klipper ...") + force_clone = True + + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + + mr_names = [f"● {moonraker.data_dir_name}" for moonraker in mr_instances] + if len(mr_names) > 1: + Logger.print_dialog( + DialogType.INFO, + [ + "The following Moonraker instances were found:", + *mr_names, + "\n\n", + "The setup will apply the same names to OctoEverywhere!", + ], + padding_top=0, + padding_bottom=0, + ) + + if not get_confirm( + "Continue OctoEverywhere for Klipper installation?", + default_choice=True, + allow_go_back=True, + ): + Logger.print_info("Exiting OctoEverywhere for Klipper installation ...") + return + + try: + git_clone_wrapper(OE_REPO, OE_DIR, force=force_clone) + + for moonraker in mr_instances: + oe_im.current_instance = Octoeverywhere(suffix=moonraker.suffix) + oe_im.create_instance() + + mr_im.restart_all_instance() + + Logger.print_dialog( + DialogType.SUCCESS, + ["OctoEverywhere for Klipper successfully installed!"], + center_content=True, + ) + + except Exception as e: + Logger.print_error( + f"Error during OctoEverywhere for Klipper installation:\n{e}" + ) + + +def update_octoeverywhere() -> None: + Logger.print_status("Updating OctoEverywhere for Klipper ...") + try: + Octoeverywhere.update() + Logger.print_dialog( + DialogType.SUCCESS, + ["OctoEverywhere for Klipper successfully updated!"], + center_content=True, + ) + + except Exception as e: + Logger.print_error(f"Error during OctoEverywhere for Klipper update:\n{e}") + + +def remove_octoeverywhere() -> None: + Logger.print_status("Removing OctoEverywhere for Klipper ...") + mr_im = InstanceManager(Moonraker) + mr_instances: List[Moonraker] = mr_im.instances + ob_im = InstanceManager(Octoeverywhere) + ob_instances: List[Octoeverywhere] = ob_im.instances + + try: + remove_oe_instances(ob_im, ob_instances) + remove_oe_dir() + remove_oe_env() + remove_config_section(f"include {OE_SYS_CFG_NAME}", mr_instances) + delete_oe_logs(ob_instances) + Logger.print_dialog( + DialogType.SUCCESS, + ["OctoEverywhere for Klipper successfully removed!"], + center_content=True, + ) + + except Exception as e: + Logger.print_error(f"Error during OctoEverywhere for Klipper removal:\n{e}") + + +def install_oe_dependencies() -> None: + oe_deps = [] + if OE_DEPS_JSON_FILE.exists(): + with open(OE_DEPS_JSON_FILE, "r") as deps: + oe_deps = json.load(deps).get("debian", []) + elif OE_INSTALL_SCRIPT.exists(): + oe_deps = parse_packages_from_file(OE_INSTALL_SCRIPT) + + if not oe_deps: + raise ValueError("Error reading OctoEverywhere dependencies!") + + check_install_dependencies(oe_deps) + install_python_requirements(OE_ENV_DIR, OE_REQ_FILE) + + +def remove_oe_instances( + instance_manager: InstanceManager, + instance_list: List[Octoeverywhere], +) -> None: + if not instance_list: + Logger.print_info("No OctoEverywhere instances found. Skipped ...") + return + + for instance in instance_list: + Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...") + instance_manager.current_instance = instance + instance_manager.stop_instance() + instance_manager.disable_instance() + instance_manager.delete_instance() + + cmd_sysctl_manage("daemon-reload") + + +def remove_oe_dir() -> None: + Logger.print_status("Removing OctoEverywhere for Klipper directory ...") + + if not OE_DIR.exists(): + Logger.print_info(f"'{OE_DIR}' does not exist. Skipped ...") + return + + run_remove_routines(OE_DIR) + + +def remove_oe_env() -> None: + Logger.print_status("Removing OctoEverywhere for Klipper environment ...") + + if not OE_ENV_DIR.exists(): + Logger.print_info(f"'{OE_ENV_DIR}' does not exist. Skipped ...") + return + + run_remove_routines(OE_ENV_DIR) + + +def delete_oe_logs(instances: List[Octoeverywhere]) -> None: + Logger.print_status("Removing OctoEverywhere logs ...") + + all_logfiles = [] + for instance in instances: + all_logfiles = list(instance.log_dir.glob(f"{OE_LOG_NAME}*")) + + install_log = Path.home().joinpath("octoeverywhere-installer.log") + if install_log.exists(): + all_logfiles.append(install_log) + + if not all_logfiles: + Logger.print_info("No OctoEverywhere logs found. Skipped ...") + return + + for log in all_logfiles: + Logger.print_status(f"Remove '{log}'") + run_remove_routines(log) diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py index f737410..e723274 100644 --- a/kiauh/components/webui_client/client_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -100,7 +100,6 @@ def print_install_client_config_dialog(client: BaseWebClient) -> None: "If you already use these macros skip this step. Otherwise you should " "consider to answer with 'Y' to download the recommended macros.", ], - end="", ) @@ -115,5 +114,4 @@ def print_ipv6_warning_dialog() -> None: "If you think this warning is a false alarm, and you are sure that " "IPv6 is disabled, you can continue with the installation.", ], - end="", ) diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 9458465..b538eb0 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -15,6 +15,7 @@ from components.klipper import klipper_setup from components.klipperscreen.klipperscreen import install_klipperscreen from components.mobileraker.mobileraker import install_mobileraker from components.moonraker import moonraker_setup +from components.octoeverywhere.octoeverywhere_setup import install_octoeverywhere from components.webui_client import client_setup from components.webui_client.client_config import client_config_setup from components.webui_client.fluidd_data import FluiddData @@ -49,6 +50,7 @@ class InstallMenu(BaseMenu): "7": Option(method=self.install_klipperscreen, menu=False), "8": Option(method=self.install_mobileraker, menu=False), "9": Option(method=self.install_crowsnest, menu=False), + "10": Option(method=self.install_octoeverywhere, menu=False), } def print_menu(self): @@ -69,8 +71,8 @@ class InstallMenu(BaseMenu): ║ 4) [Fluidd] │ Webcam Streamer: ║ ║ │ 9) [Crowsnest] ║ ║ Client-Config: │ ║ - ║ 5) [Mainsail-Config] │ ║ - ║ 6) [Fluidd-Config] │ ║ + ║ 5) [Mainsail-Config] │ Remote Access: ║ + ║ 6) [Fluidd-Config] │ 10) [OctoEverywhere] ║ ║ │ ║ ╟───────────────────────────┴───────────────────────────╢ """ @@ -103,3 +105,6 @@ class InstallMenu(BaseMenu): def install_crowsnest(self, **kwargs): install_crowsnest() + + def install_octoeverywhere(self, **kwargs): + install_octoeverywhere() diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 8de3a12..2a6497b 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -16,6 +16,7 @@ from components.klipperscreen.klipperscreen import get_klipperscreen_status from components.log_uploads.menus.log_upload_menu import LogUploadMenu from components.mobileraker.mobileraker import get_mobileraker_status from components.moonraker.moonraker_utils import get_moonraker_status +from components.octoeverywhere.octoeverywhere_setup import get_octoeverywhere_status from components.webui_client.client_utils import ( get_client_status, get_current_client_config, @@ -54,7 +55,7 @@ class MainMenu(BaseMenu): self.kl_status = self.kl_repo = self.mr_status = self.mr_repo = "" self.ms_status = self.fl_status = self.ks_status = self.mb_status = "" - self.cn_status = self.cc_status = "" + self.cn_status = self.cc_status = self.oe_status = "" self.init_status() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: @@ -74,12 +75,12 @@ class MainMenu(BaseMenu): } def init_status(self) -> None: - status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn"] + status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn", "oe"] for var in status_vars: setattr( self, f"{var}_status", - f"{COLOR_RED}Not installed!{RESET_FORMAT}", + f"{COLOR_RED}Not installed{RESET_FORMAT}", ) def fetch_status(self) -> None: @@ -91,6 +92,7 @@ class MainMenu(BaseMenu): self._get_component_status("ks", get_klipperscreen_status) self._get_component_status("mb", get_mobileraker_status) self._get_component_status("cn", get_crowsnest_status) + self._get_component_status("oe", get_octoeverywhere_status) def _get_component_status(self, name: str, status_fn: callable, *args) -> None: status_data: ComponentStatus = status_fn(*args) @@ -141,6 +143,7 @@ class MainMenu(BaseMenu): ║ │ ║ ║ Community: │ KlipperScreen: {self.ks_status:<{pad2}} ║ ║ E) [Extensions] │ Mobileraker: {self.mb_status:<{pad2}} ║ + ║ │ OctoEverywhere: {self.oe_status:<{pad2}} ║ ║ │ Crowsnest: {self.cn_status:<{pad2}} ║ ╟──────────────────┼────────────────────────────────────╢ ║ {footer1:^25} │ {footer2:^43} ║ diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index d43f998..6898ea2 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -17,6 +17,7 @@ from components.mobileraker.mobileraker import remove_mobileraker from components.moonraker.menus.moonraker_remove_menu import ( MoonrakerRemoveMenu, ) +from components.octoeverywhere.octoeverywhere_setup import remove_octoeverywhere from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData from components.webui_client.menus.client_remove_menu import ClientRemoveMenu @@ -48,6 +49,7 @@ class RemoveMenu(BaseMenu): "5": Option(method=self.remove_klipperscreen, menu=True), "6": Option(method=self.remove_mobileraker, menu=True), "7": Option(method=self.remove_crowsnest, menu=True), + "8": Option(method=self.remove_octoeverywhere, menu=True), } def print_menu(self): @@ -61,14 +63,16 @@ class RemoveMenu(BaseMenu): ╟───────────────────────────────────────────────────────╢ ║ INFO: Configurations and/or any backups will be kept! ║ ╟───────────────────────────┬───────────────────────────╢ - ║ Firmware & API: │ Touchscreen GUI: ║ - ║ 1) [Klipper] │ 5) [KlipperScreen] ║ + ║ Firmware & API: │ Android / iOS: ║ + ║ 1) [Klipper] │ 6) [Mobileraker] ║ ║ 2) [Moonraker] │ ║ - ║ │ Android / iOS: ║ - ║ Klipper Webinterface: │ 6) [Mobileraker] ║ + ║ │ Webcam Streamer: ║ + ║ Klipper Webinterface: │ 7) [Crowsnest] ║ ║ 3) [Mainsail] │ ║ - ║ 4) [Fluidd] │ Webcam Streamer: ║ - ║ │ 7) [Crowsnest] ║ + ║ 4) [Fluidd] │ Remote Access: ║ + ║ │ 8) [OctoEverywhere] ║ + ║ Touchscreen GUI: │ ║ + ║ 5) [KlipperScreen] │ ║ ╟───────────────────────────┴───────────────────────────╢ """ )[1:] @@ -94,3 +98,6 @@ class RemoveMenu(BaseMenu): def remove_crowsnest(self, **kwargs): remove_crowsnest() + + def remove_octoeverywhere(self, **kwargs): + remove_octoeverywhere() diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 579c098..322c80d 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -144,7 +144,6 @@ class SettingsMenu(BaseMenu): f"New {display_name} repository branch:", f"● {branch}", ], - end="", ) if get_confirm("Apply changes?", allow_go_back=True): diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index b3cd783..5e4f3c8 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -25,6 +25,10 @@ from components.mobileraker.mobileraker import ( ) from components.moonraker.moonraker_setup import update_moonraker from components.moonraker.moonraker_utils import get_moonraker_status +from components.octoeverywhere.octoeverywhere_setup import ( + get_octoeverywhere_status, + update_octoeverywhere, +) from components.webui_client.client_config.client_config_setup import ( update_client_config, ) @@ -57,7 +61,7 @@ class UpdateMenu(BaseMenu): self.ms_local = self.ms_remote = self.fl_local = self.fl_remote = "" self.mc_local = self.mc_remote = self.fc_local = self.fc_remote = "" self.ks_local = self.ks_remote = self.mb_local = self.mb_remote = "" - self.cn_local = self.cn_remote = "" + self.cn_local = self.cn_remote = self.oe_local = self.oe_remote = "" self.mainsail_data = MainsailData() self.fluidd_data = FluiddData() @@ -82,7 +86,8 @@ class UpdateMenu(BaseMenu): "7": Option(self.update_klipperscreen, menu=False), "8": Option(self.update_mobileraker, menu=False), "9": Option(self.update_crowsnest, menu=False), - "10": Option(self.upgrade_system_packages, menu=False), + "10": Option(self.update_octoeverywhere, menu=False), + "11": Option(self.upgrade_system_packages, menu=False), } def print_menu(self): @@ -114,8 +119,9 @@ class UpdateMenu(BaseMenu): ║ 7) KlipperScreen │ {self.ks_local:<22} │ {self.ks_remote:<22} ║ ║ 8) Mobileraker │ {self.mb_local:<22} │ {self.mb_remote:<22} ║ ║ 9) Crowsnest │ {self.cn_local:<22} │ {self.cn_remote:<22} ║ + ║ 10) OctoEverywhere │ {self.oe_local:<22} │ {self.oe_remote:<22} ║ ║ ├───────────────┴───────────────╢ - ║ 10) System │ ║ + ║ 11) System │ ║ ╟───────────────────────┴───────────────────────────────╢ """ )[1:] @@ -151,6 +157,9 @@ class UpdateMenu(BaseMenu): def update_crowsnest(self, **kwargs): update_crowsnest() + def update_octoeverywhere(self, **kwargs): + update_octoeverywhere() + def upgrade_system_packages(self, **kwargs): ... def _fetch_update_status(self): @@ -172,6 +181,8 @@ class UpdateMenu(BaseMenu): self._get_update_status("mb", get_mobileraker_status) # crowsnest self._get_update_status("cn", get_crowsnest_status) + # octoeverywhere + self._get_update_status("oe", get_octoeverywhere_status) def _format_local_status(self, local_version, remote_version) -> str: if local_version == remote_version: diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 7aff799..a813c0f 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -219,6 +219,5 @@ class KiauhSettings: "● default.kiauh.cfg", "● kiauh.cfg", ], - end="", ) kill() diff --git a/kiauh/extensions/obico/moonraker_obico_extension.py b/kiauh/extensions/obico/moonraker_obico_extension.py index bd70d79..e07b842 100644 --- a/kiauh/extensions/obico/moonraker_obico_extension.py +++ b/kiauh/extensions/obico/moonraker_obico_extension.py @@ -57,6 +57,7 @@ class ObicoExtension(BaseExtension): # if obico is already installed, ask if the user wants to repair an # incomplete installation or link to the obico server + force_clone = False obico_im = InstanceManager(MoonrakerObico) obico_instances: List[MoonrakerObico] = obico_im.instances if obico_instances: @@ -74,6 +75,7 @@ class ObicoExtension(BaseExtension): return else: Logger.print_status("Re-Installing Obico for Klipper ...") + force_clone = True # let the user confirm installation kl_im = InstanceManager(Klipper) @@ -89,7 +91,7 @@ class ObicoExtension(BaseExtension): return try: - git_clone_wrapper(OBICO_REPO, OBICO_DIR) + git_clone_wrapper(OBICO_REPO, OBICO_DIR, force=force_clone) self._install_dependencies() # ask the user for the obico server url @@ -192,7 +194,6 @@ class ObicoExtension(BaseExtension): "http://server_ip:port", "For instance, 'http://192.168.0.5:3334'.", ], - end="", ) def _print_moonraker_instances(self, mr_instances) -> None: @@ -206,7 +207,6 @@ class ObicoExtension(BaseExtension): "\n\n", "The setup will apply the same names to Obico!", ], - end="", ) def _print_is_already_installed(self) -> None: @@ -214,14 +214,13 @@ class ObicoExtension(BaseExtension): DialogType.INFO, [ "Obico is already installed!", - "It is save to run the installer again to link your " + "It is safe to run the installer again to link your " "printer or repair any issues.", "\n\n", "You can perform the following actions:", "L) Link printer to the Obico server", "R) Repair installation", ], - end="", ) def _get_server_url(self) -> None: @@ -324,7 +323,6 @@ class ObicoExtension(BaseExtension): "If you don't want to link the printer now, you can restart the " "linking process later by running this installer again.", ], - end="", ) if not get_confirm("Do you want to link the printers now?"): Logger.print_info("Linking to Obico server skipped ...") diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 28b0a6f..98d982b 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -150,7 +150,6 @@ def moonraker_exists(name: str = "") -> bool: "No Moonraker instances found!", f"{info}. Please install Moonraker first!", ], - end="", ) return False return True diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 111294b..7149aad 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -59,9 +59,9 @@ def create_symlink(source: Path, target: Path, sudo=False) -> None: raise -def remove_with_sudo(file_path: Path) -> None: +def remove_with_sudo(file: Path) -> None: try: - cmd = ["sudo", "rm", "-f", file_path] + cmd = ["sudo", "rm", "-rf", file] run(cmd, stderr=PIPE, check=True) except CalledProcessError as e: Logger.print_error(f"Failed to remove file: {e}") @@ -79,6 +79,30 @@ def remove_file(file_path: Path, sudo=False) -> None: raise +def run_remove_routines(file: Path) -> None: + try: + if not file.exists(): + Logger.print_info(f"File '{file}' does not exist. Skipped ...") + return + + if file.is_dir(): + shutil.rmtree(file) + elif file.is_file(): + file.unlink() + else: + raise OSError(f"File '{file}' is neither a file nor a directory!") + Logger.print_ok("Successfully removed!") + except OSError as e: + Logger.print_error(f"Unable to delete '{file}':\n{e}") + try: + Logger.print_info("Trying to remove with sudo ...") + remove_with_sudo(file) + Logger.print_ok("Successfully removed!") + except CalledProcessError as e: + Logger.print_error(f"Error deleting '{file}' with sudo:\n{e}") + Logger.print_error("Remove this directory manually!") + + def unzip(filepath: Path, target_dir: Path) -> None: """ Helper function to unzip a zip-archive into a target directory | diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py index d06a2d8..98dead9 100644 --- a/kiauh/utils/git_utils.py +++ b/kiauh/utils/git_utils.py @@ -14,7 +14,7 @@ from utils.logger import Logger def git_clone_wrapper( - repo: str, target_dir: Path, branch: Optional[str] = None + repo: str, target_dir: Path, branch: Optional[str] = None, force: bool = False ) -> None: """ Clones a repository from the given URL and checks out the specified branch if given. @@ -22,6 +22,7 @@ def git_clone_wrapper( :param repo: The URL of the repository to clone. :param branch: The branch to check out. If None, the default branch will be checked out. :param target_dir: The directory where the repository will be cloned. + :param force: Force the cloning of the repository even if it already exists. :return: None """ log = f"Cloning repository from '{repo}'" @@ -29,7 +30,7 @@ def git_clone_wrapper( try: if Path(target_dir).exists(): question = f"'{target_dir}' already exists. Overwrite?" - if not get_confirm(question, default_choice=False): + if not force and not get_confirm(question, default_choice=False): Logger.print_info("Skip cloning of repository ...") return shutil.rmtree(target_dir) diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index ef25ee5..b4d7ae0 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -90,7 +90,8 @@ class Logger: center_content: bool = False, custom_title: str = None, custom_color: DialogCustomColor = None, - end: str = "\n", + padding_top: int = 1, + padding_bottom: int = 1, ) -> None: dialog_color = Logger._get_dialog_color(title, custom_color) dialog_title = Logger._get_dialog_title(title, custom_title) @@ -99,10 +100,12 @@ class Logger: top = Logger._format_top_border(dialog_color) bottom = Logger._format_bottom_border() + print("\n" * padding_top) print( f"{top}{dialog_title_formatted}{dialog_content}{bottom}", - end=end, + end="", ) + print("\n" * padding_bottom) @staticmethod def _get_dialog_title(title: DialogType, custom_title: str = None) -> str: @@ -120,18 +123,12 @@ class Logger: @staticmethod def _format_top_border(color: str) -> str: - return textwrap.dedent( - f""" - {color}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - """ - )[1:-1] + return f"{color}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" @staticmethod def _format_bottom_border() -> str: - return textwrap.dedent( - f""" - ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - {RESET_FORMAT}""" + return ( + f"\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛{RESET_FORMAT}" ) @staticmethod From 7444ae8cea70ebd48b13637a6e9a4913ce68ae9a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 29 Jun 2024 00:08:19 +0200 Subject: [PATCH 258/296] refactor: client dialog improvements Signed-off-by: Dominik Willner --- .../components/webui_client/client_dialogs.py | 103 +++++++----------- kiauh/components/webui_client/client_setup.py | 5 +- 2 files changed, 41 insertions(+), 67 deletions(-) diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py index e723274..c4ef7f0 100644 --- a/kiauh/components/webui_client/client_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -7,82 +7,57 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import textwrap from typing import List from components.webui_client.base_data import BaseWebClient -from core.menus.base_menu import print_back_footer -from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT from utils.logger import DialogType, Logger def print_moonraker_not_found_dialog(): - line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" - line2 = f"{COLOR_YELLOW}No local Moonraker installation was found!{RESET_FORMAT}" - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | {line1:<63}| - | {line2:<63}| - |-------------------------------------------------------| - | It is possible to install Mainsail without a local | - | Moonraker installation. If you continue, you need to | - | make sure, that Moonraker is installed on another | - | machine in your network. Otherwise Mainsail will NOT | - | work correctly. | - """ - )[1:] - - print(dialog, end="") - print_back_footer() + Logger.print_dialog( + DialogType.WARNING, + [ + "No local Moonraker installation was found!", + "\n\n", + "It is possible to install Mainsail without a local Moonraker installation. " + "If you continue, you need to make sure, that Moonraker is installed on " + "another machine in your network. Otherwise Mainsail will NOT work " + "correctly.", + ], + padding_top=0, + padding_bottom=0, + ) def print_client_already_installed_dialog(name: str): - line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" - line2 = f"{COLOR_YELLOW}{name} seems to be already installed!{RESET_FORMAT}" - line3 = f"If you continue, your current {name}" - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | {line1:<63}| - | {line2:<63}| - |-------------------------------------------------------| - | {line3:<54}| - | installation will be overwritten. | - """ - )[1:] - - print(dialog, end="") - print_back_footer() + Logger.print_dialog( + DialogType.WARNING, + [ + f"{name} seems to be already installed!", + f"If you continue, your current {name} installation will be overwritten.", + ], + padding_top=0, + padding_bottom=0, + ) def print_client_port_select_dialog(name: str, port: int, ports_in_use: List[int]): - port = f"{COLOR_CYAN}{port}{RESET_FORMAT}" - line1 = f"Please select the port, {name} should be served on." - line2 = f"In case you need {name} to be served on a specific" - dialog = textwrap.dedent( - f""" - /=======================================================\\ - | {line1:<54}| - | If you are unsure what to select, hit Enter to apply | - | the suggested value of: {port:38} | - | | - | {line2:<54}| - | port, you can set it now. Make sure the port is not | - | used by any other application on your system! | - """ - )[1:] - - if len(ports_in_use) > 0: - dialog += "|-------------------------------------------------------|\n" - dialog += "| The following ports were found to be in use already: |\n" - for port in ports_in_use: - port = f"{COLOR_CYAN}● {port}{RESET_FORMAT}" - dialog += f"| {port:60} |\n" - - dialog += "\\=======================================================/\n" - - print(dialog, end="") + Logger.print_dialog( + DialogType.CUSTOM, + [ + f"Please select the port, {name} should be served on. If your are unsure " + f"what to select, hit Enter to apply the suggested value of: {port}", + "\n\n", + f"In case you need {name} to be served on a specific port, you can set it " + f"now. Make sure that the port is not already used by another application " + f"on your system!", + "\n\n", + "The following ports were found to be in use already:", + *[f"● {port}" for port in ports_in_use], + ], + padding_top=0, + padding_bottom=0, + ) def print_install_client_config_dialog(client: BaseWebClient) -> None: @@ -100,6 +75,8 @@ def print_install_client_config_dialog(client: BaseWebClient) -> None: "If you already use these macros skip this step. Otherwise you should " "consider to answer with 'Y' to download the recommended macros.", ], + padding_top=0, + padding_bottom=0, ) diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index b1eda56..ec38cd9 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -73,10 +73,7 @@ def install_client(client: BaseWebClient) -> None: enable_remotemode = False if not mr_instances: print_moonraker_not_found_dialog() - if not get_confirm( - f"Continue {client.display_name} installation?", - allow_go_back=True, - ): + if not get_confirm(f"Continue {client.display_name} installation?"): return # if moonraker is not installed or multiple instances From 2ad11d68de7b9976a21c8b67243b5ca1459af550 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 29 Jun 2024 08:11:07 +0200 Subject: [PATCH 259/296] refactor: remove ipv6 check doesn't seem to be necessary Signed-off-by: Dominik Willner --- kiauh/components/webui_client/client_setup.py | 9 ------ kiauh/utils/sys_utils.py | 31 ------------------- 2 files changed, 40 deletions(-) diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index ec38cd9..c8f671a 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -24,7 +24,6 @@ from components.webui_client.client_config.client_config_setup import ( from components.webui_client.client_dialogs import ( print_client_port_select_dialog, print_install_client_config_dialog, - print_ipv6_warning_dialog, print_moonraker_not_found_dialog, ) from components.webui_client.client_utils import ( @@ -50,7 +49,6 @@ from utils.fs_utils import ( from utils.input_utils import get_confirm, get_number_input from utils.logger import Logger from utils.sys_utils import ( - check_ipv6, cmd_sysctl_service, download_file, get_ipv4_addr, @@ -114,13 +112,6 @@ def install_client(client: BaseWebClient) -> None: ) valid_port = is_valid_port(port, ports_in_use) - # check if ipv6 is enabled, as this may cause issues with nginx - if check_ipv6(): - print_ipv6_warning_dialog() - if not get_confirm(f"Continue with {client.display_name} installation?"): - Logger.print_info(f"Exiting {client.display_name} installation ...") - return - check_install_dependencies(["nginx", "unzip"]) try: diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 8f2ec5e..d3d3b0d 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -402,34 +402,3 @@ def log_process(process: Popen) -> None: if process.poll() is not None: break - - -def check_ipv6() -> bool: - """ - Check if IPv6 is enabled - :return: True if IPv6 is enabled, False otherwise - """ - try: - file1 = Path("/proc/sys/net/ipv6/conf/all/disable_ipv6") - if file1.exists(): - with open(file1, "r") as file: - if file.read().strip() == "0": - return True - elif file.read().strip() == "1": - return False - - file3 = Path("/etc/sysctl.conf") - if file3.exists(): - with open(file3, "r") as file: - for line in file.readlines(): - if ( - "net.ipv6.conf.all.disable_ipv6" in line - and not line.startswith("#") - and line.split("=")[1].strip() == "0" - ): - return True - return False - - except Exception as e: - Logger.print_error(f"Error checking IPv6: {e}") - return True From 59e619ea0ff7f99a749b8fe9b0f476ba885c0bb8 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 29 Jun 2024 08:13:24 +0200 Subject: [PATCH 260/296] refactor: fix padding in dialog Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 1 + kiauh/components/klipperscreen/klipperscreen.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index c4b852f..3132ad1 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -220,6 +220,7 @@ def check_user_groups(): "INFO:", "Relog required for group assignments to take effect!", ], + padding_bottom=0, ) if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index d64d500..23a456d 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -62,6 +62,8 @@ def install_klipperscreen() -> None: "KlipperScreens update manager configuration for Moonraker " "will not be added to any moonraker.conf.", ], + padding_top=0, + padding_bottom=0, ) if not get_confirm( "Continue KlipperScreen installation?", From 61618d064d5879b21c04f2a5d079247fcd89be58 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 29 Jun 2024 08:55:51 +0200 Subject: [PATCH 261/296] refactor: go back do remove menu when component was removed Signed-off-by: Dominik Willner --- .../klipper/menus/klipper_remove_menu.py | 37 ++++++++++----- .../moonraker/menus/moonraker_remove_menu.py | 39 ++++++++++----- .../components/webui_client/client_remove.py | 8 ++-- .../webui_client/menus/client_remove_menu.py | 47 ++++++++++++------- 4 files changed, 85 insertions(+), 46 deletions(-) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index b80983b..f61459b 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -25,7 +25,8 @@ class KlipperRemoveMenu(BaseMenu): self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False - self.delete_klipper_logs = False + self.remove_klipper_logs = False + self.selection_state = False def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.remove_menu import RemoveMenu @@ -36,7 +37,7 @@ class KlipperRemoveMenu(BaseMenu): def set_options(self) -> None: self.options = { - "0": Option(method=self.toggle_all, menu=False), + "a": Option(method=self.toggle_all, menu=False), "1": Option(method=self.toggle_remove_klipper_service, menu=False), "2": Option(method=self.toggle_remove_klipper_dir, menu=False), "3": Option(method=self.toggle_remove_klipper_env, menu=False), @@ -53,7 +54,7 @@ class KlipperRemoveMenu(BaseMenu): o1 = checked if self.remove_klipper_service else unchecked o2 = checked if self.remove_klipper_dir else unchecked o3 = checked if self.remove_klipper_env else unchecked - o4 = checked if self.delete_klipper_logs else unchecked + o4 = checked if self.remove_klipper_logs else unchecked menu = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ @@ -62,7 +63,7 @@ class KlipperRemoveMenu(BaseMenu): ║ Enter a number and hit enter to select / deselect ║ ║ the specific option for removal. ║ ╟───────────────────────────────────────────────────────╢ - ║ 0) Select everything ║ + ║ a) {self._get_selection_state_str():37} ║ ╟───────────────────────────────────────────────────────╢ ║ 1) {o1} Remove Service ║ ║ 2) {o2} Remove Local Repository ║ @@ -76,10 +77,11 @@ class KlipperRemoveMenu(BaseMenu): print(menu, end="") def toggle_all(self, **kwargs) -> None: - self.remove_klipper_service = True - self.remove_klipper_dir = True - self.remove_klipper_env = True - self.delete_klipper_logs = True + self.remove_klipper_service = not self.remove_klipper_service + self.remove_klipper_dir = not self.remove_klipper_dir + self.remove_klipper_env = not self.remove_klipper_env + self.remove_klipper_logs = not self.remove_klipper_logs + self.selection_state = not self.selection_state def toggle_remove_klipper_service(self, **kwargs) -> None: self.remove_klipper_service = not self.remove_klipper_service @@ -91,14 +93,14 @@ class KlipperRemoveMenu(BaseMenu): self.remove_klipper_env = not self.remove_klipper_env def toggle_delete_klipper_logs(self, **kwargs) -> None: - self.delete_klipper_logs = not self.delete_klipper_logs + self.remove_klipper_logs = not self.remove_klipper_logs def run_removal_process(self, **kwargs) -> None: if ( not self.remove_klipper_service and not self.remove_klipper_dir and not self.remove_klipper_env - and not self.delete_klipper_logs + and not self.remove_klipper_logs ): error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" print(error) @@ -108,10 +110,21 @@ class KlipperRemoveMenu(BaseMenu): self.remove_klipper_service, self.remove_klipper_dir, self.remove_klipper_env, - self.delete_klipper_logs, + self.remove_klipper_logs, ) self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False - self.delete_klipper_logs = False + self.remove_klipper_logs = 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() diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 85054dd..03a458a 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -25,7 +25,8 @@ class MoonrakerRemoveMenu(BaseMenu): self.remove_moonraker_dir = False self.remove_moonraker_env = False self.remove_moonraker_polkit = False - self.delete_moonraker_logs = False + self.remove_moonraker_logs = False + self.selection_state = False def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.remove_menu import RemoveMenu @@ -36,7 +37,7 @@ class MoonrakerRemoveMenu(BaseMenu): def set_options(self) -> None: self.options = { - "0": Option(method=self.toggle_all, menu=False), + "a": Option(method=self.toggle_all, menu=False), "1": Option(method=self.toggle_remove_moonraker_service, menu=False), "2": Option(method=self.toggle_remove_moonraker_dir, menu=False), "3": Option(method=self.toggle_remove_moonraker_env, menu=False), @@ -55,7 +56,7 @@ class MoonrakerRemoveMenu(BaseMenu): 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 - o5 = checked if self.delete_moonraker_logs else unchecked + o5 = checked if self.remove_moonraker_logs else unchecked menu = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ @@ -64,7 +65,7 @@ class MoonrakerRemoveMenu(BaseMenu): ║ Enter a number and hit enter to select / deselect ║ ║ the specific option for removal. ║ ╟───────────────────────────────────────────────────────╢ - ║ 0) Select everything ║ + ║ a) {self._get_selection_state_str():37} ║ ╟───────────────────────────────────────────────────────╢ ║ 1) {o1} Remove Service ║ ║ 2) {o2} Remove Local Repository ║ @@ -79,11 +80,12 @@ class MoonrakerRemoveMenu(BaseMenu): print(menu, end="") def toggle_all(self, **kwargs) -> None: - self.remove_moonraker_service = True - self.remove_moonraker_dir = True - self.remove_moonraker_env = True - self.remove_moonraker_polkit = True - self.delete_moonraker_logs = True + self.remove_moonraker_service = not self.remove_moonraker_service + self.remove_moonraker_dir = not self.remove_moonraker_dir + self.remove_moonraker_env = not self.remove_moonraker_env + self.remove_moonraker_polkit = not self.remove_moonraker_polkit + self.remove_moonraker_logs = not self.remove_moonraker_logs + self.selection_state = not self.selection_state def toggle_remove_moonraker_service(self, **kwargs) -> None: self.remove_moonraker_service = not self.remove_moonraker_service @@ -98,7 +100,7 @@ class MoonrakerRemoveMenu(BaseMenu): self.remove_moonraker_polkit = not self.remove_moonraker_polkit def toggle_delete_moonraker_logs(self, **kwargs) -> None: - self.delete_moonraker_logs = not self.delete_moonraker_logs + self.remove_moonraker_logs = not self.remove_moonraker_logs def run_removal_process(self, **kwargs) -> None: if ( @@ -106,7 +108,7 @@ class MoonrakerRemoveMenu(BaseMenu): and not self.remove_moonraker_dir and not self.remove_moonraker_env and not self.remove_moonraker_polkit - and not self.delete_moonraker_logs + and not self.remove_moonraker_logs ): error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" print(error) @@ -117,11 +119,22 @@ class MoonrakerRemoveMenu(BaseMenu): self.remove_moonraker_dir, self.remove_moonraker_env, self.remove_moonraker_polkit, - self.delete_moonraker_logs, + self.remove_moonraker_logs, ) self.remove_moonraker_service = False self.remove_moonraker_dir = False self.remove_moonraker_env = False self.remove_moonraker_polkit = False - self.delete_moonraker_logs = False + self.remove_moonraker_logs = 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() diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index a4a0b82..2e41c12 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -32,8 +32,8 @@ from utils.logger import Logger def run_client_removal( client: BaseWebClient, - rm_client: bool, - rm_client_config: bool, + remove_client: bool, + remove_client_cfg: bool, backup_ms_config_json: bool, ) -> None: mr_im = InstanceManager(Moonraker) @@ -44,7 +44,7 @@ def run_client_removal( if backup_ms_config_json and client.client == WebClientType.MAINSAIL: backup_mainsail_config_json() - if rm_client: + if remove_client: client_name = client.name remove_client_dir(client) remove_nginx_config(client_name) @@ -53,7 +53,7 @@ def run_client_removal( section = f"update_manager {client_name}" remove_config_section(section, mr_instances) - if rm_client_config: + if remove_client_cfg: run_client_config_removal( client.client_config, kl_instances, diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index 9374177..8b4a270 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -25,9 +25,10 @@ class ClientRemoveMenu(BaseMenu): super().__init__() self.previous_menu = previous_menu self.client = client - self.rm_client = False - self.rm_client_config = False + self.remove_client = False + self.remove_client_cfg = False self.backup_mainsail_config_json = False + self.selection_state = False def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.remove_menu import RemoveMenu @@ -38,7 +39,7 @@ class ClientRemoveMenu(BaseMenu): def set_options(self) -> None: self.options = { - "0": Option(method=self.toggle_all, menu=False), + "a": Option(method=self.toggle_all, menu=False), "1": Option(method=self.toggle_rm_client, menu=False), "2": Option(method=self.toggle_rm_client_config, menu=False), "c": Option(method=self.run_removal_process, menu=False), @@ -56,8 +57,8 @@ class ClientRemoveMenu(BaseMenu): count = 62 - len(color) - len(RESET_FORMAT) checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" unchecked = "[ ]" - o1 = checked if self.rm_client else unchecked - o2 = checked if self.rm_client_config else unchecked + o1 = checked if self.remove_client else unchecked + o2 = checked if self.remove_client_cfg else unchecked menu = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ @@ -66,7 +67,7 @@ class ClientRemoveMenu(BaseMenu): ║ Enter a number and hit enter to select / deselect ║ ║ the specific option for removal. ║ ╟───────────────────────────────────────────────────────╢ - ║ 0) Select everything ║ + ║ a) {self._get_selection_state_str():37} ║ ╟───────────────────────────────────────────────────────╢ ║ 1) {o1} Remove {client_name:16} ║ ║ 2) {o2} Remove {client_config_name:24} ║ @@ -91,23 +92,24 @@ class ClientRemoveMenu(BaseMenu): print(menu, end="") def toggle_all(self, **kwargs) -> None: - self.rm_client = True - self.rm_client_config = True - self.backup_mainsail_config_json = True + self.remove_client = not self.remove_client + self.remove_client_cfg = not self.remove_client_cfg + self.backup_mainsail_config_json = not self.backup_mainsail_config_json + self.selection_state = not self.selection_state def toggle_rm_client(self, **kwargs) -> None: - self.rm_client = not self.rm_client + self.remove_client = not self.remove_client def toggle_rm_client_config(self, **kwargs) -> None: - self.rm_client_config = not self.rm_client_config + self.remove_client_cfg = not self.remove_client_cfg def toggle_backup_mainsail_config_json(self, **kwargs) -> None: self.backup_mainsail_config_json = not self.backup_mainsail_config_json def run_removal_process(self, **kwargs) -> None: if ( - not self.rm_client - and not self.rm_client_config + not self.remove_client + and not self.remove_client_cfg and not self.backup_mainsail_config_json ): error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}" @@ -116,11 +118,22 @@ class ClientRemoveMenu(BaseMenu): client_remove.run_client_removal( client=self.client, - rm_client=self.rm_client, - rm_client_config=self.rm_client_config, + remove_client=self.remove_client, + remove_client_cfg=self.remove_client_cfg, backup_ms_config_json=self.backup_mainsail_config_json, ) - self.rm_client = False - self.rm_client_config = False + self.remove_client = False + self.remove_client_cfg = False self.backup_mainsail_config_json = 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() From 6636365cb764bfe7e3754638730e25a0a007160e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 29 Jun 2024 08:58:34 +0200 Subject: [PATCH 262/296] fix: use correct footer in klipper remove menu Signed-off-by: Dominik Willner --- kiauh/components/klipper/menus/klipper_remove_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index f61459b..915dbb4 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -21,7 +21,7 @@ class KlipperRemoveMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() self.previous_menu = previous_menu - self.footer_type = FooterType.BACK_HELP + self.footer_type = FooterType.BACK self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False From 8a620cdbd458575b9eca4880115c970a3b40cb68 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 29 Jun 2024 09:20:26 +0200 Subject: [PATCH 263/296] refactor: improve component removal routines Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_remove.py | 31 ++---------- .../components/moonraker/moonraker_remove.py | 47 ++++--------------- .../client_config/client_config_remove.py | 27 ++--------- .../components/webui_client/client_remove.py | 12 +---- kiauh/components/webui_client/client_utils.py | 1 - kiauh/utils/fs_utils.py | 25 ++++------ 6 files changed, 27 insertions(+), 116 deletions(-) diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index 8e8eca8..0b75ebd 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -7,14 +7,13 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import shutil from typing import List, Union from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import print_instance_overview from core.instance_manager.instance_manager import InstanceManager -from utils.fs_utils import remove_file +from utils.fs_utils import run_remove_routines from utils.input_utils import get_selection_input from utils.logger import Logger from utils.sys_utils import cmd_sysctl_manage @@ -49,10 +48,10 @@ def run_klipper_removal( else: if remove_dir: Logger.print_status("Removing Klipper local repository ...") - remove_klipper_dir() + run_remove_routines(KLIPPER_DIR) if remove_env: Logger.print_status("Removing Klipper Python environment ...") - remove_klipper_env() + run_remove_routines(KLIPPER_ENV_DIR) # delete klipper logs of all instances if delete_logs: @@ -96,28 +95,6 @@ def remove_instances( cmd_sysctl_manage("daemon-reload") -def remove_klipper_dir() -> None: - if not KLIPPER_DIR.exists(): - Logger.print_info(f"'{KLIPPER_DIR}' does not exist. Skipped ...") - return - - try: - shutil.rmtree(KLIPPER_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{KLIPPER_DIR}':\n{e}") - - -def remove_klipper_env() -> None: - if not KLIPPER_ENV_DIR.exists(): - Logger.print_info(f"'{KLIPPER_ENV_DIR}' does not exist. Skipped ...") - return - - try: - shutil.rmtree(KLIPPER_ENV_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{KLIPPER_ENV_DIR}':\n{e}") - - def delete_klipper_logs(instances: List[Klipper]) -> None: all_logfiles = [] for instance in instances: @@ -128,4 +105,4 @@ def delete_klipper_logs(instances: List[Klipper]) -> None: for log in all_logfiles: Logger.print_status(f"Remove '{log}'") - remove_file(log) + run_remove_routines(log) diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index f3733a8..6c73834 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -7,15 +7,14 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import shutil -import subprocess +from subprocess import DEVNULL, PIPE, CalledProcessError, run from typing import List, Union 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 utils.fs_utils import remove_file +from utils.fs_utils import run_remove_routines from utils.input_utils import get_selection_input from utils.logger import Logger from utils.sys_utils import cmd_sysctl_manage @@ -55,10 +54,10 @@ def run_moonraker_removal( remove_polkit_rules() if remove_dir: Logger.print_status("Removing Moonraker local repository ...") - remove_moonraker_dir() + run_remove_routines(MOONRAKER_DIR) if remove_env: Logger.print_status("Removing Moonraker Python environment ...") - remove_moonraker_env() + run_remove_routines(MOONRAKER_ENV_DIR) # delete moonraker logs of all instances if delete_logs: @@ -102,28 +101,6 @@ def remove_instances( cmd_sysctl_manage("daemon-reload") -def remove_moonraker_dir() -> None: - if not MOONRAKER_DIR.exists(): - Logger.print_info(f"'{MOONRAKER_DIR}' does not exist. Skipped ...") - return - - try: - shutil.rmtree(MOONRAKER_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{MOONRAKER_DIR}':\n{e}") - - -def remove_moonraker_env() -> None: - if not MOONRAKER_ENV_DIR.exists(): - Logger.print_info(f"'{MOONRAKER_ENV_DIR}' does not exist. Skipped ...") - return - - try: - shutil.rmtree(MOONRAKER_ENV_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{MOONRAKER_ENV_DIR}':\n{e}") - - def remove_polkit_rules() -> None: if not MOONRAKER_DIR.exists(): log = "Cannot remove policykit rules. Moonraker directory not found." @@ -131,17 +108,9 @@ def remove_polkit_rules() -> None: return try: - command = [ - f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", - "--clear", - ] - subprocess.run( - command, - stderr=subprocess.PIPE, - stdout=subprocess.DEVNULL, - check=True, - ) - except subprocess.CalledProcessError as e: + 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!") @@ -157,4 +126,4 @@ def delete_moonraker_logs(instances: List[Moonraker]) -> None: for log in all_logfiles: Logger.print_status(f"Remove '{log}'") - remove_file(log) + run_remove_routines(log) diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index cf8add7..b0324fc 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -8,8 +8,6 @@ # ======================================================================= # -import shutil -import subprocess from typing import List from components.klipper.klipper import Klipper @@ -17,7 +15,7 @@ from components.moonraker.moonraker import Moonraker from components.webui_client.base_data import BaseWebClientConfig from core.instance_manager.instance_manager import InstanceManager from utils.config_utils import remove_config_section -from utils.fs_utils import remove_file +from utils.fs_utils import run_remove_routines from utils.logger import Logger @@ -33,29 +31,12 @@ def run_client_config_removal( def remove_client_config_dir(client_config: BaseWebClientConfig) -> None: - Logger.print_status(f"Removing {client_config.name} ...") - client_config_dir = client_config.config_dir - if not client_config_dir.exists(): - Logger.print_info(f"'{client_config_dir}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(client_config_dir) - except OSError as e: - Logger.print_error(f"Unable to delete '{client_config_dir}':\n{e}") + Logger.print_status(f"Removing {client_config.display_name} ...") + run_remove_routines(client_config.config_dir) def remove_client_config_symlink(client_config: BaseWebClientConfig) -> None: im = InstanceManager(Klipper) instances: List[Klipper] = im.instances for instance in instances: - Logger.print_status(f"Removing symlink from '{instance.cfg_dir}' ...") - symlink = instance.cfg_dir.joinpath(client_config.config_filename) - if not symlink.is_symlink(): - Logger.print_info(f"'{symlink}' does not exist. Skipping ...") - continue - - try: - remove_file(symlink) - except subprocess.CalledProcessError: - Logger.print_error("Failed to remove symlink!") + run_remove_routines(instance.cfg_dir.joinpath(client_config.config_filename)) diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index 2e41c12..64669f8 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -8,7 +8,6 @@ # ======================================================================= # -import shutil from typing import List from components.klipper.klipper import Klipper @@ -26,6 +25,7 @@ from utils.config_utils import remove_config_section from utils.fs_utils import ( remove_nginx_config, remove_nginx_logs, + run_remove_routines, ) from utils.logger import Logger @@ -63,12 +63,4 @@ def run_client_removal( def remove_client_dir(client: BaseWebClient) -> None: Logger.print_status(f"Removing {client.display_name} ...") - client_dir = client.client_dir - if not client.client_dir.exists(): - Logger.print_info(f"'{client_dir}' does not exist. Skipping ...") - return - - try: - shutil.rmtree(client_dir) - except OSError as e: - Logger.print_error(f"Unable to delete '{client_dir}':\n{e}") + run_remove_routines(client.client_dir) diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 7b42ae2..b64b7fb 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -74,7 +74,6 @@ def get_current_client_config(clients: List[BaseWebClient]) -> str: def backup_mainsail_config_json(is_temp=False) -> None: c_json = MainsailData().client_dir.joinpath("config.json") - Logger.print_status(f"Backup '{c_json}' ...") bm = BackupManager() if is_temp: fn = Path.home().joinpath("config.json.kiauh.bak") diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 7149aad..38fc37d 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -237,27 +237,20 @@ def get_next_free_port(ports_in_use: List[int]) -> int: def remove_nginx_config(name: str) -> None: Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...") - try: - remove_file(NGINX_SITES_AVAILABLE.joinpath(name), True) - remove_file(NGINX_SITES_ENABLED.joinpath(name), True) - except CalledProcessError as e: - log = f"Unable to remove NGINX config '{name}':\n{e.stderr.decode()}" - Logger.print_error(log) + run_remove_routines(NGINX_SITES_AVAILABLE.joinpath(name)) + run_remove_routines(NGINX_SITES_ENABLED.joinpath(name)) def remove_nginx_logs(name: str, instances: List[Klipper]) -> None: Logger.print_status(f"Removing NGINX logs for {name.capitalize()} ...") - try: - remove_file(Path(f"/var/log/nginx/{name}-access.log"), True) - remove_file(Path(f"/var/log/nginx/{name}-error.log"), True) - if not instances: - return + run_remove_routines(Path(f"/var/log/nginx/{name}-access.log")) + run_remove_routines(Path(f"/var/log/nginx/{name}-error.log")) - for instance in instances: - remove_file(instance.log_dir.joinpath(f"{name}-access.log")) - remove_file(instance.log_dir.joinpath(f"{name}-error.log")) + if not instances: + return - except (OSError, CalledProcessError) as e: - Logger.print_error(f"Unable to remove NGINX logs:\n{e}") + for instance in instances: + run_remove_routines(instance.log_dir.joinpath(f"{name}-access.log")) + run_remove_routines(instance.log_dir.joinpath(f"{name}-error.log")) From 956666605cfe05ec39e63afadda4546725b01ae3 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 13:45:07 +0200 Subject: [PATCH 264/296] refactor: rework update menu, logic and typing Signed-off-by: Dominik Willner --- kiauh/components/webui_client/client_utils.py | 23 +-- kiauh/core/menus/main_menu.py | 22 ++- kiauh/core/menus/update_menu.py | 161 +++++++++++------- kiauh/utils/common.py | 10 +- kiauh/utils/git_utils.py | 21 +-- kiauh/utils/types.py | 39 +++-- 6 files changed, 163 insertions(+), 113 deletions(-) diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index b64b7fb..1548876 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -6,6 +6,7 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations import json # noqa: I001 import shutil @@ -29,7 +30,7 @@ from utils.git_utils import ( get_latest_unstable_tag, ) from utils.logger import Logger -from utils.types import ComponentStatus, InstallStatus +from utils.types import ComponentStatus def get_client_status( @@ -40,16 +41,16 @@ def get_client_status( NGINX_CONFD.joinpath("upstreams.conf"), NGINX_CONFD.joinpath("common_vars.conf"), ] - status = get_install_status(client.client_dir, files=files) + comp_status: ComponentStatus = get_install_status(client.client_dir, files=files) # if the client dir does not exist, set the status to not # installed even if the other files are present if not client.client_dir.exists(): - status["status"] = InstallStatus.NOT_INSTALLED + comp_status.status = 0 - status["local"] = get_local_client_version(client) - status["remote"] = get_remote_client_version(client) if fetch_remote else None - return status + comp_status.local = get_local_client_version(client) + comp_status.remote = get_remote_client_version(client) if fetch_remote else None + return comp_status def get_client_config_status(client: BaseWebClient) -> ComponentStatus: @@ -125,12 +126,12 @@ def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None: desti_error.symlink_to(error_log) -def get_local_client_version(client: BaseWebClient) -> str: +def get_local_client_version(client: BaseWebClient) -> str | None: relinfo_file = client.client_dir.joinpath("release_info.json") version_file = client.client_dir.joinpath(".version") if not client.client_dir.exists(): - return "-" + return None if not relinfo_file.is_file() and not version_file.is_file(): return "n/a" @@ -142,13 +143,13 @@ def get_local_client_version(client: BaseWebClient) -> str: return f.readlines()[0] -def get_remote_client_version(client: BaseWebClient) -> str: +def get_remote_client_version(client: BaseWebClient) -> str | None: try: if (tag := get_latest_tag(client.repo_path)) != "": return tag - return "ERROR" + return None except Exception: - return "ERROR" + return None def backup_client_data(client: BaseWebClient) -> None: diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 2a6497b..4000457 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -41,7 +41,7 @@ from utils.constants import ( RESET_FORMAT, ) from utils.logger import Logger -from utils.types import ComponentStatus +from utils.types import ComponentStatus, StatusMap, StatusText # noinspection PyUnusedLocal @@ -84,6 +84,7 @@ class MainMenu(BaseMenu): ) def fetch_status(self) -> None: + pass self._get_component_status("kl", get_klipper_status) self._get_component_status("mr", get_moonraker_status) self._get_component_status("ms", get_client_status, MainsailData()) @@ -96,10 +97,10 @@ class MainMenu(BaseMenu): def _get_component_status(self, name: str, status_fn: callable, *args) -> None: status_data: ComponentStatus = status_fn(*args) - code: int = status_data.get("status").value.code - status: str = status_data.get("status").value.txt - repo: str = status_data.get("repo") - instance_count: int = status_data.get("instances") + code: int = status_data.status + status: StatusText = StatusMap[code] + repo: str = status_data.repo + instance_count: int = status_data.instances count_txt: str = "" if instance_count > 0 and code == 1: @@ -109,12 +110,15 @@ class MainMenu(BaseMenu): setattr(self, f"{name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT}") def _format_by_code(self, code: int, status: str, count: str) -> str: - if code == 1: - return f"{COLOR_GREEN}{status}{count}{RESET_FORMAT}" + color = COLOR_RED + if code == 0: + color = COLOR_RED + elif code == 1: + color = COLOR_YELLOW elif code == 2: - return f"{COLOR_RED}{status}{count}{RESET_FORMAT}" + color = COLOR_GREEN - return f"{COLOR_YELLOW}{status}{count}{RESET_FORMAT}" + return f"{color}{status}{count}{RESET_FORMAT}" def print_menu(self): self.fetch_status() diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 5e4f3c8..5ea217e 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -47,6 +47,7 @@ from utils.constants import ( COLOR_YELLOW, RESET_FORMAT, ) +from utils.logger import Logger from utils.types import ComponentStatus @@ -57,14 +58,32 @@ class UpdateMenu(BaseMenu): super().__init__() self.previous_menu = previous_menu - self.kl_local = self.kl_remote = self.mr_local = self.mr_remote = "" - self.ms_local = self.ms_remote = self.fl_local = self.fl_remote = "" - self.mc_local = self.mc_remote = self.fc_local = self.fc_remote = "" - self.ks_local = self.ks_remote = self.mb_local = self.mb_remote = "" - self.cn_local = self.cn_remote = self.oe_local = self.oe_remote = "" + self.klipper_local = self.klipper_remote = None + self.moonraker_local = self.moonraker_remote = None + self.mainsail_local = self.mainsail_remote = None + self.mainsail_config_local = self.mainsail_config_remote = None + self.fluidd_local = self.fluidd_remote = None + self.fluidd_config_local = self.fluidd_config_remote = None + self.klipperscreen_local = self.klipperscreen_remote = None + self.mobileraker_local = self.mobileraker_remote = None + self.crowsnest_local = self.crowsnest_remote = None + self.octoeverywhere_local = self.octoeverywhere_remote = None self.mainsail_data = MainsailData() self.fluidd_data = FluiddData() + self.status_data = { + "klipper": {"installed": False, "local": None, "remote": None}, + "moonraker": {"installed": False, "local": None, "remote": None}, + "mainsail": {"installed": False, "local": None, "remote": None}, + "mainsail_config": {"installed": False, "local": None, "remote": None}, + "fluidd": {"installed": False, "local": None, "remote": None}, + "fluidd_config": {"installed": False, "local": None, "remote": None}, + "mobileraker": {"installed": False, "local": None, "remote": None}, + "klipperscreen": {"installed": False, "local": None, "remote": None}, + "crowsnest": {"installed": False, "local": None, "remote": None}, + "octoeverywhere": {"installed": False, "local": None, "remote": None}, + } + self._fetch_update_status() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: @@ -76,7 +95,7 @@ class UpdateMenu(BaseMenu): def set_options(self) -> None: self.options = { - "0": Option(self.update_all, menu=False), + "a": Option(self.update_all, menu=False), "1": Option(self.update_klipper, menu=False), "2": Option(self.update_moonraker, menu=False), "3": Option(self.update_mainsail, menu=False), @@ -101,25 +120,25 @@ class UpdateMenu(BaseMenu): ╔═══════════════════════════════════════════════════════╗ ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────┬───────────────┬───────────────╢ - ║ 0) Update all │ │ ║ + ║ a) Update all │ │ ║ ║ │ Current: │ Latest: ║ ║ Klipper & API: ├───────────────┼───────────────╢ - ║ 1) Klipper │ {self.kl_local:<22} │ {self.kl_remote:<22} ║ - ║ 2) Moonraker │ {self.mr_local:<22} │ {self.mr_remote:<22} ║ + ║ 1) Klipper │ {self.klipper_local:<22} │ {self.klipper_remote:<22} ║ + ║ 2) Moonraker │ {self.moonraker_local:<22} │ {self.moonraker_remote:<22} ║ ║ │ │ ║ ║ Webinterface: ├───────────────┼───────────────╢ - ║ 3) Mainsail │ {self.ms_local:<22} │ {self.ms_remote:<22} ║ - ║ 4) Fluidd │ {self.fl_local:<22} │ {self.fl_remote:<22} ║ + ║ 3) Mainsail │ {self.mainsail_local:<22} │ {self.mainsail_remote:<22} ║ + ║ 4) Fluidd │ {self.fluidd_local:<22} │ {self.fluidd_remote:<22} ║ ║ │ │ ║ ║ Client-Config: ├───────────────┼───────────────╢ - ║ 5) Mainsail-Config │ {self.mc_local:<22} │ {self.mc_remote:<22} ║ - ║ 6) Fluidd-Config │ {self.fc_local:<22} │ {self.fc_remote:<22} ║ + ║ 5) Mainsail-Config │ {self.mainsail_config_local:<22} │ {self.mainsail_config_remote:<22} ║ + ║ 6) Fluidd-Config │ {self.fluidd_config_local:<22} │ {self.fluidd_config_remote:<22} ║ ║ │ │ ║ ║ Other: ├───────────────┼───────────────╢ - ║ 7) KlipperScreen │ {self.ks_local:<22} │ {self.ks_remote:<22} ║ - ║ 8) Mobileraker │ {self.mb_local:<22} │ {self.mb_remote:<22} ║ - ║ 9) Crowsnest │ {self.cn_local:<22} │ {self.cn_remote:<22} ║ - ║ 10) OctoEverywhere │ {self.oe_local:<22} │ {self.oe_remote:<22} ║ + ║ 7) KlipperScreen │ {self.klipperscreen_local:<22} │ {self.klipperscreen_remote:<22} ║ + ║ 8) Mobileraker │ {self.mobileraker_local:<22} │ {self.mobileraker_remote:<22} ║ + ║ 9) Crowsnest │ {self.crowsnest_local:<22} │ {self.crowsnest_remote:<22} ║ + ║ 10) OctoEverywhere │ {self.octoeverywhere_local:<22} │ {self.octoeverywhere_remote:<22} ║ ║ ├───────────────┴───────────────╢ ║ 11) System │ ║ ╟───────────────────────┴───────────────────────────────╢ @@ -131,68 +150,96 @@ class UpdateMenu(BaseMenu): print("update_all") def update_klipper(self, **kwargs): - update_klipper() + if self._check_is_installed("klipper"): + update_klipper() def update_moonraker(self, **kwargs): - update_moonraker() + if self._check_is_installed("moonraker"): + update_moonraker() def update_mainsail(self, **kwargs): - update_client(self.mainsail_data) + if self._check_is_installed("mainsail"): + update_client(self.mainsail_data) def update_mainsail_config(self, **kwargs): - update_client_config(self.mainsail_data) + if self._check_is_installed("mainsail_config"): + update_client_config(self.mainsail_data) def update_fluidd(self, **kwargs): - update_client(self.fluidd_data) + if self._check_is_installed("fluidd"): + update_client(self.fluidd_data) def update_fluidd_config(self, **kwargs): - update_client_config(self.fluidd_data) + if self._check_is_installed("fluidd_config"): + update_client_config(self.fluidd_data) def update_klipperscreen(self, **kwargs): - update_klipperscreen() + if self._check_is_installed("klipperscreen"): + update_klipperscreen() def update_mobileraker(self, **kwargs): - update_mobileraker() + if self._check_is_installed("mobileraker"): + update_mobileraker() def update_crowsnest(self, **kwargs): - update_crowsnest() + if self._check_is_installed("crowsnest"): + update_crowsnest() def update_octoeverywhere(self, **kwargs): - update_octoeverywhere() + if self._check_is_installed("octoeverywhere"): + update_octoeverywhere() def upgrade_system_packages(self, **kwargs): ... def _fetch_update_status(self): - # klipper - self._get_update_status("kl", get_klipper_status) - # moonraker - self._get_update_status("mr", get_moonraker_status) - # mainsail - self._get_update_status("ms", get_client_status, self.mainsail_data, True) - # mainsail-config - self._get_update_status("mc", get_client_config_status, self.mainsail_data) - # fluidd - self._get_update_status("fl", get_client_status, self.fluidd_data, True) - # fluidd-config - self._get_update_status("fc", get_client_config_status, self.fluidd_data) - # klipperscreen - self._get_update_status("ks", get_klipperscreen_status) - # mobileraker - self._get_update_status("mb", get_mobileraker_status) - # crowsnest - self._get_update_status("cn", get_crowsnest_status) - # octoeverywhere - self._get_update_status("oe", get_octoeverywhere_status) + self._set_status_data("klipper", get_klipper_status) + self._set_status_data("moonraker", get_moonraker_status) + self._set_status_data("mainsail", get_client_status, self.mainsail_data, True) + self._set_status_data( + "mainsail_config", get_client_config_status, self.mainsail_data + ) + self._set_status_data("fluidd", get_client_status, self.fluidd_data, True) + self._set_status_data( + "fluidd_config", get_client_config_status, self.fluidd_data + ) + self._set_status_data("klipperscreen", get_klipperscreen_status) + self._set_status_data("mobileraker", get_mobileraker_status) + self._set_status_data("crowsnest", get_crowsnest_status) + self._set_status_data("octoeverywhere", get_octoeverywhere_status) def _format_local_status(self, local_version, remote_version) -> str: - if local_version == remote_version: - return f"{COLOR_GREEN}{local_version}{RESET_FORMAT}" - return f"{COLOR_YELLOW}{local_version}{RESET_FORMAT}" + color = COLOR_RED + if not local_version: + color = COLOR_RED + elif local_version == remote_version: + color = COLOR_GREEN + elif local_version != remote_version: + color = COLOR_YELLOW - def _get_update_status(self, name: str, status_fn: callable, *args) -> None: - status_data: ComponentStatus = status_fn(*args) - local_ver = status_data.get("local") - remote_ver = status_data.get("remote") - color = COLOR_GREEN if remote_ver != "ERROR" else COLOR_RED - setattr(self, f"{name}_local", self._format_local_status(local_ver, remote_ver)) - setattr(self, f"{name}_remote", f"{color}{remote_ver}{RESET_FORMAT}") + return f"{color}{local_version or '-'}{RESET_FORMAT}" + + def _set_status_data(self, name: str, status_fn: callable, *args) -> None: + comp_status: ComponentStatus = status_fn(*args) + + self.status_data[name]["installed"] = True if comp_status.status == 2 else False + self.status_data[name]["local"] = comp_status.local + self.status_data[name]["remote"] = comp_status.remote + + self._set_status_string(name) + + def _set_status_string(self, name: str) -> None: + local_status = self.status_data[name].get("local", None) + remote_status = self.status_data[name].get("remote", None) + + color = COLOR_GREEN if remote_status else COLOR_RED + local_txt = self._format_local_status(local_status, remote_status) + remote_txt = f"{color}{remote_status or '-'}{RESET_FORMAT}" + + setattr(self, f"{name}_local", local_txt) + setattr(self, f"{name}_remote", remote_txt) + + def _check_is_installed(self, name: str) -> bool: + if not self.status_data[name]["installed"]: + Logger.print_info(f"{name.capitalize()} is not installed! Skipped ...") + return False + return True diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 98d982b..c38464e 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -26,7 +26,7 @@ from utils.sys_utils import ( install_system_packages, update_system_package_lists, ) -from utils.types import ComponentStatus, InstallStatus +from utils.types import ComponentStatus, StatusCode def convert_camelcase_to_kebabcase(name: str) -> str: @@ -92,13 +92,11 @@ def get_install_status( checks.append(f.exists()) if all(checks): - status = InstallStatus.INSTALLED - + status: StatusCode = 2 # installed elif not any(checks): - status = InstallStatus.NOT_INSTALLED - + status: StatusCode = 0 # not installed else: - status = InstallStatus.INCOMPLETE + status: StatusCode = 1 # incomplete return ComponentStatus( status=status, diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py index 98dead9..8952887 100644 --- a/kiauh/utils/git_utils.py +++ b/kiauh/utils/git_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import shutil import urllib.request @@ -63,11 +65,11 @@ def git_pull_wrapper(repo: str, target_dir: Path) -> None: return -def get_repo_name(repo: Path) -> str: +def get_repo_name(repo: Path) -> str | None: """ Helper method to extract the organisation and name of a repository | :param repo: repository to extract the values from - :return: String in form of "/" + :return: String in form of "/" or None """ if not repo.exists() or not repo.joinpath(".git").exists(): return "-" @@ -77,7 +79,7 @@ def get_repo_name(repo: Path) -> str: result = check_output(cmd, stderr=DEVNULL) return "/".join(result.decode().strip().split("/")[-2:]) except CalledProcessError: - return "-" + return None def get_tags(repo_path: str) -> List[str]: @@ -110,7 +112,6 @@ def get_latest_tag(repo_path: str) -> str: else: return "" except Exception: - Logger.print_error("Error while getting the latest tag") raise @@ -130,20 +131,20 @@ def get_latest_unstable_tag(repo_path: str) -> str: raise -def get_local_commit(repo: Path) -> str: +def get_local_commit(repo: Path) -> str | None: if not repo.exists() or not repo.joinpath(".git").exists(): - return "-" + return None try: cmd = f"cd {repo} && git describe HEAD --always --tags | cut -d '-' -f 1,2" return check_output(cmd, shell=True, text=True).strip() except CalledProcessError: - return "-" + return None -def get_remote_commit(repo: Path) -> str: +def get_remote_commit(repo: Path) -> str | None: if not repo.exists() or not repo.joinpath(".git").exists(): - return "-" + return None try: # get locally checked out branch @@ -153,7 +154,7 @@ def get_remote_commit(repo: Path) -> str: cmd = f"cd {repo} && git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2" return check_output(cmd, shell=True, text=True).strip() except CalledProcessError: - return "-" + return None def git_cmd_clone(repo: str, target_dir: Path) -> None: diff --git a/kiauh/utils/types.py b/kiauh/utils/types.py index c49a1c9..274ef9d 100644 --- a/kiauh/utils/types.py +++ b/kiauh/utils/types.py @@ -6,25 +6,24 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from enum import Enum -from typing import Optional, TypedDict +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, Literal + +StatusText = Literal["Installed", "Not installed", "Incomplete"] +StatusCode = Literal[0, 1, 2] +StatusMap: Dict[StatusCode, StatusText] = { + 0: "Not installed", + 1: "Incomplete", + 2: "Installed", +} -class StatusInfo: - def __init__(self, txt: str, code: int): - self.txt: str = txt - self.code: int = code - - -class InstallStatus(Enum): - INSTALLED = StatusInfo("Installed", 1) - NOT_INSTALLED = StatusInfo("Not installed", 2) - INCOMPLETE = StatusInfo("Incomplete", 3) - - -class ComponentStatus(TypedDict): - status: InstallStatus - repo: Optional[str] - local: Optional[str] - remote: Optional[str] - instances: Optional[int] +@dataclass +class ComponentStatus: + status: StatusCode + repo: str | None = None + local: str | None = None + remote: str | None = None + instances: int | None = None From 7c9dcea3592c04d8345cec093b1f08e5cb5da9a9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 16:01:11 +0200 Subject: [PATCH 265/296] feat: add loading spinner Signed-off-by: Dominik Willner --- kiauh/utils/spinner.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 kiauh/utils/spinner.py diff --git a/kiauh/utils/spinner.py b/kiauh/utils/spinner.py new file mode 100644 index 0000000..d4c1ced --- /dev/null +++ b/kiauh/utils/spinner.py @@ -0,0 +1,33 @@ +import sys +import threading +import time + + +class Spinner: + def __init__(self, message="Loading", delay=0.2): + self.message = f"{message} ..." + self.delay = delay + self._stop_event = threading.Event() + self._thread = threading.Thread(target=self._animate) + + def _animate(self): + animation = ["◜", "◝", "◞", "◟"] + while not self._stop_event.is_set(): + for char in animation: + sys.stdout.write(f"\r{char} {self.message}") + sys.stdout.flush() + time.sleep(self.delay) + if self._stop_event.is_set(): + break + sys.stdout.write("\r" + " " * (len(self.message) + 1) + "\r") + sys.stdout.flush() + + def start(self): + self._stop_event.clear() + if not self._thread.is_alive(): + self._thread = threading.Thread(target=self._animate) + self._thread.start() + + def stop(self): + self._stop_event.set() + self._thread.join() From 481394abf915e7d18c0664930f0ac0b1363ed855 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 16:01:25 +0200 Subject: [PATCH 266/296] refactor: use loading spinner in update menu Signed-off-by: Dominik Willner --- kiauh/core/menus/update_menu.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index 5ea217e..a9f0644 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -6,7 +6,6 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # - import textwrap from typing import Optional, Type @@ -48,6 +47,7 @@ from utils.constants import ( RESET_FORMAT, ) from utils.logger import Logger +from utils.spinner import Spinner from utils.types import ComponentStatus @@ -58,16 +58,16 @@ class UpdateMenu(BaseMenu): super().__init__() self.previous_menu = previous_menu - self.klipper_local = self.klipper_remote = None - self.moonraker_local = self.moonraker_remote = None - self.mainsail_local = self.mainsail_remote = None - self.mainsail_config_local = self.mainsail_config_remote = None - self.fluidd_local = self.fluidd_remote = None - self.fluidd_config_local = self.fluidd_config_remote = None - self.klipperscreen_local = self.klipperscreen_remote = None - self.mobileraker_local = self.mobileraker_remote = None - self.crowsnest_local = self.crowsnest_remote = None - self.octoeverywhere_local = self.octoeverywhere_remote = None + self.klipper_local = self.klipper_remote = "" + self.moonraker_local = self.moonraker_remote = "" + self.mainsail_local = self.mainsail_remote = "" + self.mainsail_config_local = self.mainsail_config_remote = "" + self.fluidd_local = self.fluidd_remote = "" + self.fluidd_config_local = self.fluidd_config_remote = "" + self.klipperscreen_local = self.klipperscreen_remote = "" + self.mobileraker_local = self.mobileraker_remote = "" + self.crowsnest_local = self.crowsnest_remote = "" + self.octoeverywhere_local = self.octoeverywhere_remote = "" self.mainsail_data = MainsailData() self.fluidd_data = FluiddData() @@ -84,8 +84,6 @@ class UpdateMenu(BaseMenu): "octoeverywhere": {"installed": False, "local": None, "remote": None}, } - self._fetch_update_status() - def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu @@ -110,8 +108,13 @@ class UpdateMenu(BaseMenu): } def print_menu(self): + spinner = Spinner() + spinner.start() + self._fetch_update_status() + spinner.stop() + header = " [ Update Menu ] " color = COLOR_GREEN count = 62 - len(color) - len(RESET_FORMAT) From 6b7057882b83a3e509b2a2c54f52ff86c9916ffd Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 16:07:32 +0200 Subject: [PATCH 267/296] fix: remove rogue 'pass' statement Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 4000457..c378c31 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -84,7 +84,6 @@ class MainMenu(BaseMenu): ) def fetch_status(self) -> None: - pass self._get_component_status("kl", get_klipper_status) self._get_component_status("mr", get_moonraker_status) self._get_component_status("ms", get_client_status, MainsailData()) From 372712ba3241dab3c0ef211c573868206426ef3d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 18:05:42 +0200 Subject: [PATCH 268/296] refactor: delete klipper logs with their respective instances upon instance removal Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper.py | 46 +++++++++++-------- kiauh/components/klipper/klipper_remove.py | 18 ++------ .../klipper/menus/klipper_remove_menu.py | 11 ----- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index 2847ae5..b82a376 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -7,8 +7,8 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import subprocess from pathlib import Path +from subprocess import DEVNULL, CalledProcessError, run from typing import List from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH @@ -50,43 +50,46 @@ class Klipper(BaseInstance): def create(self) -> None: Logger.print_status("Creating new Klipper Instance ...") + service_template_path = MODULE_PATH.joinpath("assets/klipper.service") service_file_name = self.get_service_file_name(extension=True) service_file_target = SYSTEMD.joinpath(service_file_name) + env_template_file_path = MODULE_PATH.joinpath("assets/klipper.env") env_file_target = self.sysd_dir.joinpath("klipper.env") try: self.create_folders() - self.write_service_file( - service_template_path, service_file_target, env_file_target + self._write_service_file( + service_template_path, + service_file_target, + env_file_target, ) - self.write_env_file(env_template_file_path, env_file_target) + self._write_env_file(env_template_file_path, env_file_target) - except subprocess.CalledProcessError as e: - Logger.print_error( - f"Error creating service file {service_file_target}: {e}" - ) + except CalledProcessError as e: + Logger.print_error(f"Error creating instance: {e}") raise except OSError as e: - Logger.print_error(f"Error creating env file {env_file_target}: {e}") + Logger.print_error(f"Error creating env file: {e}") raise def delete(self) -> None: service_file = self.get_service_file_name(extension=True) service_file_path = self.get_service_file_path() - Logger.print_status(f"Deleting Klipper Instance: {service_file}") + Logger.print_status(f"Removing Klipper Instance: {service_file}") try: command = ["sudo", "rm", "-f", service_file_path] - subprocess.run(command, check=True) - Logger.print_ok(f"Service file deleted: {service_file_path}") - except subprocess.CalledProcessError as e: - Logger.print_error(f"Error deleting service file: {e}") + run(command, check=True) + self._delete_logfiles() + Logger.print_ok("Instance successfully removed!") + except CalledProcessError as e: + Logger.print_error(f"Error removing instance: {e}") raise - def write_service_file( + def _write_service_file( self, service_template_path: Path, service_file_target: Path, @@ -96,15 +99,15 @@ class Klipper(BaseInstance): service_template_path, env_file_target ) command = ["sudo", "tee", service_file_target] - subprocess.run( + run( command, input=service_content.encode(), - stdout=subprocess.DEVNULL, + stdout=DEVNULL, check=True, ) Logger.print_ok(f"Service file created: {service_file_target}") - def write_env_file( + def _write_env_file( self, env_template_file_path: Path, env_file_target: Path ) -> None: env_file_content = self._prep_env_file(env_template_file_path) @@ -150,3 +153,10 @@ class Klipper(BaseInstance): env_file_content = env_file_content.replace("%LOG%", str(self.log)) env_file_content = env_file_content.replace("%UDS%", str(self.uds)) return env_file_content + + def _delete_logfiles(self) -> None: + from utils.fs_utils import run_remove_routines + + for log in list(self.log_dir.glob(f"{self.log}*")): + Logger.print_status(f"Remove '{log}'") + run_remove_routines(log) diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index 0b75ebd..e3ee1d1 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -23,7 +23,6 @@ def run_klipper_removal( remove_service: bool, remove_dir: bool, remove_env: bool, - delete_logs: bool, ) -> None: im = InstanceManager(Klipper) @@ -36,15 +35,9 @@ def run_klipper_removal( Logger.print_info("No Klipper Services installed! Skipped ...") if (remove_dir or remove_env) and im.instances: - Logger.print_warn("There are still other Klipper services installed!") - Logger.print_warn("Therefor the following parts cannot be removed:") - Logger.print_warn( - """ - ● Klipper local repository - ● Klipper Python environment - """, - False, - ) + Logger.print_info("There are still other Klipper services installed:") + Logger.print_info(f"● '{KLIPPER_DIR}' was not removed.", prefix=False) + Logger.print_info(f"● '{KLIPPER_ENV_DIR}' was not removed.", prefix=False) else: if remove_dir: Logger.print_status("Removing Klipper local repository ...") @@ -53,11 +46,6 @@ def run_klipper_removal( Logger.print_status("Removing Klipper Python environment ...") run_remove_routines(KLIPPER_ENV_DIR) - # delete klipper logs of all instances - if delete_logs: - Logger.print_status("Removing all Klipper logs ...") - delete_klipper_logs(im.instances) - def select_instances_to_remove( instances: List[Klipper], diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index 915dbb4..bed4dcd 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -25,7 +25,6 @@ class KlipperRemoveMenu(BaseMenu): self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False - self.remove_klipper_logs = False self.selection_state = False def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: @@ -41,7 +40,6 @@ class KlipperRemoveMenu(BaseMenu): "1": Option(method=self.toggle_remove_klipper_service, menu=False), "2": Option(method=self.toggle_remove_klipper_dir, menu=False), "3": Option(method=self.toggle_remove_klipper_env, menu=False), - "4": Option(method=self.toggle_delete_klipper_logs, menu=False), "c": Option(method=self.run_removal_process, menu=False), } @@ -54,7 +52,6 @@ class KlipperRemoveMenu(BaseMenu): o1 = checked if self.remove_klipper_service else unchecked o2 = checked if self.remove_klipper_dir else unchecked o3 = checked if self.remove_klipper_env else unchecked - o4 = checked if self.remove_klipper_logs else unchecked menu = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ @@ -68,7 +65,6 @@ class KlipperRemoveMenu(BaseMenu): ║ 1) {o1} Remove Service ║ ║ 2) {o2} Remove Local Repository ║ ║ 3) {o3} Remove Python Environment ║ - ║ 4) {o4} Delete all Log-Files ║ ╟───────────────────────────────────────────────────────╢ ║ C) Continue ║ ╟───────────────────────────────────────────────────────╢ @@ -80,7 +76,6 @@ class KlipperRemoveMenu(BaseMenu): self.remove_klipper_service = not self.remove_klipper_service self.remove_klipper_dir = not self.remove_klipper_dir self.remove_klipper_env = not self.remove_klipper_env - self.remove_klipper_logs = not self.remove_klipper_logs self.selection_state = not self.selection_state def toggle_remove_klipper_service(self, **kwargs) -> None: @@ -92,15 +87,11 @@ class KlipperRemoveMenu(BaseMenu): def toggle_remove_klipper_env(self, **kwargs) -> None: self.remove_klipper_env = not self.remove_klipper_env - def toggle_delete_klipper_logs(self, **kwargs) -> None: - self.remove_klipper_logs = not self.remove_klipper_logs - def run_removal_process(self, **kwargs) -> None: if ( not self.remove_klipper_service and not self.remove_klipper_dir and not self.remove_klipper_env - and not self.remove_klipper_logs ): error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" print(error) @@ -110,13 +101,11 @@ class KlipperRemoveMenu(BaseMenu): self.remove_klipper_service, self.remove_klipper_dir, self.remove_klipper_env, - self.remove_klipper_logs, ) self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False - self.remove_klipper_logs = False self._go_back() From 9ec12ba0b83d8fc0b5c18c07760f3848e011f269 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 18:06:23 +0200 Subject: [PATCH 269/296] refactor: use 1-based indexing for klipper instance selection Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_dialogs.py | 3 ++- kiauh/components/klipper/klipper_remove.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py index fc43c7a..4d1137f 100644 --- a/kiauh/components/klipper/klipper_dialogs.py +++ b/kiauh/components/klipper/klipper_dialogs.py @@ -32,6 +32,7 @@ def print_instance_overview( display_type: DisplayType = DisplayType.SERVICE_NAME, show_headline=True, show_index=False, + start_index=0, show_select_all=False, ): dialog = "╔═══════════════════════════════════════════════════════╗\n" @@ -55,7 +56,7 @@ def print_instance_overview( name = s.get_service_file_name() else: name = s.data_dir - line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {name}{RESET_FORMAT}" + line = f"{COLOR_CYAN}{f'{i + start_index})' if show_index else '●'} {name}{RESET_FORMAT}" dialog += f"║ {line:<63}║\n" dialog += "╟───────────────────────────────────────────────────────╢\n" diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index e3ee1d1..c565f50 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -50,11 +50,17 @@ def run_klipper_removal( def select_instances_to_remove( instances: List[Klipper], ) -> Union[List[Klipper], None]: - print_instance_overview(instances, show_index=True, show_select_all=True) - - options = [str(i) for i in range(len(instances))] + start_index = 1 + options = [str(i + start_index) for i in range(len(instances))] options.extend(["a", "A", "b", "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 Klipper instance to remove", options) instances_to_remove = [] @@ -63,8 +69,7 @@ def select_instances_to_remove( elif selection == "a".lower(): instances_to_remove.extend(instances) else: - instance = instances[int(selection)] - instances_to_remove.append(instance) + instances_to_remove.append(instance_map[selection]) return instances_to_remove From 94e95671cae4c87773768d77774f99a21a4863fd Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 18:27:00 +0200 Subject: [PATCH 270/296] refactor: delete moonraker logs with their respective instances upon instance removal Signed-off-by: Dominik Willner --- .../moonraker/menus/moonraker_remove_menu.py | 11 ----- kiauh/components/moonraker/moonraker.py | 48 +++++++++++-------- .../components/moonraker/moonraker_remove.py | 20 ++------ 3 files changed, 34 insertions(+), 45 deletions(-) diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 03a458a..a30a599 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -25,7 +25,6 @@ class MoonrakerRemoveMenu(BaseMenu): self.remove_moonraker_dir = False self.remove_moonraker_env = False self.remove_moonraker_polkit = False - self.remove_moonraker_logs = False self.selection_state = False def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: @@ -42,7 +41,6 @@ class MoonrakerRemoveMenu(BaseMenu): "2": Option(method=self.toggle_remove_moonraker_dir, menu=False), "3": Option(method=self.toggle_remove_moonraker_env, menu=False), "4": Option(method=self.toggle_remove_moonraker_polkit, menu=False), - "5": Option(method=self.toggle_delete_moonraker_logs, menu=False), "c": Option(method=self.run_removal_process, menu=False), } @@ -56,7 +54,6 @@ class MoonrakerRemoveMenu(BaseMenu): 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 - o5 = checked if self.remove_moonraker_logs else unchecked menu = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ @@ -71,7 +68,6 @@ class MoonrakerRemoveMenu(BaseMenu): ║ 2) {o2} Remove Local Repository ║ ║ 3) {o3} Remove Python Environment ║ ║ 4) {o4} Remove Policy Kit Rules ║ - ║ 5) {o5} Delete all Log-Files ║ ╟───────────────────────────────────────────────────────╢ ║ C) Continue ║ ╟───────────────────────────────────────────────────────╢ @@ -84,7 +80,6 @@ class MoonrakerRemoveMenu(BaseMenu): self.remove_moonraker_dir = not self.remove_moonraker_dir self.remove_moonraker_env = not self.remove_moonraker_env self.remove_moonraker_polkit = not self.remove_moonraker_polkit - self.remove_moonraker_logs = not self.remove_moonraker_logs self.selection_state = not self.selection_state def toggle_remove_moonraker_service(self, **kwargs) -> None: @@ -99,16 +94,12 @@ class MoonrakerRemoveMenu(BaseMenu): def toggle_remove_moonraker_polkit(self, **kwargs) -> None: self.remove_moonraker_polkit = not self.remove_moonraker_polkit - def toggle_delete_moonraker_logs(self, **kwargs) -> None: - self.remove_moonraker_logs = not self.remove_moonraker_logs - 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 - and not self.remove_moonraker_logs ): error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" print(error) @@ -119,14 +110,12 @@ class MoonrakerRemoveMenu(BaseMenu): self.remove_moonraker_dir, self.remove_moonraker_env, self.remove_moonraker_polkit, - self.remove_moonraker_logs, ) self.remove_moonraker_service = False self.remove_moonraker_dir = False self.remove_moonraker_env = False self.remove_moonraker_polkit = False - self.remove_moonraker_logs = False self._go_back() diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index e333e71..b02b828 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -8,8 +8,8 @@ # ======================================================================= # from __future__ import annotations -import subprocess from pathlib import Path +from subprocess import DEVNULL, CalledProcessError, run from typing import List from components.moonraker import MODULE_PATH, MOONRAKER_DIR, MOONRAKER_ENV_DIR @@ -49,43 +49,46 @@ class Moonraker(BaseInstance): def create(self, create_example_cfg: bool = False) -> None: Logger.print_status("Creating new Moonraker Instance ...") + service_template_path = MODULE_PATH.joinpath("assets/moonraker.service") - env_template_file_path = MODULE_PATH.joinpath("assets/moonraker.env") service_file_name = self.get_service_file_name(extension=True) service_file_target = SYSTEMD.joinpath(service_file_name) + + env_template_file_path = MODULE_PATH.joinpath("assets/moonraker.env") env_file_target = self.sysd_dir.joinpath("moonraker.env") try: self.create_folders([self.backup_dir, self.certs_dir, self._db_dir]) - self.write_service_file( - service_template_path, service_file_target, env_file_target + self._write_service_file( + service_template_path, + service_file_target, + env_file_target, ) - self.write_env_file(env_template_file_path, env_file_target) + self._write_env_file(env_template_file_path, env_file_target) - except subprocess.CalledProcessError as e: - Logger.print_error( - f"Error creating service file {service_file_target}: {e}" - ) + except CalledProcessError as e: + Logger.print_error(f"Error creating instance: {e}") raise except OSError as e: - Logger.print_error(f"Error writing file: {e}") + Logger.print_error(f"Error creating env file: {e}") raise def delete(self) -> None: service_file = self.get_service_file_name(extension=True) service_file_path = self.get_service_file_path() - Logger.print_status(f"Deleting Moonraker Instance: {service_file}") + Logger.print_status(f"Removing Moonraker Instance: {service_file}") try: command = ["sudo", "rm", "-f", service_file_path] - subprocess.run(command, check=True) - Logger.print_ok(f"Service file deleted: {service_file_path}") - except subprocess.CalledProcessError as e: - Logger.print_error(f"Error deleting service file: {e}") + run(command, check=True) + self._delete_logfiles() + Logger.print_ok("Instance successfully removed!") + except CalledProcessError as e: + Logger.print_error(f"Error removing instance: {e}") raise - def write_service_file( + def _write_service_file( self, service_template_path: Path, service_file_target: Path, @@ -95,15 +98,15 @@ class Moonraker(BaseInstance): service_template_path, env_file_target ) command = ["sudo", "tee", service_file_target] - subprocess.run( + run( command, input=service_content.encode(), - stdout=subprocess.DEVNULL, + stdout=DEVNULL, check=True, ) Logger.print_ok(f"Service file created: {service_file_target}") - def write_env_file( + def _write_env_file( self, env_template_file_path: Path, env_file_target: Path ) -> None: env_file_content = self._prep_env_file(env_template_file_path) @@ -156,3 +159,10 @@ class Moonraker(BaseInstance): port = scp.getint("server", "port", fallback=None) return port + + def _delete_logfiles(self) -> None: + from utils.fs_utils import run_remove_routines + + for log in list(self.log_dir.glob(f"{self.log}*")): + Logger.print_status(f"Remove '{log}'") + run_remove_routines(log) diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index 6c73834..c2b5a58 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -25,7 +25,6 @@ def run_moonraker_removal( remove_dir: bool, remove_env: bool, remove_polkit: bool, - delete_logs: bool, ) -> None: im = InstanceManager(Moonraker) @@ -38,16 +37,12 @@ def run_moonraker_removal( Logger.print_info("No Moonraker Services installed! Skipped ...") if (remove_polkit or remove_dir or remove_env) and im.instances: - Logger.print_warn("There are still other Moonraker services installed!") - Logger.print_warn("Therefor the following parts cannot be removed:") - Logger.print_warn( - """ - ● Moonraker PolicyKit rules - ● Moonraker local repository - ● Moonraker Python environment - """, - False, + 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 ...") @@ -59,11 +54,6 @@ def run_moonraker_removal( Logger.print_status("Removing Moonraker Python environment ...") run_remove_routines(MOONRAKER_ENV_DIR) - # delete moonraker logs of all instances - if delete_logs: - Logger.print_status("Removing all Moonraker logs ...") - delete_moonraker_logs(im.instances) - def select_instances_to_remove( instances: List[Moonraker], From 9655f9ba5c99e05194e13ac535c8a711c6f5b08c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 18:29:53 +0200 Subject: [PATCH 271/296] refactor: use 1-based indexing for moonraker instance selection Signed-off-by: Dominik Willner --- kiauh/components/moonraker/moonraker_remove.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index c2b5a58..e4dc9b1 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -58,11 +58,17 @@ def run_moonraker_removal( def select_instances_to_remove( instances: List[Moonraker], ) -> Union[List[Moonraker], None]: - print_instance_overview(instances, show_index=True, show_select_all=True) - - options = [str(i) for i in range(len(instances))] + start_index = 1 + options = [str(i + start_index) for i in range(len(instances))] options.extend(["a", "A", "b", "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 = [] @@ -71,8 +77,7 @@ def select_instances_to_remove( elif selection == "a".lower(): instances_to_remove.extend(instances) else: - instance = instances[int(selection)] - instances_to_remove.append(instance) + instances_to_remove.append(instance_map[selection]) return instances_to_remove From e530c75307e71c947e318bbeb0a318dad9a9d3c9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 18:32:16 +0200 Subject: [PATCH 272/296] fix: f-string in glob caused exception Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper.py | 2 +- kiauh/components/moonraker/moonraker.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index b82a376..cf06871 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -157,6 +157,6 @@ class Klipper(BaseInstance): def _delete_logfiles(self) -> None: from utils.fs_utils import run_remove_routines - for log in list(self.log_dir.glob(f"{self.log}*")): + for log in list(self.log_dir.glob("klippy.log*")): Logger.print_status(f"Remove '{log}'") run_remove_routines(log) diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index b02b828..5bfddf9 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -163,6 +163,6 @@ class Moonraker(BaseInstance): def _delete_logfiles(self) -> None: from utils.fs_utils import run_remove_routines - for log in list(self.log_dir.glob(f"{self.log}*")): + for log in list(self.log_dir.glob("moonraker.log*")): Logger.print_status(f"Remove '{log}'") run_remove_routines(log) From 01deab7c64b5e30ae330e1bf76b1e888e25feb0e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 20:51:04 +0200 Subject: [PATCH 273/296] fix: disallow installing client config if another client config is installed Signed-off-by: Dominik Willner --- .../client_config/client_config_setup.py | 4 +-- kiauh/components/webui_client/client_setup.py | 4 +-- kiauh/components/webui_client/client_utils.py | 30 ++++++++----------- kiauh/components/webui_client/fluidd_data.py | 3 +- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py index 05c26bb..e1aadc2 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -20,7 +20,7 @@ from components.webui_client.client_dialogs import ( ) from components.webui_client.client_utils import ( backup_client_config_data, - config_for_other_client_exist, + detect_client_cfg_conflict, ) from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings @@ -36,7 +36,7 @@ def install_client_config(client_data: BaseWebClient) -> None: client_config: BaseWebClientConfig = client_data.client_config display_name = client_config.display_name - if config_for_other_client_exist(client_data.client): + if detect_client_cfg_conflict(client_data): Logger.print_info("Another Client-Config is already installed! Skipped ...") return diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index c8f671a..06e2686 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -28,7 +28,7 @@ from components.webui_client.client_dialogs import ( ) from components.webui_client.client_utils import ( backup_mainsail_config_json, - config_for_other_client_exist, + detect_client_cfg_conflict, enable_mainsail_remotemode, restore_mainsail_config_json, symlink_webui_nginx_log, @@ -90,7 +90,7 @@ def install_client(client: BaseWebClient) -> None: if ( kl_instances and not client_config.config_dir.exists() - and not config_for_other_client_exist(client_to_ignore=client.client) + and not detect_client_cfg_conflict(client) ): print_install_client_config_dialog(client) question = f"Download the recommended {client_config.display_name}?" diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 1548876..e964428 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -8,7 +8,7 @@ # ======================================================================= # from __future__ import annotations -import json # noqa: I001 +import json import shutil from pathlib import Path from typing import List, get_args @@ -16,9 +16,9 @@ from typing import List, get_args from components.klipper.klipper import Klipper from components.webui_client.base_data import ( BaseWebClient, - BaseWebClientConfig, WebClientType, ) +from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager from core.settings.kiauh_settings import KiauhSettings @@ -187,30 +187,24 @@ def get_existing_clients() -> List[BaseWebClient]: return installed_clients -def get_existing_client_config() -> List[BaseWebClient]: - clients = list(get_args(WebClientType)) - installed_client_configs: List[BaseWebClient] = [] - for client in clients: - c_config_data: BaseWebClientConfig = client.client_config - if c_config_data.config_dir.exists(): - installed_client_configs.append(client) - - return installed_client_configs - - -def config_for_other_client_exist(client_to_ignore: WebClientType) -> bool: +def detect_client_cfg_conflict(curr_client: BaseWebClient) -> bool: """ Check if any other client configs are present on the system. It is usually not harmful, but chances are they can conflict each other. Multiple client configs are, at least, redundant to have them installed - :param client_to_ignore: The client name to ignore for the check + :param curr_client: The client name to check for the conflict :return: True, if other client configs were found, else False """ - clients = set([c.name for c in get_existing_client_config()]) - clients = clients - {client_to_ignore.value} + mainsail_cfg_status: ComponentStatus = get_client_config_status(MainsailData()) + fluidd_cfg_status: ComponentStatus = get_client_config_status(FluiddData()) - return True if len(clients) > 0 else False + if curr_client.client == WebClientType.MAINSAIL and fluidd_cfg_status.status == 2: + return True + if curr_client.client == WebClientType.FLUIDD and mainsail_cfg_status.status == 2: + return True + + return False def get_download_url(base_url: str, client: BaseWebClient) -> str: diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py index 59e75d6..29738c4 100644 --- a/kiauh/components/webui_client/fluidd_data.py +++ b/kiauh/components/webui_client/fluidd_data.py @@ -18,7 +18,6 @@ from components.webui_client.base_data import ( WebClientConfigType, WebClientType, ) -from components.webui_client.client_utils import get_download_url from core.backup_manager import BACKUP_ROOT_DIR @@ -47,6 +46,8 @@ class FluiddData(BaseWebClient): @property def download_url(self) -> str: + from components.webui_client.client_utils import get_download_url + return get_download_url(self.BASE_DL_URL, self) @property From c1f600f539e9ae0d1e075279ab816c79db098609 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 21:25:55 +0200 Subject: [PATCH 274/296] refactor: replace glob with iterdir Signed-off-by: Dominik Willner --- kiauh/components/klipper/__init__.py | 2 ++ kiauh/components/klipper/klipper.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index 6e8d50c..f1a1a24 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -13,6 +13,8 @@ from core.backup_manager import BACKUP_ROOT_DIR MODULE_PATH = Path(__file__).resolve().parent +# names +KLIPPER_LOG_NAME = "klippy.log" KLIPPER_DIR = Path.home().joinpath("klipper") KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups") diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index cf06871..f6e04db 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -158,5 +158,8 @@ class Klipper(BaseInstance): from utils.fs_utils import run_remove_routines for log in list(self.log_dir.glob("klippy.log*")): + files = self.log_dir.iterdir() + logs = [f for f in files if f.name.startswith(KLIPPER_LOG_NAME)] + for log in logs: Logger.print_status(f"Remove '{log}'") run_remove_routines(log) From 7632c3c98051f55321864bda7acf48f6325461fb Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 21:29:54 +0200 Subject: [PATCH 275/296] refactor: implement constants for klipper use ubuntu 22.04 install script Signed-off-by: Dominik Willner --- kiauh/components/klipper/__init__.py | 15 ++++++++++- kiauh/components/klipper/klipper.py | 33 ++++++++++++++--------- kiauh/components/klipper/klipper_setup.py | 21 +++++++-------- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index f1a1a24..2e282e4 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -15,9 +15,22 @@ MODULE_PATH = Path(__file__).resolve().parent # names KLIPPER_LOG_NAME = "klippy.log" +KLIPPER_CFG_NAME = "printer.cfg" +KLIPPER_SERIAL_NAME = "klippy.serial" +KLIPPER_UDS_NAME = "klippy.sock" +KLIPPER_ENV_FILE_NAME = "klipper.env" +KLIPPER_SERVICE_NAME = "klipper.service" + +# directories KLIPPER_DIR = Path.home().joinpath("klipper") KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups") -KLIPPER_REQUIREMENTS_TXT = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") + +# files +KLIPPER_REQ_FILE = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") +KLIPPER_INSTALL_SCRIPT = KLIPPER_DIR.joinpath("scripts/install-ubuntu-22.04.sh") +KLIPPER_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{KLIPPER_SERVICE_NAME}") +KLIPPER_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{KLIPPER_ENV_FILE_NAME}") + EXIT_KLIPPER_SETUP = "Exiting Klipper setup ..." diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index f6e04db..993bfb4 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -11,7 +11,17 @@ from pathlib import Path from subprocess import DEVNULL, CalledProcessError, run from typing import List -from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH +from components.klipper import ( + KLIPPER_CFG_NAME, + KLIPPER_DIR, + KLIPPER_ENV_DIR, + KLIPPER_ENV_FILE_NAME, + KLIPPER_ENV_FILE_TEMPLATE, + KLIPPER_LOG_NAME, + KLIPPER_SERIAL_NAME, + KLIPPER_SERVICE_TEMPLATE, + KLIPPER_UDS_NAME, +) from core.instance_manager.base_instance import BaseInstance from utils.constants import SYSTEMD from utils.logger import Logger @@ -27,10 +37,10 @@ class Klipper(BaseInstance): super().__init__(instance_type=self, suffix=suffix) self.klipper_dir: Path = KLIPPER_DIR self.env_dir: Path = KLIPPER_ENV_DIR - self._cfg_file = self.cfg_dir.joinpath("printer.cfg") - self._log = self.log_dir.joinpath("klippy.log") - self._serial = self.comms_dir.joinpath("klippy.serial") - self._uds = self.comms_dir.joinpath("klippy.sock") + self._cfg_file = self.cfg_dir.joinpath(KLIPPER_CFG_NAME) + self._log = self.log_dir.joinpath(KLIPPER_LOG_NAME) + self._serial = self.comms_dir.joinpath(KLIPPER_SERIAL_NAME) + self._uds = self.comms_dir.joinpath(KLIPPER_UDS_NAME) @property def cfg_file(self) -> Path: @@ -51,21 +61,18 @@ class Klipper(BaseInstance): def create(self) -> None: Logger.print_status("Creating new Klipper Instance ...") - service_template_path = MODULE_PATH.joinpath("assets/klipper.service") service_file_name = self.get_service_file_name(extension=True) service_file_target = SYSTEMD.joinpath(service_file_name) - - env_template_file_path = MODULE_PATH.joinpath("assets/klipper.env") - env_file_target = self.sysd_dir.joinpath("klipper.env") + env_file_target = self.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME) try: self.create_folders() self._write_service_file( - service_template_path, + KLIPPER_SERVICE_TEMPLATE, service_file_target, env_file_target, ) - self._write_env_file(env_template_file_path, env_file_target) + self._write_env_file(KLIPPER_ENV_FILE_TEMPLATE, env_file_target) except CalledProcessError as e: Logger.print_error(f"Error creating instance: {e}") @@ -147,7 +154,8 @@ class Klipper(BaseInstance): "%KLIPPER_DIR%", str(self.klipper_dir) ) env_file_content = env_file_content.replace( - "%CFG%", f"{self.cfg_dir}/printer.cfg" + "%CFG%", + f"{self.cfg_dir}/{KLIPPER_CFG_NAME}", ) env_file_content = env_file_content.replace("%SERIAL%", str(self.serial)) env_file_content = env_file_content.replace("%LOG%", str(self.log)) @@ -157,7 +165,6 @@ class Klipper(BaseInstance): def _delete_logfiles(self) -> None: from utils.fs_utils import run_remove_routines - for log in list(self.log_dir.glob("klippy.log*")): files = self.log_dir.iterdir() logs = [f for f in files if f.name.startswith(KLIPPER_LOG_NAME)] for log in logs: diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 85d7469..f085d7c 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -13,7 +13,8 @@ from components.klipper import ( EXIT_KLIPPER_SETUP, KLIPPER_DIR, KLIPPER_ENV_DIR, - KLIPPER_REQUIREMENTS_TXT, + KLIPPER_INSTALL_SCRIPT, + KLIPPER_REQ_FILE, ) from components.klipper.klipper import Klipper from components.klipper.klipper_utils import ( @@ -115,21 +116,19 @@ def setup_klipper_prerequesites() -> None: # install klipper dependencies and create python virtualenv try: - install_klipper_packages(KLIPPER_DIR) + install_klipper_packages() create_python_venv(KLIPPER_ENV_DIR) - install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT) + install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE) except Exception: Logger.print_error("Error during installation of Klipper requirements!") raise -def install_klipper_packages(klipper_dir: Path) -> None: - script = klipper_dir.joinpath("scripts/install-debian.sh") +def install_klipper_packages() -> None: + script = KLIPPER_INSTALL_SCRIPT packages = parse_packages_from_file(script) - packages = [pkg.replace("python-dev", "python3-dev") for pkg in packages] - packages.append("python3-venv") - # Add dfu-util for octopi-images - packages.append("dfu-util") + packages.append("python3-venv") # todo: remove once switched to virtualenv + # Add dbus requirement for DietPi distro if Path("/boot/dietpi/.version").exists(): packages.append("dbus") @@ -160,9 +159,9 @@ def update_klipper() -> None: git_pull_wrapper(repo=settings.klipper.repo_url, target_dir=KLIPPER_DIR) # install possible new system packages - install_klipper_packages(KLIPPER_DIR) + install_klipper_packages() # install possible new python dependencies - install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT) + install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE) instance_manager.start_all_instance() From 0cb1e35b06a1e96ef9bdc1ea8255f83b573bd17d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 21:40:48 +0200 Subject: [PATCH 276/296] refactor: improve klipper class structure Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper.py | 96 ++++++++++++++++------------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index 993bfb4..90d2506 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -61,18 +61,10 @@ class Klipper(BaseInstance): def create(self) -> None: Logger.print_status("Creating new Klipper Instance ...") - service_file_name = self.get_service_file_name(extension=True) - service_file_target = SYSTEMD.joinpath(service_file_name) - env_file_target = self.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME) - try: self.create_folders() - self._write_service_file( - KLIPPER_SERVICE_TEMPLATE, - service_file_target, - env_file_target, - ) - self._write_env_file(KLIPPER_ENV_FILE_TEMPLATE, env_file_target) + self._write_service_file() + self._write_env_file() except CalledProcessError as e: Logger.print_error(f"Error creating instance: {e}") @@ -96,15 +88,10 @@ class Klipper(BaseInstance): Logger.print_error(f"Error removing instance: {e}") raise - def _write_service_file( - self, - service_template_path: Path, - service_file_target: Path, - env_file_target: Path, - ) -> None: - service_content = self._prep_service_file( - service_template_path, env_file_target - ) + def _write_service_file(self) -> None: + service_file_name = self.get_service_file_name(extension=True) + service_file_target = SYSTEMD.joinpath(service_file_name) + service_content = self._prep_service_file() command = ["sudo", "tee", service_file_target] run( command, @@ -114,52 +101,73 @@ class Klipper(BaseInstance): ) Logger.print_ok(f"Service file created: {service_file_target}") - def _write_env_file( - self, env_template_file_path: Path, env_file_target: Path - ) -> None: - env_file_content = self._prep_env_file(env_template_file_path) + def _write_env_file(self) -> None: + env_file_content = self._prep_env_file() + env_file_target = self.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME) + with open(env_file_target, "w") as env_file: env_file.write(env_file_content) + Logger.print_ok(f"Env file created: {env_file_target}") - def _prep_service_file( - self, service_template_path: Path, env_file_path: Path - ) -> str: + def _prep_service_file(self) -> str: + template = KLIPPER_SERVICE_TEMPLATE + try: - with open(service_template_path, "r") as template_file: + with open(template, "r") as template_file: template_content = template_file.read() except FileNotFoundError: - Logger.print_error( - f"Unable to open {service_template_path} - File not found" - ) + Logger.print_error(f"Unable to open {template} - File not found") raise - service_content = template_content.replace("%USER%", self.user) - service_content = service_content.replace( - "%KLIPPER_DIR%", str(self.klipper_dir) + + service_content = template_content.replace( + "%USER%", + self.user, + ) + service_content = service_content.replace( + "%KLIPPER_DIR%", + self.klipper_dir.as_posix(), + ) + service_content = service_content.replace( + "%ENV%", + self.env_dir.as_posix(), + ) + service_content = service_content.replace( + "%ENV_FILE%", + self.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME).as_posix(), ) - service_content = service_content.replace("%ENV%", str(self.env_dir)) - service_content = service_content.replace("%ENV_FILE%", str(env_file_path)) return service_content - def _prep_env_file(self, env_template_file_path: Path) -> str: + def _prep_env_file(self) -> str: + template = KLIPPER_ENV_FILE_TEMPLATE + try: - with open(env_template_file_path, "r") as env_file: + with open(template, "r") as env_file: env_template_file_content = env_file.read() except FileNotFoundError: - Logger.print_error( - f"Unable to open {env_template_file_path} - File not found" - ) + Logger.print_error(f"Unable to open {template} - File not found") raise + env_file_content = env_template_file_content.replace( - "%KLIPPER_DIR%", str(self.klipper_dir) + "%KLIPPER_DIR%", self.klipper_dir.as_posix() ) env_file_content = env_file_content.replace( "%CFG%", f"{self.cfg_dir}/{KLIPPER_CFG_NAME}", ) - env_file_content = env_file_content.replace("%SERIAL%", str(self.serial)) - env_file_content = env_file_content.replace("%LOG%", str(self.log)) - env_file_content = env_file_content.replace("%UDS%", str(self.uds)) + env_file_content = env_file_content.replace( + "%SERIAL%", + self.serial.as_posix(), + ) + env_file_content = env_file_content.replace( + "%LOG%", + self.log.as_posix(), + ) + env_file_content = env_file_content.replace( + "%UDS%", + self.uds.as_posix(), + ) + return env_file_content def _delete_logfiles(self) -> None: From 92ed67ddd2a9744059508e29d9f7554edde0769d Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 30 Jun 2024 22:01:12 +0200 Subject: [PATCH 277/296] fix(mobileraker): fix typo and add more constants Signed-off-by: Dominik Willner --- kiauh/components/mobileraker/__init__.py | 13 +++++- kiauh/components/mobileraker/mobileraker.py | 44 ++++++++++----------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/kiauh/components/mobileraker/__init__.py b/kiauh/components/mobileraker/__init__.py index f5fd1b2..c789aac 100644 --- a/kiauh/components/mobileraker/__init__.py +++ b/kiauh/components/mobileraker/__init__.py @@ -9,8 +9,19 @@ from pathlib import Path from core.backup_manager import BACKUP_ROOT_DIR +from utils.constants import SYSTEMD +# names MOBILERAKER_REPO = "https://github.com/Clon1998/mobileraker_companion.git" +MOBILERAKER_UPDATER_SECTION_NAME = "update_manager mobileraker" +MOBILERAKER_LOG_NAME = "mobileraker.log" + +# directories MOBILERAKER_DIR = Path.home().joinpath("mobileraker_companion") -MOBILERAKER_ENV = Path.home().joinpath("mobileraker-env") MOBILERAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mobileraker-backups") + +# files +MOBILERAKER_ENV = Path.home().joinpath("mobileraker-env") +MOBILERAKER_INSTALL_SCRIPT = MOBILERAKER_DIR.joinpath("scripts/install.sh") +MOBILERAKER_REQ_FILE = MOBILERAKER_DIR.joinpath("scripts/mobileraker-requirements.txt") +MOBILERAKER_SERVICE_FILE = SYSTEMD.joinpath("mobileraker.service") diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py index 5fee326..72c36d5 100644 --- a/kiauh/components/mobileraker/mobileraker.py +++ b/kiauh/components/mobileraker/mobileraker.py @@ -16,7 +16,12 @@ from components.mobileraker import ( MOBILERAKER_BACKUP_DIR, MOBILERAKER_DIR, MOBILERAKER_ENV, + MOBILERAKER_INSTALL_SCRIPT, + MOBILERAKER_LOG_NAME, MOBILERAKER_REPO, + MOBILERAKER_REQ_FILE, + MOBILERAKER_SERVICE_FILE, + MOBILERAKER_UPDATER_SECTION_NAME, ) from components.moonraker.moonraker import Moonraker from core.backup_manager.backup_manager import BackupManager @@ -24,7 +29,6 @@ from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings from utils.common import check_install_dependencies, get_install_status from utils.config_utils import add_config_section, remove_config_section -from utils.constants import SYSTEMD from utils.fs_utils import remove_with_sudo from utils.git_utils import ( git_clone_wrapper, @@ -72,8 +76,7 @@ def install_mobileraker() -> None: git_clone_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR) try: - script = f"{MOBILERAKER_DIR}/scripts/install.sh" - run(script, shell=True, check=True) + run(MOBILERAKER_INSTALL_SCRIPT.as_posix(), shell=True, check=True) if mr_instances: patch_mobileraker_update_manager(mr_instances) mr_im.restart_all_instance() @@ -89,19 +92,18 @@ def install_mobileraker() -> None: def patch_mobileraker_update_manager(instances: List[Moonraker]) -> None: - env_py = f"{MOBILERAKER_ENV}/bin/python" add_config_section( - section="update_manager mobileraker", + section=MOBILERAKER_UPDATER_SECTION_NAME, instances=instances, options=[ ("type", "git_repo"), - ("path", "mobileraker_companion"), - ("orgin", MOBILERAKER_REPO), + ("path", MOBILERAKER_DIR.as_posix()), + ("origin", MOBILERAKER_REPO), ("primary_branch", "main"), ("managed_services", "mobileraker"), - ("env", env_py), - ("requirements", "scripts/mobileraker-requirements.txt"), - ("install_script", "scripts/install.sh"), + ("env", f"{MOBILERAKER_ENV}/bin/python"), + ("requirements", MOBILERAKER_REQ_FILE.as_posix()), + ("install_script", MOBILERAKER_INSTALL_SCRIPT.as_posix()), ], ) @@ -124,8 +126,7 @@ def update_mobileraker() -> None: git_pull_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR) - requirements = MOBILERAKER_DIR.joinpath("/scripts/mobileraker-requirements.txt") - install_python_requirements(MOBILERAKER_ENV, requirements) + install_python_requirements(MOBILERAKER_ENV, MOBILERAKER_REQ_FILE) cmd_sysctl_service("mobileraker", "start") @@ -139,7 +140,7 @@ def get_mobileraker_status() -> ComponentStatus: return get_install_status( MOBILERAKER_DIR, MOBILERAKER_ENV, - files=[SYSTEMD.joinpath("mobileraker.service")], + files=[MOBILERAKER_SERVICE_FILE], ) @@ -160,12 +161,11 @@ def remove_mobileraker() -> None: else: Logger.print_warn("Mobileraker's companion environment not found!") - service = SYSTEMD.joinpath("mobileraker.service") - if service.exists(): + if MOBILERAKER_SERVICE_FILE.exists(): Logger.print_status("Removing mobileraker service ...") - cmd_sysctl_service(service, "stop") - cmd_sysctl_service(service, "disable") - remove_with_sudo(service) + cmd_sysctl_service(MOBILERAKER_SERVICE_FILE, "stop") + cmd_sysctl_service(MOBILERAKER_SERVICE_FILE, "disable") + remove_with_sudo(MOBILERAKER_SERVICE_FILE) cmd_sysctl_manage("daemon-reload") cmd_sysctl_manage("reset-failed") Logger.print_ok("Mobileraker's companion service successfully removed!") @@ -173,7 +173,7 @@ def remove_mobileraker() -> None: kl_im = InstanceManager(Klipper) kl_instances: List[Klipper] = kl_im.instances for instance in kl_instances: - logfile = instance.log_dir.joinpath("mobileraker.log") + logfile = instance.log_dir.joinpath(MOBILERAKER_LOG_NAME) if logfile.exists(): Logger.print_status(f"Removing {logfile} ...") Path(logfile).unlink() @@ -185,7 +185,7 @@ def remove_mobileraker() -> None: Logger.print_status( "Removing Mobileraker's companion from update manager ..." ) - remove_config_section("update_manager mobileraker", mr_instances) + remove_config_section(MOBILERAKER_UPDATER_SECTION_NAME, mr_instances) Logger.print_ok( "Mobileraker's companion successfully removed from update manager!" ) @@ -199,12 +199,12 @@ def remove_mobileraker() -> None: def backup_mobileraker_dir() -> None: bm = BackupManager() bm.backup_directory( - "mobileraker_companion", + MOBILERAKER_DIR.name, source=MOBILERAKER_DIR, target=MOBILERAKER_BACKUP_DIR, ) bm.backup_directory( - "mobileraker-env", + MOBILERAKER_ENV.name, source=MOBILERAKER_ENV, target=MOBILERAKER_BACKUP_DIR, ) From 2391f491bb9059d771ca0b1fc5f93616b7c1a26c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 1 Jul 2024 20:42:22 +0200 Subject: [PATCH 278/296] refactor: implement constants for klipper Signed-off-by: Dominik Willner --- kiauh/components/moonraker/__init__.py | 17 ++- kiauh/components/moonraker/moonraker.py | 105 ++++++++++-------- kiauh/components/moonraker/moonraker_setup.py | 10 +- kiauh/components/moonraker/moonraker_utils.py | 4 +- 4 files changed, 75 insertions(+), 61 deletions(-) diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index e357204..99cf6f2 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -13,15 +13,21 @@ from core.backup_manager import BACKUP_ROOT_DIR MODULE_PATH = Path(__file__).resolve().parent +# names +MOONRAKER_CFG_NAME = "moonraker.conf" +MOONRAKER_LOG_NAME = "moonraker.log" +MOONRAKER_SERVICE_NAME = "moonraker.service" +MOONRAKER_DEFAULT_PORT = 7125 +MOONRAKER_ENV_FILE_NAME = "moonraker.env" + +# directories MOONRAKER_DIR = Path.home().joinpath("moonraker") MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env") MOONRAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-backups") MOONRAKER_DB_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-db-backups") -MOONRAKER_REQUIREMENTS_TXT = MOONRAKER_DIR.joinpath( - "scripts/moonraker-requirements.txt" -) -DEFAULT_MOONRAKER_PORT = 7125 +# files +MOONRAKER_REQ_FILE = MOONRAKER_DIR.joinpath("scripts/moonraker-requirements.txt") # introduced due to # https://github.com/Arksine/moonraker/issues/349 # https://github.com/Arksine/moonraker/pull/346 @@ -29,5 +35,8 @@ POLKIT_LEGACY_FILE = Path("/etc/polkit-1/localauthority/50-local.d/10-moonraker. POLKIT_FILE = Path("/etc/polkit-1/rules.d/moonraker.rules") POLKIT_USR_FILE = Path("/usr/share/polkit-1/rules.d/moonraker.rules") POLKIT_SCRIPT = Path.home().joinpath("moonraker/scripts/set-policykit-rules.sh") +MOONRAKER_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{MOONRAKER_SERVICE_NAME}") +MOONRAKER_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{MOONRAKER_ENV_FILE_NAME}") + EXIT_MOONRAKER_SETUP = "Exiting Moonraker setup ..." diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index 5bfddf9..68f8596 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -12,7 +12,15 @@ from pathlib import Path from subprocess import DEVNULL, CalledProcessError, run from typing import List -from components.moonraker import MODULE_PATH, MOONRAKER_DIR, MOONRAKER_ENV_DIR +from components.moonraker import ( + MOONRAKER_CFG_NAME, + MOONRAKER_DIR, + MOONRAKER_ENV_DIR, + MOONRAKER_ENV_FILE_NAME, + MOONRAKER_ENV_FILE_TEMPLATE, + MOONRAKER_LOG_NAME, + MOONRAKER_SERVICE_TEMPLATE, +) from core.instance_manager.base_instance import BaseInstance from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( SimpleConfigParser, @@ -31,13 +39,13 @@ class Moonraker(BaseInstance): super().__init__(instance_type=self, suffix=suffix) self.moonraker_dir: Path = MOONRAKER_DIR self.env_dir: Path = MOONRAKER_ENV_DIR - self.cfg_file = self.cfg_dir.joinpath("moonraker.conf") + self.cfg_file = self.cfg_dir.joinpath(MOONRAKER_CFG_NAME) self.port = self._get_port() self.backup_dir = self.data_dir.joinpath("backup") self.certs_dir = self.data_dir.joinpath("certs") self._db_dir = self.data_dir.joinpath("database") self._comms_dir = self.data_dir.joinpath("comms") - self.log = self.log_dir.joinpath("moonraker.log") + self.log = self.log_dir.joinpath(MOONRAKER_LOG_NAME) @property def db_dir(self) -> Path: @@ -50,21 +58,10 @@ class Moonraker(BaseInstance): def create(self, create_example_cfg: bool = False) -> None: Logger.print_status("Creating new Moonraker Instance ...") - service_template_path = MODULE_PATH.joinpath("assets/moonraker.service") - service_file_name = self.get_service_file_name(extension=True) - service_file_target = SYSTEMD.joinpath(service_file_name) - - env_template_file_path = MODULE_PATH.joinpath("assets/moonraker.env") - env_file_target = self.sysd_dir.joinpath("moonraker.env") - try: self.create_folders([self.backup_dir, self.certs_dir, self._db_dir]) - self._write_service_file( - service_template_path, - service_file_target, - env_file_target, - ) - self._write_env_file(env_template_file_path, env_file_target) + self._write_service_file() + self._write_env_file() except CalledProcessError as e: Logger.print_error(f"Error creating instance: {e}") @@ -88,15 +85,10 @@ class Moonraker(BaseInstance): Logger.print_error(f"Error removing instance: {e}") raise - def _write_service_file( - self, - service_template_path: Path, - service_file_target: Path, - env_file_target: Path, - ) -> None: - service_content = self._prep_service_file( - service_template_path, env_file_target - ) + def _write_service_file(self) -> None: + service_file_name = self.get_service_file_name(extension=True) + service_file_target = SYSTEMD.joinpath(service_file_name) + service_content = self._prep_service_file() command = ["sudo", "tee", service_file_target] run( command, @@ -106,48 +98,61 @@ class Moonraker(BaseInstance): ) Logger.print_ok(f"Service file created: {service_file_target}") - def _write_env_file( - self, env_template_file_path: Path, env_file_target: Path - ) -> None: - env_file_content = self._prep_env_file(env_template_file_path) + def _write_env_file(self) -> None: + env_file_content = self._prep_env_file() + env_file_target = self.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME) + with open(env_file_target, "w") as env_file: env_file.write(env_file_content) Logger.print_ok(f"Env file created: {env_file_target}") - def _prep_service_file( - self, service_template_path: Path, env_file_path: Path - ) -> str: + def _prep_service_file(self) -> str: + template = MOONRAKER_SERVICE_TEMPLATE + try: - with open(service_template_path, "r") as template_file: + with open(template, "r") as template_file: template_content = template_file.read() except FileNotFoundError: - Logger.print_error( - f"Unable to open {service_template_path} - File not found" - ) + Logger.print_error(f"Unable to open {template} - File not found") raise - service_content = template_content.replace("%USER%", self.user) - service_content = service_content.replace( - "%MOONRAKER_DIR%", str(self.moonraker_dir) + + service_content = template_content.replace( + "%USER%", + self.user, + ) + service_content = service_content.replace( + "%MOONRAKER_DIR%", + self.moonraker_dir.as_posix(), + ) + service_content = service_content.replace( + "%ENV%", + self.env_dir.as_posix(), + ) + service_content = service_content.replace( + "%ENV_FILE%", + self.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME).as_posix(), ) - service_content = service_content.replace("%ENV%", str(self.env_dir)) - service_content = service_content.replace("%ENV_FILE%", str(env_file_path)) return service_content - def _prep_env_file(self, env_template_file_path: Path) -> str: + def _prep_env_file(self) -> str: + template = MOONRAKER_ENV_FILE_TEMPLATE + try: - with open(env_template_file_path, "r") as env_file: + with open(template, "r") as env_file: env_template_file_content = env_file.read() except FileNotFoundError: - Logger.print_error( - f"Unable to open {env_template_file_path} - File not found" - ) + Logger.print_error(f"Unable to open {template} - File not found") raise + env_file_content = env_template_file_content.replace( - "%MOONRAKER_DIR%", str(self.moonraker_dir) + "%MOONRAKER_DIR%", + self.moonraker_dir.as_posix(), ) env_file_content = env_file_content.replace( - "%PRINTER_DATA%", str(self.data_dir) + "%PRINTER_DATA%", + self.data_dir.as_posix(), ) + return env_file_content def _get_port(self) -> int | None: @@ -163,6 +168,8 @@ class Moonraker(BaseInstance): def _delete_logfiles(self) -> None: from utils.fs_utils import run_remove_routines - for log in list(self.log_dir.glob("moonraker.log*")): + files = self.log_dir.iterdir() + logs = [f for f in files if f.name.startswith(MOONRAKER_LOG_NAME)] + for log in logs: Logger.print_status(f"Remove '{log}'") run_remove_routines(log) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 0a05a3f..7431059 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -15,7 +15,7 @@ from components.moonraker import ( EXIT_MOONRAKER_SETUP, MOONRAKER_DIR, MOONRAKER_ENV_DIR, - MOONRAKER_REQUIREMENTS_TXT, + MOONRAKER_REQ_FILE, POLKIT_FILE, POLKIT_LEGACY_FILE, POLKIT_SCRIPT, @@ -145,7 +145,7 @@ def setup_moonraker_prerequesites() -> None: # install moonraker dependencies and create python virtualenv install_moonraker_packages(MOONRAKER_DIR) create_python_venv(MOONRAKER_ENV_DIR) - install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT) + install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) def install_moonraker_packages(moonraker_dir: Path) -> None: @@ -206,13 +206,11 @@ def update_moonraker() -> None: instance_manager = InstanceManager(Moonraker) instance_manager.stop_all_instance() - git_pull_wrapper( - repo=settings.moonraker.repo_url, target_dir=MOONRAKER_DIR - ) + git_pull_wrapper(repo=settings.moonraker.repo_url, target_dir=MOONRAKER_DIR) # install possible new system packages install_moonraker_packages(MOONRAKER_DIR) # install possible new python dependencies - install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT) + install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) instance_manager.start_all_instance() diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index a16ef82..8d141ef 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -11,10 +11,10 @@ import shutil from typing import Dict, List, Optional from components.moonraker import ( - DEFAULT_MOONRAKER_PORT, MODULE_PATH, MOONRAKER_BACKUP_DIR, MOONRAKER_DB_BACKUP_DIR, + MOONRAKER_DEFAULT_PORT, MOONRAKER_DIR, MOONRAKER_ENV_DIR, ) @@ -68,7 +68,7 @@ def create_example_moonraker_conf( # of moonraker-1 is 7125 and moonraker-3 is 7127 and there are moonraker.conf files for moonraker-1 # and moonraker-3 already. though, there does not seem to be a very reliable way of always assigning # the correct port to each instance and the user will likely be required to correct the value manually. - port = max(ports) + 1 if ports else DEFAULT_MOONRAKER_PORT + port = max(ports) + 1 if ports else MOONRAKER_DEFAULT_PORT else: port = ports_map.get(instance.suffix) From 1cd9414caea2c25faac963f13d05a2836be13e60 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Mon, 1 Jul 2024 21:04:15 +0200 Subject: [PATCH 279/296] refactor: extract redundant code into shared methods Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper.py | 54 ++++++-------------- kiauh/components/moonraker/moonraker.py | 51 +++++------------- kiauh/core/instance_manager/base_instance.py | 10 ++++ kiauh/utils/sys_utils.py | 36 +++++++++++++ 4 files changed, 76 insertions(+), 75 deletions(-) diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index 90d2506..72e65cb 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -8,7 +8,7 @@ # ======================================================================= # from pathlib import Path -from subprocess import DEVNULL, CalledProcessError, run +from subprocess import CalledProcessError, run from typing import List from components.klipper import ( @@ -23,7 +23,6 @@ from components.klipper import ( KLIPPER_UDS_NAME, ) from core.instance_manager.base_instance import BaseInstance -from utils.constants import SYSTEMD from utils.logger import Logger @@ -59,12 +58,22 @@ class Klipper(BaseInstance): return self._uds def create(self) -> None: + from utils.sys_utils import create_env_file, create_service_file + Logger.print_status("Creating new Klipper Instance ...") try: self.create_folders() - self._write_service_file() - self._write_env_file() + + create_service_file( + name=self.get_service_file_name(extension=True), + content=self._prep_service_file_content(), + ) + + create_env_file( + path=self.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME), + content=self._prep_env_file_content(), + ) except CalledProcessError as e: Logger.print_error(f"Error creating instance: {e}") @@ -82,35 +91,13 @@ class Klipper(BaseInstance): try: command = ["sudo", "rm", "-f", service_file_path] run(command, check=True) - self._delete_logfiles() + self.delete_logfiles(KLIPPER_LOG_NAME) Logger.print_ok("Instance successfully removed!") except CalledProcessError as e: Logger.print_error(f"Error removing instance: {e}") raise - def _write_service_file(self) -> None: - service_file_name = self.get_service_file_name(extension=True) - service_file_target = SYSTEMD.joinpath(service_file_name) - service_content = self._prep_service_file() - command = ["sudo", "tee", service_file_target] - run( - command, - input=service_content.encode(), - stdout=DEVNULL, - check=True, - ) - Logger.print_ok(f"Service file created: {service_file_target}") - - def _write_env_file(self) -> None: - env_file_content = self._prep_env_file() - env_file_target = self.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME) - - with open(env_file_target, "w") as env_file: - env_file.write(env_file_content) - - Logger.print_ok(f"Env file created: {env_file_target}") - - def _prep_service_file(self) -> str: + def _prep_service_file_content(self) -> str: template = KLIPPER_SERVICE_TEMPLATE try: @@ -138,7 +125,7 @@ class Klipper(BaseInstance): ) return service_content - def _prep_env_file(self) -> str: + def _prep_env_file_content(self) -> str: template = KLIPPER_ENV_FILE_TEMPLATE try: @@ -169,12 +156,3 @@ class Klipper(BaseInstance): ) return env_file_content - - def _delete_logfiles(self) -> None: - from utils.fs_utils import run_remove_routines - - files = self.log_dir.iterdir() - logs = [f for f in files if f.name.startswith(KLIPPER_LOG_NAME)] - for log in logs: - Logger.print_status(f"Remove '{log}'") - run_remove_routines(log) diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index 68f8596..3dd5131 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -9,7 +9,7 @@ from __future__ import annotations from pathlib import Path -from subprocess import DEVNULL, CalledProcessError, run +from subprocess import CalledProcessError, run from typing import List from components.moonraker import ( @@ -25,7 +25,6 @@ from core.instance_manager.base_instance import BaseInstance from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( SimpleConfigParser, ) -from utils.constants import SYSTEMD from utils.logger import Logger @@ -56,12 +55,20 @@ class Moonraker(BaseInstance): return self._comms_dir def create(self, create_example_cfg: bool = False) -> None: + from utils.sys_utils import create_env_file, create_service_file + Logger.print_status("Creating new Moonraker Instance ...") try: self.create_folders([self.backup_dir, self.certs_dir, self._db_dir]) - self._write_service_file() - self._write_env_file() + create_service_file( + name=self.get_service_file_name(extension=True), + content=self._prep_service_file_content(), + ) + create_env_file( + path=self.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME), + content=self._prep_env_file_content(), + ) except CalledProcessError as e: Logger.print_error(f"Error creating instance: {e}") @@ -79,34 +86,13 @@ class Moonraker(BaseInstance): try: command = ["sudo", "rm", "-f", service_file_path] run(command, check=True) - self._delete_logfiles() + self.delete_logfiles(MOONRAKER_LOG_NAME) Logger.print_ok("Instance successfully removed!") except CalledProcessError as e: Logger.print_error(f"Error removing instance: {e}") raise - def _write_service_file(self) -> None: - service_file_name = self.get_service_file_name(extension=True) - service_file_target = SYSTEMD.joinpath(service_file_name) - service_content = self._prep_service_file() - command = ["sudo", "tee", service_file_target] - run( - command, - input=service_content.encode(), - stdout=DEVNULL, - check=True, - ) - Logger.print_ok(f"Service file created: {service_file_target}") - - def _write_env_file(self) -> None: - env_file_content = self._prep_env_file() - env_file_target = self.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME) - - with open(env_file_target, "w") as env_file: - env_file.write(env_file_content) - Logger.print_ok(f"Env file created: {env_file_target}") - - def _prep_service_file(self) -> str: + def _prep_service_file_content(self) -> str: template = MOONRAKER_SERVICE_TEMPLATE try: @@ -134,7 +120,7 @@ class Moonraker(BaseInstance): ) return service_content - def _prep_env_file(self) -> str: + def _prep_env_file_content(self) -> str: template = MOONRAKER_ENV_FILE_TEMPLATE try: @@ -164,12 +150,3 @@ class Moonraker(BaseInstance): port = scp.getint("server", "port", fallback=None) return port - - def _delete_logfiles(self) -> None: - from utils.fs_utils import run_remove_routines - - files = self.log_dir.iterdir() - logs = [f for f in files if f.name.startswith(MOONRAKER_LOG_NAME)] - for log in logs: - Logger.print_status(f"Remove '{log}'") - run_remove_routines(log) diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index f99db2a..da0a4b0 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -14,6 +14,7 @@ from pathlib import Path from typing import List, Optional from utils.constants import CURRENT_USER, SYSTEMD +from utils.logger import Logger class BaseInstance(ABC): @@ -159,3 +160,12 @@ class BaseInstance(ABC): return f"printer_{self._suffix}" else: return self._suffix + + def delete_logfiles(self, log_name: str) -> None: + from utils.fs_utils import run_remove_routines + + files = self.log_dir.iterdir() + logs = [f for f in files if f.name.startswith(log_name)] + for log in logs: + Logger.print_status(f"Remove '{log}'") + run_remove_routines(log) diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index d3d3b0d..707f364 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -402,3 +402,39 @@ def log_process(process: Popen) -> None: if process.poll() is not None: break + + +def create_service_file(name: str, content: str) -> None: + """ + Creates a service file at the provided path with the provided content. + :param name: the name of the service file + :param content: the content of the service file + :return: None + """ + try: + run( + ["sudo", "tee", SYSTEMD.joinpath(name)], + input=content.encode(), + stdout=DEVNULL, + check=True, + ) + Logger.print_ok(f"Service file created: {SYSTEMD.joinpath(name)}") + except CalledProcessError as e: + Logger.print_error(f"Error creating service file: {e}") + raise + + +def create_env_file(path: Path, content: str) -> None: + """ + Creates an env file at the provided path with the provided content. + :param path: the path of the env file + :param content: the content of the env file + :return: None + """ + try: + with open(path, "w") as env_file: + env_file.write(content) + Logger.print_ok(f"Env file created: {path}") + except OSError as e: + Logger.print_error(f"Error creating env file: {e}") + raise From 64ea337e7e7aca0ba3ede9c67a8807a6f07b5fdf Mon Sep 17 00:00:00 2001 From: dw-0 Date: Tue, 2 Jul 2024 22:07:52 +0200 Subject: [PATCH 280/296] refactor: create service removal helper function Signed-off-by: Dominik Willner --- kiauh/utils/sys_utils.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 707f364..477a5d2 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -22,7 +22,7 @@ from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, run from typing import List, Literal from utils.constants import SYSTEMD -from utils.fs_utils import check_file_exist +from utils.fs_utils import check_file_exist, remove_with_sudo from utils.input_utils import get_confirm from utils.logger import Logger @@ -438,3 +438,23 @@ def create_env_file(path: Path, content: str) -> None: except OSError as e: Logger.print_error(f"Error creating env file: {e}") raise + + +def remove_service_file(service_name: str, service_file: Path) -> None: + """ + Removes a systemd service file at the provided path with the provided name. + :param service_name: the name of the service + :param service_file: the path of the service file + :return: None + """ + try: + Logger.print_status(f"Removing {service_name} ...") + cmd_sysctl_service(service_name, "stop") + cmd_sysctl_service(service_name, "disable") + remove_with_sudo(service_file) + cmd_sysctl_manage("daemon-reload") + cmd_sysctl_manage("reset-failed") + Logger.print_ok(f"{service_name} successfully removed!") + except Exception as e: + Logger.print_error(f"Error removing {service_name}:\n{e}") + raise From 7e251eb31e15c6911681adf8cdd9e310940640ec Mon Sep 17 00:00:00 2001 From: dw-0 Date: Tue, 2 Jul 2024 22:08:09 +0200 Subject: [PATCH 281/296] refactor: more extraction into constant Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/__init__.py | 16 +++- kiauh/components/crowsnest/crowsnest.py | 37 +++++---- kiauh/components/klipperscreen/__init__.py | 20 ++++- .../components/klipperscreen/klipperscreen.py | 78 +++++++++---------- kiauh/components/mobileraker/__init__.py | 9 ++- kiauh/components/mobileraker/mobileraker.py | 35 ++++----- kiauh/components/moonraker/__init__.py | 4 +- kiauh/components/moonraker/moonraker_setup.py | 19 +++-- kiauh/components/octoeverywhere/__init__.py | 1 + .../octoeverywhere/octoeverywhere.py | 6 +- .../octoeverywhere/octoeverywhere_setup.py | 25 +----- 11 files changed, 133 insertions(+), 117 deletions(-) diff --git a/kiauh/components/crowsnest/__init__.py b/kiauh/components/crowsnest/__init__.py index bc1ae8a..9c1247c 100644 --- a/kiauh/components/crowsnest/__init__.py +++ b/kiauh/components/crowsnest/__init__.py @@ -10,7 +10,21 @@ from pathlib import Path from core.backup_manager import BACKUP_ROOT_DIR +from utils.constants import SYSTEMD -CROWSNEST_DIR = Path.home().joinpath("crowsnest") +# repo CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git" + +# names +CROWSNEST_SERVICE_NAME = "crowsnest.service" + +# directories +CROWSNEST_DIR = Path.home().joinpath("crowsnest") CROWSNEST_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("crowsnest-backups") + +# files +CROWSNEST_MULTI_CONFIG = CROWSNEST_DIR.joinpath("tools/.config") +CROWSNEST_INSTALL_SCRIPT = CROWSNEST_DIR.joinpath("tools/install.sh") +CROWSNEST_BIN_FILE = Path("/usr/local/bin/crowsnest") +CROWSNEST_LOGROTATE_FILE = Path("/etc/logrotate.d/crowsnest") +CROWSNEST_SERVICE_FILE = SYSTEMD.joinpath(CROWSNEST_SERVICE_NAME) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index ed774c0..0eb909a 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -14,7 +14,17 @@ from pathlib import Path from subprocess import CalledProcessError, run from typing import List -from components.crowsnest import CROWSNEST_BACKUP_DIR, CROWSNEST_DIR, CROWSNEST_REPO +from components.crowsnest import ( + CROWSNEST_BACKUP_DIR, + CROWSNEST_BIN_FILE, + CROWSNEST_DIR, + CROWSNEST_INSTALL_SCRIPT, + CROWSNEST_LOGROTATE_FILE, + CROWSNEST_MULTI_CONFIG, + CROWSNEST_REPO, + CROWSNEST_SERVICE_FILE, + CROWSNEST_SERVICE_NAME, +) from components.klipper.klipper import Klipper from core.backup_manager.backup_manager import BackupManager from core.instance_manager.instance_manager import InstanceManager @@ -75,7 +85,6 @@ def install_crowsnest() -> None: def print_multi_instance_warning(instances: List[Klipper]) -> None: - _instances = [f"● {instance.data_dir_name}" for instance in instances] Logger.print_dialog( DialogType.WARNING, [ @@ -86,13 +95,12 @@ def print_multi_instance_warning(instances: List[Klipper]) -> None: "this instance to set up your 'crowsnest.conf' and steering it's service.", "\n\n", "The following instances were found:", - *_instances, + *[f"● {instance.data_dir_name}" for instance in instances], ], ) def configure_multi_instance() -> None: - config = Path(CROWSNEST_DIR).joinpath("tools/.config") try: run( "make config", @@ -102,17 +110,17 @@ def configure_multi_instance() -> None: ) except CalledProcessError as e: Logger.print_error(f"Something went wrong! Please try again...\n{e}") - if config.exists(): - Path.unlink(config) + if CROWSNEST_MULTI_CONFIG.exists(): + Path.unlink(CROWSNEST_MULTI_CONFIG) return - if not config.exists(): + if not CROWSNEST_MULTI_CONFIG.exists(): Logger.print_error("Generating .config failed, installation aborted") def update_crowsnest() -> None: try: - cmd_sysctl_service("crowsnest", "stop") + cmd_sysctl_service(CROWSNEST_SERVICE_NAME, "stop") if not CROWSNEST_DIR.exists(): git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master") @@ -123,18 +131,17 @@ def update_crowsnest() -> None: if settings.kiauh.backup_before_update: bm = BackupManager() bm.backup_directory( - "crowsnest", + CROWSNEST_DIR.name, source=CROWSNEST_DIR, target=CROWSNEST_BACKUP_DIR, ) git_pull_wrapper(CROWSNEST_REPO, CROWSNEST_DIR) - script = CROWSNEST_DIR.joinpath("tools/install.sh") - deps = parse_packages_from_file(script) + deps = parse_packages_from_file(CROWSNEST_INSTALL_SCRIPT) check_install_dependencies(deps) - cmd_sysctl_service("crowsnest", "restart") + cmd_sysctl_service(CROWSNEST_SERVICE_NAME, "restart") Logger.print_ok("Crowsnest updated successfully.", end="\n\n") except CalledProcessError as e: @@ -144,9 +151,9 @@ def update_crowsnest() -> None: def get_crowsnest_status() -> ComponentStatus: files = [ - Path("/usr/local/bin/crowsnest"), - Path("/etc/logrotate.d/crowsnest"), - Path("/etc/systemd/system/crowsnest.service"), + CROWSNEST_BIN_FILE, + CROWSNEST_LOGROTATE_FILE, + CROWSNEST_SERVICE_FILE, ] return get_install_status(CROWSNEST_DIR, files=files) diff --git a/kiauh/components/klipperscreen/__init__.py b/kiauh/components/klipperscreen/__init__.py index c79b463..901bc5f 100644 --- a/kiauh/components/klipperscreen/__init__.py +++ b/kiauh/components/klipperscreen/__init__.py @@ -9,8 +9,26 @@ from pathlib import Path from core.backup_manager import BACKUP_ROOT_DIR +from utils.constants import SYSTEMD +# repo KLIPPERSCREEN_REPO = "https://github.com/KlipperScreen/KlipperScreen.git" + +# names +KLIPPERSCREEN_SERVICE_NAME = "KlipperScreen.service" +KLIPPERSCREEN_UPDATER_SECTION_NAME = "update_manager KlipperScreen" +KLIPPERSCREEN_LOG_NAME = "KlipperScreen.log" + +# directories KLIPPERSCREEN_DIR = Path.home().joinpath("KlipperScreen") -KLIPPERSCREEN_ENV = Path.home().joinpath(".KlipperScreen-env") +KLIPPERSCREEN_ENV_DIR = Path.home().joinpath(".KlipperScreen-env") KLIPPERSCREEN_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipperscreen-backups") + +# files +KLIPPERSCREEN_REQ_FILE = KLIPPERSCREEN_DIR.joinpath( + "scripts/KlipperScreen-requirements.txt" +) +KLIPPERSCREEN_INSTALL_SCRIPT = KLIPPERSCREEN_DIR.joinpath( + "scripts/KlipperScreen-install.sh" +) +KLIPPERSCREEN_SERVICE_FILE = SYSTEMD.joinpath(KLIPPERSCREEN_SERVICE_NAME) diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index 23a456d..cc88796 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -15,8 +15,14 @@ from components.klipper.klipper import Klipper from components.klipperscreen import ( KLIPPERSCREEN_BACKUP_DIR, KLIPPERSCREEN_DIR, - KLIPPERSCREEN_ENV, + KLIPPERSCREEN_ENV_DIR, + KLIPPERSCREEN_INSTALL_SCRIPT, + KLIPPERSCREEN_LOG_NAME, KLIPPERSCREEN_REPO, + KLIPPERSCREEN_REQ_FILE, + KLIPPERSCREEN_SERVICE_FILE, + KLIPPERSCREEN_SERVICE_NAME, + KLIPPERSCREEN_UPDATER_SECTION_NAME, ) from components.moonraker.moonraker import Moonraker from core.backup_manager.backup_manager import BackupManager @@ -37,9 +43,9 @@ from utils.input_utils import get_confirm from utils.logger import DialogType, Logger from utils.sys_utils import ( check_python_version, - cmd_sysctl_manage, cmd_sysctl_service, install_python_requirements, + remove_service_file, ) from utils.types import ComponentStatus @@ -78,8 +84,7 @@ def install_klipperscreen() -> None: git_clone_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR) try: - script = f"{KLIPPERSCREEN_DIR}/scripts/KlipperScreen-install.sh" - run(script, shell=True, check=True) + run(KLIPPERSCREEN_INSTALL_SCRIPT.as_posix(), shell=True, check=True) if mr_instances: patch_klipperscreen_update_manager(mr_instances) mr_im.restart_all_instance() @@ -95,34 +100,30 @@ def install_klipperscreen() -> None: def patch_klipperscreen_update_manager(instances: List[Moonraker]) -> None: - env_py = f"{KLIPPERSCREEN_ENV}/bin/python" add_config_section( - section="update_manager KlipperScreen", + section=KLIPPERSCREEN_UPDATER_SECTION_NAME, instances=instances, options=[ ("type", "git_repo"), - ("path", str(KLIPPERSCREEN_DIR)), + ("path", KLIPPERSCREEN_DIR.as_posix()), ("orgin", KLIPPERSCREEN_REPO), - ("env", env_py), - ("requirements", "scripts/KlipperScreen-requirements.txt"), - ("install_script", "scripts/KlipperScreen-install.sh"), + ("manages_servcies", "KlipperScreen"), + ("env", f"{KLIPPERSCREEN_ENV_DIR}/bin/python"), + ("requirements", KLIPPERSCREEN_REQ_FILE.as_posix()), + ("install_script", KLIPPERSCREEN_INSTALL_SCRIPT.as_posix()), ], ) def update_klipperscreen() -> None: + if not KLIPPERSCREEN_DIR.exists(): + Logger.print_info("KlipperScreen does not seem to be installed! Skipping ...") + return + try: - cmd_sysctl_service("KlipperScreen", "stop") - - if not KLIPPERSCREEN_DIR.exists(): - Logger.print_info( - "KlipperScreen does not seem to be installed! Skipping ..." - ) - return - Logger.print_status("Updating KlipperScreen ...") - cmd_sysctl_service("KlipperScreen", "stop") + cmd_sysctl_service(KLIPPERSCREEN_SERVICE_NAME, "stop") settings = KiauhSettings() if settings.kiauh.backup_before_update: @@ -130,12 +131,9 @@ def update_klipperscreen() -> None: git_pull_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR) - requirements = KLIPPERSCREEN_DIR.joinpath( - "/scripts/KlipperScreen-requirements.txt" - ) - install_python_requirements(KLIPPERSCREEN_ENV, requirements) + install_python_requirements(KLIPPERSCREEN_ENV_DIR, KLIPPERSCREEN_REQ_FILE) - cmd_sysctl_service("KlipperScreen", "start") + cmd_sysctl_service(KLIPPERSCREEN_SERVICE_NAME, "start") Logger.print_ok("KlipperScreen updated successfully.", end="\n\n") except CalledProcessError as e: @@ -146,8 +144,8 @@ def update_klipperscreen() -> None: def get_klipperscreen_status() -> ComponentStatus: return get_install_status( KLIPPERSCREEN_DIR, - KLIPPERSCREEN_ENV, - files=[SYSTEMD.joinpath("KlipperScreen.service")], + KLIPPERSCREEN_ENV_DIR, + files=[SYSTEMD.joinpath(KLIPPERSCREEN_SERVICE_NAME)], ) @@ -161,24 +159,20 @@ def remove_klipperscreen() -> None: else: Logger.print_warn("KlipperScreen directory not found!") - if KLIPPERSCREEN_ENV.exists(): + if KLIPPERSCREEN_ENV_DIR.exists(): Logger.print_status("Removing KlipperScreen environment ...") - shutil.rmtree(KLIPPERSCREEN_ENV) + shutil.rmtree(KLIPPERSCREEN_ENV_DIR) Logger.print_ok("KlipperScreen environment successfully removed!") else: Logger.print_warn("KlipperScreen environment not found!") - service = SYSTEMD.joinpath("KlipperScreen.service") - if service.exists(): - Logger.print_status("Removing KlipperScreen service ...") - cmd_sysctl_service(service, "stop") - cmd_sysctl_service(service, "disable") - remove_with_sudo(service) - cmd_sysctl_manage("daemon-reload") - cmd_sysctl_manage("reset-failed") - Logger.print_ok("KlipperScreen service successfully removed!") + if KLIPPERSCREEN_SERVICE_FILE.exists(): + remove_service_file( + KLIPPERSCREEN_SERVICE_NAME, + KLIPPERSCREEN_SERVICE_FILE, + ) - logfile = Path("/tmp/KlipperScreen.log") + logfile = Path(f"/tmp/{KLIPPERSCREEN_LOG_NAME}") if logfile.exists(): Logger.print_status("Removing KlipperScreen log file ...") remove_with_sudo(logfile) @@ -187,7 +181,7 @@ def remove_klipperscreen() -> None: kl_im = InstanceManager(Klipper) kl_instances: List[Klipper] = kl_im.instances for instance in kl_instances: - logfile = instance.log_dir.joinpath("KlipperScreen.log") + logfile = instance.log_dir.joinpath(KLIPPERSCREEN_LOG_NAME) if logfile.exists(): Logger.print_status(f"Removing {logfile} ...") Path(logfile).unlink() @@ -209,12 +203,12 @@ def remove_klipperscreen() -> None: def backup_klipperscreen_dir() -> None: bm = BackupManager() bm.backup_directory( - "KlipperScreen", + KLIPPERSCREEN_DIR.name, source=KLIPPERSCREEN_DIR, target=KLIPPERSCREEN_BACKUP_DIR, ) bm.backup_directory( - "KlipperScreen-env", - source=KLIPPERSCREEN_ENV, + KLIPPERSCREEN_ENV_DIR.name, + source=KLIPPERSCREEN_ENV_DIR, target=KLIPPERSCREEN_BACKUP_DIR, ) diff --git a/kiauh/components/mobileraker/__init__.py b/kiauh/components/mobileraker/__init__.py index c789aac..b41d2fb 100644 --- a/kiauh/components/mobileraker/__init__.py +++ b/kiauh/components/mobileraker/__init__.py @@ -11,17 +11,20 @@ from pathlib import Path from core.backup_manager import BACKUP_ROOT_DIR from utils.constants import SYSTEMD -# names +# repo MOBILERAKER_REPO = "https://github.com/Clon1998/mobileraker_companion.git" + +# names +MOBILERAKER_SERVICE_NAME = "mobileraker.service" MOBILERAKER_UPDATER_SECTION_NAME = "update_manager mobileraker" MOBILERAKER_LOG_NAME = "mobileraker.log" # directories MOBILERAKER_DIR = Path.home().joinpath("mobileraker_companion") +MOBILERAKER_ENV_DIR = Path.home().joinpath("mobileraker-env") MOBILERAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mobileraker-backups") # files -MOBILERAKER_ENV = Path.home().joinpath("mobileraker-env") MOBILERAKER_INSTALL_SCRIPT = MOBILERAKER_DIR.joinpath("scripts/install.sh") MOBILERAKER_REQ_FILE = MOBILERAKER_DIR.joinpath("scripts/mobileraker-requirements.txt") -MOBILERAKER_SERVICE_FILE = SYSTEMD.joinpath("mobileraker.service") +MOBILERAKER_SERVICE_FILE = SYSTEMD.joinpath(MOBILERAKER_SERVICE_NAME) diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py index 72c36d5..8da3114 100644 --- a/kiauh/components/mobileraker/mobileraker.py +++ b/kiauh/components/mobileraker/mobileraker.py @@ -15,12 +15,13 @@ from components.klipper.klipper import Klipper from components.mobileraker import ( MOBILERAKER_BACKUP_DIR, MOBILERAKER_DIR, - MOBILERAKER_ENV, + MOBILERAKER_ENV_DIR, MOBILERAKER_INSTALL_SCRIPT, MOBILERAKER_LOG_NAME, MOBILERAKER_REPO, MOBILERAKER_REQ_FILE, MOBILERAKER_SERVICE_FILE, + MOBILERAKER_SERVICE_NAME, MOBILERAKER_UPDATER_SECTION_NAME, ) from components.moonraker.moonraker import Moonraker @@ -29,7 +30,6 @@ from core.instance_manager.instance_manager import InstanceManager from core.settings.kiauh_settings import KiauhSettings from utils.common import check_install_dependencies, get_install_status from utils.config_utils import add_config_section, remove_config_section -from utils.fs_utils import remove_with_sudo from utils.git_utils import ( git_clone_wrapper, git_pull_wrapper, @@ -38,9 +38,9 @@ from utils.input_utils import get_confirm from utils.logger import DialogType, Logger from utils.sys_utils import ( check_python_version, - cmd_sysctl_manage, cmd_sysctl_service, install_python_requirements, + remove_service_file, ) from utils.types import ComponentStatus @@ -101,7 +101,7 @@ def patch_mobileraker_update_manager(instances: List[Moonraker]) -> None: ("origin", MOBILERAKER_REPO), ("primary_branch", "main"), ("managed_services", "mobileraker"), - ("env", f"{MOBILERAKER_ENV}/bin/python"), + ("env", f"{MOBILERAKER_ENV_DIR}/bin/python"), ("requirements", MOBILERAKER_REQ_FILE.as_posix()), ("install_script", MOBILERAKER_INSTALL_SCRIPT.as_posix()), ], @@ -118,7 +118,7 @@ def update_mobileraker() -> None: Logger.print_status("Updating Mobileraker's companion ...") - cmd_sysctl_service("mobileraker", "stop") + cmd_sysctl_service(MOBILERAKER_SERVICE_NAME, "stop") settings = KiauhSettings() if settings.kiauh.backup_before_update: @@ -126,9 +126,9 @@ def update_mobileraker() -> None: git_pull_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR) - install_python_requirements(MOBILERAKER_ENV, MOBILERAKER_REQ_FILE) + install_python_requirements(MOBILERAKER_ENV_DIR, MOBILERAKER_REQ_FILE) - cmd_sysctl_service("mobileraker", "start") + cmd_sysctl_service(MOBILERAKER_SERVICE_NAME, "start") Logger.print_ok("Mobileraker's companion updated successfully.", end="\n\n") except CalledProcessError as e: @@ -139,7 +139,7 @@ def update_mobileraker() -> None: def get_mobileraker_status() -> ComponentStatus: return get_install_status( MOBILERAKER_DIR, - MOBILERAKER_ENV, + MOBILERAKER_ENV_DIR, files=[MOBILERAKER_SERVICE_FILE], ) @@ -154,21 +154,18 @@ def remove_mobileraker() -> None: else: Logger.print_warn("Mobileraker's companion directory not found!") - if MOBILERAKER_ENV.exists(): + if MOBILERAKER_ENV_DIR.exists(): Logger.print_status("Removing Mobileraker's companion environment ...") - shutil.rmtree(MOBILERAKER_ENV) + shutil.rmtree(MOBILERAKER_ENV_DIR) Logger.print_ok("Mobileraker's companion environment successfully removed!") else: Logger.print_warn("Mobileraker's companion environment not found!") if MOBILERAKER_SERVICE_FILE.exists(): - Logger.print_status("Removing mobileraker service ...") - cmd_sysctl_service(MOBILERAKER_SERVICE_FILE, "stop") - cmd_sysctl_service(MOBILERAKER_SERVICE_FILE, "disable") - remove_with_sudo(MOBILERAKER_SERVICE_FILE) - cmd_sysctl_manage("daemon-reload") - cmd_sysctl_manage("reset-failed") - Logger.print_ok("Mobileraker's companion service successfully removed!") + remove_service_file( + MOBILERAKER_SERVICE_NAME, + MOBILERAKER_SERVICE_FILE, + ) kl_im = InstanceManager(Klipper) kl_instances: List[Klipper] = kl_im.instances @@ -204,7 +201,7 @@ def backup_mobileraker_dir() -> None: target=MOBILERAKER_BACKUP_DIR, ) bm.backup_directory( - MOBILERAKER_ENV.name, - source=MOBILERAKER_ENV, + MOBILERAKER_ENV_DIR.name, + source=MOBILERAKER_ENV_DIR, target=MOBILERAKER_BACKUP_DIR, ) diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index 99cf6f2..54d61eb 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -27,14 +27,16 @@ MOONRAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-backups") MOONRAKER_DB_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-db-backups") # files +MOONRAKER_INSTALL_SCRIPT = MOONRAKER_DIR.joinpath("scripts/install-moonraker.sh") MOONRAKER_REQ_FILE = MOONRAKER_DIR.joinpath("scripts/moonraker-requirements.txt") +MOONRAKER_DEPS_JSON_FILE = MOONRAKER_DIR.joinpath("scripts/system-dependencies.json") # introduced due to # https://github.com/Arksine/moonraker/issues/349 # https://github.com/Arksine/moonraker/pull/346 POLKIT_LEGACY_FILE = Path("/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla") POLKIT_FILE = Path("/etc/polkit-1/rules.d/moonraker.rules") POLKIT_USR_FILE = Path("/usr/share/polkit-1/rules.d/moonraker.rules") -POLKIT_SCRIPT = Path.home().joinpath("moonraker/scripts/set-policykit-rules.sh") +POLKIT_SCRIPT = MOONRAKER_DIR.joinpath("scripts/set-policykit-rules.sh") MOONRAKER_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{MOONRAKER_SERVICE_NAME}") MOONRAKER_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{MOONRAKER_ENV_FILE_NAME}") diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 7431059..146a8a4 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -8,13 +8,14 @@ # ======================================================================= # import json import subprocess -from pathlib import Path 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_REQ_FILE, POLKIT_FILE, POLKIT_LEGACY_FILE, @@ -143,21 +144,19 @@ def setup_moonraker_prerequesites() -> None: git_clone_wrapper(repo, MOONRAKER_DIR, branch) # install moonraker dependencies and create python virtualenv - install_moonraker_packages(MOONRAKER_DIR) + install_moonraker_packages() create_python_venv(MOONRAKER_ENV_DIR) install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) -def install_moonraker_packages(moonraker_dir: Path) -> None: - install_script = moonraker_dir.joinpath("scripts/install-moonraker.sh") - deps_json = MOONRAKER_DIR.joinpath("scripts/system-dependencies.json") +def install_moonraker_packages() -> None: moonraker_deps = [] - if deps_json.exists(): - with open(deps_json, "r") as deps: + if MOONRAKER_DEPS_JSON_FILE.exists(): + with open(MOONRAKER_DEPS_JSON_FILE, "r") as deps: moonraker_deps = json.load(deps).get("debian", []) - elif install_script.exists(): - moonraker_deps = parse_packages_from_file(install_script) + elif MOONRAKER_INSTALL_SCRIPT.exists(): + moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT) if not moonraker_deps: raise ValueError("Error reading Moonraker dependencies!") @@ -209,7 +208,7 @@ def update_moonraker() -> None: git_pull_wrapper(repo=settings.moonraker.repo_url, target_dir=MOONRAKER_DIR) # install possible new system packages - install_moonraker_packages(MOONRAKER_DIR) + install_moonraker_packages() # install possible new python dependencies install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) diff --git a/kiauh/components/octoeverywhere/__init__.py b/kiauh/components/octoeverywhere/__init__.py index a816526..84c0e63 100644 --- a/kiauh/components/octoeverywhere/__init__.py +++ b/kiauh/components/octoeverywhere/__init__.py @@ -21,6 +21,7 @@ OE_REQ_FILE = OE_DIR.joinpath("requirements.txt") OE_DEPS_JSON_FILE = OE_DIR.joinpath("moonraker-system-dependencies.json") OE_INSTALL_SCRIPT = OE_DIR.joinpath("install.sh") OE_UPDATE_SCRIPT = OE_DIR.joinpath("update.sh") +OE_INSTALLER_LOG_FILE = Path.home().joinpath("octoeverywhere-installer.log") # filenames OE_CFG_NAME = "octoeverywhere.conf" diff --git a/kiauh/components/octoeverywhere/octoeverywhere.py b/kiauh/components/octoeverywhere/octoeverywhere.py index 5c8ad53..46fe05f 100644 --- a/kiauh/components/octoeverywhere/octoeverywhere.py +++ b/kiauh/components/octoeverywhere/octoeverywhere.py @@ -11,6 +11,7 @@ from pathlib import Path from subprocess import CalledProcessError, run from typing import List +from components.moonraker import MOONRAKER_CFG_NAME from components.octoeverywhere import ( OE_CFG_NAME, OE_DIR, @@ -55,7 +56,7 @@ class Octoeverywhere(BaseInstance): Logger.print_status("Creating OctoEverywhere for Klipper Instance ...") try: - cmd = f"{OE_INSTALL_SCRIPT} {self.cfg_dir}/moonraker.conf" + cmd = f"{OE_INSTALL_SCRIPT} {self.cfg_dir}/{MOONRAKER_CFG_NAME}" run(cmd, check=True, shell=True) except CalledProcessError as e: @@ -65,7 +66,7 @@ class Octoeverywhere(BaseInstance): @staticmethod def update(): try: - run(str(OE_UPDATE_SCRIPT), check=True, shell=True, cwd=OE_DIR) + run(OE_UPDATE_SCRIPT.as_posix(), check=True, shell=True, cwd=OE_DIR) except CalledProcessError as e: Logger.print_error(f"Error updating OctoEverywhere for Klipper: {e}") @@ -82,6 +83,7 @@ class Octoeverywhere(BaseInstance): try: command = ["sudo", "rm", "-f", service_file_path] run(command, check=True) + self.delete_logfiles(OE_LOG_NAME) Logger.print_ok(f"Service file deleted: {service_file_path}") except CalledProcessError as e: Logger.print_error(f"Error deleting service file: {e}") diff --git a/kiauh/components/octoeverywhere/octoeverywhere_setup.py b/kiauh/components/octoeverywhere/octoeverywhere_setup.py index 13634d1..cfea41d 100644 --- a/kiauh/components/octoeverywhere/octoeverywhere_setup.py +++ b/kiauh/components/octoeverywhere/octoeverywhere_setup.py @@ -7,7 +7,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # import json -from pathlib import Path from typing import List from components.moonraker.moonraker import Moonraker @@ -16,7 +15,7 @@ from components.octoeverywhere import ( OE_DIR, OE_ENV_DIR, OE_INSTALL_SCRIPT, - OE_LOG_NAME, + OE_INSTALLER_LOG_FILE, OE_REPO, OE_REQ_FILE, OE_SYS_CFG_NAME, @@ -147,7 +146,7 @@ def remove_octoeverywhere() -> None: remove_oe_dir() remove_oe_env() remove_config_section(f"include {OE_SYS_CFG_NAME}", mr_instances) - delete_oe_logs(ob_instances) + run_remove_routines(OE_INSTALLER_LOG_FILE) Logger.print_dialog( DialogType.SUCCESS, ["OctoEverywhere for Klipper successfully removed!"], @@ -209,23 +208,3 @@ def remove_oe_env() -> None: return run_remove_routines(OE_ENV_DIR) - - -def delete_oe_logs(instances: List[Octoeverywhere]) -> None: - Logger.print_status("Removing OctoEverywhere logs ...") - - all_logfiles = [] - for instance in instances: - all_logfiles = list(instance.log_dir.glob(f"{OE_LOG_NAME}*")) - - install_log = Path.home().joinpath("octoeverywhere-installer.log") - if install_log.exists(): - all_logfiles.append(install_log) - - if not all_logfiles: - Logger.print_info("No OctoEverywhere logs found. Skipped ...") - return - - for log in all_logfiles: - Logger.print_status(f"Remove '{log}'") - run_remove_routines(log) From bdb2c85e9bfdc4ec8ef4ece60b3bb151f9d58079 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Jul 2024 16:51:55 +0200 Subject: [PATCH 282/296] fix: fix usage of wrong status code Signed-off-by: Dominik Willner --- kiauh/core/menus/main_menu.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index c378c31..29d17ce 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -56,7 +56,7 @@ class MainMenu(BaseMenu): self.kl_status = self.kl_repo = self.mr_status = self.mr_repo = "" self.ms_status = self.fl_status = self.ks_status = self.mb_status = "" self.cn_status = self.cc_status = self.oe_status = "" - self.init_status() + self._init_status() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: """MainMenu does not have a previous menu""" @@ -74,7 +74,7 @@ class MainMenu(BaseMenu): "s": Option(method=self.settings_menu, menu=True), } - def init_status(self) -> None: + def _init_status(self) -> None: status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn", "oe"] for var in status_vars: setattr( @@ -83,7 +83,7 @@ class MainMenu(BaseMenu): f"{COLOR_RED}Not installed{RESET_FORMAT}", ) - def fetch_status(self) -> None: + def _fetch_status(self) -> None: self._get_component_status("kl", get_klipper_status) self._get_component_status("mr", get_moonraker_status) self._get_component_status("ms", get_client_status, MainsailData()) @@ -102,7 +102,7 @@ class MainMenu(BaseMenu): instance_count: int = status_data.instances count_txt: str = "" - if instance_count > 0 and code == 1: + if instance_count > 0 and code == 2: count_txt = f": {instance_count}" setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt)) @@ -120,7 +120,7 @@ class MainMenu(BaseMenu): return f"{color}{status}{count}{RESET_FORMAT}" def print_menu(self): - self.fetch_status() + self._fetch_status() header = " [ Main Menu ] " footer1 = f"{COLOR_CYAN}KIAUH v6.0.0{RESET_FORMAT}" From 005e2d3339bc31bf869be4bf05e7eca2033b750e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Jul 2024 17:24:50 +0200 Subject: [PATCH 283/296] refactor: improve robustness of instance sorting Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/instance_manager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py index 7cfd857..5305551 100644 --- a/kiauh/core/instance_manager/instance_manager.py +++ b/kiauh/core/instance_manager/instance_manager.py @@ -185,8 +185,10 @@ class InstanceManager: suffix = file_path.stem[len(name) :] return suffix[1:] if suffix else "" - def _sort_instance_list(self, s: Union[int, str, None]): - if s is None: + def _sort_instance_list(self, suffix: Union[int, str, None]): + if suffix is None: return - - return int(s) if s.isdigit() else s + elif suffix.isdigit(): + return f"{int(suffix):04}" + else: + return suffix From 75ac8a22d506d079775b36e4f78f978561e9cde6 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Jul 2024 17:25:08 +0200 Subject: [PATCH 284/296] refactor: add regex pattern to assign custom names Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 3132ad1..98c0ce7 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -141,8 +141,10 @@ def assign_custom_name(key: int, name_dict: Dict[int, str]) -> None: existing_names = [] existing_names.extend(Klipper.blacklist()) existing_names.extend(name_dict[n] for n in name_dict) + pattern = r"^[a-zA-Z0-9]+$" + question = f"Enter name for instance {key + 1}" - name_dict[key] = get_string_input(question, exclude=existing_names) + name_dict[key] = get_string_input(question, exclude=existing_names, regex=pattern) def handle_to_multi_instance_conversion(new_name: str) -> None: From ed2e318d0e02dae3fa522801915faa0fb3902eda Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Jul 2024 17:25:58 +0200 Subject: [PATCH 285/296] refactor: add __repr__ to Klipper class This commit adds a __repr__ method to the Klipper class. This method returns a JSON string representation of the instance, which can be used for debugging purposes. Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index 72e65cb..de25994 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -6,7 +6,7 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # - +import json from pathlib import Path from subprocess import CalledProcessError, run from typing import List @@ -41,6 +41,20 @@ class Klipper(BaseInstance): self._serial = self.comms_dir.joinpath(KLIPPER_SERIAL_NAME) self._uds = self.comms_dir.joinpath(KLIPPER_UDS_NAME) + def __repr__(self): + return json.dumps( + { + "suffix": self.suffix, + "klipper_dir": self.klipper_dir.as_posix(), + "env_dir": self.env_dir.as_posix(), + "cfg_file": self.cfg_file.as_posix(), + "log": self.log.as_posix(), + "serial": self.serial.as_posix(), + "uds": self.uds.as_posix(), + }, + indent=4, + ) + @property def cfg_file(self) -> Path: return self._cfg_file From 398705b17611ce63cf4b582b48934180eb1cbb99 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Jul 2024 17:27:21 +0200 Subject: [PATCH 286/296] fix: prevent exception when trying to remove log files from non-existing directory Signed-off-by: Dominik Willner --- kiauh/core/instance_manager/base_instance.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index da0a4b0..db8ed96 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -164,6 +164,9 @@ class BaseInstance(ABC): def delete_logfiles(self, log_name: str) -> None: from utils.fs_utils import run_remove_routines + if not self.log_dir.exists(): + return + files = self.log_dir.iterdir() logs = [f for f in files if f.name.startswith(log_name)] for log in logs: From 6bf55b5f69936edab875edf8b8ce7d3a51e1bad7 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Jul 2024 21:38:57 +0200 Subject: [PATCH 287/296] refactor: use virtualenv instead of venv Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 3 +-- kiauh/utils/sys_utils.py | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index f085d7c..1c6b5bc 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -75,7 +75,7 @@ def install_klipper() -> None: try: if not kl_im.instances: - check_install_dependencies(["git"]) + check_install_dependencies(["git", "python3-virtualenv"]) setup_klipper_prerequesites() count = 0 @@ -127,7 +127,6 @@ def setup_klipper_prerequesites() -> None: def install_klipper_packages() -> None: script = KLIPPER_INSTALL_SCRIPT packages = parse_packages_from_file(script) - packages.append("python3-venv") # todo: remove once switched to virtualenv # Add dbus requirement for DietPi distro if Path("/boot/dietpi/.version").exists(): diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 477a5d2..9b150ce 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -16,7 +16,6 @@ import sys import time import urllib.error import urllib.request -import venv from pathlib import Path from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, run from typing import List, Literal @@ -96,13 +95,11 @@ def create_python_venv(target: Path) -> None: Logger.print_status("Set up Python virtual environment ...") if not target.exists(): try: - venv.create(target, with_pip=True) + cmd = ["virtualenv", "-p", "/usr/bin/python3", target.as_posix()] + run(cmd, check=True) Logger.print_ok("Setup of virtualenv successful!") - except OSError as e: - Logger.print_error(f"Error setting up virtualenv:\n{e}") - raise except CalledProcessError as e: - Logger.print_error(f"Error setting up virtualenv:\n{e.output.decode()}") + Logger.print_error(f"Error setting up virtualenv:\n{e}") raise else: if get_confirm("Virtualenv already exists. Re-create?", default_choice=False): From 1384f7328a11cca4af71b3c0ed456c0b1e7aac0c Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Jul 2024 22:02:21 +0200 Subject: [PATCH 288/296] refactor: use global deps list to check for generally required dependencies Signed-off-by: Dominik Willner --- kiauh/components/mobileraker/mobileraker.py | 3 +-- kiauh/components/moonraker/moonraker_setup.py | 2 +- kiauh/components/webui_client/client_setup.py | 2 +- kiauh/utils/__init__.py | 2 ++ kiauh/utils/common.py | 15 ++++++++++----- kiauh/utils/sys_utils.py | 4 ++-- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/kiauh/components/mobileraker/mobileraker.py b/kiauh/components/mobileraker/mobileraker.py index 8da3114..2c47efb 100644 --- a/kiauh/components/mobileraker/mobileraker.py +++ b/kiauh/components/mobileraker/mobileraker.py @@ -70,8 +70,7 @@ def install_mobileraker() -> None: ): return - package_list = ["git", "wget", "curl", "unzip", "dfu-util"] - check_install_dependencies(package_list) + check_install_dependencies() git_clone_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 146a8a4..b8639b3 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -90,7 +90,7 @@ def install_moonraker() -> None: create_example_cfg = get_confirm("Create example moonraker.conf?") try: - check_install_dependencies(["git"]) + check_install_dependencies() setup_moonraker_prerequesites() install_moonraker_polkit() diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 06e2686..827f8e4 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -112,7 +112,7 @@ def install_client(client: BaseWebClient) -> None: ) valid_port = is_valid_port(port, ports_in_use) - check_install_dependencies(["nginx", "unzip"]) + check_install_dependencies(["nginx"]) try: download_client(client) diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py index 2435e5d..4c40067 100644 --- a/kiauh/utils/__init__.py +++ b/kiauh/utils/__init__.py @@ -15,6 +15,8 @@ MODULE_PATH = Path(__file__).resolve().parent INVALID_CHOICE = "Invalid choice. Please select a valid value." PRINTER_CFG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-cfg-backups") +GLOBAL_DEPS = ["git", "wget", "curl", "unzip", "dfu-util", "python3-virtualenv"] + # ================== NGINX =====================# NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled") diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index c38464e..574ea7e 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -6,6 +6,8 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations + import re from datetime import datetime from pathlib import Path @@ -14,7 +16,7 @@ from typing import Dict, List, Literal, Optional, Type from components.klipper.klipper import Klipper from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager -from utils import PRINTER_CFG_BACKUP_DIR +from utils import GLOBAL_DEPS, PRINTER_CFG_BACKUP_DIR from utils.constants import ( COLOR_CYAN, RESET_FORMAT, @@ -45,19 +47,22 @@ def get_current_date() -> Dict[Literal["date", "time"], str]: return {"date": date, "time": time} -def check_install_dependencies(deps: List[str]) -> None: +def check_install_dependencies(deps: List[str] | None = None) -> None: """ Common helper method to check if dependencies are installed and if not, install them automatically | :param deps: List of strings of package names to check if installed :return: None """ - requirements = check_package_install(deps) + if deps is None: + deps = [] + + requirements = check_package_install({*GLOBAL_DEPS, *deps}) if requirements: Logger.print_status("Installing dependencies ...") Logger.print_info("The following packages need installation:") - for _ in requirements: - print(f"{COLOR_CYAN}● {_}{RESET_FORMAT}") + for r in requirements: + print(f"{COLOR_CYAN}● {r}{RESET_FORMAT}") update_system_package_lists(silent=False) install_system_packages(requirements) diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 9b150ce..e0575f7 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -18,7 +18,7 @@ import urllib.error import urllib.request from pathlib import Path from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, run -from typing import List, Literal +from typing import List, Literal, Set from utils.constants import SYSTEMD from utils.fs_utils import check_file_exist, remove_with_sudo @@ -217,7 +217,7 @@ def update_system_package_lists(silent: bool, rls_info_change=False) -> None: raise -def check_package_install(packages: List[str]) -> List[str]: +def check_package_install(packages: Set[str]) -> List[str]: """ Checks the system for installed packages | :param packages: List of strings of package names From 31ea6c2e5aeeff95735bd8496c0304c06e741fa8 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 7 Jul 2024 22:29:49 +0200 Subject: [PATCH 289/296] refactor: add moonraker speedup dependencies Signed-off-by: Dominik Willner --- kiauh/components/moonraker/__init__.py | 1 + kiauh/components/moonraker/moonraker_setup.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index 54d61eb..8924129 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -29,6 +29,7 @@ MOONRAKER_DB_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-db-backups") # files MOONRAKER_INSTALL_SCRIPT = MOONRAKER_DIR.joinpath("scripts/install-moonraker.sh") MOONRAKER_REQ_FILE = MOONRAKER_DIR.joinpath("scripts/moonraker-requirements.txt") +MOONRAKER_SPEEDUPS_REQ_FILE = MOONRAKER_DIR.joinpath("scripts/moonraker-speedups.txt") MOONRAKER_DEPS_JSON_FILE = MOONRAKER_DIR.joinpath("scripts/system-dependencies.json") # introduced due to # https://github.com/Arksine/moonraker/issues/349 diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index b8639b3..2dbfce6 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -17,6 +17,7 @@ from components.moonraker import ( MOONRAKER_ENV_DIR, MOONRAKER_INSTALL_SCRIPT, MOONRAKER_REQ_FILE, + MOONRAKER_SPEEDUPS_REQ_FILE, POLKIT_FILE, POLKIT_LEGACY_FILE, POLKIT_SCRIPT, @@ -147,6 +148,7 @@ def setup_moonraker_prerequesites() -> None: install_moonraker_packages() 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: From e5bcab5d857d2ca0f2b2697b64355bd3b53e275f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 13 Jul 2024 13:34:55 +0200 Subject: [PATCH 290/296] fix: return if instance_list is empty Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_remove.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index c565f50..7cf0894 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -78,6 +78,9 @@ def remove_instances( instance_manager: InstanceManager, instance_list: List[Klipper], ) -> None: + if not instance_list: + return + for instance in instance_list: Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...") instance_manager.current_instance = instance From fee2dd0bdaea736eedfe8b8e7591644fc891e28a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 14 Jul 2024 14:44:08 +0200 Subject: [PATCH 291/296] refactor: use | instead of Union Signed-off-by: Dominik Willner --- .../klipper_firmware/flash_options.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/kiauh/components/klipper_firmware/flash_options.py b/kiauh/components/klipper_firmware/flash_options.py index 775fa9c..420a426 100644 --- a/kiauh/components/klipper_firmware/flash_options.py +++ b/kiauh/components/klipper_firmware/flash_options.py @@ -6,10 +6,11 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations from dataclasses import field from enum import Enum -from typing import List, Union +from typing import List class FlashMethod(Enum): @@ -30,9 +31,9 @@ class ConnectionType(Enum): class FlashOptions: _instance = None - _flash_method: Union[FlashMethod, None] = None - _flash_command: Union[FlashCommand, None] = None - _connection_type: Union[ConnectionType, None] = None + _flash_method: FlashMethod | None = None + _flash_command: FlashCommand | None = None + _connection_type: ConnectionType | None = None _mcu_list: List[str] = field(default_factory=list) _selected_mcu: str = "" _selected_board: str = "" @@ -48,27 +49,27 @@ class FlashOptions: cls._instance = None @property - def flash_method(self) -> Union[FlashMethod, None]: + def flash_method(self) -> FlashMethod | None: return self._flash_method @flash_method.setter - def flash_method(self, value: Union[FlashMethod, None]): + def flash_method(self, value: FlashMethod | None): self._flash_method = value @property - def flash_command(self) -> Union[FlashCommand, None]: + def flash_command(self) -> FlashCommand | None: return self._flash_command @flash_command.setter - def flash_command(self, value: Union[FlashCommand, None]): + def flash_command(self, value: FlashCommand | None): self._flash_command = value @property - def connection_type(self) -> Union[ConnectionType, None]: + def connection_type(self) -> ConnectionType | None: return self._connection_type @connection_type.setter - def connection_type(self, value: Union[ConnectionType, None]): + def connection_type(self, value: ConnectionType | None): self._connection_type = value @property From 871bedb76bd96043d13cf704ca02be5541f73ebf Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 27 Jul 2024 21:13:56 +0200 Subject: [PATCH 292/296] refactor: overhaul of the klipper setup process Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper.py | 56 ++----- kiauh/components/klipper/klipper_dialogs.py | 14 +- kiauh/components/klipper/klipper_setup.py | 151 ++++++++++++----- kiauh/components/klipper/klipper_utils.py | 145 +--------------- kiauh/components/moonraker/moonraker.py | 18 +- kiauh/components/moonraker/moonraker_utils.py | 54 ------ kiauh/core/instance_manager/base_instance.py | 157 ++++++------------ kiauh/utils/input_utils.py | 8 +- kiauh/utils/logger.py | 14 ++ 9 files changed, 211 insertions(+), 406 deletions(-) diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py index de25994..aeda046 100644 --- a/kiauh/components/klipper/klipper.py +++ b/kiauh/components/klipper/klipper.py @@ -6,10 +6,9 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import json +from dataclasses import dataclass from pathlib import Path from subprocess import CalledProcessError, run -from typing import List from components.klipper import ( KLIPPER_CFG_NAME, @@ -27,49 +26,24 @@ from utils.logger import Logger # noinspection PyMethodMayBeStatic +@dataclass class Klipper(BaseInstance): - @classmethod - def blacklist(cls) -> List[str]: - return ["None", "mcu"] + klipper_dir: Path = KLIPPER_DIR + env_dir: Path = KLIPPER_ENV_DIR + cfg_file: Path = None + log: Path = None + serial: Path = None + uds: Path = None - def __init__(self, suffix: str = ""): + def __init__(self, suffix: str = "") -> None: super().__init__(instance_type=self, suffix=suffix) - self.klipper_dir: Path = KLIPPER_DIR - self.env_dir: Path = KLIPPER_ENV_DIR - self._cfg_file = self.cfg_dir.joinpath(KLIPPER_CFG_NAME) - self._log = self.log_dir.joinpath(KLIPPER_LOG_NAME) - self._serial = self.comms_dir.joinpath(KLIPPER_SERIAL_NAME) - self._uds = self.comms_dir.joinpath(KLIPPER_UDS_NAME) - def __repr__(self): - return json.dumps( - { - "suffix": self.suffix, - "klipper_dir": self.klipper_dir.as_posix(), - "env_dir": self.env_dir.as_posix(), - "cfg_file": self.cfg_file.as_posix(), - "log": self.log.as_posix(), - "serial": self.serial.as_posix(), - "uds": self.uds.as_posix(), - }, - indent=4, - ) - - @property - def cfg_file(self) -> Path: - return self._cfg_file - - @property - def log(self) -> Path: - return self._log - - @property - def serial(self) -> Path: - return self._serial - - @property - def uds(self) -> Path: - return self._uds + def __post_init__(self) -> None: + super().__post_init__() + self.cfg_file = self.cfg_dir.joinpath(KLIPPER_CFG_NAME) + self.log = self.log_dir.joinpath(KLIPPER_LOG_NAME) + self.serial = self.comms_dir.joinpath(KLIPPER_SERIAL_NAME) + self.uds = self.comms_dir.joinpath(KLIPPER_UDS_NAME) def create(self) -> None: from utils.sys_utils import create_env_file, create_service_file diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py index 4d1137f..a147b68 100644 --- a/kiauh/components/klipper/klipper_dialogs.py +++ b/kiauh/components/klipper/klipper_dialogs.py @@ -90,9 +90,19 @@ def print_select_custom_name_dialog(): dialog = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ - ║ You can now assign a custom name to each instance. ║ + ║ Do you want to assign a custom name to each instance? ║ + ║ ║ + ║ Assigning a custom name will create a Klipper service ║ + ║ and a printer directory with the chosen name. ║ + ║ ║ + ║ Example for custom name 'kiauh': ║ + ║ ● Klipper service: klipper-kiauh.service ║ + ║ ● Printer directory: printer_kiauh_data ║ + ║ ║ ║ If skipped, each instance will get an index assigned ║ - ║ in ascending order, starting at index '1'. ║ + ║ in ascending order, starting at '1' in case of a new ║ + ║ installation. Otherwise, the index will be derived ║ + ║ from amount of already existing instances. ║ ║ ║ ║ {line1:<63}║ ║ {line2:<63}║ diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index 1c6b5bc..a4f5332 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -6,8 +6,10 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations from pathlib import Path +from typing import Dict, List, Tuple from components.klipper import ( EXIT_KLIPPER_SETUP, @@ -17,18 +19,16 @@ from components.klipper import ( KLIPPER_REQ_FILE, ) from components.klipper.klipper import Klipper +from components.klipper.klipper_dialogs import ( + print_select_custom_name_dialog, +) from components.klipper.klipper_utils import ( - add_to_existing, + assign_custom_name, backup_klipper_dir, - check_is_single_to_multi_conversion, check_user_groups, create_example_printer_cfg, get_install_count, handle_disruptive_system_packages, - handle_instance_naming, - handle_to_multi_instance_conversion, - init_name_scheme, - update_name_scheme, ) from components.moonraker.moonraker import Moonraker from components.webui_client.client_utils import ( @@ -49,57 +49,65 @@ from utils.sys_utils import ( def install_klipper() -> None: - kl_im = InstanceManager(Klipper) + Logger.print_status("Installing Klipper ...") - # ask to add new instances, if there are existing ones - if kl_im.instances and not add_to_existing(): - Logger.print_status(EXIT_KLIPPER_SETUP) - return + klipper_list: List[Klipper] = InstanceManager(Klipper).instances + moonraker_list: List[Moonraker] = InstanceManager(Moonraker).instances + match_moonraker: bool = False - install_count = get_install_count() - if install_count is None: - Logger.print_status(EXIT_KLIPPER_SETUP) - return + # if there are more moonraker instances than klipper instances, ask the user to + # match the klipper instance count to the count of moonraker instances with the same suffix + if len(moonraker_list) > len(klipper_list): + is_confirmed = display_moonraker_info(moonraker_list) + if not is_confirmed: + Logger.print_status(EXIT_KLIPPER_SETUP) + return + match_moonraker = True - # create a dict of the size of the existing instances + install count - name_dict = {c: "" for c in range(len(kl_im.instances) + install_count)} - name_scheme = init_name_scheme(kl_im.instances, install_count) - mr_im = InstanceManager(Moonraker) - name_scheme = update_name_scheme( - name_scheme, name_dict, kl_im.instances, mr_im.instances + install_count, name_dict = get_install_count_and_name_dict( + klipper_list, moonraker_list ) - handle_instance_naming(name_dict, name_scheme) + if install_count == 0 or name_dict == {}: + Logger.print_status(EXIT_KLIPPER_SETUP) + return + + is_multi_install = install_count > 1 or (len(name_dict) >= 1 and install_count >= 1) + if not name_dict and install_count == 1: + name_dict = {0: ""} + elif is_multi_install and not match_moonraker: + custom_names = use_custom_names_or_go_back() + if custom_names is None: + Logger.print_status(EXIT_KLIPPER_SETUP) + return + + handle_instance_names(install_count, name_dict, custom_names) create_example_cfg = get_confirm("Create example printer.cfg?") - + # run the actual installation try: - if not kl_im.instances: - check_install_dependencies(["git", "python3-virtualenv"]) - setup_klipper_prerequesites() - - count = 0 - for name in name_dict: - if name_dict[name] in [n.suffix for n in kl_im.instances]: - continue - - if check_is_single_to_multi_conversion(kl_im.instances): - handle_to_multi_instance_conversion(name_dict[name]) - continue - - count += 1 - create_klipper_instance(name_dict[name], create_example_cfg) - - if count == install_count: - break - - cmd_sysctl_manage("daemon-reload") - + run_klipper_setup(klipper_list, name_dict, create_example_cfg) except Exception as e: Logger.print_error(e) Logger.print_error("Klipper installation failed!") return + +def run_klipper_setup( + klipper_list: List[Klipper], name_dict: Dict[int, str], example_cfg: bool +) -> None: + if not klipper_list: + setup_klipper_prerequesites() + + for i in name_dict: + # skip this iteration if there is already an instance with the name + if name_dict[i] in [n.suffix for n in klipper_list]: + continue + + create_klipper_instance(name_dict[i], example_cfg) + + cmd_sysctl_manage("daemon-reload") + # step 4: check/handle conflicting packages/services handle_disruptive_system_packages() @@ -107,6 +115,35 @@ def install_klipper() -> None: check_user_groups() +def handle_instance_names( + install_count: int, name_dict: Dict[int, str], custom_names: bool +) -> None: + for i in range(install_count): + index = len(name_dict) + i + 1 + if custom_names: + assign_custom_name(index, name_dict) + else: + name_dict[i + 1] = str(index) + + +def get_install_count_and_name_dict( + klipper_list: List[Klipper], moonraker_list: List[Moonraker] +) -> Tuple[int, Dict[int, str]]: + if len(moonraker_list) > len(klipper_list): + install_count = len(moonraker_list) + name_dict = {i: moonraker.suffix for i, moonraker in enumerate(moonraker_list)} + + else: + install_count = get_install_count() + name_dict = {i: klipper.suffix for i, klipper in enumerate(klipper_list)} + + if install_count is None: + Logger.print_status(EXIT_KLIPPER_SETUP) + return 0, {} + + return install_count, name_dict + + def setup_klipper_prerequesites() -> None: settings = KiauhSettings() repo = settings.klipper.repo_url @@ -176,3 +213,29 @@ def create_klipper_instance(name: str, create_example_cfg: bool) -> None: clients = get_existing_clients() create_example_printer_cfg(new_instance, clients) kl_im.start_instance() + + +def use_custom_names_or_go_back() -> bool | None: + print_select_custom_name_dialog() + return get_confirm( + "Assign custom names?", + False, + allow_go_back=True, + ) + + +def display_moonraker_info(moonraker_list: List[Moonraker]) -> bool: + # todo: only show the klipper instances that are not already installed + Logger.print_dialog( + DialogType.INFO, + [ + "Existing Moonraker instances detected:", + *[f"● {m.get_service_file_name()}" for m in moonraker_list], + "\n\n", + "The following Klipper instances will be installed:", + *[f"● klipper-{m.suffix}" for m in moonraker_list], + ], + padding_top=0, + padding_bottom=0, + ) + return get_confirm("Proceed with installation?") diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 98c0ce7..f0cc6cd 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -6,13 +6,13 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations import grp import os -import re import shutil from subprocess import CalledProcessError, run -from typing import Dict, List, Optional, Union +from typing import Dict, List from components.klipper import ( KLIPPER_BACKUP_DIR, @@ -23,23 +23,17 @@ from components.klipper import ( from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import ( print_instance_overview, - print_select_custom_name_dialog, print_select_instance_count_dialog, ) -from components.moonraker.moonraker import Moonraker -from components.moonraker.moonraker_utils import moonraker_to_multi_conversion from components.webui_client.base_data import BaseWebClient from components.webui_client.client_config.client_config_setup import ( create_client_config_symlink, ) from core.backup_manager.backup_manager import BackupManager -from core.instance_manager.base_instance import BaseInstance from core.instance_manager.instance_manager import InstanceManager -from core.instance_manager.name_scheme import NameScheme from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( SimpleConfigParser, ) -from utils import PRINTER_CFG_BACKUP_DIR from utils.common import get_install_status from utils.constants import CURRENT_USER from utils.input_utils import get_confirm, get_number_input, get_string_input @@ -52,75 +46,13 @@ def get_klipper_status() -> ComponentStatus: return get_install_status(KLIPPER_DIR, KLIPPER_ENV_DIR, Klipper) -def check_is_multi_install( - existing_instances: List[Klipper], install_count: int -) -> bool: - return not existing_instances and install_count > 1 - - -def check_is_single_to_multi_conversion( - existing_instances: List[Klipper], -) -> bool: - return len(existing_instances) == 1 and existing_instances[0].suffix == "" - - -def init_name_scheme( - existing_instances: List[Klipper], install_count: int -) -> NameScheme: - if check_is_multi_install( - existing_instances, install_count - ) or check_is_single_to_multi_conversion(existing_instances): - print_select_custom_name_dialog() - if get_confirm("Assign custom names?", False, allow_go_back=True): - return NameScheme.CUSTOM - else: - return NameScheme.INDEX - else: - return NameScheme.SINGLE - - -def update_name_scheme( - name_scheme: NameScheme, - name_dict: Dict[int, str], - klipper_instances: List[Klipper], - moonraker_instances: List[Moonraker], -) -> NameScheme: - # if there are more moonraker instances installed - # than klipper, we load their names into the name_dict, - # as we will detect and enforce that naming scheme - if len(moonraker_instances) > len(klipper_instances): - update_name_dict(name_dict, moonraker_instances) - return detect_name_scheme(moonraker_instances) - elif len(klipper_instances) > 1: - update_name_dict(name_dict, klipper_instances) - return detect_name_scheme(klipper_instances) - else: - return name_scheme - - -def update_name_dict(name_dict: Dict[int, str], instances: List[BaseInstance]) -> None: - for k, v in enumerate(instances): - name_dict[k] = v.suffix - - -def handle_instance_naming(name_dict: Dict[int, str], name_scheme: NameScheme) -> None: - if name_scheme == NameScheme.SINGLE: - return - - for k in name_dict: - if name_dict[k] == "" and name_scheme == NameScheme.INDEX: - name_dict[k] = str(k + 1) - elif name_dict[k] == "" and name_scheme == NameScheme.CUSTOM: - assign_custom_name(k, name_dict) - - def add_to_existing() -> bool: kl_instances = InstanceManager(Klipper).instances print_instance_overview(kl_instances) return get_confirm("Add new instances?", allow_go_back=True) -def get_install_count() -> Union[int, None]: +def get_install_count() -> int | None: """ Print a dialog for selecting the amount of Klipper instances to set up with an option to navigate back. Returns None if the @@ -143,64 +75,10 @@ def assign_custom_name(key: int, name_dict: Dict[int, str]) -> None: existing_names.extend(name_dict[n] for n in name_dict) pattern = r"^[a-zA-Z0-9]+$" - question = f"Enter name for instance {key + 1}" + question = f"Enter name for instance {key}" name_dict[key] = get_string_input(question, exclude=existing_names, regex=pattern) -def handle_to_multi_instance_conversion(new_name: str) -> None: - Logger.print_status("Converting single instance to multi instances ...") - klipper_to_multi_conversion(new_name) - moonraker_to_multi_conversion(new_name) - - -def klipper_to_multi_conversion(new_name: str) -> None: - Logger.print_status("Convert Klipper single to multi instance ...") - im = InstanceManager(Klipper) - im.current_instance = im.instances[0] - - # temporarily store the data dir path - old_data_dir = im.instances[0].data_dir - old_data_dir_name = im.instances[0].data_dir_name - - # backup the old data_dir - bm = BackupManager() - name = f"config-{old_data_dir_name}" - bm.backup_directory( - name, - source=im.current_instance.cfg_dir, - target=PRINTER_CFG_BACKUP_DIR, - ) - - # remove the old single instance - im.stop_instance() - im.disable_instance() - im.delete_instance() - - # create a new klipper instance with the new name - new_instance = Klipper(suffix=new_name) - im.current_instance = new_instance - - if not new_instance.data_dir.is_dir(): - # rename the old data dir and use it for the new instance - Logger.print_status(f"Rename '{old_data_dir}' to '{new_instance.data_dir}' ...") - old_data_dir.rename(new_instance.data_dir) - else: - Logger.print_info(f"Existing '{new_instance.data_dir}' found ...") - - # patch the virtual_sdcard sections path - # value to match the new printer_data foldername - scp = SimpleConfigParser() - scp.read(new_instance.cfg_file) - if scp.has_section("virtual_sdcard"): - scp.set("virtual_sdcard", "path", str(new_instance.gcodes_dir)) - scp.write(new_instance.cfg_file) - - # finalize creating the new instance - im.create_instance() - im.enable_instance() - im.start_instance() - - def check_user_groups(): user_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] missing_groups = [g for g in ["tty", "dialout"] if g not in user_groups] @@ -277,21 +155,8 @@ def handle_disruptive_system_packages() -> None: ) -def detect_name_scheme(instance_list: List[BaseInstance]) -> NameScheme: - pattern = re.compile("^\d+$") - for instance in instance_list: - if not pattern.match(instance.suffix): - return NameScheme.CUSTOM - - return NameScheme.INDEX - - -def get_highest_index(instance_list: List[Klipper]) -> int: - return max([int(instance.suffix.split("-")[-1]) for instance in instance_list]) - - def create_example_printer_cfg( - instance: Klipper, clients: Optional[List[BaseWebClient]] = None + instance: Klipper, clients: List[BaseWebClient] | None = None ) -> None: Logger.print_status(f"Creating example printer.cfg in '{instance.cfg_dir}'") if instance.cfg_file.is_file(): diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index 3dd5131..f4db5de 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -10,7 +10,6 @@ from __future__ import annotations from pathlib import Path from subprocess import CalledProcessError, run -from typing import List from components.moonraker import ( MOONRAKER_CFG_NAME, @@ -30,10 +29,6 @@ from utils.logger import Logger # noinspection PyMethodMayBeStatic class Moonraker(BaseInstance): - @classmethod - def blacklist(cls) -> List[str]: - return ["None", "mcu", "obico"] - def __init__(self, suffix: str = ""): super().__init__(instance_type=self, suffix=suffix) self.moonraker_dir: Path = MOONRAKER_DIR @@ -42,25 +37,16 @@ class Moonraker(BaseInstance): self.port = self._get_port() self.backup_dir = self.data_dir.joinpath("backup") self.certs_dir = self.data_dir.joinpath("certs") - self._db_dir = self.data_dir.joinpath("database") - self._comms_dir = self.data_dir.joinpath("comms") + self.db_dir = self.data_dir.joinpath("database") self.log = self.log_dir.joinpath(MOONRAKER_LOG_NAME) - @property - def db_dir(self) -> Path: - return self._db_dir - - @property - def comms_dir(self) -> Path: - return self._comms_dir - def create(self, create_example_cfg: bool = False) -> None: from utils.sys_utils import create_env_file, create_service_file Logger.print_status("Creating new Moonraker Instance ...") try: - self.create_folders([self.backup_dir, self.certs_dir, self._db_dir]) + self.create_folders([self.backup_dir, self.certs_dir, self.db_dir]) create_service_file( name=self.get_service_file_name(extension=True), content=self._prep_service_file_content(), diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index 8d141ef..c68755d 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -20,8 +20,6 @@ from components.moonraker import ( ) from components.moonraker.moonraker import Moonraker from components.webui_client.base_data import BaseWebClient -from components.webui_client.client_utils import enable_mainsail_remotemode -from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager from core.instance_manager.instance_manager import InstanceManager from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( @@ -128,58 +126,6 @@ def create_example_moonraker_conf( Logger.print_ok(f"Example moonraker.conf created in '{instance.cfg_dir}'") -def moonraker_to_multi_conversion(new_name: str) -> None: - """ - Converts the first instance in the List of Moonraker instances to an instance - with a new name. This method will be called when converting from a single Klipper - instance install to a multi instance install when Moonraker is also already - installed with a single instance. - :param new_name: new name the previous single instance is renamed to - :return: None - """ - im = InstanceManager(Moonraker) - instances: List[Moonraker] = im.instances - if not instances: - return - - # in case there are multiple Moonraker instances, we don't want to do anything - if len(instances) > 1: - Logger.print_info("More than a single Moonraker instance found. Skipped ...") - return - - Logger.print_status("Convert Moonraker single to multi instance ...") - - # remove the old single instance - im.current_instance = im.instances[0] - im.stop_instance() - im.disable_instance() - im.delete_instance() - - # create a new moonraker instance with the new name - new_instance = Moonraker(suffix=new_name) - im.current_instance = new_instance - - # patch the server sections klippy_uds_address value to match the new printer_data foldername - scp = SimpleConfigParser() - scp.read(new_instance.cfg_file) - if scp.has_section("server"): - scp.set( - "server", - "klippy_uds_address", - str(new_instance.comms_dir.joinpath("klippy.sock")), - ) - scp.write(new_instance.cfg_file) - - # create, enable and start the new moonraker instance - im.create_instance() - im.enable_instance() - im.start_instance() - - # if mainsail is installed, we enable mainsails remote mode - if MainsailData().client_dir.exists() and len(im.instances) > 1: - enable_mainsail_remotemode() - - def backup_moonraker_dir(): bm = BackupManager() bm.backup_directory("moonraker", source=MOONRAKER_DIR, target=MOONRAKER_BACKUP_DIR) diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py index db8ed96..c5b5571 100644 --- a/kiauh/core/instance_manager/base_instance.py +++ b/kiauh/core/instance_manager/base_instance.py @@ -9,7 +9,9 @@ from __future__ import annotations +import re from abc import ABC, abstractmethod +from dataclasses import dataclass, field from pathlib import Path from typing import List, Optional @@ -17,106 +19,32 @@ from utils.constants import CURRENT_USER, SYSTEMD from utils.logger import Logger +@dataclass class BaseInstance(ABC): + instance_type: BaseInstance + suffix: str + user: str = field(default=CURRENT_USER, init=False) + data_dir: Path = None + data_dir_name: str = "" + is_legacy_instance: bool = False + cfg_dir: Path = None + log_dir: Path = None + comms_dir: Path = None + sysd_dir: Path = None + gcodes_dir: Path = None + + def __post_init__(self) -> None: + self._set_data_dir() + self._set_is_legacy_instance() + self.cfg_dir = self.data_dir.joinpath("config") + self.log_dir = self.data_dir.joinpath("logs") + self.comms_dir = self.data_dir.joinpath("comms") + self.sysd_dir = self.data_dir.joinpath("systemd") + self.gcodes_dir = self.data_dir.joinpath("gcodes") + @classmethod def blacklist(cls) -> List[str]: - return [] - - def __init__( - self, - suffix: str, - instance_type: BaseInstance, - ): - self._instance_type = instance_type - self._suffix = suffix - self._user = CURRENT_USER - self._data_dir_name = self.get_data_dir_name_from_suffix() - self._data_dir = Path.home().joinpath(f"{self._data_dir_name}_data") - self._cfg_dir = self.data_dir.joinpath("config") - self._log_dir = self.data_dir.joinpath("logs") - self._comms_dir = self.data_dir.joinpath("comms") - self._sysd_dir = self.data_dir.joinpath("systemd") - self._gcodes_dir = self.data_dir.joinpath("gcodes") - - @property - def instance_type(self) -> BaseInstance: - return self._instance_type - - @instance_type.setter - def instance_type(self, value: BaseInstance) -> None: - self._instance_type = value - - @property - def suffix(self) -> str: - return self._suffix - - @suffix.setter - def suffix(self, value: str) -> None: - self._suffix = value - - @property - def user(self) -> str: - return self._user - - @user.setter - def user(self, value: str) -> None: - self._user = value - - @property - def data_dir_name(self) -> str: - return self._data_dir_name - - @data_dir_name.setter - def data_dir_name(self, value: str) -> None: - self._data_dir_name = value - - @property - def data_dir(self) -> Path: - return self._data_dir - - @data_dir.setter - def data_dir(self, value: Path) -> None: - self._data_dir = value - - @property - def cfg_dir(self) -> Path: - return self._cfg_dir - - @cfg_dir.setter - def cfg_dir(self, value: Path) -> None: - self._cfg_dir = value - - @property - def log_dir(self) -> Path: - return self._log_dir - - @log_dir.setter - def log_dir(self, value: Path) -> None: - self._log_dir = value - - @property - def comms_dir(self) -> Path: - return self._comms_dir - - @comms_dir.setter - def comms_dir(self, value: Path) -> None: - self._comms_dir = value - - @property - def sysd_dir(self) -> Path: - return self._sysd_dir - - @sysd_dir.setter - def sysd_dir(self, value: Path) -> None: - self._sysd_dir = value - - @property - def gcodes_dir(self) -> Path: - return self._gcodes_dir - - @gcodes_dir.setter - def gcodes_dir(self, value: Path) -> None: - self._gcodes_dir = value + return ["None", "mcu", "obico", "bambu", "companion"] @abstractmethod def create(self) -> None: @@ -133,6 +61,7 @@ class BaseInstance(ABC): self.log_dir, self.comms_dir, self.sysd_dir, + self.gcodes_dir, ] if add_dirs: @@ -141,6 +70,7 @@ class BaseInstance(ABC): for _dir in dirs: _dir.mkdir(exist_ok=True) + # todo: refactor into a set method and access the value by accessing the property def get_service_file_name(self, extension: bool = False) -> str: from utils.common import convert_camelcase_to_kebabcase @@ -150,17 +80,10 @@ class BaseInstance(ABC): return name if not extension else f"{name}.service" + # todo: refactor into a set method and access the value by accessing the property def get_service_file_path(self) -> Path: return SYSTEMD.joinpath(self.get_service_file_name(extension=True)) - def get_data_dir_name_from_suffix(self) -> str: - if self._suffix == "": - return "printer" - elif self._suffix.isdigit(): - return f"printer_{self._suffix}" - else: - return self._suffix - def delete_logfiles(self, log_name: str) -> None: from utils.fs_utils import run_remove_routines @@ -172,3 +95,27 @@ class BaseInstance(ABC): for log in logs: Logger.print_status(f"Remove '{log}'") run_remove_routines(log) + + def _set_data_dir(self) -> None: + if self.suffix == "": + self.data_dir = Path.home().joinpath("printer_data") + else: + self.data_dir = Path.home().joinpath(f"printer_{self.suffix}_data") + + if self.get_service_file_path().exists(): + with open(self.get_service_file_path(), "r") as service_file: + service_content = service_file.read() + pattern = re.compile("^EnvironmentFile=(.+)(/systemd/.+\.env)") + match = re.search(pattern, service_content) + if match: + self.data_dir = Path(match.group(1)) + + def _set_is_legacy_instance(self) -> None: + if ( + self.suffix != "" + and not self.data_dir_name.startswith("printer_") + and not self.data_dir_name.endswith("_data") + ): + self.is_legacy_instance = True + else: + self.is_legacy_instance = False diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 4c6ca3c..58e5ae7 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -6,6 +6,8 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations + import re from typing import List, Union @@ -14,9 +16,7 @@ from utils.constants import COLOR_CYAN, RESET_FORMAT from utils.logger import Logger -def get_confirm( - question: str, default_choice=True, allow_go_back=False -) -> Union[bool, None]: +def get_confirm(question: str, default_choice=True, allow_go_back=False) -> bool | None: """ Helper method for validating confirmation (yes/no) user input. | :param question: The question to display @@ -56,7 +56,7 @@ def get_number_input( max_count=None, default=None, allow_go_back=False, -) -> Union[int, None]: +) -> int | None: """ Helper method to get a number input from the user :param question: The question to display diff --git a/kiauh/utils/logger.py b/kiauh/utils/logger.py index b4d7ae0..aa60ebb 100644 --- a/kiauh/utils/logger.py +++ b/kiauh/utils/logger.py @@ -93,6 +93,20 @@ class Logger: padding_top: int = 1, padding_bottom: int = 1, ) -> None: + """ + Prints a dialog with the given title and content. + Those dialogs should be used to display verbose messages to the user which + require simple interaction like confirmation or input. Do not use this for + navigating through the application. + + :param title: The type of the dialog. + :param content: The content of the dialog. + :param center_content: Whether to center the content or not. + :param custom_title: A custom title for the dialog. + :param custom_color: A custom color for the dialog. + :param padding_top: The number of empty lines to print before the dialog. + :param padding_bottom: The number of empty lines to print after the dialog. + """ dialog_color = Logger._get_dialog_color(title, custom_color) dialog_title = Logger._get_dialog_title(title, custom_title) dialog_title_formatted = Logger._format_dialog_title(dialog_title) From 32742943a0d4f87ee4416a8cc27d5393c059ee3b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 27 Jul 2024 22:23:06 +0200 Subject: [PATCH 293/296] refactor: start at index 1 in moonraker setup dialog if multi instance Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_remove.py | 6 +-- .../components/moonraker/moonraker_dialogs.py | 2 +- .../components/moonraker/moonraker_remove.py | 6 +-- kiauh/components/moonraker/moonraker_setup.py | 49 ++++++++++--------- .../mainsail_theme_installer_extension.py | 6 +-- .../obico/moonraker_obico_extension.py | 2 +- kiauh/utils/input_utils.py | 16 ++++-- 7 files changed, 47 insertions(+), 40 deletions(-) diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index 7cf0894..da39ce9 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -52,7 +52,7 @@ def select_instances_to_remove( ) -> Union[List[Klipper], None]: start_index = 1 options = [str(i + start_index) for i in range(len(instances))] - options.extend(["a", "A", "b", "B"]) + options.extend(["a", "b"]) instance_map = {options[i]: instances[i] for i in range(len(instances))} print_instance_overview( @@ -64,9 +64,9 @@ def select_instances_to_remove( selection = get_selection_input("Select Klipper instance to remove", options) instances_to_remove = [] - if selection == "b".lower(): + if selection == "b": return None - elif selection == "a".lower(): + elif selection == "a": instances_to_remove.extend(instances) else: instances_to_remove.append(instance_map[selection]) diff --git a/kiauh/components/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py index e425c21..ded728e 100644 --- a/kiauh/components/moonraker/moonraker_dialogs.py +++ b/kiauh/components/moonraker/moonraker_dialogs.py @@ -48,7 +48,7 @@ def print_moonraker_overview( for i, k in enumerate(instance_map): mr_name = instance_map.get(k) m = f"<-> {mr_name}" if mr_name != "" else "" - line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {k} {m} {RESET_FORMAT}" + line = f"{COLOR_CYAN}{f'{i+1})' if show_index else '●'} {k} {m} {RESET_FORMAT}" dialog += f"║ {line:<63}║\n" warn_l1 = f"{COLOR_YELLOW}PLEASE NOTE: {RESET_FORMAT}" diff --git a/kiauh/components/moonraker/moonraker_remove.py b/kiauh/components/moonraker/moonraker_remove.py index e4dc9b1..432a63d 100644 --- a/kiauh/components/moonraker/moonraker_remove.py +++ b/kiauh/components/moonraker/moonraker_remove.py @@ -60,7 +60,7 @@ def select_instances_to_remove( ) -> Union[List[Moonraker], None]: start_index = 1 options = [str(i + start_index) for i in range(len(instances))] - options.extend(["a", "A", "b", "B"]) + options.extend(["a", "b"]) instance_map = {options[i]: instances[i] for i in range(len(instances))} print_instance_overview( @@ -72,9 +72,9 @@ def select_instances_to_remove( selection = get_selection_input("Select Moonraker instance to remove", options) instances_to_remove = [] - if selection == "b".lower(): + if selection == "b": return None - elif selection == "a".lower(): + elif selection == "a": instances_to_remove.extend(instances) else: instances_to_remove.append(instance_map[selection]) diff --git a/kiauh/components/moonraker/moonraker_setup.py b/kiauh/components/moonraker/moonraker_setup.py index 2dbfce6..9956d71 100644 --- a/kiauh/components/moonraker/moonraker_setup.py +++ b/kiauh/components/moonraker/moonraker_setup.py @@ -6,8 +6,11 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +from __future__ import annotations + import json import subprocess +from typing import List from components.klipper.klipper import Klipper from components.moonraker import ( @@ -57,36 +60,36 @@ def install_moonraker() -> None: if not check_moonraker_install_requirements(): return - kl_im = InstanceManager(Klipper) - klipper_instances = kl_im.instances + klipper_list: List[Klipper] = InstanceManager(Klipper).instances mr_im = InstanceManager(Moonraker) - moonraker_instances = mr_im.instances + moonraker_list: List[Moonraker] = mr_im.instances - selected_klipper_instance = 0 - if len(klipper_instances) > 1: + instance_names = [] + selected_option: str | Klipper + + if len(klipper_list) == 0: + instance_names.append(klipper_list[0].suffix) + else: print_moonraker_overview( - klipper_instances, - moonraker_instances, + klipper_list, + moonraker_list, show_index=True, show_select_all=True, ) - options = [str(i) for i in range(len(klipper_instances))] - options.extend(["a", "A", "b", "B"]) + 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_klipper_instance = get_selection_input(question, options).lower() + selected_option = get_selection_input(question, options) - instance_names = [] - if selected_klipper_instance == "b": - Logger.print_status(EXIT_MOONRAKER_SETUP) - return + if selected_option == "b": + Logger.print_status(EXIT_MOONRAKER_SETUP) + return - elif selected_klipper_instance == "a": - for instance in klipper_instances: - instance_names.append(instance.suffix) - - else: - index = int(selected_klipper_instance) - instance_names.append(klipper_instances[index].suffix) + if selected_option == "a": + instance_names.extend([k.suffix for k in klipper_list]) + else: + instance_names.append(options.get(selected_option).suffix) create_example_cfg = get_confirm("Create example moonraker.conf?") @@ -95,9 +98,7 @@ def install_moonraker() -> None: setup_moonraker_prerequesites() install_moonraker_polkit() - used_ports_map = { - instance.suffix: instance.port for instance in moonraker_instances - } + used_ports_map = {m.suffix: m.port for m in moonraker_list} for name in instance_names: current_instance = Moonraker(suffix=name) diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index 1807af1..1c50606 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -157,7 +157,7 @@ def get_printer_selection( instances: List[BaseInstance], is_install: bool ) -> Union[List[BaseInstance], None]: options = [str(i) for i in range(len(instances))] - options.extend(["a", "A", "b", "B"]) + options.extend(["a", "b"]) if is_install: q = "Select the printer to install the theme for" @@ -166,9 +166,9 @@ def get_printer_selection( selection = get_selection_input(q, options) install_for = [] - if selection == "b".lower(): + if selection == "b": return None - elif selection == "a".lower(): + elif selection == "a": install_for.extend(instances) else: instance = instances[int(selection)] diff --git a/kiauh/extensions/obico/moonraker_obico_extension.py b/kiauh/extensions/obico/moonraker_obico_extension.py index e07b842..2ba0638 100644 --- a/kiauh/extensions/obico/moonraker_obico_extension.py +++ b/kiauh/extensions/obico/moonraker_obico_extension.py @@ -62,7 +62,7 @@ class ObicoExtension(BaseExtension): obico_instances: List[MoonrakerObico] = obico_im.instances if obico_instances: self._print_is_already_installed() - options = ["l", "L", "r", "R", "b", "B"] + options = ["l", "r", "b"] action = get_selection_input("Perform action", option_list=options) if action.lower() == "b": Logger.print_info("Exiting Obico for Klipper installation ...") diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 58e5ae7..cd3119d 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -9,7 +9,7 @@ from __future__ import annotations import re -from typing import List, Union +from typing import Dict, List, Union from utils import INVALID_CHOICE from utils.constants import COLOR_CYAN, RESET_FORMAT @@ -120,7 +120,7 @@ def get_string_input( Logger.print_error(INVALID_CHOICE) -def get_selection_input(question: str, option_list: List, default=None) -> str: +def get_selection_input(question: str, option_list: List | Dict, default=None) -> str: """ Helper method to get a selection from a list of options from the user :param question: The question to display @@ -129,10 +129,16 @@ def get_selection_input(question: str, option_list: List, default=None) -> str: :return: The option that was selected by the user """ while True: - _input = input(format_question(question, default)).strip() + _input = input(format_question(question, default)).strip().lower() - if _input in option_list: - return _input + if isinstance(option_list, list): + if _input in option_list: + return _input + elif isinstance(option_list, dict): + if _input in option_list.keys(): + return _input + else: + raise ValueError("Invalid option_list type") Logger.print_error(INVALID_CHOICE) From a7c67721b6dd6abd3c5d852bb731bd5a66a6647a Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 27 Jul 2024 22:30:53 +0200 Subject: [PATCH 294/296] refactor: make Moonraker to dataclass Signed-off-by: Dominik Willner --- kiauh/components/moonraker/moonraker.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py index f4db5de..f62f5bd 100644 --- a/kiauh/components/moonraker/moonraker.py +++ b/kiauh/components/moonraker/moonraker.py @@ -8,6 +8,7 @@ # ======================================================================= # from __future__ import annotations +from dataclasses import dataclass from pathlib import Path from subprocess import CalledProcessError, run @@ -28,11 +29,22 @@ from utils.logger import Logger # noinspection PyMethodMayBeStatic +@dataclass class Moonraker(BaseInstance): + moonraker_dir: Path = MOONRAKER_DIR + env_dir: Path = MOONRAKER_ENV_DIR + cfg_file: Path = None + port: int = None + backup_dir: Path = None + certs_dir: Path = None + db_dir: Path = None + log: Path = None + def __init__(self, suffix: str = ""): super().__init__(instance_type=self, suffix=suffix) - self.moonraker_dir: Path = MOONRAKER_DIR - self.env_dir: Path = MOONRAKER_ENV_DIR + + def __post_init__(self) -> None: + super().__post_init__() self.cfg_file = self.cfg_dir.joinpath(MOONRAKER_CFG_NAME) self.port = self._get_port() self.backup_dir = self.data_dir.joinpath("backup") From f578247b7473747e1149c94b97ff16768957bd6b Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 27 Jul 2024 23:02:08 +0200 Subject: [PATCH 295/296] fix: fix logic bug in conditional Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index a4f5332..c894642 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -68,7 +68,7 @@ def install_klipper() -> None: klipper_list, moonraker_list ) - if install_count == 0 or name_dict == {}: + if install_count == 0: Logger.print_status(EXIT_KLIPPER_SETUP) return From f00d41b1bfd9fe81e9c0e73016fa305f68158286 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 27 Jul 2024 23:18:32 +0200 Subject: [PATCH 296/296] fix: fix logic bug in handle_instance_names Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kiauh/components/klipper/klipper_setup.py b/kiauh/components/klipper/klipper_setup.py index c894642..414ab6f 100644 --- a/kiauh/components/klipper/klipper_setup.py +++ b/kiauh/components/klipper/klipper_setup.py @@ -118,12 +118,12 @@ def run_klipper_setup( def handle_instance_names( install_count: int, name_dict: Dict[int, str], custom_names: bool ) -> None: - for i in range(install_count): - index = len(name_dict) + i + 1 + for i in range(install_count): # 3 + key = max(name_dict.keys()) + 1 if custom_names: - assign_custom_name(index, name_dict) + assign_custom_name(key, name_dict) else: - name_dict[i + 1] = str(index) + name_dict[key] = str(len(name_dict) + 1) def get_install_count_and_name_dict(