Compare commits

...

23 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
dw-0
425d86a12f Release v6.0.0-alpha.8
Merge develop into master (Release v6.0.0-alpha.8)
2024-10-21 19:45:55 +02:00
dw-0
ff6162d799 refactor: do not silently configure Fluidd for port 81 (#582)
* refactor: use port 80 as default for fluidd

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

* refactor: improve port selection logic, write last port selection for client to kiauh.cfg

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-21 19:30:03 +02:00
dw-0
674c174224 fix: correctly add Spoolman to moonraker.asvc (#581)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-20 18:11:30 +02:00
CODeRUS
a368331693 feat(flashing): Flash RP2040 in Boot Mode (#580)
* feat(flashing): Flash RP2040 in Boot Mode

* docs: add info about STM32 DFU and RP2040 boot modes
2024-10-18 18:56:21 +02:00
Pedro Lamas
406b64d1e5 refactor: add client name to Moonraker not found dialog (#574)
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
2024-10-14 17:18:10 +02:00
dw-0
1b5691f2f5 Release v6.0.0-alpha.7
Merge develop into master (v6.0.0-alpha.7)

fixes #561
fixes #564
fixes #565
2024-10-13 11:51:19 +02:00
dw-0
e7eae5a0d1 fix: correctly handle IPs in nginx config files when parsing ports (#568)
* chore: add jupyter files to .gitignore

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

* fix: correctly handle IPs in nginx config files when parsing ports

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-13 11:21:18 +02:00
dw-0
dc561a562c fix: always return string tuple from get_repo_name() (#567)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-13 10:27:31 +02:00
dw-0
55cfe124b2 feat: add SimplyPrint extension (#566)
* refactor: correctly sort extensions in extension menu

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

* refactor: use different name for printer_data backup dir

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

* refactor: change return type to List for moonraker_exists function

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

* feat: add SimplyPrint extension

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-13 09:35:15 +02:00
Christian Würthner
43d6598be6 fix: remove octoapp_store dir when uninstalling (#562)
Co-authored-by: dw-0 <th33xitus@gmail.com>
2024-10-05 12:35:14 +02:00
dw-0
dc026a7a2b Release v6.0.0-alpha.6
Merge develop into master (v6.0.0-alpha.6)

fixes #545
fixes #553
fixes #557
2024-10-05 08:29:40 +02:00
dw-0
ac54d04b40 fix: correctly find connected UART devices (#559)
fixes #557

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-05 08:21:39 +02:00
dw-0
c19364360c fix: correctly find connected USB DFU devices (#555)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-05 08:07:47 +02:00
dw-0
2e6c66e524 fix: allow moonraker-telegram-bot-env access to systems site-packages dir (#556)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-03 10:22:32 +02:00
Christian Würthner
cd8003add9 feat(extension): add OctoApp (#554)
* Add OctoApp to v6

* fix: set correct index to new extension

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
2024-10-03 08:51:38 +02:00
Kenneth Jiang
1f75395063 fix: self.cfg_file is already a full path (#552)
Signed-off-by: Kenneth Jiang <kenneth.jiang@gmail.com>
2024-09-29 20:33:54 +02:00
Kenneth Jiang
6e1bffa975 fix: remove "obico" from the suffix_blacklist so that it can discover its own instances. (#551)
Signed-off-by: Kenneth Jiang <kenneth.jiang@gmail.com>
2024-09-29 16:41:20 +02:00
37 changed files with 940 additions and 173 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,10 @@
.idea .idea
.vscode .vscode
.pytest_cache .pytest_cache
.jupyter
*.ipynb
*.ipynb_checkpoints
*.tmp
__pycache__ __pycache__
.kiauh-env .kiauh-env
*.code-workspace *.code-workspace

View File

@@ -14,5 +14,5 @@ port: 80
unstable_releases: False unstable_releases: False
[fluidd] [fluidd]
port: 81 port: 80
unstable_releases: False unstable_releases: False

View File

@@ -9,7 +9,13 @@
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= # # ======================================================================= #
import io
import sys
from kiauh.main import main 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__": if __name__ == "__main__":
main() main()

View File

@@ -10,7 +10,7 @@
#=======================================================================# #=======================================================================#
set -e set -e
clear clear -x
# make sure we have the correct permissions while running the script # make sure we have the correct permissions while running the script
umask 022 umask 022
@@ -110,7 +110,7 @@ function launch_kiauh_v6() {
export PYTHONPATH="${entrypoint}" export PYTHONPATH="${entrypoint}"
clear clear -x
python3 "${entrypoint}/kiauh.py" python3 "${entrypoint}/kiauh.py"
} }

View File

@@ -6,8 +6,16 @@
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= # # ======================================================================= #
import re
from subprocess import PIPE, STDOUT, CalledProcessError, Popen, check_output, run from subprocess import (
DEVNULL,
PIPE,
STDOUT,
CalledProcessError,
Popen,
check_output,
run,
)
from typing import List from typing import List
from components.klipper import KLIPPER_DIR from components.klipper import KLIPPER_DIR
@@ -32,16 +40,18 @@ def find_firmware_file() -> bool:
f3 = "klipper.bin" f3 = "klipper.bin"
f4 = "klipper.uf2" f4 = "klipper.uf2"
fw_file_exists: bool = ( fw_file_exists: bool = (
target.joinpath(f1).exists() and target.joinpath(f2).exists() (target.joinpath(f1).exists() and target.joinpath(f2).exists())
) or target.joinpath(f3).exists() or target.joinpath(f4).exists() or target.joinpath(f3).exists()
or target.joinpath(f4).exists()
)
return target_exists and fw_file_exists return target_exists and fw_file_exists
def find_usb_device_by_id() -> List[str]: def find_usb_device_by_id() -> List[str]:
try: try:
command = "find /dev/serial/by-id/* 2>/dev/null" command = "find /dev/serial/by-id/*"
output = check_output(command, shell=True, text=True) output = check_output(command, shell=True, text=True, stderr=DEVNULL)
return output.splitlines() return output.splitlines()
except CalledProcessError as e: except CalledProcessError as e:
Logger.print_error("Unable to find a USB device!") Logger.print_error("Unable to find a USB device!")
@@ -51,9 +61,14 @@ def find_usb_device_by_id() -> List[str]:
def find_uart_device() -> List[str]: def find_uart_device() -> List[str]:
try: try:
command = '"find /dev -maxdepth 1 -regextype posix-extended -regex "^\/dev\/tty(AMA0|S0)$" 2>/dev/null"' cmd = "find /dev -maxdepth 1"
output = check_output(command, shell=True, text=True) output = check_output(cmd, shell=True, text=True, stderr=DEVNULL)
return output.splitlines() device_list = []
if output:
pattern = r"^/dev/tty(AMA0|S0)$"
devices = output.splitlines()
device_list = [d for d in devices if re.search(pattern, d)]
return device_list
except CalledProcessError as e: except CalledProcessError as e:
Logger.print_error("Unable to find a UART device!") Logger.print_error("Unable to find a UART device!")
Logger.print_error(e, prefix=False) Logger.print_error(e, prefix=False)
@@ -62,15 +77,34 @@ def find_uart_device() -> List[str]:
def find_usb_dfu_device() -> List[str]: def find_usb_dfu_device() -> List[str]:
try: try:
command = '"lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null"' output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL)
output = check_output(command, shell=True, text=True) device_list = []
return output.splitlines() if output:
devices = output.splitlines()
device_list = [d.split(" ")[5] for d in devices if "DFU" in d]
return device_list
except CalledProcessError as e: except CalledProcessError as e:
Logger.print_error("Unable to find a USB DFU device!") Logger.print_error("Unable to find a USB DFU device!")
Logger.print_error(e, prefix=False) Logger.print_error(e, prefix=False)
return [] return []
def find_usb_rp2_boot_device() -> List[str]:
try:
output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL)
device_list = []
if output:
devices = output.splitlines()
device_list = [d.split(" ")[5] for d in devices if "RP2 Boot" in d]
return device_list
except CalledProcessError as e:
Logger.print_error("Unable to find a USB RP2 Boot device!")
Logger.print_error(e, prefix=False)
return []
def get_sd_flash_board_list() -> List[str]: def get_sd_flash_board_list() -> List[str]:
if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists(): if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists():
return [] return []

View File

@@ -26,6 +26,7 @@ class FlashCommand(Enum):
class ConnectionType(Enum): class ConnectionType(Enum):
USB = "USB" USB = "USB"
USB_DFU = "USB (DFU)" USB_DFU = "USB (DFU)"
USB_RP2040 = "USB (RP2040)"
UART = "UART" UART = "UART"

View File

@@ -143,6 +143,8 @@ class KlipperMcuConnectionHelpMenu(BaseMenu):
count = 62 - len(color) - len(RESET_FORMAT) count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}" subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}" subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}"
subheader3 = f"{COLOR_CYAN}USB DFU:{RESET_FORMAT}"
subheader4 = f"{COLOR_CYAN}USB RP2040 Boot:{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗
@@ -164,6 +166,19 @@ class KlipperMcuConnectionHelpMenu(BaseMenu):
║ port your controller board is connected to when using ║ ║ port your controller board is connected to when using ║
║ this connection method. ║ ║ this connection method. ║
║ ║ ║ ║
{subheader3:<62}
║ Selecting USB DFU as the connection method will scan ║
║ the USB ports for connected controller boards in ║
║ STM32 DFU mode, which is usually done by holding down ║
║ the BOOT button or setting a special jumper on the ║
║ board before powering up. ║
║ ║
{subheader4:<62}
║ Selecting USB RP2 Boot as the connection method will ║
║ scan the USB ports for connected RP2040 controller ║
║ boards in Boot mode, which is usually done by holding ║
║ down the BOOT button before powering up. ║
║ ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
""" """
)[1:] )[1:]

View File

@@ -17,6 +17,7 @@ from components.klipper_firmware.firmware_utils import (
find_uart_device, find_uart_device,
find_usb_device_by_id, find_usb_device_by_id,
find_usb_dfu_device, find_usb_dfu_device,
find_usb_rp2_boot_device,
get_sd_flash_board_list, get_sd_flash_board_list,
start_flash_process, start_flash_process,
) )
@@ -177,6 +178,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
"1": Option(method=self.select_usb), "1": Option(method=self.select_usb),
"2": Option(method=self.select_dfu), "2": Option(method=self.select_dfu),
"3": Option(method=self.select_usb_dfu), "3": Option(method=self.select_usb_dfu),
"4": Option(method=self.select_usb_rp2040),
} }
def print_menu(self) -> None: def print_menu(self) -> None:
@@ -193,6 +195,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
║ 1) USB ║ ║ 1) USB ║
║ 2) UART ║ ║ 2) UART ║
║ 3) USB (DFU mode) ║ ║ 3) USB (DFU mode) ║
║ 4) USB (RP2040 mode) ║
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢
""" """
)[1:] )[1:]
@@ -210,6 +213,10 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
self.flash_options.connection_type = ConnectionType.USB_DFU self.flash_options.connection_type = ConnectionType.USB_DFU
self.get_mcu_list() self.get_mcu_list()
def select_usb_rp2040(self, **kwargs):
self.flash_options.connection_type = ConnectionType.USB_RP2040
self.get_mcu_list()
def get_mcu_list(self, **kwargs): def get_mcu_list(self, **kwargs):
conn_type = self.flash_options.connection_type conn_type = self.flash_options.connection_type
@@ -222,6 +229,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
elif conn_type is ConnectionType.USB_DFU: elif conn_type is ConnectionType.USB_DFU:
Logger.print_status("Identifying MCU connected via USB in DFU mode ...") Logger.print_status("Identifying MCU connected via USB in DFU mode ...")
self.flash_options.mcu_list = find_usb_dfu_device() self.flash_options.mcu_list = find_usb_dfu_device()
elif conn_type is ConnectionType.USB_RP2040:
Logger.print_status("Identifying MCU connected via USB in RP2 Boot mode ...")
self.flash_options.mcu_list = find_usb_rp2_boot_device()
if len(self.flash_options.mcu_list) < 1: if len(self.flash_options.mcu_list) < 1:
Logger.print_warn("No MCUs found!") Logger.print_warn("No MCUs found!")

