Compare commits

...

6 Commits

Author SHA1 Message Date
dw-0
a63cf8c9d9 Release v6.0.0-alpha.9
Merge develop into master (Release v6.0.0-alpha.9)
2024-10-24 12:29:24 +02:00
dw-0
02ed3e7da0 feat: show actual current branch in settings menu (#588)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:26:18 +02:00
dw-0
4427ae94af fix: make sure all output is utf-8 encoded (#587)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:26:01 +02:00
dw-0
81b7b156b9 feat: implement port reconfiguration for webclients (#586)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:25:44 +02:00
dw-0
2df364512b fix: Path.rename() not working across devices (#584)
causes `[Errno 18] Invalid cross-device link` on tmpfs filesystems

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:25:22 +02:00
dw-0
dfa0036326 refactor: don't clear scrollback on clear
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:24:05 +02:00
14 changed files with 281 additions and 89 deletions

View File

@@ -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()

View File

@@ -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"
}

View File

@@ -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,6 +56,7 @@ def install_client_config(client_data: BaseWebClient) -> None:
download_client_config(client_config)
create_client_config_symlink(client_config, kl_instances)
if cfg_backup:
backup_printer_config_dir()
add_config_section(

View File

@@ -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],
]
)

View File

@@ -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,7 +95,8 @@ def install_client(client: BaseWebClient) -> None:
download_client(client)
if enable_remotemode and client.client == WebClientType.MAINSAIL:
enable_mainsail_remotemode()
if mr_instances:
backup_printer_config_dir()
add_config_section(
section=f"update_manager {client.name}",
instances=mr_instances,
@@ -108,8 +108,9 @@ def install_client(client: BaseWebClient) -> None:
],
)
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:

View File

@@ -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)

View File

@@ -0,0 +1,104 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# 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()

View File

@@ -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:

View File

@@ -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()

View File

@@ -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,

View File

@@ -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

View File

@@ -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),
)

View File

@@ -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!")

View File

@@ -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