fix: fix switching of repositories (#519)

* fix: fix repo switching

Extend the functionality of repo switching by creating a backup before the switch. Also implement a rollback mechanic in case of an error.

Signed-off-by: Dominik Willner <th33xitus@gmail.com>

* refactor: fail when installing requirements fails

Signed-off-by: Dominik Willner <th33xitus@gmail.com>

* refactor: display owner and repo in main menu on separate lines

long owner and repo names would case the menu to be too wide

Signed-off-by: Dominik Willner <th33xitus@gmail.com>

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
This commit is contained in:
dw-0
2024-09-05 20:31:38 +02:00
committed by GitHub
parent e438081c35
commit a54514c400
9 changed files with 264 additions and 134 deletions

View File

@@ -17,6 +17,10 @@ from core.logger import Logger
from utils.common import get_current_date
class BackupManagerException(Exception):
pass
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class BackupManager:
@@ -65,7 +69,7 @@ class BackupManager:
def backup_directory(
self, name: str, source: Path, target: Path | None = None
) -> None:
) -> Path | None:
Logger.print_status(f"Creating backup of {name} in {target} ...")
if source is None or not Path(source).exists():
@@ -76,15 +80,15 @@ class BackupManager:
try:
date = get_current_date().get("date")
time = get_current_date().get("time")
shutil.copytree(
source,
target.joinpath(f"{name.lower()}-{date}-{time}"),
ignore=self.ignore_folders_func,
)
backup_target = target.joinpath(f"{name.lower()}-{date}-{time}")
shutil.copytree(source, backup_target, ignore=self.ignore_folders_func)
Logger.print_ok("Backup successful!")
return backup_target
except OSError as e:
Logger.print_error(f"Unable to backup directory '{source}':\n{e}")
return
raise BackupManagerException(f"Unable to backup directory '{source}':\n{e}")
def ignore_folders_func(self, dirpath, filenames) -> List[str]:
return (

View File

@@ -57,7 +57,8 @@ class MainMenu(BaseMenu):
self.footer_type: FooterType = FooterType.QUIT
self.version = ""
self.kl_status = self.kl_repo = self.mr_status = self.mr_repo = ""
self.kl_status = self.kl_owner = self.kl_repo = ""
self.mr_status = self.mr_owner = self.mr_repo = ""
self.ms_status = self.fl_status = self.ks_status = self.mb_status = ""
self.cn_status = self.cc_status = self.oe_status = ""
self._init_status()
@@ -103,6 +104,7 @@ class MainMenu(BaseMenu):
status_data: ComponentStatus = status_fn(*args)
code: int = status_data.status
status: StatusText = StatusMap[code]
owner: str = status_data.owner
repo: str = status_data.repo
instance_count: int = status_data.instances
@@ -111,6 +113,7 @@ class MainMenu(BaseMenu):
count_txt = f": {instance_count}"
setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt))
setattr(self, f"{name}_owner", f"{COLOR_CYAN}{owner}{RESET_FORMAT}")
setattr(self, f"{name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT}")
def _format_by_code(self, code: int, status: str, count: str) -> str:
@@ -140,17 +143,19 @@ class MainMenu(BaseMenu):
{color}{header:~^{count}}{RESET_FORMAT}
╟──────────────────┬────────────────────────────────────╢
║ 0) [Log-Upload] │ Klipper: {self.kl_status:<{pad1}}
║ │ Repo: {self.kl_repo:<{pad1}}
║ 1) [Install] ├────────────────────────────────────╢
║ 2) [Update] │ Moonraker: {self.mr_status:<{pad1}}
║ 3) [Remove] │ Repo: {self.mr_repo:<{pad1}}
║ 4) [Advanced] ├────────────────────────────────────╢
║ 5) [Backup] │ Mainsail: {self.ms_status:<{pad2}}
║ │ Owner: {self.kl_owner:<{pad1}}
║ 1) [Install] │ Repo: {self.kl_repo:<{pad1}}
║ 2) [Update] ├────────────────────────────────────╢
║ 3) [Remove] │ Moonraker: {self.mr_status:<{pad1}}
║ 4) [Advanced] │ Owner: {self.mr_owner:<{pad1}}
║ 5) [Backup] │ Repo: {self.mr_repo:<{pad1}}
║ ├────────────────────────────────────╢
║ S) [Settings] │ Mainsail: {self.ms_status:<{pad2}}
║ │ Fluidd: {self.fl_status:<{pad2}}
S) [Settings] │ Client-Config: {self.cc_status:<{pad2}}
│ ║
Community: │ KlipperScreen: {self.ks_status:<{pad2}}
E) [Extensions] │ Mobileraker: {self.mb_status:<{pad2}}
Community: │ Client-Config: {self.cc_status:<{pad2}}
E) [Extensions] │ ║
│ KlipperScreen: {self.ks_status:<{pad2}}
│ Mobileraker: {self.mb_status:<{pad2}}
║ │ OctoEverywhere: {self.oe_status:<{pad2}}
║ │ Crowsnest: {self.cn_status:<{pad2}}
╟──────────────────┼────────────────────────────────────╢

View File