View File

@@ -34,7 +34,7 @@ from utils.input_utils import get_confirm
from utils.instance_utils import get_instances 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 client_config: BaseWebClientConfig = client_data.client_config
display_name = client_config.display_name display_name = client_config.display_name
@@ -56,7 +56,8 @@ def install_client_config(client_data: BaseWebClient) -> None:
download_client_config(client_config) download_client_config(client_config)
create_client_config_symlink(client_config, kl_instances) create_client_config_symlink(client_config, kl_instances)
backup_printer_config_dir() if cfg_backup:
backup_printer_config_dir()
add_config_section( add_config_section(
section=f"update_manager {client_config.name}", section=f"update_manager {client_config.name}",

View File

@@ -13,15 +13,15 @@ from components.webui_client.base_data import BaseWebClient
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
def print_moonraker_not_found_dialog() -> None: def print_moonraker_not_found_dialog(name: str) -> None:
Logger.print_dialog( Logger.print_dialog(
DialogType.WARNING, DialogType.WARNING,
[ [
"No local Moonraker installation was found!", "No local Moonraker installation was found!",
"\n\n", "\n\n",
"It is possible to install Mainsail without a local Moonraker installation. " f"It is possible to install {name} without a local Moonraker installation. "
"If you continue, you need to make sure, that Moonraker is installed on " "If you continue, you need to make sure, that Moonraker is installed on "
"another machine in your network. Otherwise Mainsail will NOT work " f"another machine in your network. Otherwise {name} will NOT work "
"correctly.", "correctly.",
], ],
) )
@@ -40,20 +40,25 @@ def print_client_already_installed_dialog(name: str) -> None:
def print_client_port_select_dialog( def print_client_port_select_dialog(
name: str, port: int, ports_in_use: List[int] name: str, port: int, ports_in_use: List[int]
) -> None: ) -> None:
Logger.print_dialog( dialog_content: List[str] = [
DialogType.CUSTOM, f"Please select the port, {name} should be served on. If your are unsure "
[ f"what to select, hit Enter to apply the suggested value of: {port}",
f"Please select the port, {name} should be served on. If your are unsure " "\n\n",
f"what to select, hit Enter to apply the suggested value of: {port}", f"In case you need {name} to be served on a specific port, you can set it "
"\n\n", f"now. Make sure that the port is not already used by another application "
f"In case you need {name} to be served on a specific port, you can set it " f"on your system!",
f"now. Make sure that the port is not already used by another application " ]
f"on your system!",
"\n\n", if ports_in_use:
"The following ports were found to be in use already:", dialog_content.extend(
*[f"{port}" for port in ports_in_use], [
], "\n\n",
) "The following ports were found to be already in use:",
*[f"{p}" for p in ports_in_use if p != port],
]
)
Logger.print_dialog(DialogType.CUSTOM, dialog_content)
def print_install_client_config_dialog(client: BaseWebClient) -> None: def print_install_client_config_dialog(client: BaseWebClient) -> None:

View File

@@ -23,7 +23,6 @@ from components.webui_client.client_config.client_config_setup import (
install_client_config, install_client_config,
) )
from components.webui_client.client_dialogs import ( from components.webui_client.client_dialogs import (
print_client_port_select_dialog,
print_install_client_config_dialog, print_install_client_config_dialog,
print_moonraker_not_found_dialog, print_moonraker_not_found_dialog,
) )
@@ -33,18 +32,16 @@ from components.webui_client.client_utils import (
create_nginx_cfg, create_nginx_cfg,
detect_client_cfg_conflict, detect_client_cfg_conflict,
enable_mainsail_remotemode, enable_mainsail_remotemode,
get_next_free_port, get_client_port_selection,
is_valid_port,
read_ports_from_nginx_configs,
symlink_webui_nginx_log, symlink_webui_nginx_log,
) )
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger from core.logger import DialogCustomColor, DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings
from utils.common import check_install_dependencies from utils.common import backup_printer_config_dir, check_install_dependencies
from utils.config_utils import add_config_section from utils.config_utils import add_config_section
from utils.fs_utils import unzip from utils.fs_utils import unzip
from utils.input_utils import get_confirm, get_number_input from utils.input_utils import get_confirm
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
from utils.sys_utils import ( from utils.sys_utils import (
cmd_sysctl_service, cmd_sysctl_service,
@@ -53,21 +50,16 @@ from utils.sys_utils import (
) )
def install_client(client: BaseWebClient) -> None: def install_client(
if client is None: client: BaseWebClient,
raise ValueError("Missing parameter client_data!") settings: KiauhSettings,
reinstall: bool = False,
if client.client_dir.exists(): ) -> None:
Logger.print_info(
f"{client.display_name} seems to be already installed! Skipped ..."
)
return
mr_instances: List[Moonraker] = get_instances(Moonraker) mr_instances: List[Moonraker] = get_instances(Moonraker)
enable_remotemode = False enable_remotemode = False
if not mr_instances: if not mr_instances:
print_moonraker_not_found_dialog() print_moonraker_not_found_dialog(client.display_name)
if not get_confirm(f"Continue {client.display_name} installation?"): if not get_confirm(f"Continue {client.display_name} installation?"):
return return
@@ -92,21 +84,10 @@ def install_client(client: BaseWebClient) -> None:
question = f"Download the recommended {client_config.display_name}?" question = f"Download the recommended {client_config.display_name}?"
install_client_cfg = get_confirm(question, allow_go_back=False) install_client_cfg = get_confirm(question, allow_go_back=False)
settings = KiauhSettings() default_port: int = int(settings.get(client.name, "port"))
port: int = settings.get(client.name, "port") port: int = (
ports_in_use: List[int] = read_ports_from_nginx_configs() default_port if reinstall else get_client_port_selection(client, settings)
)
# 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 = 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)
check_install_dependencies({"nginx"}) check_install_dependencies({"nginx"})
@@ -114,20 +95,22 @@ def install_client(client: BaseWebClient) -> None:
download_client(client) download_client(client)
if enable_remotemode and client.client == WebClientType.MAINSAIL: if enable_remotemode and client.client == WebClientType.MAINSAIL:
enable_mainsail_remotemode() enable_mainsail_remotemode()
if mr_instances:
add_config_section( backup_printer_config_dir()
section=f"update_manager {client.name}", add_config_section(
instances=mr_instances, section=f"update_manager {client.name}",
options=[ instances=mr_instances,
("type", "web"), options=[
("channel", "stable"), ("type", "web"),
("repo", str(client.repo_path)), ("channel", "stable"),
("path", str(client.client_dir)), ("repo", str(client.repo_path)),
], ("path", str(client.client_dir)),
) ],
InstanceManager.restart_all(mr_instances) )
InstanceManager.restart_all(mr_instances)
if install_client_cfg and kl_instances: if install_client_cfg and kl_instances:
install_client_config(client) install_client_config(client, False)
copy_upstream_nginx_cfg() copy_upstream_nginx_cfg()
copy_common_vars_nginx_cfg() copy_common_vars_nginx_cfg()
@@ -145,12 +128,24 @@ def install_client(client: BaseWebClient) -> None:
cmd_sysctl_service("nginx", "restart") cmd_sysctl_service("nginx", "restart")
except Exception as e: 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 return
log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}" # noinspection HttpUrlsUsage
Logger.print_ok(f"{client.display_name} installation complete!", start="\n") Logger.print_dialog(
Logger.print_ok(log, prefix=False, end="\n\n") 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: def download_client(client: BaseWebClient) -> None:

View File

@@ -21,6 +21,7 @@ from components.webui_client.base_data import (
BaseWebClient, BaseWebClient,
WebClientType, WebClientType,
) )
from components.webui_client.client_dialogs import print_client_port_select_dialog
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from core.backup_manager.backup_manager import BackupManager from core.backup_manager.backup_manager import BackupManager
@@ -33,7 +34,7 @@ from core.constants import (
RESET_FORMAT, RESET_FORMAT,
) )
from core.logger import Logger from core.logger import Logger
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings, WebUiSettings
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser, SimpleConfigParser,
) )
@@ -44,6 +45,7 @@ from utils.git_utils import (
get_latest_remote_tag, get_latest_remote_tag,
get_latest_unstable_tag, get_latest_unstable_tag,
) )
from utils.input_utils import get_number_input
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
@@ -353,17 +355,51 @@ def read_ports_from_nginx_configs() -> List[int]:
lines = cfg.readlines() lines = cfg.readlines()
for line in lines: for line in lines:
line = line.replace("default_server", "") line = re.sub(
line = re.sub(r"[;:\[\]]", "", line.strip()) r"default_server|http://|https://|[;\[\]]",
if line.startswith("listen") and line.split()[-1] not in port_list: "",
port_list.append(line.split()[-1]) line.strip(),
)
if line.startswith("listen"):
if ":" not in line:
port_list.append(line.split()[-1])
else:
port_list.append(line.split(":")[-1])
ports_to_ints_list = [int(port) for port in port_list] ports_to_ints_list = [int(port) for port in port_list]
return sorted(ports_to_ints_list, key=lambda x: int(x)) return sorted(ports_to_ints_list, key=lambda x: int(x))
def is_valid_port(port: int, ports_in_use: List[int]) -> bool: def get_client_port_selection(
return port not in ports_in_use 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 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:
_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:
client_settings: WebUiSettings = settings[client.name]
client_settings.port = port_input
settings.save()
return port_input
Logger.print_error("This port is already in use. Please select another one.")
def get_next_free_port(ports_in_use: List[int]) -> int: def get_next_free_port(ports_in_use: List[int]) -> int:
@@ -371,3 +407,23 @@ def get_next_free_port(ports_in_use: List[int]) -> int:
used_ports = set(map(int, ports_in_use)) used_ports = set(map(int, ports_in_use))
return min(valid_ports - used_ports) 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

@@ -33,7 +33,7 @@ CURRENT_USER = pwd.getpwuid(os.getuid())[0]
# dirs # dirs
SYSTEMD = Path("/etc/systemd/system") SYSTEMD = Path("/etc/systemd/system")
PRINTER_CFG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-cfg-backups") PRINTER_DATA_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-data-backups")
NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available")
NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled") NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled")
NGINX_CONFD = Path("/etc/nginx/conf.d") NGINX_CONFD = Path("/etc/nginx/conf.d")

