mirror of
https://github.com/dw-0/kiauh.git
synced 2025-12-25 08:43:36 +05:00
Compare commits
20 Commits
ea8621af0c
...
docs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f08537bcf | ||
|
|
393822b8b6 | ||
|
|
e590f668e6 | ||
|
|
075f2d384b | ||
|
|
afdde34721 | ||
|
|
393dd1d5bf | ||
|
|
8170057434 | ||
|
|
985b66d41f | ||
|
|
f95d2586bf | ||
|
|
f5141e7eff | ||
|
|
33113e72e9 | ||
|
|
6f59fd06aa | ||
|
|
56ea43ccb6 | ||
|
|
25e22c993f | ||
|
|
ead521b377 | ||
|
|
3c952ccc12 | ||
|
|
c8f713c00e | ||
|
|
95cf809378 | ||
|
|
c91816d13f | ||
|
|
1a6f06eaf2 |
@@ -11,5 +11,5 @@ end_of_line = lf
|
|||||||
[*.py]
|
[*.py]
|
||||||
max_line_length = 88
|
max_line_length = 88
|
||||||
|
|
||||||
[*.{sh,yml,yaml}]
|
[*.{sh,yml,yaml,json,md}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
31
.github/workflows/deploy-docs.yml
vendored
Normal file
31
.github/workflows/deploy-docs.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Deploy Documentation
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- docs
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Configure Git Credentials
|
||||||
|
run: |
|
||||||
|
git config user.name github-actions[bot]
|
||||||
|
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.x
|
||||||
|
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
key: mkdocs-material-${{ env.cache_id }}
|
||||||
|
path: .cache
|
||||||
|
restore-keys: |
|
||||||
|
mkdocs-material-
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install -r requirements.txt
|
||||||
|
- name: Build and deploy documentation
|
||||||
|
run: mkdocs gh-deploy --force
|
||||||
6
Dockerfile
Normal file
6
Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM squidfunk/mkdocs-material:latest
|
||||||
|
|
||||||
|
# Install additional plugins required by our mkdocs configuration
|
||||||
|
RUN pip install \
|
||||||
|
mkdocs-git-revision-date-localized-plugin \
|
||||||
|
mkdocstrings[python]
|
||||||
206
README_zh.md
Normal file
206
README_zh.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# KIAUH - Klipper 安装与更新助手
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a>
|
||||||
|
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/kiauh.png" alt="KIAUH logo" height="181">
|
||||||
|
<h1 align="center">Klipper Installation And Update Helper</h1>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
一个方便的安装脚本,让安装Klipper(以及更多组件)变得轻松!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a><img src="https://img.shields.io/github/license/dw-0/kiauh"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/stars/dw-0/kiauh"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/forks/dw-0/kiauh"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/languages/top/dw-0/kiauh?logo=gnubash&logoColor=white"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/v/tag/dw-0/kiauh"></a>
|
||||||
|
<br />
|
||||||
|
<a><img src="https://img.shields.io/github/last-commit/dw-0/kiauh"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/contributors/dw-0/kiauh"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 📄 使用说明
|
||||||
|
|
||||||
|
### 📋 系统要求
|
||||||
|
KIAUH 是一个帮助您在 Linux 系统上安装 Klipper 的脚本工具,
|
||||||
|
它需要一个已经写入树莓派(或其他单板计算机)SD 卡的 Linux 系统。
|
||||||
|
如果您使用树莓派,推荐使用 `Raspberry Pi OS Lite (32位或64位)` 系统镜像。
|
||||||
|
[官方 Raspberry Pi Imager](https://www.raspberrypi.com/software/) 是将此类镜像写入 SD 卡的最简单方式。
|
||||||
|
|
||||||
|
* 下载、安装并启动 Raspberry Pi Imager 后,
|
||||||
|
选择 `Choose OS -> Raspberry Pi OS (other)`:
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager1.png" alt="KIAUH logo" height="350">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
* 然后选择 `Raspberry Pi OS Lite (32位)` (或如果您想使用64位版本):
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager2.png" alt="KIAUH logo" height="350">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
* 返回 Raspberry Pi Imager 主界面,选择对应的 SD 卡作为写入目标。
|
||||||
|
|
||||||
|
* 确保点击左下角的齿轮图标(在主菜单中)
|
||||||
|
启用 SSH 并配置 Wi-Fi。
|
||||||
|
|
||||||
|
* 如果您需要更多关于使用 Raspberry Pi Imager 的帮助,请访问 [官方文档](https://www.raspberrypi.com/documentation/computers/getting-started.html)。
|
||||||
|
|
||||||
|
这些步骤**仅适用于**您实际使用树莓派的情况。如果您想使用其他单板计算机(如香橙派或其他 Pi 衍生产品),
|
||||||
|
请查找如何将合适的 Linux 镜像写入 SD 卡(通常使用 Balena Etcher)。
|
||||||
|
同时确保 KIAUH 能够在您要安装的 Linux 发行版上运行。
|
||||||
|
您在使用基于 Debian 11 Bullseye 的系统时可能会获得最佳体验。
|
||||||
|
请阅读本文档下方的注意事项。
|
||||||
|
|
||||||
|
### 💾 下载并使用 KIAUH
|
||||||
|
|
||||||
|
**📢 免责声明:使用此脚本的风险由您自行承担!**
|
||||||
|
|
||||||
|
* **第一步:**
|
||||||
|
要下载此脚本,需要先安装 git。
|
||||||
|
如果您不确定是否已安装 git,请运行以下命令:
|
||||||
|
```shell
|
||||||
|
sudo apt-get update && sudo apt-get install git -y
|
||||||
|
```
|
||||||
|
|
||||||
|
* **第二步:**
|
||||||
|
安装完 git 后,
|
||||||
|
使用以下命令将 KIAUH 下载到您的主目录:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd ~ && git clone https://github.com/dw-0/kiauh.git
|
||||||
|
```
|
||||||
|
|
||||||
|
* **第三步:**
|
||||||
|
最后,通过运行以下命令启动 KIAUH:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./kiauh/kiauh.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
* **第四步:**
|
||||||
|
您现在应该会看到 KIAUH 的主菜单。
|
||||||
|
根据您的选择,
|
||||||
|
您会看到几个可选操作。
|
||||||
|
要选择某个操作,只需在 "Perform action" 提示后输入对应的数字并按回车键确认。
|
||||||
|
|
||||||
|
## ❗ 注意事项
|
||||||
|
|
||||||
|
### **📋 请查看 [更新日志](docs/changelog.md) 以了解可能的重要更新!**
|
||||||
|
|
||||||
|
- 主要在 Raspberry Pi OS Lite (Debian 10 Buster / Debian 11 Bullseye) 上测试
|
||||||
|
- 其他基于 Debian 的发行版(如 Ubuntu 20 到 22)也可能正常工作
|
||||||
|
- 据报告在 Armbian 上也可用,但未进行详细测试
|
||||||
|
- 在使用此脚本的过程中,
|
||||||
|
您会被要求输入 sudo 密码。
|
||||||
|
因为有几个功能需要 sudo 权限。
|
||||||
|
|
||||||
|
## 🌐 相关资源与更多信息
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr>
|
||||||
|
<th><h3><a href="https://github.com/Klipper3d/klipper">Klipper</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/Arksine/moonraker">Moonraker</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/mainsail-crew/mainsail">Mainsail</a></h3></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img src="https://raw.githubusercontent.com/Klipper3d/klipper/master/docs/img/klipper-logo.png" alt="Klipper Logo" height="64"></th>
|
||||||
|
<th><img src="https://avatars.githubusercontent.com/u/9563098?v=4" alt="Arksine avatar" height="64"></th>
|
||||||
|
<th><img src="https://raw.githubusercontent.com/mainsail-crew/docs/master/assets/img/logo.png" alt="Mainsail Logo" height="64"></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>由 <a href="https://github.com/KevinOConnor">KevinOConnor</a></th>
|
||||||
|
<th>由 <a href="https://github.com/Arksine">Arksine</a></th>
|
||||||
|
<th>由 <a href="https://github.com/mainsail-crew">mainsail-crew</a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th><h3><a href="https://github.com/fluidd-core/fluidd">Fluidd</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/jordanruthe/KlipperScreen">KlipperScreen</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th>
|
||||||
|
</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://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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>由 <a href="https://github.com/fluidd-core">fluidd-core</a></th>
|
||||||
|
<th>由 <a href="https://github.com/jordanruthe">jordanruthe</a></th>
|
||||||
|
<th>由 <a href="https://github.com/OctoPrint">OctoPrint</a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/TheSpaghettiDetective/moonraker-obico">Obico for Klipper</a></h3></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img src="https://avatars.githubusercontent.com/u/52351624?v=4" alt="nlef avatar" height="64"></th>
|
||||||
|
<th><img src="https://avatars.githubusercontent.com/u/5917231?v=4" alt="Kragrathea avatar" height="64"></th>
|
||||||
|
<th><img src="https://avatars.githubusercontent.com/u/46323662?s=200&v=4" alt="Obico logo" height="64"></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>由 <a href="https://github.com/nlef">nlef</a></th>
|
||||||
|
<th>由 <a href="https://github.com/Kragrathea">Kragrathea</a></th>
|
||||||
|
<th>由 <a href="https://github.com/TheSpaghettiDetective">Obico</a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th>
|
||||||
|
<th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/crysxd/OctoApp-Plugin">OctoApp For Klipper</a></h3></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><a href="https://github.com/Clon1998/mobileraker_companion"><img src="https://raw.githubusercontent.com/Clon1998/mobileraker/master/assets/icon/mr_appicon.png" alt="Mobileraker Logo" height="64"></a></th>
|
||||||
|
<th><a href="https://octoeverywhere.com/?source=kiauh_readme"><img src="https://octoeverywhere.com/img/logo.svg" alt="OctoEverywhere Logo" height="64"></a></th>
|
||||||
|
<th><a href="https://octoapp.eu/?source=kiauh_readme"><img src="https://octoapp.eu/octoapp.webp" alt="OctoApp Logo" height="64"></a></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>由 <a href="https://github.com/Clon1998">Patrick Schmidt</a></th>
|
||||||
|
<th>由 <a href="https://github.com/QuinnDamerell">Quinn Damerell</a></th>
|
||||||
|
<th>由 <a href="https://github.com/crysxd">Christian Würthner</a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th><h3><a href="https://github.com/staubgeborener/klipper-backup">Klipper-Backup</a></h3></th>
|
||||||
|
<th><h3><a href="https://simplyprint.io/">SimplyPrint for Klipper</a></h3></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><a href="https://github.com/staubgeborener/klipper-backup"><img src="https://avatars.githubusercontent.com/u/28908603?v=4" alt="Staubgeroner Avatar" height="64"></a></th>
|
||||||
|
<th><a href="https://github.com/SimplyPrint"><img src="https://avatars.githubusercontent.com/u/64896552?s=200&v=4" alt="" height="64"></a></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>由 <a href="https://github.com/Staubgeborener">Staubgeborener</a></th>
|
||||||
|
<th>由 <a href="https://github.com/SimplyPrint">SimplyPrint</a></th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 🎖️ 贡献者
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/dw-0/kiauh/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=dw-0/kiauh" alt=""/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://repobeats.axiom.co/api/embed/a1afbda9190c04a90cf4bd3061e5573bc836cb05.svg" alt="Repobeats analytics image"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## ✨ 特别感谢
|
||||||
|
|
||||||
|
* 非常感谢 [lixxbox](https://github.com/lixxbox) 设计了如此出色的 KIAUH 标志!
|
||||||
|
* 同时,非常感谢所有通过 [Ko-fi](https://ko-fi.com/dw__0) 支持我的工作的人!
|
||||||
|
* 最后但同样重要的是:感谢所有为 Klipper 社区做出贡献的成员,以及喜欢和分享这个项目的朋友们!
|
||||||
|
|
||||||
|
<h4 align="center">特别感谢 JetBrains 为本项目提供其出色的软件赞助!</h4>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.jetbrains.com/community/opensource/#support" target="_blank">
|
||||||
|
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo." height="128">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
@@ -13,6 +13,10 @@ repositories:
|
|||||||
https://github.com/Klipper3d/klipper
|
https://github.com/Klipper3d/klipper
|
||||||
|
|
||||||
[moonraker]
|
[moonraker]
|
||||||
|
# Moonraker supports two optional Python packages that can be used to reduce its CPU load
|
||||||
|
# If set to true, those packages will be installed during the Moonraker installation
|
||||||
|
optional_speedups: True
|
||||||
|
|
||||||
# add custom repositories here, if at least one is given, the first in the list will be used by default
|
# add custom repositories here, if at least one is given, the first in the list will be used by default
|
||||||
# otherwise the official repository is used
|
# otherwise the official repository is used
|
||||||
#
|
#
|
||||||
|
|||||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
mkdocs:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./:/docs
|
||||||
|
command: serve --dev-addr=0.0.0.0:8000
|
||||||
BIN
docs/assets/logo-large.png
Normal file
BIN
docs/assets/logo-large.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
BIN
docs/assets/rpi_imager1.png
Normal file
BIN
docs/assets/rpi_imager1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/assets/rpi_imager2.png
Normal file
BIN
docs/assets/rpi_imager2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -276,7 +276,7 @@ Each service gets its corresponding instance added to the service filename.
|
|||||||
|
|
||||||
* The user can now choose to install Klipper as a systemd service.
|
* The user can now choose to install Klipper as a systemd service.
|
||||||
|
|
||||||
* The Shell Command extension and `shell_command.py` got renamed to G-Code Shell Command extension and `gcode_shell_command.py`. In case the [pending PR](https://github.com/KevinOConnor/klipper/pull/2173) will be merged in the future, this was an early attempt to dodge possible incompatibilities. The [G-Code Shell Command docs](gcode_shell_command.md) has been updated accordingly.
|
* The Shell Command extension and `shell_command.py` got renamed to G-Code Shell Command extension and `gcode_shell_command.py`. In case the [pending PR](https://github.com/KevinOConnor/klipper/pull/2173) will be merged in the future, this was an early attempt to dodge possible incompatibilities. The [G-Code Shell Command docs](extensions/gcode-shell-command) has been updated accordingly.
|
||||||
|
|
||||||
* The way how KIAUH interacts and writes to the users printer.cfg got changed. Usually KIAUH wrote everything directly into the printer.cfg. The way it will work from now on is, that a new file called `kiauh.cfg` will be created if there is something that needs to be written to the printer.cfg and everything gets written to `kiauh.cfg` instead. The only thing which then gets written to the users printer.cfg is `[include kiauh.cfg]`. This line will be located at the very top of the existing printer.cfg with a little comment as a note. The user can then decide to either keep the `kiauh.cfg` or take its content, places it into the printer.cfg directly and remove the `[include kiauh.cfg]`.
|
* The way how KIAUH interacts and writes to the users printer.cfg got changed. Usually KIAUH wrote everything directly into the printer.cfg. The way it will work from now on is, that a new file called `kiauh.cfg` will be created if there is something that needs to be written to the printer.cfg and everything gets written to `kiauh.cfg` instead. The only thing which then gets written to the users printer.cfg is `[include kiauh.cfg]`. This line will be located at the very top of the existing printer.cfg with a little comment as a note. The user can then decide to either keep the `kiauh.cfg` or take its content, places it into the printer.cfg directly and remove the `[include kiauh.cfg]`.
|
||||||
|
|
||||||
1
docs/extensions/index.md
Normal file
1
docs/extensions/index.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Community Extensions
|
||||||
23
docs/index.md
Normal file
23
docs/index.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
!!! tip "Important"
|
||||||
|
This documentation is for KIAUH version 6 and still work in progress!
|
||||||
|
|
||||||
|
<h1 align="center">
|
||||||
|
KIAUH - Klipper Installation And Update Helper
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="assets/logo-large.png" alt="KIAUH logo" width="400"/>
|
||||||
|
</p>
|
||||||
|
<p align="center" style="font-size: 1.2em; font-weight: bold;">
|
||||||
|
A handy installation script that makes installing Klipper (and more) a breeze!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Easy installation of Klipper and related components
|
||||||
|
- Support for multiple instances
|
||||||
|
- Extension system for additional functionality
|
||||||
|
- Configuration management
|
||||||
|
- And more!
|
||||||
|
|
||||||
39
docs/setup/installation.md
Normal file
39
docs/setup/installation.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Installing KIAUH
|
||||||
|
|
||||||
|
In the following sections, you will be guided through the installation
|
||||||
|
process step-by-step.
|
||||||
|
|
||||||
|
To use KIAUH, it is enough to download the script and run it on your
|
||||||
|
Raspberry Pi or other compatible device. If you need to know how to
|
||||||
|
set up a Raspberry Pi or if you are unsure whether your current setup
|
||||||
|
is sufficient, please refer to the [Raspberry Pi Installation Guide](raspberry-pi-setup.md)
|
||||||
|
and follow the steps therein. Afterwards, you can return to this guide to install KIAUH.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
Before you can download and run KIAUH, you need to ensure that ``git`` is
|
||||||
|
installed on your system. Open a terminal and run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update && sudo apt-get install git -y
|
||||||
|
```
|
||||||
|
|
||||||
|
### Downloading KIAUH
|
||||||
|
After `git` was successfully installed, you can download KIAUH by
|
||||||
|
cloning the repository from GitHub. It is recommended to clone it into
|
||||||
|
your home directory. Run the following command in your terminal:
|
||||||
|
```bash
|
||||||
|
cd ~ && git clone https://github.com/dw-0/kiauh.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running KIAUH
|
||||||
|
Once the repository is cloned, you can start KIAUH. Make sure you are in
|
||||||
|
your home directory and execute the script by running the following
|
||||||
|
command:
|
||||||
|
```bash
|
||||||
|
./kiauh/kiauh.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
After executing the command, you will be presented with the KIAUH menu,
|
||||||
|
which allows you to install and manage various 3D printing software.
|
||||||
|
For more information on how to use KIAUH, please refer to the
|
||||||
|
[Usage Guide](usage.md).
|
||||||
49
docs/setup/raspberry-pi-setup.md
Normal file
49
docs/setup/raspberry-pi-setup.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Raspberry Pi Setup
|
||||||
|
|
||||||
|
This guide will help you set up a Raspberry Pi for running Klipper and other,
|
||||||
|
Klipper related 3D printing software. In case you are using a different single-board
|
||||||
|
computer (SBC), please refer to the manufacturer's instructions for installing
|
||||||
|
a compatible version of Linux on your device.
|
||||||
|
|
||||||
|
It is assumed that you have at least a Raspberry Pi 3 or newer, along with a
|
||||||
|
microSD card (at least 8GB, preferably 16GB or more) and a power supply.
|
||||||
|
Additionally, you will need a computer with an SD card reader to prepare
|
||||||
|
the microSD card.
|
||||||
|
|
||||||
|
KIAUH requires a Linux operating system that has already been flashed to your
|
||||||
|
Raspberry Pi's (or other SBC's) microSD card. As a result, you must ensure that you
|
||||||
|
already have a functional Linux system on hand before you can proceed with
|
||||||
|
installing KIAUH. `Raspberry Pi OS Lite` (either 32bit or 64bit) is a recommended Linux image
|
||||||
|
if you are using a Raspberry Pi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To flash `Raspberry Pi OS Lite` to your microSD card using the official [Raspberry Pi Imager](https://www.raspberrypi.com/software/),
|
||||||
|
follow the steps below. If you encounter any issues or need further assistance, please refer to the [official Raspberry Pi documentation](https://www.raspberrypi.com/documentation/computers/getting-started.html).
|
||||||
|
|
||||||
|
1. Open the Raspberry Pi Imager application on your computer.
|
||||||
|
2. Click on `Choose OS` and select `Raspberry Pi OS (other)`.
|
||||||
|

|
||||||
|
3. Choose `Raspberry Pi OS Lite (32bit)` (or 64bit if desired).
|
||||||
|

|
||||||
|
4. Insert the microSD card into your computer's SD card reader.
|
||||||
|
5. In the main menu of the Imager, select the correct microSD card.
|
||||||
|
6. Click the gear icon at the bottom left of the main menu to open advanced options.
|
||||||
|
7. Enable SSH and enter your Wi-Fi credentials.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Wi-Fi is only necessary if you want to connect to your Raspberry Pi over a wireless network. If you plan to use a wired Ethernet connection, you can skip this step. SSH is required for remote access to your Raspberry Pi, so make sure to enable it.
|
||||||
|
|
||||||
|
8. Click `Save` to close the advanced options menu.
|
||||||
|
9. Click `Write` to start flashing the image to the microSD card.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
All data on the microSD card will be overwritten!
|
||||||
|
|
||||||
|
10. Once the flashing process is complete, safely eject the microSD card from your computer.
|
||||||
|
11. Insert the microSD card into your Raspberry Pi.
|
||||||
|
12. Connect your Raspberry Pi to a power source to boot it up.
|
||||||
|
13. Wait for a few minutes to allow the Raspberry Pi to complete its initial setup.
|
||||||
|
14. You can now connect to your Raspberry Pi via SSH using the IP address assigned by your router. The default username is `pi` and the default password is `raspberry`.
|
||||||
|
|
||||||
|
If you successfully connected to your Raspberry Pi via SSH, you can proceed to install KIAUH by following the instructions in the [Installation Guide](installation.md).
|
||||||
@@ -102,6 +102,8 @@ class KlipperSetupService:
|
|||||||
self.moonraker_list = self.misvc.get_all_instances()
|
self.moonraker_list = self.misvc.get_all_instances()
|
||||||
|
|
||||||
def install(self) -> None:
|
def install(self) -> None:
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
Logger.print_status("Installing Klipper ...")
|
Logger.print_status("Installing Klipper ...")
|
||||||
|
|
||||||
match_moonraker: bool = False
|
match_moonraker: bool = False
|
||||||
@@ -275,7 +277,7 @@ class KlipperSetupService:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
install_klipper_packages()
|
install_klipper_packages()
|
||||||
if create_python_venv(KLIPPER_ENV_DIR):
|
if create_python_venv(KLIPPER_ENV_DIR, False, False, self.settings.klipper.use_python_binary):
|
||||||
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE)
|
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE)
|
||||||
except Exception:
|
except Exception:
|
||||||
Logger.print_error("Error during installation of Klipper requirements!")
|
Logger.print_error("Error during installation of Klipper requirements!")
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
|
|||||||
self.flash_options.selected_baudrate = get_number_input(
|
self.flash_options.selected_baudrate = get_number_input(
|
||||||
question="Please set the baud rate",
|
question="Please set the baud rate",
|
||||||
default=250000,
|
default=250000,
|
||||||
min_count=0,
|
min_value=0,
|
||||||
allow_go_back=True,
|
allow_go_back=True,
|
||||||
)
|
)
|
||||||
KlipperFlashOverviewMenu(previous_menu=self.__class__).run()
|
KlipperFlashOverviewMenu(previous_menu=self.__class__).run()
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ from __future__ import annotations
|
|||||||
import textwrap
|
import textwrap
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from components.moonraker import moonraker_remove
|
from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService
|
||||||
from core.menus import Option
|
from core.menus import FooterType, Option
|
||||||
from core.menus.base_menu import BaseMenu
|
from core.menus.base_menu import BaseMenu
|
||||||
from core.types.color import Color
|
from core.types.color import Color
|
||||||
|
|
||||||
@@ -21,14 +21,19 @@ from core.types.color import Color
|
|||||||
class MoonrakerRemoveMenu(BaseMenu):
|
class MoonrakerRemoveMenu(BaseMenu):
|
||||||
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
|
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.title = "Remove Moonraker"
|
self.title = "Remove Moonraker"
|
||||||
self.title_color = Color.RED
|
self.title_color = Color.RED
|
||||||
self.previous_menu: Type[BaseMenu] | None = previous_menu
|
self.previous_menu: Type[BaseMenu] | None = previous_menu
|
||||||
self.remove_moonraker_service = False
|
self.footer_type = FooterType.BACK
|
||||||
self.remove_moonraker_dir = False
|
|
||||||
self.remove_moonraker_env = False
|
self.rm_svc = False
|
||||||
self.remove_moonraker_polkit = False
|
self.rm_dir = False
|
||||||
self.selection_state = False
|
self.rm_env = False
|
||||||
|
self.rm_pk = False
|
||||||
|
self.select_state = False
|
||||||
|
|
||||||
|
self.mrsvc = MoonrakerSetupService()
|
||||||
|
|
||||||
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
|
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
|
||||||
from core.menus.remove_menu import RemoveMenu
|
from core.menus.remove_menu import RemoveMenu
|
||||||
@@ -48,17 +53,18 @@ class MoonrakerRemoveMenu(BaseMenu):
|
|||||||
def print_menu(self) -> None:
|
def print_menu(self) -> None:
|
||||||
checked = f"[{Color.apply('x', Color.CYAN)}]"
|
checked = f"[{Color.apply('x', Color.CYAN)}]"
|
||||||
unchecked = "[ ]"
|
unchecked = "[ ]"
|
||||||
o1 = checked if self.remove_moonraker_service else unchecked
|
o1 = checked if self.rm_svc else unchecked
|
||||||
o2 = checked if self.remove_moonraker_dir else unchecked
|
o2 = checked if self.rm_dir else unchecked
|
||||||
o3 = checked if self.remove_moonraker_env else unchecked
|
o3 = checked if self.rm_env else unchecked
|
||||||
o4 = checked if self.remove_moonraker_polkit else unchecked
|
o4 = checked if self.rm_pk else unchecked
|
||||||
|
sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything"
|
||||||
menu = textwrap.dedent(
|
menu = textwrap.dedent(
|
||||||
f"""
|
f"""
|
||||||
╟───────────────────────────────────────────────────────╢
|
╟───────────────────────────────────────────────────────╢
|
||||||
║ Enter a number and hit enter to select / deselect ║
|
║ Enter a number and hit enter to select / deselect ║
|
||||||
║ the specific option for removal. ║
|
║ the specific option for removal. ║
|
||||||
╟───────────────────────────────────────────────────────╢
|
╟───────────────────────────────────────────────────────╢
|
||||||
║ a) {self._get_selection_state_str():37} ║
|
║ a) {sel_state:49} ║
|
||||||
╟───────────────────────────────────────────────────────╢
|
╟───────────────────────────────────────────────────────╢
|
||||||
║ 1) {o1} Remove Service ║
|
║ 1) {o1} Remove Service ║
|
||||||
║ 2) {o2} Remove Local Repository ║
|
║ 2) {o2} Remove Local Repository ║
|
||||||
@@ -72,57 +78,33 @@ class MoonrakerRemoveMenu(BaseMenu):
|
|||||||
print(menu, end="")
|
print(menu, end="")
|
||||||
|
|
||||||
def toggle_all(self, **kwargs) -> None:
|
def toggle_all(self, **kwargs) -> None:
|
||||||
self.selection_state = not self.selection_state
|
self.select_state = not self.select_state
|
||||||
self.remove_moonraker_service = self.selection_state
|
self.rm_svc = self.select_state
|
||||||
self.remove_moonraker_dir = self.selection_state
|
self.rm_dir = self.select_state
|
||||||
self.remove_moonraker_env = self.selection_state
|
self.rm_env = self.select_state
|
||||||
self.remove_moonraker_polkit = self.selection_state
|
self.rm_pk = self.select_state
|
||||||
|
|
||||||
def toggle_remove_moonraker_service(self, **kwargs) -> None:
|
def toggle_remove_moonraker_service(self, **kwargs) -> None:
|
||||||
self.remove_moonraker_service = not self.remove_moonraker_service
|
self.rm_svc = not self.rm_svc
|
||||||
|
|
||||||
def toggle_remove_moonraker_dir(self, **kwargs) -> None:
|
def toggle_remove_moonraker_dir(self, **kwargs) -> None:
|
||||||
self.remove_moonraker_dir = not self.remove_moonraker_dir
|
self.rm_dir = not self.rm_dir
|
||||||
|
|
||||||
def toggle_remove_moonraker_env(self, **kwargs) -> None:
|
def toggle_remove_moonraker_env(self, **kwargs) -> None:
|
||||||
self.remove_moonraker_env = not self.remove_moonraker_env
|
self.rm_env = not self.rm_env
|
||||||
|
|
||||||
def toggle_remove_moonraker_polkit(self, **kwargs) -> None:
|
def toggle_remove_moonraker_polkit(self, **kwargs) -> None:
|
||||||
self.remove_moonraker_polkit = not self.remove_moonraker_polkit
|
self.rm_pk = not self.rm_pk
|
||||||
|
|
||||||
def run_removal_process(self, **kwargs) -> None:
|
def run_removal_process(self, **kwargs) -> None:
|
||||||
if (
|
if not self.rm_svc and not self.rm_dir and not self.rm_env and not self.rm_pk:
|
||||||
not self.remove_moonraker_service
|
msg = "Nothing selected! Select options to remove first."
|
||||||
and not self.remove_moonraker_dir
|
print(Color.apply(msg, Color.RED))
|
||||||
and not self.remove_moonraker_env
|
|
||||||
and not self.remove_moonraker_polkit
|
|
||||||
):
|
|
||||||
print(
|
|
||||||
Color.apply(
|
|
||||||
"Nothing selected! Select options to remove first.", Color.RED
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
moonraker_remove.run_moonraker_removal(
|
self.mrsvc.remove(self.rm_svc, self.rm_dir, self.rm_env, self.rm_pk)
|
||||||
self.remove_moonraker_service,
|
|
||||||
self.remove_moonraker_dir,
|
|
||||||
self.remove_moonraker_env,
|
|
||||||
self.remove_moonraker_polkit,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.remove_moonraker_service = False
|
self.rm_svc = False
|
||||||
self.remove_moonraker_dir = False
|
self.rm_dir = False
|
||||||
self.remove_moonraker_env = False
|
self.rm_env = False
|
||||||
self.remove_moonraker_polkit = False
|
self.rm_pk = False
|
||||||
|
|
||||||
self._go_back()
|
|
||||||
|
|
||||||
def _get_selection_state_str(self) -> str:
|
|
||||||
return (
|
|
||||||
"Select everything" if not self.selection_state else "Deselect everything"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _go_back(self, **kwargs) -> None:
|
|
||||||
if self.previous_menu is not None:
|
|
||||||
self.previous_menu().run()
|
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
# ======================================================================= #
|
|
||||||
# Copyright (C) 2020 - 2025 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 subprocess import DEVNULL, PIPE, CalledProcessError, run
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from components.klipper.klipper_dialogs import print_instance_overview
|
|
||||||
from components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR
|
|
||||||
from components.moonraker.moonraker import Moonraker
|
|
||||||
from core.instance_manager.instance_manager import InstanceManager
|
|
||||||
from core.logger import Logger
|
|
||||||
from utils.fs_utils import run_remove_routines
|
|
||||||
from utils.input_utils import get_selection_input
|
|
||||||
from utils.instance_utils import get_instances
|
|
||||||
from utils.sys_utils import unit_file_exists
|
|
||||||
|
|
||||||
|
|
||||||
def run_moonraker_removal(
|
|
||||||
remove_service: bool,
|
|
||||||
remove_dir: bool,
|
|
||||||
remove_env: bool,
|
|
||||||
remove_polkit: bool,
|
|
||||||
) -> None:
|
|
||||||
instances = get_instances(Moonraker)
|
|
||||||
|
|
||||||
if remove_service:
|
|
||||||
Logger.print_status("Removing Moonraker instances ...")
|
|
||||||
if instances:
|
|
||||||
instances_to_remove = select_instances_to_remove(instances)
|
|
||||||
remove_instances(instances_to_remove)
|
|
||||||
else:
|
|
||||||
Logger.print_info("No Moonraker Services installed! Skipped ...")
|
|
||||||
|
|
||||||
delete_remaining: bool = remove_polkit or remove_dir or remove_env
|
|
||||||
if delete_remaining and unit_file_exists("moonraker", suffix="service"):
|
|
||||||
Logger.print_info("There are still other Moonraker services installed")
|
|
||||||
Logger.print_info(
|
|
||||||
"● Moonraker PolicyKit rules were not removed.", prefix=False
|
|
||||||
)
|
|
||||||
Logger.print_info(f"● '{MOONRAKER_DIR}' was not removed.", prefix=False)
|
|
||||||
Logger.print_info(f"● '{MOONRAKER_ENV_DIR}' was not removed.", prefix=False)
|
|
||||||
else:
|
|
||||||
if remove_polkit:
|
|
||||||
Logger.print_status("Removing all Moonraker policykit rules ...")
|
|
||||||
remove_polkit_rules()
|
|
||||||
if remove_dir:
|
|
||||||
Logger.print_status("Removing Moonraker local repository ...")
|
|
||||||
run_remove_routines(MOONRAKER_DIR)
|
|
||||||
if remove_env:
|
|
||||||
Logger.print_status("Removing Moonraker Python environment ...")
|
|
||||||
run_remove_routines(MOONRAKER_ENV_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
def select_instances_to_remove(
|
|
||||||
instances: List[Moonraker],
|
|
||||||
) -> List[Moonraker] | None:
|
|
||||||
start_index = 1
|
|
||||||
options = [str(i + start_index) for i in range(len(instances))]
|
|
||||||
options.extend(["a", "b"])
|
|
||||||
instance_map = {options[i]: instances[i] for i in range(len(instances))}
|
|
||||||
|
|
||||||
print_instance_overview(
|
|
||||||
instances,
|
|
||||||
start_index=start_index,
|
|
||||||
show_index=True,
|
|
||||||
show_select_all=True,
|
|
||||||
)
|
|
||||||
selection = get_selection_input("Select Moonraker instance to remove", options)
|
|
||||||
|
|
||||||
instances_to_remove = []
|
|
||||||
if selection == "b":
|
|
||||||
return None
|
|
||||||
elif selection == "a":
|
|
||||||
instances_to_remove.extend(instances)
|
|
||||||
else:
|
|
||||||
instances_to_remove.append(instance_map[selection])
|
|
||||||
|
|
||||||
return instances_to_remove
|
|
||||||
|
|
||||||
|
|
||||||
def remove_instances(
|
|
||||||
instance_list: List[Moonraker] | None,
|
|
||||||
) -> None:
|
|
||||||
if not instance_list:
|
|
||||||
Logger.print_info("No Moonraker instances found. Skipped ...")
|
|
||||||
return
|
|
||||||
for instance in instance_list:
|
|
||||||
Logger.print_status(f"Removing instance {instance.service_file_path.stem} ...")
|
|
||||||
InstanceManager.remove(instance)
|
|
||||||
delete_moonraker_env_file(instance)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_polkit_rules() -> None:
|
|
||||||
if not MOONRAKER_DIR.exists():
|
|
||||||
log = "Cannot remove policykit rules. Moonraker directory not found."
|
|
||||||
Logger.print_warn(log)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
cmd = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"]
|
|
||||||
run(cmd, stderr=PIPE, stdout=DEVNULL, check=True)
|
|
||||||
except CalledProcessError as e:
|
|
||||||
Logger.print_error(f"Error while removing policykit rules: {e}")
|
|
||||||
|
|
||||||
Logger.print_ok("Policykit rules successfully removed!")
|
|
||||||
|
|
||||||
|
|
||||||
def delete_moonraker_env_file(instance: Moonraker):
|
|
||||||
Logger.print_status(f"Remove '{instance.env_file}'")
|
|
||||||
if not instance.env_file.exists():
|
|
||||||
msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..."
|
|
||||||
Logger.print_info(msg)
|
|
||||||
return
|
|
||||||
run_remove_routines(instance.env_file)
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
# ======================================================================= #
|
|
||||||
# Copyright (C) 2020 - 2025 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 subprocess
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from components.klipper.klipper import Klipper
|
|
||||||
from components.moonraker import (
|
|
||||||
EXIT_MOONRAKER_SETUP,
|
|
||||||
MOONRAKER_DEPS_JSON_FILE,
|
|
||||||
MOONRAKER_DIR,
|
|
||||||
MOONRAKER_ENV_DIR,
|
|
||||||
MOONRAKER_INSTALL_SCRIPT,
|
|
||||||
MOONRAKER_REPO_URL,
|
|
||||||
MOONRAKER_REQ_FILE,
|
|
||||||
MOONRAKER_SPEEDUPS_REQ_FILE,
|
|
||||||
POLKIT_FILE,
|
|
||||||
POLKIT_LEGACY_FILE,
|
|
||||||
POLKIT_SCRIPT,
|
|
||||||
POLKIT_USR_FILE,
|
|
||||||
)
|
|
||||||
from components.moonraker.moonraker import Moonraker
|
|
||||||
from components.moonraker.moonraker_dialogs import print_moonraker_overview
|
|
||||||
from components.moonraker.services.moonraker_instance_service import (
|
|
||||||
MoonrakerInstanceService,
|
|
||||||
)
|
|
||||||
from components.moonraker.utils.sysdeps_parser import SysDepsParser
|
|
||||||
from components.moonraker.utils.utils import (
|
|
||||||
backup_moonraker_dir,
|
|
||||||
create_example_moonraker_conf,
|
|
||||||
load_sysdeps_json,
|
|
||||||
)
|
|
||||||
from components.webui_client.client_utils import (
|
|
||||||
enable_mainsail_remotemode,
|
|
||||||
get_existing_clients,
|
|
||||||
)
|
|
||||||
from components.webui_client.mainsail_data import MainsailData
|
|
||||||
from core.instance_manager.instance_manager import InstanceManager
|
|
||||||
from core.logger import DialogType, Logger
|
|
||||||
from core.settings.kiauh_settings import KiauhSettings
|
|
||||||
from core.types.color import Color
|
|
||||||
from utils.common import check_install_dependencies
|
|
||||||
from utils.fs_utils import check_file_exist
|
|
||||||
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
|
|
||||||
from utils.input_utils import (
|
|
||||||
get_confirm,
|
|
||||||
get_selection_input,
|
|
||||||
)
|
|
||||||
from utils.instance_utils import get_instances
|
|
||||||
from utils.sys_utils import (
|
|
||||||
check_python_version,
|
|
||||||
cmd_sysctl_manage,
|
|
||||||
cmd_sysctl_service,
|
|
||||||
create_python_venv,
|
|
||||||
get_ipv4_addr,
|
|
||||||
install_python_requirements,
|
|
||||||
parse_packages_from_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def install_moonraker() -> None:
|
|
||||||
klipper_list: List[Klipper] = get_instances(Klipper)
|
|
||||||
|
|
||||||
if not check_moonraker_install_requirements(klipper_list):
|
|
||||||
return
|
|
||||||
|
|
||||||
instance_service = MoonrakerInstanceService()
|
|
||||||
instance_service.load_instances()
|
|
||||||
|
|
||||||
moonraker_list: List[Moonraker] = instance_service.get_all_instances()
|
|
||||||
new_instances: List[Moonraker] = []
|
|
||||||
selected_option: str | Klipper
|
|
||||||
|
|
||||||
if len(klipper_list) == 1:
|
|
||||||
suffix: str = klipper_list[0].suffix
|
|
||||||
new_inst = instance_service.create_new_instance(suffix)
|
|
||||||
new_instances.append(new_inst)
|
|
||||||
|
|
||||||
else:
|
|
||||||
print_moonraker_overview(
|
|
||||||
klipper_list,
|
|
||||||
moonraker_list,
|
|
||||||
show_index=True,
|
|
||||||
show_select_all=True,
|
|
||||||
)
|
|
||||||
options = {str(i + 1): k for i, k in enumerate(klipper_list)}
|
|
||||||
additional_options = {"a": None, "b": None}
|
|
||||||
options = {**options, **additional_options}
|
|
||||||
question = "Select Klipper instance to setup Moonraker for"
|
|
||||||
selected_option = get_selection_input(question, options)
|
|
||||||
|
|
||||||
if selected_option == "b":
|
|
||||||
Logger.print_status(EXIT_MOONRAKER_SETUP)
|
|
||||||
return
|
|
||||||
|
|
||||||
if selected_option == "a":
|
|
||||||
new_inst_list: List[Moonraker] = [
|
|
||||||
instance_service.create_new_instance(k.suffix) for k in klipper_list
|
|
||||||
]
|
|
||||||
new_instances.extend(new_inst_list)
|
|
||||||
else:
|
|
||||||
klipper_instance: Klipper | None = options.get(selected_option)
|
|
||||||
if klipper_instance is None:
|
|
||||||
raise Exception("Error selecting instance!")
|
|
||||||
new_inst = instance_service.create_new_instance(klipper_instance.suffix)
|
|
||||||
new_instances.append(new_inst)
|
|
||||||
|
|
||||||
create_example_cfg = get_confirm("Create example moonraker.conf?")
|
|
||||||
|
|
||||||
try:
|
|
||||||
check_install_dependencies()
|
|
||||||
setup_moonraker_prerequesites()
|
|
||||||
install_moonraker_polkit()
|
|
||||||
|
|
||||||
ports_map = instance_service.get_instance_port_map()
|
|
||||||
for instance in new_instances:
|
|
||||||
instance.create()
|
|
||||||
cmd_sysctl_service(instance.service_file_path.name, "enable")
|
|
||||||
|
|
||||||
if create_example_cfg:
|
|
||||||
# if a webclient and/or it's config is installed, patch
|
|
||||||
# its update section to the config
|
|
||||||
clients = get_existing_clients()
|
|
||||||
create_example_moonraker_conf(instance, ports_map, clients)
|
|
||||||
|
|
||||||
cmd_sysctl_service(instance.service_file_path.name, "start")
|
|
||||||
|
|
||||||
cmd_sysctl_manage("daemon-reload")
|
|
||||||
|
|
||||||
# if mainsail is installed, and we installed
|
|
||||||
# multiple moonraker instances, we enable mainsails remote mode
|
|
||||||
if MainsailData().client_dir.exists() and len(moonraker_list) > 1:
|
|
||||||
enable_mainsail_remotemode()
|
|
||||||
|
|
||||||
instance_service.load_instances()
|
|
||||||
new_instances = [
|
|
||||||
instance_service.get_instance_by_suffix(i.suffix) for i in new_instances
|
|
||||||
]
|
|
||||||
|
|
||||||
ip: str = get_ipv4_addr()
|
|
||||||
# noinspection HttpUrlsUsage
|
|
||||||
url_list = [
|
|
||||||
f"● {i.service_file_path.stem}: http://{ip}:{i.port}"
|
|
||||||
for i in new_instances
|
|
||||||
if i.port
|
|
||||||
]
|
|
||||||
dialog_content = []
|
|
||||||
if url_list:
|
|
||||||
dialog_content.append("You can access Moonraker via the following URL:")
|
|
||||||
dialog_content.extend(url_list)
|
|
||||||
|
|
||||||
Logger.print_dialog(
|
|
||||||
DialogType.CUSTOM,
|
|
||||||
custom_title="Moonraker successfully installed!",
|
|
||||||
custom_color=Color.GREEN,
|
|
||||||
content=dialog_content,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
Logger.print_error(f"Error while installing Moonraker: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def check_moonraker_install_requirements(klipper_list: List[Klipper]) -> bool:
|
|
||||||
def check_klipper_instances() -> bool:
|
|
||||||
if len(klipper_list) >= 1:
|
|
||||||
return True
|
|
||||||
|
|
||||||
Logger.print_warn("Klipper not installed!")
|
|
||||||
Logger.print_warn("Moonraker cannot be installed! Install Klipper first.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return check_python_version(3, 7) and check_klipper_instances()
|
|
||||||
|
|
||||||
|
|
||||||
def setup_moonraker_prerequesites() -> None:
|
|
||||||
settings = KiauhSettings()
|
|
||||||
default_repo = (MOONRAKER_REPO_URL, "master")
|
|
||||||
repo = settings.moonraker.repositories
|
|
||||||
# pull the first repo defined in kiauh.cfg or fallback to the official Moonraker repo
|
|
||||||
repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo
|
|
||||||
git_clone_wrapper(repo, MOONRAKER_DIR, branch)
|
|
||||||
|
|
||||||
# install moonraker dependencies and create python virtualenv
|
|
||||||
install_moonraker_packages()
|
|
||||||
if create_python_venv(MOONRAKER_ENV_DIR):
|
|
||||||
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE)
|
|
||||||
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_SPEEDUPS_REQ_FILE)
|
|
||||||
|
|
||||||
|
|
||||||
def install_moonraker_packages() -> None:
|
|
||||||
Logger.print_status("Parsing Moonraker system dependencies ...")
|
|
||||||
|
|
||||||
moonraker_deps = []
|
|
||||||
if MOONRAKER_DEPS_JSON_FILE.exists():
|
|
||||||
Logger.print_info(
|
|
||||||
f"Parsing system dependencies from {MOONRAKER_DEPS_JSON_FILE.name} ..."
|
|
||||||
)
|
|
||||||
parser = SysDepsParser()
|
|
||||||
sysdeps = load_sysdeps_json(MOONRAKER_DEPS_JSON_FILE)
|
|
||||||
moonraker_deps.extend(parser.parse_dependencies(sysdeps))
|
|
||||||
|
|
||||||
elif MOONRAKER_INSTALL_SCRIPT.exists():
|
|
||||||
Logger.print_warn(f"{MOONRAKER_DEPS_JSON_FILE.name} not found!")
|
|
||||||
Logger.print_info(
|
|
||||||
f"Parsing system dependencies from {MOONRAKER_INSTALL_SCRIPT.name} ..."
|
|
||||||
)
|
|
||||||
moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT)
|
|
||||||
|
|
||||||
if not moonraker_deps:
|
|
||||||
raise ValueError("Error parsing Moonraker dependencies!")
|
|
||||||
|
|
||||||
check_install_dependencies({*moonraker_deps})
|
|
||||||
|
|
||||||
|
|
||||||
def install_moonraker_polkit() -> None:
|
|
||||||
Logger.print_status("Installing Moonraker policykit rules ...")
|
|
||||||
|
|
||||||
legacy_file_exists = check_file_exist(POLKIT_LEGACY_FILE, True)
|
|
||||||
polkit_file_exists = check_file_exist(POLKIT_FILE, True)
|
|
||||||
usr_file_exists = check_file_exist(POLKIT_USR_FILE, True)
|
|
||||||
|
|
||||||
if legacy_file_exists or (polkit_file_exists and usr_file_exists):
|
|
||||||
Logger.print_info("Moonraker policykit rules are already installed.")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
command = [POLKIT_SCRIPT, "--disable-systemctl"]
|
|
||||||
result = subprocess.run(
|
|
||||||
command,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0 or result.stderr:
|
|
||||||
Logger.print_error(f"{result.stderr}", False)
|
|
||||||
Logger.print_error("Installing Moonraker policykit rules failed!")
|
|
||||||
return
|
|
||||||
|
|
||||||
Logger.print_ok("Moonraker policykit rules successfully installed!")
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
log = f"Error while installing Moonraker policykit rules: {e.stderr.decode()}"
|
|
||||||
Logger.print_error(log)
|
|
||||||
|
|
||||||
|
|
||||||
def update_moonraker() -> None:
|
|
||||||
if not get_confirm("Update Moonraker now?"):
|
|
||||||
return
|
|
||||||
|
|
||||||
settings = KiauhSettings()
|
|
||||||
if settings.kiauh.backup_before_update:
|
|
||||||
backup_moonraker_dir()
|
|
||||||
|
|
||||||
instances = get_instances(Moonraker)
|
|
||||||
InstanceManager.stop_all(instances)
|
|
||||||
|
|
||||||
git_pull_wrapper(MOONRAKER_DIR)
|
|
||||||
|
|
||||||
# install possible new system packages
|
|
||||||
install_moonraker_packages()
|
|
||||||
# install possible new python dependencies
|
|
||||||
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE)
|
|
||||||
|
|
||||||
InstanceManager.start_all(instances)
|
|
||||||
408
kiauh/components/moonraker/services/moonraker_setup_service.py
Normal file
408
kiauh/components/moonraker/services/moonraker_setup_service.py
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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 copy import copy
|
||||||
|
from subprocess import DEVNULL, PIPE, CalledProcessError, run
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper.klipper_dialogs import print_instance_overview
|
||||||
|
from components.klipper.services.klipper_instance_service import KlipperInstanceService
|
||||||
|
from components.moonraker import (
|
||||||
|
EXIT_MOONRAKER_SETUP,
|
||||||
|
MOONRAKER_DIR,
|
||||||
|
MOONRAKER_ENV_DIR,
|
||||||
|
MOONRAKER_REPO_URL,
|
||||||
|
MOONRAKER_REQ_FILE,
|
||||||
|
MOONRAKER_SPEEDUPS_REQ_FILE,
|
||||||
|
POLKIT_FILE,
|
||||||
|
POLKIT_LEGACY_FILE,
|
||||||
|
POLKIT_SCRIPT,
|
||||||
|
POLKIT_USR_FILE,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.moonraker_dialogs import print_moonraker_overview
|
||||||
|
from components.moonraker.services.moonraker_instance_service import (
|
||||||
|
MoonrakerInstanceService,
|
||||||
|
)
|
||||||
|
from components.moonraker.utils.utils import (
|
||||||
|
backup_moonraker_dir,
|
||||||
|
create_example_moonraker_conf,
|
||||||
|
install_moonraker_packages,
|
||||||
|
remove_polkit_rules,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
enable_mainsail_remotemode,
|
||||||
|
get_existing_clients,
|
||||||
|
)
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.logger import DialogType, Logger
|
||||||
|
from core.services.message_service import Message, MessageService
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from core.types.color import Color
|
||||||
|
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,
|
||||||
|
get_selection_input,
|
||||||
|
)
|
||||||
|
from utils.sys_utils import (
|
||||||
|
check_python_version,
|
||||||
|
cmd_sysctl_manage,
|
||||||
|
cmd_sysctl_service,
|
||||||
|
create_python_venv,
|
||||||
|
get_ipv4_addr,
|
||||||
|
install_python_requirements,
|
||||||
|
unit_file_exists,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class MoonrakerSetupService:
|
||||||
|
__cls_instance = None
|
||||||
|
|
||||||
|
kisvc: KlipperInstanceService
|
||||||
|
misvc: MoonrakerInstanceService
|
||||||
|
msgsvc = MessageService
|
||||||
|
|
||||||
|
settings: KiauhSettings
|
||||||
|
klipper_list: List[Klipper]
|
||||||
|
moonraker_list: List[Moonraker]
|
||||||
|
|
||||||
|
def __new__(cls) -> "MoonrakerSetupService":
|
||||||
|
if cls.__cls_instance is None:
|
||||||
|
cls.__cls_instance = super(MoonrakerSetupService, cls).__new__(cls)
|
||||||
|
return cls.__cls_instance
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if not hasattr(self, "__initialized"):
|
||||||
|
self.__initialized = False
|
||||||
|
if self.__initialized:
|
||||||
|
return
|
||||||
|
self.__initialized = True
|
||||||
|
self.__init_state()
|
||||||
|
|
||||||
|
def __init_state(self) -> None:
|
||||||
|
self.settings = KiauhSettings()
|
||||||
|
|
||||||
|
self.kisvc = KlipperInstanceService()
|
||||||
|
self.kisvc.load_instances()
|
||||||
|
self.klipper_list = self.kisvc.get_all_instances()
|
||||||
|
|
||||||
|
self.misvc = MoonrakerInstanceService()
|
||||||
|
self.misvc.load_instances()
|
||||||
|
self.moonraker_list = self.misvc.get_all_instances()
|
||||||
|
|
||||||
|
self.msgsvc = MessageService()
|
||||||
|
|
||||||
|
def __refresh_state(self) -> None:
|
||||||
|
self.kisvc.load_instances()
|
||||||
|
self.klipper_list = self.kisvc.get_all_instances()
|
||||||
|
|
||||||
|
self.misvc.load_instances()
|
||||||
|
self.moonraker_list = self.misvc.get_all_instances()
|
||||||
|
|
||||||
|
def install(self) -> None:
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
|
if not self.__check_requirements(self.klipper_list):
|
||||||
|
return
|
||||||
|
|
||||||
|
new_instances: List[Moonraker] = []
|
||||||
|
selected_option: str | Klipper
|
||||||
|
|
||||||
|
if len(self.klipper_list) == 1:
|
||||||
|
suffix: str = self.klipper_list[0].suffix
|
||||||
|
new_inst = self.misvc.create_new_instance(suffix)
|
||||||
|
new_instances.append(new_inst)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print_moonraker_overview(
|
||||||
|
self.klipper_list,
|
||||||
|
self.moonraker_list,
|
||||||
|
show_index=True,
|
||||||
|
show_select_all=True,
|
||||||
|
)
|
||||||
|
options = {str(i + 1): k for i, k in enumerate(self.klipper_list)}
|
||||||
|
additional_options = {"a": None, "b": None}
|
||||||
|
options = {**options, **additional_options}
|
||||||
|
question = "Select Klipper instance to setup Moonraker for"
|
||||||
|
selected_option = get_selection_input(question, options)
|
||||||
|
|
||||||
|
if selected_option == "b":
|
||||||
|
Logger.print_status(EXIT_MOONRAKER_SETUP)
|
||||||
|
return
|
||||||
|
|
||||||
|
if selected_option == "a":
|
||||||
|
new_inst_list: List[Moonraker] = [
|
||||||
|
self.misvc.create_new_instance(k.suffix) for k in self.klipper_list
|
||||||
|
]
|
||||||
|
new_instances.extend(new_inst_list)
|
||||||
|
else:
|
||||||
|
klipper_instance: Klipper | None = options.get(selected_option)
|
||||||
|
if klipper_instance is None:
|
||||||
|
raise Exception("Error selecting instance!")
|
||||||
|
new_inst = self.misvc.create_new_instance(klipper_instance.suffix)
|
||||||
|
new_instances.append(new_inst)
|
||||||
|
|
||||||
|
create_example_cfg = get_confirm("Create example moonraker.conf?")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.__run_setup(new_instances, create_example_cfg)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Error while installing Moonraker: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.WARNING,
|
||||||
|
[
|
||||||
|
"Be careful if there are ongoing prints running!",
|
||||||
|
"All Moonraker instances will be restarted during the update process and "
|
||||||
|
"ongoing prints COULD FAIL.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not get_confirm("Update Moonraker now?"):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
|
if self.settings.kiauh.backup_before_update:
|
||||||
|
backup_moonraker_dir()
|
||||||
|
|
||||||
|
InstanceManager.stop_all(self.moonraker_list)
|
||||||
|
git_pull_wrapper(MOONRAKER_DIR)
|
||||||
|
install_moonraker_packages()
|
||||||
|
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE)
|
||||||
|
InstanceManager.start_all(self.moonraker_list)
|
||||||
|
|
||||||
|
def remove(
|
||||||
|
self,
|
||||||
|
remove_service: bool,
|
||||||
|
remove_dir: bool,
|
||||||
|
remove_env: bool,
|
||||||
|
remove_polkit: bool,
|
||||||
|
) -> None:
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
|
completion_msg = Message(
|
||||||
|
title="Moonraker Removal Process completed",
|
||||||
|
color=Color.GREEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
if remove_service:
|
||||||
|
Logger.print_status("Removing Moonraker instances ...")
|
||||||
|
if self.moonraker_list:
|
||||||
|
instances_to_remove = self.__get_instances_to_remove()
|
||||||
|
self.__remove_instances(instances_to_remove)
|
||||||
|
if instances_to_remove:
|
||||||
|
instance_names = [
|
||||||
|
i.service_file_path.stem for i in instances_to_remove
|
||||||
|
]
|
||||||
|
txt = f"● Moonraker instances removed: {', '.join(instance_names)}"
|
||||||
|
completion_msg.text.append(txt)
|
||||||
|
else:
|
||||||
|
Logger.print_info("No Moonraker Services installed! Skipped ...")
|
||||||
|
|
||||||
|
if (remove_polkit or remove_dir or remove_env) and unit_file_exists(
|
||||||
|
"moonraker", suffix="service"
|
||||||
|
):
|
||||||
|
completion_msg.text = [
|
||||||
|
"Some Klipper services are still installed:",
|
||||||
|
"● Moonraker PolicyKit rules were not removed, even though selected for removal.",
|
||||||
|
f"● '{MOONRAKER_DIR}' was not removed, even though selected for removal.",
|
||||||
|
f"● '{MOONRAKER_ENV_DIR}' was not removed, even though selected for removal.",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
if remove_polkit:
|
||||||
|
Logger.print_status("Removing all Moonraker policykit rules ...")
|
||||||
|
if remove_polkit_rules():
|
||||||
|
completion_msg.text.append("● Moonraker policykit rules removed")
|
||||||
|
if remove_dir:
|
||||||
|
Logger.print_status("Removing Moonraker local repository ...")
|
||||||
|
if run_remove_routines(MOONRAKER_DIR):
|
||||||
|
completion_msg.text.append("● Moonraker local repository removed")
|
||||||
|
if remove_env:
|
||||||
|
Logger.print_status("Removing Moonraker Python environment ...")
|
||||||
|
if run_remove_routines(MOONRAKER_ENV_DIR):
|
||||||
|
completion_msg.text.append("● Moonraker Python environment removed")
|
||||||
|
|
||||||
|
if completion_msg.text:
|
||||||
|
completion_msg.text.insert(0, "The following actions were performed:")
|
||||||
|
else:
|
||||||
|
completion_msg.color = Color.YELLOW
|
||||||
|
completion_msg.centered = True
|
||||||
|
completion_msg.text = ["Nothing to remove."]
|
||||||
|
|
||||||
|
self.msgsvc.set_message(completion_msg)
|
||||||
|
|
||||||
|
def __run_setup(
|
||||||
|
self, new_instances: List[Moonraker], create_example_cfg: bool
|
||||||
|
) -> None:
|
||||||
|
check_install_dependencies()
|
||||||
|
self.__install_deps()
|
||||||
|
|
||||||
|
ports_map = self.misvc.get_instance_port_map()
|
||||||
|
for i in new_instances:
|
||||||
|
i.create()
|
||||||
|
cmd_sysctl_service(i.service_file_path.name, "enable")
|
||||||
|
|
||||||
|
if create_example_cfg:
|
||||||
|
# if a webclient and/or it's config is installed, patch
|
||||||
|
# its update section to the config
|
||||||
|
clients = get_existing_clients()
|
||||||
|
create_example_moonraker_conf(i, ports_map, clients)
|
||||||
|
|
||||||
|
cmd_sysctl_service(i.service_file_path.name, "start")
|
||||||
|
|
||||||
|
cmd_sysctl_manage("daemon-reload")
|
||||||
|
|
||||||
|
# if mainsail is installed, and we installed
|
||||||
|
# multiple moonraker instances, we enable mainsails remote mode
|
||||||
|
if MainsailData().client_dir.exists() and len(self.moonraker_list) > 1:
|
||||||
|
enable_mainsail_remotemode()
|
||||||
|
|
||||||
|
self.misvc.load_instances()
|
||||||
|
new_instances = [
|
||||||
|
self.misvc.get_instance_by_suffix(i.suffix) for i in new_instances
|
||||||
|
]
|
||||||
|
|
||||||
|
ip: str = get_ipv4_addr()
|
||||||
|
# noinspection HttpUrlsUsage
|
||||||
|
url_list = [
|
||||||
|
f"● {i.service_file_path.stem}: http://{ip}:{i.port}"
|
||||||
|
for i in new_instances
|
||||||
|
if i.port
|
||||||
|
]
|
||||||
|
dialog_content = []
|
||||||
|
if url_list:
|
||||||
|
dialog_content.append("You can access Moonraker via the following URL:")
|
||||||
|
dialog_content.extend(url_list)
|
||||||
|
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.CUSTOM,
|
||||||
|
custom_title="Moonraker successfully installed!",
|
||||||
|
custom_color=Color.GREEN,
|
||||||
|
content=dialog_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __check_requirements(self, klipper_list: List[Klipper]) -> bool:
|
||||||
|
is_klipper_installed = len(klipper_list) >= 1
|
||||||
|
if not is_klipper_installed:
|
||||||
|
Logger.print_warn("Klipper not installed!")
|
||||||
|
Logger.print_warn("Moonraker cannot be installed! Install Klipper first.")
|
||||||
|
|
||||||
|
is_python_ok = check_python_version(3, 7)
|
||||||
|
|
||||||
|
return is_klipper_installed and is_python_ok
|
||||||
|
|
||||||
|
def __install_deps(self) -> None:
|
||||||
|
default_repo = (MOONRAKER_REPO_URL, "master")
|
||||||
|
repo = self.settings.moonraker.repositories
|
||||||
|
# pull the first repo defined in kiauh.cfg or fallback to the official Moonraker repo
|
||||||
|
repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo
|
||||||
|
git_clone_wrapper(repo, MOONRAKER_DIR, branch)
|
||||||
|
|
||||||
|
try:
|
||||||
|
install_moonraker_packages()
|
||||||
|
if create_python_venv(MOONRAKER_ENV_DIR, False, False, self.settings.moonraker.use_python_binary):
|
||||||
|
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE)
|
||||||
|
if self.settings.moonraker.optional_speedups:
|
||||||
|
install_python_requirements(
|
||||||
|
MOONRAKER_ENV_DIR, MOONRAKER_SPEEDUPS_REQ_FILE
|
||||||
|
)
|
||||||
|
self.__install_polkit()
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error("Error during installation of Moonraker requirements!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __install_polkit(self) -> None:
|
||||||
|
Logger.print_status("Installing Moonraker policykit rules ...")
|
||||||
|
|
||||||
|
legacy_file_exists = check_file_exist(POLKIT_LEGACY_FILE, True)
|
||||||
|
polkit_file_exists = check_file_exist(POLKIT_FILE, True)
|
||||||
|
usr_file_exists = check_file_exist(POLKIT_USR_FILE, True)
|
||||||
|
|
||||||
|
if legacy_file_exists or (polkit_file_exists and usr_file_exists):
|
||||||
|
Logger.print_info("Moonraker policykit rules are already installed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = [POLKIT_SCRIPT, "--disable-systemctl"]
|
||||||
|
result = run(
|
||||||
|
command,
|
||||||
|
stderr=PIPE,
|
||||||
|
stdout=DEVNULL,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0 or result.stderr:
|
||||||
|
Logger.print_error(f"{result.stderr}", False)
|
||||||
|
Logger.print_error("Installing Moonraker policykit rules failed!")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_ok("Moonraker policykit rules successfully installed!")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
log = (
|
||||||
|
f"Error while installing Moonraker policykit rules: {e.stderr.decode()}"
|
||||||
|
)
|
||||||
|
Logger.print_error(log)
|
||||||
|
|
||||||
|
def __get_instances_to_remove(self) -> List[Moonraker] | None:
|
||||||
|
start_index = 1
|
||||||
|
curr_instances: List[Moonraker] = self.moonraker_list
|
||||||
|
instance_count = len(curr_instances)
|
||||||
|
|
||||||
|
options = [str(i + start_index) for i in range(instance_count)]
|
||||||
|
options.extend(["a", "b"])
|
||||||
|
instance_map = {
|
||||||
|
options[i]: self.moonraker_list[i] for i in range(instance_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
print_instance_overview(
|
||||||
|
self.moonraker_list,
|
||||||
|
start_index=start_index,
|
||||||
|
show_index=True,
|
||||||
|
show_select_all=True,
|
||||||
|
)
|
||||||
|
selection = get_selection_input("Select Moonraker instance to remove", options)
|
||||||
|
|
||||||
|
if selection == "b":
|
||||||
|
return None
|
||||||
|
elif selection == "a":
|
||||||
|
return copy(self.moonraker_list)
|
||||||
|
|
||||||
|
return [instance_map[selection]]
|
||||||
|
|
||||||
|
def __remove_instances(
|
||||||
|
self,
|
||||||
|
instance_list: List[Moonraker] | None,
|
||||||
|
) -> None:
|
||||||
|
if not instance_list:
|
||||||
|
return
|
||||||
|
|
||||||
|
for instance in instance_list:
|
||||||
|
Logger.print_status(
|
||||||
|
f"Removing instance {instance.service_file_path.stem} ..."
|
||||||
|
)
|
||||||
|
InstanceManager.remove(instance)
|
||||||
|
self.__delete_env_file(instance)
|
||||||
|
|
||||||
|
self.__refresh_state()
|
||||||
|
|
||||||
|
def __delete_env_file(self, instance: Moonraker):
|
||||||
|
Logger.print_status(f"Remove '{instance.env_file}'")
|
||||||
|
if not instance.env_file.exists():
|
||||||
|
msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..."
|
||||||
|
Logger.print_info(msg)
|
||||||
|
return
|
||||||
|
run_remove_routines(instance.env_file)
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from subprocess import DEVNULL, PIPE, CalledProcessError, run
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from components.moonraker import (
|
from components.moonraker import (
|
||||||
@@ -16,10 +17,13 @@ from components.moonraker import (
|
|||||||
MOONRAKER_BACKUP_DIR,
|
MOONRAKER_BACKUP_DIR,
|
||||||
MOONRAKER_DB_BACKUP_DIR,
|
MOONRAKER_DB_BACKUP_DIR,
|
||||||
MOONRAKER_DEFAULT_PORT,
|
MOONRAKER_DEFAULT_PORT,
|
||||||
|
MOONRAKER_DEPS_JSON_FILE,
|
||||||
MOONRAKER_DIR,
|
MOONRAKER_DIR,
|
||||||
MOONRAKER_ENV_DIR,
|
MOONRAKER_ENV_DIR,
|
||||||
|
MOONRAKER_INSTALL_SCRIPT,
|
||||||
)
|
)
|
||||||
from components.moonraker.moonraker import Moonraker
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.utils.sysdeps_parser import SysDepsParser
|
||||||
from components.webui_client.base_data import BaseWebClient
|
from components.webui_client.base_data import BaseWebClient
|
||||||
from core.backup_manager.backup_manager import BackupManager
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
from core.logger import Logger
|
from core.logger import Logger
|
||||||
@@ -27,10 +31,11 @@ from core.submodules.simple_config_parser.src.simple_config_parser.simple_config
|
|||||||
SimpleConfigParser,
|
SimpleConfigParser,
|
||||||
)
|
)
|
||||||
from core.types.component_status import ComponentStatus
|
from core.types.component_status import ComponentStatus
|
||||||
from utils.common import get_install_status
|
from utils.common import check_install_dependencies, get_install_status
|
||||||
from utils.instance_utils import get_instances
|
from utils.instance_utils import get_instances
|
||||||
from utils.sys_utils import (
|
from utils.sys_utils import (
|
||||||
get_ipv4_addr,
|
get_ipv4_addr,
|
||||||
|
parse_packages_from_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -38,6 +43,46 @@ def get_moonraker_status() -> ComponentStatus:
|
|||||||
return get_install_status(MOONRAKER_DIR, MOONRAKER_ENV_DIR, Moonraker)
|
return get_install_status(MOONRAKER_DIR, MOONRAKER_ENV_DIR, Moonraker)
|
||||||
|
|
||||||
|
|
||||||
|
def install_moonraker_packages() -> None:
|
||||||
|
Logger.print_status("Parsing Moonraker system dependencies ...")
|
||||||
|
|
||||||
|
moonraker_deps = []
|
||||||
|
if MOONRAKER_DEPS_JSON_FILE.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
f"Parsing system dependencies from {MOONRAKER_DEPS_JSON_FILE.name} ..."
|
||||||
|
)
|
||||||
|
parser = SysDepsParser()
|
||||||
|
sysdeps = load_sysdeps_json(MOONRAKER_DEPS_JSON_FILE)
|
||||||
|
moonraker_deps.extend(parser.parse_dependencies(sysdeps))
|
||||||
|
|
||||||
|
elif MOONRAKER_INSTALL_SCRIPT.exists():
|
||||||
|
Logger.print_warn(f"{MOONRAKER_DEPS_JSON_FILE.name} not found!")
|
||||||
|
Logger.print_info(
|
||||||
|
f"Parsing system dependencies from {MOONRAKER_INSTALL_SCRIPT.name} ..."
|
||||||
|
)
|
||||||
|
moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT)
|
||||||
|
|
||||||
|
if not moonraker_deps:
|
||||||
|
raise ValueError("Error parsing Moonraker dependencies!")
|
||||||
|
|
||||||
|
check_install_dependencies({*moonraker_deps})
|
||||||
|
|
||||||
|
|
||||||
|
def remove_polkit_rules() -> bool:
|
||||||
|
if not MOONRAKER_DIR.exists():
|
||||||
|
log = "Cannot remove policykit rules. Moonraker directory not found."
|
||||||
|
Logger.print_warn(log)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"]
|
||||||
|
run(cmd, stderr=PIPE, stdout=DEVNULL, check=True)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error while removing policykit rules: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_example_moonraker_conf(
|
def create_example_moonraker_conf(
|
||||||
instance: Moonraker,
|
instance: Moonraker,
|
||||||
ports_map: Dict[str, int],
|
ports_map: Dict[str, int],
|
||||||
@@ -81,7 +126,7 @@ def create_example_moonraker_conf(
|
|||||||
scp.read_file(target)
|
scp.read_file(target)
|
||||||
trusted_clients: List[str] = [
|
trusted_clients: List[str] = [
|
||||||
f" {'.'.join(ip)}\n",
|
f" {'.'.join(ip)}\n",
|
||||||
*scp.getval("authorization", "trusted_clients"),
|
*scp.getvals("authorization", "trusted_clients"),
|
||||||
]
|
]
|
||||||
|
|
||||||
scp.set_option("server", "port", str(port))
|
scp.set_option("server", "port", str(port))
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ def install_client(
|
|||||||
section=f"update_manager {client.name}",
|
section=f"update_manager {client.name}",
|
||||||
instances=mr_instances,
|
instances=mr_instances,
|
||||||
options=[
|
options=[
|
||||||
|
("persistent_files", ["config.json"]),
|
||||||
("type", "web"),
|
("type", "web"),
|
||||||
("channel", "stable"),
|
("channel", "stable"),
|
||||||
("repo", str(client.repo_path)),
|
("repo", str(client.repo_path)),
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ def enable_mainsail_remotemode() -> None:
|
|||||||
c_json = MainsailData().client_dir.joinpath("config.json")
|
c_json = MainsailData().client_dir.joinpath("config.json")
|
||||||
with open(c_json, "r") as f:
|
with open(c_json, "r") as f:
|
||||||
config_data = json.load(f)
|
config_data = json.load(f)
|
||||||
|
|
||||||
if config_data["instancesDB"] == "browser":
|
if config_data["instancesDB"] == "browser" or config_data["instancesDB"] == "json":
|
||||||
Logger.print_info("Remote mode already configured. Skipped ...")
|
Logger.print_info("Remote mode already configured. Skipped ...")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ def get_client_port_selection(
|
|||||||
while True:
|
while True:
|
||||||
_type = "Reconfigure" if reconfigure else "Configure"
|
_type = "Reconfigure" if reconfigure else "Configure"
|
||||||
question = f"{_type} {client.display_name} for port"
|
question = f"{_type} {client.display_name} for port"
|
||||||
port_input = get_number_input(question, min_count=80, default=port)
|
port_input = get_number_input(question, min_value=80, default=port)
|
||||||
|
|
||||||
if port_input not in ports_in_use:
|
if port_input not in ports_in_use:
|
||||||
client_settings: WebUiSettings = settings[client.name]
|
client_settings: WebUiSettings = settings[client.name]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from typing import Type
|
|||||||
from components.crowsnest.crowsnest import install_crowsnest
|
from components.crowsnest.crowsnest import install_crowsnest
|
||||||
from components.klipper.services.klipper_setup_service import KlipperSetupService
|
from components.klipper.services.klipper_setup_service import KlipperSetupService
|
||||||
from components.klipperscreen.klipperscreen import install_klipperscreen
|
from components.klipperscreen.klipperscreen import install_klipperscreen
|
||||||
from components.moonraker import moonraker_setup
|
from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService
|
||||||
from components.webui_client.client_config.client_config_setup import (
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
install_client_config,
|
install_client_config,
|
||||||
)
|
)
|
||||||
@@ -37,6 +37,7 @@ class InstallMenu(BaseMenu):
|
|||||||
self.title_color = Color.GREEN
|
self.title_color = Color.GREEN
|
||||||
self.previous_menu: Type[BaseMenu] | None = previous_menu
|
self.previous_menu: Type[BaseMenu] | None = previous_menu
|
||||||
self.klsvc = KlipperSetupService()
|
self.klsvc = KlipperSetupService()
|
||||||
|
self.mrsvc = MoonrakerSetupService()
|
||||||
|
|
||||||
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
|
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
|
||||||
from core.menus.main_menu import MainMenu
|
from core.menus.main_menu import MainMenu
|
||||||
@@ -79,7 +80,7 @@ class InstallMenu(BaseMenu):
|
|||||||
self.klsvc.install()
|
self.klsvc.install()
|
||||||
|
|
||||||
def install_moonraker(self, **kwargs) -> None:
|
def install_moonraker(self, **kwargs) -> None:
|
||||||
moonraker_setup.install_moonraker()
|
self.mrsvc.install()
|
||||||
|
|
||||||
def install_mainsail(self, **kwargs) -> None:
|
def install_mainsail(self, **kwargs) -> None:
|
||||||
client: MainsailData = MainsailData()
|
client: MainsailData = MainsailData()
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import List, Literal, Type
|
from typing import List, Literal, Type
|
||||||
|
|
||||||
from core.logger import Logger
|
from core.logger import Logger, DialogType
|
||||||
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, Repository
|
from core.settings.kiauh_settings import KiauhSettings, Repository
|
||||||
from core.types.color import Color
|
from core.types.color import Color
|
||||||
from procedures.switch_repo import run_switch_repo_routine
|
from procedures.switch_repo import run_switch_repo_routine
|
||||||
|
from utils.input_utils import get_string_input, get_number_input, get_confirm
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
class RepoSelectMenu(BaseMenu):
|
class RepoSelectMenu(BaseMenu):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -48,26 +51,27 @@ class RepoSelectMenu(BaseMenu):
|
|||||||
|
|
||||||
def set_options(self) -> None:
|
def set_options(self) -> None:
|
||||||
self.options = {}
|
self.options = {}
|
||||||
|
if self.repos:
|
||||||
if not self.repos:
|
for idx, repo in enumerate(self.repos, start=1):
|
||||||
return
|
self.options[str(idx)] = Option(
|
||||||
|
method=self.select_repository, opt_data=repo
|
||||||
for idx, repo in enumerate(self.repos, start=1):
|
)
|
||||||
self.options[str(idx)] = Option(
|
self.options["a"] = Option(method=self.add_repository)
|
||||||
method=self.select_repository, opt_data=repo
|
self.options["r"] = Option(method=self.remove_repository)
|
||||||
)
|
self.options["b"] = Option(method=self.go_back)
|
||||||
|
|
||||||
def print_menu(self) -> None:
|
def print_menu(self) -> None:
|
||||||
menu = "╟───────────────────────────────────────────────────────╢\n"
|
menu = "╟───────────────────────────────────────────────────────╢\n"
|
||||||
menu += "║ Available Repositories: ║\n"
|
menu += "║ Available Repositories: ║\n"
|
||||||
menu += "╟───────────────────────────────────────────────────────╢\n"
|
menu += "╟───────────────────────────────────────────────────────╢\n"
|
||||||
|
|
||||||
for idx, repo in enumerate(self.repos, start=1):
|
for idx, repo in enumerate(self.repos, start=1):
|
||||||
url = f"● Repo: {repo.url.replace('.git', '')}"
|
url = f"● Repo: {repo.url.replace('.git', '')}"
|
||||||
branch = f"└► Branch: {repo.branch}"
|
branch = f"└► Branch: {repo.branch}"
|
||||||
menu += f"║ {idx}) {Color.apply(url, Color.CYAN):<59} ║\n"
|
menu += f"║ {idx}) {Color.apply(url, Color.CYAN):<59} ║\n"
|
||||||
menu += f"║ {Color.apply(branch, Color.CYAN):<59} ║\n"
|
menu += f"║ {Color.apply(branch, Color.CYAN):<59} ║\n"
|
||||||
|
menu += "╟───────────────────────────────────────────────────────╢\n"
|
||||||
|
menu += "║ A) Add repository ║\n"
|
||||||
|
menu += "║ R) Remove repository ║\n"
|
||||||
menu += "╟───────────────────────────────────────────────────────╢\n"
|
menu += "╟───────────────────────────────────────────────────────╢\n"
|
||||||
print(menu, end="")
|
print(menu, end="")
|
||||||
|
|
||||||
@@ -77,3 +81,82 @@ class RepoSelectMenu(BaseMenu):
|
|||||||
f"Switching to {self.name.capitalize()}'s new source repository ..."
|
f"Switching to {self.name.capitalize()}'s new source repository ..."
|
||||||
)
|
)
|
||||||
run_switch_repo_routine(self.name, repo.url, repo.branch)
|
run_switch_repo_routine(self.name, repo.url, repo.branch)
|
||||||
|
|
||||||
|
def add_repository(self, **kwargs) -> None:
|
||||||
|
while True:
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.CUSTOM,
|
||||||
|
custom_title="Enter the repository URL",
|
||||||
|
content=[
|
||||||
|
"NOTE: There is no input validation in place, "
|
||||||
|
"please check your input for correctness",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
url = get_string_input("Repository URL", allow_special_chars=True).strip()
|
||||||
|
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.CUSTOM,
|
||||||
|
custom_title="Enter the branch name",
|
||||||
|
content=[ "Press Enter to use the default branch (master)." ],
|
||||||
|
center_content=False,
|
||||||
|
)
|
||||||
|
branch = get_string_input("Branch", allow_special_chars=True, default="master").strip()
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.CUSTOM,
|
||||||
|
custom_title="Summary",
|
||||||
|
content=[
|
||||||
|
f"● URL: {url}",
|
||||||
|
f"● Branch: {branch}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
confirm = get_confirm("Save repository")
|
||||||
|
if confirm:
|
||||||
|
repo = Repository(url, branch)
|
||||||
|
if self.name == "klipper":
|
||||||
|
self.settings.klipper.repositories.append(repo)
|
||||||
|
self.settings.save()
|
||||||
|
self.repos = self.settings.klipper.repositories
|
||||||
|
else:
|
||||||
|
self.settings.moonraker.repositories.append(repo)
|
||||||
|
self.settings.save()
|
||||||
|
self.repos = self.settings.moonraker.repositories
|
||||||
|
Logger.print_ok("Repository added and saved.")
|
||||||
|
|
||||||
|
# Refresh menu to show new repo immediately and update options
|
||||||
|
self.set_options()
|
||||||
|
self.run()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
Logger.print_info("Operation cancelled by user.")
|
||||||
|
break
|
||||||
|
|
||||||
|
def remove_repository(self, **kwargs) -> None:
|
||||||
|
repos = self.repos
|
||||||
|
if not repos:
|
||||||
|
Logger.print_info("No repositories configured.")
|
||||||
|
return
|
||||||
|
repo_lines = [f"{idx}) {repo.url} [{repo.branch}]" for idx, repo in enumerate(repos, start=1)]
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.CUSTOM,
|
||||||
|
custom_title="Available Repositories",
|
||||||
|
content=[*repo_lines],
|
||||||
|
)
|
||||||
|
idx = get_number_input("Select the repository to remove", 1, len(repos))
|
||||||
|
removed = repos.pop(idx - 1)
|
||||||
|
if self.name == "klipper":
|
||||||
|
self.settings.klipper.repositories = repos
|
||||||
|
self.settings.save()
|
||||||
|
self.repos = self.settings.klipper.repositories
|
||||||
|
else:
|
||||||
|
self.settings.moonraker.repositories = repos
|
||||||
|
self.settings.save()
|
||||||
|
self.repos = self.settings.moonraker.repositories
|
||||||
|
Logger.print_ok(f"Removed repository: {removed.url} [{removed.branch}]")
|
||||||
|
|
||||||
|
# Refresh menu to show updated repo list and options
|
||||||
|
self.set_options()
|
||||||
|
self.run()
|
||||||
|
|
||||||
|
def go_back(self, **kwargs) -> None:
|
||||||
|
from core.menus.settings_menu import SettingsMenu
|
||||||
|
SettingsMenu().run()
|
||||||
|
|||||||
@@ -117,20 +117,12 @@ class SettingsMenu(BaseMenu):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def switch_klipper_repo(self, **kwargs) -> None:
|
def switch_klipper_repo(self, **kwargs) -> None:
|
||||||
name = "Klipper"
|
|
||||||
repos = self.settings.klipper.repositories
|
repos = self.settings.klipper.repositories
|
||||||
if not repos:
|
RepoSelectMenu("klipper", repos=repos, previous_menu=self.__class__).run()
|
||||||
self._warn_no_repos(name)
|
|
||||||
return
|
|
||||||
RepoSelectMenu(name.lower(), repos=repos, previous_menu=self.__class__).run()
|
|
||||||
|
|
||||||
def switch_moonraker_repo(self, **kwargs) -> None:
|
def switch_moonraker_repo(self, **kwargs) -> None:
|
||||||
name = "Moonraker"
|
|
||||||
repos = self.settings.moonraker.repositories
|
repos = self.settings.moonraker.repositories
|
||||||
if not repos:
|
RepoSelectMenu("moonraker", repos=repos, previous_menu=self.__class__).run()
|
||||||
self._warn_no_repos(name)
|
|
||||||
return
|
|
||||||
RepoSelectMenu(name.lower(), repos=repos, previous_menu=self.__class__).run()
|
|
||||||
|
|
||||||
def toggle_mainsail_release(self, **kwargs) -> None:
|
def toggle_mainsail_release(self, **kwargs) -> None:
|
||||||
self.mainsail_unstable = not self.mainsail_unstable
|
self.mainsail_unstable = not self.mainsail_unstable
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from components.klipperscreen.klipperscreen import (
|
|||||||
get_klipperscreen_status,
|
get_klipperscreen_status,
|
||||||
update_klipperscreen,
|
update_klipperscreen,
|
||||||
)
|
)
|
||||||
from components.moonraker.moonraker_setup import update_moonraker
|
from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService
|
||||||
from components.moonraker.utils.utils import get_moonraker_status
|
from components.moonraker.utils.utils import get_moonraker_status
|
||||||
from components.webui_client.client_config.client_config_setup import (
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
update_client_config,
|
update_client_config,
|
||||||
@@ -197,7 +197,8 @@ class UpdateMenu(BaseMenu):
|
|||||||
self._run_update_routine("klipper", klsvc.update)
|
self._run_update_routine("klipper", klsvc.update)
|
||||||
|
|
||||||
def update_moonraker(self, **kwargs) -> None:
|
def update_moonraker(self, **kwargs) -> None:
|
||||||
self._run_update_routine("moonraker", update_moonraker)
|
mrsvc = MoonrakerSetupService()
|
||||||
|
self._run_update_routine("moonraker", mrsvc.update)
|
||||||
|
|
||||||
def update_mainsail(self, **kwargs) -> None:
|
def update_mainsail(self, **kwargs) -> None:
|
||||||
self._run_update_routine(
|
self._run_update_routine(
|
||||||
|
|||||||
@@ -8,14 +8,15 @@
|
|||||||
# ======================================================================= #
|
# ======================================================================= #
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, List
|
from typing import Any, Callable, List, TypeVar
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_REPO_URL
|
||||||
|
from components.moonraker import MOONRAKER_REPO_URL
|
||||||
from core.backup_manager.backup_manager import BackupManager
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
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 (
|
||||||
NoOptionError,
|
|
||||||
NoSectionError,
|
|
||||||
SimpleConfigParser,
|
SimpleConfigParser,
|
||||||
)
|
)
|
||||||
from utils.input_utils import get_confirm
|
from utils.input_utils import get_confirm
|
||||||
@@ -26,13 +27,7 @@ from kiauh import PROJECT_ROOT
|
|||||||
DEFAULT_CFG = PROJECT_ROOT.joinpath("default.kiauh.cfg")
|
DEFAULT_CFG = PROJECT_ROOT.joinpath("default.kiauh.cfg")
|
||||||
CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg")
|
CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg")
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
class NoValueError(Exception):
|
|
||||||
"""Raised when a required value is not defined for an option"""
|
|
||||||
|
|
||||||
def __init__(self, section: str, option: str):
|
|
||||||
msg = f"Missing value for option '{option}' in section '{section}'"
|
|
||||||
super().__init__(msg)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidValueError(Exception):
|
class InvalidValueError(Exception):
|
||||||
@@ -55,8 +50,16 @@ class Repository:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RepoSettings:
|
class KlipperSettings:
|
||||||
repositories: List[Repository] | None = field(default=None)
|
repositories: List[Repository] | None = field(default=None)
|
||||||
|
use_python_binary: str | None = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MoonrakerSettings:
|
||||||
|
optional_speedups: bool | None = field(default=None)
|
||||||
|
repositories: List[Repository] | None = field(default=None)
|
||||||
|
use_python_binary: str | None = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -69,6 +72,7 @@ class WebUiSettings:
|
|||||||
# noinspection PyMethodMayBeStatic
|
# noinspection PyMethodMayBeStatic
|
||||||
class KiauhSettings:
|
class KiauhSettings:
|
||||||
__instance = None
|
__instance = None
|
||||||
|
__initialized = False
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs) -> "KiauhSettings":
|
def __new__(cls, *args, **kwargs) -> "KiauhSettings":
|
||||||
if cls.__instance is None:
|
if cls.__instance is None:
|
||||||
@@ -86,20 +90,20 @@ class KiauhSettings:
|
|||||||
return getattr(self, item)
|
return getattr(self, item)
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
if not hasattr(self, "__initialized"):
|
|
||||||
self.__initialized = False
|
|
||||||
if self.__initialized:
|
if self.__initialized:
|
||||||
return
|
return
|
||||||
self.__initialized = True
|
self.__initialized = True
|
||||||
|
|
||||||
self.config = SimpleConfigParser()
|
self.config = SimpleConfigParser()
|
||||||
self.kiauh = AppSettings()
|
self.kiauh = AppSettings()
|
||||||
self.klipper = RepoSettings()
|
self.klipper = KlipperSettings()
|
||||||
self.moonraker = RepoSettings()
|
self.moonraker = MoonrakerSettings()
|
||||||
self.mainsail = WebUiSettings()
|
self.mainsail = WebUiSettings()
|
||||||
self.fluidd = WebUiSettings()
|
self.fluidd = WebUiSettings()
|
||||||
|
|
||||||
self._load_config()
|
self.__read_config_set_internal_state()
|
||||||
|
|
||||||
|
# todo: refactor this, at least rename to something else!
|
||||||
def get(self, section: str, option: str) -> str | int | bool:
|
def get(self, section: str, option: str) -> str | int | bool:
|
||||||
"""
|
"""
|
||||||
Get a value from the settings state by providing the section and option name as
|
Get a value from the settings state by providing the section and option name as
|
||||||
@@ -117,128 +121,175 @@ class KiauhSettings:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
self._set_config_options_state()
|
self.__write_internal_state_to_cfg()
|
||||||
self.config.write_file(CUSTOM_CFG)
|
self.__read_config_set_internal_state()
|
||||||
self._load_config()
|
|
||||||
|
|
||||||
def _load_config(self) -> None:
|
def __read_config_set_internal_state(self) -> None:
|
||||||
if not CUSTOM_CFG.exists() and not DEFAULT_CFG.exists():
|
if not CUSTOM_CFG.exists() and not DEFAULT_CFG.exists():
|
||||||
self.__kill()
|
Logger.print_dialog(
|
||||||
|
DialogType.ERROR,
|
||||||
cfg = CUSTOM_CFG if CUSTOM_CFG.exists() else DEFAULT_CFG
|
[
|
||||||
self.config.read_file(cfg)
|
"No KIAUH configuration file found! Please make sure you have at least "
|
||||||
|
"one of the following configuration files in KIAUH's root directory:",
|
||||||
needs_migration = self._check_deprecated_repo_config()
|
"● default.kiauh.cfg",
|
||||||
if needs_migration:
|
"● kiauh.cfg",
|
||||||
self._prompt_migration_dialog()
|
],
|
||||||
return
|
)
|
||||||
else:
|
|
||||||
# Only validate if no migration was needed
|
|
||||||
self._validate_cfg()
|
|
||||||
self.__set_internal_state()
|
|
||||||
|
|
||||||
def _validate_cfg(self) -> None:
|
|
||||||
def __err_and_kill(error: str) -> None:
|
|
||||||
Logger.print_error(f"Error validating kiauh.cfg: {error}")
|
|
||||||
kill()
|
kill()
|
||||||
|
|
||||||
try:
|
# copy default config to custom config if it does not exist
|
||||||
self._validate_bool("kiauh", "backup_before_update")
|
if not CUSTOM_CFG.exists():
|
||||||
|
shutil.copyfile(DEFAULT_CFG, CUSTOM_CFG)
|
||||||
|
|
||||||
self._validate_repositories("klipper", "repositories")
|
self.config.read_file(CUSTOM_CFG)
|
||||||
self._validate_repositories("moonraker", "repositories")
|
|
||||||
|
|
||||||
self._validate_int("mainsail", "port")
|
# check if there are deprecated repo_url and branch options in the kiauh.cfg
|
||||||
self._validate_bool("mainsail", "unstable_releases")
|
if self._check_deprecated_repo_config():
|
||||||
|
self._prompt_migration_dialog()
|
||||||
|
|
||||||
self._validate_int("fluidd", "port")
|
self.__set_internal_state()
|
||||||
self._validate_bool("fluidd", "unstable_releases")
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
err = f"Invalid value for option '{self._v_option}' in section '{self._v_section}'"
|
|
||||||
__err_and_kill(err)
|
|
||||||
except NoSectionError:
|
|
||||||
err = f"Missing section '{self._v_section}' in config file"
|
|
||||||
__err_and_kill(err)
|
|
||||||
except NoOptionError:
|
|
||||||
err = f"Missing option '{self._v_option}' in section '{self._v_section}'"
|
|
||||||
__err_and_kill(err)
|
|
||||||
except NoValueError:
|
|
||||||
err = f"Missing value for option '{self._v_option}' in section '{self._v_section}'"
|
|
||||||
__err_and_kill(err)
|
|
||||||
|
|
||||||
def _validate_bool(self, section: str, option: str) -> None:
|
|
||||||
self._v_section, self._v_option = (section, option)
|
|
||||||
(bool(self.config.getboolean(section, option)))
|
|
||||||
|
|
||||||
def _validate_int(self, section: str, option: str) -> None:
|
|
||||||
self._v_section, self._v_option = (section, option)
|
|
||||||
int(self.config.getint(section, option))
|
|
||||||
|
|
||||||
def _validate_str(self, section: str, option: str) -> None:
|
|
||||||
self._v_section, self._v_option = (section, option)
|
|
||||||
v = self.config.getval(section, option)
|
|
||||||
|
|
||||||
if not v:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
def _validate_repositories(self, section: str, option: str) -> None:
|
|
||||||
self._v_section, self._v_option = (section, option)
|
|
||||||
repos = self.config.getval(section, option)
|
|
||||||
if not repos:
|
|
||||||
raise NoValueError(section, option)
|
|
||||||
|
|
||||||
for repo in repos:
|
|
||||||
if repo.strip().startswith("#") or repo.strip().startswith(";"):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if "," in repo:
|
|
||||||
url, branch = repo.strip().split(",")
|
|
||||||
if not url:
|
|
||||||
raise InvalidValueError(section, option, repo)
|
|
||||||
else:
|
|
||||||
url = repo.strip()
|
|
||||||
if not url:
|
|
||||||
raise InvalidValueError(section, option, repo)
|
|
||||||
except ValueError:
|
|
||||||
raise InvalidValueError(section, option, repo)
|
|
||||||
|
|
||||||
def __set_internal_state(self) -> None:
|
def __set_internal_state(self) -> None:
|
||||||
self.kiauh.backup_before_update = self.config.getboolean(
|
# parse Kiauh options
|
||||||
"kiauh", "backup_before_update"
|
self.kiauh.backup_before_update = self.__read_from_cfg(
|
||||||
|
"kiauh",
|
||||||
|
"backup_before_update",
|
||||||
|
self.config.getboolean,
|
||||||
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
kl_repos = self.config.getval("klipper", "repositories")
|
# parse Klipper options
|
||||||
self.klipper.repositories = self.__set_repo_state(kl_repos)
|
self.klipper.use_python_binary = self.__read_from_cfg(
|
||||||
|
"klipper",
|
||||||
mr_repos = self.config.getval("moonraker", "repositories")
|
"use_python_binary",
|
||||||
self.moonraker.repositories = self.__set_repo_state(mr_repos)
|
self.config.getval,
|
||||||
|
None,
|
||||||
self.mainsail.port = self.config.getint("mainsail", "port")
|
True,
|
||||||
self.mainsail.unstable_releases = self.config.getboolean(
|
|
||||||
"mainsail", "unstable_releases"
|
|
||||||
)
|
)
|
||||||
self.fluidd.port = self.config.getint("fluidd", "port")
|
kl_repos: List[str] = self.__read_from_cfg(
|
||||||
self.fluidd.unstable_releases = self.config.getboolean(
|
"klipper",
|
||||||
"fluidd", "unstable_releases"
|
"repositories",
|
||||||
|
self.config.getvals,
|
||||||
|
[KLIPPER_REPO_URL],
|
||||||
|
)
|
||||||
|
self.klipper.repositories = self.__set_repo_state("klipper", kl_repos)
|
||||||
|
|
||||||
|
# parse Moonraker options
|
||||||
|
self.moonraker.use_python_binary = self.__read_from_cfg(
|
||||||
|
"moonraker",
|
||||||
|
"use_python_binary",
|
||||||
|
self.config.getval,
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.moonraker.optional_speedups = self.__read_from_cfg(
|
||||||
|
"moonraker",
|
||||||
|
"optional_speedups",
|
||||||
|
self.config.getboolean,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
mr_repos: List[str] = self.__read_from_cfg(
|
||||||
|
"moonraker",
|
||||||
|
"repositories",
|
||||||
|
self.config.getvals,
|
||||||
|
[MOONRAKER_REPO_URL],
|
||||||
|
)
|
||||||
|
self.moonraker.repositories = self.__set_repo_state("moonraker", mr_repos)
|
||||||
|
|
||||||
|
# parse Mainsail options
|
||||||
|
self.mainsail.port = self.__read_from_cfg(
|
||||||
|
"mainsail",
|
||||||
|
"port",
|
||||||
|
self.config.getint,
|
||||||
|
80,
|
||||||
|
)
|
||||||
|
self.mainsail.unstable_releases = self.__read_from_cfg(
|
||||||
|
"mainsail",
|
||||||
|
"unstable_releases",
|
||||||
|
self.config.getboolean,
|
||||||
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __set_repo_state(self, repos: List[str]) -> List[Repository]:
|
# parse Fluidd options
|
||||||
|
self.fluidd.port = self.__read_from_cfg(
|
||||||
|
"fluidd",
|
||||||
|
"port",
|
||||||
|
self.config.getint,
|
||||||
|
80,
|
||||||
|
)
|
||||||
|
self.fluidd.unstable_releases = self.__read_from_cfg(
|
||||||
|
"fluidd",
|
||||||
|
"unstable_releases",
|
||||||
|
self.config.getboolean,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __check_option_exists(
|
||||||
|
self, section: str, option: str, fallback: Any, silent: bool = False
|
||||||
|
) -> bool:
|
||||||
|
has_section = self.config.has_section(section)
|
||||||
|
has_option = self.config.has_option(section, option)
|
||||||
|
|
||||||
|
if not (has_section and has_option):
|
||||||
|
if not silent:
|
||||||
|
Logger.print_warn(
|
||||||
|
f"Option '{option}' in section '{section}' not defined. Falling back to '{fallback}'."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __read_bool_from_cfg(
|
||||||
|
self,
|
||||||
|
section: str,
|
||||||
|
option: str,
|
||||||
|
fallback: bool | None = None,
|
||||||
|
silent: bool = False,
|
||||||
|
) -> bool | None:
|
||||||
|
if not self.__check_option_exists(section, option, fallback, silent):
|
||||||
|
return fallback
|
||||||
|
return self.config.getboolean(section, option, fallback)
|
||||||
|
|
||||||
|
def __read_from_cfg(
|
||||||
|
self,
|
||||||
|
section: str,
|
||||||
|
option: str,
|
||||||
|
getter: Callable[[str, str, T | None], T],
|
||||||
|
fallback: T = None,
|
||||||
|
silent: bool = False,
|
||||||
|
) -> T:
|
||||||
|
if not self.__check_option_exists(section, option, fallback, silent):
|
||||||
|
return fallback
|
||||||
|
return getter(section, option, fallback)
|
||||||
|
|
||||||
|
def __set_repo_state(self, section: str, repos: List[str]) -> List[Repository]:
|
||||||
_repos: List[Repository] = []
|
_repos: List[Repository] = []
|
||||||
for repo in repos:
|
for repo in repos:
|
||||||
if repo.strip().startswith("#") or repo.strip().startswith(";"):
|
try:
|
||||||
continue
|
if repo.strip().startswith("#") or repo.strip().startswith(";"):
|
||||||
if "," in repo:
|
continue
|
||||||
url, branch = repo.strip().split(",")
|
if "," in repo:
|
||||||
if not branch:
|
url, branch = repo.strip().split(",")
|
||||||
|
|
||||||
|
if not branch:
|
||||||
|
branch = "master"
|
||||||
|
else:
|
||||||
|
url = repo.strip()
|
||||||
branch = "master"
|
branch = "master"
|
||||||
else:
|
|
||||||
url = repo.strip()
|
# url must not be empty otherwise it's considered
|
||||||
branch = "master"
|
# as an unrecoverable, invalid configuration
|
||||||
_repos.append(Repository(url.strip(), branch.strip()))
|
if not url:
|
||||||
|
raise InvalidValueError(section, "repositories", repo)
|
||||||
|
|
||||||
|
_repos.append(Repository(url.strip(), branch.strip()))
|
||||||
|
|
||||||
|
except InvalidValueError as e:
|
||||||
|
Logger.print_error(f"Error parsing kiauh.cfg: {e}")
|
||||||
|
kill()
|
||||||
|
|
||||||
return _repos
|
return _repos
|
||||||
|
|
||||||
def _set_config_options_state(self) -> None:
|
def __write_internal_state_to_cfg(self) -> None:
|
||||||
"""Updates the config with current settings, preserving values that haven't been modified"""
|
"""Updates the config with current settings, preserving values that haven't been modified"""
|
||||||
if self.kiauh.backup_before_update is not None:
|
if self.kiauh.backup_before_update is not None:
|
||||||
self.config.set_option(
|
self.config.set_option(
|
||||||
@@ -276,6 +327,8 @@ class KiauhSettings:
|
|||||||
"fluidd", "unstable_releases", str(self.fluidd.unstable_releases)
|
"fluidd", "unstable_releases", str(self.fluidd.unstable_releases)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.config.write_file(CUSTOM_CFG)
|
||||||
|
|
||||||
def _check_deprecated_repo_config(self) -> bool:
|
def _check_deprecated_repo_config(self) -> bool:
|
||||||
# repo_url and branch are deprecated - 2025.03.23
|
# repo_url and branch are deprecated - 2025.03.23
|
||||||
for section in ["klipper", "moonraker"]:
|
for section in ["klipper", "moonraker"]:
|
||||||
@@ -287,22 +340,23 @@ class KiauhSettings:
|
|||||||
|
|
||||||
def _prompt_migration_dialog(self) -> None:
|
def _prompt_migration_dialog(self) -> None:
|
||||||
migration_1: List[str] = [
|
migration_1: List[str] = [
|
||||||
"The old 'repo_url' and 'branch' options are now combined under 'repositories'.",
|
"Options 'repo_url' and 'branch' are now combined into a 'repositories' option.",
|
||||||
"\n\n",
|
"\n\n",
|
||||||
"Example format:",
|
"● Old format:",
|
||||||
"[klipper]",
|
" [klipper]",
|
||||||
"repositories:",
|
" repo_url: https://github.com/Klipper3d/klipper",
|
||||||
" https://github.com/Klipper3d/klipper, master",
|
" branch: master",
|
||||||
"\n\n",
|
"\n\n",
|
||||||
"[moonraker]",
|
"● New format:",
|
||||||
"repositories:",
|
" [klipper]",
|
||||||
" https://github.com/Arksine/moonraker, master",
|
" repositories:",
|
||||||
|
" https://github.com/Klipper3d/klipper, master",
|
||||||
]
|
]
|
||||||
Logger.print_dialog(
|
Logger.print_dialog(
|
||||||
DialogType.ATTENTION,
|
DialogType.ATTENTION,
|
||||||
[
|
[
|
||||||
"Deprecated repository configuration found!",
|
"Deprecated kiauh.cfg configuration found!",
|
||||||
"KAIUH can now attempt to automatically migrate your configuration.",
|
"KAIUH can now attempt to automatically migrate the configuration.",
|
||||||
"\n\n",
|
"\n\n",
|
||||||
*migration_1,
|
*migration_1,
|
||||||
],
|
],
|
||||||
@@ -313,7 +367,7 @@ class KiauhSettings:
|
|||||||
Logger.print_dialog(
|
Logger.print_dialog(
|
||||||
DialogType.ERROR,
|
DialogType.ERROR,
|
||||||
[
|
[
|
||||||
"Please update your configuration file manually.",
|
"Please update the configuration file manually.",
|
||||||
],
|
],
|
||||||
center_content=True,
|
center_content=True,
|
||||||
)
|
)
|
||||||
@@ -354,23 +408,7 @@ class KiauhSettings:
|
|||||||
self.config.write_file(CUSTOM_CFG)
|
self.config.write_file(CUSTOM_CFG)
|
||||||
self.config.read_file(CUSTOM_CFG) # reload config
|
self.config.read_file(CUSTOM_CFG) # reload config
|
||||||
|
|
||||||
# Validate the migrated config
|
|
||||||
self._validate_cfg()
|
|
||||||
self.__set_internal_state()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.print_error(f"Error migrating configuration: {e}")
|
Logger.print_error(f"Error migrating configuration: {e}")
|
||||||
Logger.print_error("Please migrate manually.")
|
Logger.print_error("Please migrate manually.")
|
||||||
kill()
|
kill()
|
||||||
|
|
||||||
def __kill(self) -> None:
|
|
||||||
Logger.print_dialog(
|
|
||||||
DialogType.ERROR,
|
|
||||||
[
|
|
||||||
"No KIAUH configuration file found! Please make sure you have at least "
|
|
||||||
"one of the following configuration files in KIAUH's root directory:",
|
|
||||||
"● default.kiauh.cfg",
|
|
||||||
"● kiauh.cfg",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
kill()
|
|
||||||
|
|||||||
@@ -314,9 +314,7 @@ class SimpleConfigParser:
|
|||||||
elements.pop(i)
|
elements.pop(i)
|
||||||
break
|
break
|
||||||
|
|
||||||
def getval(
|
def getval(self, section: str, option: str, fallback: str | _UNSET = _UNSET) -> str:
|
||||||
self, section: str, option: str, fallback: str | _UNSET = _UNSET
|
|
||||||
) -> str | List[str]:
|
|
||||||
"""
|
"""
|
||||||
Return the value of the given option in the given section
|
Return the value of the given option in the given section
|
||||||
|
|
||||||
@@ -329,22 +327,34 @@ class SimpleConfigParser:
|
|||||||
if option not in self.get_options(section):
|
if option not in self.get_options(section):
|
||||||
raise NoOptionError(option, section)
|
raise NoOptionError(option, section)
|
||||||
|
|
||||||
# Find the option in the elements list
|
|
||||||
for element in self.config[section]["elements"]:
|
for element in self.config[section]["elements"]:
|
||||||
if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value] and element["name"] == option:
|
if element["type"] is LineType.OPTION.value and element["name"] == option:
|
||||||
raw_value = element["value"]
|
return str(element["value"].strip().replace("\n", ""))
|
||||||
if isinstance(raw_value, str) and raw_value.endswith("\n"):
|
return ""
|
||||||
return raw_value[:-1].strip()
|
|
||||||
elif isinstance(raw_value, list):
|
except (NoSectionError, NoOptionError):
|
||||||
values: List[str] = []
|
if fallback is _UNSET:
|
||||||
for i, val in enumerate(raw_value):
|
raise
|
||||||
val = val.strip().strip("\n")
|
return fallback
|
||||||
if len(val) < 1:
|
|
||||||
continue
|
def getvals(self, section: str, option: str, fallback: List[str] | _UNSET = _UNSET) -> List[str]:
|
||||||
values.append(val.strip())
|
"""
|
||||||
return values
|
Return the values of the given multi-line option in the given section
|
||||||
return str(raw_value)
|
|
||||||
raise NoOptionError(option, section)
|
If the key is not found and 'fallback' is provided, it is used as
|
||||||
|
a fallback value.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if section not in self.get_sections():
|
||||||
|
raise NoSectionError(section)
|
||||||
|
if option not in self.get_options(section):
|
||||||
|
raise NoOptionError(option, section)
|
||||||
|
|
||||||
|
for element in self.config[section]["elements"]:
|
||||||
|
if element["type"] is LineType.OPTION_BLOCK.value and element["name"] == option:
|
||||||
|
return [val.strip() for val in element["value"] if val.strip()]
|
||||||
|
return []
|
||||||
|
|
||||||
except (NoSectionError, NoOptionError):
|
except (NoSectionError, NoOptionError):
|
||||||
if fallback is _UNSET:
|
if fallback is _UNSET:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ def test_getval(parser):
|
|||||||
assert parser.getval("section_2", "option_2") == "value_2"
|
assert parser.getval("section_2", "option_2") == "value_2"
|
||||||
|
|
||||||
# test multiline option values
|
# test multiline option values
|
||||||
ml_val = parser.getval("section number 5", "multi_option")
|
ml_val = parser.getvals("section number 5", "multi_option")
|
||||||
assert isinstance(ml_val, list)
|
assert isinstance(ml_val, list)
|
||||||
assert len(ml_val) > 0
|
assert len(ml_val) > 0
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ def test_set_new_option(parser):
|
|||||||
assert parser.getval("new_section", "very_new_option") == "very_new_value"
|
assert parser.getval("new_section", "very_new_option") == "very_new_value"
|
||||||
|
|
||||||
parser.set_option("section_2", "array_option", ["value_1", "value_2", "value_3"])
|
parser.set_option("section_2", "array_option", ["value_1", "value_2", "value_3"])
|
||||||
assert parser.getval("section_2", "array_option") == [
|
assert parser.getvals("section_2", "array_option") == [
|
||||||
"value_1",
|
"value_1",
|
||||||
"value_2",
|
"value_2",
|
||||||
"value_3",
|
"value_3",
|
||||||
|
|||||||
@@ -143,6 +143,31 @@ class ExtensionSubmenu(BaseMenu):
|
|||||||
"""
|
"""
|
||||||
)[1:]
|
)[1:]
|
||||||
menu += f"{description_text}\n"
|
menu += f"{description_text}\n"
|
||||||
|
|
||||||
|
# add links if available
|
||||||
|
website: str = (self.extension.metadata.get("website") or "").strip()
|
||||||
|
repo: str = (self.extension.metadata.get("repo") or "").strip()
|
||||||
|
if website or repo:
|
||||||
|
links_lines: List[str] = ["Links:"]
|
||||||
|
if website:
|
||||||
|
links_lines.append(f"- Website: {website}")
|
||||||
|
if repo:
|
||||||
|
links_lines.append(f"- GitHub: {repo}")
|
||||||
|
|
||||||
|
links_text = Logger.format_content(
|
||||||
|
links_lines,
|
||||||
|
line_width,
|
||||||
|
border_left="║",
|
||||||
|
border_right="║",
|
||||||
|
)
|
||||||
|
|
||||||
|
menu += textwrap.dedent(
|
||||||
|
"""
|
||||||
|
╟───────────────────────────────────────────────────────╢
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
menu += f"{links_text}\n"
|
||||||
|
|
||||||
menu += textwrap.dedent(
|
menu += textwrap.dedent(
|
||||||
"""
|
"""
|
||||||
╟───────────────────────────────────────────────────────╢
|
╟───────────────────────────────────────────────────────╢
|
||||||
|
|||||||
22
kiauh/extensions/octoprint/__init__.py
Normal file
22
kiauh/extensions/octoprint/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
OP_DEFAULT_PORT = 5000
|
||||||
|
|
||||||
|
# OctoPrint instance naming/prefixes
|
||||||
|
OP_ENV_PREFIX = "OctoPrint"
|
||||||
|
OP_BASEDIR_PREFIX = ".octoprint"
|
||||||
|
|
||||||
|
# Service/log filenames
|
||||||
|
OP_LOG_NAME = "octoprint.log"
|
||||||
|
|
||||||
|
# Files/paths (computed per-instance where applicable)
|
||||||
|
OP_SUDOERS_FILE = Path("/etc/sudoers.d/octoprint-shutdown")
|
||||||
18
kiauh/extensions/octoprint/metadata.json
Normal file
18
kiauh/extensions/octoprint/metadata.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"index": 12,
|
||||||
|
"module": "octoprint_extension",
|
||||||
|
"maintained_by": "dw-0",
|
||||||
|
"display_name": "OctoPrint",
|
||||||
|
"description": [
|
||||||
|
"Open-source web interface to control and monitor your 3D printer",
|
||||||
|
"- Upload and manage G-code, start/pause/cancel prints",
|
||||||
|
"- Live webcam view and timelapse support",
|
||||||
|
"- Real-time temperature graphs and printer status",
|
||||||
|
"- Powerful plugin ecosystem"
|
||||||
|
],
|
||||||
|
"website": "https://octoprint.org",
|
||||||
|
"repo": "https://github.com/OctoPrint/OctoPrint",
|
||||||
|
"updates": false
|
||||||
|
}
|
||||||
|
}
|
||||||
116
kiauh/extensions/octoprint/octoprint.py
Normal file
116
kiauh/extensions/octoprint/octoprint.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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 textwrap import dedent
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from core.constants import CURRENT_USER
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.logger import Logger
|
||||||
|
from extensions.octoprint import (
|
||||||
|
OP_BASEDIR_PREFIX,
|
||||||
|
OP_ENV_PREFIX,
|
||||||
|
OP_LOG_NAME,
|
||||||
|
)
|
||||||
|
from utils.fs_utils import create_folders
|
||||||
|
from utils.sys_utils import create_service_file, get_service_file_path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Octoprint:
|
||||||
|
suffix: str
|
||||||
|
base: BaseInstance = field(init=False, repr=False)
|
||||||
|
service_file_path: Path = field(init=False)
|
||||||
|
log_file_name = OP_LOG_NAME
|
||||||
|
env_dir: Path = field(init=False)
|
||||||
|
basedir: Path = field(init=False)
|
||||||
|
cfg_file: Path = field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.base = BaseInstance(Klipper, self.suffix)
|
||||||
|
self.base.log_file_name = self.log_file_name
|
||||||
|
|
||||||
|
self.service_file_path = get_service_file_path(Octoprint, self.suffix)
|
||||||
|
|
||||||
|
# OctoPrint stores its data under ~/.octoprint[_SUFFIX]
|
||||||
|
self.basedir = (
|
||||||
|
Path.home().joinpath(OP_BASEDIR_PREFIX)
|
||||||
|
if self.suffix == ""
|
||||||
|
else Path.home().joinpath(f"{OP_BASEDIR_PREFIX}_{self.suffix}")
|
||||||
|
)
|
||||||
|
self.cfg_file = self.basedir.joinpath("config.yaml")
|
||||||
|
|
||||||
|
# OctoPrint virtualenv lives under ~/OctoPrint[_SUFFIX]
|
||||||
|
self.env_dir = (
|
||||||
|
Path.home().joinpath(OP_ENV_PREFIX)
|
||||||
|
if self.suffix == ""
|
||||||
|
else Path.home().joinpath(f"{OP_ENV_PREFIX}_{self.suffix}")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, port: int) -> None:
|
||||||
|
Logger.print_status(
|
||||||
|
f"Creating OctoPrint instance '{self.service_file_path.stem}' ..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure basedir exists and config.yaml is present
|
||||||
|
create_folders([self.basedir])
|
||||||
|
if not self.cfg_file.exists():
|
||||||
|
Logger.print_status("Creating config.yaml ...")
|
||||||
|
self.cfg_file.write_text(self._prep_config_yaml())
|
||||||
|
Logger.print_ok("config.yaml created!")
|
||||||
|
else:
|
||||||
|
Logger.print_info("config.yaml already exists. Skipped ...")
|
||||||
|
|
||||||
|
create_service_file(self.service_file_path.name, self._prep_service_content(port))
|
||||||
|
|
||||||
|
def _prep_service_content(self, port: int) -> str:
|
||||||
|
basedir = self.basedir.as_posix()
|
||||||
|
cfg = self.cfg_file.as_posix()
|
||||||
|
octo_exec = self.env_dir.joinpath("bin/octoprint").as_posix()
|
||||||
|
|
||||||
|
return dedent(
|
||||||
|
f"""\
|
||||||
|
[Unit]
|
||||||
|
Description=Starts OctoPrint on startup
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Environment="LC_ALL=C.UTF-8"
|
||||||
|
Environment="LANG=C.UTF-8"
|
||||||
|
Type=simple
|
||||||
|
User={CURRENT_USER}
|
||||||
|
ExecStart={octo_exec} --basedir {basedir} --config {cfg} --port={port} serve
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prep_config_yaml(self) -> str:
|
||||||
|
printer = self.base.comms_dir.joinpath("klippy.serial").as_posix()
|
||||||
|
restart_service = self.service_file_path.stem
|
||||||
|
|
||||||
|
return dedent(
|
||||||
|
f"""\
|
||||||
|
serial:
|
||||||
|
additionalPorts:
|
||||||
|
- {printer}
|
||||||
|
disconnectOnErrors: false
|
||||||
|
port: {printer}
|
||||||
|
server:
|
||||||
|
commands:
|
||||||
|
serverRestartCommand: sudo service {restart_service} restart
|
||||||
|
systemRestartCommand: sudo shutdown -r now
|
||||||
|
systemShutdownCommand: sudo shutdown -h now
|
||||||
|
"""
|
||||||
|
)
|
||||||
286
kiauh/extensions/octoprint/octoprint_extension.py
Normal file
286
kiauh/extensions/octoprint/octoprint_extension.py
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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 re
|
||||||
|
from typing import Dict, List, Optional, Set
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.logger import DialogType, Logger
|
||||||
|
from core.types.color import Color
|
||||||
|
from core.menus.base_menu import print_back_footer
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from extensions.octoprint import (
|
||||||
|
OP_SUDOERS_FILE, OP_DEFAULT_PORT,
|
||||||
|
)
|
||||||
|
from extensions.octoprint.octoprint import Octoprint
|
||||||
|
from utils.common import check_install_dependencies
|
||||||
|
from utils.fs_utils import run_remove_routines, remove_with_sudo
|
||||||
|
from utils.input_utils import get_selection_input, get_confirm
|
||||||
|
from utils.instance_utils import get_instances
|
||||||
|
from utils.sys_utils import (
|
||||||
|
create_python_venv,
|
||||||
|
get_ipv4_addr,
|
||||||
|
install_python_packages,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class OctoprintExtension(BaseExtension):
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
Logger.print_status("Installing OctoPrint ...")
|
||||||
|
|
||||||
|
klipper_instances: List[Klipper] = get_instances(Klipper)
|
||||||
|
if not klipper_instances:
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.WARNING,
|
||||||
|
[
|
||||||
|
"Klipper not found! Please install Klipper first.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
existing_ops: List[Octoprint] = get_instances(Octoprint)
|
||||||
|
existing_by_suffix: Dict[str, Octoprint] = {op.suffix: op for op in existing_ops}
|
||||||
|
candidates: List[Klipper] = [k for k in klipper_instances if k.suffix not in existing_by_suffix]
|
||||||
|
|
||||||
|
chosen: List[Klipper] = []
|
||||||
|
|
||||||
|
if len(klipper_instances) == 1:
|
||||||
|
k = klipper_instances[0]
|
||||||
|
if k.suffix in existing_by_suffix:
|
||||||
|
if not get_confirm(
|
||||||
|
f"OctoPrint already exists for '{k.service_file_path.stem}'. Reinstall?",
|
||||||
|
default_choice=True,
|
||||||
|
allow_go_back=True,
|
||||||
|
):
|
||||||
|
Logger.print_info("Aborted OctoPrint installation.")
|
||||||
|
return
|
||||||
|
chosen = [k]
|
||||||
|
else:
|
||||||
|
while True:
|
||||||
|
dialog = "╔═══════════════════════════════════════════════════════╗\n"
|
||||||
|
headline = Color.apply(
|
||||||
|
"The following Klipper instances were found:", Color.GREEN
|
||||||
|
)
|
||||||
|
dialog += f"║{headline:^64}║\n"
|
||||||
|
dialog += "╟───────────────────────────────────────────────────────╢\n"
|
||||||
|
|
||||||
|
if candidates:
|
||||||
|
line_all = Color.apply("a) Select all (install for all missing)", Color.YELLOW)
|
||||||
|
dialog += f"║ {line_all:<63}║\n"
|
||||||
|
dialog += "║ ║\n"
|
||||||
|
|
||||||
|
index_map: Dict[str, Klipper] = {}
|
||||||
|
for i, k in enumerate(klipper_instances, start=1):
|
||||||
|
mapping = existing_by_suffix.get(k.suffix)
|
||||||
|
suffix = f" <-> {mapping.service_file_path.stem}" if mapping else ""
|
||||||
|
line = Color.apply(f"{i}) {k.service_file_path.stem}{suffix}", Color.CYAN)
|
||||||
|
dialog += f"║ {line:<63}║\n"
|
||||||
|
index_map[str(i)] = k
|
||||||
|
|
||||||
|
dialog += "╟───────────────────────────────────────────────────────╢\n"
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
allowed = list(index_map.keys()) + ["b"] + (["a"] if candidates else [])
|
||||||
|
choice = get_selection_input("Choose instance to install OctoPrint for", allowed)
|
||||||
|
|
||||||
|
if choice == "b":
|
||||||
|
Logger.print_info("Aborted OctoPrint installation.")
|
||||||
|
return
|
||||||
|
if choice == "a":
|
||||||
|
chosen = candidates
|
||||||
|
break
|
||||||
|
|
||||||
|
selected = index_map[choice]
|
||||||
|
if selected.suffix in existing_by_suffix:
|
||||||
|
confirm = get_confirm(
|
||||||
|
f"OctoPrint already exists for '{selected.service_file_path.stem}'. Reinstall?",
|
||||||
|
default_choice=True,
|
||||||
|
allow_go_back=True,
|
||||||
|
)
|
||||||
|
if not confirm:
|
||||||
|
# back to menu
|
||||||
|
continue
|
||||||
|
chosen = [selected]
|
||||||
|
break
|
||||||
|
|
||||||
|
deps = {
|
||||||
|
"git",
|
||||||
|
"wget",
|
||||||
|
"python3-pip",
|
||||||
|
"python3-dev",
|
||||||
|
"libyaml-dev",
|
||||||
|
"build-essential",
|
||||||
|
"python3-setuptools",
|
||||||
|
"python3-virtualenv",
|
||||||
|
}
|
||||||
|
check_install_dependencies(deps)
|
||||||
|
|
||||||
|
# Determine used ports from existing OctoPrint services and prepare regex
|
||||||
|
used_ports: Set[int] = set()
|
||||||
|
port_re = re.compile(r"--port=(\d+)")
|
||||||
|
for op in existing_ops:
|
||||||
|
try:
|
||||||
|
content = op.service_file_path.read_text()
|
||||||
|
m = port_re.search(content)
|
||||||
|
if m:
|
||||||
|
used_ports.add(int(m.group(1)))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# noinspection PyShadowingNames
|
||||||
|
def read_existing_port(suffix: str) -> Optional[int]:
|
||||||
|
op = existing_by_suffix.get(suffix)
|
||||||
|
if not op:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
content = op.service_file_path.read_text()
|
||||||
|
m = port_re.search(content)
|
||||||
|
return int(m.group(1)) if m else None
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def next_free_port(start: int, used: Set[int]) -> int:
|
||||||
|
p = start
|
||||||
|
while p in used:
|
||||||
|
p += 1
|
||||||
|
used.add(p)
|
||||||
|
return p
|
||||||
|
|
||||||
|
created_ops: List[Octoprint] = []
|
||||||
|
for k in chosen:
|
||||||
|
# Keep existing port on reinstall, otherwise assign next free one
|
||||||
|
existing_port = read_existing_port(k.suffix)
|
||||||
|
port = existing_port if existing_port is not None else next_free_port(OP_DEFAULT_PORT, used_ports)
|
||||||
|
|
||||||
|
instance = Octoprint(suffix=k.suffix)
|
||||||
|
|
||||||
|
if create_python_venv(instance.env_dir, force=False):
|
||||||
|
Logger.print_ok(
|
||||||
|
f"Virtualenv created: {instance.env_dir}", prefix=False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
f"Virtualenv exists: {instance.env_dir}. Skipping creation ..."
|
||||||
|
)
|
||||||
|
|
||||||
|
install_python_packages(instance.env_dir, ["octoprint"])
|
||||||
|
|
||||||
|
instance.create(port=port)
|
||||||
|
created_ops.append(instance)
|
||||||
|
|
||||||
|
for inst in created_ops:
|
||||||
|
try:
|
||||||
|
InstanceManager.enable(inst)
|
||||||
|
InstanceManager.start(inst)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Failed to enable/start {inst.service_file_path.name}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ip = get_ipv4_addr()
|
||||||
|
lines = ["Access your new OctoPrint instance(s) at:"]
|
||||||
|
for inst in created_ops:
|
||||||
|
try:
|
||||||
|
content = inst.service_file_path.read_text()
|
||||||
|
m = port_re.search(content)
|
||||||
|
if m:
|
||||||
|
# noinspection HttpUrlsUsage
|
||||||
|
lines.append(f"● {inst.service_file_path.stem}: http://{ip}:{m.group(1)}")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
Logger.print_dialog(DialogType.SUCCESS, lines, center_content=False)
|
||||||
|
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
Logger.print_status("Removing OctoPrint ...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
op_instances: List[Octoprint] = get_instances(Octoprint)
|
||||||
|
if not op_instances:
|
||||||
|
Logger.print_info("No OctoPrint instances found. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
remove_all = False
|
||||||
|
if len(op_instances) == 1:
|
||||||
|
to_remove = op_instances
|
||||||
|
else:
|
||||||
|
dialog = "╔═══════════════════════════════════════════════════════╗\n"
|
||||||
|
headline = Color.apply(
|
||||||
|
"The following OctoPrint instances were found:", Color.GREEN
|
||||||
|
)
|
||||||
|
dialog += f"║{headline:^64}║\n"
|
||||||
|
dialog += "╟───────────────────────────────────────────────────────╢\n"
|
||||||
|
select_all = Color.apply("a) Select all", Color.YELLOW)
|
||||||
|
dialog += f"║ {select_all:<63}║\n"
|
||||||
|
dialog += "║ ║\n"
|
||||||
|
|
||||||
|
for i, inst in enumerate(op_instances, start=1):
|
||||||
|
line = Color.apply(
|
||||||
|
f"{i}) {inst.service_file_path.stem}", Color.CYAN
|
||||||
|
)
|
||||||
|
dialog += f"║ {line:<63}║\n"
|
||||||
|
dialog += "╟───────────────────────────────────────────────────────╢\n"
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
allowed = [str(i) for i in range(1, len(op_instances) + 1)]
|
||||||
|
allowed.extend(["a", "b"])
|
||||||
|
choice = get_selection_input("Choose instance to remove", allowed)
|
||||||
|
|
||||||
|
if choice == "a":
|
||||||
|
remove_all = True
|
||||||
|
to_remove = op_instances
|
||||||
|
elif choice == "b":
|
||||||
|
Logger.print_info("Aborted OctoPrint removal.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
idx = int(choice) - 1
|
||||||
|
to_remove = [op_instances[idx]]
|
||||||
|
|
||||||
|
for inst in to_remove:
|
||||||
|
Logger.print_status(
|
||||||
|
f"Removing instance {inst.service_file_path.stem} ..."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
InstanceManager.remove(inst)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Failed to remove service {inst.service_file_path.name}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove only this instance's env and basedir
|
||||||
|
if inst.env_dir.exists():
|
||||||
|
Logger.print_status(f"Removing {inst.env_dir} ...")
|
||||||
|
run_remove_routines(inst.env_dir)
|
||||||
|
if inst.basedir.exists():
|
||||||
|
Logger.print_status(f"Removing {inst.basedir} ...")
|
||||||
|
run_remove_routines(inst.basedir)
|
||||||
|
|
||||||
|
# Remove sudoers file only if no instances remain
|
||||||
|
remaining = get_instances(Octoprint)
|
||||||
|
if not remaining and OP_SUDOERS_FILE.exists():
|
||||||
|
Logger.print_status(f"Removing {OP_SUDOERS_FILE} ...")
|
||||||
|
remove_with_sudo(OP_SUDOERS_FILE)
|
||||||
|
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.SUCCESS,
|
||||||
|
[
|
||||||
|
"Selected OctoPrint instance(s) successfully removed!"
|
||||||
|
if not remove_all
|
||||||
|
else "All OctoPrint instances successfully removed!",
|
||||||
|
],
|
||||||
|
center_content=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Error during OctoPrint removal: {e}")
|
||||||
@@ -43,7 +43,7 @@ class PrettyGcodeExtension(BaseExtension):
|
|||||||
|
|
||||||
port = get_number_input(
|
port = get_number_input(
|
||||||
"On which port should PrettyGCode run",
|
"On which port should PrettyGCode run",
|
||||||
min_count=0,
|
min_value=0,
|
||||||
default=7136,
|
default=7136,
|
||||||
allow_go_back=True,
|
allow_go_back=True,
|
||||||
)
|
)
|
||||||
|
|||||||
16
kiauh/extensions/spoolman/__init__.py
Normal file
16
kiauh/extensions/spoolman/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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
|
||||||
|
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
SPOOLMAN_DOCKER_IMAGE = "ghcr.io/donkie/spoolman:latest"
|
||||||
|
SPOOLMAN_DIR = Path.home().joinpath("spoolman")
|
||||||
|
SPOOLMAN_DATA_DIR = SPOOLMAN_DIR.joinpath("data")
|
||||||
|
SPOOLMAN_COMPOSE_FILE = SPOOLMAN_DIR.joinpath("docker-compose.yml")
|
||||||
|
SPOOLMAN_DEFAULT_PORT = 7912
|
||||||
14
kiauh/extensions/spoolman/assets/docker-compose.yml
Normal file
14
kiauh/extensions/spoolman/assets/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
spoolman:
|
||||||
|
image: ghcr.io/donkie/spoolman:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# Mount the host machine's ./data directory into the container's /home/app/.local/share/spoolman directory
|
||||||
|
- type: bind
|
||||||
|
source: ./data # This is where the data will be stored locally. Could also be set to for example `source: /home/pi/printer_data/spoolman`.
|
||||||
|
target: /home/app/.local/share/spoolman # Do NOT modify this line
|
||||||
|
ports:
|
||||||
|
# Map the host machine's port 7912 to the container's port 8000
|
||||||
|
- "7912:8000"
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Stockholm # Optional, defaults to UTC
|
||||||
18
kiauh/extensions/spoolman/metadata.json
Normal file
18
kiauh/extensions/spoolman/metadata.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"index": 11,
|
||||||
|
"module": "spoolman_extension",
|
||||||
|
"maintained_by": "dw-0",
|
||||||
|
"display_name": "Spoolman (Docker)",
|
||||||
|
"description": [
|
||||||
|
"Filament manager for 3D printing",
|
||||||
|
"- Track your filament inventory",
|
||||||
|
"- Monitor filament usage",
|
||||||
|
"- Manage vendors, materials, and spools",
|
||||||
|
"- Integrates with Moonraker",
|
||||||
|
"\n\n",
|
||||||
|
"Note: This extension installs Spoolman using Docker. Docker must be installed on your system before installing Spoolman."
|
||||||
|
],
|
||||||
|
"updates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
190
kiauh/extensions/spoolman/spoolman.py
Normal file
190
kiauh/extensions/spoolman/spoolman.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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 shutil
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import CalledProcessError, run
|
||||||
|
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.logger import Logger
|
||||||
|
from extensions.spoolman import (
|
||||||
|
MODULE_PATH,
|
||||||
|
SPOOLMAN_COMPOSE_FILE,
|
||||||
|
SPOOLMAN_DIR,
|
||||||
|
SPOOLMAN_DOCKER_IMAGE,
|
||||||
|
)
|
||||||
|
from utils.sys_utils import get_system_timezone
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Spoolman:
|
||||||
|
suffix: str
|
||||||
|
base: BaseInstance = field(init=False, repr=False)
|
||||||
|
dir: Path = SPOOLMAN_DIR
|
||||||
|
data_dir: Path = field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.base: BaseInstance = BaseInstance(Moonraker, self.suffix)
|
||||||
|
self.data_dir = self.base.data_dir
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_container_running() -> bool:
|
||||||
|
"""Check if the Spoolman container is running"""
|
||||||
|
try:
|
||||||
|
result = run(
|
||||||
|
["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "ps", "-q"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return bool(result.stdout.strip())
|
||||||
|
except CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_docker_available() -> bool:
|
||||||
|
"""Check if Docker is installed and available"""
|
||||||
|
try:
|
||||||
|
run(["docker", "--version"], capture_output=True, check=True)
|
||||||
|
return True
|
||||||
|
except (CalledProcessError, FileNotFoundError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_docker_compose_available() -> bool:
|
||||||
|
"""Check if Docker Compose is installed and available"""
|
||||||
|
try:
|
||||||
|
# Try modern docker compose command
|
||||||
|
run(["docker", "compose", "version"], capture_output=True, check=True)
|
||||||
|
return True
|
||||||
|
except (CalledProcessError, FileNotFoundError):
|
||||||
|
# Try legacy docker-compose command
|
||||||
|
try:
|
||||||
|
run(["docker-compose", "--version"], capture_output=True, check=True)
|
||||||
|
return True
|
||||||
|
except (CalledProcessError, FileNotFoundError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_docker_compose() -> bool:
|
||||||
|
"""Copy the docker-compose.yml file for Spoolman and set system timezone"""
|
||||||
|
try:
|
||||||
|
shutil.copy(
|
||||||
|
MODULE_PATH.joinpath("assets/docker-compose.yml"),
|
||||||
|
SPOOLMAN_COMPOSE_FILE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# get system timezone
|
||||||
|
timezone = get_system_timezone()
|
||||||
|
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
content = content.replace("TZ=Europe/Stockholm", f"TZ={timezone}")
|
||||||
|
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Error creating Docker Compose file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def start_container() -> bool:
|
||||||
|
"""Start the Spoolman container"""
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "up", "-d"],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to start Spoolman container: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_container() -> bool:
|
||||||
|
"""Update the Spoolman container"""
|
||||||
|
|
||||||
|
def __get_image_id() -> str:
|
||||||
|
"""Get the image ID of the Spoolman Docker image"""
|
||||||
|
try:
|
||||||
|
result = run(
|
||||||
|
["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except CalledProcessError:
|
||||||
|
raise Exception("Failed to get Spoolman Docker image ID")
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_image_id = __get_image_id()
|
||||||
|
Logger.print_status("Pulling latest Spoolman image...")
|
||||||
|
Spoolman.pull_image()
|
||||||
|
new_image_id = __get_image_id()
|
||||||
|
Logger.print_status("Tearing down old Spoolman container...")
|
||||||
|
Spoolman.tear_down_container()
|
||||||
|
Logger.print_status("Spinning up new Spoolman container...")
|
||||||
|
Spoolman.start_container()
|
||||||
|
if old_image_id != new_image_id:
|
||||||
|
Logger.print_status("Removing old Spoolman image...")
|
||||||
|
run(["docker", "rmi", old_image_id], check=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to update Spoolman container: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def tear_down_container() -> bool:
|
||||||
|
"""Stop and remove the Spoolman container"""
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "down"],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to tear down Spoolman container: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pull_image() -> bool:
|
||||||
|
"""Pull the Spoolman Docker image"""
|
||||||
|
try:
|
||||||
|
run(["docker", "pull", SPOOLMAN_DOCKER_IMAGE], check=True)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to pull Spoolman Docker image: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_image() -> bool:
|
||||||
|
"""Remove the Spoolman Docker image"""
|
||||||
|
try:
|
||||||
|
image_exists = run(
|
||||||
|
["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
).stdout.strip()
|
||||||
|
if not image_exists:
|
||||||
|
Logger.print_info("Spoolman Docker image not found. Nothing to remove.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
run(["docker", "rmi", SPOOLMAN_DOCKER_IMAGE], check=True)
|
||||||
|
return True
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to remove Spoolman Docker image: {e}")
|
||||||
|
return False
|
||||||
344
kiauh/extensions/spoolman/spoolman_extension.py
Normal file
344
kiauh/extensions/spoolman/spoolman_extension.py
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2025 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 re
|
||||||
|
from subprocess import CalledProcessError, run
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.services.moonraker_instance_service import (
|
||||||
|
MoonrakerInstanceService,
|
||||||
|
)
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.logger import DialogType, Logger
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from extensions.spoolman import (
|
||||||
|
SPOOLMAN_COMPOSE_FILE,
|
||||||
|
SPOOLMAN_DATA_DIR,
|
||||||
|
SPOOLMAN_DEFAULT_PORT,
|
||||||
|
SPOOLMAN_DIR,
|
||||||
|
)
|
||||||
|
from extensions.spoolman.spoolman import Spoolman
|
||||||
|
from utils.config_utils import (
|
||||||
|
add_config_section,
|
||||||
|
remove_config_section,
|
||||||
|
)
|
||||||
|
from utils.fs_utils import run_remove_routines
|
||||||
|
from utils.input_utils import get_confirm, get_number_input
|
||||||
|
from utils.sys_utils import get_ipv4_addr
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class SpoolmanExtension(BaseExtension):
|
||||||
|
ip: str = ""
|
||||||
|
port: int = SPOOLMAN_DEFAULT_PORT
|
||||||
|
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
Logger.print_status("Installing Spoolman using Docker...")
|
||||||
|
|
||||||
|
docker_available, docker_compose_available = self.__check_docker_prereqs()
|
||||||
|
if not docker_available or not docker_compose_available:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.__handle_existing_installation():
|
||||||
|
self.ip: str = get_ipv4_addr()
|
||||||
|
self.__run_setup()
|
||||||
|
|
||||||
|
# noinspection HttpUrlsUsage
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.SUCCESS,
|
||||||
|
[
|
||||||
|
"Spoolman successfully installed using Docker!",
|
||||||
|
"You can access Spoolman via the following URL:",
|
||||||
|
f"http://{self.ip}:{self.port}",
|
||||||
|
],
|
||||||
|
center_content=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_extension(self, **kwargs) -> None:
|
||||||
|
Logger.print_status("Updating Spoolman Docker container...")
|
||||||
|
|
||||||
|
if not SPOOLMAN_DIR.exists() or not SPOOLMAN_COMPOSE_FILE.exists():
|
||||||
|
Logger.print_error("Spoolman installation not found or incomplete.")
|
||||||
|
return
|
||||||
|
|
||||||
|
docker_available, docker_compose_available = self.__check_docker_prereqs()
|
||||||
|
if not docker_available or not docker_compose_available:
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Updating Spoolman container...")
|
||||||
|
if not Spoolman.update_container():
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.SUCCESS,
|
||||||
|
["Spoolman Docker container successfully updated!"],
|
||||||
|
center_content=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
Logger.print_status("Removing Spoolman Docker container...")
|
||||||
|
|
||||||
|
if not SPOOLMAN_DIR.exists():
|
||||||
|
Logger.print_info("Spoolman is not installed. Nothing to remove.")
|
||||||
|
return
|
||||||
|
|
||||||
|
docker_available, docker_compose_available = self.__check_docker_prereqs()
|
||||||
|
if not docker_available or not docker_compose_available:
|
||||||
|
return
|
||||||
|
|
||||||
|
# remove moonraker integration
|
||||||
|
mrsvc = MoonrakerInstanceService()
|
||||||
|
mrsvc.load_instances()
|
||||||
|
mr_instances: List[Moonraker] = mrsvc.get_all_instances()
|
||||||
|
|
||||||
|
Logger.print_status("Removing Spoolman configuration from moonraker.conf...")
|
||||||
|
remove_config_section("spoolman", mr_instances)
|
||||||
|
|
||||||
|
Logger.print_status("Removing Spoolman from moonraker.asvc...")
|
||||||
|
self.__remove_from_moonraker_asvc()
|
||||||
|
|
||||||
|
# stop and remove the container if docker-compose exists
|
||||||
|
if SPOOLMAN_COMPOSE_FILE.exists():
|
||||||
|
Logger.print_status("Stopping and removing Spoolman container...")
|
||||||
|
|
||||||
|
if Spoolman.tear_down_container():
|
||||||
|
Logger.print_ok("Spoolman container removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_error(
|
||||||
|
"Failed to remove Spoolman container! Please remove it manually."
|
||||||
|
)
|
||||||
|
|
||||||
|
if Spoolman.remove_image():
|
||||||
|
Logger.print_ok("Spoolman container and image removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_error(
|
||||||
|
"Failed to remove Spoolman image! Please remove it manually."
|
||||||
|
)
|
||||||
|
|
||||||
|
# backup Spoolman directory to ~/spoolman_data-<timestamp> before removing it
|
||||||
|
try:
|
||||||
|
bm = BackupManager()
|
||||||
|
result = bm.backup_directory(
|
||||||
|
f"{SPOOLMAN_DIR.name}_data",
|
||||||
|
source=SPOOLMAN_DIR,
|
||||||
|
target=SPOOLMAN_DIR.parent,
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
Logger.print_ok(f"Spoolman data backed up to {result}")
|
||||||
|
Logger.print_status("Removing Spoolman directory...")
|
||||||
|
if run_remove_routines(SPOOLMAN_DIR):
|
||||||
|
Logger.print_ok("Spoolman directory removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_error(
|
||||||
|
"Failed to remove Spoolman directory! Please remove it manually."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Failed to backup Spoolman directory: {e}")
|
||||||
|
Logger.print_info("Skipping Spoolman directory removal...")
|
||||||
|
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.SUCCESS,
|
||||||
|
["Spoolman successfully removed!"],
|
||||||
|
center_content=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __run_setup(self) -> None:
|
||||||
|
# Create Spoolman directory and data directory
|
||||||
|
Logger.print_status("Setting up Spoolman directories...")
|
||||||
|
SPOOLMAN_DIR.mkdir(parents=True)
|
||||||
|
Logger.print_ok(f"Directory {SPOOLMAN_DIR} created!")
|
||||||
|
SPOOLMAN_DATA_DIR.mkdir(parents=True)
|
||||||
|
Logger.print_ok(f"Directory {SPOOLMAN_DATA_DIR} created!")
|
||||||
|
|
||||||
|
# Set correct permissions for data directory
|
||||||
|
try:
|
||||||
|
Logger.print_status("Setting permissions for Spoolman data directory...")
|
||||||
|
run(["chown", "1000:1000", str(SPOOLMAN_DATA_DIR)], check=True)
|
||||||
|
Logger.print_ok("Permissions set!")
|
||||||
|
except CalledProcessError:
|
||||||
|
Logger.print_warn(
|
||||||
|
"Could not set permissions on data directory. This might cause issues."
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.print_status("Creating Docker Compose file...")
|
||||||
|
if Spoolman.create_docker_compose():
|
||||||
|
Logger.print_ok("Docker Compose file created!")
|
||||||
|
else:
|
||||||
|
Logger.print_error("Failed to create Docker Compose file!")
|
||||||
|
|
||||||
|
self.__port_config_prompt()
|
||||||
|
|
||||||
|
Logger.print_status("Spinning up Spoolman container...")
|
||||||
|
if Spoolman.start_container():
|
||||||
|
Logger.print_ok("Spoolman container started!")
|
||||||
|
else:
|
||||||
|
Logger.print_error("Failed to start Spoolman container!")
|
||||||
|
|
||||||
|
if self.__add_moonraker_integration():
|
||||||
|
Logger.print_ok("Spoolman integration added to Moonraker!")
|
||||||
|
else:
|
||||||
|
Logger.print_info("Moonraker integration skipped.")
|
||||||
|
|
||||||
|
def __check_docker_prereqs(self) -> Tuple[bool, bool]:
|
||||||
|
# check if Docker is available
|
||||||
|
is_docker_available = Spoolman.is_docker_available()
|
||||||
|
if not is_docker_available:
|
||||||
|
Logger.print_error("Docker is not installed or not available.")
|
||||||
|
Logger.print_info(
|
||||||
|
"Please install Docker first: https://docs.docker.com/engine/install/"
|
||||||
|
)
|
||||||
|
|
||||||
|
# check if Docker Compose is available
|
||||||
|
is_docker_compose_available = Spoolman.is_docker_compose_available()
|
||||||
|
if not is_docker_compose_available:
|
||||||
|
Logger.print_error("Docker Compose is not installed or not available.")
|
||||||
|
|
||||||
|
return is_docker_available, is_docker_compose_available
|
||||||
|
|
||||||
|
def __port_config_prompt(self) -> None:
|
||||||
|
"""Prompt for advanced configuration options"""
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.INFO,
|
||||||
|
[
|
||||||
|
"You can configure Spoolman to run on a different port than the default. "
|
||||||
|
"Make sure you don't select a port which is already in use by "
|
||||||
|
"another application. Your input will not be validated! "
|
||||||
|
"The default port is 7912.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if not get_confirm("Continue with default port 7912?", default_choice=True):
|
||||||
|
self.__set_port()
|
||||||
|
|
||||||
|
def __set_port(self) -> None:
|
||||||
|
"""Configure advanced options for Spoolman Docker container"""
|
||||||
|
port = get_number_input(
|
||||||
|
"Which port should Spoolman run on?",
|
||||||
|
default=SPOOLMAN_DEFAULT_PORT,
|
||||||
|
min_value=1024,
|
||||||
|
max_value=65535,
|
||||||
|
)
|
||||||
|
|
||||||
|
if port != SPOOLMAN_DEFAULT_PORT:
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
port_mapping_pattern = r'"(\d+):8000"'
|
||||||
|
content = re.sub(port_mapping_pattern, f'"{port}:8000"', content)
|
||||||
|
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
Logger.print_ok(f"Port set to {port}...")
|
||||||
|
|
||||||
|
def __handle_existing_installation(self) -> bool:
|
||||||
|
if not (SPOOLMAN_DIR.exists() and SPOOLMAN_DIR.is_dir()):
|
||||||
|
return False
|
||||||
|
|
||||||
|
compose_file_exists = SPOOLMAN_COMPOSE_FILE.exists()
|
||||||
|
container_running = Spoolman.is_container_running()
|
||||||
|
|
||||||
|
if container_running and compose_file_exists:
|
||||||
|
Logger.print_info("Spoolman is already installed!")
|
||||||
|
return True
|
||||||
|
elif container_running and not compose_file_exists:
|
||||||
|
Logger.print_status(
|
||||||
|
"Spoolman container is running but Docker Compose file is missing..."
|
||||||
|
)
|
||||||
|
if get_confirm(
|
||||||
|
"Do you want to recreate the Docker Compose file?",
|
||||||
|
default_choice=True,
|
||||||
|
):
|
||||||
|
Spoolman.create_docker_compose()
|
||||||
|
self.__port_config_prompt()
|
||||||
|
return True
|
||||||
|
elif not container_running and compose_file_exists:
|
||||||
|
Logger.print_status(
|
||||||
|
"Docker Compose file exists but container is not running..."
|
||||||
|
)
|
||||||
|
Spoolman.start_container()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __add_moonraker_integration(self) -> bool:
|
||||||
|
"""Enable Moonraker integration for Spoolman Docker container"""
|
||||||
|
if not get_confirm("Add Moonraker integration?", default_choice=True):
|
||||||
|
return False
|
||||||
|
|
||||||
|
Logger.print_status("Adding Spoolman integration to Moonraker...")
|
||||||
|
|
||||||
|
# read port from the docker-compose file
|
||||||
|
port = SPOOLMAN_DEFAULT_PORT
|
||||||
|
if SPOOLMAN_COMPOSE_FILE.exists():
|
||||||
|
with open(SPOOLMAN_COMPOSE_FILE, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
# Extract port from the port mapping
|
||||||
|
port_match = re.search(r'"(\d+):8000"', content)
|
||||||
|
if port_match:
|
||||||
|
port = port_match.group(1)
|
||||||
|
|
||||||
|
mrsvc = MoonrakerInstanceService()
|
||||||
|
mrsvc.load_instances()
|
||||||
|
mr_instances = mrsvc.get_all_instances()
|
||||||
|
|
||||||
|
# noinspection HttpUrlsUsage
|
||||||
|
add_config_section(
|
||||||
|
section="spoolman",
|
||||||
|
instances=mr_instances,
|
||||||
|
options=[("server", f"http://{self.ip}:{port}")],
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.print_status("Adding Spoolman to moonraker.asvc...")
|
||||||
|
self.__add_to_moonraker_asvc()
|
||||||
|
|
||||||
|
InstanceManager.restart_all(mr_instances)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __add_to_moonraker_asvc(self) -> None:
|
||||||
|
"""Add Spoolman to moonraker.asvc"""
|
||||||
|
mrsvc = MoonrakerInstanceService()
|
||||||
|
mrsvc.load_instances()
|
||||||
|
mr_instances = mrsvc.get_all_instances()
|
||||||
|
for instance in mr_instances:
|
||||||
|
asvc_path = instance.data_dir.joinpath("moonraker.asvc")
|
||||||
|
if asvc_path.exists():
|
||||||
|
if "Spoolman" in open(asvc_path).read():
|
||||||
|
Logger.print_info(f"Spoolman already in {asvc_path}. Skipping...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(asvc_path, "a") as f:
|
||||||
|
f.write("Spoolman\n")
|
||||||
|
|
||||||
|
Logger.print_ok(f"Spoolman added to {asvc_path}!")
|
||||||
|
|
||||||
|
def __remove_from_moonraker_asvc(self) -> None:
|
||||||
|
"""Remove Spoolman from moonraker.asvc"""
|
||||||
|
mrsvc = MoonrakerInstanceService()
|
||||||
|
mrsvc.load_instances()
|
||||||
|
mr_instances = mrsvc.get_all_instances()
|
||||||
|
for instance in mr_instances:
|
||||||
|
asvc_path = instance.data_dir.joinpath("moonraker.asvc")
|
||||||
|
if asvc_path.exists():
|
||||||
|
if "Spoolman" not in open(asvc_path).read():
|
||||||
|
Logger.print_info(f"Spoolman not in {asvc_path}. Skipping...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(asvc_path, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
new_lines = [line for line in lines if "Spoolman" not in line]
|
||||||
|
|
||||||
|
with open(asvc_path, "w") as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
|
||||||
|
Logger.print_ok(f"Spoolman removed from {asvc_path}!")
|
||||||
@@ -27,7 +27,9 @@ from components.moonraker import (
|
|||||||
MOONRAKER_REQ_FILE,
|
MOONRAKER_REQ_FILE,
|
||||||
)
|
)
|
||||||
from components.moonraker.moonraker import Moonraker
|
from components.moonraker.moonraker import Moonraker
|
||||||
from components.moonraker.moonraker_setup import install_moonraker_packages
|
from components.moonraker.services.moonraker_setup_service import (
|
||||||
|
install_moonraker_packages,
|
||||||
|
)
|
||||||
from core.backup_manager.backup_manager import BackupManager, BackupManagerException
|
from core.backup_manager.backup_manager import BackupManager, BackupManagerException
|
||||||
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
|
||||||
|
|||||||
@@ -42,10 +42,13 @@ from utils.sys_utils import (
|
|||||||
def get_kiauh_version() -> str:
|
def get_kiauh_version() -> str:
|
||||||
"""
|
"""
|
||||||
Helper method to get the current KIAUH version by reading the latest tag
|
Helper method to get the current KIAUH version by reading the latest tag
|
||||||
:return: string of the latest tag
|
:return: string of the latest tag or a default value if no tags exist
|
||||||
"""
|
"""
|
||||||
lastest_tag: str = get_local_tags(Path(__file__).parent.parent)[-1]
|
tags = get_local_tags(Path(__file__).parent.parent)
|
||||||
return lastest_tag
|
if tags:
|
||||||
|
return tags[-1]
|
||||||
|
else:
|
||||||
|
return "v?.?.?"
|
||||||
|
|
||||||
|
|
||||||
def convert_camelcase_to_kebabcase(name: str) -> str:
|
def convert_camelcase_to_kebabcase(name: str) -> str:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple, Union
|
||||||
|
|
||||||
from core.logger import Logger
|
from core.logger import 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 (
|
||||||
@@ -19,7 +19,7 @@ from core.submodules.simple_config_parser.src.simple_config_parser.simple_config
|
|||||||
)
|
)
|
||||||
from utils.instance_type import InstanceType
|
from utils.instance_type import InstanceType
|
||||||
|
|
||||||
ConfigOption = Tuple[str, str]
|
ConfigOption = Tuple[str, Union[str, List[str]]]
|
||||||
|
|
||||||
|
|
||||||
def add_config_section(
|
def add_config_section(
|
||||||
|
|||||||
@@ -52,16 +52,16 @@ def get_confirm(question: str, default_choice=True, allow_go_back=False) -> bool
|
|||||||
|
|
||||||
def get_number_input(
|
def get_number_input(
|
||||||
question: str,
|
question: str,
|
||||||
min_count: int,
|
min_value: int,
|
||||||
max_count: int | None = None,
|
max_value: int | None = None,
|
||||||
default: int | None = None,
|
default: int | None = None,
|
||||||
allow_go_back: bool = False,
|
allow_go_back: bool = False,
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
"""
|
"""
|
||||||
Helper method to get a number input from the user
|
Helper method to get a number input from the user
|
||||||
:param question: The question to display
|
:param question: The question to display
|
||||||
:param min_count: The lowest allowed value
|
:param min_value: The lowest allowed value
|
||||||
:param max_count: The highest allowed value (or None)
|
:param max_value: The highest allowed value (or None)
|
||||||
:param default: Optional default value
|
:param default: Optional default value
|
||||||
:param allow_go_back: Navigate back to a previous dialog
|
:param allow_go_back: Navigate back to a previous dialog
|
||||||
:return: Either the validated number input, or None on go_back
|
:return: Either the validated number input, or None on go_back
|
||||||
@@ -77,7 +77,7 @@ def get_number_input(
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return validate_number_input(_input, min_count, max_count)
|
return validate_number_input(_input, min_value, max_value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
Logger.print_error(INVALID_CHOICE)
|
Logger.print_error(INVALID_CHOICE)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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.octoapp.octoapp import Octoapp
|
||||||
from extensions.telegram_bot.moonraker_telegram_bot import MoonrakerTelegramBot
|
from extensions.telegram_bot.moonraker_telegram_bot import MoonrakerTelegramBot
|
||||||
|
from extensions.octoprint.octoprint import Octoprint
|
||||||
|
|
||||||
InstanceType = TypeVar(
|
InstanceType = TypeVar(
|
||||||
"InstanceType",
|
"InstanceType",
|
||||||
@@ -24,4 +25,5 @@ InstanceType = TypeVar(
|
|||||||
MoonrakerObico,
|
MoonrakerObico,
|
||||||
Octoeverywhere,
|
Octoeverywhere,
|
||||||
Octoapp,
|
Octoapp,
|
||||||
|
Octoprint,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ def create_python_venv(
|
|||||||
target: Path,
|
target: Path,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
allow_access_to_system_site_packages: bool = False,
|
allow_access_to_system_site_packages: bool = False,
|
||||||
|
use_python_binary: str | None = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Create a python 3 virtualenv at the provided target destination.
|
Create a python 3 virtualenv at the provided target destination.
|
||||||
@@ -103,36 +104,46 @@ def create_python_venv(
|
|||||||
: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 force: Force recreation of the virtualenv
|
||||||
:param allow_access_to_system_site_packages: give the virtual environment access to the system site-packages dir
|
:param allow_access_to_system_site_packages: give the virtual environment access to the system site-packages dir
|
||||||
|
:param use_python_binary: allows to override default python binary
|
||||||
: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()]
|
# If binarry override is not set, we use default defined here
|
||||||
|
python_binary = use_python_binary if use_python_binary else "/usr/bin/python3"
|
||||||
|
cmd = ["virtualenv", "-p", python_binary, target.as_posix()]
|
||||||
cmd.append(
|
cmd.append(
|
||||||
"--system-site-packages"
|
"--system-site-packages"
|
||||||
) if allow_access_to_system_site_packages else None
|
) if allow_access_to_system_site_packages else None
|
||||||
if not target.exists():
|
|
||||||
try:
|
|
||||||
run(cmd, check=True)
|
|
||||||
Logger.print_ok("Setup of virtualenv successful!")
|
|
||||||
return True
|
|
||||||
except CalledProcessError as e:
|
|
||||||
Logger.print_error(f"Error setting up virtualenv:\n{e}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
if not force and not get_confirm(
|
|
||||||
"Virtualenv already exists. Re-create?", default_choice=False
|
|
||||||
):
|
|
||||||
Logger.print_info("Skipping re-creation of virtualenv ...")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
n = 2
|
||||||
shutil.rmtree(target)
|
while(n > 0):
|
||||||
create_python_venv(target)
|
if not target.exists():
|
||||||
return True
|
try:
|
||||||
except OSError as e:
|
run(cmd, check=True)
|
||||||
log = f"Error removing existing virtualenv: {e.strerror}"
|
Logger.print_ok("Setup of virtualenv successful!")
|
||||||
Logger.print_error(log, False)
|
return True
|
||||||
return False
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error setting up virtualenv:\n{e}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if n == 1:
|
||||||
|
# This case should never happen,
|
||||||
|
# but the function should still behave correctly
|
||||||
|
Logger.print_error("Virtualenv still exists after deletion.")
|
||||||
|
return False
|
||||||
|
if not force and not get_confirm(
|
||||||
|
"Virtualenv already exists. Re-create?", default_choice=False
|
||||||
|
):
|
||||||
|
Logger.print_info("Skipping re-creation of virtualenv ...")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(target)
|
||||||
|
n -= 1
|
||||||
|
except OSError as e:
|
||||||
|
log = f"Error removing existing virtualenv: {e.strerror}"
|
||||||
|
Logger.print_error(log, False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def update_python_pip(target: Path) -> None:
|
def update_python_pip(target: Path) -> None:
|
||||||
@@ -173,9 +184,6 @@ def install_python_requirements(target: Path, requirements: Path) -> None:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# always update pip before installing requirements
|
|
||||||
update_python_pip(target)
|
|
||||||
|
|
||||||
Logger.print_status("Installing Python requirements ...")
|
Logger.print_status("Installing Python requirements ...")
|
||||||
command = [
|
command = [
|
||||||
target.joinpath("bin/pip").as_posix(),
|
target.joinpath("bin/pip").as_posix(),
|
||||||
@@ -185,7 +193,7 @@ def install_python_requirements(target: Path, requirements: Path) -> None:
|
|||||||
]
|
]
|
||||||
result = run(command, stderr=PIPE, text=True)
|
result = run(command, stderr=PIPE, text=True)
|
||||||
|
|
||||||
if result.returncode != 0 or result.stderr:
|
if result.returncode != 0:
|
||||||
Logger.print_error(f"{result.stderr}", False)
|
Logger.print_error(f"{result.stderr}", False)
|
||||||
raise VenvCreationFailedException("Installing Python requirements failed!")
|
raise VenvCreationFailedException("Installing Python requirements failed!")
|
||||||
|
|
||||||
@@ -205,9 +213,6 @@ def install_python_packages(target: Path, packages: List[str]) -> None:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# always update pip before installing requirements
|
|
||||||
update_python_pip(target)
|
|
||||||
|
|
||||||
Logger.print_status("Installing Python requirements ...")
|
Logger.print_status("Installing Python requirements ...")
|
||||||
command = [
|
command = [
|
||||||
target.joinpath("bin/pip").as_posix(),
|
target.joinpath("bin/pip").as_posix(),
|
||||||
@@ -217,7 +222,7 @@ def install_python_packages(target: Path, packages: List[str]) -> None:
|
|||||||
command.append(pkg)
|
command.append(pkg)
|
||||||
result = run(command, stderr=PIPE, text=True)
|
result = run(command, stderr=PIPE, text=True)
|
||||||
|
|
||||||
if result.returncode != 0 or result.stderr:
|
if result.returncode != 0:
|
||||||
Logger.print_error(f"{result.stderr}", False)
|
Logger.print_error(f"{result.stderr}", False)
|
||||||
raise VenvCreationFailedException("Installing Python requirements failed!")
|
raise VenvCreationFailedException("Installing Python requirements failed!")
|
||||||
|
|
||||||
@@ -359,11 +364,12 @@ def get_ipv4_addr() -> str:
|
|||||||
try:
|
try:
|
||||||
# doesn't even have to be reachable
|
# doesn't even have to be reachable
|
||||||
s.connect(("192.255.255.255", 1))
|
s.connect(("192.255.255.255", 1))
|
||||||
return str(s.getsockname()[0])
|
ipv4: str = str(s.getsockname()[0])
|
||||||
except Exception:
|
|
||||||
return "127.0.0.1"
|
|
||||||
finally:
|
|
||||||
s.close()
|
s.close()
|
||||||
|
return ipv4
|
||||||
|
except Exception:
|
||||||
|
s.close()
|
||||||
|
return "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
def download_file(url: str, target: Path, show_progress=True) -> None:
|
def download_file(url: str, target: Path, show_progress=True) -> None:
|
||||||
@@ -600,3 +606,33 @@ def get_distro_info() -> Tuple[str, str]:
|
|||||||
raise ValueError("Error reading distro version!")
|
raise ValueError("Error reading distro version!")
|
||||||
|
|
||||||
return distro_id.lower(), distro_version
|
return distro_id.lower(), distro_version
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_timezone() -> str:
|
||||||
|
timezone = "UTC"
|
||||||
|
try:
|
||||||
|
with open("/etc/timezone", "r") as f:
|
||||||
|
timezone = f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
# fallback to reading timezone from timedatectl
|
||||||
|
try:
|
||||||
|
result = run(
|
||||||
|
["timedatectl", "show", "--property=Timezone"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
timezone = result.stdout.strip().split("=")[1]
|
||||||
|
except CalledProcessError:
|
||||||
|
# fallback if timedatectl fails, try reading from readlink
|
||||||
|
try:
|
||||||
|
result = run(
|
||||||
|
["readlink", "-f", "/etc/localtime"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
timezone = result.stdout.strip().split("zoneinfo/")[1]
|
||||||
|
except (CalledProcessError, IndexError):
|
||||||
|
Logger.print_warn("Could not determine system timezone, using UTC")
|
||||||
|
return timezone
|
||||||
|
|||||||
84
mkdocs.yml
Normal file
84
mkdocs.yml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
site_name: KIAUH Documentation
|
||||||
|
site_description: Documentation for the Klipper Installation And Update Helper
|
||||||
|
repo_url: https://github.com/dw-0/kiauh
|
||||||
|
repo_name: dw-0/kiauh
|
||||||
|
edit_uri: edit/master/docs
|
||||||
|
|
||||||
|
copyright: Copyright © 2025 Dominik Willner
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
logo: assets/logo.png
|
||||||
|
favicon: assets/logo.png
|
||||||
|
icon:
|
||||||
|
repo: fontawesome/brands/github
|
||||||
|
palette:
|
||||||
|
- media: "(prefers-color-scheme: light)"
|
||||||
|
scheme: default
|
||||||
|
primary: blue-grey
|
||||||
|
accent: cyan
|
||||||
|
toggle:
|
||||||
|
icon: material/weather-night
|
||||||
|
name: Switch to dark mode
|
||||||
|
- media: "(prefers-color-scheme: dark)"
|
||||||
|
scheme: slate
|
||||||
|
primary: blue-grey
|
||||||
|
accent: cyan
|
||||||
|
toggle:
|
||||||
|
icon: material/weather-sunny
|
||||||
|
name: Switch to light mode
|
||||||
|
features:
|
||||||
|
- navigation.instant
|
||||||
|
- navigation.tracking
|
||||||
|
- navigation.sections
|
||||||
|
- navigation.expand
|
||||||
|
- navigation.indexes
|
||||||
|
- navigation.top
|
||||||
|
- toc.follow
|
||||||
|
- content.code.copy
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- search
|
||||||
|
- git-revision-date-localized:
|
||||||
|
enable_creation_date: true
|
||||||
|
- mkdocstrings:
|
||||||
|
handlers:
|
||||||
|
python:
|
||||||
|
paths: [.]
|
||||||
|
options:
|
||||||
|
docstring_style: google
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.superfences
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.snippets
|
||||||
|
- pymdownx.superfences
|
||||||
|
- tables
|
||||||
|
- attr_list
|
||||||
|
- md_in_html
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Home: index.md
|
||||||
|
- Installation:
|
||||||
|
- setup/raspberry-pi-setup.md
|
||||||
|
- setup/installation.md
|
||||||
|
- Configuration: configuration.md
|
||||||
|
- Extensions:
|
||||||
|
- extensions/index.md
|
||||||
|
- extensions/gcode-shell-command.md
|
||||||
|
- Development:
|
||||||
|
- development/contributing.md
|
||||||
|
- development/changelog.md
|
||||||
|
|
||||||
|
extra:
|
||||||
|
social:
|
||||||
|
- icon: simple/github
|
||||||
|
link: https://github.com/dw-0
|
||||||
|
- icon: simple/kofi
|
||||||
|
link: https://ko-fi.com/dw__0
|
||||||
|
- icon: simple/paypal
|
||||||
|
link: https://www.paypal.com/paypalme/dwillner0
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev=["ruff", "mypy"]
|
dev=["ruff", "pyright"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
required-version = ">=0.9.10"
|
required-version = ">=0.9.10"
|
||||||
@@ -20,14 +20,3 @@ quote-style = "double"
|
|||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
extend-select = ["I"]
|
extend-select = ["I"]
|
||||||
|
|
||||||
[tool.mypy]
|
|
||||||
python_version = "3.8"
|
|
||||||
platform = "linux"
|
|
||||||
# strict = true # TODO: enable this once everything is else is handled
|
|
||||||
check_untyped_defs = true
|
|
||||||
ignore_missing_imports = true
|
|
||||||
warn_redundant_casts = true
|
|
||||||
warn_unused_ignores = true
|
|
||||||
warn_return_any = true
|
|
||||||
warn_unreachable = true
|
|
||||||
|
|||||||
6
pyrightconfig.json
Normal file
6
pyrightconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"pythonVersion": "3.8",
|
||||||
|
"pythonPlatform": "Linux",
|
||||||
|
"typeCheckingMode": "standard",
|
||||||
|
"venvPath": "./.kiauh-env"
|
||||||
|
}
|
||||||
2
requirements-dev.txt
Normal file
2
requirements-dev.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ruff (>=0.9.10)
|
||||||
|
pyright
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
mkdocs-material
|
||||||
|
mkdocs
|
||||||
|
mkdocstrings[python]
|
||||||
|
mkdocs-git-revision-date-localized-plugin
|
||||||
@@ -280,7 +280,6 @@ function create_klipper_virtualenv() {
|
|||||||
status_msg "Installing $("python${python_version}" -V) virtual environment..."
|
status_msg "Installing $("python${python_version}" -V) virtual environment..."
|
||||||
|
|
||||||
if virtualenv -p "python${python_version}" "${KLIPPY_ENV}"; then
|
if virtualenv -p "python${python_version}" "${KLIPPY_ENV}"; then
|
||||||
(( python_version == 3 )) && "${KLIPPY_ENV}"/bin/pip install -U pip
|
|
||||||
"${KLIPPY_ENV}"/bin/pip install -r "${KLIPPER_DIR}"/scripts/klippy-requirements.txt
|
"${KLIPPY_ENV}"/bin/pip install -r "${KLIPPER_DIR}"/scripts/klippy-requirements.txt
|
||||||
else
|
else
|
||||||
log_error "failure while creating python3 klippy-env"
|
log_error "failure while creating python3 klippy-env"
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ function update_klipperscreen() {
|
|||||||
git checkout -f master && ok_msg "Checkout successfull"
|
git checkout -f master && ok_msg "Checkout successfull"
|
||||||
|
|
||||||
if [[ $(md5sum "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt" | cut -d " " -f1) != "${old_md5}" ]]; then
|
if [[ $(md5sum "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt" | cut -d " " -f1) != "${old_md5}" ]]; then
|
||||||
status_msg "New dependecies detected..."
|
status_msg "New dependencies detected..."
|
||||||
"${KLIPPERSCREEN_ENV}"/bin/pip install -r "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt"
|
"${KLIPPERSCREEN_ENV}"/bin/pip install -r "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt"
|
||||||
ok_msg "Dependencies have been installed!"
|
ok_msg "Dependencies have been installed!"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function update_mobileraker() {
|
|||||||
git checkout -f main && ok_msg "Checkout successfull"
|
git checkout -f main && ok_msg "Checkout successfull"
|
||||||
|
|
||||||
if [[ $(md5sum "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt" | cut -d " " -f1) != "${old_md5}" ]]; then
|
if [[ $(md5sum "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt" | cut -d " " -f1) != "${old_md5}" ]]; then
|
||||||
status_msg "New dependecies detected..."
|
status_msg "New dependencies detected..."
|
||||||
"${MOBILERAKER_ENV}"/bin/pip install -r "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt"
|
"${MOBILERAKER_ENV}"/bin/pip install -r "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt"
|
||||||
ok_msg "Dependencies have been installed!"
|
ok_msg "Dependencies have been installed!"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ function create_moonraker_virtualenv() {
|
|||||||
[[ -d ${MOONRAKER_ENV} ]] && rm -rf "${MOONRAKER_ENV}"
|
[[ -d ${MOONRAKER_ENV} ]] && rm -rf "${MOONRAKER_ENV}"
|
||||||
|
|
||||||
if virtualenv -p /usr/bin/python3 "${MOONRAKER_ENV}"; then
|
if virtualenv -p /usr/bin/python3 "${MOONRAKER_ENV}"; then
|
||||||
"${MOONRAKER_ENV}"/bin/pip install -U pip
|
|
||||||
"${MOONRAKER_ENV}"/bin/pip install -r "${MOONRAKER_DIR}/scripts/moonraker-requirements.txt"
|
"${MOONRAKER_ENV}"/bin/pip install -r "${MOONRAKER_DIR}/scripts/moonraker-requirements.txt"
|
||||||
else
|
else
|
||||||
log_error "failure while creating python3 moonraker-env"
|
log_error "failure while creating python3 moonraker-env"
|
||||||
|
|||||||
Reference in New Issue
Block a user