@@ -8,24 +8,16 @@
# ======================================================================= #
from __future__ import annotations
import shutil
import textwrap
from pathlib import Path
from typing import Tuple, Type
from typing import Literal, Tuple, Type
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.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger
from core.menus import Option
from core.menus.base_menu import BaseMenu
from core.settings.kiauh_settings import KiauhSettings
from utils.git_utils import git_clone_wrapper
from core.settings.kiauh_settings import KiauhSettings, RepoSettings
from procedures.switch_repo import run_switch_repo_routine
from utils.input_utils import get_confirm, get_string_input
from utils.instance_utils import get_instances
# noinspection PyUnusedLocal
@@ -105,22 +97,28 @@ class SettingsMenu(BaseMenu):
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.settings.get(repo_name, "repo_url")
repo = f"{'/'.join(repo.rsplit('/', 2)[-2:])}"
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}")
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,
[
"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!"
"There is no input validation in place! Make sure your the input is "
"valid and has no typos or invalid characters! For the change to take "
"effect, the new repository will be cloned. A backup of the old "
"repository will be created.",
"\n\n",
"Make sure you don't have any ongoing prints running, as the services "
"will be restarted during this process! You will loose any ongoing print!",
],
)
repo = get_string_input(
@@ -134,7 +132,7 @@ class SettingsMenu(BaseMenu):
return repo, branch
def _set_repo(self, repo_name: str) -> None:
def _set_repo(self, repo_name: Literal["klipper", "moonraker"]) -> None:
repo_url, branch = self._gather_input()
display_name = repo_name.capitalize()
Logger.print_dialog(
@@ -148,10 +146,13 @@ class SettingsMenu(BaseMenu):
)
if get_confirm("Apply changes?", allow_go_back=True):
self.settings.set(repo_name, "repo_url", repo_url)
self.settings.set(repo_name, "branch", branch)
repo: RepoSettings = self.settings[repo_name]
repo.repo_url = repo_url
repo.branch = branch
self.settings.save()
self._load_settings()
Logger.print_ok("Changes saved!")
else:
Logger.print_info(
@@ -161,31 +162,10 @@ class SettingsMenu(BaseMenu):
Logger.print_status(f"Switching to {display_name}'s new source repository ...")
self._switch_repo(repo_name)
Logger.print_ok(f"Switched to {repo_url} 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)
instances = get_instances(_type)
InstanceManager.stop_all(instances)
repo = self.settings.get(name, "repo_url")
branch = self.settings.get(name, "branch")
git_clone_wrapper(repo, target_dir, branch)
InstanceManager.start_all(instances)
def _switch_repo(self, name: Literal["klipper", "moonraker"]) -> None:
repo: RepoSettings = self.settings[name]
run_switch_repo_routine(name, repo)
def set_klipper_repo(self, **kwargs) -> None:
self._set_repo("klipper")

View File

@@ -8,6 +8,9 @@
# ======================================================================= #
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from core.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
NoOptionError,
@@ -22,33 +25,21 @@ DEFAULT_CFG = PROJECT_ROOT.joinpath("default.kiauh.cfg")
CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg")
@dataclass
class AppSettings:
def __init__(self) -> None:
self.backup_before_update = None
backup_before_update: bool | None = field(default=None)
class KlipperSettings:
def __init__(self) -> None:
self.repo_url = None
self.branch = None
@dataclass
class RepoSettings:
repo_url: str | None = field(default=None)
branch: str | None = field(default=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
@dataclass
class WebUiSettings:
port: str | None = field(default=None)
unstable_releases: bool | None = field(default=None)
# noinspection PyUnusedLocal
@@ -61,6 +52,12 @@ class KiauhSettings:
cls._instance = super(KiauhSettings, cls).__new__(cls, *args, **kwargs)
return cls._instance
def __repr__(self) -> str:
return f"KiauhSettings(kiauh={self.kiauh}, klipper={self.klipper}, moonraker={self.moonraker}, mainsail={self.mainsail}, fluidd={self.fluidd})"
def __getitem__(self, item: str) -> Any:
return getattr(self, item)
def __init__(self) -> None:
if not hasattr(self, "__initialized"):
self.__initialized = False
@@ -69,20 +66,10 @@ class KiauhSettings:
self.__initialized = True
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.klipper = RepoSettings()
self.moonraker = RepoSettings()
self.mainsail = WebUiSettings()
self.fluidd = WebUiSettings()
self._load_config()
@@ -102,22 +89,8 @@ class KiauhSettings:
except AttributeError:
raise
def set(self, section: str, option: str, value: str | int | bool) -> None:
"""
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 # type: ignore
except AttributeError:
raise
def save(self) -> None:
self._set_config_options()
self._set_config_options_state()
self.config.write(CUSTOM_CFG)
self._load_config()
@@ -129,7 +102,7 @@ class KiauhSettings:
self.config.read(cfg)
self._validate_cfg()
self._read_settings()
self._apply_settings_from_file()
def _validate_cfg(self) -> None:
try:
@@ -171,7 +144,7 @@ class KiauhSettings:
if v.isdigit() or v.lower() == "true" or v.lower() == "false":
raise ValueError
def _read_settings(self) -> None:
def _apply_settings_from_file(self) -> None:
self.kiauh.backup_before_update = self.config.getboolean(
"kiauh", "backup_before_update"
)
@@ -188,7 +161,7 @@ class KiauhSettings:
"fluidd", "unstable_releases"
)
def _set_config_options(self) -> None:
def _set_config_options_state(self) -> None:
self.config.set(
"kiauh",
"backup_before_update",

View File

@@ -23,6 +23,7 @@ StatusMap: Dict[StatusCode, StatusText] = {
@dataclass
class ComponentStatus:
status: StatusCode
owner: str | None = None
repo: str | None = None
local: str | None = None
remote: str | None = None