View File

@@ -29,7 +29,7 @@ from utils.input_utils import get_selection_input
def clear() -> None: def clear() -> None:
subprocess.call("clear", shell=True) subprocess.call("clear -x", shell=True)
def print_header() -> None: 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.klipper import klipper_setup
from components.klipperscreen.klipperscreen import install_klipperscreen from components.klipperscreen.klipperscreen import install_klipperscreen
from components.moonraker import moonraker_setup from components.moonraker import moonraker_setup
from components.webui_client import client_setup from components.webui_client.client_config.client_config_setup import (
from components.webui_client.client_config import client_config_setup install_client_config,
)
from components.webui_client.client_setup import install_client
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData 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.constants import COLOR_GREEN, RESET_FORMAT
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.settings.kiauh_settings import KiauhSettings
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -80,16 +84,24 @@ class InstallMenu(BaseMenu):
moonraker_setup.install_moonraker() moonraker_setup.install_moonraker()
def install_mainsail(self, **kwargs) -> None: 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: def install_mainsail_config(self, **kwargs) -> None:
client_config_setup.install_client_config(MainsailData()) install_client_config(MainsailData())
def install_fluidd(self, **kwargs) -> None: 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: def install_fluidd_config(self, **kwargs) -> None:
client_config_setup.install_client_config(FluiddData()) install_client_config(FluiddData())
def install_klipperscreen(self, **kwargs) -> None: def install_klipperscreen(self, **kwargs) -> None:
install_klipperscreen() install_klipperscreen()

