Compare commits

..

6 Commits

Author SHA1 Message Date
dw-0 040edc1d4f chore: remove AGENTS.md 2026-06-28 12:50:59 +02:00
Paul 750dba1dbe feat(extensions): add Moongate for Klipper (#809)
* feat(extensions): add Moongate for Klipper

Adds Moongate for Klipper as extension #15 (next free index after KAMP). Moongate pairs a printer with the Moongate Android app for secure remote access and print monitoring over a Cloudflare quick-tunnel fronted by an EdDSA auth proxy.

The extension is KIAUH-native for the parts KIAUH owns (Moonraker instance discovery, the install/remove confirmation dialogs, moonraker.conf backup, and the repo clone wired into the update manager) and delegates the heavy, security-sensitive install steps (cloudflared, the moongate-authproxy and moongate-tunnel systemd services, the Moonraker host rebind) to Moongate's own idempotent, non-interactive install/update/uninstall scripts so that logic stays maintained upstream in one place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(metadata): update index for Moongate extension

* refactor(moongate_extension): enhance error handling and update docstring for clarity

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
2026-06-28 12:49:30 +02:00
Cody D Dixon 6f4b471008 feat(extension): Add DroidKlipp extension (#804)
* Add DroidKlipp extension

* fix(extensions): restart adb_monitor and redeploy monitor on DroidKlipp update

The update flow only ran git_pull_wrapper, which left the running
adb_monitor service executing stale in-memory code. It also never
rebuilt the deployed ~/droidklipp_monitor.py, since that file is
generated by the installer rather than tracked in the repo.

- stop adb_monitor before the pull, start it after (mirrors mobileraker)
- after pulling, re-deploy droidklipp_monitor.py into $HOME so the
  service picks up the updated code
- best-effort restart the service if the update fails

* fix(droidklipp): clean up comments and update website link in metadata

---------

Co-authored-by: Cody Dixon <codydixon71@gmail.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
2026-06-28 12:07:19 +02:00
GustavoTheThird 5077765fd6 fix: fluidd logo (#811)
* fix: fluid logo

* fix: use correct logo.svg instead of avatar

---------

Co-authored-by: dw-0 <th33xitus@gmail.com>
2026-06-27 12:53:20 +02:00
Patrick Gehrsitz 9f97ae6c2a refactor(crowsnest): fix crowsnest install status and update for v5 (#799)
fix: fix crowsnest install status and update for v5

Crowsnest v5 introduced multiple breaking changes resulting in a
'Incomplete' install status even after successful installation.
This adds support for the new v5 setup with backwards compatibility for
old versions.

Signed-off-by: Patrick Gehrsitz <github@mryel.de>
2026-05-30 15:36:32 +02:00
_Redstone_c_ b90a8f13b1 fix(switch_repo): honor use_python_binary when recreating venv (#793)
* fix(switch_repo): honor use_python_binary when recreating venv

* fix(switch_repo): validate name at function entry
2026-04-24 13:34:45 +02:00
11 changed files with 585 additions and 78 deletions
-68
View File
@@ -1,68 +0,0 @@
# AGENTS.md - KIAUH Development Guide
## Project Overview
KIAUH (Klipper Installation And Update Helper) is a Python-based installation script for Klipper 3D printer firmware and related components written in Python 3.8+.
## Running KIAUH
```bash
./kiauh.sh
```
**Important:** Must NOT run as root. The script will exit if EUID is 0.
## Development Commands
```bash
# Install dev dependencies
pip install -r requirements-dev.txt
# Lint (ruff)
ruff check .
# Format
ruff format .
# Typecheck
mypy kiauh
# Run tests
pytest
# Run specific test file
pytest kiauh/core/simple_config_parser/tests/public_api/test_options_api.py
```
## Testing
- New tests should be placed near their corresponding components/modules (e.g., `kiauh/components/klipper/*/test_*.py`)
- Always use a `tests/` subdirectory
- Existing pytest setup in `kiauh/core/simple_config_parser/tests/` serves as reference
## Project Structure
- `kiauh.sh` - Bash entry point, sets PYTHONPATH and calls main.py
- `kiauh/main.py` - Python entry point
- `kiauh/core/` - Core functionality (menus, services, settings, types)
- `kiauh/components/` - Klipper components (klipper, moonraker, webui_client, etc.)
- `kiauh/extensions/` - Extension system for optional addons (obico, octoprint, spoolman, etc.)
- `kiauh/core/simple_config_parser/` - Custom INI-style config parser for Klipper configs
- `kiauh/core/simple_config_parser/src/simple_config_parser/` - Submodule (git subtree)
## Key Quirks
1. **Python version:** Requires Python 3.8+ (checked in kiauh.sh)
2. **Config files:** KIAUH uses `kiauh.cfg` in project root (not .ini format - it's parsed by simple_config_parser)
3. **Submodule:** `kiauh/core/simple_config_parser/` is a git subtree, not a submodule
4. **Branch check:** KIAUH only checks for updates on master branch (not develop)
5. **Target:** Designed to run on Raspberry Pi OS / Debian-based distros
## Code Style
- 4-space indentation
- 88 character line length
- Double quotes
- LF line endings
- Type hints required (mypy checks)
- Ruff with I (isort) enabled
+1 -1
View File
@@ -147,7 +147,7 @@ changes!**
<th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th> <th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th>
</tr> </tr>
<tr> <tr>
<th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th> <th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th> <th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th>
<th><img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint/master/docs/images/octoprint-logo.png" alt="OctoPrint Logo" height="64"></th> <th><img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint/master/docs/images/octoprint-logo.png" alt="OctoPrint Logo" height="64"></th>
</tr> </tr>
+2
View File
@@ -19,10 +19,12 @@ CROWSNEST_SERVICE_NAME = "crowsnest.service"
# directories # directories
CROWSNEST_DIR = Path.home().joinpath("crowsnest") CROWSNEST_DIR = Path.home().joinpath("crowsnest")
CROWSNEST_ENV_DIR = Path.home().joinpath("crowsnest-env")
# files # files
CROWSNEST_MULTI_CONFIG = CROWSNEST_DIR.joinpath("tools/.config") CROWSNEST_MULTI_CONFIG = CROWSNEST_DIR.joinpath("tools/.config")
CROWSNEST_INSTALL_SCRIPT = CROWSNEST_DIR.joinpath("tools/install.sh") CROWSNEST_INSTALL_SCRIPT = CROWSNEST_DIR.joinpath("tools/install.sh")
CROWSNEST_DEPS_JSON_FILE = CROWSNEST_DIR.joinpath("system-dependencies.json")
CROWSNEST_BIN_FILE = Path("/usr/local/bin/crowsnest") CROWSNEST_BIN_FILE = Path("/usr/local/bin/crowsnest")
CROWSNEST_LOGROTATE_FILE = Path("/etc/logrotate.d/crowsnest") CROWSNEST_LOGROTATE_FILE = Path("/etc/logrotate.d/crowsnest")
CROWSNEST_SERVICE_FILE = SYSTEMD.joinpath(CROWSNEST_SERVICE_NAME) CROWSNEST_SERVICE_FILE = SYSTEMD.joinpath(CROWSNEST_SERVICE_NAME)
+68 -8
View File
@@ -16,7 +16,9 @@ from typing import List
from components.crowsnest import ( from components.crowsnest import (
CROWSNEST_BIN_FILE, CROWSNEST_BIN_FILE,
CROWSNEST_DEPS_JSON_FILE,
CROWSNEST_DIR, CROWSNEST_DIR,
CROWSNEST_ENV_DIR,
CROWSNEST_INSTALL_SCRIPT, CROWSNEST_INSTALL_SCRIPT,
CROWSNEST_LOGROTATE_FILE, CROWSNEST_LOGROTATE_FILE,
CROWSNEST_MULTI_CONFIG, CROWSNEST_MULTI_CONFIG,
@@ -25,6 +27,8 @@ from components.crowsnest import (
CROWSNEST_SERVICE_NAME, CROWSNEST_SERVICE_NAME,
) )
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.moonraker.utils.sysdeps_parser import SysDepsParser
from components.moonraker.utils.utils import load_sysdeps_json
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.services.backup_service import BackupService from core.services.backup_service import BackupService
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings
@@ -34,6 +38,7 @@ from utils.common import (
get_install_status, get_install_status,
) )
from utils.git_utils import ( from utils.git_utils import (
get_current_branch,
git_clone_wrapper, git_clone_wrapper,
git_pull_wrapper, git_pull_wrapper,
) )
@@ -135,8 +140,7 @@ def update_crowsnest() -> None:
git_pull_wrapper(CROWSNEST_DIR) git_pull_wrapper(CROWSNEST_DIR)
deps = parse_packages_from_file(CROWSNEST_INSTALL_SCRIPT) install_crowsnest_packages()
check_install_dependencies({*deps})
cmd_sysctl_service(CROWSNEST_SERVICE_NAME, "restart") cmd_sysctl_service(CROWSNEST_SERVICE_NAME, "restart")
@@ -147,12 +151,68 @@ def update_crowsnest() -> None:
def get_crowsnest_status() -> ComponentStatus: def get_crowsnest_status() -> ComponentStatus:
files = [ """
CROWSNEST_BIN_FILE, Get the current install status of Crowsnest. Depending on the version the installed
CROWSNEST_LOGROTATE_FILE, files are different. If a version is not yet specified, it will search for a
CROWSNEST_SERVICE_FILE, non_existant file resulting in 'Incomplete' status.
] :return: Installation status
return get_install_status(CROWSNEST_DIR, files=files) """
files_dict = {
4: [
CROWSNEST_BIN_FILE,
CROWSNEST_LOGROTATE_FILE,
CROWSNEST_SERVICE_FILE,
],
5: [CROWSNEST_SERVICE_FILE],
}
version = get_crowsnest_version()
non_existant = CROWSNEST_DIR.joinpath("non_existant")
files = files_dict.get(version, [non_existant])
env_dir = None
if version >= 5:
env_dir = CROWSNEST_ENV_DIR
return get_install_status(CROWSNEST_DIR, files=files, env_dir=env_dir)
def get_crowsnest_version() -> int:
"""
Get the current major version. Starting with v5 the default branch will be named
after the major version.
:return: Current major version
"""
version = get_current_branch(CROWSNEST_DIR)
if version is None:
return 0
if version == "master":
return 4
return int(version.removeprefix("v"))
def install_crowsnest_packages() -> None:
Logger.print_status("Parsing Crowsnest system dependencies ...")
crowsnest_deps = []
crowsnest_version = get_crowsnest_version()
if crowsnest_version >= 5 and CROWSNEST_DEPS_JSON_FILE.exists():
Logger.print_info(
f"Parsing system dependencies from {CROWSNEST_DEPS_JSON_FILE.name} ..."
)
parser = SysDepsParser()
sysdeps = load_sysdeps_json(CROWSNEST_DEPS_JSON_FILE)
crowsnest_deps.extend(parser.parse_dependencies(sysdeps))
elif crowsnest_version <= 4 and CROWSNEST_INSTALL_SCRIPT.exists():
Logger.print_info(
f"Parsing system dependencies from {CROWSNEST_INSTALL_SCRIPT.name} ..."
)
crowsnest_deps = parse_packages_from_file(CROWSNEST_INSTALL_SCRIPT)
if not crowsnest_deps:
raise ValueError("Error parsing crowsnest dependencies!")
check_install_dependencies({*crowsnest_deps})
def remove_crowsnest() -> None: def remove_crowsnest() -> None:
+28
View File
@@ -0,0 +1,28 @@
# ======================================================================= #
# Copyright (C) 2026 Cody Dixon #
# #
# 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
DROIDKLIPP_REPO = "https://github.com/CodeMasterCody3D/DroidKlipp"
DROIDKLIPP_APK_URL = "https://github.com/CodeMasterCody3D/DroidKlipp-Android-APK/releases/latest/download/DroidKlipp.apk"
# directories
DROIDKLIPP_DIR = Path.home().joinpath("DroidKlipp")
# files
DROIDKLIPP_INSTALL_SCRIPT = DROIDKLIPP_DIR.joinpath("install_droidklipp.sh")
DROIDKLIPP_UNINSTALL_SCRIPT = DROIDKLIPP_DIR.joinpath("uninstall_droidklipp.sh")
DROIDKLIPP_MONITOR_FILE = DROIDKLIPP_DIR.joinpath("droidklipp_monitor.py")
DROIDKLIPP_DEPLOYED_MONITOR = Path.home().joinpath("droidklipp_monitor.py")
# service
DROIDKLIPP_SERVICE_NAME = "adb_monitor"
# packages
DROIDKLIPP_REQUIRED_PACKAGES = {"adb", "tmux", "x11-utils"}
@@ -0,0 +1,155 @@
# ======================================================================= #
# Copyright (C) 2026 Cody Dixon #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from subprocess import CalledProcessError, run
from components.klipperscreen import KLIPPERSCREEN_DIR, KLIPPERSCREEN_ENV_DIR
from core.logger import DialogType, Logger
from extensions.base_extension import BaseExtension
from extensions.droidklipp import (
DROIDKLIPP_APK_URL,
DROIDKLIPP_DEPLOYED_MONITOR,
DROIDKLIPP_DIR,
DROIDKLIPP_INSTALL_SCRIPT,
DROIDKLIPP_MONITOR_FILE,
DROIDKLIPP_REPO,
DROIDKLIPP_REQUIRED_PACKAGES,
DROIDKLIPP_SERVICE_NAME,
DROIDKLIPP_UNINSTALL_SCRIPT,
)
from utils.common import check_install_dependencies
from utils.fs_utils import check_file_exist, run_remove_routines
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
from utils.input_utils import get_confirm
from utils.sys_utils import cmd_sysctl_service
# noinspection PyMethodMayBeStatic
class DroidKlippExtension(BaseExtension):
def install_extension(self, **kwargs) -> None:
Logger.print_status("Installing DroidKlipp ...")
if not self._klipperscreen_exists():
Logger.print_dialog(
DialogType.WARNING,
[
"No KIAUH v6 KlipperScreen installation found!",
"DroidKlipp expects KlipperScreen at:",
f"{KLIPPERSCREEN_DIR.joinpath('screen.py')}",
f"{KLIPPERSCREEN_ENV_DIR.joinpath('bin/python')}",
"Install KlipperScreen first, then run this installer again.",
],
)
return
Logger.print_dialog(
DialogType.INFO,
[
"DroidKlipp requires the Android APK to be installed on your Android device:",
DROIDKLIPP_APK_URL,
"\n\n",
"The installer will configure ADB forwarding, udev rules, the DroidKlipp monitor, and WiFi fallback.",
],
)
if not get_confirm(
"Continue DroidKlipp installation?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Exiting DroidKlipp installation ...")
return
try:
check_install_dependencies(DROIDKLIPP_REQUIRED_PACKAGES)
git_clone_wrapper(DROIDKLIPP_REPO, DROIDKLIPP_DIR)
run(["chmod", "+x", DROIDKLIPP_INSTALL_SCRIPT], check=True)
run([DROIDKLIPP_INSTALL_SCRIPT], check=True)
Logger.print_dialog(
DialogType.SUCCESS,
["DroidKlipp successfully installed!"],
center_content=True,
)
except CalledProcessError as e:
Logger.print_error(f"Error during DroidKlipp installation:\n{e}")
except Exception as e:
Logger.print_error(f"Error during DroidKlipp installation:\n{e}")
def update_extension(self, **kwargs) -> None:
Logger.print_status("Updating DroidKlipp ...")
if not check_file_exist(DROIDKLIPP_DIR):
Logger.print_info("Extension does not seem to be installed! Skipping ...")
return
try:
cmd_sysctl_service(DROIDKLIPP_SERVICE_NAME, "stop")
git_pull_wrapper(DROIDKLIPP_DIR)
if check_file_exist(DROIDKLIPP_MONITOR_FILE):
run(
[
"install",
"-m",
"755",
str(DROIDKLIPP_MONITOR_FILE),
str(DROIDKLIPP_DEPLOYED_MONITOR),
],
check=True,
)
cmd_sysctl_service(DROIDKLIPP_SERVICE_NAME, "start")
Logger.print_dialog(
DialogType.SUCCESS,
["DroidKlipp successfully updated!"],
center_content=True,
)
except CalledProcessError as e:
Logger.print_error(f"Error during DroidKlipp update:\n{e}")
cmd_sysctl_service(DROIDKLIPP_SERVICE_NAME, "start")
except Exception as e:
Logger.print_error(f"Error during DroidKlipp update:\n{e}")
cmd_sysctl_service(DROIDKLIPP_SERVICE_NAME, "start")
def remove_extension(self, **kwargs) -> None:
Logger.print_status("Removing DroidKlipp ...")
if not check_file_exist(DROIDKLIPP_DIR):
Logger.print_info("Extension does not seem to be installed! Skipping ...")
return
if not get_confirm(
"Do you really want to uninstall DroidKlipp?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Exiting DroidKlipp uninstallation ...")
return
try:
if check_file_exist(DROIDKLIPP_UNINSTALL_SCRIPT):
run(["chmod", "+x", DROIDKLIPP_UNINSTALL_SCRIPT], check=True)
run([DROIDKLIPP_UNINSTALL_SCRIPT], check=True)
run_remove_routines(DROIDKLIPP_DIR)
Logger.print_dialog(
DialogType.SUCCESS,
["DroidKlipp successfully removed!"],
center_content=True,
)
except CalledProcessError as e:
Logger.print_error(f"Error during DroidKlipp removal:\n{e}")
except Exception as e:
Logger.print_error(f"Error during DroidKlipp removal:\n{e}")
def _klipperscreen_exists(self) -> bool:
return bool(
check_file_exist(KLIPPERSCREEN_DIR.joinpath("screen.py"))
and check_file_exist(KLIPPERSCREEN_ENV_DIR.joinpath("bin/python"))
)
+17
View File
@@ -0,0 +1,17 @@
{
"metadata": {
"index": 15,
"module": "droidklipp_extension",
"maintained_by": "CodeMasterCody3D",
"display_name": "DroidKlipp",
"description": [
"Use an Android device as a KlipperScreen display via ADB and DroidKlipp APK / XServer XSDL integration",
"- Automatic USB ADB forwarding",
"- Optional WiFi fallback",
"- Starts and monitors KlipperScreen on the Android X server"
],
"website": "https://github.com/CodeMasterCody3D/DroidKlipp-Android-APK/releases",
"repo": "https://github.com/CodeMasterCody3D/DroidKlipp",
"updates": true
}
}
+34
View File
@@ -0,0 +1,34 @@
# ======================================================================= #
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
# Copyright (C) 2026 Paul Sharman <github.com/PEEKYPAUL> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# It integrates Moongate for Klipper: #
# https://github.com/PEEKYPAUL/Moongate #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
# repository
MOONGATE_REPO = "https://github.com/PEEKYPAUL/moongate.git"
MOONGATE_REPO_URL = "https://github.com/PEEKYPAUL/Moongate"
# directories
MODULE_PATH = Path(__file__).resolve().parent
MOONGATE_DIR = Path.home().joinpath("moongate")
MOONGATE_PLUGIN_DIR = MOONGATE_DIR.joinpath("klipper-plugin")
# installer scripts shipped inside the cloned repo
MOONGATE_INSTALL_SCRIPT = MOONGATE_PLUGIN_DIR.joinpath("install.sh")
MOONGATE_UPDATE_SCRIPT = MOONGATE_PLUGIN_DIR.joinpath("update.sh")
MOONGATE_UNINSTALL_SCRIPT = MOONGATE_PLUGIN_DIR.joinpath("uninstall.sh")
# moonraker.conf sections the installer manages
MOONGATE_UPDATER_NAME = "update_manager moongate"
MOONGATE_CONFIG_SECTION = "moongate"
# default HTTP port the Mainsail/Fluidd UI is served on
MOONGATE_DEFAULT_PORT = 80
+15
View File
@@ -0,0 +1,15 @@
{
"metadata": {
"index": 16,
"module": "moongate_extension",
"maintained_by": "PEEKYPAUL",
"display_name": "Moongate for Klipper",
"description": [
"Pair this printer with the Moongate Android app for secure remote",
"access and print monitoring. Installs cloudflared, a Cloudflare",
"quick-tunnel and an EdDSA auth gate in front of Moonraker."
],
"repo": "https://github.com/PEEKYPAUL/Moongate",
"updates": true
}
}
@@ -0,0 +1,249 @@
# ======================================================================= #
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
# Copyright (C) 2026 Paul Sharman <github.com/PEEKYPAUL> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# It integrates Moongate for Klipper: #
# https://github.com/PEEKYPAUL/Moongate #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
import os
from pathlib import Path
from subprocess import CalledProcessError, run
from typing import Dict, List
from components.moonraker.moonraker import Moonraker
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger
from core.services.backup_service import BackupService
from extensions.base_extension import BaseExtension
from extensions.moongate import (
MOONGATE_CONFIG_SECTION,
MOONGATE_DEFAULT_PORT,
MOONGATE_DIR,
MOONGATE_INSTALL_SCRIPT,
MOONGATE_REPO,
MOONGATE_REPO_URL,
MOONGATE_UNINSTALL_SCRIPT,
MOONGATE_UPDATE_SCRIPT,
MOONGATE_UPDATER_NAME,
)
from utils.config_utils import remove_config_section
from utils.fs_utils import check_file_exist
from utils.git_utils import GitException, git_clone_wrapper, git_pull_wrapper
from utils.input_utils import get_confirm, get_number_input
from utils.instance_utils import get_instances
# noinspection PyMethodMayBeStatic
class MoongateExtension(BaseExtension):
"""
Moongate ships a substantial, security-sensitive and idempotent installer
(cloudflared, two systemd services, an EdDSA auth proxy, a Moonraker host
rebind and a tightly-scoped Avahi sudoers entry). Rather than mirror all
of that in Python — where it would drift out of sync with upstream — this
extension does the KIAUH-idiomatic parts natively (instance discovery,
confirmation, moonraker.conf backup, the repo clone wired to the update
manager) and delegates the heavy lifting to Moongate's own scripts.
"""
def install_extension(self, **kwargs) -> None:
Logger.print_status("Installing Moongate for Klipper ...")
mr_instances: List[Moonraker] = get_instances(Moonraker)
if not mr_instances:
Logger.print_dialog(
DialogType.WARNING,
[
"No Moonraker instances found!",
"Moongate is a Moonraker component and needs Moonraker to be "
"installed first. Please install Moonraker, then try again.",
],
)
return
# Moongate is a single-printer integration. On a multi-instance host we
# target the first Moonraker instance and say so.
moonraker = mr_instances[0]
if len(mr_instances) > 1:
Logger.print_dialog(
DialogType.WARNING,
[
"Multiple Moonraker instances detected.",
"Moongate currently supports a single-printer setup. The "
f"instance '{moonraker.data_dir.name}' will be used.",
],
)
if not self._confirm_install():
Logger.print_info("Installation aborted.")
return
port = get_number_input(
"HTTP port your Mainsail/Fluidd UI is served on",
min_value=1,
max_value=65535,
default=MOONGATE_DEFAULT_PORT,
)
if port is None:
return
try:
self._clone_or_update_repo()
BackupService().backup_moonraker_conf()
# Hand off to Moongate's own installer. It is idempotent,
# non-interactive and env-driven: it installs cloudflared, adds the
# two systemd services, patches moonraker.conf and restarts
# Moonraker + Klipper itself.
self._run_script(
MOONGATE_INSTALL_SCRIPT,
moonraker,
extra_env={"MOONGATE_PORT": str(port)},
)
except (GitException, CalledProcessError, OSError) as e:
Logger.print_error(f"Error during Moongate installation:\n{e}")
return
Logger.print_dialog(
DialogType.SUCCESS,
[
"Moongate installed successfully!",
"\n\n",
"Next steps:",
"● Install the Moongate app on your Android device.",
"● Run MOONGATE_PAIR in the Klipper console (or open the pair "
"page printed above) and scan the QR code.",
"● Updates from now on: Mainsail/Fluidd > Software Updates > Moongate.",
],
margin_bottom=1,
)
def update_extension(self, **kwargs) -> None:
Logger.print_status("Updating Moongate for Klipper ...")
if not check_file_exist(MOONGATE_DIR.joinpath(".git")):
Logger.print_info("Moongate does not seem to be installed. Skipping ...")
return
mr_instances: List[Moonraker] = get_instances(Moonraker)
if not mr_instances:
Logger.print_warn("No Moonraker instance found. Skipping ...")
return
try:
git_pull_wrapper(MOONGATE_DIR)
self._run_script(MOONGATE_UPDATE_SCRIPT, mr_instances[0])
InstanceManager.restart_all(mr_instances)
except (GitException, CalledProcessError, OSError) as e:
Logger.print_error(f"Error during Moongate update:\n{e}")
return
Logger.print_ok("Moongate updated successfully.", end="\n\n")
def remove_extension(self, **kwargs) -> None:
Logger.print_status("Removing Moongate for Klipper ...")
mr_instances: List[Moonraker] = get_instances(Moonraker)
if not get_confirm(
"This removes Moongate, cloudflared, both systemd services and all "
"Moongate config. Continue?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Removal aborted.")
return
# Preferred path: delegate to Moongate's own uninstaller, which stops
# and removes the services, cleans moonraker.conf, restores its backup
# and restarts Moonraker. MOONGATE_YES=1 makes it non-interactive
# (KIAUH already collected the confirmation above).
if check_file_exist(MOONGATE_UNINSTALL_SCRIPT):
try:
BackupService().backup_moonraker_conf()
target = mr_instances[0] if mr_instances else None
self._run_script(
MOONGATE_UNINSTALL_SCRIPT,
target,
extra_env={"MOONGATE_YES": "1"},
)
Logger.print_ok("Moongate removed successfully.")
return
except (CalledProcessError, OSError) as e:
Logger.print_error(f"Error during Moongate removal:\n{e}")
# fall through to a best-effort native cleanup
# Fallback: the upstream uninstaller is gone (repo already deleted).
# Do a best-effort native cleanup so moonraker.conf is left consistent.
Logger.print_warn(
"Moongate uninstaller not found — doing a best-effort cleanup. You "
"may need to remove cloudflared and the moongate-* systemd services "
"manually."
)
if mr_instances:
BackupService().backup_moonraker_conf()
remove_config_section(MOONGATE_UPDATER_NAME, mr_instances)
remove_config_section(MOONGATE_CONFIG_SECTION, mr_instances)
InstanceManager.restart_all(mr_instances)
Logger.print_ok("Moongate configuration removed.")
# ------------------------------------------------------------------ #
# helpers #
# ------------------------------------------------------------------ #
def _confirm_install(self) -> bool:
Logger.print_dialog(
DialogType.ATTENTION,
[
"Moongate pairs this printer with the Moongate Android app for "
"secure remote access and print monitoring.",
"\n\n",
"This is a heavier install than most extensions. It will:",
"● clone the Moongate repo to ~/moongate",
"● add the Moongate component to Moonraker and register it with "
"the update manager",
"● install cloudflared and open a Cloudflare quick-tunnel",
"● add two systemd services: moongate-authproxy + moongate-tunnel",
"● bind Moonraker to 127.0.0.1 (the auth proxy fronts the tunnel)",
"● add a tightly-scoped Avahi sudoers entry for LAN discovery",
"\n\n",
"Remote access relies on cloud infrastructure operated by the "
"Moongate author. Moongate is licensed under PolyForm "
"Noncommercial 1.0.0 (non-commercial use only).",
MOONGATE_REPO_URL,
],
margin_bottom=1,
)
return bool(
get_confirm(
"Continue Moongate installation?",
default_choice=True,
allow_go_back=True,
)
)
def _clone_or_update_repo(self) -> None:
if check_file_exist(MOONGATE_DIR.joinpath(".git")):
git_pull_wrapper(MOONGATE_DIR)
else:
git_clone_wrapper(MOONGATE_REPO, MOONGATE_DIR)
def _run_script(
self,
script: Path,
moonraker: Moonraker | None,
extra_env: Dict[str, str] | None = None,
) -> None:
env = os.environ.copy()
if moonraker is not None:
env["MOONRAKER_DIR"] = moonraker.moonraker_dir.as_posix()
env["PRINTER_DATA"] = moonraker.data_dir.as_posix()
if extra_env:
env.update(extra_env)
run(["bash", script.as_posix()], env=env, check=True)
+16 -1
View File
@@ -31,6 +31,7 @@ from components.moonraker.services.moonraker_setup_service import (
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
from core.services.backup_service import BackupService from core.services.backup_service import BackupService
from core.settings.kiauh_settings import KiauhSettings
from utils.git_utils import GitException, git_clone_wrapper from utils.git_utils import GitException, git_clone_wrapper
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
from utils.sys_utils import ( from utils.sys_utils import (
@@ -47,6 +48,11 @@ class RepoSwitchFailedException(Exception):
def run_switch_repo_routine( def run_switch_repo_routine(
name: Literal["klipper", "moonraker"], repo_url: str, branch: str name: Literal["klipper", "moonraker"], repo_url: str, branch: str
) -> None: ) -> None:
if name not in ("klipper", "moonraker"):
raise ValueError(
f"Invalid name: {name!r}. Must be 'klipper' or 'moonraker'."
)
repo_dir: Path = KLIPPER_DIR if name == "klipper" else MOONRAKER_DIR repo_dir: Path = KLIPPER_DIR if name == "klipper" else MOONRAKER_DIR
env_dir: Path = KLIPPER_ENV_DIR if name == "klipper" else MOONRAKER_ENV_DIR env_dir: Path = KLIPPER_ENV_DIR if name == "klipper" else MOONRAKER_ENV_DIR
req_file = KLIPPER_REQ_FILE if name == "klipper" else MOONRAKER_REQ_FILE req_file = KLIPPER_REQ_FILE if name == "klipper" else MOONRAKER_REQ_FILE
@@ -89,7 +95,16 @@ def run_switch_repo_routine(
# step 6: recreate python virtualenv # step 6: recreate python virtualenv
Logger.print_status(f"Recreating {_type.__name__} virtualenv ...") Logger.print_status(f"Recreating {_type.__name__} virtualenv ...")
if not create_python_venv(env_dir, force=True):
settings = KiauhSettings()
if name == "klipper":
use_python_binary = settings.klipper.use_python_binary
elif name == "moonraker":
use_python_binary = settings.moonraker.use_python_binary
if not create_python_venv(
env_dir, force=True, use_python_binary=use_python_binary
):
raise GitException(f"Failed to recreate virtualenv for {_type.__name__}") raise GitException(f"Failed to recreate virtualenv for {_type.__name__}")
else: else:
install_python_requirements(env_dir, req_file) install_python_requirements(env_dir, req_file)