From ce0daa52ae3fd2e66345ec76f64acb5b09010bd8 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Thu, 26 Oct 2023 13:44:43 +0200 Subject: [PATCH 001/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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 ...")