View File

@@ -11,6 +11,8 @@ from __future__ import annotations
import textwrap import textwrap
from typing import Literal, Tuple, Type 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.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.menus import Option from core.menus import Option
@@ -26,8 +28,8 @@ class SettingsMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.klipper_repo: str | None = None self.klipper_status = get_klipper_status()
self.moonraker_repo: str | None = None self.moonraker_status = get_moonraker_status()
self.mainsail_unstable: bool | None = None self.mainsail_unstable: bool | None = None
self.fluidd_unstable: bool | None = None self.fluidd_unstable: bool | None = None
self.auto_backups_enabled: bool | None = None self.auto_backups_enabled: bool | None = None
@@ -49,31 +51,41 @@ class SettingsMenu(BaseMenu):
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ KIAUH Settings ] " header = " [ KIAUH Settings ] "
color = COLOR_CYAN color, rst = COLOR_CYAN, RESET_FORMAT
count = 62 - len(color) - len(RESET_FORMAT) count = 62 - len(color) - len(rst)
checked = f"[{COLOR_GREEN}x{RESET_FORMAT}]" checked = f"[{COLOR_GREEN}x{rst}]"
unchecked = "[ ]" 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 o1 = checked if self.mainsail_unstable else unchecked
o2 = checked if self.fluidd_unstable else unchecked o2 = checked if self.fluidd_unstable else unchecked
o3 = checked if self.auto_backups_enabled else unchecked o3 = checked if self.auto_backups_enabled else unchecked
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT} {color}{header:~^{count}}{rst}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Klipper source repository: ║ Klipper:
{self.klipper_repo:<67} ● Repo: {kl_repo:51}
● Owner: {kl_owner:51}
Moonraker source repository: ● Branch: {kl_branch:51}
║ ● {self.moonraker_repo:<67} ╟───────────────────────────────────────────────────────╢
Moonraker:
Install unstable Webinterface releases: ● Repo: {mr_repo:51}
║ ● Owner: {mr_owner:51}
║ ● Branch: {mr_branch:51}
╟───────────────────────────────────────────────────────╢
║ Install unstable releases: ║
{o1} Mainsail ║ {o1} Mainsail ║
{o2} Fluidd ║ {o2} Fluidd ║
║ ║ ╟───────────────────────────────────────────────────────╢
║ Auto-Backup: ║ ║ Auto-Backup: ║
{o3} Automatic backup before update ║ {o3} Automatic backup before update ║
║ ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ 1) Set Klipper source repository ║ ║ 1) Set Klipper source repository ║
║ 2) Set Moonraker source repository ║ ║ 2) Set Moonraker source repository ║
@@ -89,25 +101,10 @@ class SettingsMenu(BaseMenu):
def _load_settings(self) -> None: def _load_settings(self) -> None:
self.settings = KiauhSettings() self.settings = KiauhSettings()
self._format_repo_str("klipper")
self._format_repo_str("moonraker")
self.auto_backups_enabled = self.settings.kiauh.backup_before_update self.auto_backups_enabled = self.settings.kiauh.backup_before_update
self.mainsail_unstable = self.settings.mainsail.unstable_releases self.mainsail_unstable = self.settings.mainsail.unstable_releases
self.fluidd_unstable = self.settings.fluidd.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]: def _gather_input(self) -> Tuple[str, str]:
Logger.print_dialog( Logger.print_dialog(
DialogType.ATTENTION, DialogType.ATTENTION,

View File

@@ -25,6 +25,7 @@ class ComponentStatus:
status: StatusCode status: StatusCode
owner: str | None = None owner: str | None = None
repo: str | None = None repo: str | None = None
branch: str = ""
local: str | None = None local: str | None = None
remote: str | None = None remote: str | None = None
instances: int | None = None instances: int | None = None

View File

@@ -58,12 +58,16 @@ class ExtensionsMenu(BaseMenu):
module_path = f"kiauh.extensions.{ext.name}.{module_name}" module_path = f"kiauh.extensions.{ext.name}.{module_name}"
# get the class name of the extension # get the class name of the extension
ext_class: Type[BaseExtension] = inspect.getmembers( module = importlib.import_module(module_path)
importlib.import_module(module_path),
predicate=lambda o: inspect.isclass(o) def predicate(o):
and issubclass(o, BaseExtension) return (
and o != BaseExtension, inspect.isclass(o)
)[0][1] and issubclass(o, BaseExtension)
and o != BaseExtension
)
ext_class: type = inspect.getmembers(module, predicate)[0][1]
# instantiate the extension with its metadata and add to dict # instantiate the extension with its metadata and add to dict
ext_instance: BaseExtension = ext_class(metadata) ext_instance: BaseExtension = ext_class(metadata)
@@ -72,7 +76,7 @@ class ExtensionsMenu(BaseMenu):
except (IOError, json.JSONDecodeError, ImportError) as e: except (IOError, json.JSONDecodeError, ImportError) as e:
print(f"Failed loading extension {ext}: {e}") print(f"Failed loading extension {ext}: {e}")
return dict(sorted(ext_dict.items())) return dict(sorted(ext_dict.items(), key=lambda x: int(x[0])))
def extension_submenu(self, **kwargs): def extension_submenu(self, **kwargs):
ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run() ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run()

View File

@@ -132,7 +132,7 @@ class MoonrakerObico:
raise raise
env_file_content = env_template_file_content.replace( env_file_content = env_template_file_content.replace(
"%CFG%", "%CFG%",
f"{self.base.cfg_dir}/{self.cfg_file}", f"{self.cfg_file}",
) )
return env_file_content return env_file_content

View File

@@ -12,6 +12,7 @@ from typing import List
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker import Moonraker
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.instance_manager.base_instance import SUFFIX_BLACKLIST
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser, SimpleConfigParser,
@@ -308,7 +309,8 @@ class ObicoExtension(BaseExtension):
def _check_and_opt_link_instances(self) -> None: def _check_and_opt_link_instances(self) -> None:
Logger.print_status("Checking link status of Obico instances ...") Logger.print_status("Checking link status of Obico instances ...")
ob_instances: List[MoonrakerObico] = get_instances(MoonrakerObico) suffix_blacklist: List[str] = [suffix for suffix in SUFFIX_BLACKLIST if suffix != 'obico']
ob_instances: List[MoonrakerObico] = get_instances(MoonrakerObico, suffix_blacklist=suffix_blacklist)
unlinked_instances: List[MoonrakerObico] = [ unlinked_instances: List[MoonrakerObico] = [
obico for obico in ob_instances if not obico.is_linked obico for obico in ob_instances if not obico.is_linked
] ]

View File

@@ -0,0 +1,28 @@
# ======================================================================= #
# 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 pathlib import Path
# repo
OA_REPO = "https://github.com/crysxd/OctoApp-Plugin.git"
# directories
OA_DIR = Path.home().joinpath("octoapp")
OA_ENV_DIR = Path.home().joinpath("octoapp-env")
# files
OA_REQ_FILE = OA_DIR.joinpath("requirements.txt")
OA_DEPS_JSON_FILE = OA_DIR.joinpath("moonraker-system-dependencies.json")
OA_INSTALL_SCRIPT = OA_DIR.joinpath("install.sh")
OA_UPDATE_SCRIPT = OA_DIR.joinpath("update.sh")
OA_INSTALLER_LOG_FILE = Path.home().joinpath("octoapp-installer.log")
# filenames
OA_CFG_NAME = "octoapp.conf"
OA_LOG_NAME = "octoapp.log"
OA_SYS_CFG_NAME = "octoapp-system.cfg"

View File

@@ -0,0 +1,17 @@
{
"metadata": {
"index": 9,
"module": "octoapp_extension",
"maintained_by": "crysxd",
"display_name": "OctoApp for Klipper",
"description": [
"Your favorite 3D printing app for iOS & Android",
"- Print notifications on your phone & watch",
"- Control and start prints from your phone",
"- Live webcam view",
"- Live Gcode preview",
"- And much much more!"
],
"updates": true
}
}

View File

@@ -0,0 +1,75 @@
# ======================================================================= #
# 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
from dataclasses import dataclass, field
from pathlib import Path
from subprocess import CalledProcessError, run
from components.moonraker import MOONRAKER_CFG_NAME
from components.moonraker.moonraker import Moonraker
from core.instance_manager.base_instance import BaseInstance
from core.logger import Logger
from extensions.octoapp import (
OA_CFG_NAME,
OA_DIR,
OA_ENV_DIR,
OA_INSTALL_SCRIPT,
OA_LOG_NAME,
OA_SYS_CFG_NAME,
OA_UPDATE_SCRIPT,
)
from utils.sys_utils import get_service_file_path
@dataclass
class Octoapp:
suffix: str
base: BaseInstance = field(init=False, repr=False)
service_file_path: Path = field(init=False)
log_file_name = OA_LOG_NAME
dir: Path = OA_DIR
env_dir: Path = OA_ENV_DIR
data_dir: Path = field(init=False)
store_dir: Path = field(init=False)
cfg_file: Path = field(init=False)
sys_cfg_file: Path = field(init=False)
def __post_init__(self):
self.base: BaseInstance = BaseInstance(Moonraker, self.suffix)
self.base.log_file_name = self.log_file_name
self.service_file_path: Path = get_service_file_path(
Octoapp, self.suffix
)
self.store_dir = self.base.data_dir.joinpath("store")
self.cfg_file = self.base.cfg_dir.joinpath(OA_CFG_NAME)
self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME)
self.data_dir = self.base.data_dir
self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME)
def create(self) -> None:
Logger.print_status("Creating OctoApp for Klipper Instance ...")
try:
cmd = f"{OA_INSTALL_SCRIPT} {self.base.cfg_dir}/{MOONRAKER_CFG_NAME}"
run(cmd, check=True, shell=True)
except CalledProcessError as e:
Logger.print_error(f"Error creating instance: {e}")
raise
@staticmethod
def update() -> None:
try:
run(OA_UPDATE_SCRIPT.as_posix(), check=True, shell=True, cwd=OA_DIR)
except CalledProcessError as e:
Logger.print_error(f"Error updating OctoApp for Klipper: {e}")
raise

