diff --git a/kiauh.py b/kiauh.py index ff930a4..85e041c 100644 --- a/kiauh.py +++ b/kiauh.py @@ -9,7 +9,13 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import io +import sys + from kiauh.main import main +# ensure that all output is utf-8 encoded +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + if __name__ == "__main__": main() diff --git a/kiauh.sh b/kiauh.sh index 4ecf842..7aec82d 100755 --- a/kiauh.sh +++ b/kiauh.sh @@ -10,7 +10,7 @@ #=======================================================================# set -e -clear +clear -x # make sure we have the correct permissions while running the script umask 022 @@ -110,7 +110,7 @@ function launch_kiauh_v6() { export PYTHONPATH="${entrypoint}" - clear + clear -x python3 "${entrypoint}/kiauh.py" } 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 1e9a54c..905ad8e 100644 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ b/kiauh/components/webui_client/client_config/client_config_setup.py @@ -34,7 +34,7 @@ from utils.input_utils import get_confirm from utils.instance_utils import get_instances -def install_client_config(client_data: BaseWebClient) -> None: +def install_client_config(client_data: BaseWebClient, cfg_backup=True) -> None: client_config: BaseWebClientConfig = client_data.client_config display_name = client_config.display_name @@ -56,7 +56,8 @@ def install_client_config(client_data: BaseWebClient) -> None: download_client_config(client_config) create_client_config_symlink(client_config, kl_instances) - backup_printer_config_dir() + if cfg_backup: + backup_printer_config_dir() add_config_section( section=f"update_manager {client_config.name}", diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py index e6fd5ab..0bb889c 100644 --- a/kiauh/components/webui_client/client_dialogs.py +++ b/kiauh/components/webui_client/client_dialogs.py @@ -53,8 +53,8 @@ def print_client_port_select_dialog( dialog_content.extend( [ "\n\n", - "The following ports were found to be in use already:", - *[f"● {port}" for port in ports_in_use], + "The following ports were found to be already in use:", + *[f"● {p}" for p in ports_in_use if p != port], ] ) diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 7db8218..89bcac8 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -36,8 +36,9 @@ from components.webui_client.client_utils import ( symlink_webui_nginx_log, ) from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from utils.common import check_install_dependencies +from core.logger import DialogCustomColor, DialogType, Logger +from core.settings.kiauh_settings import KiauhSettings +from utils.common import backup_printer_config_dir, check_install_dependencies from utils.config_utils import add_config_section from utils.fs_utils import unzip from utils.input_utils import get_confirm @@ -49,16 +50,11 @@ from utils.sys_utils import ( ) -def install_client(client: BaseWebClient) -> None: - if client is None: - raise ValueError("Missing parameter client_data!") - - if client.client_dir.exists(): - Logger.print_info( - f"{client.display_name} seems to be already installed! Skipped ..." - ) - return - +def install_client( + client: BaseWebClient, + settings: KiauhSettings, + reinstall: bool = False, +) -> None: mr_instances: List[Moonraker] = get_instances(Moonraker) enable_remotemode = False @@ -88,7 +84,10 @@ 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) - port: int = get_client_port_selection(client) + default_port: int = int(settings.get(client.name, "port")) + port: int = ( + default_port if reinstall else get_client_port_selection(client, settings) + ) check_install_dependencies({"nginx"}) @@ -96,20 +95,22 @@ def install_client(client: BaseWebClient) -> None: download_client(client) if enable_remotemode and client.client == WebClientType.MAINSAIL: enable_mainsail_remotemode() - if mr_instances: - add_config_section( - section=f"update_manager {client.name}", - instances=mr_instances, - options=[ - ("type", "web"), - ("channel", "stable"), - ("repo", str(client.repo_path)), - ("path", str(client.client_dir)), - ], - ) - InstanceManager.restart_all(mr_instances) + + backup_printer_config_dir() + add_config_section( + section=f"update_manager {client.name}", + instances=mr_instances, + options=[ + ("type", "web"), + ("channel", "stable"), + ("repo", str(client.repo_path)), + ("path", str(client.client_dir)), + ], + ) + InstanceManager.restart_all(mr_instances) + if install_client_cfg and kl_instances: - install_client_config(client) + install_client_config(client, False) copy_upstream_nginx_cfg() copy_common_vars_nginx_cfg() @@ -127,12 +128,24 @@ def install_client(client: BaseWebClient) -> None: cmd_sysctl_service("nginx", "restart") except Exception as e: - Logger.print_error(f"{client.display_name} installation failed!\n{e}") + Logger.print_error(e) + Logger.print_dialog( + DialogType.ERROR, + center_content=True, + content=[f"{client.display_name} installation failed!"], + ) return - 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") + # noinspection HttpUrlsUsage + Logger.print_dialog( + DialogType.CUSTOM, + custom_title=f"{client.display_name} installation complete!", + custom_color=DialogCustomColor.GREEN, + center_content=True, + content=[ + f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}", + ], + ) def download_client(client: BaseWebClient) -> None: diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 206f0b2..0141160 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -370,19 +370,26 @@ def read_ports_from_nginx_configs() -> List[int]: return sorted(ports_to_ints_list, key=lambda x: int(x)) -def get_client_port_selection(client: BaseWebClient) -> int: - settings = KiauhSettings() +def get_client_port_selection( + client: BaseWebClient, + settings: KiauhSettings, + reconfigure=False, +) -> int: default_port: int = int(settings.get(client.name, "port")) - ports_in_use: List[int] = read_ports_from_nginx_configs() next_free_port: int = get_next_free_port(ports_in_use) - port: int = next_free_port if default_port in ports_in_use else default_port + port: int = ( + next_free_port + if not reconfigure and default_port in ports_in_use + else default_port + ) print_client_port_select_dialog(client.display_name, port, ports_in_use) while True: - question = f"Configure {client.display_name} for port" + _type = "Reconfigure" if reconfigure else "Configure" + question = f"{_type} {client.display_name} for port" port_input = get_number_input(question, min_count=80, default=port) if port_input not in ports_in_use: @@ -400,3 +407,23 @@ def get_next_free_port(ports_in_use: List[int]) -> int: used_ports = set(map(int, ports_in_use)) return min(valid_ports - used_ports) + + +def set_listen_port(client: BaseWebClient, curr_port: int, new_port: int) -> None: + """ + Set the port the client should listen on in the NGINX config + :param curr_port: The current port the client listens on + :param new_port: The new port to set + :param client: The client to set the port for + :return: None + """ + config = NGINX_SITES_AVAILABLE.joinpath(client.name) + with open(config, "r") as f: + lines = f.readlines() + + for i, line in enumerate(lines): + if "listen" in line: + lines[i] = line.replace(str(curr_port), str(new_port)) + + with open(config, "w") as f: + f.writelines(lines) diff --git a/kiauh/components/webui_client/menus/client_install_menu.py b/kiauh/components/webui_client/menus/client_install_menu.py new file mode 100644 index 0000000..a741888 --- /dev/null +++ b/kiauh/components/webui_client/menus/client_install_menu.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 # +# ======================================================================= # +from __future__ import annotations + +import textwrap +from typing import Type + +from components.webui_client.base_data import BaseWebClient +from components.webui_client.client_setup import install_client +from components.webui_client.client_utils import ( + get_client_port_selection, + set_listen_port, +) +from core.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT +from core.logger import DialogCustomColor, DialogType, Logger +from core.menus import Option +from core.menus.base_menu import BaseMenu +from core.settings.kiauh_settings import KiauhSettings, WebUiSettings +from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr + + +# noinspection PyUnusedLocal +class ClientInstallMenu(BaseMenu): + def __init__( + self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None + ): + super().__init__() + self.previous_menu: Type[BaseMenu] | None = previous_menu + self.client: BaseWebClient = client + self.settings = KiauhSettings() + self.client_settings: WebUiSettings = self.settings[client.name] + + def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: + from core.menus.install_menu import InstallMenu + + self.previous_menu = previous_menu if previous_menu is not None else InstallMenu + + def set_options(self) -> None: + self.options = { + "1": Option(method=self.reinstall_client), + "2": Option(method=self.change_listen_port), + } + + def print_menu(self) -> None: + client_name = self.client.display_name + + header = f" [ Installation Menu > {client_name} ] " + color = COLOR_GREEN + count = 62 - len(color) - len(RESET_FORMAT) + port = f"(Current: {COLOR_CYAN}{int(self.client_settings.port)}{RESET_FORMAT})" + menu = textwrap.dedent( + f""" + ╔═══════════════════════════════════════════════════════╗ + ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + ╟───────────────────────────────────────────────────────╢ + ║ 1) Reinstall {client_name:16} ║ + ║ 2) Reconfigure Listen Port {port:<34} ║ + ╟───────────────────────────────────────────────────────╢ + """ + )[1:] + print(menu, end="") + + def reinstall_client(self, **kwargs) -> None: + install_client(self.client, settings=self.settings, reinstall=True) + + def change_listen_port(self, **kwargs) -> None: + curr_port = int(self.client_settings.port) + new_port = get_client_port_selection( + self.client, + self.settings, + reconfigure=True, + ) + + cmd_sysctl_service("nginx", "stop") + set_listen_port(self.client, curr_port, new_port) + + Logger.print_status("Saving new port configuration ...") + self.client_settings.port = new_port + self.settings.save() + Logger.print_ok("Port configuration saved!") + + cmd_sysctl_service("nginx", "start") + + # noinspection HttpUrlsUsage + Logger.print_dialog( + DialogType.CUSTOM, + custom_title="Port reconfiguration complete!", + custom_color=DialogCustomColor.GREEN, + center_content=True, + content=[ + f"Open {self.client.display_name} now on: " + f"http://{get_ipv4_addr()}:{new_port}", + ], + ) + + def _go_back(self, **kwargs) -> None: + if self.previous_menu is not None: + self.previous_menu().run() diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 4963d93..a97f3a1 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -29,7 +29,7 @@ from utils.input_utils import get_selection_input def clear() -> None: - subprocess.call("clear", shell=True) + subprocess.call("clear -x", shell=True) def print_header() -> None: diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index 8318006..b3f7576 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -15,13 +15,17 @@ 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 +from components.webui_client.client_config.client_config_setup import ( + install_client_config, +) +from components.webui_client.client_setup import install_client from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData +from components.webui_client.menus.client_install_menu import ClientInstallMenu from core.constants import COLOR_GREEN, RESET_FORMAT from core.menus import Option from core.menus.base_menu import BaseMenu +from core.settings.kiauh_settings import KiauhSettings # noinspection PyUnusedLocal @@ -80,16 +84,24 @@ class InstallMenu(BaseMenu): moonraker_setup.install_moonraker() def install_mainsail(self, **kwargs) -> None: - client_setup.install_client(MainsailData()) + client: MainsailData = MainsailData() + if client.client_dir.exists(): + ClientInstallMenu(client, self.__class__).run() + else: + install_client(client, settings=KiauhSettings()) def install_mainsail_config(self, **kwargs) -> None: - client_config_setup.install_client_config(MainsailData()) + install_client_config(MainsailData()) def install_fluidd(self, **kwargs) -> None: - client_setup.install_client(FluiddData()) + client: FluiddData = FluiddData() + if client.client_dir.exists(): + ClientInstallMenu(client, self.__class__).run() + else: + install_client(client, settings=KiauhSettings()) def install_fluidd_config(self, **kwargs) -> None: - client_config_setup.install_client_config(FluiddData()) + install_client_config(FluiddData()) def install_klipperscreen(self, **kwargs) -> None: install_klipperscreen() diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 86f8007..dd67d2d 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -11,6 +11,8 @@ from __future__ import annotations import textwrap from typing import Literal, Tuple, Type +from components.klipper.klipper_utils import get_klipper_status +from components.moonraker.moonraker_utils import get_moonraker_status from core.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT from core.logger import DialogType, Logger from core.menus import Option @@ -26,8 +28,8 @@ class SettingsMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: super().__init__() self.previous_menu: Type[BaseMenu] | None = previous_menu - self.klipper_repo: str | None = None - self.moonraker_repo: str | None = None + self.klipper_status = get_klipper_status() + self.moonraker_status = get_moonraker_status() self.mainsail_unstable: bool | None = None self.fluidd_unstable: bool | None = None self.auto_backups_enabled: bool | None = None @@ -49,31 +51,41 @@ class SettingsMenu(BaseMenu): def print_menu(self) -> None: header = " [ KIAUH Settings ] " - color = COLOR_CYAN - count = 62 - len(color) - len(RESET_FORMAT) - checked = f"[{COLOR_GREEN}x{RESET_FORMAT}]" + color, rst = COLOR_CYAN, RESET_FORMAT + count = 62 - len(color) - len(rst) + checked = f"[{COLOR_GREEN}x{rst}]" unchecked = "[ ]" + + kl_repo: str = f"{color}{self.klipper_status.repo}{rst}" + kl_branch: str = f"{color}{self.klipper_status.branch}{rst}" + kl_owner: str = f"{color}{self.klipper_status.owner}{rst}" + mr_repo: str = f"{color}{self.moonraker_status.repo}{rst}" + mr_branch: str = f"{color}{self.moonraker_status.branch}{rst}" + mr_owner: str = f"{color}{self.moonraker_status.owner}{rst}" 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} ║ + ║ {color}{header:~^{count}}{rst} ║ ╟───────────────────────────────────────────────────────╢ - ║ Klipper source repository: ║ - ║ ● {self.klipper_repo:<67} ║ - ║ ║ - ║ Moonraker source repository: ║ - ║ ● {self.moonraker_repo:<67} ║ - ║ ║ - ║ Install unstable Webinterface releases: ║ + ║ Klipper: ║ + ║ ● Repo: {kl_repo:51} ║ + ║ ● Owner: {kl_owner:51} ║ + ║ ● Branch: {kl_branch:51} ║ + ╟───────────────────────────────────────────────────────╢ + ║ Moonraker: ║ + ║ ● Repo: {mr_repo:51} ║ + ║ ● Owner: {mr_owner:51} ║ + ║ ● Branch: {mr_branch:51} ║ + ╟───────────────────────────────────────────────────────╢ + ║ Install unstable releases: ║ ║ {o1} Mainsail ║ ║ {o2} Fluidd ║ - ║ ║ + ╟───────────────────────────────────────────────────────╢ ║ Auto-Backup: ║ ║ {o3} Automatic backup before update ║ - ║ ║ ╟───────────────────────────────────────────────────────╢ ║ 1) Set Klipper source repository ║ ║ 2) Set Moonraker source repository ║ @@ -89,25 +101,10 @@ class SettingsMenu(BaseMenu): def _load_settings(self) -> None: self.settings = KiauhSettings() - - self._format_repo_str("klipper") - self._format_repo_str("moonraker") - 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: Literal["klipper", "moonraker"]) -> None: - repo: RepoSettings = self.settings[repo_name] - repo_str = f"{'/'.join(repo.repo_url.rsplit('/', 2)[-2:])}" - branch_str = f"({COLOR_CYAN}@ {repo.branch}{RESET_FORMAT})" - - setattr( - self, - f"{repo_name}_repo", - f"{COLOR_CYAN}{repo_str}{RESET_FORMAT} {branch_str}", - ) - def _gather_input(self) -> Tuple[str, str]: Logger.print_dialog( DialogType.ATTENTION, diff --git a/kiauh/core/types.py b/kiauh/core/types.py index 6f5a0e6..191a8b8 100644 --- a/kiauh/core/types.py +++ b/kiauh/core/types.py @@ -25,6 +25,7 @@ class ComponentStatus: status: StatusCode owner: str | None = None repo: str | None = None + branch: str = "" local: str | None = None remote: str | None = None instances: int | None = None diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 796839d..97dcff7 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -24,6 +24,7 @@ from core.constants import ( from core.logger import DialogType, Logger from core.types import ComponentStatus, StatusCode from utils.git_utils import ( + get_current_branch, get_local_commit, get_local_tags, get_remote_commit, @@ -103,7 +104,12 @@ def get_install_status( """ from utils.instance_utils import get_instances - checks = [repo_dir.exists()] + checks = [] + branch: str = "" + + if repo_dir.exists(): + checks.append(True) + branch = get_current_branch(repo_dir) if env_dir is not None: checks.append(env_dir.exists()) @@ -131,6 +137,7 @@ def get_install_status( instances=instances, owner=org, repo=repo, + branch=branch, local=get_local_commit(repo_dir), remote=get_remote_commit(repo_dir), ) diff --git a/kiauh/utils/config_utils.py b/kiauh/utils/config_utils.py index fd13ce0..e9bbf14 100644 --- a/kiauh/utils/config_utils.py +++ b/kiauh/utils/config_utils.py @@ -8,6 +8,7 @@ # ======================================================================= # from __future__ import annotations +import shutil import tempfile from pathlib import Path from typing import List, Tuple @@ -26,6 +27,9 @@ def add_config_section( instances: List[InstanceType], options: List[ConfigOption] | None = None, ) -> None: + if not instances: + return + for instance in instances: cfg_file = instance.cfg_file Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...") @@ -69,7 +73,7 @@ def add_config_section_at_top(section: str, instances: List[InstanceType]) -> No tmp.writelines(org_content) cfg_file.unlink() - tmp_cfg_path.rename(cfg_file) + shutil.move(tmp_cfg_path, cfg_file) Logger.print_ok("OK!") diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py index 3991dcc..ee7073e 100644 --- a/kiauh/utils/git_utils.py +++ b/kiauh/utils/git_utils.py @@ -87,12 +87,29 @@ def get_repo_name(repo: Path) -> Tuple[str, str]: orga: str = substrings[0] if substrings[0] else "-" name: str = substrings[1] if substrings[1] else "-" - return orga, name + return orga, name.replace(".git", "") except CalledProcessError: return "-", "-" +def get_current_branch(repo: Path) -> str: + """ + Get the current branch of a local Git repository + :param repo: Path to the local Git repository + :return: Current branch + """ + try: + cmd = ["git", "branch", "--show-current"] + result: str = check_output(cmd, stderr=DEVNULL, cwd=repo).decode( + encoding="utf-8" + ) + return result.strip() + + except CalledProcessError: + return "" + + def get_local_tags(repo_path: Path, _filter: str | None = None) -> List[str]: """ Get all tags of a local Git repository @@ -209,8 +226,8 @@ def get_local_commit(repo: Path) -> str | None: return None try: - cmd = f"cd {repo} && git describe HEAD --always --tags | cut -d '-' -f 1,2" - return check_output(cmd, shell=True, text=True).strip() + cmd = "git describe HEAD --always --tags | cut -d '-' -f 1,2" + return check_output(cmd, shell=True, text=True, cwd=repo).strip() except CalledProcessError: return None @@ -220,12 +237,15 @@ def get_remote_commit(repo: Path) -> str | None: return None 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() + branch = get_current_branch(repo) + cmd = f"git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2" + return check_output( + cmd, + shell=True, + text=True, + cwd=repo, + stderr=DEVNULL, + ).strip() except CalledProcessError: return None