View File

@@ -0,0 +1,208 @@
# ======================================================================= #
# 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 #
# ======================================================================= #
import json
from typing import List
from components.moonraker.moonraker import Moonraker
from components.klipper.klipper import Klipper
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger
from extensions.base_extension import BaseExtension
from extensions.octoapp import (
OA_DEPS_JSON_FILE,
OA_DIR,
OA_ENV_DIR,
OA_INSTALL_SCRIPT,
OA_INSTALLER_LOG_FILE,
OA_REPO,
OA_REQ_FILE,
OA_SYS_CFG_NAME,
)
from extensions.octoapp.octoapp import Octoapp
from utils.common import (
check_install_dependencies,
moonraker_exists,
)
from utils.config_utils import (
remove_config_section,
)
from utils.fs_utils import run_remove_routines
from utils.git_utils import git_clone_wrapper
from utils.input_utils import get_confirm
from utils.instance_utils import get_instances
from utils.sys_utils import (
install_python_requirements,
parse_packages_from_file,
)
# noinspection PyMethodMayBeStatic
class OctoappExtension(BaseExtension):
def install_extension(self, **kwargs) -> None:
Logger.print_status("Installing OctoApp for Klipper ...")
# check if moonraker is installed. if not, notify the user and exit
if not moonraker_exists():
return
force_clone = False
OA_instances: List[Octoapp] = get_instances(Octoapp)
if OA_instances:
Logger.print_dialog(
DialogType.INFO,
[
"OctoApp is already installed!",
"It is safe to run the installer again to link your "
"printer or repair any issues.",
],
)
if not get_confirm("Re-run OctoApp installation?"):
Logger.print_info("Exiting OctoApp for Klipper installation ...")
return
else:
Logger.print_status("Re-Installing OctoApp for Klipper ...")
force_clone = True
mr_instances: List[Moonraker] = get_instances(Moonraker)
mr_names = [f"{moonraker.data_dir.name}" for moonraker in mr_instances]
if len(mr_names) > 1:
Logger.print_dialog(
DialogType.INFO,
[
"The following Moonraker instances were found:",
*mr_names,
"\n\n",
"The setup will apply the same names to OctoApp!",
],
)
if not get_confirm(
"Continue OctoApp for Klipper installation?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Exiting OctoApp for Klipper installation ...")
return
try:
git_clone_wrapper(OA_REPO, OA_DIR, force=force_clone)
for moonraker in mr_instances:
instance = Octoapp(suffix=moonraker.suffix)
instance.create()
InstanceManager.restart_all(mr_instances)
Logger.print_dialog(
DialogType.SUCCESS,
["OctoApp for Klipper successfully installed!"],
center_content=True,
)
except Exception as e:
Logger.print_error(
f"Error during OctoApp for Klipper installation:\n{e}"
)
def update_extension(self, **kwargs) -> None:
Logger.print_status("Updating OctoApp for Klipper ...")
try:
Octoapp.update()
Logger.print_dialog(
DialogType.SUCCESS,
["OctoApp for Klipper successfully updated!"],
center_content=True,
)
except Exception as e:
Logger.print_error(f"Error during OctoApp for Klipper update:\n{e}")
def remove_extension(self, **kwargs) -> None:
Logger.print_status("Removing OctoApp for Klipper ...")
mr_instances: List[Moonraker] = get_instances(Moonraker)
ob_instances: List[Octoapp] = get_instances(Octoapp)
try:
self._remove_OA_instances(ob_instances)
self._remove_OA_store_dirs()
self._remove_OA_dir()
self._remove_OA_env()
remove_config_section(f"include {OA_SYS_CFG_NAME}", mr_instances)
run_remove_routines(OA_INSTALLER_LOG_FILE)
Logger.print_dialog(
DialogType.SUCCESS,
["OctoApp for Klipper successfully removed!"],
center_content=True,
)
except Exception as e:
Logger.print_error(f"Error during OctoApp for Klipper removal:\n{e}")
def _install_OA_dependencies(self) -> None:
OA_deps = []
if OA_DEPS_JSON_FILE.exists():
with open(OA_DEPS_JSON_FILE, "r") as deps:
OA_deps = json.load(deps).get("debian", [])
elif OA_INSTALL_SCRIPT.exists():
OA_deps = parse_packages_from_file(OA_INSTALL_SCRIPT)
if not OA_deps:
raise ValueError("Error reading OctoApp dependencies!")
check_install_dependencies({*OA_deps})
install_python_requirements(OA_ENV_DIR, OA_REQ_FILE)
def _remove_OA_instances(
self,
instance_list: List[Octoapp],
) -> None:
if not instance_list:
Logger.print_info("No OctoApp instances found. Skipped ...")
return
for instance in instance_list:
Logger.print_status(
f"Removing instance {instance.service_file_path.stem} ..."
)
InstanceManager.remove(instance)
def _remove_OA_dir(self) -> None:
Logger.print_status("Removing OctoApp for Klipper directory ...")
if not OA_DIR.exists():
Logger.print_info(f"'{OA_DIR}' does not exist. Skipped ...")
return
run_remove_routines(OA_DIR)
def _remove_OA_store_dirs(self) -> None:
Logger.print_status("Removing OctoApp for Klipper store directory ...")
klipper_instances: List[Moonraker] = get_instances(Klipper)
for instance in klipper_instances:
store_dir = instance.data_dir.joinpath("octoapp-store")
if not store_dir.exists():
Logger.print_info(f"'{store_dir}' does not exist. Skipped ...")
return
run_remove_routines(store_dir)
def _remove_OA_env(self) -> None:
Logger.print_status("Removing OctoApp for Klipper environment ...")
if not OA_ENV_DIR.exists():
Logger.print_info(f"'{OA_ENV_DIR}' does not exist. Skipped ...")
return
run_remove_routines(OA_ENV_DIR)

View File

@@ -0,0 +1,13 @@
{
"metadata": {
"index": 10,
"module": "simply_print_extension",
"maintained_by": "dw-0",
"display_name": "SimplyPrint",
"description": [
"3D Printer Cloud Management Software.",
"\n\n",
"3D printing doesn't have to be a complicated, analog, SD card-filled experience; step into the future of modern 3D printing"
]
}
}

View File

@@ -0,0 +1,131 @@
# ======================================================================= #
# 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 typing import List
from components.moonraker.moonraker import Moonraker
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser,
)
from extensions.base_extension import BaseExtension
from utils.common import backup_printer_config_dir, moonraker_exists
from utils.input_utils import get_confirm
# noinspection PyMethodMayBeStatic
class SimplyPrintExtension(BaseExtension):
def install_extension(self, **kwargs) -> None:
Logger.print_status("Installing SimplyPrint ...")
if not (mr_instances := moonraker_exists("SimplyPrint Installer")):
return
Logger.print_dialog(
DialogType.INFO,
self._construct_dialog(mr_instances, True),
)
if not get_confirm(
"Continue SimplyPrint installation?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Exiting SimplyPrint installation ...")
return
try:
self._patch_moonraker_confs(mr_instances, True)
except Exception as e:
Logger.print_error(f"Error during SimplyPrint installation:\n{e}")
def remove_extension(self, **kwargs) -> None:
Logger.print_status("Removing SimplyPrint ...")
if not (mr_instances := moonraker_exists("SimplyPrint Uninstaller")):
return
Logger.print_dialog(
DialogType.INFO,
self._construct_dialog(mr_instances, False),
)
if not get_confirm(
"Do you really want to uninstall SimplyPrint?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Exiting SimplyPrint uninstallation ...")
return
try:
self._patch_moonraker_confs(mr_instances, False)
except Exception as e:
Logger.print_error(f"Error during SimplyPrint installation:\n{e}")
def _construct_dialog(
self, mr_instances: List[Moonraker], is_install: bool
) -> List[str]:
mr_names = [f"{m.service_file_path.name}" for m in mr_instances]
_type = "install" if is_install else "uninstall"
return [
"The following Moonraker instances were found:",
*mr_names,
"\n\n",
f"The setup will {_type} SimplyPrint for all Moonraker instances. "
f"After {_type}ation, all Moonraker services will be restarted!",
]
def _patch_moonraker_confs(
self, mr_instances: List[Moonraker], is_install: bool
) -> None:
section = "simplyprint"
_type, _ft = ("Adding", "to") if is_install else ("Removing", "from")
patched_files = []
for moonraker in mr_instances:
Logger.print_status(
f"{_type} section 'simplyprint' {_ft} {moonraker.cfg_file} ..."
)
scp = SimpleConfigParser()
scp.read_file(moonraker.cfg_file)
install_and_has_section = is_install and scp.has_section(section)
uninstall_and_has_no_section = not is_install and not scp.has_section(
section
)
if install_and_has_section or uninstall_and_has_no_section:
status = "already" if is_install else "does not"
Logger.print_info(
f"Section 'simplyprint' {status} exists! Skipping ..."
)
continue
if is_install and not scp.has_section("simplyprint"):
backup_printer_config_dir()
scp.add_section(section)
elif not is_install and scp.has_section("simplyprint"):
backup_printer_config_dir()
scp.remove_section(section)
scp.write_file(moonraker.cfg_file)
patched_files.append(moonraker.cfg_file)
if patched_files:
InstanceManager.restart_all(mr_instances)
install_state = "successfully" if patched_files else "was already"
Logger.print_dialog(
DialogType.SUCCESS,
[f"SimplyPrint {install_state} {'' if is_install else 'un'}installed!"],
center_content=True,
)

View File

@@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Moonraker Telegram Bot SV1 %INST% Description=Moonraker Telegram Bot SV1
Documentation=https://github.com/nlef/moonraker-telegram-bot/wiki Documentation=https://github.com/nlef/moonraker-telegram-bot/wiki
After=network-online.target After=network-online.target

View File

@@ -161,10 +161,11 @@ class TelegramBotExtension(BaseExtension):
# install dependencies # install dependencies
script = TG_BOT_DIR.joinpath("scripts/install.sh") script = TG_BOT_DIR.joinpath("scripts/install.sh")
package_list = parse_packages_from_file(script) package_list = parse_packages_from_file(script)
check_install_dependencies({*package_list}) check_install_dependencies({*package_list})
# create virtualenv # create virtualenv
if create_python_venv(TG_BOT_ENV): if create_python_venv(TG_BOT_ENV, allow_access_to_system_site_packages=True):
install_python_requirements(TG_BOT_ENV, TG_BOT_REQ_FILE) install_python_requirements(TG_BOT_ENV, TG_BOT_REQ_FILE)
def _patch_bot_update_manager(self, instances: List[Moonraker]) -> None: def _patch_bot_update_manager(self, instances: List[Moonraker]) -> None:

View File

@@ -14,15 +14,17 @@ from pathlib import Path
from typing import Dict, List, Literal, Optional, Set from typing import Dict, List, Literal, Optional, Set
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from core.constants import ( from core.constants import (
COLOR_CYAN, COLOR_CYAN,
GLOBAL_DEPS, GLOBAL_DEPS,
PRINTER_CFG_BACKUP_DIR, PRINTER_DATA_BACKUP_DIR,
RESET_FORMAT, RESET_FORMAT,
) )
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.types import ComponentStatus, StatusCode from core.types import ComponentStatus, StatusCode
from utils.git_utils import ( from utils.git_utils import (
get_current_branch,
get_local_commit, get_local_commit,
get_local_tags, get_local_tags,
get_remote_commit, get_remote_commit,
@@ -102,7 +104,12 @@ def get_install_status(
""" """
from utils.instance_utils import get_instances 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: if env_dir is not None:
checks.append(env_dir.exists()) checks.append(env_dir.exists())
@@ -130,6 +137,7 @@ def get_install_status(
instances=instances, instances=instances,
owner=org, owner=org,
repo=repo, repo=repo,
branch=branch,
local=get_local_commit(repo_dir), local=get_local_commit(repo_dir),
remote=get_remote_commit(repo_dir), remote=get_remote_commit(repo_dir),
) )
@@ -142,23 +150,25 @@ def backup_printer_config_dir() -> None:
instances: List[Klipper] = get_instances(Klipper) instances: List[Klipper] = get_instances(Klipper)
bm = BackupManager() bm = BackupManager()
if not instances:
Logger.print_info("Unable to find directory to backup!")
Logger.print_info("Are there no Klipper instances installed?")
return
for instance in instances: for instance in instances:
name = f"config-{instance.data_dir.name}"
bm.backup_directory( bm.backup_directory(
name, instance.data_dir.name,
source=instance.base.cfg_dir, source=instance.base.cfg_dir,
target=PRINTER_CFG_BACKUP_DIR, target=PRINTER_DATA_BACKUP_DIR,
) )
def moonraker_exists(name: str = "") -> bool: def moonraker_exists(name: str = "") -> List[Moonraker]:
""" """
Helper method to check if a Moonraker instance exists Helper method to check if a Moonraker instance exists
:param name: Optional name of an installer where the check is performed :param name: Optional name of an installer where the check is performed
:return: True if at least one Moonraker instance exists, False otherwise :return: True if at least one Moonraker instance exists, False otherwise
""" """
from components.moonraker.moonraker import Moonraker
mr_instances: List[Moonraker] = get_instances(Moonraker) mr_instances: List[Moonraker] = get_instances(Moonraker)
info = ( info = (
@@ -175,8 +185,8 @@ def moonraker_exists(name: str = "") -> bool:
f"{info}. Please install Moonraker first!", f"{info}. Please install Moonraker first!",
], ],
) )
return False return []
return True return mr_instances
def trunc_string(input_str: str, length: int) -> str: def trunc_string(input_str: str, length: int) -> str:

View File

@@ -8,6 +8,7 @@
# ======================================================================= # # ======================================================================= #
from __future__ import annotations from __future__ import annotations
import shutil
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import List, Tuple from typing import List, Tuple
@@ -26,6 +27,9 @@ def add_config_section(
instances: List[InstanceType], instances: List[InstanceType],
options: List[ConfigOption] | None = None, options: List[ConfigOption] | None = None,
) -> None: ) -> None:
if not instances:
return
for instance in instances: for instance in instances:
cfg_file = instance.cfg_file cfg_file = instance.cfg_file
Logger.print_status(f"Add section '[{section}]' to '{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) tmp.writelines(org_content)
cfg_file.unlink() cfg_file.unlink()
tmp_cfg_path.rename(cfg_file) shutil.move(tmp_cfg_path, cfg_file)
Logger.print_ok("OK!") Logger.print_ok("OK!")

View File

@@ -7,7 +7,7 @@ from http.client import HTTPResponse
from json import JSONDecodeError from json import JSONDecodeError
from pathlib import Path from pathlib import Path
from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run
from typing import List, Type from typing import List, Tuple, Type
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger from core.logger import Logger
@@ -70,7 +70,7 @@ def git_pull_wrapper(repo: str, target_dir: Path) -> None:
return return
def get_repo_name(repo: Path) -> tuple[str, str] | None: def get_repo_name(repo: Path) -> Tuple[str, str]:
""" """
Helper method to extract the organisation and name of a repository | Helper method to extract the organisation and name of a repository |
:param repo: repository to extract the values from :param repo: repository to extract the values from
@@ -83,11 +83,31 @@ def get_repo_name(repo: Path) -> tuple[str, str] | None:
cmd = ["git", "-C", repo.as_posix(), "config", "--get", "remote.origin.url"] cmd = ["git", "-C", repo.as_posix(), "config", "--get", "remote.origin.url"]
result: str = check_output(cmd, stderr=DEVNULL).decode(encoding="utf-8") result: str = check_output(cmd, stderr=DEVNULL).decode(encoding="utf-8")
substrings: List[str] = result.strip().split("/")[-2:] substrings: List[str] = result.strip().split("/")[-2:]
return substrings[0], substrings[1]
# return "/".join(substrings).replace(".git", "") orga: str = substrings[0] if substrings[0] else "-"
name: str = substrings[1] if substrings[1] else "-"
return orga, name.replace(".git", "")
except CalledProcessError: except CalledProcessError:
return None 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]: def get_local_tags(repo_path: Path, _filter: str | None = None) -> List[str]:
@@ -184,7 +204,7 @@ def compare_semver_tags(tag1: str, tag2: str) -> bool:
if tag1 == tag2: if tag1 == tag2:
return False return False
def parse_version(v): def parse_version(v) -> List[int]:
return list(map(int, v[1:].split("."))) return list(map(int, v[1:].split(".")))
tag1_parts = parse_version(tag1) tag1_parts = parse_version(tag1)
@@ -206,8 +226,8 @@ def get_local_commit(repo: Path) -> str | None:
return None return None
try: try:
cmd = f"cd {repo} && git describe HEAD --always --tags | cut -d '-' -f 1,2" cmd = "git describe HEAD --always --tags | cut -d '-' -f 1,2"
return check_output(cmd, shell=True, text=True).strip() return check_output(cmd, shell=True, text=True, cwd=repo).strip()
except CalledProcessError: except CalledProcessError:
return None return None
@@ -217,12 +237,15 @@ def get_remote_commit(repo: Path) -> str | None:
return None return None
try: try:
# get locally checked out branch branch = get_current_branch(repo)
branch_cmd = f"cd {repo} && git branch | grep -E '\*'" cmd = f"git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2"
branch = check_output(branch_cmd, shell=True, text=True) return check_output(
branch = branch.split("*")[-1].strip() cmd,
cmd = f"cd {repo} && git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2" shell=True,
return check_output(cmd, shell=True, text=True).strip() text=True,
cwd=repo,
stderr=DEVNULL,
).strip()
except CalledProcessError: except CalledProcessError:
return None return None

View File

@@ -13,6 +13,7 @@ from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker import Moonraker
from extensions.obico.moonraker_obico import MoonrakerObico from extensions.obico.moonraker_obico import MoonrakerObico
from extensions.octoeverywhere.octoeverywhere import Octoeverywhere from extensions.octoeverywhere.octoeverywhere import Octoeverywhere
from extensions.octoapp.octoapp import Octoapp
from extensions.telegram_bot.moonraker_telegram_bot import MoonrakerTelegramBot from extensions.telegram_bot.moonraker_telegram_bot import MoonrakerTelegramBot
InstanceType = TypeVar( InstanceType = TypeVar(
@@ -22,4 +23,5 @@ InstanceType = TypeVar(
MoonrakerTelegramBot, MoonrakerTelegramBot,
MoonrakerObico, MoonrakerObico,
Octoeverywhere, Octoeverywhere,
Octoapp,
) )

View File

@@ -17,7 +17,7 @@ from core.instance_manager.base_instance import SUFFIX_BLACKLIST
from utils.instance_type import InstanceType from utils.instance_type import InstanceType
def get_instances(instance_type: type) -> List[InstanceType]: def get_instances(instance_type: type, suffix_blacklist: List[str] = SUFFIX_BLACKLIST) -> List[InstanceType]:
from utils.common import convert_camelcase_to_kebabcase from utils.common import convert_camelcase_to_kebabcase
if not isinstance(instance_type, type): if not isinstance(instance_type, type):
@@ -30,7 +30,7 @@ def get_instances(instance_type: type) -> List[InstanceType]:
Path(SYSTEMD, service) Path(SYSTEMD, service)
for service in SYSTEMD.iterdir() for service in SYSTEMD.iterdir()
if pattern.search(service.name) if pattern.search(service.name)
and not any(s in service.name for s in SUFFIX_BLACKLIST) and not any(s in service.name for s in suffix_blacklist)
] ]
instance_list = [ instance_list = [

View File

@@ -91,19 +91,27 @@ def parse_packages_from_file(source_file: Path) -> List[str]:
return packages return packages
def create_python_venv(target: Path, force: bool = False) -> bool: def create_python_venv(
target: Path,
force: bool = False,
allow_access_to_system_site_packages: bool = False,
) -> bool:
""" """
Create a python 3 virtualenv at the provided target destination. Create a python 3 virtualenv at the provided target destination.
Returns True if the virtualenv was created successfully. Returns True if the virtualenv was created successfully.
Returns False if the virtualenv already exists, recreation was declined or creation failed. Returns False if the virtualenv already exists, recreation was declined or creation failed.
:param force: Force recreation of the virtualenv
:param target: Path where to create the virtualenv at :param target: Path where to create the virtualenv at
:param force: Force recreation of the virtualenv
:param allow_access_to_system_site_packages: give the virtual environment access to the system site-packages dir
:return: bool :return: bool
""" """
Logger.print_status("Set up Python virtual environment ...") Logger.print_status("Set up Python virtual environment ...")
cmd = ["virtualenv", "-p", "/usr/bin/python3", target.as_posix()]
cmd.append(
"--system-site-packages"
) if allow_access_to_system_site_packages else None
if not target.exists(): if not target.exists():
try: try:
cmd = ["virtualenv", "-p", "/usr/bin/python3", target.as_posix()]
run(cmd, check=True) run(cmd, check=True)
Logger.print_ok("Setup of virtualenv successful!") Logger.print_ok("Setup of virtualenv successful!")
return True return True

View File

@@ -127,9 +127,9 @@ managed_services: Spoolman
regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/moonraker\.asvc" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/moonraker\.asvc"
moonraker_asvc=$(find "${HOME}" -maxdepth 2 -type f -regextype posix-extended -regex "${regex}" | sort) moonraker_asvc=$(find "${HOME}" -maxdepth 2 -type f -regextype posix-extended -regex "${regex}" | sort)
if [[ -n ${moonraker_asvc} ]]; then if ! grep -q "^Spoolman$" "${moonraker_asvc}"; then
status_msg "Adding Spoolman service to moonraker.asvc..." status_msg "Adding Spoolman service to moonraker.asvc..."
/bin/sh -c "echo 'Spoolman' >> ${moonraker_asvc}" sed -i '$a''Spoolman' "${moonraker_asvc}"
fi fi
} }
@@ -248,7 +248,7 @@ function get_spoolman_status() {
function get_local_spoolman_version() { function get_local_spoolman_version() {
[[ ! -d "${SPOOLMAN_DIR}" ]] && return [[ ! -d "${SPOOLMAN_DIR}" ]] && return
local version local version
version=$(grep -o '"version":\s*"[^"]*' "${SPOOLMAN_DIR}"/release_info.json | cut -d'"' -f4) version=$(grep -o '"version":\s*"[^"]*' "${SPOOLMAN_DIR}"/release_info.json | cut -d'"' -f4)
echo "${version}" echo "${version}"