mirror of
https://github.com/dw-0/kiauh.git
synced 2025-12-23 15:53:36 +05:00
Compare commits
282 Commits
v3
...
c67ea2245d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c67ea2245d | ||
|
|
fda99bb70a | ||
|
|
2c1c94c904 | ||
|
|
b020f10967 | ||
|
|
aa1b435da5 | ||
|
|
449317b118 | ||
|
|
336414c43c | ||
|
|
cd63034b74 | ||
|
|
8de7ab7e11 | ||
|
|
c2b0ca5b19 | ||
|
|
ecb673a088 | ||
|
|
da4c5fe109 | ||
|
|
bb769fdf6d | ||
|
|
409aa3da25 | ||
|
|
0b41d63496 | ||
|
|
44301c0c87 | ||
|
|
ace47e2873 | ||
|
|
06801a47eb | ||
|
|
1484ebf445 | ||
|
|
4547ac571a | ||
|
|
b2dd5d8ed7 | ||
|
|
e50ce1fc71 | ||
|
|
417180f724 | ||
|
|
f2691f33d3 | ||
|
|
39f0bd8b0a | ||
|
|
dc87d30770 | ||
|
|
aaf5216275 | ||
|
|
ebdfadac07 | ||
|
|
cac73cc58d | ||
|
|
78dbf31576 | ||
|
|
fef8b58510 | ||
|
|
d800d356ca | ||
|
|
72e3a56e4f | ||
|
|
e64aa94df4 | ||
|
|
58719a4ca0 | ||
|
|
59a83aee12 | ||
|
|
7104eb078f | ||
|
|
341ecb325c | ||
|
|
e3a6d8a0ab | ||
|
|
0183988d5d | ||
|
|
03c3ed20f3 | ||
|
|
5c1c98b6b8 | ||
|
|
ef13c130e0 | ||
|
|
2acd74cbd9 | ||
|
|
00665109c2 | ||
|
|
a5dce136f3 | ||
|
|
4ffa057931 | ||
|
|
ed35dc9e03 | ||
|
|
7ec055f562 | ||
|
|
9eb0531cdf | ||
|
|
84cda99af8 | ||
|
|
5f823c2d3a | ||
|
|
758a783ede | ||
|
|
682baaa105 | ||
|
|
601ccb2191 | ||
|
|
c0caab13b3 | ||
|
|
7c754de08e | ||
|
|
9dc556e7e4 | ||
|
|
655b781aef | ||
|
|
14aafd558a | ||
|
|
bd1aa1ae2b | ||
|
|
8df75dc8d0 | ||
|
|
5c37b68463 | ||
|
|
1620efe56c | ||
|
|
7fd91e6cef | ||
|
|
750cb7b307 | ||
|
|
384503c4f5 | ||
|
|
b6c6edb622 | ||
|
|
2a4fcf3a3a | ||
|
|
573dc7c3c9 | ||
|
|
05b4ef2d18 | ||
|
|
863c62511c | ||
|
|
be5f345a7c | ||
|
|
948927cfd3 | ||
|
|
34ebe5d15e | ||
|
|
3bef6ecb85 | ||
|
|
5ace920d3e | ||
|
|
2f34253bad | ||
|
|
0447bc4405 | ||
|
|
7cb2231584 | ||
|
|
5a3d21c40b | ||
|
|
099d47df2f | ||
|
|
ba1cdb3739 | ||
|
|
ad56b51e70 | ||
|
|
c6999f1990 | ||
|
|
bc30cf418b | ||
|
|
ee81ee4c0c | ||
|
|
35911604af | ||
|
|
77f1089041 | ||
|
|
8e7d4db988 | ||
|
|
8f960495ba | ||
|
|
095823bf28 | ||
|
|
397038e43e | ||
|
|
061e222664 | ||
|
|
3f5ff50d69 | ||
|
|
7820155094 | ||
|
|
c28d5c28b9 | ||
|
|
cda6d31a7c | ||
|
|
9a657daffd | ||
|
|
85b4b68f16 | ||
|
|
dfbce3b489 | ||
|
|
f3b0e45e39 | ||
|
|
83e5d9c0d5 | ||
|
|
8f44187568 | ||
|
|
625a808484 | ||
|
|
ad0dbf63b8 | ||
|
|
9dedf38079 | ||
|
|
1b4c76d080 | ||
|
|
d20d82aeac | ||
|
|
16a28ffda0 | ||
|
|
a9367cc064 | ||
|
|
b165d88855 | ||
|
|
6c59d58193 | ||
|
|
b4f5c3c1ac | ||
|
|
b69ecbc9b5 | ||
|
|
fc9fa39eee | ||
|
|
142b4498a3 | ||
|
|
012b6c4bb7 | ||
|
|
8aeb01aca0 | ||
|
|
da533fdd67 | ||
|
|
8cb0754296 | ||
|
|
7a6590e86a | ||
|
|
2f0feb317e | ||
|
|
b9479db766 | ||
|
|
14132fc34b | ||
|
|
3d5e83d5ab | ||
|
|
edd5f5c6fd | ||
|
|
8ff0b9d81d | ||
|
|
22e8e314db | ||
|
|
12bd8eb799 | ||
|
|
4915896099 | ||
|
|
cd38970bbd | ||
|
|
b8640f45a6 | ||
|
|
5fb4444f03 | ||
|
|
926ba1acb4 | ||
|
|
c2e7ee98df | ||
|
|
3865266da1 | ||
|
|
b83f642a13 | ||
|
|
30b4414469 | ||
|
|
1178d3c730 | ||
|
|
59d8867c8c | ||
|
|
80a953a587 | ||
|
|
a80f0bb0e8 | ||
|
|
78cefddb2e | ||
|
|
b20613819e | ||
|
|
5ebe941125 | ||
|
|
f5eb9486cc | ||
|
|
1836beab42 | ||
|
|
545397f598 | ||
|
|
f709cf84e7 | ||
|
|
f62c10dc8b | ||
|
|
7a9e752f9c | ||
|
|
30bc56b198 | ||
|
|
b2567995de | ||
|
|
e121ba8a62 | ||
|
|
9a1a66aa64 | ||
|
|
420b193f4b | ||
|
|
de20f0c412 | ||
|
|
57f34b07c6 | ||
|
|
e35e44a76a | ||
|
|
bfb10c742b | ||
|
|
458c89a78a | ||
|
|
6128e35d45 | ||
|
|
279d000bb0 | ||
|
|
a4a3d5eecb | ||
|
|
1392ca9f82 | ||
|
|
47121f6875 | ||
|
|
d0d2404132 | ||
|
|
6ed5395f17 | ||
|
|
be805c169b | ||
|
|
eaf12db27e | ||
|
|
fe8767113b | ||
|
|
2148d95cf4 | ||
|
|
682be48e8d | ||
|
|
68369753fd | ||
|
|
44ed3b6ddf | ||
|
|
e12e578098 | ||
|
|
515a42f098 | ||
|
|
f9ecad0eca | ||
|
|
fb09acf660 | ||
|
|
093da73dd1 | ||
|
|
c9e8c4807e | ||
|
|
6fcd7a3f08 | ||
|
|
09e874214b | ||
|
|
623bd7553b | ||
|
|
1e0c74b549 | ||
|
|
358c666da9 | ||
|
|
84a530be7d | ||
|
|
bfff3019cb | ||
|
|
2a100c2934 | ||
|
|
25dfbb83df | ||
|
|
ce0daa52ae | ||
|
|
899b204dc7 | ||
|
|
5cf4b018fc | ||
|
|
ae9d1b98da | ||
|
|
16d3388ff2 | ||
|
|
b88d0085ba | ||
|
|
0b6613e464 | ||
|
|
d99cda544a | ||
|
|
a50dce20de | ||
|
|
f45da66e9e | ||
|
|
2822499344 | ||
|
|
c777ba3e6b | ||
|
|
9f410450d7 | ||
|
|
0497d49066 | ||
|
|
229da227b0 | ||
|
|
65854c8da6 | ||
|
|
5985646633 | ||
|
|
979c39dc02 | ||
|
|
197058bd00 | ||
|
|
d3b5122ebb | ||
|
|
8ce4daf403 | ||
|
|
b0a65fe14e | ||
|
|
98866caefa | ||
|
|
345b7b66a3 | ||
|
|
8eb2924832 | ||
|
|
5d7debd65e | ||
|
|
7df3dd489f | ||
|
|
0cd058320f | ||
|
|
bcbb185bd7 | ||
|
|
477f3ca72c | ||
|
|
c19acb1694 | ||
|
|
8228943850 | ||
|
|
5b890fb0fb | ||
|
|
7989cec8d4 | ||
|
|
858301aa9a | ||
|
|
ae9e79c579 | ||
|
|
1215446a6c | ||
|
|
8526acf8b6 | ||
|
|
cc27aaec7c | ||
|
|
1e9493461c | ||
|
|
31616ebad5 | ||
|
|
faf56ed1b1 | ||
|
|
d6837af2a2 | ||
|
|
afe6f7499a | ||
|
|
e3ed223b5c | ||
|
|
fd27db28d4 | ||
|
|
68a02ad3f5 | ||
|
|
99b7672dc9 | ||
|
|
bb3ec79756 | ||
|
|
ce595abd60 | ||
|
|
c79dc280e3 | ||
|
|
7aa186e8b9 | ||
|
|
8493269c6f | ||
|
|
150ef0142f | ||
|
|
f70faa52cc | ||
|
|
e796f74640 | ||
|
|
2c9f5bed60 | ||
|
|
e9c23ca93e | ||
|
|
67afa26ed7 | ||
|
|
54be7e4e21 | ||
|
|
811c071b74 | ||
|
|
6116fc92cf | ||
|
|
5524a40f04 | ||
|
|
cb3661b8b5 | ||
|
|
2cec90b29c | ||
|
|
d2c009df9a | ||
|
|
046178f801 | ||
|
|
442980dbd0 | ||
|
|
798e56f4dc | ||
|
|
9d90daec7f | ||
|
|
f25726cfed | ||
|
|
f46b099b74 | ||
|
|
03be46f012 | ||
|
|
c11e628c55 | ||
|
|
4c8d43e365 | ||
|
|
9d7144b493 | ||
|
|
6df388f42b | ||
|
|
1d7fb010af | ||
|
|
d4207d710c | ||
|
|
6cb8d70b63 | ||
|
|
ae011963da | ||
|
|
491d6f40bb | ||
|
|
8bbe2f79ea | ||
|
|
0bdf61a714 | ||
|
|
b07a83c8ad | ||
|
|
39e22acbed | ||
|
|
8ba46fa4ac | ||
|
|
d6b95c9d10 | ||
|
|
f3a769e03e | ||
|
|
646e5acd3a | ||
|
|
fcf059df73 |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -3,10 +3,10 @@
|
|||||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
patreon: # Replace with a single Patreon username
|
patreon: # Replace with a single Patreon username
|
||||||
open_collective: # Replace with a single Open Collective username
|
open_collective: # Replace with a single Open Collective username
|
||||||
ko_fi: th33xitus
|
ko_fi: dw__0
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
liberapay: # Replace with a single Liberapay username
|
liberapay: # Replace with a single Liberapay username
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
otechie: # Replace with a single Otechie username
|
otechie: # Replace with a single Otechie username
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: https://paypal.me/dwillner0
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,2 +1,8 @@
|
|||||||
.idea
|
.idea
|
||||||
.shellcheckrc
|
.vscode
|
||||||
|
.idea
|
||||||
|
.pytest_cache
|
||||||
|
.kiauh-env
|
||||||
|
*.code-workspace
|
||||||
|
*.iml
|
||||||
|
kiauh.cfg
|
||||||
|
|||||||
15
.shellcheckrc
Normal file
15
.shellcheckrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
source=scripts
|
||||||
|
|
||||||
|
enable=avoid-nullary-conditions
|
||||||
|
enable=deprecate-which
|
||||||
|
enable=quote-safe-variables
|
||||||
|
enable=require-variable-braces
|
||||||
|
enable=require-double-brackets
|
||||||
|
|
||||||
|
# SC2162: `read` without `-r` will mangle backslashes.
|
||||||
|
# https://github.com/koalaman/shellcheck/wiki/SC2162
|
||||||
|
disable=SC2162
|
||||||
|
|
||||||
|
# SC2164: Use `cd ... || exit` in case `cd` fails
|
||||||
|
# https://github.com/koalaman/shellcheck/wiki/SC2164
|
||||||
|
disable=SC2164
|
||||||
220
README.md
220
README.md
@@ -1,72 +1,202 @@
|
|||||||

|
<p align="center">
|
||||||
# Klipper Installation And Update Helper
|
<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>
|
||||||
|
|
||||||
### **📋 Please see the [Changelog](docs/changelog.md) for possible important information !**
|
<p align="center">
|
||||||
|
A handy installation script that makes installing Klipper (and more) a breeze!
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 align="center">
|
||||||
|
📄️ Instructions 📄
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
### 📋 Prerequisites
|
||||||
|
KIAUH is a script that assists you in installing Klipper on a Linux operating system that has
|
||||||
|
already been flashed to your Raspberry Pi's (or other SBC's) SD card. As a result, you must ensure
|
||||||
|
that you have a functional Linux system on hand. `Raspberry Pi OS Lite (either 32bit or 64bit)` is a recommended Linux image
|
||||||
|
if you are using a Raspberry Pi. The [official Raspberry Pi Imager](https://www.raspberrypi.com/software/)
|
||||||
|
is the simplest way to flash an image like this to an SD card.
|
||||||
|
|
||||||
|
* Once you have downloaded, installed and launched the Raspberry Pi Imager,
|
||||||
|
select `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>
|
||||||
|
|
||||||
|
* Then select `Raspberry Pi OS Lite (32bit)` (or 64bit if you want to use that instead):
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager2.png" alt="KIAUH logo" height="350">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
* Back in the Raspberry Pi Imager's main menu, select the corresponding SD card to which
|
||||||
|
you want to flash the image.
|
||||||
|
|
||||||
|
* Make sure to go into the Advanced Option (the cog icon in the lower left corner of the main menu)
|
||||||
|
and enable SSH and configure Wi-Fi.
|
||||||
|
|
||||||
|
* If you need more help for using the Raspberry Pi Imager, please visit the [official documentation](https://www.raspberrypi.com/documentation/computers/getting-started.html).
|
||||||
|
|
||||||
|
These steps **only** apply if you are actually using a Raspberry Pi. In case you want
|
||||||
|
to use a different SBC (like an Orange Pi or any other Pi derivates), please look up on how to get an appropriate Linux image flashed
|
||||||
|
to the SD card before proceeding further (usually done with Balena Etcher in those cases). Also make sure that KIAUH will be able to run
|
||||||
|
and operate on the Linux Distribution you are going to flash. You likely will have the most success with
|
||||||
|
distributions based on Debian 11 Bullseye. Read the notes further down below in this document.
|
||||||
|
|
||||||
|
### 💾 Download and use KIAUH
|
||||||
**📢 Disclaimer: Usage of this script happens at your own risk!**
|
**📢 Disclaimer: Usage of this script happens at your own risk!**
|
||||||
|
|
||||||
|
* **Step 1:** \
|
||||||
|
To download this script, it is necessary to have git installed. If you don't have git already installed, or if you are unsure, run the following command:
|
||||||
|
```shell
|
||||||
|
sudo apt-get update && sudo apt-get install git -y
|
||||||
|
```
|
||||||
|
|
||||||
## **🛠️ Instructions:**
|
* **Step 2:** \
|
||||||
|
Once git is installed, use the following command to download KIAUH into your home-directory:
|
||||||
For downloading this script it is necessary to have git installed.\
|
|
||||||
If you haven't, please run `sudo apt-get install git -y` to install git first.\
|
|
||||||
After git is installed, use the following commands in the given order to download and execute the script:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd ~
|
cd ~ && git clone https://github.com/dw-0/kiauh.git
|
||||||
|
```
|
||||||
|
|
||||||
git clone https://github.com/th33xitus/kiauh.git
|
* **Step 3:** \
|
||||||
|
Finally, start KIAUH by running the next command:
|
||||||
|
|
||||||
|
```shell
|
||||||
./kiauh/kiauh.sh
|
./kiauh/kiauh.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* **Step 4:** \
|
||||||
|
You should now find yourself in the main menu of KIAUH. You will see several actions to choose from depending
|
||||||
|
on what you want to do. To choose an action, simply type the corresponding number into the "Perform action"
|
||||||
|
prompt and confirm by hitting ENTER.
|
||||||
|
|
||||||
## **🧰 Functions and Features:**
|
<hr>
|
||||||
|
|
||||||
### **Core Functions:**
|
<h2 align="center">❗ Notes ❗</h2>
|
||||||
|
|
||||||
- **Installing** Klipper to your Raspberry Pi or other Debian based Linux Distribution.
|
### **📋 Please see the [Changelog](docs/changelog.md) for possible important changes!**
|
||||||
- **Installing** of the Moonraker API (needed for Mainsail, Fluidd and KlipperScreen)
|
|
||||||
- **Installing** several web interfaces such as Mainsail, Fluidd, Duet Web Control or OctoPrint including their dependencies.
|
|
||||||
- **Installing** of KlipperScreen (OctoScreen but for Klipper!)
|
|
||||||
- **Updating** of all the listed installations above excluding OctoPrint. For updating OctoPrint, please use the OctoPrint interface!
|
|
||||||
- **Removing** of all the listed installations above.
|
|
||||||
- **Backup** of all the listed installations above.
|
|
||||||
|
|
||||||
### **Also possible:**
|
- Mainly tested on Raspberry Pi OS Lite (Debian 10 Buster / Debian 11 Bullseye)
|
||||||
|
- Other Debian based distributions (like Ubuntu 20 to 22) likely work too
|
||||||
|
- Reported to work on Armbian as well but not tested in detail
|
||||||
|
- During the use of this script you will be asked for your sudo password. There are several functions involved which need sudo privileges.
|
||||||
|
|
||||||
- Build the Klipper Firmware
|
<hr>
|
||||||
- Flash the MCU
|
|
||||||
- Read ID of the currently connected MCU
|
|
||||||
- and more ...
|
|
||||||
|
|
||||||
### **For a list of additional features please see: [Feature List](docs/features.md)**
|
<h2 align="center">🌐 Sources & Further Information</h2>
|
||||||
|
|
||||||
## **❗ Notes:**
|
<table>
|
||||||
|
<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>by <a href="https://github.com/KevinOConnor">KevinOConnor</a></th>
|
||||||
|
<th>by <a href="https://github.com/Arksine">Arksine</a></th>
|
||||||
|
<th>by <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>by <a href="https://github.com/fluidd-core">fluidd-core</a></th>
|
||||||
|
<th>by <a href="https://github.com/jordanruthe">jordanruthe</a></th>
|
||||||
|
<th>by <a href="https://github.com/OctoPrint">OctoPrint</a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
- Tested **only** on Raspberry Pi OS Lite (Debian 10 Buster)
|
<tr>
|
||||||
- Other Debian based distributions can work
|
<th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th>
|
||||||
- Reported to work on Armbian too
|
<th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></th>
|
||||||
- During the use of this script you might be asked for your sudo password. There are several functions involved which need sudo privileges.
|
<th><h3><a href="https://github.com/TheSpaghettiDetective/moonraker-obico">Obico for Klipper</a></h3></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
## **🌐 Sources & Further Information**
|
<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>
|
||||||
|
|
||||||
For more information or instructions to the various components KIAUH can install, please check out the corresponding repositories listed below:
|
<tr>
|
||||||
|
<th>by <a href="https://github.com/nlef">nlef</a></th>
|
||||||
|
<th>by <a href="https://github.com/Kragrathea">Kragrathea</a></th>
|
||||||
|
<th>by <a href="https://github.com/TheSpaghettiDetective">Obico</a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
* ⛵[Klipper](https://github.com/Klipper3d/klipper) by [KevinOConnor](https://github.com/KevinOConnor)
|
<tr>
|
||||||
* 🌙[Moonraker](https://github.com/Arksine/moonraker) by [Arksine](https://github.com/Arksine)
|
<th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th>
|
||||||
* 💨[Mainsail](https://github.com/mainsail-crew/mainsail) by [mainsail-crew](https://github.com/mainsail-crew)
|
<th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th>
|
||||||
* 🌊[Fluidd](https://github.com/fluidd-core/fluidd) by [fluidd-core](https://github.com/fluidd-core)
|
<th><h3><a href="https://github.com/crysxd/OctoPrint-OctoApp">OctoApp For Klipper</a></h3></th>
|
||||||
* 🕸️[Duet Web Control](https://github.com/Duet3D/DuetWebControl) by [Duet3D](https://github.com/Duet3D)
|
<th><h3></h3></th>
|
||||||
* 🕸️[DWC2-for-Klipper-Socket](https://github.com/Stephan3/dwc2-for-klipper-socket) by [Stephan3](https://github.com/Stephan3)
|
</tr>
|
||||||
* 🖥️[KlipperScreen](https://github.com/jordanruthe/KlipperScreen) by [jordanruthe](https://github.com/jordanruthe)
|
|
||||||
* 🐙[OctoPrint](https://github.com/OctoPrint/OctoPrint) by [OctoPrint](https://github.com/OctoPrint)
|
|
||||||
* 🔬[PrettyGCode](https://github.com/Kragrathea/pgcode) by [Kragrathea](https://github.com/Kragrathea)
|
|
||||||
* 🤖[Moonraker-Telegram-Bot](https://github.com/nlef/moonraker-telegram-bot) by [nlef](https://github.com/nlef)
|
|
||||||
|
|
||||||
## **Credits**
|
<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="OctoEverywhere 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>by <a href="https://github.com/Clon1998">Patrick Schmidt</a></th>
|
||||||
|
<th>by <a href="https://github.com/QuinnDamerell">Quinn Damerell</a></th>
|
||||||
|
<th>by <a href="https://github.com/crysxd">Christian Würthner</a></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 align="center">🎖️ Contributors 🎖️</h2>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 align="center">✨ Credits ✨</h2>
|
||||||
|
|
||||||
* A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo!
|
* A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo!
|
||||||
* Also a big thank you to everyone who supported my work with a [Ko-fi](https://ko-fi.com/th33xitus) !
|
* Also, a big thank you to everyone who supported my work with a [Ko-fi](https://ko-fi.com/dw__0) !
|
||||||
* Last but not least: Thank you to all contributors and members of the Klipper Community who like and share this project!
|
* Last but not least: Thank you to all contributors and members of the Klipper Community who like and share this project!
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4 align="center">A special thank you to JetBrains for sponsoring this project with their incredible software!</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>
|
||||||
|
|||||||
@@ -2,6 +2,105 @@
|
|||||||
|
|
||||||
This document covers possible important changes to KIAUH.
|
This document covers possible important changes to KIAUH.
|
||||||
|
|
||||||
|
### 2023-06-17
|
||||||
|
KIAUH has now added support for installing Mobileraker's companion!
|
||||||
|
Mobileraker is a free and Open Source Android and iOS App for Klipper, utilizing the Moonraker API, allowing you
|
||||||
|
to control your printer. Thank you to [Clon1998](https://github.com/Clon1998) for adding this feature!
|
||||||
|
|
||||||
|
### 2023-02-03
|
||||||
|
The installer for MJPG-Streamer got replaced by crowsnest. It is an improved webcam service, utilizing ustreamer.
|
||||||
|
Please have a look here for additional info about crowsnest and how to configure it: https://github.com/mainsail-crew/crowsnest \
|
||||||
|
It's unsure if the previous MJPG-Streamer installer will be updated and make its way back into KIAUH.
|
||||||
|
A big thanks to [KwadFan](https://github.com/KwadFan) for writing the crowsnest implementation.
|
||||||
|
|
||||||
|
### 2022-10-31
|
||||||
|
Some functions got updated, though not all of them.
|
||||||
|
|
||||||
|
The following functions are still currently unavailable:
|
||||||
|
- Installation of: MJPG-Streamer
|
||||||
|
- All backup functions and the Log-Upload
|
||||||
|
|
||||||
|
### 2022-10-20
|
||||||
|
KIAUH has now reached major version 5 !
|
||||||
|
|
||||||
|
Recently Moonraker introduced some changes which makes it necessary to change the folder structure of printer setups.
|
||||||
|
If you are interested in the details, check out this PR: https://github.com/Arksine/moonraker/pull/491 \
|
||||||
|
Although Moonraker has some mechanics available to migrate existing setups to the new file structure with the use of symlinks, fresh and clean installs
|
||||||
|
should be considered.
|
||||||
|
|
||||||
|
The version jump of KIAUH to v5 is a breaking change due to those major changes! That means v4 and v5 are not compatible with each other!
|
||||||
|
This is also the reason why you will currently be greeted by a yellow notification in the main menu of KIAUH leading to this changelog.
|
||||||
|
I decided to disable a few functions of the script and focus on releasing the required changes to the core components of this script.
|
||||||
|
I will work on updating the other parts of the script piece by piece during the next days/weeks.
|
||||||
|
So I am already sorry in advance if one of your desired components you wanted to install or use temporarily cannot be installed or used right now.
|
||||||
|
|
||||||
|
The following functions are currently unavailable:
|
||||||
|
- Installation of: KlipperScreen, Obico, Octoprint, MJPG-Streamer, Telegram Bot and PrettyGCode
|
||||||
|
- All backup functions and the Log-Upload
|
||||||
|
|
||||||
|
**So what is working?**\
|
||||||
|
Installation of Klipper, Moonraker, Mainsail and Fluidd. Both, single and multi-instance setups work!\
|
||||||
|
As already said, the rest will follow in the near future. Updating and removal of already installed components should continue to work.
|
||||||
|
|
||||||
|
**What was removed?**\
|
||||||
|
The option to change Klippers configuration directory got removed. From now on it will not be possible anymore to change
|
||||||
|
the configuration directory from within KIAUH and the new filestructure is enforced.
|
||||||
|
|
||||||
|
**What if I don't have an existing Klipper/Moonraker install right now?**\
|
||||||
|
Nothing important to think about, install Klipper and Moonraker. KIAUH will install both of them with the new filestructure.
|
||||||
|
|
||||||
|
**What if I have an existing Klipper/Moonraker install?**\
|
||||||
|
First of all: Backups! Please copy all of your config files and the Moonraker database (it is a hidden folder, usually `~/.moonraker_database`) to a safe location.
|
||||||
|
After that, uninstall Klipper and Moonraker with KIAUH. You can then proceed and re-install both of them with KIAUH again. It is important that you are on KIAUH v5 for that!
|
||||||
|
Once everything is installed again, you need to manually copy your configuration files from the old `~/klipper_config` folder to the new `~/printer_data/config` folder.
|
||||||
|
Previous, by Moonraker created symlinks to folder of the old filestructure will not work anymore, you need to move the files to their new location now!
|
||||||
|
Do the same with the two files inside of `~/.moonraker_database`. Move/copy them into `~/printer_data/database`. If `~/printer_data/database` is already populated with a `data.mdb` and `lock.mdb`
|
||||||
|
delete them or simply overwrite them. Nothing should be lost as those should be empty database files. Anyway, you made backups, right?
|
||||||
|
You can now proceed and restart Moonraker. Either from within Mainsail or Fluidd, or use SSH and execute `sudo systemctl restart moonraker`.
|
||||||
|
If everything went smooth, you should be good to go again. If you see some Moonraker warnings about deprecated options in the `moonraker.conf`, go ahead and resolve them.
|
||||||
|
I will not cover them in detail here. A good source is the Moonraker documentation: https://moonraker.readthedocs.io/en/latest/configuration/
|
||||||
|
|
||||||
|
**What if I have an existing Klipper/Moonraker multi-instance install?**\
|
||||||
|
Pretty much the same steps that are required for single instance installs apply to multi-instance setups. So please go ahead and read the previous paragraph if you didn't already.
|
||||||
|
Make backups of everything first. Then remove and install the desired amount of Klipper and Moonraker instances again.
|
||||||
|
Now you need to move all config and database files to their new locations.\
|
||||||
|
Example with an instance called `printer_1`:\
|
||||||
|
The config files go from `~/klipper_config/printer_1` to `~/printer_1_data/config`.
|
||||||
|
The database files go from `~/.moonraker_database_1` to `~/printer_1_data/database`.
|
||||||
|
Now restart all Moonraker services. You can restart all of them at once if you launch KIAUH, and in the main menu type `restart moonraker` and hit Enter.
|
||||||
|
|
||||||
|
I hope I have covered the most important things. In case you need further support, the official Klipper Discord is a good place to ask for help.
|
||||||
|
|
||||||
|
### 2022-08-15
|
||||||
|
Support for "Obico for Klipper" was added! Huge thanks to [kennethjiang](https://github.com/kennethjiang) for helping me with the implementation!
|
||||||
|
|
||||||
|
### 2022-05-29
|
||||||
|
KIAUH has now reached major version 4 !
|
||||||
|
* feat: Klipper can be installed under Python3 (still considered as experimental)
|
||||||
|
* feat: Klipper can be installed from custom repositories / inofficial forks
|
||||||
|
* feat: Custom instance name for multi instance installations of Klipper
|
||||||
|
* Any other multi instance will share the same name given to the corresponding Klipper instance
|
||||||
|
* E.g. klipper-voron2 -> moonraker-voron2 -> moonraker-telegram-bot-voron2
|
||||||
|
* feat: Option to allow installation of / updating to unstable Mainsail and Fluidd versions
|
||||||
|
* by default only stable versions get installed/updated
|
||||||
|
* feat: Multi-Instance OctoPrint installations now each have their own virtual python environment
|
||||||
|
* allows independent installation of plugins for each instance
|
||||||
|
* feat: Implementing the use of shellcheck during development
|
||||||
|
* feat: Implementing a simple logging mechanic
|
||||||
|
* feat: Log-upload function now also allows uploading other logfiles (kiauh.log, webcamd.log etc.)
|
||||||
|
* feat: added several new help dialogs which try to explain various functions
|
||||||
|
* fix: During Klipper installation, checks for group membership of `tty` and `dialout` are made
|
||||||
|
* refactor: rework of the settings menu for better control the new KIAUH features
|
||||||
|
* refactor: Support for DWC and DWC-for-Klipper has been removed
|
||||||
|
* refactor: The backup before update settings were moved to the KIAUH settings menu
|
||||||
|
* refactor: Switch branch function has been removed (was replaced by the custom Klipper repo feature)
|
||||||
|
* refactor: The update manager sections for Mainsail, Fluidd and KlipperScreen were removed from the moonraker.conf template
|
||||||
|
* They will now be individually added during installation of the corresponding interface
|
||||||
|
* refactor: The rollback function was reworked and now also allows rollbacks of Moonraker
|
||||||
|
* It now takes numerical inputs and reverts the corresponding repository by the given amount instead
|
||||||
|
* KIAUH does not save previous states to its config anymore like it did with the previous approach
|
||||||
|
|
||||||
|
|
||||||
### 2022-01-29
|
### 2022-01-29
|
||||||
* Starting from the 28th of January, Moonraker can make use of PackageKit and PolicyKit.\
|
* Starting from the 28th of January, Moonraker can make use of PackageKit and PolicyKit.\
|
||||||
More details on that can be found [here](
|
More details on that can be found [here](
|
||||||
@@ -114,9 +213,9 @@ Each service gets its corresponding instance added to the service filename.
|
|||||||
--> moonraker-2.service
|
--> moonraker-2.service
|
||||||
--> moonraker-n.service
|
--> moonraker-n.service
|
||||||
```
|
```
|
||||||
* The same service file rules from above apply to DWC and OctoPrint even though only Klipper and Moonraker are shown in this example.
|
* The same service file rules from above apply to OctoPrint even though only Klipper and Moonraker are shown in this example.
|
||||||
|
|
||||||
* You can start, stop and restart all Klipper, Moonraker, DWC and OctoPrint instances from the KIAUH main menu. For doing this, just type "stop klipper", "start moonraker", "restart octoprint" and so on.
|
* You can start, stop and restart all Klipper, Moonraker and OctoPrint instances from the KIAUH main menu. For doing this, just type "stop klipper", "start moonraker", "restart octoprint" and so on.
|
||||||
|
|
||||||
* KIAUH v3.0 relocated its ini-file. It is now a hidden file in the users home-directory calles `.kiauh.ini`. This has the benefit of keeping all values in that file between possible re-installations of KIAUH. Otherwise that file would be lost.
|
* KIAUH v3.0 relocated its ini-file. It is now a hidden file in the users home-directory calles `.kiauh.ini`. This has the benefit of keeping all values in that file between possible re-installations of KIAUH. Otherwise that file would be lost.
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# Feature List:
|
|
||||||
|
|
||||||
- **Automatic dependency check:**\
|
|
||||||
If packages are missing but needed for the asked task, the script will automatically install them
|
|
||||||
- **Switch between different Klipper Forks:**\
|
|
||||||
[origin/master](https://github.com/KevinOConnor/klipper/tree/master) or [scurve-shaping](https://github.com/dmbutyugin/klipper/tree/scurve-shaping) or [scurve-smoothing](https://github.com/dmbutyugin/klipper/tree/scurve-smoothing)\
|
|
||||||
The update function of the script will always update the currently selected/active fork!
|
|
||||||
- **Toggle auto-create backups before updating:**\
|
|
||||||
When enabled, a backup of the installation you want to update is made prior updating
|
|
||||||
- **Rollback:**\
|
|
||||||
When updating Klipper, KIAUH saves the current commit hash to a local ini-file. In case of an unsuccesfull update you can use this function to quickly revert back to the commit with the hash you updated from.
|
|
||||||
- **Preconfigure OctoPrint:**\
|
|
||||||
When installing OctoPrint, a config is created which preconfigures your installation to be used with Klipper.\
|
|
||||||
That means:
|
|
||||||
- adding the restart/shutdown commands for OctoPrint
|
|
||||||
- adding the serial port `/tmp/printer`
|
|
||||||
- set the behavior to "Cancel any ongoing prints but stay connected to the printer"
|
|
||||||
- **Enable/Disable OctoPrint Service:**\
|
|
||||||
Usefull when using DWC2/Mainsail/Fluidd and OctoPrint at the same time to prevent them interfering with each other
|
|
||||||
|
|
||||||
- **Installing a G-Code Shell Command extension:**\
|
|
||||||
For further information about that extension please see the [G-Code Shell Command Extension Doc](gcode_shell_command.md)
|
|
||||||
|
|
||||||
- **Uploading logfiles:**\
|
|
||||||
You can directly upload logfiles like klippy.log, moonraker.log and dwc2.log from the KIAUH main menu for providing them for troubleshooting purposes.
|
|
||||||
|
|
||||||
|
|
||||||
to be continued...
|
|
||||||
@@ -41,7 +41,7 @@ Execute with:
|
|||||||
`RUN_SHELL_COMMAND CMD=hello_world`
|
`RUN_SHELL_COMMAND CMD=hello_world`
|
||||||
|
|
||||||
### Passing parameters:
|
### Passing parameters:
|
||||||
As of commit [f231fa9](https://github.com/th33xitus/kiauh/commit/f231fa9c69191f23277b4e3319f6b675bfa0ee42) it is also possible to pass optional parameters to a `gcode_shell_command`.
|
As of commit [f231fa9](https://github.com/dw-0/kiauh/commit/f231fa9c69191f23277b4e3319f6b675bfa0ee42) it is also possible to pass optional parameters to a `gcode_shell_command`.
|
||||||
The following short example shows storing the extruder temperature into a variable, passing that value with a parameter to a `gcode_shell_command`, which then,
|
The following short example shows storing the extruder temperature into a variable, passing that value with a parameter to a `gcode_shell_command`, which then,
|
||||||
once the gcode_macro runs and the gcode_shell_command gets called, executes the `script.sh`. The script then echoes a message to the console (if `verbose: True`)
|
once the gcode_macro runs and the gcode_shell_command gets called, executes the `script.sh`. The script then echoes a message to the console (if `verbose: True`)
|
||||||
and writes the value of the parameter into a textfile called `test.txt` located in the home directory.
|
and writes the value of the parameter into a textfile called `test.txt` located in the home directory.
|
||||||
|
|||||||
20
kiauh.cfg.example
Normal file
20
kiauh.cfg.example
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[kiauh]
|
||||||
|
backup_before_update: False
|
||||||
|
|
||||||
|
[klipper]
|
||||||
|
repository_url: https://github.com/Klipper3d/klipper
|
||||||
|
branch: master
|
||||||
|
method: https
|
||||||
|
|
||||||
|
[moonraker]
|
||||||
|
repository_url: https://github.com/Arksine/moonraker
|
||||||
|
branch: master
|
||||||
|
method: https
|
||||||
|
|
||||||
|
[mainsail]
|
||||||
|
port: 80
|
||||||
|
unstable_releases: False
|
||||||
|
|
||||||
|
[fluidd]
|
||||||
|
port: 80
|
||||||
|
unstable_releases: False
|
||||||
15
kiauh.py
Normal file
15
kiauh.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from kiauh.main import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
199
kiauh.sh
199
kiauh.sh
@@ -1,105 +1,108 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
clear
|
|
||||||
|
#=======================================================================#
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
#=======================================================================#
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
clear
|
||||||
|
|
||||||
### set color variables
|
function main() {
|
||||||
green=$(echo -en "\e[92m")
|
local python_command
|
||||||
yellow=$(echo -en "\e[93m")
|
local entrypoint
|
||||||
red=$(echo -en "\e[91m")
|
|
||||||
cyan=$(echo -en "\e[96m")
|
|
||||||
default=$(echo -en "\e[39m")
|
|
||||||
|
|
||||||
### sourcing all additional scripts
|
if command -v python3 &>/dev/null; then
|
||||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
python_command="python3"
|
||||||
for script in "${SRCDIR}/kiauh/scripts/"*.sh; do . $script; done
|
elif command -v python &>/dev/null; then
|
||||||
for script in "${SRCDIR}/kiauh/scripts/ui/"*.sh; do . $script; done
|
python_command="python"
|
||||||
|
else
|
||||||
### set important directories
|
echo "Python is not installed. Please install Python and try again."
|
||||||
#klipper
|
exit 1
|
||||||
KLIPPER_DIR=${HOME}/klipper
|
|
||||||
KLIPPY_ENV=${HOME}/klippy-env
|
|
||||||
#nginx
|
|
||||||
NGINX_SA=/etc/nginx/sites-available
|
|
||||||
NGINX_SE=/etc/nginx/sites-enabled
|
|
||||||
NGINX_CONFD=/etc/nginx/conf.d
|
|
||||||
#moonraker
|
|
||||||
MOONRAKER_DIR=${HOME}/moonraker
|
|
||||||
MOONRAKER_ENV=${HOME}/moonraker-env
|
|
||||||
#mainsail
|
|
||||||
MAINSAIL_DIR=${HOME}/mainsail
|
|
||||||
#fluidd
|
|
||||||
FLUIDD_DIR=${HOME}/fluidd
|
|
||||||
#dwc2
|
|
||||||
DWC2FK_DIR=${HOME}/dwc2-for-klipper-socket
|
|
||||||
DWC_ENV_DIR=${HOME}/dwc-env
|
|
||||||
DWC2_DIR=${HOME}/duetwebcontrol
|
|
||||||
#octoprint
|
|
||||||
OCTOPRINT_DIR=${HOME}/OctoPrint
|
|
||||||
OCTOPRINT_CFG_DIR=${HOME}/.octoprint
|
|
||||||
#KlipperScreen
|
|
||||||
KLIPPERSCREEN_DIR=${HOME}/KlipperScreen
|
|
||||||
KLIPPERSCREEN_ENV_DIR=${HOME}/.KlipperScreen-env
|
|
||||||
#MoonrakerTelegramBot
|
|
||||||
MOONRAKER_TELEGRAM_BOT_DIR=${HOME}/moonraker-telegram-bot
|
|
||||||
MOONRAKER_TELEGRAM_BOT_ENV_DIR=${HOME}/moonraker-telegram-bot-env
|
|
||||||
#misc
|
|
||||||
INI_FILE=${HOME}/.kiauh.ini
|
|
||||||
BACKUP_DIR=${HOME}/kiauh-backups
|
|
||||||
|
|
||||||
### set github repos
|
|
||||||
KLIPPER_REPO=https://github.com/Klipper3d/klipper.git
|
|
||||||
ARKSINE_REPO=https://github.com/Arksine/klipper.git
|
|
||||||
DMBUTYUGIN_REPO=https://github.com/dmbutyugin/klipper.git
|
|
||||||
DWC2FK_REPO=https://github.com/Stephan3/dwc2-for-klipper-socket.git
|
|
||||||
KLIPPERSCREEN_REPO=https://github.com/jordanruthe/KlipperScreen.git
|
|
||||||
NLEF_REPO=https://github.com/nlef/moonraker-telegram-bot.git
|
|
||||||
#branches
|
|
||||||
BRANCH_SCURVE_SMOOTHING=dmbutyugin/scurve-smoothing
|
|
||||||
BRANCH_SCURVE_SHAPING=dmbutyugin/scurve-shaping
|
|
||||||
|
|
||||||
### set some messages
|
|
||||||
warn_msg(){
|
|
||||||
echo -e "${red}<!!!!> $1${default}"
|
|
||||||
}
|
|
||||||
status_msg(){
|
|
||||||
echo; echo -e "${yellow}###### $1${default}"
|
|
||||||
}
|
|
||||||
ok_msg(){
|
|
||||||
echo -e "${green}>>>>>> $1${default}"
|
|
||||||
}
|
|
||||||
title_msg(){
|
|
||||||
echo -e "${cyan}$1${default}"
|
|
||||||
}
|
|
||||||
get_date(){
|
|
||||||
current_date=$(date +"%y%m%d-%H%M")
|
|
||||||
}
|
|
||||||
print_unkown_cmd(){
|
|
||||||
ERROR_MSG="Invalid command!"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_msg(){
|
|
||||||
if [[ "$ERROR_MSG" != "" ]]; then
|
|
||||||
echo -e "${red}"
|
|
||||||
echo -e "#########################################################"
|
|
||||||
echo -e " $ERROR_MSG "
|
|
||||||
echo -e "#########################################################"
|
|
||||||
echo -e "${default}"
|
|
||||||
fi
|
|
||||||
if [ "$CONFIRM_MSG" != "" ]; then
|
|
||||||
echo -e "${green}"
|
|
||||||
echo -e "#########################################################"
|
|
||||||
echo -e " $CONFIRM_MSG "
|
|
||||||
echo -e "#########################################################"
|
|
||||||
echo -e "${default}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
entrypoint=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
||||||
|
|
||||||
|
${python_command} "${entrypoint}/kiauh.py"
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_msg(){
|
main
|
||||||
unset CONFIRM_MSG
|
|
||||||
unset ERROR_MSG
|
|
||||||
}
|
|
||||||
|
|
||||||
check_euid
|
#### sourcing all additional scripts
|
||||||
init_ini
|
#KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||||
kiauh_status
|
#for script in "${KIAUH_SRCDIR}/scripts/"*.sh; do . "${script}"; done
|
||||||
main_menu
|
#for script in "${KIAUH_SRCDIR}/scripts/ui/"*.sh; do . "${script}"; done
|
||||||
|
#
|
||||||
|
##===================================================#
|
||||||
|
##=================== UPDATE KIAUH ==================#
|
||||||
|
##===================================================#
|
||||||
|
#
|
||||||
|
#function update_kiauh() {
|
||||||
|
# status_msg "Updating KIAUH ..."
|
||||||
|
#
|
||||||
|
# cd "${KIAUH_SRCDIR}"
|
||||||
|
# git reset --hard && git pull
|
||||||
|
#
|
||||||
|
# ok_msg "Update complete! Please restart KIAUH."
|
||||||
|
# exit 0
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
##===================================================#
|
||||||
|
##=================== KIAUH STATUS ==================#
|
||||||
|
##===================================================#
|
||||||
|
#
|
||||||
|
#function kiauh_update_avail() {
|
||||||
|
# [[ ! -d "${KIAUH_SRCDIR}/.git" ]] && return
|
||||||
|
# local origin head
|
||||||
|
#
|
||||||
|
# cd "${KIAUH_SRCDIR}"
|
||||||
|
#
|
||||||
|
# ### abort if not on master branch
|
||||||
|
# ! git branch -a | grep -q "\* master" && return
|
||||||
|
#
|
||||||
|
# ### compare commit hash
|
||||||
|
# git fetch -q
|
||||||
|
# origin=$(git rev-parse --short=8 origin/master)
|
||||||
|
# head=$(git rev-parse --short=8 HEAD)
|
||||||
|
#
|
||||||
|
# if [[ ${origin} != "${head}" ]]; then
|
||||||
|
# echo "true"
|
||||||
|
# fi
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#function kiauh_update_dialog() {
|
||||||
|
# [[ ! $(kiauh_update_avail) == "true" ]] && return
|
||||||
|
# top_border
|
||||||
|
# echo -e "|${green} New KIAUH update available! ${white}|"
|
||||||
|
# hr
|
||||||
|
# echo -e "|${green} View Changelog: https://git.io/JnmlX ${white}|"
|
||||||
|
# blank_line
|
||||||
|
# echo -e "|${yellow} It is recommended to keep KIAUH up to date. Updates ${white}|"
|
||||||
|
# echo -e "|${yellow} usually contain bugfixes, important changes or new ${white}|"
|
||||||
|
# echo -e "|${yellow} features. Please consider updating! ${white}|"
|
||||||
|
# bottom_border
|
||||||
|
#
|
||||||
|
# local yn
|
||||||
|
# read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn
|
||||||
|
# while true; do
|
||||||
|
# case "${yn}" in
|
||||||
|
# Y|y|Yes|yes|"")
|
||||||
|
# do_action "update_kiauh"
|
||||||
|
# break;;
|
||||||
|
# N|n|No|no)
|
||||||
|
# break;;
|
||||||
|
# *)
|
||||||
|
# deny_action "kiauh_update_dialog";;
|
||||||
|
# esac
|
||||||
|
# done
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#check_euid
|
||||||
|
#init_logfile
|
||||||
|
#set_globals
|
||||||
|
#kiauh_update_dialog
|
||||||
|
#main_menu
|
||||||
|
|||||||
17
kiauh/__init__.py
Normal file
17
kiauh/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
KIAUH_CFG = PROJECT_ROOT.joinpath("kiauh.cfg")
|
||||||
|
|
||||||
|
APPLICATION_ROOT = Path(__file__).resolve().parent
|
||||||
|
sys.path.append(str(APPLICATION_ROOT))
|
||||||
0
kiauh/components/__init__.py
Normal file
0
kiauh/components/__init__.py
Normal file
22
kiauh/components/klipper/__init__.py
Normal file
22
kiauh/components/klipper/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
KLIPPER_DIR = Path.home().joinpath("klipper")
|
||||||
|
KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env")
|
||||||
|
KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups")
|
||||||
|
KLIPPER_REQUIREMENTS_TXT = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt")
|
||||||
|
DEFAULT_KLIPPER_REPO_URL = "https://github.com/Klipper3D/klipper"
|
||||||
|
|
||||||
|
EXIT_KLIPPER_SETUP = "Exiting Klipper setup ..."
|
||||||
1
kiauh/components/klipper/assets/klipper.env
Normal file
1
kiauh/components/klipper/assets/klipper.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
KLIPPER_ARGS="%KLIPPER_DIR%/klippy/klippy.py %CFG% -I %SERIAL% -l %LOG% -a %UDS%"
|
||||||
18
kiauh/components/klipper/assets/klipper.service
Normal file
18
kiauh/components/klipper/assets/klipper.service
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Klipper 3D Printer Firmware SV1
|
||||||
|
Documentation=https://www.klipper3d.org/
|
||||||
|
After=network-online.target
|
||||||
|
Wants=udev.target
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=%USER%
|
||||||
|
RemainAfterExit=yes
|
||||||
|
WorkingDirectory=%KLIPPER_DIR%
|
||||||
|
EnvironmentFile=%ENV_FILE%
|
||||||
|
ExecStart=%ENV%/bin/python $KLIPPER_ARGS
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
[mcu]
|
[mcu]
|
||||||
serial: /dev/serial/by-id/<your-mcu-id>
|
serial: /dev/serial/by-id/<your-mcu-id>
|
||||||
|
|
||||||
[pause_resume]
|
|
||||||
|
|
||||||
[display_status]
|
|
||||||
|
|
||||||
[virtual_sdcard]
|
[virtual_sdcard]
|
||||||
path: ~/gcode_files
|
path: %GCODES_DIR%
|
||||||
|
on_error_gcode: CANCEL_PRINT
|
||||||
|
|
||||||
[printer]
|
[printer]
|
||||||
kinematics: none
|
kinematics: none
|
||||||
max_velocity: 1000
|
max_velocity: 1000
|
||||||
max_accel: 1000
|
max_accel: 1000
|
||||||
152
kiauh/components/klipper/klipper.py
Normal file
152
kiauh/components/klipper/klipper.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from utils.constants import SYSTEMD
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class Klipper(BaseInstance):
|
||||||
|
@classmethod
|
||||||
|
def blacklist(cls) -> List[str]:
|
||||||
|
return ["None", "mcu"]
|
||||||
|
|
||||||
|
def __init__(self, suffix: str = ""):
|
||||||
|
super().__init__(instance_type=self, suffix=suffix)
|
||||||
|
self.klipper_dir: Path = KLIPPER_DIR
|
||||||
|
self.env_dir: Path = KLIPPER_ENV_DIR
|
||||||
|
self._cfg_file = self.cfg_dir.joinpath("printer.cfg")
|
||||||
|
self._log = self.log_dir.joinpath("klippy.log")
|
||||||
|
self._serial = self.comms_dir.joinpath("klippy.serial")
|
||||||
|
self._uds = self.comms_dir.joinpath("klippy.sock")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cfg_file(self) -> Path:
|
||||||
|
return self._cfg_file
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log(self) -> Path:
|
||||||
|
return self._log
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial(self) -> Path:
|
||||||
|
return self._serial
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uds(self) -> Path:
|
||||||
|
return self._uds
|
||||||
|
|
||||||
|
def create(self) -> None:
|
||||||
|
Logger.print_status("Creating new Klipper Instance ...")
|
||||||
|
service_template_path = MODULE_PATH.joinpath("assets/klipper.service")
|
||||||
|
service_file_name = self.get_service_file_name(extension=True)
|
||||||
|
service_file_target = SYSTEMD.joinpath(service_file_name)
|
||||||
|
env_template_file_path = MODULE_PATH.joinpath("assets/klipper.env")
|
||||||
|
env_file_target = self.sysd_dir.joinpath("klipper.env")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.create_folders()
|
||||||
|
self.write_service_file(
|
||||||
|
service_template_path, service_file_target, env_file_target
|
||||||
|
)
|
||||||
|
self.write_env_file(env_template_file_path, env_file_target)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Error creating service file {service_file_target}: {e}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Error creating env file {env_file_target}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
service_file = self.get_service_file_name(extension=True)
|
||||||
|
service_file_path = self.get_service_file_path()
|
||||||
|
|
||||||
|
Logger.print_status(f"Deleting Klipper Instance: {service_file}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = ["sudo", "rm", "-f", service_file_path]
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
Logger.print_ok(f"Service file deleted: {service_file_path}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error deleting service file: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def write_service_file(
|
||||||
|
self,
|
||||||
|
service_template_path: Path,
|
||||||
|
service_file_target: Path,
|
||||||
|
env_file_target: Path,
|
||||||
|
) -> None:
|
||||||
|
service_content = self._prep_service_file(
|
||||||
|
service_template_path, env_file_target
|
||||||
|
)
|
||||||
|
command = ["sudo", "tee", service_file_target]
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
|
input=service_content.encode(),
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
Logger.print_ok(f"Service file created: {service_file_target}")
|
||||||
|
|
||||||
|
def write_env_file(
|
||||||
|
self, env_template_file_path: Path, env_file_target: Path
|
||||||
|
) -> None:
|
||||||
|
env_file_content = self._prep_env_file(env_template_file_path)
|
||||||
|
with open(env_file_target, "w") as env_file:
|
||||||
|
env_file.write(env_file_content)
|
||||||
|
Logger.print_ok(f"Env file created: {env_file_target}")
|
||||||
|
|
||||||
|
def _prep_service_file(
|
||||||
|
self, service_template_path: Path, env_file_path: Path
|
||||||
|
) -> str:
|
||||||
|
try:
|
||||||
|
with open(service_template_path, "r") as template_file:
|
||||||
|
template_content = template_file.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Unable to open {service_template_path} - File not found"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
service_content = template_content.replace("%USER%", self.user)
|
||||||
|
service_content = service_content.replace(
|
||||||
|
"%KLIPPER_DIR%", str(self.klipper_dir)
|
||||||
|
)
|
||||||
|
service_content = service_content.replace("%ENV%", str(self.env_dir))
|
||||||
|
service_content = service_content.replace("%ENV_FILE%", str(env_file_path))
|
||||||
|
return service_content
|
||||||
|
|
||||||
|
def _prep_env_file(self, env_template_file_path: Path) -> str:
|
||||||
|
try:
|
||||||
|
with open(env_template_file_path, "r") as env_file:
|
||||||
|
env_template_file_content = env_file.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Unable to open {env_template_file_path} - File not found"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
env_file_content = env_template_file_content.replace(
|
||||||
|
"%KLIPPER_DIR%", str(self.klipper_dir)
|
||||||
|
)
|
||||||
|
env_file_content = env_file_content.replace(
|
||||||
|
"%CFG%", f"{self.cfg_dir}/printer.cfg"
|
||||||
|
)
|
||||||
|
env_file_content = env_file_content.replace("%SERIAL%", str(self.serial))
|
||||||
|
env_file_content = env_file_content.replace("%LOG%", str(self.log))
|
||||||
|
env_file_content = env_file_content.replace("%UDS%", str(self.uds))
|
||||||
|
return env_file_content
|
||||||
151
kiauh/components/klipper/klipper_dialogs.py
Normal file
151
kiauh/components/klipper/klipper_dialogs.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from enum import Enum, unique
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.menus.base_menu import print_back_footer
|
||||||
|
from utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class DisplayType(Enum):
|
||||||
|
SERVICE_NAME = "SERVICE_NAME"
|
||||||
|
PRINTER_NAME = "PRINTER_NAME"
|
||||||
|
|
||||||
|
|
||||||
|
def print_instance_overview(
|
||||||
|
instances: List[BaseInstance],
|
||||||
|
display_type: DisplayType = DisplayType.SERVICE_NAME,
|
||||||
|
show_headline=True,
|
||||||
|
show_index=False,
|
||||||
|
show_select_all=False,
|
||||||
|
):
|
||||||
|
dialog = "/=======================================================\\\n"
|
||||||
|
if show_headline:
|
||||||
|
d_type = (
|
||||||
|
"Klipper instances"
|
||||||
|
if display_type is DisplayType.SERVICE_NAME
|
||||||
|
else "printer directories"
|
||||||
|
)
|
||||||
|
headline = f"{COLOR_GREEN}The following {d_type} were found:{RESET_FORMAT}"
|
||||||
|
dialog += f"|{headline:^64}|\n"
|
||||||
|
dialog += "|-------------------------------------------------------|\n"
|
||||||
|
|
||||||
|
if show_select_all:
|
||||||
|
select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}"
|
||||||
|
dialog += f"| {select_all:<63}|\n"
|
||||||
|
dialog += "| |\n"
|
||||||
|
|
||||||
|
for i, s in enumerate(instances):
|
||||||
|
if display_type is DisplayType.SERVICE_NAME:
|
||||||
|
name = s.get_service_file_name()
|
||||||
|
else:
|
||||||
|
name = s.data_dir
|
||||||
|
line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {name}{RESET_FORMAT}"
|
||||||
|
dialog += f"| {line:<63}|\n"
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_select_instance_count_dialog():
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| Please select the number of Klipper instances to set |
|
||||||
|
| up. The number of Klipper instances will determine |
|
||||||
|
| the amount of printers you can run from this host. |
|
||||||
|
| |
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_select_custom_name_dialog():
|
||||||
|
line1 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| You can now assign a custom name to each instance. |
|
||||||
|
| If skipped, each instance will get an index assigned |
|
||||||
|
| in ascending order, starting at index '1'. |
|
||||||
|
| |
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_missing_usergroup_dialog(missing_groups) -> None:
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_CYAN}● tty{RESET_FORMAT}"
|
||||||
|
line3 = f"{COLOR_CYAN}● dialout{RESET_FORMAT}"
|
||||||
|
line4 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}"
|
||||||
|
line5 = f"{COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT}"
|
||||||
|
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<63}|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if "tty" in missing_groups:
|
||||||
|
dialog += f"| {line2:<63}|\n"
|
||||||
|
if "dialout" in missing_groups:
|
||||||
|
dialog += f"| {line3:<63}|\n"
|
||||||
|
|
||||||
|
dialog += textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
| |
|
||||||
|
| It is possible that you won't be able to successfully |
|
||||||
|
| connect and/or flash the controller board without |
|
||||||
|
| your user being a member of that group. |
|
||||||
|
| If you want to add the current user to the group(s) |
|
||||||
|
| listed above, answer with 'Y'. Else skip with 'n'. |
|
||||||
|
| |
|
||||||
|
| {line4:<63}|
|
||||||
|
| {line5:<63}|
|
||||||
|
\\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_update_warn_dialog() -> None:
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}Do NOT continue if there are ongoing prints running!{RESET_FORMAT}"
|
||||||
|
line3 = f"{COLOR_YELLOW}All Klipper instances will be restarted during the {RESET_FORMAT}"
|
||||||
|
line4 = f"{COLOR_YELLOW}update process and ongoing prints WILL FAIL.{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
| {line3:<63}|
|
||||||
|
| {line4:<63}|
|
||||||
|
\\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
130
kiauh/components/klipper/klipper_remove.py
Normal file
130
kiauh/components/klipper/klipper_remove.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper.klipper_dialogs import print_instance_overview
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.filesystem_utils import remove_file
|
||||||
|
from utils.input_utils import get_selection_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def run_klipper_removal(
|
||||||
|
remove_service: bool,
|
||||||
|
remove_dir: bool,
|
||||||
|
remove_env: bool,
|
||||||
|
delete_logs: bool,
|
||||||
|
) -> None:
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
|
||||||
|
if remove_service:
|
||||||
|
Logger.print_status("Removing Klipper instances ...")
|
||||||
|
if im.instances:
|
||||||
|
instances_to_remove = select_instances_to_remove(im.instances)
|
||||||
|
remove_instances(im, instances_to_remove)
|
||||||
|
else:
|
||||||
|
Logger.print_info("No Klipper Services installed! Skipped ...")
|
||||||
|
|
||||||
|
if (remove_dir or remove_env) and im.instances:
|
||||||
|
Logger.print_warn("There are still other Klipper services installed!")
|
||||||
|
Logger.print_warn("Therefor the following parts cannot be removed:")
|
||||||
|
Logger.print_warn(
|
||||||
|
"""
|
||||||
|
● Klipper local repository
|
||||||
|
● Klipper Python environment
|
||||||
|
""",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if remove_dir:
|
||||||
|
Logger.print_status("Removing Klipper local repository ...")
|
||||||
|
remove_klipper_dir()
|
||||||
|
if remove_env:
|
||||||
|
Logger.print_status("Removing Klipper Python environment ...")
|
||||||
|
remove_klipper_env()
|
||||||
|
|
||||||
|
# delete klipper logs of all instances
|
||||||
|
if delete_logs:
|
||||||
|
Logger.print_status("Removing all Klipper logs ...")
|
||||||
|
delete_klipper_logs(im.instances)
|
||||||
|
|
||||||
|
|
||||||
|
def select_instances_to_remove(
|
||||||
|
instances: List[Klipper],
|
||||||
|
) -> Union[List[Klipper], None]:
|
||||||
|
print_instance_overview(instances, show_index=True, show_select_all=True)
|
||||||
|
|
||||||
|
options = [str(i) for i in range(len(instances))]
|
||||||
|
options.extend(["a", "A", "b", "B"])
|
||||||
|
|
||||||
|
selection = get_selection_input("Select Klipper instance to remove", options)
|
||||||
|
|
||||||
|
instances_to_remove = []
|
||||||
|
if selection == "b".lower():
|
||||||
|
return None
|
||||||
|
elif selection == "a".lower():
|
||||||
|
instances_to_remove.extend(instances)
|
||||||
|
else:
|
||||||
|
instance = instances[int(selection)]
|
||||||
|
instances_to_remove.append(instance)
|
||||||
|
|
||||||
|
return instances_to_remove
|
||||||
|
|
||||||
|
|
||||||
|
def remove_instances(
|
||||||
|
instance_manager: InstanceManager,
|
||||||
|
instance_list: List[Klipper],
|
||||||
|
) -> None:
|
||||||
|
for instance in instance_list:
|
||||||
|
Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...")
|
||||||
|
instance_manager.current_instance = instance
|
||||||
|
instance_manager.stop_instance()
|
||||||
|
instance_manager.disable_instance()
|
||||||
|
instance_manager.delete_instance()
|
||||||
|
|
||||||
|
instance_manager.reload_daemon()
|
||||||
|
|
||||||
|
|
||||||
|
def remove_klipper_dir() -> None:
|
||||||
|
if not KLIPPER_DIR.exists():
|
||||||
|
Logger.print_info(f"'{KLIPPER_DIR}' does not exist. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(KLIPPER_DIR)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{KLIPPER_DIR}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_klipper_env() -> None:
|
||||||
|
if not KLIPPER_ENV_DIR.exists():
|
||||||
|
Logger.print_info(f"'{KLIPPER_ENV_DIR}' does not exist. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(KLIPPER_ENV_DIR)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{KLIPPER_ENV_DIR}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_klipper_logs(instances: List[Klipper]) -> None:
|
||||||
|
all_logfiles = []
|
||||||
|
for instance in instances:
|
||||||
|
all_logfiles = list(instance.log_dir.glob("klippy.log*"))
|
||||||
|
if not all_logfiles:
|
||||||
|
Logger.print_info("No Klipper logs found. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
for log in all_logfiles:
|
||||||
|
Logger.print_status(f"Remove '{log}'")
|
||||||
|
remove_file(log)
|
||||||
188
kiauh/components/klipper/klipper_setup.py
Normal file
188
kiauh/components/klipper/klipper_setup.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
get_existing_clients,
|
||||||
|
)
|
||||||
|
from kiauh import KIAUH_CFG
|
||||||
|
from components.klipper import (
|
||||||
|
EXIT_KLIPPER_SETUP,
|
||||||
|
DEFAULT_KLIPPER_REPO_URL,
|
||||||
|
KLIPPER_DIR,
|
||||||
|
KLIPPER_ENV_DIR,
|
||||||
|
KLIPPER_REQUIREMENTS_TXT,
|
||||||
|
)
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper.klipper_dialogs import print_update_warn_dialog
|
||||||
|
from components.klipper.klipper_utils import (
|
||||||
|
handle_disruptive_system_packages,
|
||||||
|
check_user_groups,
|
||||||
|
handle_to_multi_instance_conversion,
|
||||||
|
create_example_printer_cfg,
|
||||||
|
add_to_existing,
|
||||||
|
get_install_count,
|
||||||
|
init_name_scheme,
|
||||||
|
check_is_single_to_multi_conversion,
|
||||||
|
update_name_scheme,
|
||||||
|
handle_instance_naming,
|
||||||
|
backup_klipper_dir,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.repo_manager.repo_manager import RepoManager
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.system_utils import (
|
||||||
|
parse_packages_from_file,
|
||||||
|
create_python_venv,
|
||||||
|
install_python_requirements,
|
||||||
|
update_system_package_lists,
|
||||||
|
install_system_packages,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_klipper() -> None:
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
|
||||||
|
# ask to add new instances, if there are existing ones
|
||||||
|
if kl_im.instances and not add_to_existing():
|
||||||
|
Logger.print_status(EXIT_KLIPPER_SETUP)
|
||||||
|
return
|
||||||
|
|
||||||
|
install_count = get_install_count()
|
||||||
|
if install_count is None:
|
||||||
|
Logger.print_status(EXIT_KLIPPER_SETUP)
|
||||||
|
return
|
||||||
|
|
||||||
|
# create a dict of the size of the existing instances + install count
|
||||||
|
name_dict = {c: "" for c in range(len(kl_im.instances) + install_count)}
|
||||||
|
name_scheme = init_name_scheme(kl_im.instances, install_count)
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
name_scheme = update_name_scheme(
|
||||||
|
name_scheme, name_dict, kl_im.instances, mr_im.instances
|
||||||
|
)
|
||||||
|
|
||||||
|
handle_instance_naming(name_dict, name_scheme)
|
||||||
|
|
||||||
|
create_example_cfg = get_confirm("Create example printer.cfg?")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not kl_im.instances:
|
||||||
|
setup_klipper_prerequesites()
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for name in name_dict:
|
||||||
|
if name_dict[name] in [n.suffix for n in kl_im.instances]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if check_is_single_to_multi_conversion(kl_im.instances):
|
||||||
|
handle_to_multi_instance_conversion(name_dict[name])
|
||||||
|
continue
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
create_klipper_instance(name_dict[name], create_example_cfg)
|
||||||
|
|
||||||
|
if count == install_count:
|
||||||
|
break
|
||||||
|
|
||||||
|
kl_im.reload_daemon()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(e)
|
||||||
|
Logger.print_error("Klipper installation failed!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# step 4: check/handle conflicting packages/services
|
||||||
|
handle_disruptive_system_packages()
|
||||||
|
|
||||||
|
# step 5: check for required group membership
|
||||||
|
check_user_groups()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_klipper_prerequesites() -> None:
|
||||||
|
cm = ConfigManager(cfg_file=KIAUH_CFG)
|
||||||
|
repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL)
|
||||||
|
branch = str(cm.get_value("klipper", "branch") or "master")
|
||||||
|
|
||||||
|
repo_manager = RepoManager(
|
||||||
|
repo=repo,
|
||||||
|
branch=branch,
|
||||||
|
target_dir=KLIPPER_DIR,
|
||||||
|
)
|
||||||
|
repo_manager.clone_repo()
|
||||||
|
|
||||||
|
# install klipper dependencies and create python virtualenv
|
||||||
|
try:
|
||||||
|
install_klipper_packages(KLIPPER_DIR)
|
||||||
|
create_python_venv(KLIPPER_ENV_DIR)
|
||||||
|
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT)
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error("Error during installation of Klipper requirements!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def install_klipper_packages(klipper_dir: Path) -> None:
|
||||||
|
script = klipper_dir.joinpath("scripts/install-debian.sh")
|
||||||
|
packages = parse_packages_from_file(script)
|
||||||
|
packages = [pkg.replace("python-dev", "python3-dev") for pkg in packages]
|
||||||
|
packages.append("python3-venv")
|
||||||
|
# Add dfu-util for octopi-images
|
||||||
|
packages.append("dfu-util")
|
||||||
|
# Add dbus requirement for DietPi distro
|
||||||
|
if Path("/boot/dietpi/.version").exists():
|
||||||
|
packages.append("dbus")
|
||||||
|
|
||||||
|
update_system_package_lists(silent=False)
|
||||||
|
install_system_packages(packages)
|
||||||
|
|
||||||
|
|
||||||
|
def update_klipper() -> None:
|
||||||
|
print_update_warn_dialog()
|
||||||
|
if not get_confirm("Update Klipper now?"):
|
||||||
|
return
|
||||||
|
|
||||||
|
cm = ConfigManager(cfg_file=KIAUH_CFG)
|
||||||
|
if cm.get_value("kiauh", "backup_before_update"):
|
||||||
|
backup_klipper_dir()
|
||||||
|
|
||||||
|
instance_manager = InstanceManager(Klipper)
|
||||||
|
instance_manager.stop_all_instance()
|
||||||
|
|
||||||
|
repo = str(cm.get_value("klipper", "repository_url") or DEFAULT_KLIPPER_REPO_URL)
|
||||||
|
branch = str(cm.get_value("klipper", "branch") or "master")
|
||||||
|
|
||||||
|
repo_manager = RepoManager(
|
||||||
|
repo=repo,
|
||||||
|
branch=branch,
|
||||||
|
target_dir=KLIPPER_DIR,
|
||||||
|
)
|
||||||
|
repo_manager.pull_repo()
|
||||||
|
|
||||||
|
# install possible new system packages
|
||||||
|
install_klipper_packages(KLIPPER_DIR)
|
||||||
|
# install possible new python dependencies
|
||||||
|
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT)
|
||||||
|
|
||||||
|
instance_manager.start_all_instance()
|
||||||
|
|
||||||
|
|
||||||
|
def create_klipper_instance(name: str, create_example_cfg: bool) -> None:
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
new_instance = Klipper(suffix=name)
|
||||||
|
kl_im.current_instance = new_instance
|
||||||
|
kl_im.create_instance()
|
||||||
|
kl_im.enable_instance()
|
||||||
|
if create_example_cfg:
|
||||||
|
# if a client-config is installed, include it in the new example cfg
|
||||||
|
clients = get_existing_clients()
|
||||||
|
create_example_printer_cfg(new_instance, clients)
|
||||||
|
kl_im.start_instance()
|
||||||
325
kiauh/components/klipper/klipper_utils.py
Normal file
325
kiauh/components/klipper/klipper_utils.py
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import grp
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import textwrap
|
||||||
|
from typing import List, Union, Literal, Dict, Optional
|
||||||
|
|
||||||
|
from components.klipper import (
|
||||||
|
MODULE_PATH,
|
||||||
|
KLIPPER_DIR,
|
||||||
|
KLIPPER_ENV_DIR,
|
||||||
|
KLIPPER_BACKUP_DIR,
|
||||||
|
)
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper.klipper_dialogs import (
|
||||||
|
print_missing_usergroup_dialog,
|
||||||
|
print_instance_overview,
|
||||||
|
print_select_instance_count_dialog,
|
||||||
|
print_select_custom_name_dialog,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.moonraker_utils import moonraker_to_multi_conversion
|
||||||
|
from components.webui_client.base_data import BaseWebClient
|
||||||
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
|
create_client_config_symlink,
|
||||||
|
)
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.instance_manager.name_scheme import NameScheme
|
||||||
|
from core.repo_manager.repo_manager import RepoManager
|
||||||
|
from utils import PRINTER_CFG_BACKUP_DIR
|
||||||
|
from utils.common import get_install_status_common
|
||||||
|
from utils.constants import CURRENT_USER
|
||||||
|
from utils.input_utils import get_confirm, get_string_input, get_number_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.system_utils import mask_system_service
|
||||||
|
|
||||||
|
|
||||||
|
def get_klipper_status() -> (
|
||||||
|
Dict[
|
||||||
|
Literal["status", "status_code", "instances", "repo", "local", "remote"],
|
||||||
|
Union[str, int],
|
||||||
|
]
|
||||||
|
):
|
||||||
|
status = get_install_status_common(Klipper, KLIPPER_DIR, KLIPPER_ENV_DIR)
|
||||||
|
return {
|
||||||
|
"status": status.get("status"),
|
||||||
|
"status_code": status.get("status_code"),
|
||||||
|
"instances": status.get("instances"),
|
||||||
|
"repo": RepoManager.get_repo_name(KLIPPER_DIR),
|
||||||
|
"local": RepoManager.get_local_commit(KLIPPER_DIR),
|
||||||
|
"remote": RepoManager.get_remote_commit(KLIPPER_DIR),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_is_multi_install(
|
||||||
|
existing_instances: List[Klipper], install_count: int
|
||||||
|
) -> bool:
|
||||||
|
return not existing_instances and install_count > 1
|
||||||
|
|
||||||
|
|
||||||
|
def check_is_single_to_multi_conversion(
|
||||||
|
existing_instances: List[Klipper],
|
||||||
|
) -> bool:
|
||||||
|
return len(existing_instances) == 1 and existing_instances[0].suffix == ""
|
||||||
|
|
||||||
|
|
||||||
|
def init_name_scheme(
|
||||||
|
existing_instances: List[Klipper], install_count: int
|
||||||
|
) -> NameScheme:
|
||||||
|
if check_is_multi_install(
|
||||||
|
existing_instances, install_count
|
||||||
|
) or check_is_single_to_multi_conversion(existing_instances):
|
||||||
|
print_select_custom_name_dialog()
|
||||||
|
if get_confirm("Assign custom names?", False, allow_go_back=True):
|
||||||
|
return NameScheme.CUSTOM
|
||||||
|
else:
|
||||||
|
return NameScheme.INDEX
|
||||||
|
else:
|
||||||
|
return NameScheme.SINGLE
|
||||||
|
|
||||||
|
|
||||||
|
def update_name_scheme(
|
||||||
|
name_scheme: NameScheme,
|
||||||
|
name_dict: Dict[int, str],
|
||||||
|
klipper_instances: List[Klipper],
|
||||||
|
moonraker_instances: List[Moonraker],
|
||||||
|
) -> NameScheme:
|
||||||
|
# if there are more moonraker instances installed than klipper, we
|
||||||
|
# load their names into the name_dict, as we will detect and enforce that naming scheme
|
||||||
|
if len(moonraker_instances) > len(klipper_instances):
|
||||||
|
update_name_dict(name_dict, moonraker_instances)
|
||||||
|
return detect_name_scheme(moonraker_instances)
|
||||||
|
elif len(klipper_instances) > 1:
|
||||||
|
update_name_dict(name_dict, klipper_instances)
|
||||||
|
return detect_name_scheme(klipper_instances)
|
||||||
|
else:
|
||||||
|
return name_scheme
|
||||||
|
|
||||||
|
|
||||||
|
def update_name_dict(name_dict: Dict[int, str], instances: List[BaseInstance]) -> None:
|
||||||
|
for k, v in enumerate(instances):
|
||||||
|
name_dict[k] = v.suffix
|
||||||
|
|
||||||
|
|
||||||
|
def handle_instance_naming(name_dict: Dict[int, str], name_scheme: NameScheme) -> None:
|
||||||
|
if name_scheme == NameScheme.SINGLE:
|
||||||
|
return
|
||||||
|
|
||||||
|
for k in name_dict:
|
||||||
|
if name_dict[k] == "" and name_scheme == NameScheme.INDEX:
|
||||||
|
name_dict[k] = str(k + 1)
|
||||||
|
elif name_dict[k] == "" and name_scheme == NameScheme.CUSTOM:
|
||||||
|
assign_custom_name(k, name_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_existing() -> bool:
|
||||||
|
kl_instances = InstanceManager(Klipper).instances
|
||||||
|
print_instance_overview(kl_instances)
|
||||||
|
return get_confirm("Add new instances?", allow_go_back=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_install_count() -> Union[int, None]:
|
||||||
|
"""
|
||||||
|
Print a dialog for selecting the amount of Klipper instances
|
||||||
|
to set up with an option to navigate back. Returns None if the
|
||||||
|
user selected to go back, otherwise an integer greater or equal than 1 |
|
||||||
|
:return: Integer >= 1 or None
|
||||||
|
"""
|
||||||
|
kl_instances = InstanceManager(Klipper).instances
|
||||||
|
print_select_instance_count_dialog()
|
||||||
|
question = f"Number of{' additional' if len(kl_instances) > 0 else ''} Klipper instances to set up"
|
||||||
|
return get_number_input(question, 1, default=1, allow_go_back=True)
|
||||||
|
|
||||||
|
|
||||||
|
def assign_custom_name(key: int, name_dict: Dict[int, str]) -> None:
|
||||||
|
existing_names = []
|
||||||
|
existing_names.extend(Klipper.blacklist())
|
||||||
|
existing_names.extend(name_dict[n] for n in name_dict)
|
||||||
|
question = f"Enter name for instance {key + 1}"
|
||||||
|
name_dict[key] = get_string_input(question, exclude=existing_names)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_to_multi_instance_conversion(new_name: str) -> None:
|
||||||
|
Logger.print_status("Converting single instance to multi instances ...")
|
||||||
|
klipper_to_multi_conversion(new_name)
|
||||||
|
moonraker_to_multi_conversion(new_name)
|
||||||
|
|
||||||
|
|
||||||
|
def klipper_to_multi_conversion(new_name: str) -> None:
|
||||||
|
Logger.print_status("Convert Klipper single to multi instance ...")
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
im.current_instance = im.instances[0]
|
||||||
|
|
||||||
|
# temporarily store the data dir path
|
||||||
|
old_data_dir = im.instances[0].data_dir
|
||||||
|
old_data_dir_name = im.instances[0].data_dir_name
|
||||||
|
|
||||||
|
# backup the old data_dir
|
||||||
|
bm = BackupManager()
|
||||||
|
name = f"config-{old_data_dir_name}"
|
||||||
|
bm.backup_directory(
|
||||||
|
name,
|
||||||
|
source=im.current_instance.cfg_dir,
|
||||||
|
target=PRINTER_CFG_BACKUP_DIR,
|
||||||
|
)
|
||||||
|
|
||||||
|
# remove the old single instance
|
||||||
|
im.stop_instance()
|
||||||
|
im.disable_instance()
|
||||||
|
im.delete_instance()
|
||||||
|
|
||||||
|
# create a new klipper instance with the new name
|
||||||
|
new_instance = Klipper(suffix=new_name)
|
||||||
|
im.current_instance = new_instance
|
||||||
|
|
||||||
|
if not new_instance.data_dir.is_dir():
|
||||||
|
# rename the old data dir and use it for the new instance
|
||||||
|
Logger.print_status(f"Rename '{old_data_dir}' to '{new_instance.data_dir}' ...")
|
||||||
|
old_data_dir.rename(new_instance.data_dir)
|
||||||
|
else:
|
||||||
|
Logger.print_info(f"Existing '{new_instance.data_dir}' found ...")
|
||||||
|
|
||||||
|
# patch the virtual_sdcard sections path value to match the new printer_data foldername
|
||||||
|
cm = ConfigManager(new_instance.cfg_file)
|
||||||
|
if cm.config.has_section("virtual_sdcard"):
|
||||||
|
cm.set_value("virtual_sdcard", "path", str(new_instance.gcodes_dir))
|
||||||
|
cm.write_config()
|
||||||
|
|
||||||
|
# finalize creating the new instance
|
||||||
|
im.create_instance()
|
||||||
|
im.enable_instance()
|
||||||
|
im.start_instance()
|
||||||
|
|
||||||
|
|
||||||
|
def check_user_groups():
|
||||||
|
current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()]
|
||||||
|
|
||||||
|
missing_groups = []
|
||||||
|
if "tty" not in current_groups:
|
||||||
|
missing_groups.append("tty")
|
||||||
|
if "dialout" not in current_groups:
|
||||||
|
missing_groups.append("dialout")
|
||||||
|
|
||||||
|
if not missing_groups:
|
||||||
|
return
|
||||||
|
|
||||||
|
print_missing_usergroup_dialog(missing_groups)
|
||||||
|
if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"):
|
||||||
|
log = "Skipped adding user to required groups. You might encounter issues."
|
||||||
|
Logger.warn(log)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
for group in missing_groups:
|
||||||
|
Logger.print_status(f"Adding user '{CURRENT_USER}' to group {group} ...")
|
||||||
|
command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER]
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Unable to add user to usergroups: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
log = "Remember to relog/restart this machine for the group(s) to be applied!"
|
||||||
|
Logger.print_warn(log)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_disruptive_system_packages() -> None:
|
||||||
|
services = []
|
||||||
|
|
||||||
|
command = ["systemctl", "is-enabled", "brltty"]
|
||||||
|
brltty_status = subprocess.run(command, capture_output=True, text=True)
|
||||||
|
|
||||||
|
command = ["systemctl", "is-enabled", "brltty-udev"]
|
||||||
|
brltty_udev_status = subprocess.run(command, capture_output=True, text=True)
|
||||||
|
|
||||||
|
command = ["systemctl", "is-enabled", "ModemManager"]
|
||||||
|
modem_manager_status = subprocess.run(command, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if "enabled" in brltty_status.stdout:
|
||||||
|
services.append("brltty")
|
||||||
|
if "enabled" in brltty_udev_status.stdout:
|
||||||
|
services.append("brltty-udev")
|
||||||
|
if "enabled" in modem_manager_status.stdout:
|
||||||
|
services.append("ModemManager")
|
||||||
|
|
||||||
|
for service in services if services else []:
|
||||||
|
try:
|
||||||
|
log = f"{service} service detected! Masking {service} service ..."
|
||||||
|
Logger.print_status(log)
|
||||||
|
mask_system_service(service)
|
||||||
|
Logger.print_ok(f"{service} service masked!")
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
warn_msg = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
KIAUH was unable to mask the {service} system service.
|
||||||
|
Please fix the problem manually. Otherwise, this may have
|
||||||
|
undesirable effects on the operation of Klipper.
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
Logger.print_warn(warn_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_name_scheme(instance_list: List[BaseInstance]) -> NameScheme:
|
||||||
|
pattern = re.compile("^\d+$")
|
||||||
|
for instance in instance_list:
|
||||||
|
if not pattern.match(instance.suffix):
|
||||||
|
return NameScheme.CUSTOM
|
||||||
|
|
||||||
|
return NameScheme.INDEX
|
||||||
|
|
||||||
|
|
||||||
|
def get_highest_index(instance_list: List[Klipper]) -> int:
|
||||||
|
indices = [int(instance.suffix.split("-")[-1]) for instance in instance_list]
|
||||||
|
return max(indices)
|
||||||
|
|
||||||
|
|
||||||
|
def create_example_printer_cfg(
|
||||||
|
instance: Klipper, clients: Optional[List[BaseWebClient]] = None
|
||||||
|
) -> None:
|
||||||
|
Logger.print_status(f"Creating example printer.cfg in '{instance.cfg_dir}'")
|
||||||
|
if instance.cfg_file.is_file():
|
||||||
|
Logger.print_info(f"'{instance.cfg_file}' already exists.")
|
||||||
|
return
|
||||||
|
|
||||||
|
source = MODULE_PATH.joinpath("assets/printer.cfg")
|
||||||
|
target = instance.cfg_file
|
||||||
|
try:
|
||||||
|
shutil.copy(source, target)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to create example printer.cfg:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
cm = ConfigManager(target)
|
||||||
|
cm.set_value("virtual_sdcard", "path", str(instance.gcodes_dir))
|
||||||
|
|
||||||
|
# include existing client configs in the example config
|
||||||
|
if clients is not None and len(clients) > 0:
|
||||||
|
for c in clients:
|
||||||
|
client_config = c.client_config
|
||||||
|
section = client_config.config_section
|
||||||
|
cm.config.add_section(section=section)
|
||||||
|
create_client_config_symlink(client_config, [instance])
|
||||||
|
|
||||||
|
cm.write_config()
|
||||||
|
|
||||||
|
Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'")
|
||||||
|
|
||||||
|
|
||||||
|
def backup_klipper_dir() -> None:
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory("klipper", source=KLIPPER_DIR, target=KLIPPER_BACKUP_DIR)
|
||||||
|
bm.backup_directory("klippy-env", source=KLIPPER_ENV_DIR, target=KLIPPER_BACKUP_DIR)
|
||||||
0
kiauh/components/klipper/menus/__init__.py
Normal file
0
kiauh/components/klipper/menus/__init__.py
Normal file
116
kiauh/components/klipper/menus/klipper_remove_menu.py
Normal file
116
kiauh/components/klipper/menus/klipper_remove_menu.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.klipper import klipper_remove
|
||||||
|
from core.menus import FooterType, Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
class KlipperRemoveMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
self.remove_klipper_service = False
|
||||||
|
self.remove_klipper_dir = False
|
||||||
|
self.remove_klipper_env = False
|
||||||
|
self.delete_klipper_logs = False
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.remove_menu import RemoveMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else RemoveMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"0": Option(method=self.toggle_all, menu=False),
|
||||||
|
"1": Option(method=self.toggle_remove_klipper_service, menu=False),
|
||||||
|
"2": Option(method=self.toggle_remove_klipper_dir, menu=False),
|
||||||
|
"3": Option(method=self.toggle_remove_klipper_env, menu=False),
|
||||||
|
"4": Option(method=self.toggle_delete_klipper_logs, menu=False),
|
||||||
|
"c": Option(method=self.run_removal_process, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " [ Remove Klipper ] "
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
|
||||||
|
unchecked = "[ ]"
|
||||||
|
o1 = checked if self.remove_klipper_service else unchecked
|
||||||
|
o2 = checked if self.remove_klipper_dir else unchecked
|
||||||
|
o3 = checked if self.remove_klipper_env else unchecked
|
||||||
|
o4 = checked if self.delete_klipper_logs else unchecked
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Enter a number and hit enter to select / deselect |
|
||||||
|
| the specific option for removal. |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) Select everything |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 1) {o1} Remove Service |
|
||||||
|
| 2) {o2} Remove Local Repository |
|
||||||
|
| 3) {o3} Remove Python Environment |
|
||||||
|
| 4) {o4} Delete all Log-Files |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| C) Continue |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def toggle_all(self, **kwargs) -> None:
|
||||||
|
self.remove_klipper_service = True
|
||||||
|
self.remove_klipper_dir = True
|
||||||
|
self.remove_klipper_env = True
|
||||||
|
self.delete_klipper_logs = True
|
||||||
|
|
||||||
|
def toggle_remove_klipper_service(self, **kwargs) -> None:
|
||||||
|
self.remove_klipper_service = not self.remove_klipper_service
|
||||||
|
|
||||||
|
def toggle_remove_klipper_dir(self, **kwargs) -> None:
|
||||||
|
self.remove_klipper_dir = not self.remove_klipper_dir
|
||||||
|
|
||||||
|
def toggle_remove_klipper_env(self, **kwargs) -> None:
|
||||||
|
self.remove_klipper_env = not self.remove_klipper_env
|
||||||
|
|
||||||
|
def toggle_delete_klipper_logs(self, **kwargs) -> None:
|
||||||
|
self.delete_klipper_logs = not self.delete_klipper_logs
|
||||||
|
|
||||||
|
def run_removal_process(self, **kwargs) -> None:
|
||||||
|
if (
|
||||||
|
not self.remove_klipper_service
|
||||||
|
and not self.remove_klipper_dir
|
||||||
|
and not self.remove_klipper_env
|
||||||
|
and not self.delete_klipper_logs
|
||||||
|
):
|
||||||
|
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}"
|
||||||
|
print(error)
|
||||||
|
return
|
||||||
|
|
||||||
|
klipper_remove.run_klipper_removal(
|
||||||
|
self.remove_klipper_service,
|
||||||
|
self.remove_klipper_dir,
|
||||||
|
self.remove_klipper_env,
|
||||||
|
self.delete_klipper_logs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.remove_klipper_service = False
|
||||||
|
self.remove_klipper_dir = False
|
||||||
|
self.remove_klipper_env = False
|
||||||
|
self.delete_klipper_logs = False
|
||||||
12
kiauh/components/klipper_firmware/__init__.py
Normal file
12
kiauh/components/klipper_firmware/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR
|
||||||
|
|
||||||
|
SD_FLASH_SCRIPT = KLIPPER_DIR.joinpath("scripts/flash-sdcard.sh")
|
||||||
174
kiauh/components/klipper_firmware/firmware_utils.py
Normal file
174
kiauh/components/klipper_firmware/firmware_utils.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from subprocess import CalledProcessError, check_output, Popen, PIPE, STDOUT, run
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper_firmware import SD_FLASH_SCRIPT
|
||||||
|
from components.klipper_firmware.flash_options import (
|
||||||
|
FlashOptions,
|
||||||
|
FlashMethod,
|
||||||
|
)
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.system_utils import log_process
|
||||||
|
|
||||||
|
|
||||||
|
def find_firmware_file() -> bool:
|
||||||
|
target = KLIPPER_DIR.joinpath("out")
|
||||||
|
target_exists = target.exists()
|
||||||
|
|
||||||
|
f1 = "klipper.elf.hex"
|
||||||
|
f2 = "klipper.elf"
|
||||||
|
f3 = "klipper.bin"
|
||||||
|
fw_file_exists = (
|
||||||
|
target.joinpath(f1).exists() and target.joinpath(f2).exists()
|
||||||
|
) or target.joinpath(f3).exists()
|
||||||
|
|
||||||
|
return target_exists and fw_file_exists
|
||||||
|
|
||||||
|
|
||||||
|
def find_usb_device_by_id() -> List[str]:
|
||||||
|
try:
|
||||||
|
command = "find /dev/serial/by-id/* 2>/dev/null"
|
||||||
|
output = check_output(command, shell=True, text=True)
|
||||||
|
return output.splitlines()
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error("Unable to find a USB device!")
|
||||||
|
Logger.print_error(e, prefix=False)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def find_uart_device() -> List[str]:
|
||||||
|
try:
|
||||||
|
command = '"find /dev -maxdepth 1 -regextype posix-extended -regex "^\/dev\/tty(AMA0|S0)$" 2>/dev/null"'
|
||||||
|
output = check_output(command, shell=True, text=True)
|
||||||
|
return output.splitlines()
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error("Unable to find a UART device!")
|
||||||
|
Logger.print_error(e, prefix=False)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def find_usb_dfu_device() -> List[str]:
|
||||||
|
try:
|
||||||
|
command = '"lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null"'
|
||||||
|
output = check_output(command, shell=True, text=True)
|
||||||
|
return output.splitlines()
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error("Unable to find a USB DFU device!")
|
||||||
|
Logger.print_error(e, prefix=False)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_sd_flash_board_list() -> List[str]:
|
||||||
|
if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = f"{SD_FLASH_SCRIPT} -l"
|
||||||
|
blist = check_output(cmd, shell=True, text=True)
|
||||||
|
return blist.splitlines()[1:]
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"An unexpected error occured:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def start_flash_process(flash_options: FlashOptions) -> None:
|
||||||
|
Logger.print_status(f"Flashing '{flash_options.selected_mcu}' ...")
|
||||||
|
try:
|
||||||
|
if not flash_options.flash_method:
|
||||||
|
raise Exception("Missing value for flash_method!")
|
||||||
|
if not flash_options.flash_command:
|
||||||
|
raise Exception("Missing value for flash_command!")
|
||||||
|
if not flash_options.selected_mcu:
|
||||||
|
raise Exception("Missing value for selected_mcu!")
|
||||||
|
if not flash_options.connection_type:
|
||||||
|
raise Exception("Missing value for connection_type!")
|
||||||
|
if (
|
||||||
|
flash_options.flash_method == FlashMethod.SD_CARD
|
||||||
|
and not flash_options.selected_board
|
||||||
|
):
|
||||||
|
raise Exception("Missing value for selected_board!")
|
||||||
|
|
||||||
|
if flash_options.flash_method is FlashMethod.REGULAR:
|
||||||
|
cmd = [
|
||||||
|
"make",
|
||||||
|
flash_options.flash_command.value,
|
||||||
|
f"FLASH_DEVICE={flash_options.selected_mcu}",
|
||||||
|
]
|
||||||
|
elif flash_options.flash_method is FlashMethod.SD_CARD:
|
||||||
|
if not SD_FLASH_SCRIPT.exists():
|
||||||
|
raise Exception("Unable to find Klippers sdcard flash script!")
|
||||||
|
cmd = [
|
||||||
|
SD_FLASH_SCRIPT.as_posix(),
|
||||||
|
f"-b {flash_options.selected_baudrate}",
|
||||||
|
flash_options.selected_mcu,
|
||||||
|
flash_options.selected_board,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
raise Exception("Invalid value for flash_method!")
|
||||||
|
|
||||||
|
instance_manager = InstanceManager(Klipper)
|
||||||
|
instance_manager.stop_all_instance()
|
||||||
|
|
||||||
|
process = Popen(cmd, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True)
|
||||||
|
log_process(process)
|
||||||
|
|
||||||
|
instance_manager.start_all_instance()
|
||||||
|
|
||||||
|
rc = process.returncode
|
||||||
|
if rc != 0:
|
||||||
|
raise Exception(f"Flashing failed with returncode: {rc}")
|
||||||
|
else:
|
||||||
|
Logger.print_ok("Flashing successfull!", start="\n", end="\n\n")
|
||||||
|
|
||||||
|
except (Exception, CalledProcessError):
|
||||||
|
Logger.print_error("Flashing failed!", start="\n")
|
||||||
|
Logger.print_error("See the console output above!", end="\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def run_make_clean() -> None:
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
"make clean",
|
||||||
|
cwd=KLIPPER_DIR,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Unexpected error:\n{e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def run_make_menuconfig() -> None:
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
"make PYTHON=python3 menuconfig",
|
||||||
|
cwd=KLIPPER_DIR,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Unexpected error:\n{e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def run_make() -> None:
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
"make PYTHON=python3",
|
||||||
|
cwd=KLIPPER_DIR,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Unexpected error:\n{e}")
|
||||||
|
raise
|
||||||
104
kiauh/components/klipper_firmware/flash_options.py
Normal file
104
kiauh/components/klipper_firmware/flash_options.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from dataclasses import field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Union, List
|
||||||
|
|
||||||
|
|
||||||
|
class FlashMethod(Enum):
|
||||||
|
REGULAR = "Regular"
|
||||||
|
SD_CARD = "SD Card"
|
||||||
|
|
||||||
|
|
||||||
|
class FlashCommand(Enum):
|
||||||
|
FLASH = "flash"
|
||||||
|
SERIAL_FLASH = "serialflash"
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionType(Enum):
|
||||||
|
USB = "USB"
|
||||||
|
USB_DFU = "USB (DFU)"
|
||||||
|
UART = "UART"
|
||||||
|
|
||||||
|
|
||||||
|
class FlashOptions:
|
||||||
|
_instance = None
|
||||||
|
_flash_method: Union[FlashMethod, None] = None
|
||||||
|
_flash_command: Union[FlashCommand, None] = None
|
||||||
|
_connection_type: Union[ConnectionType, None] = None
|
||||||
|
_mcu_list: List[str] = field(default_factory=list)
|
||||||
|
_selected_mcu: str = ""
|
||||||
|
_selected_board: str = ""
|
||||||
|
_selected_baudrate: int = 250000
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if not cls._instance:
|
||||||
|
cls._instance = super(FlashOptions, cls).__new__(cls, *args, **kwargs)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def destroy(cls):
|
||||||
|
cls._instance = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def flash_method(self) -> Union[FlashMethod, None]:
|
||||||
|
return self._flash_method
|
||||||
|
|
||||||
|
@flash_method.setter
|
||||||
|
def flash_method(self, value: Union[FlashMethod, None]):
|
||||||
|
self._flash_method = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def flash_command(self) -> Union[FlashCommand, None]:
|
||||||
|
return self._flash_command
|
||||||
|
|
||||||
|
@flash_command.setter
|
||||||
|
def flash_command(self, value: Union[FlashCommand, None]):
|
||||||
|
self._flash_command = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connection_type(self) -> Union[ConnectionType, None]:
|
||||||
|
return self._connection_type
|
||||||
|
|
||||||
|
@connection_type.setter
|
||||||
|
def connection_type(self, value: Union[ConnectionType, None]):
|
||||||
|
self._connection_type = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mcu_list(self) -> List[str]:
|
||||||
|
return self._mcu_list
|
||||||
|
|
||||||
|
@mcu_list.setter
|
||||||
|
def mcu_list(self, value: List[str]) -> None:
|
||||||
|
self._mcu_list = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected_mcu(self) -> str:
|
||||||
|
return self._selected_mcu
|
||||||
|
|
||||||
|
@selected_mcu.setter
|
||||||
|
def selected_mcu(self, value: str) -> None:
|
||||||
|
self._selected_mcu = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected_board(self) -> str:
|
||||||
|
return self._selected_board
|
||||||
|
|
||||||
|
@selected_board.setter
|
||||||
|
def selected_board(self, value: str) -> None:
|
||||||
|
self._selected_board = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected_baudrate(self) -> int:
|
||||||
|
return self._selected_baudrate
|
||||||
|
|
||||||
|
@selected_baudrate.setter
|
||||||
|
def selected_baudrate(self, value: int) -> None:
|
||||||
|
self._selected_baudrate = value
|
||||||
112
kiauh/components/klipper_firmware/menus/klipper_build_menu.py
Normal file
112
kiauh/components/klipper_firmware/menus/klipper_build_menu.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR
|
||||||
|
from components.klipper_firmware.firmware_utils import (
|
||||||
|
run_make_clean,
|
||||||
|
run_make_menuconfig,
|
||||||
|
run_make,
|
||||||
|
)
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_GREEN, COLOR_RED
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.system_utils import (
|
||||||
|
check_package_install,
|
||||||
|
update_system_package_lists,
|
||||||
|
install_system_packages,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperBuildFirmwareMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.deps = ["build-essential", "dpkg-dev", "make"]
|
||||||
|
self.missing_deps = check_package_install(self.deps)
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else AdvancedMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
if len(self.missing_deps) == 0:
|
||||||
|
self.input_label_txt = "Press ENTER to continue"
|
||||||
|
self.default_option = Option(method=self.start_build_process, menu=False)
|
||||||
|
else:
|
||||||
|
self.input_label_txt = "Press ENTER to install dependencies"
|
||||||
|
self.default_option = Option(method=self.install_missing_deps, menu=False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " [ Build Firmware Menu ] "
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| The following dependencies are required: |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
for d in self.deps:
|
||||||
|
status_ok = f"{COLOR_GREEN}*INSTALLED*{RESET_FORMAT}"
|
||||||
|
status_missing = f"{COLOR_RED}*MISSING*{RESET_FORMAT}"
|
||||||
|
status = status_missing if d in self.missing_deps else status_ok
|
||||||
|
padding = 39 - len(d) + len(status) + (len(status_ok) - len(status))
|
||||||
|
d = f" {COLOR_CYAN}● {d}{RESET_FORMAT}"
|
||||||
|
menu += f"| {d}{status:>{padding}} |\n"
|
||||||
|
|
||||||
|
menu += "| |\n"
|
||||||
|
if len(self.missing_deps) == 0:
|
||||||
|
line = f"{COLOR_GREEN}All dependencies are met!{RESET_FORMAT}"
|
||||||
|
else:
|
||||||
|
line = f"{COLOR_RED}Dependencies are missing!{RESET_FORMAT}"
|
||||||
|
|
||||||
|
menu += f"| {line:<62} |\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def install_missing_deps(self, **kwargs) -> None:
|
||||||
|
try:
|
||||||
|
update_system_package_lists(silent=False)
|
||||||
|
Logger.print_status("Installing system packages...")
|
||||||
|
install_system_packages(self.missing_deps)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(e)
|
||||||
|
Logger.print_error("Installding dependencies failed!")
|
||||||
|
finally:
|
||||||
|
# restart this menu
|
||||||
|
KlipperBuildFirmwareMenu().run()
|
||||||
|
|
||||||
|
def start_build_process(self, **kwargs) -> None:
|
||||||
|
try:
|
||||||
|
run_make_clean()
|
||||||
|
run_make_menuconfig()
|
||||||
|
run_make()
|
||||||
|
|
||||||
|
Logger.print_ok("Firmware successfully built!")
|
||||||
|
Logger.print_ok(f"Firmware file located in '{KLIPPER_DIR}/out'!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(e)
|
||||||
|
Logger.print_error("Building Klipper Firmware failed!")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.previous_menu().run()
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.klipper_firmware.flash_options import FlashOptions, FlashMethod
|
||||||
|
from core.menus import FooterType, Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_RED, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperNoFirmwareErrorMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
self.footer_type = FooterType.BLANK
|
||||||
|
self.input_label_txt = "Press ENTER to go back to [Advanced Menu]"
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.default_option = Option(self.go_back, False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "!!! NO FIRMWARE FILE FOUND !!!"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
line1 = f"{color}Unable to find a compiled firmware file!{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line1:<62} |
|
||||||
|
| |
|
||||||
|
| Make sure, that: |
|
||||||
|
| ● the folder '~/klipper/out' and its content exist |
|
||||||
|
| ● the folder contains the following file: |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if self.flash_options.flash_method is FlashMethod.REGULAR:
|
||||||
|
menu += "| ● 'klipper.elf' |\n"
|
||||||
|
menu += "| ● 'klipper.elf.hex' |\n"
|
||||||
|
else:
|
||||||
|
menu += "| ● 'klipper.bin' |\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def go_back(self, **kwargs) -> None:
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
|
||||||
|
AdvancedMenu().run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperNoBoardTypesErrorMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.footer_type = FooterType.BLANK
|
||||||
|
self.input_label_txt = "Press ENTER to go back to [Main Menu]"
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.default_option = Option(self.go_back, False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "!!! ERROR GETTING BOARD LIST !!!"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
line1 = f"{color}Reading the list of supported boards failed!{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line1:<62} |
|
||||||
|
| |
|
||||||
|
| Make sure, that: |
|
||||||
|
| ● the folder '~/klipper' and all its content exist |
|
||||||
|
| ● the content of folder '~/klipper' is not currupted |
|
||||||
|
| ● the file '~/klipper/scripts/flash-sd.py' exist |
|
||||||
|
| ● your current user has access to those files/folders |
|
||||||
|
| |
|
||||||
|
| If in doubt or this process continues to fail, please |
|
||||||
|
| consider to download Klipper again. |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def go_back(self, **kwargs) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
MainMenu().run()
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection DuplicatedCode
|
||||||
|
class KlipperFlashMethodHelpMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_menu import (
|
||||||
|
KlipperFlashMethodMenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperFlashMethodMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " < ? > Help: Flash MCU < ? > "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}"
|
||||||
|
subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {subheader1:<62} |
|
||||||
|
| The default method to flash controller boards which |
|
||||||
|
| are connected and updated over USB and not by placing |
|
||||||
|
| a compiled firmware file onto an internal SD-Card. |
|
||||||
|
| |
|
||||||
|
| Common controllers that get flashed that way are: |
|
||||||
|
| - Arduino Mega 2560 |
|
||||||
|
| - Fysetc F6 / S6 (used without a Display + SD-Slot) |
|
||||||
|
| |
|
||||||
|
| {subheader2:<62} |
|
||||||
|
| Many popular controller boards ship with a bootloader |
|
||||||
|
| capable of updating the firmware via SD-Card. |
|
||||||
|
| Choose this method if your controller board supports |
|
||||||
|
| this way of updating. This method ONLY works for up- |
|
||||||
|
| grading firmware. The initial flashing procedure must |
|
||||||
|
| be done manually per the instructions that apply to |
|
||||||
|
| your controller board. |
|
||||||
|
| |
|
||||||
|
| Common controllers that can be flashed that way are: |
|
||||||
|
| - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 |
|
||||||
|
| - Fysetc F6 / S6 (used with a Display + SD-Slot) |
|
||||||
|
| - Fysetc Spider |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection DuplicatedCode
|
||||||
|
class KlipperFlashCommandHelpMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_menu import (
|
||||||
|
KlipperFlashCommandMenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperFlashCommandMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " < ? > Help: Flash MCU < ? > "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}"
|
||||||
|
subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {subheader1:<62} |
|
||||||
|
| The default command to flash controller board, it |
|
||||||
|
| will detect selected microcontroller and use suitable |
|
||||||
|
| tool for flashing it. |
|
||||||
|
| |
|
||||||
|
| {subheader2:<62} |
|
||||||
|
| Special command to flash STM32 microcontrollers in |
|
||||||
|
| DFU mode but connected via serial. stm32flash command |
|
||||||
|
| will be used internally. |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection DuplicatedCode
|
||||||
|
class KlipperMcuConnectionHelpMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_menu import (
|
||||||
|
KlipperSelectMcuConnectionMenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu
|
||||||
|
if previous_menu is not None
|
||||||
|
else KlipperSelectMcuConnectionMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " < ? > Help: Flash MCU < ? > "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}"
|
||||||
|
subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {subheader1:<62} |
|
||||||
|
| Selecting USB as the connection method will scan the |
|
||||||
|
| USB ports for connected controller boards. This will |
|
||||||
|
| be similar to the 'ls /dev/serial/by-id/*' command |
|
||||||
|
| suggested by the official Klipper documentation for |
|
||||||
|
| determining successfull USB connections! |
|
||||||
|
| |
|
||||||
|
| {subheader2:<62} |
|
||||||
|
| Selecting UART as the connection method will list all |
|
||||||
|
| possible UART serial ports. Note: This method ALWAYS |
|
||||||
|
| returns something as it seems impossible to determine |
|
||||||
|
| if a valid Klipper controller board is connected or |
|
||||||
|
| not. Because of that, you MUST know which UART serial |
|
||||||
|
| port your controller board is connected to when using |
|
||||||
|
| this connection method. |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
443
kiauh/components/klipper_firmware/menus/klipper_flash_menu.py
Normal file
443
kiauh/components/klipper_firmware/menus/klipper_flash_menu.py
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
import time
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.klipper_firmware.flash_options import (
|
||||||
|
FlashOptions,
|
||||||
|
FlashMethod,
|
||||||
|
FlashCommand,
|
||||||
|
ConnectionType,
|
||||||
|
)
|
||||||
|
from components.klipper_firmware.firmware_utils import (
|
||||||
|
find_usb_device_by_id,
|
||||||
|
find_uart_device,
|
||||||
|
find_usb_dfu_device,
|
||||||
|
get_sd_flash_board_list,
|
||||||
|
start_flash_process,
|
||||||
|
find_firmware_file,
|
||||||
|
)
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_error_menu import (
|
||||||
|
KlipperNoBoardTypesErrorMenu,
|
||||||
|
KlipperNoFirmwareErrorMenu,
|
||||||
|
)
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_help_menu import (
|
||||||
|
KlipperMcuConnectionHelpMenu,
|
||||||
|
KlipperFlashCommandHelpMenu,
|
||||||
|
KlipperFlashMethodHelpMenu,
|
||||||
|
)
|
||||||
|
from core.menus import FooterType, Option
|
||||||
|
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_RED
|
||||||
|
from utils.input_utils import get_number_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperFlashMethodMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.help_menu = KlipperFlashMethodHelpMenu
|
||||||
|
self.input_label_txt = "Select flash method"
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else AdvancedMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(self.select_regular, menu=False),
|
||||||
|
"2": Option(self.select_sdcard, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " [ MCU Flash Menu ] "
|
||||||
|
subheader = f"{COLOR_YELLOW}ATTENTION:{RESET_FORMAT}"
|
||||||
|
subline1 = f"{COLOR_YELLOW}Make sure to select the correct method for the MCU!{RESET_FORMAT}"
|
||||||
|
subline2 = f"{COLOR_YELLOW}Not all MCUs support both methods!{RESET_FORMAT}"
|
||||||
|
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Select the flash method for flashing the MCU. |
|
||||||
|
| |
|
||||||
|
| {subheader:<62} |
|
||||||
|
| {subline1:<62} |
|
||||||
|
| {subline2:<62} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| |
|
||||||
|
| 1) Regular flashing method |
|
||||||
|
| 2) Updating via SD-Card Update |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def select_regular(self, **kwargs):
|
||||||
|
self.flash_options.flash_method = FlashMethod.REGULAR
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def select_sdcard(self, **kwargs):
|
||||||
|
self.flash_options.flash_method = FlashMethod.SD_CARD
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def goto_next_menu(self, **kwargs):
|
||||||
|
if find_firmware_file():
|
||||||
|
KlipperFlashCommandMenu(previous_menu=self.__class__).run()
|
||||||
|
else:
|
||||||
|
KlipperNoFirmwareErrorMenu().run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperFlashCommandMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.help_menu = KlipperFlashCommandHelpMenu
|
||||||
|
self.input_label_txt = "Select flash command"
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperFlashMethodMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(self.select_flash, menu=False),
|
||||||
|
"2": Option(self.select_serialflash, menu=False),
|
||||||
|
}
|
||||||
|
self.default_option = Option(self.select_flash, menu=False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
/=======================================================\\
|
||||||
|
| |
|
||||||
|
| Which flash command to use for flashing the MCU? |
|
||||||
|
| 1) make flash (default) |
|
||||||
|
| 2) make serialflash (stm32flash) |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def select_flash(self, **kwargs):
|
||||||
|
self.flash_options.flash_command = FlashCommand.FLASH
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def select_serialflash(self, **kwargs):
|
||||||
|
self.flash_options.flash_command = FlashCommand.SERIAL_FLASH
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def goto_next_menu(self, **kwargs):
|
||||||
|
KlipperSelectMcuConnectionMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperSelectMcuConnectionMenu(BaseMenu):
|
||||||
|
def __init__(
|
||||||
|
self, previous_menu: Optional[Type[BaseMenu]] = None, standalone: bool = False
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.__standalone = standalone
|
||||||
|
self.help_menu = KlipperMcuConnectionHelpMenu
|
||||||
|
self.input_label_txt = "Select connection type"
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperFlashCommandMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.select_usb, menu=False),
|
||||||
|
"2": Option(method=self.select_dfu, menu=False),
|
||||||
|
"3": Option(method=self.select_usb_dfu, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "Make sure that the controller board is connected now!"
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| |
|
||||||
|
| How is the controller board connected to the host? |
|
||||||
|
| 1) USB |
|
||||||
|
| 2) UART |
|
||||||
|
| 3) USB (DFU mode) |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def select_usb(self, **kwargs):
|
||||||
|
self.flash_options.connection_type = ConnectionType.USB
|
||||||
|
self.get_mcu_list()
|
||||||
|
|
||||||
|
def select_dfu(self, **kwargs):
|
||||||
|
self.flash_options.connection_type = ConnectionType.UART
|
||||||
|
self.get_mcu_list()
|
||||||
|
|
||||||
|
def select_usb_dfu(self, **kwargs):
|
||||||
|
self.flash_options.connection_type = ConnectionType.USB_DFU
|
||||||
|
self.get_mcu_list()
|
||||||
|
|
||||||
|
def get_mcu_list(self, **kwargs):
|
||||||
|
conn_type = self.flash_options.connection_type
|
||||||
|
|
||||||
|
if conn_type is ConnectionType.USB:
|
||||||
|
Logger.print_status("Identifying MCU connected via USB ...")
|
||||||
|
self.flash_options.mcu_list = find_usb_device_by_id()
|
||||||
|
elif conn_type is ConnectionType.UART:
|
||||||
|
Logger.print_status("Identifying MCU possibly connected via UART ...")
|
||||||
|
self.flash_options.mcu_list = find_uart_device()
|
||||||
|
elif conn_type is ConnectionType.USB_DFU:
|
||||||
|
Logger.print_status("Identifying MCU connected via USB in DFU mode ...")
|
||||||
|
self.flash_options.mcu_list = find_usb_dfu_device()
|
||||||
|
|
||||||
|
if len(self.flash_options.mcu_list) < 1:
|
||||||
|
Logger.print_warn("No MCUs found!")
|
||||||
|
Logger.print_warn("Make sure they are connected and repeat this step.")
|
||||||
|
|
||||||
|
# if standalone is True, we only display the MCUs to the user and return
|
||||||
|
if self.__standalone and len(self.flash_options.mcu_list) > 0:
|
||||||
|
Logger.print_ok("The following MCUs were found:", prefix=False)
|
||||||
|
for i, mcu in enumerate(self.flash_options.mcu_list):
|
||||||
|
print(f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}")
|
||||||
|
time.sleep(3)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def goto_next_menu(self, **kwargs):
|
||||||
|
KlipperSelectMcuIdMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperSelectMcuIdMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
self.mcu_list = self.flash_options.mcu_list
|
||||||
|
self.input_label_txt = "Select MCU to flash"
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu
|
||||||
|
if previous_menu is not None
|
||||||
|
else KlipperSelectMcuConnectionMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
f"{i}": Option(self.flash_mcu, False, f"{i}")
|
||||||
|
for i in range(len(self.mcu_list))
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "!!! ATTENTION !!!"
|
||||||
|
header2 = f"[{COLOR_CYAN}List of available MCUs{RESET_FORMAT}]"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Make sure, to select the correct MCU! |
|
||||||
|
| ONLY flash a firmware created for the respective MCU! |
|
||||||
|
| |
|
||||||
|
|{header2:-^64}|
|
||||||
|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
for i, mcu in enumerate(self.mcu_list):
|
||||||
|
mcu = mcu.split("/")[-1]
|
||||||
|
menu += f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n"
|
||||||
|
|
||||||
|
print(menu, end="\n")
|
||||||
|
|
||||||
|
def flash_mcu(self, **kwargs):
|
||||||
|
index = int(kwargs.get("opt_index"))
|
||||||
|
selected_mcu = self.mcu_list[index]
|
||||||
|
self.flash_options.selected_mcu = selected_mcu
|
||||||
|
|
||||||
|
if self.flash_options.flash_method == FlashMethod.SD_CARD:
|
||||||
|
KlipperSelectSDFlashBoardMenu(previous_menu=self.__class__).run()
|
||||||
|
elif self.flash_options.flash_method == FlashMethod.REGULAR:
|
||||||
|
KlipperFlashOverviewMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperSelectSDFlashBoardMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
self.available_boards = get_sd_flash_board_list()
|
||||||
|
self.input_label_txt = "Select board type"
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperSelectMcuIdMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
f"{i}": Option(self.board_select, False, f"{i}")
|
||||||
|
for i in range(len(self.available_boards))
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
if len(self.available_boards) < 1:
|
||||||
|
KlipperNoBoardTypesErrorMenu().run()
|
||||||
|
else:
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
/=======================================================\\
|
||||||
|
| Please select the type of board that corresponds to |
|
||||||
|
| the currently selected MCU ID you chose before. |
|
||||||
|
| |
|
||||||
|
| The following boards are currently supported: |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
for i, board in enumerate(self.available_boards):
|
||||||
|
line = f" {i}) {board}"
|
||||||
|
menu += f"|{line:<55}|\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def board_select(self, **kwargs):
|
||||||
|
board = int(kwargs.get("opt_index"))
|
||||||
|
self.flash_options.selected_board = self.available_boards[board]
|
||||||
|
self.baudrate_select()
|
||||||
|
|
||||||
|
def baudrate_select(self, **kwargs):
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
/=======================================================\\
|
||||||
|
| If your board is flashed with firmware that connects |
|
||||||
|
| at a custom baud rate, please change it now. |
|
||||||
|
| |
|
||||||
|
| If you are unsure, stick to the default 250000! |
|
||||||
|
\\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
self.flash_options.selected_baudrate = get_number_input(
|
||||||
|
question="Please set the baud rate",
|
||||||
|
default=250000,
|
||||||
|
min_count=0,
|
||||||
|
allow_go_back=True,
|
||||||
|
)
|
||||||
|
KlipperFlashOverviewMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperFlashOverviewMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
self.input_label_txt = "Perform action (default=Y)"
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = previous_menu
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"Y": Option(self.execute_flash, menu=False),
|
||||||
|
"N": Option(self.abort_process, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.default_option = Option(self.execute_flash, menu=False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "!!! ATTENTION !!!"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
|
||||||
|
method = self.flash_options.flash_method.value
|
||||||
|
command = self.flash_options.flash_command.value
|
||||||
|
conn_type = self.flash_options.connection_type.value
|
||||||
|
mcu = self.flash_options.selected_mcu
|
||||||
|
board = self.flash_options.selected_board
|
||||||
|
baudrate = self.flash_options.selected_baudrate
|
||||||
|
subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Before contuining the flashing process, please check |
|
||||||
|
| if all parameters were set correctly! Once you made |
|
||||||
|
| sure everything is correct, start the process. If any |
|
||||||
|
| parameter needs to be changed, you can go back (B) |
|
||||||
|
| step by step or abort and start from the beginning. |
|
||||||
|
|{subheader:-^64}|
|
||||||
|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
menu += f" ● MCU: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n"
|
||||||
|
menu += f" ● Connection: {COLOR_CYAN}{conn_type}{RESET_FORMAT}\n"
|
||||||
|
menu += f" ● Flash method: {COLOR_CYAN}{method}{RESET_FORMAT}\n"
|
||||||
|
menu += f" ● Flash command: {COLOR_CYAN}{command}{RESET_FORMAT}\n"
|
||||||
|
|
||||||
|
if self.flash_options.flash_method is FlashMethod.SD_CARD:
|
||||||
|
menu += f" ● Board type: {COLOR_CYAN}{board}{RESET_FORMAT}\n"
|
||||||
|
menu += f" ● Baudrate: {COLOR_CYAN}{baudrate}{RESET_FORMAT}\n"
|
||||||
|
|
||||||
|
menu += textwrap.dedent(
|
||||||
|
"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Y) Start flash process |
|
||||||
|
| N) Abort - Return to Advanced Menu |
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def execute_flash(self, **kwargs):
|
||||||
|
start_flash_process(self.flash_options)
|
||||||
|
Logger.print_info("Returning to MCU Flash Menu in 5 seconds ...")
|
||||||
|
time.sleep(5)
|
||||||
|
KlipperFlashMethodMenu().run()
|
||||||
|
|
||||||
|
def abort_process(self, **kwargs):
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
|
||||||
|
AdvancedMenu().run()
|
||||||
14
kiauh/components/log_uploads/__init__.py
Normal file
14
kiauh/components/log_uploads/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Union, Literal
|
||||||
|
|
||||||
|
FileKey = Literal["filepath", "display_name"]
|
||||||
|
LogFile = Dict[FileKey, Union[str, Path]]
|
||||||
54
kiauh/components/log_uploads/log_upload_utils.py
Normal file
54
kiauh/components/log_uploads/log_upload_utils.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.log_uploads import LogFile
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_logfile_list() -> List[LogFile]:
|
||||||
|
cm = InstanceManager(Klipper)
|
||||||
|
log_dirs: List[Path] = [instance.log_dir for instance in cm.instances]
|
||||||
|
|
||||||
|
logfiles: List[LogFile] = []
|
||||||
|
for _dir in log_dirs:
|
||||||
|
for f in _dir.iterdir():
|
||||||
|
logfiles.append({"filepath": f, "display_name": get_display_name(f)})
|
||||||
|
|
||||||
|
return logfiles
|
||||||
|
|
||||||
|
|
||||||
|
def get_display_name(filepath: Path) -> str:
|
||||||
|
printer = " ".join(filepath.parts[-3].split("_")[:-1])
|
||||||
|
name = filepath.name
|
||||||
|
|
||||||
|
return f"{printer}: {name}"
|
||||||
|
|
||||||
|
|
||||||
|
def upload_logfile(logfile: LogFile) -> None:
|
||||||
|
file = logfile.get("filepath")
|
||||||
|
name = logfile.get("display_name")
|
||||||
|
Logger.print_status(f"Uploading the following logfile from {name} ...")
|
||||||
|
|
||||||
|
with open(file, "rb") as f:
|
||||||
|
headers = {"x-random": ""}
|
||||||
|
req = urllib.request.Request("http://paste.c-net.org/", headers=headers, data=f)
|
||||||
|
try:
|
||||||
|
response = urllib.request.urlopen(req)
|
||||||
|
link = response.read().decode("utf-8")
|
||||||
|
Logger.print_ok("Upload successful! Access it via the following link:")
|
||||||
|
Logger.print_ok(f">>>> {link}", False)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error("Uploading logfile failed!")
|
||||||
|
Logger.print_error(str(e))
|
||||||
62
kiauh/components/log_uploads/menus/log_upload_menu.py
Normal file
62
kiauh/components/log_uploads/menus/log_upload_menu.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.log_uploads.log_upload_utils import get_logfile_list
|
||||||
|
from components.log_uploads.log_upload_utils import upload_logfile
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import RESET_FORMAT, COLOR_YELLOW
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class LogUploadMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.logfile_list = get_logfile_list()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
f"{index}": Option(self.upload, False, opt_index=f"{index}")
|
||||||
|
for index in range(len(self.logfile_list))
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Log Upload ] "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| You can select the following logfiles for uploading: |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
for logfile in enumerate(self.logfile_list):
|
||||||
|
line = f"{logfile[0]}) {logfile[1].get('display_name')}"
|
||||||
|
menu += f"| {line:<54}|\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def upload(self, **kwargs):
|
||||||
|
index = int(kwargs.get("opt_index"))
|
||||||
|
upload_logfile(self.logfile_list[index])
|
||||||
34
kiauh/components/moonraker/__init__.py
Normal file
34
kiauh/components/moonraker/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
MOONRAKER_DIR = Path.home().joinpath("moonraker")
|
||||||
|
MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env")
|
||||||
|
MOONRAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-backups")
|
||||||
|
MOONRAKER_DB_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-db-backups")
|
||||||
|
MOONRAKER_REQUIREMENTS_TXT = MOONRAKER_DIR.joinpath(
|
||||||
|
"scripts/moonraker-requirements.txt"
|
||||||
|
)
|
||||||
|
DEFAULT_MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker"
|
||||||
|
DEFAULT_MOONRAKER_PORT = 7125
|
||||||
|
|
||||||
|
# introduced due to
|
||||||
|
# https://github.com/Arksine/moonraker/issues/349
|
||||||
|
# https://github.com/Arksine/moonraker/pull/346
|
||||||
|
POLKIT_LEGACY_FILE = Path("/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla")
|
||||||
|
POLKIT_FILE = Path("/etc/polkit-1/rules.d/moonraker.rules")
|
||||||
|
POLKIT_USR_FILE = Path("/usr/share/polkit-1/rules.d/moonraker.rules")
|
||||||
|
POLKIT_SCRIPT = Path.home().joinpath("moonraker/scripts/set-policykit-rules.sh")
|
||||||
|
|
||||||
|
EXIT_MOONRAKER_SETUP = "Exiting Moonraker setup ..."
|
||||||
29
kiauh/components/moonraker/assets/moonraker.conf
Normal file
29
kiauh/components/moonraker/assets/moonraker.conf
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[server]
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: %PORT%
|
||||||
|
klippy_uds_address: %UDS%
|
||||||
|
|
||||||
|
[authorization]
|
||||||
|
trusted_clients:
|
||||||
|
10.0.0.0/8
|
||||||
|
127.0.0.0/8
|
||||||
|
169.254.0.0/16
|
||||||
|
172.16.0.0/12
|
||||||
|
192.168.0.0/16
|
||||||
|
FE80::/10
|
||||||
|
::1/128
|
||||||
|
cors_domains:
|
||||||
|
*.lan
|
||||||
|
*.local
|
||||||
|
*://localhost
|
||||||
|
*://localhost:*
|
||||||
|
*://my.mainsail.xyz
|
||||||
|
*://app.fluidd.xyz
|
||||||
|
|
||||||
|
[octoprint_compat]
|
||||||
|
|
||||||
|
[history]
|
||||||
|
|
||||||
|
[update_manager]
|
||||||
|
channel: dev
|
||||||
|
refresh_interval: 168
|
||||||
1
kiauh/components/moonraker/assets/moonraker.env
Normal file
1
kiauh/components/moonraker/assets/moonraker.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
MOONRAKER_ARGS="%MOONRAKER_DIR%/moonraker/moonraker.py -d %PRINTER_DATA%"
|
||||||
19
kiauh/components/moonraker/assets/moonraker.service
Normal file
19
kiauh/components/moonraker/assets/moonraker.service
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=API Server for Klipper SV1
|
||||||
|
Documentation=https://moonraker.readthedocs.io/
|
||||||
|
Requires=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=%USER%
|
||||||
|
SupplementaryGroups=moonraker-admin
|
||||||
|
RemainAfterExit=yes
|
||||||
|
WorkingDirectory=%MOONRAKER_DIR%
|
||||||
|
EnvironmentFile=%ENV_FILE%
|
||||||
|
ExecStart=%ENV%/bin/python $MOONRAKER_ARGS
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
0
kiauh/components/moonraker/menus/__init__.py
Normal file
0
kiauh/components/moonraker/menus/__init__.py
Normal file
126
kiauh/components/moonraker/menus/moonraker_remove_menu.py
Normal file
126
kiauh/components/moonraker/menus/moonraker_remove_menu.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.moonraker import moonraker_remove
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
class MoonrakerRemoveMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.remove_moonraker_service = False
|
||||||
|
self.remove_moonraker_dir = False
|
||||||
|
self.remove_moonraker_env = False
|
||||||
|
self.remove_moonraker_polkit = False
|
||||||
|
self.delete_moonraker_logs = False
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.remove_menu import RemoveMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else RemoveMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"0": Option(method=self.toggle_all, menu=False),
|
||||||
|
"1": Option(method=self.toggle_remove_moonraker_service, menu=False),
|
||||||
|
"2": Option(method=self.toggle_remove_moonraker_dir, menu=False),
|
||||||
|
"3": Option(method=self.toggle_remove_moonraker_env, menu=False),
|
||||||
|
"4": Option(method=self.toggle_remove_moonraker_polkit, menu=False),
|
||||||
|
"5": Option(method=self.toggle_delete_moonraker_logs, menu=False),
|
||||||
|
"c": Option(method=self.run_removal_process, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " [ Remove Moonraker ] "
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
|
||||||
|
unchecked = "[ ]"
|
||||||
|
o1 = checked if self.remove_moonraker_service else unchecked
|
||||||
|
o2 = checked if self.remove_moonraker_dir else unchecked
|
||||||
|
o3 = checked if self.remove_moonraker_env else unchecked
|
||||||
|
o4 = checked if self.remove_moonraker_polkit else unchecked
|
||||||
|
o5 = checked if self.delete_moonraker_logs else unchecked
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Enter a number and hit enter to select / deselect |
|
||||||
|
| the specific option for removal. |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) Select everything |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 1) {o1} Remove Service |
|
||||||
|
| 2) {o2} Remove Local Repository |
|
||||||
|
| 3) {o3} Remove Python Environment |
|
||||||
|
| 4) {o4} Remove Policy Kit Rules |
|
||||||
|
| 5) {o5} Delete all Log-Files |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| C) Continue |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def toggle_all(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_service = True
|
||||||
|
self.remove_moonraker_dir = True
|
||||||
|
self.remove_moonraker_env = True
|
||||||
|
self.remove_moonraker_polkit = True
|
||||||
|
self.delete_moonraker_logs = True
|
||||||
|
|
||||||
|
def toggle_remove_moonraker_service(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_service = not self.remove_moonraker_service
|
||||||
|
|
||||||
|
def toggle_remove_moonraker_dir(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_dir = not self.remove_moonraker_dir
|
||||||
|
|
||||||
|
def toggle_remove_moonraker_env(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_env = not self.remove_moonraker_env
|
||||||
|
|
||||||
|
def toggle_remove_moonraker_polkit(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_polkit = not self.remove_moonraker_polkit
|
||||||
|
|
||||||
|
def toggle_delete_moonraker_logs(self, **kwargs) -> None:
|
||||||
|
self.delete_moonraker_logs = not self.delete_moonraker_logs
|
||||||
|
|
||||||
|
def run_removal_process(self, **kwargs) -> None:
|
||||||
|
if (
|
||||||
|
not self.remove_moonraker_service
|
||||||
|
and not self.remove_moonraker_dir
|
||||||
|
and not self.remove_moonraker_env
|
||||||
|
and not self.remove_moonraker_polkit
|
||||||
|
and not self.delete_moonraker_logs
|
||||||
|
):
|
||||||
|
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}"
|
||||||
|
print(error)
|
||||||
|
return
|
||||||
|
|
||||||
|
moonraker_remove.run_moonraker_removal(
|
||||||
|
self.remove_moonraker_service,
|
||||||
|
self.remove_moonraker_dir,
|
||||||
|
self.remove_moonraker_env,
|
||||||
|
self.remove_moonraker_polkit,
|
||||||
|
self.delete_moonraker_logs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.remove_moonraker_service = False
|
||||||
|
self.remove_moonraker_dir = False
|
||||||
|
self.remove_moonraker_env = False
|
||||||
|
self.remove_moonraker_polkit = False
|
||||||
|
self.delete_moonraker_logs = False
|
||||||
154
kiauh/components/moonraker/moonraker.py
Normal file
154
kiauh/components/moonraker/moonraker.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from utils.constants import SYSTEMD
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class Moonraker(BaseInstance):
|
||||||
|
@classmethod
|
||||||
|
def blacklist(cls) -> List[str]:
|
||||||
|
return ["None", "mcu"]
|
||||||
|
|
||||||
|
def __init__(self, suffix: str = ""):
|
||||||
|
super().__init__(instance_type=self, suffix=suffix)
|
||||||
|
self.moonraker_dir: Path = MOONRAKER_DIR
|
||||||
|
self.env_dir: Path = MOONRAKER_ENV_DIR
|
||||||
|
self.cfg_file = self.cfg_dir.joinpath("moonraker.conf")
|
||||||
|
self.port = self._get_port()
|
||||||
|
self.backup_dir = self.data_dir.joinpath("backup")
|
||||||
|
self.certs_dir = self.data_dir.joinpath("certs")
|
||||||
|
self._db_dir = self.data_dir.joinpath("database")
|
||||||
|
self._comms_dir = self.data_dir.joinpath("comms")
|
||||||
|
self.log = self.log_dir.joinpath("moonraker.log")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_dir(self) -> Path:
|
||||||
|
return self._db_dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comms_dir(self) -> Path:
|
||||||
|
return self._comms_dir
|
||||||
|
|
||||||
|
def create(self, create_example_cfg: bool = False) -> None:
|
||||||
|
Logger.print_status("Creating new Moonraker Instance ...")
|
||||||
|
service_template_path = MODULE_PATH.joinpath("assets/moonraker.service")
|
||||||
|
env_template_file_path = MODULE_PATH.joinpath("assets/moonraker.env")
|
||||||
|
service_file_name = self.get_service_file_name(extension=True)
|
||||||
|
service_file_target = SYSTEMD.joinpath(service_file_name)
|
||||||
|
env_file_target = self.sysd_dir.joinpath("moonraker.env")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.create_folders([self.backup_dir, self.certs_dir, self._db_dir])
|
||||||
|
self.write_service_file(
|
||||||
|
service_template_path, service_file_target, env_file_target
|
||||||
|
)
|
||||||
|
self.write_env_file(env_template_file_path, env_file_target)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Error creating service file {service_file_target}: {e}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Error writing file: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
service_file = self.get_service_file_name(extension=True)
|
||||||
|
service_file_path = self.get_service_file_path()
|
||||||
|
|
||||||
|
Logger.print_status(f"Deleting Moonraker Instance: {service_file}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = ["sudo", "rm", "-f", service_file_path]
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
Logger.print_ok(f"Service file deleted: {service_file_path}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error deleting service file: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def write_service_file(
|
||||||
|
self,
|
||||||
|
service_template_path: Path,
|
||||||
|
service_file_target: Path,
|
||||||
|
env_file_target: Path,
|
||||||
|
) -> None:
|
||||||
|
service_content = self._prep_service_file(
|
||||||
|
service_template_path, env_file_target
|
||||||
|
)
|
||||||
|
command = ["sudo", "tee", service_file_target]
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
|
input=service_content.encode(),
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
Logger.print_ok(f"Service file created: {service_file_target}")
|
||||||
|
|
||||||
|
def write_env_file(
|
||||||
|
self, env_template_file_path: Path, env_file_target: Path
|
||||||
|
) -> None:
|
||||||
|
env_file_content = self._prep_env_file(env_template_file_path)
|
||||||
|
with open(env_file_target, "w") as env_file:
|
||||||
|
env_file.write(env_file_content)
|
||||||
|
Logger.print_ok(f"Env file created: {env_file_target}")
|
||||||
|
|
||||||
|
def _prep_service_file(
|
||||||
|
self, service_template_path: Path, env_file_path: Path
|
||||||
|
) -> str:
|
||||||
|
try:
|
||||||
|
with open(service_template_path, "r") as template_file:
|
||||||
|
template_content = template_file.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Unable to open {service_template_path} - File not found"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
service_content = template_content.replace("%USER%", self.user)
|
||||||
|
service_content = service_content.replace(
|
||||||
|
"%MOONRAKER_DIR%", str(self.moonraker_dir)
|
||||||
|
)
|
||||||
|
service_content = service_content.replace("%ENV%", str(self.env_dir))
|
||||||
|
service_content = service_content.replace("%ENV_FILE%", str(env_file_path))
|
||||||
|
return service_content
|
||||||
|
|
||||||
|
def _prep_env_file(self, env_template_file_path: Path) -> str:
|
||||||
|
try:
|
||||||
|
with open(env_template_file_path, "r") as env_file:
|
||||||
|
env_template_file_content = env_file.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Unable to open {env_template_file_path} - File not found"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
env_file_content = env_template_file_content.replace(
|
||||||
|
"%MOONRAKER_DIR%", str(self.moonraker_dir)
|
||||||
|
)
|
||||||
|
env_file_content = env_file_content.replace(
|
||||||
|
"%PRINTER_DATA%", str(self.data_dir)
|
||||||
|
)
|
||||||
|
return env_file_content
|
||||||
|
|
||||||
|
def _get_port(self) -> Union[int, None]:
|
||||||
|
if not self.cfg_file.is_file():
|
||||||
|
return None
|
||||||
|
|
||||||
|
cm = ConfigManager(cfg_file=self.cfg_file)
|
||||||
|
port = cm.get_value("server", "port")
|
||||||
|
|
||||||
|
return int(port) if port is not None else port
|
||||||
70
kiauh/components/moonraker/moonraker_dialogs.py
Normal file
70
kiauh/components/moonraker/moonraker_dialogs.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.menus.base_menu import print_back_footer
|
||||||
|
from utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN
|
||||||
|
|
||||||
|
|
||||||
|
def print_moonraker_overview(
|
||||||
|
klipper_instances: List[Klipper],
|
||||||
|
moonraker_instances: List[Moonraker],
|
||||||
|
show_index=False,
|
||||||
|
show_select_all=False,
|
||||||
|
):
|
||||||
|
headline = f"{COLOR_GREEN}The following instances were found:{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
|{headline:^64}|
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if show_select_all:
|
||||||
|
select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}"
|
||||||
|
dialog += f"| {select_all:<63}|\n"
|
||||||
|
dialog += "| |\n"
|
||||||
|
|
||||||
|
instance_map = {
|
||||||
|
k.get_service_file_name(): (
|
||||||
|
k.get_service_file_name().replace("klipper", "moonraker")
|
||||||
|
if k.suffix in [m.suffix for m in moonraker_instances]
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
for k in klipper_instances
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, k in enumerate(instance_map):
|
||||||
|
mr_name = instance_map.get(k)
|
||||||
|
m = f"<-> {mr_name}" if mr_name != "" else ""
|
||||||
|
line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {k} {m} {RESET_FORMAT}"
|
||||||
|
dialog += f"| {line:<63}|\n"
|
||||||
|
|
||||||
|
warn_l1 = f"{COLOR_YELLOW}PLEASE NOTE: {RESET_FORMAT}"
|
||||||
|
warn_l2 = f"{COLOR_YELLOW}If you select an instance with an existing Moonraker{RESET_FORMAT}"
|
||||||
|
warn_l3 = f"{COLOR_YELLOW}instance, that Moonraker instance will be re-created!{RESET_FORMAT}"
|
||||||
|
warning = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
| |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {warn_l1:<63}|
|
||||||
|
| {warn_l2:<63}|
|
||||||
|
| {warn_l3:<63}|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
dialog += warning
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
159
kiauh/components/moonraker/moonraker_remove.py
Normal file
159
kiauh/components/moonraker/moonraker_remove.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
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 utils.filesystem_utils import remove_file
|
||||||
|
from utils.input_utils import get_selection_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def run_moonraker_removal(
|
||||||
|
remove_service: bool,
|
||||||
|
remove_dir: bool,
|
||||||
|
remove_env: bool,
|
||||||
|
remove_polkit: bool,
|
||||||
|
delete_logs: bool,
|
||||||
|
) -> None:
|
||||||
|
im = InstanceManager(Moonraker)
|
||||||
|
|
||||||
|
if remove_service:
|
||||||
|
Logger.print_status("Removing Moonraker instances ...")
|
||||||
|
if im.instances:
|
||||||
|
instances_to_remove = select_instances_to_remove(im.instances)
|
||||||
|
remove_instances(im, instances_to_remove)
|
||||||
|
else:
|
||||||
|
Logger.print_info("No Moonraker Services installed! Skipped ...")
|
||||||
|
|
||||||
|
if (remove_polkit or remove_dir or remove_env) and im.instances:
|
||||||
|
Logger.print_warn("There are still other Moonraker services installed!")
|
||||||
|
Logger.print_warn("Therefor the following parts cannot be removed:")
|
||||||
|
Logger.print_warn(
|
||||||
|
"""
|
||||||
|
● Moonraker PolicyKit rules
|
||||||
|
● Moonraker local repository
|
||||||
|
● Moonraker Python environment
|
||||||
|
""",
|
||||||
|
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 ...")
|
||||||
|
remove_moonraker_dir()
|
||||||
|
if remove_env:
|
||||||
|
Logger.print_status("Removing Moonraker Python environment ...")
|
||||||
|
remove_moonraker_env()
|
||||||
|
|
||||||
|
# delete moonraker logs of all instances
|
||||||
|
if delete_logs:
|
||||||
|
Logger.print_status("Removing all Moonraker logs ...")
|
||||||
|
delete_moonraker_logs(im.instances)
|
||||||
|
|
||||||
|
|
||||||
|
def select_instances_to_remove(
|
||||||
|
instances: List[Moonraker],
|
||||||
|
) -> Union[List[Moonraker], None]:
|
||||||
|
print_instance_overview(instances, show_index=True, show_select_all=True)
|
||||||
|
|
||||||
|
options = [str(i) for i in range(len(instances))]
|
||||||
|
options.extend(["a", "A", "b", "B"])
|
||||||
|
|
||||||
|
selection = get_selection_input("Select Moonraker instance to remove", options)
|
||||||
|
|
||||||
|
instances_to_remove = []
|
||||||
|
if selection == "b".lower():
|
||||||
|
return None
|
||||||
|
elif selection == "a".lower():
|
||||||
|
instances_to_remove.extend(instances)
|
||||||
|
else:
|
||||||
|
instance = instances[int(selection)]
|
||||||
|
instances_to_remove.append(instance)
|
||||||
|
|
||||||
|
return instances_to_remove
|
||||||
|
|
||||||
|
|
||||||
|
def remove_instances(
|
||||||
|
instance_manager: InstanceManager,
|
||||||
|
instance_list: List[Moonraker],
|
||||||
|
) -> None:
|
||||||
|
for instance in instance_list:
|
||||||
|
Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...")
|
||||||
|
instance_manager.current_instance = instance
|
||||||
|
instance_manager.stop_instance()
|
||||||
|
instance_manager.disable_instance()
|
||||||
|
instance_manager.delete_instance()
|
||||||
|
|
||||||
|
instance_manager.reload_daemon()
|
||||||
|
|
||||||
|
|
||||||
|
def remove_moonraker_dir() -> None:
|
||||||
|
if not MOONRAKER_DIR.exists():
|
||||||
|
Logger.print_info(f"'{MOONRAKER_DIR}' does not exist. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(MOONRAKER_DIR)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{MOONRAKER_DIR}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_moonraker_env() -> None:
|
||||||
|
if not MOONRAKER_ENV_DIR.exists():
|
||||||
|
Logger.print_info(f"'{MOONRAKER_ENV_DIR}' does not exist. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(MOONRAKER_ENV_DIR)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{MOONRAKER_ENV_DIR}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
command = [
|
||||||
|
f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh",
|
||||||
|
"--clear",
|
||||||
|
]
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error while removing policykit rules: {e}")
|
||||||
|
|
||||||
|
Logger.print_ok("Policykit rules successfully removed!")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_moonraker_logs(instances: List[Moonraker]) -> None:
|
||||||
|
all_logfiles = []
|
||||||
|
for instance in instances:
|
||||||
|
all_logfiles = list(instance.log_dir.glob("moonraker.log*"))
|
||||||
|
if not all_logfiles:
|
||||||
|
Logger.print_info("No Moonraker logs found. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
for log in all_logfiles:
|
||||||
|
Logger.print_status(f"Remove '{log}'")
|
||||||
|
remove_file(log)
|
||||||
220
kiauh/components/moonraker/moonraker_setup.py
Normal file
220
kiauh/components/moonraker/moonraker_setup.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
enable_mainsail_remotemode,
|
||||||
|
get_existing_clients,
|
||||||
|
)
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from kiauh import KIAUH_CFG
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker import (
|
||||||
|
EXIT_MOONRAKER_SETUP,
|
||||||
|
DEFAULT_MOONRAKER_REPO_URL,
|
||||||
|
MOONRAKER_DIR,
|
||||||
|
MOONRAKER_ENV_DIR,
|
||||||
|
MOONRAKER_REQUIREMENTS_TXT,
|
||||||
|
POLKIT_LEGACY_FILE,
|
||||||
|
POLKIT_FILE,
|
||||||
|
POLKIT_USR_FILE,
|
||||||
|
POLKIT_SCRIPT,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.moonraker_dialogs import print_moonraker_overview
|
||||||
|
from components.moonraker.moonraker_utils import (
|
||||||
|
create_example_moonraker_conf,
|
||||||
|
backup_moonraker_dir,
|
||||||
|
)
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.repo_manager.repo_manager import RepoManager
|
||||||
|
from utils.filesystem_utils import check_file_exist
|
||||||
|
from utils.input_utils import (
|
||||||
|
get_confirm,
|
||||||
|
get_selection_input,
|
||||||
|
)
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.system_utils import (
|
||||||
|
parse_packages_from_file,
|
||||||
|
create_python_venv,
|
||||||
|
install_python_requirements,
|
||||||
|
update_system_package_lists,
|
||||||
|
install_system_packages,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_moonraker() -> None:
|
||||||
|
if not check_moonraker_install_requirements():
|
||||||
|
return
|
||||||
|
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
klipper_instances = kl_im.instances
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
moonraker_instances = mr_im.instances
|
||||||
|
|
||||||
|
selected_klipper_instance = 0
|
||||||
|
if len(klipper_instances) > 1:
|
||||||
|
print_moonraker_overview(
|
||||||
|
klipper_instances,
|
||||||
|
moonraker_instances,
|
||||||
|
show_index=True,
|
||||||
|
show_select_all=True,
|
||||||
|
)
|
||||||
|
options = [str(i) for i in range(len(klipper_instances))]
|
||||||
|
options.extend(["a", "A", "b", "B"])
|
||||||
|
question = "Select Klipper instance to setup Moonraker for"
|
||||||
|
selected_klipper_instance = get_selection_input(question, options).lower()
|
||||||
|
|
||||||
|
instance_names = []
|
||||||
|
if selected_klipper_instance == "b":
|
||||||
|
Logger.print_status(EXIT_MOONRAKER_SETUP)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif selected_klipper_instance == "a":
|
||||||
|
for instance in klipper_instances:
|
||||||
|
instance_names.append(instance.suffix)
|
||||||
|
|
||||||
|
else:
|
||||||
|
index = int(selected_klipper_instance)
|
||||||
|
instance_names.append(klipper_instances[index].suffix)
|
||||||
|
|
||||||
|
create_example_cfg = get_confirm("Create example moonraker.conf?")
|
||||||
|
setup_moonraker_prerequesites()
|
||||||
|
install_moonraker_polkit()
|
||||||
|
|
||||||
|
used_ports_map = {
|
||||||
|
instance.suffix: instance.port for instance in moonraker_instances
|
||||||
|
}
|
||||||
|
for name in instance_names:
|
||||||
|
current_instance = Moonraker(suffix=name)
|
||||||
|
|
||||||
|
mr_im.current_instance = current_instance
|
||||||
|
mr_im.create_instance()
|
||||||
|
mr_im.enable_instance()
|
||||||
|
|
||||||
|
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(current_instance, used_ports_map, clients)
|
||||||
|
|
||||||
|
mr_im.start_instance()
|
||||||
|
|
||||||
|
mr_im.reload_daemon()
|
||||||
|
|
||||||
|
# if mainsail is installed, and we installed
|
||||||
|
# multiple moonraker instances, we enable mainsails remote mode
|
||||||
|
if MainsailData().client_dir.exists() and len(mr_im.instances) > 1:
|
||||||
|
enable_mainsail_remotemode()
|
||||||
|
|
||||||
|
|
||||||
|
def check_moonraker_install_requirements() -> bool:
|
||||||
|
if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7):
|
||||||
|
Logger.print_error("Versioncheck failed!")
|
||||||
|
Logger.print_error("Python 3.7 or newer required to run Moonraker.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
kl_instance_count = len(InstanceManager(Klipper).instances)
|
||||||
|
if kl_instance_count < 1:
|
||||||
|
Logger.print_warn("Klipper not installed!")
|
||||||
|
Logger.print_warn("Moonraker cannot be installed! Install Klipper first.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def setup_moonraker_prerequesites() -> None:
|
||||||
|
cm = ConfigManager(cfg_file=KIAUH_CFG)
|
||||||
|
repo = str(
|
||||||
|
cm.get_value("moonraker", "repository_url") or DEFAULT_MOONRAKER_REPO_URL
|
||||||
|
)
|
||||||
|
branch = str(cm.get_value("moonraker", "branch") or "master")
|
||||||
|
|
||||||
|
repo_manager = RepoManager(
|
||||||
|
repo=repo,
|
||||||
|
branch=branch,
|
||||||
|
target_dir=MOONRAKER_DIR,
|
||||||
|
)
|
||||||
|
repo_manager.clone_repo()
|
||||||
|
|
||||||
|
# install moonraker dependencies and create python virtualenv
|
||||||
|
install_moonraker_packages(MOONRAKER_DIR)
|
||||||
|
create_python_venv(MOONRAKER_ENV_DIR)
|
||||||
|
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT)
|
||||||
|
|
||||||
|
|
||||||
|
def install_moonraker_packages(moonraker_dir: Path) -> None:
|
||||||
|
script = moonraker_dir.joinpath("scripts/install-moonraker.sh")
|
||||||
|
packages = parse_packages_from_file(script)
|
||||||
|
update_system_package_lists(silent=False)
|
||||||
|
install_system_packages(packages)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
cm = ConfigManager(cfg_file=KIAUH_CFG)
|
||||||
|
if cm.get_value("kiauh", "backup_before_update"):
|
||||||
|
backup_moonraker_dir()
|
||||||
|
|
||||||
|
instance_manager = InstanceManager(Moonraker)
|
||||||
|
instance_manager.stop_all_instance()
|
||||||
|
|
||||||
|
repo = str(
|
||||||
|
cm.get_value("moonraker", "repository_url") or DEFAULT_MOONRAKER_REPO_URL
|
||||||
|
)
|
||||||
|
branch = str(cm.get_value("moonraker", "branch") or "master")
|
||||||
|
|
||||||
|
repo_manager = RepoManager(
|
||||||
|
repo=repo,
|
||||||
|
branch=branch,
|
||||||
|
target_dir=MOONRAKER_DIR,
|
||||||
|
)
|
||||||
|
repo_manager.pull_repo()
|
||||||
|
|
||||||
|
# install possible new system packages
|
||||||
|
install_moonraker_packages(MOONRAKER_DIR)
|
||||||
|
# install possible new python dependencies
|
||||||
|
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT)
|
||||||
|
|
||||||
|
instance_manager.start_all_instance()
|
||||||
202
kiauh/components/moonraker/moonraker_utils.py
Normal file
202
kiauh/components/moonraker/moonraker_utils.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from typing import Dict, Literal, List, Union, Optional
|
||||||
|
|
||||||
|
from components.moonraker import (
|
||||||
|
DEFAULT_MOONRAKER_PORT,
|
||||||
|
MODULE_PATH,
|
||||||
|
MOONRAKER_DIR,
|
||||||
|
MOONRAKER_ENV_DIR,
|
||||||
|
MOONRAKER_BACKUP_DIR,
|
||||||
|
MOONRAKER_DB_BACKUP_DIR,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.base_data import BaseWebClient
|
||||||
|
from components.webui_client.client_utils import enable_mainsail_remotemode
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.repo_manager.repo_manager import RepoManager
|
||||||
|
from utils.common import get_install_status_common
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.system_utils import (
|
||||||
|
get_ipv4_addr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_moonraker_status() -> (
|
||||||
|
Dict[
|
||||||
|
Literal["status", "status_code", "instances", "repo", "local", "remote"],
|
||||||
|
Union[str, int],
|
||||||
|
]
|
||||||
|
):
|
||||||
|
status = get_install_status_common(Moonraker, MOONRAKER_DIR, MOONRAKER_ENV_DIR)
|
||||||
|
return {
|
||||||
|
"status": status.get("status"),
|
||||||
|
"status_code": status.get("status_code"),
|
||||||
|
"instances": status.get("instances"),
|
||||||
|
"repo": RepoManager.get_repo_name(MOONRAKER_DIR),
|
||||||
|
"local": RepoManager.get_local_commit(MOONRAKER_DIR),
|
||||||
|
"remote": RepoManager.get_remote_commit(MOONRAKER_DIR),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_example_moonraker_conf(
|
||||||
|
instance: Moonraker,
|
||||||
|
ports_map: Dict[str, int],
|
||||||
|
clients: Optional[List[BaseWebClient]] = None,
|
||||||
|
) -> None:
|
||||||
|
Logger.print_status(f"Creating example moonraker.conf in '{instance.cfg_dir}'")
|
||||||
|
if instance.cfg_file.is_file():
|
||||||
|
Logger.print_info(f"'{instance.cfg_file}' already exists.")
|
||||||
|
return
|
||||||
|
|
||||||
|
source = MODULE_PATH.joinpath("assets/moonraker.conf")
|
||||||
|
target = instance.cfg_file
|
||||||
|
try:
|
||||||
|
shutil.copy(source, target)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to create example moonraker.conf:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
ports = [
|
||||||
|
ports_map.get(instance)
|
||||||
|
for instance in ports_map
|
||||||
|
if ports_map.get(instance) is not None
|
||||||
|
]
|
||||||
|
if ports_map.get(instance.suffix) is None:
|
||||||
|
# this could be improved to not increment the max value of the ports list and assign it as the port
|
||||||
|
# as it can lead to situation where the port for e.g. instance moonraker-2 becomes 7128 if the port
|
||||||
|
# of moonraker-1 is 7125 and moonraker-3 is 7127 and there are moonraker.conf files for moonraker-1
|
||||||
|
# and moonraker-3 already. though, there does not seem to be a very reliable way of always assigning
|
||||||
|
# the correct port to each instance and the user will likely be required to correct the value manually.
|
||||||
|
port = max(ports) + 1 if ports else DEFAULT_MOONRAKER_PORT
|
||||||
|
else:
|
||||||
|
port = ports_map.get(instance.suffix)
|
||||||
|
|
||||||
|
ports_map[instance.suffix] = port
|
||||||
|
|
||||||
|
ip = get_ipv4_addr().split(".")[:2]
|
||||||
|
ip.extend(["0", "0/16"])
|
||||||
|
uds = instance.comms_dir.joinpath("klippy.sock")
|
||||||
|
|
||||||
|
cm = ConfigManager(target)
|
||||||
|
trusted_clients = f"\n{'.'.join(ip)}"
|
||||||
|
trusted_clients += cm.get_value("authorization", "trusted_clients")
|
||||||
|
|
||||||
|
cm.set_value("server", "port", str(port))
|
||||||
|
cm.set_value("server", "klippy_uds_address", str(uds))
|
||||||
|
cm.set_value("authorization", "trusted_clients", trusted_clients)
|
||||||
|
|
||||||
|
# add existing client and client configs in the update section
|
||||||
|
if clients is not None and len(clients) > 0:
|
||||||
|
for c in clients:
|
||||||
|
# client part
|
||||||
|
c_section = f"update_manager {c.name}"
|
||||||
|
c_options = [
|
||||||
|
("type", "web"),
|
||||||
|
("channel", "stable"),
|
||||||
|
("repo", c.repo_path),
|
||||||
|
("path", c.client_dir),
|
||||||
|
]
|
||||||
|
cm.config.add_section(section=c_section)
|
||||||
|
for option in c_options:
|
||||||
|
cm.config.set(c_section, option[0], option[1])
|
||||||
|
|
||||||
|
# client config part
|
||||||
|
c_config = c.client_config
|
||||||
|
if c_config.config_dir.exists():
|
||||||
|
c_config_section = f"update_manager {c_config.name}"
|
||||||
|
c_config_options = [
|
||||||
|
("type", "git_repo"),
|
||||||
|
("primary_branch", "master"),
|
||||||
|
("path", c_config.config_dir),
|
||||||
|
("origin", c_config.repo_url),
|
||||||
|
("managed_services", "klipper"),
|
||||||
|
]
|
||||||
|
cm.config.add_section(section=c_config_section)
|
||||||
|
for option in c_config_options:
|
||||||
|
cm.config.set(c_config_section, option[0], option[1])
|
||||||
|
|
||||||
|
cm.write_config()
|
||||||
|
Logger.print_ok(f"Example moonraker.conf created in '{instance.cfg_dir}'")
|
||||||
|
|
||||||
|
|
||||||
|
def moonraker_to_multi_conversion(new_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Converts the first instance in the List of Moonraker instances to an instance
|
||||||
|
with a new name. This method will be called when converting from a single Klipper
|
||||||
|
instance install to a multi instance install when Moonraker is also already
|
||||||
|
installed with a single instance.
|
||||||
|
:param new_name: new name the previous single instance is renamed to
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
im = InstanceManager(Moonraker)
|
||||||
|
instances: List[Moonraker] = im.instances
|
||||||
|
if not instances:
|
||||||
|
return
|
||||||
|
|
||||||
|
# in case there are multiple Moonraker instances, we don't want to do anything
|
||||||
|
if len(instances) > 1:
|
||||||
|
Logger.print_info("More than a single Moonraker instance found. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Convert Moonraker single to multi instance ...")
|
||||||
|
|
||||||
|
# remove the old single instance
|
||||||
|
im.current_instance = im.instances[0]
|
||||||
|
im.stop_instance()
|
||||||
|
im.disable_instance()
|
||||||
|
im.delete_instance()
|
||||||
|
|
||||||
|
# create a new moonraker instance with the new name
|
||||||
|
new_instance = Moonraker(suffix=new_name)
|
||||||
|
im.current_instance = new_instance
|
||||||
|
|
||||||
|
# patch the server sections klippy_uds_address value to match the new printer_data foldername
|
||||||
|
cm = ConfigManager(new_instance.cfg_file)
|
||||||
|
if cm.config.has_section("server"):
|
||||||
|
cm.set_value(
|
||||||
|
"server",
|
||||||
|
"klippy_uds_address",
|
||||||
|
str(new_instance.comms_dir.joinpath("klippy.sock")),
|
||||||
|
)
|
||||||
|
cm.write_config()
|
||||||
|
|
||||||
|
# create, enable and start the new moonraker instance
|
||||||
|
im.create_instance()
|
||||||
|
im.enable_instance()
|
||||||
|
im.start_instance()
|
||||||
|
|
||||||
|
# if mainsail is installed, we enable mainsails remote mode
|
||||||
|
if MainsailData().client_dir.exists() and len(im.instances) > 1:
|
||||||
|
enable_mainsail_remotemode()
|
||||||
|
|
||||||
|
|
||||||
|
def backup_moonraker_dir():
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory("moonraker", source=MOONRAKER_DIR, target=MOONRAKER_BACKUP_DIR)
|
||||||
|
bm.backup_directory(
|
||||||
|
"moonraker-env", source=MOONRAKER_ENV_DIR, target=MOONRAKER_BACKUP_DIR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def backup_moonraker_db_dir() -> None:
|
||||||
|
im = InstanceManager(Moonraker)
|
||||||
|
instances: List[Moonraker] = im.instances
|
||||||
|
bm = BackupManager()
|
||||||
|
|
||||||
|
for instance in instances:
|
||||||
|
name = f"database-{instance.data_dir_name}"
|
||||||
|
bm.backup_directory(
|
||||||
|
name, source=instance.db_dir, target=MOONRAKER_DB_BACKUP_DIR
|
||||||
|
)
|
||||||
0
kiauh/components/webui_client/__init__.py
Normal file
0
kiauh/components/webui_client/__init__.py
Normal file
117
kiauh/components/webui_client/base_data.py
Normal file
117
kiauh/components/webui_client/base_data.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class WebClientType(Enum):
|
||||||
|
MAINSAIL: str = "mainsail"
|
||||||
|
FLUIDD: str = "fluidd"
|
||||||
|
|
||||||
|
|
||||||
|
class WebClientConfigType(Enum):
|
||||||
|
MAINSAIL: str = "mainsail-config"
|
||||||
|
FLUIDD: str = "fluidd-config"
|
||||||
|
|
||||||
|
|
||||||
|
class BaseWebClient(ABC):
|
||||||
|
"""Base class for webclient data"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def client(self) -> WebClientType:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def display_name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def client_dir(self) -> Path:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def backup_dir(self) -> Path:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def repo_path(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def stable_url(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def unstable_url(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def client_config(self) -> BaseWebClientConfig:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class BaseWebClientConfig(ABC):
|
||||||
|
"""Base class for webclient config data"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def client_config(self) -> WebClientConfigType:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def display_name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def config_filename(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def config_dir(self) -> Path:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def backup_dir(self) -> Path:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def repo_url(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def config_section(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.base_data import BaseWebClientConfig
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.filesystem_utils import remove_file, remove_config_section
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def run_client_config_removal(
|
||||||
|
client_config: BaseWebClientConfig,
|
||||||
|
kl_instances: List[Klipper],
|
||||||
|
mr_instances: List[Moonraker],
|
||||||
|
) -> None:
|
||||||
|
remove_client_config_dir(client_config)
|
||||||
|
remove_client_config_symlink(client_config)
|
||||||
|
remove_config_section(f"update_manager {client_config.name}", mr_instances)
|
||||||
|
remove_config_section(client_config.config_section, kl_instances)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_client_config_dir(client_config: BaseWebClientConfig) -> None:
|
||||||
|
Logger.print_status(f"Removing {client_config.name} ...")
|
||||||
|
client_config_dir = client_config.config_dir
|
||||||
|
if not client_config_dir.exists():
|
||||||
|
Logger.print_info(f"'{client_config_dir}' does not exist. Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(client_config_dir)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{client_config_dir}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_client_config_symlink(client_config: BaseWebClientConfig) -> None:
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
instances: List[Klipper] = im.instances
|
||||||
|
for instance in instances:
|
||||||
|
Logger.print_status(f"Removing symlink from '{instance.cfg_dir}' ...")
|
||||||
|
symlink = instance.cfg_dir.joinpath(client_config.config_filename)
|
||||||
|
if not symlink.is_symlink():
|
||||||
|
Logger.print_info(f"'{symlink}' does not exist. Skipping ...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
remove_file(symlink)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
Logger.print_error("Failed to remove symlink!")
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.webui_client.base_data import BaseWebClient, BaseWebClientConfig
|
||||||
|
from kiauh import KIAUH_CFG
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.client_dialogs import (
|
||||||
|
print_client_already_installed_dialog,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
backup_client_config_data,
|
||||||
|
config_for_other_client_exist,
|
||||||
|
)
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.repo_manager.repo_manager import RepoManager
|
||||||
|
from utils.common import backup_printer_config_dir
|
||||||
|
from utils.filesystem_utils import (
|
||||||
|
create_symlink,
|
||||||
|
add_config_section,
|
||||||
|
add_config_section_at_top,
|
||||||
|
)
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def install_client_config(client_data: BaseWebClient) -> None:
|
||||||
|
client_config: BaseWebClientConfig = client_data.client_config
|
||||||
|
display_name = client_config.display_name
|
||||||
|
|
||||||
|
if config_for_other_client_exist(client_data.client):
|
||||||
|
Logger.print_info("Another Client-Config is already installed! Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
if client_config.config_dir.exists():
|
||||||
|
print_client_already_installed_dialog(display_name)
|
||||||
|
if get_confirm(f"Re-install {display_name}?", allow_go_back=True):
|
||||||
|
shutil.rmtree(client_config.config_dir)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances: List[Moonraker] = mr_im.instances
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
kl_instances = kl_im.instances
|
||||||
|
|
||||||
|
try:
|
||||||
|
download_client_config(client_config)
|
||||||
|
create_client_config_symlink(client_config, kl_instances)
|
||||||
|
|
||||||
|
backup_printer_config_dir()
|
||||||
|
|
||||||
|
add_config_section(
|
||||||
|
section=f"update_manager {client_config.name}",
|
||||||
|
instances=mr_instances,
|
||||||
|
options=[
|
||||||
|
("type", "git_repo"),
|
||||||
|
("primary_branch", "master"),
|
||||||
|
("path", str(client_config.config_dir)),
|
||||||
|
("origin", str(client_config.repo_url)),
|
||||||
|
("managed_services", "klipper"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
add_config_section_at_top(client_config.config_section, kl_instances)
|
||||||
|
kl_im.restart_all_instance()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"{display_name} installation failed!\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_ok(f"{display_name} installation complete!", start="\n")
|
||||||
|
|
||||||
|
|
||||||
|
def download_client_config(client_config: BaseWebClientConfig) -> None:
|
||||||
|
try:
|
||||||
|
Logger.print_status(f"Downloading {client_config.display_name} ...")
|
||||||
|
rm = RepoManager(
|
||||||
|
client_config.repo_url,
|
||||||
|
target_dir=str(client_config.config_dir),
|
||||||
|
)
|
||||||
|
rm.clone_repo()
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error(f"Downloading {client_config.display_name} failed!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def update_client_config(client: BaseWebClient) -> None:
|
||||||
|
client_config: BaseWebClientConfig = client.client_config
|
||||||
|
|
||||||
|
Logger.print_status(f"Updating {client_config.display_name} ...")
|
||||||
|
|
||||||
|
if not client_config.config_dir.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
f"Unable to update {client_config.display_name}. Directory does not exist! Skipping ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
cm = ConfigManager(cfg_file=KIAUH_CFG)
|
||||||
|
if cm.get_value("kiauh", "backup_before_update"):
|
||||||
|
backup_client_config_data(client)
|
||||||
|
|
||||||
|
repo_manager = RepoManager(
|
||||||
|
repo=client_config.repo_url,
|
||||||
|
branch="master",
|
||||||
|
target_dir=str(client_config.config_dir),
|
||||||
|
)
|
||||||
|
repo_manager.pull_repo()
|
||||||
|
|
||||||
|
Logger.print_ok(f"Successfully updated {client_config.display_name}.")
|
||||||
|
Logger.print_info("Restart Klipper to reload the configuration!")
|
||||||
|
|
||||||
|
|
||||||
|
def create_client_config_symlink(
|
||||||
|
client_config: BaseWebClientConfig, klipper_instances: List[Klipper] = None
|
||||||
|
) -> None:
|
||||||
|
if klipper_instances is None:
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
klipper_instances = kl_im.instances
|
||||||
|
|
||||||
|
Logger.print_status(f"Create symlink for {client_config.config_filename} ...")
|
||||||
|
source = Path(client_config.config_dir, client_config.config_filename)
|
||||||
|
for instance in klipper_instances:
|
||||||
|
target = instance.cfg_dir
|
||||||
|
Logger.print_status(f"Linking {source} to {target}")
|
||||||
|
try:
|
||||||
|
create_symlink(source, target)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
Logger.print_error("Creating symlink failed!")
|
||||||
108
kiauh/components/webui_client/client_dialogs.py
Normal file
108
kiauh/components/webui_client/client_dialogs.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.webui_client.base_data import BaseWebClient
|
||||||
|
from core.menus.base_menu import print_back_footer
|
||||||
|
from utils.constants import RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN
|
||||||
|
|
||||||
|
|
||||||
|
def print_moonraker_not_found_dialog():
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}No local Moonraker installation was found!{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| It is possible to install Mainsail without a local |
|
||||||
|
| Moonraker installation. If you continue, you need to |
|
||||||
|
| make sure, that Moonraker is installed on another |
|
||||||
|
| machine in your network. Otherwise Mainsail will NOT |
|
||||||
|
| work correctly. |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_client_already_installed_dialog(name: str):
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}{name} seems to be already installed!{RESET_FORMAT}"
|
||||||
|
line3 = f"If you continue, your current {name}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line3:<54}|
|
||||||
|
| installation will be overwritten. |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_client_port_select_dialog(name: str, port: str, ports_in_use: List[str]):
|
||||||
|
port = f"{COLOR_CYAN}{port}{RESET_FORMAT}"
|
||||||
|
line1 = f"Please select the port, {name} should be served on."
|
||||||
|
line2 = f"In case you need {name} to be served on a specific"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<54}|
|
||||||
|
| If you are unsure what to select, hit Enter to apply |
|
||||||
|
| the suggested value of: {port:38} |
|
||||||
|
| |
|
||||||
|
| {line2:<54}|
|
||||||
|
| port, you can set it now. Make sure the port is not |
|
||||||
|
| used by any other application on your system! |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if len(ports_in_use) > 0:
|
||||||
|
dialog += "|-------------------------------------------------------|\n"
|
||||||
|
dialog += "| The following ports were found to be in use already: |\n"
|
||||||
|
for port in ports_in_use:
|
||||||
|
port = f"{COLOR_CYAN}● {port}{RESET_FORMAT}"
|
||||||
|
dialog += f"| {port:60} |\n"
|
||||||
|
|
||||||
|
dialog += "\\=======================================================/\n"
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_install_client_config_dialog(client: BaseWebClient):
|
||||||
|
name = client.display_name
|
||||||
|
url = client.client_config.repo_url.replace(".git", "")
|
||||||
|
line1 = f"have {name} fully functional and working."
|
||||||
|
line2 = f"The recommended macros for {name} can be seen here:"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| It is recommended to use special macros in order to |
|
||||||
|
| {line1:<54}|
|
||||||
|
| |
|
||||||
|
| {line2:<54}|
|
||||||
|
| {url:<54}|
|
||||||
|
| |
|
||||||
|
| If you already use these macros skip this step. |
|
||||||
|
| Otherwise you should consider to answer with 'Y' to |
|
||||||
|
| download the recommended macros. |
|
||||||
|
\\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
75
kiauh/components/webui_client/client_remove.py
Normal file
75
kiauh/components/webui_client/client_remove.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
BaseWebClient,
|
||||||
|
WebClientType,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_config.client_config_remove import (
|
||||||
|
run_client_config_removal,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import backup_mainsail_config_json
|
||||||
|
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.filesystem_utils import (
|
||||||
|
remove_nginx_config,
|
||||||
|
remove_nginx_logs,
|
||||||
|
remove_config_section,
|
||||||
|
)
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def run_client_removal(
|
||||||
|
client: BaseWebClient,
|
||||||
|
rm_client: bool,
|
||||||
|
rm_client_config: bool,
|
||||||
|
backup_ms_config_json: bool,
|
||||||
|
) -> None:
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances: List[Moonraker] = mr_im.instances
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
kl_instances: List[Klipper] = kl_im.instances
|
||||||
|
|
||||||
|
if backup_ms_config_json and client.client == WebClientType.MAINSAIL:
|
||||||
|
backup_mainsail_config_json()
|
||||||
|
|
||||||
|
if rm_client:
|
||||||
|
client_name = client.name
|
||||||
|
remove_client_dir(client)
|
||||||
|
remove_nginx_config(client_name)
|
||||||
|
remove_nginx_logs(client_name)
|
||||||
|
|
||||||
|
section = f"update_manager {client_name}"
|
||||||
|
remove_config_section(section, mr_instances)
|
||||||
|
|
||||||
|
if rm_client_config:
|
||||||
|
run_client_config_removal(
|
||||||
|
client.client_config,
|
||||||
|
kl_instances,
|
||||||
|
mr_instances,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_client_dir(client: BaseWebClient) -> None:
|
||||||
|
Logger.print_status(f"Removing {client.display_name} ...")
|
||||||
|
client_dir = client.client_dir
|
||||||
|
if not client.client_dir.exists():
|
||||||
|
Logger.print_info(f"'{client_dir}' does not exist. Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(client_dir)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{client_dir}':\n{e}")
|
||||||
212
kiauh/components/webui_client/client_setup.py
Normal file
212
kiauh/components/webui_client/client_setup.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
WebClientType,
|
||||||
|
BaseWebClient,
|
||||||
|
BaseWebClientConfig,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
|
install_client_config,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_dialogs import (
|
||||||
|
print_moonraker_not_found_dialog,
|
||||||
|
print_client_port_select_dialog,
|
||||||
|
print_install_client_config_dialog,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
backup_mainsail_config_json,
|
||||||
|
restore_mainsail_config_json,
|
||||||
|
enable_mainsail_remotemode,
|
||||||
|
symlink_webui_nginx_log,
|
||||||
|
config_for_other_client_exist,
|
||||||
|
)
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from kiauh import KIAUH_CFG
|
||||||
|
from utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
|
||||||
|
from utils.common import check_install_dependencies
|
||||||
|
from utils.filesystem_utils import (
|
||||||
|
unzip,
|
||||||
|
copy_upstream_nginx_cfg,
|
||||||
|
copy_common_vars_nginx_cfg,
|
||||||
|
create_nginx_cfg,
|
||||||
|
create_symlink,
|
||||||
|
remove_file,
|
||||||
|
add_config_section,
|
||||||
|
read_ports_from_nginx_configs,
|
||||||
|
is_valid_port,
|
||||||
|
get_next_free_port,
|
||||||
|
)
|
||||||
|
from utils.input_utils import get_confirm, get_number_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.system_utils import (
|
||||||
|
download_file,
|
||||||
|
set_nginx_permissions,
|
||||||
|
get_ipv4_addr,
|
||||||
|
control_systemd_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_client(client: BaseWebClient) -> None:
|
||||||
|
if client is None:
|
||||||
|
raise ValueError("Missing parameter client_data!")
|
||||||
|
|
||||||
|
if client.client_dir.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
f"{client.display_name} seems to be already installed! Skipped ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances: List[Moonraker] = mr_im.instances
|
||||||
|
|
||||||
|
enable_remotemode = False
|
||||||
|
if not mr_instances:
|
||||||
|
print_moonraker_not_found_dialog()
|
||||||
|
if not get_confirm(
|
||||||
|
f"Continue {client.display_name} installation?",
|
||||||
|
allow_go_back=True,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
# if moonraker is not installed or multiple instances
|
||||||
|
# are installed we enable mainsails remote mode
|
||||||
|
if (
|
||||||
|
client.client == WebClientType.MAINSAIL
|
||||||
|
and not mr_instances
|
||||||
|
or len(mr_instances) > 1
|
||||||
|
):
|
||||||
|
enable_remotemode = True
|
||||||
|
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
kl_instances = kl_im.instances
|
||||||
|
install_client_cfg = False
|
||||||
|
client_config: BaseWebClientConfig = client.client_config
|
||||||
|
if (
|
||||||
|
kl_instances
|
||||||
|
and not client_config.config_dir.exists()
|
||||||
|
and not config_for_other_client_exist(client_to_ignore=client.client)
|
||||||
|
):
|
||||||
|
print_install_client_config_dialog(client)
|
||||||
|
question = f"Download the recommended {client_config.display_name}?"
|
||||||
|
install_client_cfg = get_confirm(question, allow_go_back=False)
|
||||||
|
|
||||||
|
cm = ConfigManager(cfg_file=KIAUH_CFG)
|
||||||
|
default_port = cm.get_value(client.name, "port")
|
||||||
|
client_port = default_port if default_port and default_port.isdigit() else "80"
|
||||||
|
ports_in_use = read_ports_from_nginx_configs()
|
||||||
|
|
||||||
|
# check if configured port is a valid number and not in use already
|
||||||
|
valid_port = is_valid_port(client_port, ports_in_use)
|
||||||
|
while not valid_port:
|
||||||
|
next_port = get_next_free_port(ports_in_use)
|
||||||
|
print_client_port_select_dialog(client.display_name, next_port, ports_in_use)
|
||||||
|
client_port = str(
|
||||||
|
get_number_input(
|
||||||
|
f"Configure {client.display_name} for port",
|
||||||
|
min_count=int(next_port),
|
||||||
|
default=next_port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
valid_port = is_valid_port(client_port, ports_in_use)
|
||||||
|
|
||||||
|
check_install_dependencies(["nginx"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
download_client(client)
|
||||||
|
if enable_remotemode and client.client == WebClientType.MAINSAIL:
|
||||||
|
enable_mainsail_remotemode()
|
||||||
|
if mr_instances:
|
||||||
|
add_config_section(
|
||||||
|
section=f"update_manager {client.name}",
|
||||||
|
instances=mr_instances,
|
||||||
|
options=[
|
||||||
|
("type", "web"),
|
||||||
|
("channel", "stable"),
|
||||||
|
("repo", str(client.repo_path)),
|
||||||
|
("path", str(client.client_dir)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
mr_im.restart_all_instance()
|
||||||
|
if install_client_cfg and kl_instances:
|
||||||
|
install_client_config(client)
|
||||||
|
|
||||||
|
copy_upstream_nginx_cfg()
|
||||||
|
copy_common_vars_nginx_cfg()
|
||||||
|
create_client_nginx_cfg(client, client_port)
|
||||||
|
if kl_instances:
|
||||||
|
symlink_webui_nginx_log(kl_instances)
|
||||||
|
control_systemd_service("nginx", "restart")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"{client.display_name} installation failed!\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{client_port}"
|
||||||
|
Logger.print_ok(f"{client.display_name} installation complete!", start="\n")
|
||||||
|
Logger.print_ok(log, prefix=False, end="\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def download_client(client: BaseWebClient) -> None:
|
||||||
|
zipfile = f"{client.name.lower()}.zip"
|
||||||
|
target = Path().home().joinpath(zipfile)
|
||||||
|
try:
|
||||||
|
Logger.print_status(f"Downloading {zipfile} ...")
|
||||||
|
download_file(client.stable_url, target, True)
|
||||||
|
Logger.print_ok("Download complete!")
|
||||||
|
|
||||||
|
Logger.print_status(f"Extracting {zipfile} ...")
|
||||||
|
unzip(target, client.client_dir)
|
||||||
|
target.unlink(missing_ok=True)
|
||||||
|
Logger.print_ok("OK!")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error(f"Downloading {zipfile} failed!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def update_client(client: BaseWebClient) -> None:
|
||||||
|
Logger.print_status(f"Updating {client.display_name} ...")
|
||||||
|
if not client.client_dir.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
f"Unable to update {client.display_name}. Directory does not exist! Skipping ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if client.client == WebClientType.MAINSAIL:
|
||||||
|
backup_mainsail_config_json(is_temp=True)
|
||||||
|
|
||||||
|
download_client(client)
|
||||||
|
|
||||||
|
if client.client == WebClientType.MAINSAIL:
|
||||||
|
restore_mainsail_config_json()
|
||||||
|
|
||||||
|
|
||||||
|
def create_client_nginx_cfg(client: BaseWebClient, port: int) -> None:
|
||||||
|
display_name = client.display_name
|
||||||
|
root_dir = client.client_dir
|
||||||
|
source = NGINX_SITES_AVAILABLE.joinpath(client.name)
|
||||||
|
target = NGINX_SITES_ENABLED.joinpath(client.name)
|
||||||
|
try:
|
||||||
|
Logger.print_status(f"Creating NGINX config for {display_name} ...")
|
||||||
|
remove_file(Path("/etc/nginx/sites-enabled/default"), True)
|
||||||
|
create_nginx_cfg(client.name, port, root_dir)
|
||||||
|
create_symlink(source, target, True)
|
||||||
|
set_nginx_permissions()
|
||||||
|
Logger.print_ok(f"NGINX config for {display_name} successfully created.")
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error(f"Creating NGINX config for {display_name} failed!")
|
||||||
|
raise
|
||||||
203
kiauh/components/webui_client/client_utils.py
Normal file
203
kiauh/components/webui_client/client_utils.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Literal, Union, get_args
|
||||||
|
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
WebClientType,
|
||||||
|
BaseWebClient,
|
||||||
|
BaseWebClientConfig,
|
||||||
|
)
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.repo_manager.repo_manager import RepoManager
|
||||||
|
from utils import NGINX_SITES_AVAILABLE, NGINX_CONFD
|
||||||
|
from utils.common import get_install_status_webui
|
||||||
|
from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW
|
||||||
|
from utils.git_utils import get_latest_tag
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_status(client: BaseWebClient) -> str:
|
||||||
|
return get_install_status_webui(
|
||||||
|
client.client_dir,
|
||||||
|
NGINX_SITES_AVAILABLE.joinpath(client.name),
|
||||||
|
NGINX_CONFD.joinpath("upstreams.conf"),
|
||||||
|
NGINX_CONFD.joinpath("common_vars.conf"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_config_status(
|
||||||
|
client: BaseWebClient,
|
||||||
|
) -> Dict[
|
||||||
|
Literal["repo", "local", "remote"],
|
||||||
|
Union[str, int],
|
||||||
|
]:
|
||||||
|
client_config = client.client_config
|
||||||
|
client_config = client_config.config_dir
|
||||||
|
|
||||||
|
return {
|
||||||
|
"repo": RepoManager.get_repo_name(client_config),
|
||||||
|
"local": RepoManager.get_local_commit(client_config),
|
||||||
|
"remote": RepoManager.get_remote_commit(client_config),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_client_config(clients: List[BaseWebClient]) -> str:
|
||||||
|
installed = []
|
||||||
|
for client in clients:
|
||||||
|
client_config = client.client_config
|
||||||
|
if client_config.config_dir.exists():
|
||||||
|
installed.append(client)
|
||||||
|
|
||||||
|
if len(installed) > 1:
|
||||||
|
return f"{COLOR_YELLOW}Conflict!{RESET_FORMAT}"
|
||||||
|
elif len(installed) == 1:
|
||||||
|
cfg = installed[0].client_config
|
||||||
|
return f"{COLOR_CYAN}{cfg.display_name}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
return f"{COLOR_CYAN}-{RESET_FORMAT}"
|
||||||
|
|
||||||
|
|
||||||
|
def backup_mainsail_config_json(is_temp=False) -> None:
|
||||||
|
c_json = MainsailData().client_dir.joinpath("config.json")
|
||||||
|
Logger.print_status(f"Backup '{c_json}' ...")
|
||||||
|
bm = BackupManager()
|
||||||
|
if is_temp:
|
||||||
|
fn = Path.home().joinpath("config.json.kiauh.bak")
|
||||||
|
bm.backup_file(c_json, custom_filename=fn)
|
||||||
|
else:
|
||||||
|
bm.backup_file(c_json)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_mainsail_config_json() -> None:
|
||||||
|
try:
|
||||||
|
c_json = MainsailData().client_dir.joinpath("config.json")
|
||||||
|
Logger.print_status(f"Restore '{c_json}' ...")
|
||||||
|
source = Path.home().joinpath("config.json.kiauh.bak")
|
||||||
|
shutil.copy(source, c_json)
|
||||||
|
except OSError:
|
||||||
|
Logger.print_info("Unable to restore config.json. Skipped ...")
|
||||||
|
|
||||||
|
|
||||||
|
def enable_mainsail_remotemode() -> None:
|
||||||
|
Logger.print_status("Enable Mainsails remote mode ...")
|
||||||
|
c_json = MainsailData().client_dir.joinpath("config.json")
|
||||||
|
with open(c_json, "r") as f:
|
||||||
|
config_data = json.load(f)
|
||||||
|
|
||||||
|
if config_data["instancesDB"] == "browser":
|
||||||
|
Logger.print_info("Remote mode already configured. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Setting instance storage location to 'browser' ...")
|
||||||
|
config_data["instancesDB"] = "browser"
|
||||||
|
|
||||||
|
with open(c_json, "w") as f:
|
||||||
|
json.dump(config_data, f, indent=4)
|
||||||
|
Logger.print_ok("Mainsails remote mode enabled!")
|
||||||
|
|
||||||
|
|
||||||
|
def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None:
|
||||||
|
Logger.print_status("Link NGINX logs into log directory ...")
|
||||||
|
access_log = Path("/var/log/nginx/mainsail-access.log")
|
||||||
|
error_log = Path("/var/log/nginx/mainsail-error.log")
|
||||||
|
|
||||||
|
for instance in klipper_instances:
|
||||||
|
desti_access = instance.log_dir.joinpath("mainsail-access.log")
|
||||||
|
if not desti_access.exists():
|
||||||
|
desti_access.symlink_to(access_log)
|
||||||
|
|
||||||
|
desti_error = instance.log_dir.joinpath("mainsail-error.log")
|
||||||
|
if not desti_error.exists():
|
||||||
|
desti_error.symlink_to(error_log)
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_client_version(client: BaseWebClient) -> str:
|
||||||
|
relinfo_file = client.client_dir.joinpath("release_info.json")
|
||||||
|
if not relinfo_file.is_file():
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
with open(relinfo_file, "r") as f:
|
||||||
|
return json.load(f)["version"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_remote_client_version(client: BaseWebClient) -> str:
|
||||||
|
try:
|
||||||
|
if (tag := get_latest_tag(client.repo_path)) != "":
|
||||||
|
return tag
|
||||||
|
return "ERROR"
|
||||||
|
except Exception:
|
||||||
|
return "ERROR"
|
||||||
|
|
||||||
|
|
||||||
|
def backup_client_data(client: BaseWebClient) -> None:
|
||||||
|
name = client.name
|
||||||
|
src = client.client_dir
|
||||||
|
dest = client.backup_dir
|
||||||
|
|
||||||
|
with open(src.joinpath(".version"), "r") as v:
|
||||||
|
version = v.readlines()[0]
|
||||||
|
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory(f"{name}-{version}", src, dest)
|
||||||
|
if name == "mainsail":
|
||||||
|
c_json = MainsailData().client_dir.joinpath("config.json")
|
||||||
|
bm.backup_file(c_json, dest)
|
||||||
|
bm.backup_file(NGINX_SITES_AVAILABLE.joinpath(name), dest)
|
||||||
|
|
||||||
|
|
||||||
|
def backup_client_config_data(client: BaseWebClient) -> None:
|
||||||
|
client_config = client.client_config
|
||||||
|
name = client_config.name
|
||||||
|
source = client_config.config_dir
|
||||||
|
target = client_config.backup_dir
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory(name, source, target)
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_clients() -> List[BaseWebClient]:
|
||||||
|
clients = list(get_args(WebClientType))
|
||||||
|
installed_clients: List[BaseWebClient] = []
|
||||||
|
for client in clients:
|
||||||
|
if client.client_dir.exists():
|
||||||
|
installed_clients.append(client)
|
||||||
|
|
||||||
|
return installed_clients
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_client_config() -> List[BaseWebClient]:
|
||||||
|
clients = list(get_args(WebClientType))
|
||||||
|
installed_client_configs: List[BaseWebClient] = []
|
||||||
|
for client in clients:
|
||||||
|
c_config_data: BaseWebClientConfig = client.client_config
|
||||||
|
if c_config_data.config_dir.exists():
|
||||||
|
installed_client_configs.append(client)
|
||||||
|
|
||||||
|
return installed_client_configs
|
||||||
|
|
||||||
|
|
||||||
|
def config_for_other_client_exist(client_to_ignore: WebClientType) -> bool:
|
||||||
|
"""
|
||||||
|
Check if any other client configs are present on the system.
|
||||||
|
It is usually not harmful, but chances are they can conflict each other.
|
||||||
|
Multiple client configs are, at least, redundant to have them installed
|
||||||
|
:param client_to_ignore: The client name to ignore for the check
|
||||||
|
:return: True, if other client configs were found, else False
|
||||||
|
"""
|
||||||
|
|
||||||
|
clients = set([c.name for c in get_existing_client_config()])
|
||||||
|
clients = clients - {client_to_ignore.value}
|
||||||
|
|
||||||
|
return True if len(clients) > 0 else False
|
||||||
65
kiauh/components/webui_client/fluidd_data.py
Normal file
65
kiauh/components/webui_client/fluidd_data.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
BaseWebClientConfig,
|
||||||
|
WebClientConfigType,
|
||||||
|
WebClientType,
|
||||||
|
BaseWebClient,
|
||||||
|
)
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
from utils.git_utils import get_latest_unstable_tag
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FluiddConfigWeb(BaseWebClientConfig):
|
||||||
|
client_config: WebClientConfigType = WebClientConfigType.FLUIDD
|
||||||
|
name: str = client_config.value
|
||||||
|
display_name: str = name.title()
|
||||||
|
config_dir: Path = Path.home().joinpath("fluidd-config")
|
||||||
|
config_filename: str = "fluidd.cfg"
|
||||||
|
config_section: str = f"include {config_filename}"
|
||||||
|
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-config-backups")
|
||||||
|
repo_url: str = "https://github.com/fluidd-core/fluidd-config.git"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FluiddData(BaseWebClient):
|
||||||
|
BASE_DL_URL = "https://github.com/fluidd-core/fluidd/releases"
|
||||||
|
|
||||||
|
client: WebClientType = WebClientType.FLUIDD
|
||||||
|
name: str = client.value
|
||||||
|
display_name: str = name.capitalize()
|
||||||
|
client_dir: Path = Path.home().joinpath("fluidd")
|
||||||
|
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups")
|
||||||
|
repo_path: str = "fluidd-core/fluidd"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stable_url(self) -> str:
|
||||||
|
return f"{self.BASE_DL_URL}/latest/download/fluidd.zip"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unstable_url(self) -> str:
|
||||||
|
try:
|
||||||
|
unstable_tag = get_latest_unstable_tag(self.repo_path)
|
||||||
|
if unstable_tag != "":
|
||||||
|
return f"{self.BASE_DL_URL}/download/{unstable_tag}/fluidd.zip"
|
||||||
|
else:
|
||||||
|
raise Exception
|
||||||
|
except Exception:
|
||||||
|
return self.stable_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_config(self) -> BaseWebClientConfig:
|
||||||
|
return FluiddConfigWeb()
|
||||||
65
kiauh/components/webui_client/mainsail_data.py
Normal file
65
kiauh/components/webui_client/mainsail_data.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
BaseWebClientConfig,
|
||||||
|
WebClientConfigType,
|
||||||
|
WebClientType,
|
||||||
|
BaseWebClient,
|
||||||
|
)
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
from utils.git_utils import get_latest_unstable_tag
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MainsailConfigWeb(BaseWebClientConfig):
|
||||||
|
client_config: WebClientConfigType = WebClientConfigType.MAINSAIL
|
||||||
|
name: str = client_config.value
|
||||||
|
display_name: str = name.title()
|
||||||
|
config_dir: Path = Path.home().joinpath("mainsail-config")
|
||||||
|
config_filename: str = "mainsail.cfg"
|
||||||
|
config_section: str = f"include {config_filename}"
|
||||||
|
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-config-backups")
|
||||||
|
repo_url: str = "https://github.com/mainsail-crew/mainsail-config.git"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MainsailData(BaseWebClient):
|
||||||
|
BASE_DL_URL: str = "https://github.com/mainsail-crew/mainsail/releases"
|
||||||
|
|
||||||
|
client: WebClientType = WebClientType.MAINSAIL
|
||||||
|
name: str = WebClientType.MAINSAIL.value
|
||||||
|
display_name: str = name.capitalize()
|
||||||
|
client_dir: Path = Path.home().joinpath("mainsail")
|
||||||
|
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-backups")
|
||||||
|
repo_path: str = "mainsail-crew/mainsail"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stable_url(self) -> str:
|
||||||
|
return f"{self.BASE_DL_URL}/latest/download/mainsail.zip"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unstable_url(self) -> str:
|
||||||
|
try:
|
||||||
|
unstable_tag = get_latest_unstable_tag(self.repo_path)
|
||||||
|
if unstable_tag != "":
|
||||||
|
return f"{self.BASE_DL_URL}/download/{unstable_tag}/mainsail.zip"
|
||||||
|
else:
|
||||||
|
raise Exception
|
||||||
|
except Exception:
|
||||||
|
return self.stable_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_config(self) -> BaseWebClientConfig:
|
||||||
|
return MainsailConfigWeb()
|
||||||
0
kiauh/components/webui_client/menus/__init__.py
Normal file
0
kiauh/components/webui_client/menus/__init__.py
Normal file
127
kiauh/components/webui_client/menus/client_remove_menu.py
Normal file
127
kiauh/components/webui_client/menus/client_remove_menu.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Dict, Type, Optional
|
||||||
|
|
||||||
|
from components.webui_client import client_remove
|
||||||
|
from components.webui_client.base_data import BaseWebClient, WebClientType
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
class ClientRemoveMenu(BaseMenu):
|
||||||
|
def __init__(
|
||||||
|
self, client: BaseWebClient, previous_menu: Optional[Type[BaseMenu]] = None
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.client = client
|
||||||
|
self.rm_client = False
|
||||||
|
self.rm_client_config = False
|
||||||
|
self.backup_mainsail_config_json = False
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.remove_menu import RemoveMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else RemoveMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> Dict[str, Option]:
|
||||||
|
options = {
|
||||||
|
"0": Option(method=self.toggle_all, menu=False),
|
||||||
|
"1": Option(method=self.toggle_rm_client, menu=False),
|
||||||
|
"2": Option(method=self.toggle_rm_client_config, menu=False),
|
||||||
|
"c": Option(method=self.run_removal_process, menu=False),
|
||||||
|
}
|
||||||
|
if self.client.client == WebClientType.MAINSAIL:
|
||||||
|
options["3"] = Option(self.toggle_backup_mainsail_config_json, False)
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
client_name = self.client.display_name
|
||||||
|
client_config = self.client.client_config
|
||||||
|
client_config_name = client_config.display_name
|
||||||
|
|
||||||
|
header = f" [ Remove {client_name} ] "
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
|
||||||
|
unchecked = "[ ]"
|
||||||
|
o1 = checked if self.rm_client else unchecked
|
||||||
|
o2 = checked if self.rm_client_config else unchecked
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Enter a number and hit enter to select / deselect |
|
||||||
|
| the specific option for removal. |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) Select everything |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 1) {o1} Remove {client_name:16} |
|
||||||
|
| 2) {o2} Remove {client_config_name:24} |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if self.client.client == WebClientType.MAINSAIL:
|
||||||
|
o3 = checked if self.backup_mainsail_config_json else unchecked
|
||||||
|
menu += textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
| 3) {o3} Backup config.json |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
menu += textwrap.dedent(
|
||||||
|
"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| C) Continue |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def toggle_all(self, **kwargs) -> None:
|
||||||
|
self.rm_client = True
|
||||||
|
self.rm_client_config = True
|
||||||
|
self.backup_mainsail_config_json = True
|
||||||
|
|
||||||
|
def toggle_rm_client(self, **kwargs) -> None:
|
||||||
|
self.rm_client = not self.rm_client
|
||||||
|
|
||||||
|
def toggle_rm_client_config(self, **kwargs) -> None:
|
||||||
|
self.rm_client_config = not self.rm_client_config
|
||||||
|
|
||||||
|
def toggle_backup_mainsail_config_json(self, **kwargs) -> None:
|
||||||
|
self.backup_mainsail_config_json = not self.backup_mainsail_config_json
|
||||||
|
|
||||||
|
def run_removal_process(self, **kwargs) -> None:
|
||||||
|
if (
|
||||||
|
not self.rm_client
|
||||||
|
and not self.rm_client_config
|
||||||
|
and not self.backup_mainsail_config_json
|
||||||
|
):
|
||||||
|
error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}"
|
||||||
|
print(error)
|
||||||
|
return
|
||||||
|
|
||||||
|
client_remove.run_client_removal(
|
||||||
|
client=self.client,
|
||||||
|
rm_client=self.rm_client,
|
||||||
|
rm_client_config=self.rm_client_config,
|
||||||
|
backup_ms_config_json=self.backup_mainsail_config_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.rm_client = False
|
||||||
|
self.rm_client_config = False
|
||||||
|
self.backup_mainsail_config_json = False
|
||||||
0
kiauh/core/__init__.py
Normal file
0
kiauh/core/__init__.py
Normal file
12
kiauh/core/backup_manager/__init__.py
Normal file
12
kiauh/core/backup_manager/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BACKUP_ROOT_DIR = Path.home().joinpath("kiauh-backups")
|
||||||
88
kiauh/core/backup_manager/backup_manager.py
Normal file
88
kiauh/core/backup_manager/backup_manager.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
from utils.common import get_current_date
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class BackupManager:
|
||||||
|
def __init__(self, backup_root_dir: Path = BACKUP_ROOT_DIR):
|
||||||
|
self._backup_root_dir = backup_root_dir
|
||||||
|
self._ignore_folders = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backup_root_dir(self) -> Path:
|
||||||
|
return self._backup_root_dir
|
||||||
|
|
||||||
|
@backup_root_dir.setter
|
||||||
|
def backup_root_dir(self, value: Path):
|
||||||
|
self._backup_root_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_folders(self) -> List[str]:
|
||||||
|
return self._ignore_folders
|
||||||
|
|
||||||
|
@ignore_folders.setter
|
||||||
|
def ignore_folders(self, value: List[str]):
|
||||||
|
self._ignore_folders = value
|
||||||
|
|
||||||
|
def backup_file(self, file: Path = None, target: Path = None, custom_filename=None):
|
||||||
|
if not file:
|
||||||
|
raise ValueError("Parameter 'file' cannot be None!")
|
||||||
|
|
||||||
|
target = self.backup_root_dir if target is None else target
|
||||||
|
|
||||||
|
Logger.print_status(f"Creating backup of {file} ...")
|
||||||
|
if Path(file).is_file():
|
||||||
|
date = get_current_date().get("date")
|
||||||
|
time = get_current_date().get("time")
|
||||||
|
filename = f"{file.stem}-{date}-{time}{file.suffix}"
|
||||||
|
filename = custom_filename if custom_filename is not None else filename
|
||||||
|
try:
|
||||||
|
Path(target).mkdir(exist_ok=True)
|
||||||
|
shutil.copyfile(file, target.joinpath(filename))
|
||||||
|
Logger.print_ok("Backup successful!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to backup '{file}':\n{e}")
|
||||||
|
else:
|
||||||
|
Logger.print_info(f"File '{file}' not found ...")
|
||||||
|
|
||||||
|
def backup_directory(self, name: str, source: Path, target: Path = None) -> None:
|
||||||
|
if source is None or not Path(source).exists():
|
||||||
|
raise OSError("Parameter 'source' is None or Path does not exist!")
|
||||||
|
|
||||||
|
target = self.backup_root_dir if target is None else target
|
||||||
|
try:
|
||||||
|
log = f"Creating backup of {name} in {target} ..."
|
||||||
|
Logger.print_status(log)
|
||||||
|
date = get_current_date().get("date")
|
||||||
|
time = get_current_date().get("time")
|
||||||
|
shutil.copytree(
|
||||||
|
source,
|
||||||
|
target.joinpath(f"{name.lower()}-{date}-{time}"),
|
||||||
|
ignore=self.ignore_folders_func,
|
||||||
|
)
|
||||||
|
Logger.print_ok("Backup successful!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to backup directory '{source}':\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
def ignore_folders_func(self, dirpath, filenames):
|
||||||
|
return (
|
||||||
|
[f for f in filenames if f in self._ignore_folders]
|
||||||
|
if self._ignore_folders is not None
|
||||||
|
else []
|
||||||
|
)
|
||||||
0
kiauh/core/config_manager/__init__.py
Normal file
0
kiauh/core/config_manager/__init__.py
Normal file
83
kiauh/core/config_manager/config_manager.py
Normal file
83
kiauh/core/config_manager/config_manager.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class ConfigManager:
|
||||||
|
def __init__(self, cfg_file: Path):
|
||||||
|
self.config_file = cfg_file
|
||||||
|
self.config = CustomConfigParser()
|
||||||
|
|
||||||
|
if cfg_file.is_file():
|
||||||
|
self.read_config()
|
||||||
|
|
||||||
|
def read_config(self) -> None:
|
||||||
|
if not self.config_file:
|
||||||
|
Logger.print_error("Unable to read config file. File not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.config.read_file(open(self.config_file, "r"))
|
||||||
|
|
||||||
|
def write_config(self) -> None:
|
||||||
|
with open(self.config_file, "w") as cfg:
|
||||||
|
self.config.write(cfg)
|
||||||
|
|
||||||
|
def get_value(self, section: str, key: str, silent=True) -> Union[str, bool, None]:
|
||||||
|
if not self.config.has_section(section):
|
||||||
|
if not silent:
|
||||||
|
log = f"Section not defined. Unable to read section: [{section}]."
|
||||||
|
Logger.print_error(log)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self.config.has_option(section, key):
|
||||||
|
if not silent:
|
||||||
|
log = f"Option not defined in section [{section}]. Unable to read option: '{key}'."
|
||||||
|
Logger.print_error(log)
|
||||||
|
return None
|
||||||
|
|
||||||
|
value = self.config.get(section, key)
|
||||||
|
if value == "True" or value == "true":
|
||||||
|
return True
|
||||||
|
elif value == "False" or value == "false":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set_value(self, section: str, key: str, value: str):
|
||||||
|
self.config.set(section, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomConfigParser(configparser.ConfigParser):
|
||||||
|
"""
|
||||||
|
A custom ConfigParser class overwriting the write() method of configparser.Configparser.
|
||||||
|
Key and value will be delimited by a ": ".
|
||||||
|
Note the whitespace AFTER the colon, which is the whole reason for that overwrite.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def write(self, fp, space_around_delimiters=False):
|
||||||
|
if self._defaults:
|
||||||
|
fp.write("[%s]\n" % configparser.DEFAULTSECT)
|
||||||
|
for key, value in self._defaults.items():
|
||||||
|
fp.write("%s: %s\n" % (key, str(value).replace("\n", "\n\t")))
|
||||||
|
fp.write("\n")
|
||||||
|
for section in self._sections:
|
||||||
|
fp.write("[%s]\n" % section)
|
||||||
|
for key, value in self._sections[section].items():
|
||||||
|
if key == "__name__":
|
||||||
|
continue
|
||||||
|
if (value is not None) or (self._optcre == self.OPTCRE):
|
||||||
|
key = ": ".join((key, str(value).replace("\n", "\n\t")))
|
||||||
|
fp.write("%s\n" % key)
|
||||||
|
fp.write("\n")
|
||||||
0
kiauh/core/instance_manager/__init__.py
Normal file
0
kiauh/core/instance_manager/__init__.py
Normal file
158
kiauh/core/instance_manager/base_instance.py
Normal file
158
kiauh/core/instance_manager/base_instance.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from abc import abstractmethod, ABC
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from utils.constants import SYSTEMD, CURRENT_USER
|
||||||
|
|
||||||
|
|
||||||
|
class BaseInstance(ABC):
|
||||||
|
@classmethod
|
||||||
|
def blacklist(cls) -> List[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
suffix: str,
|
||||||
|
instance_type: BaseInstance,
|
||||||
|
):
|
||||||
|
self._instance_type = instance_type
|
||||||
|
self._suffix = suffix
|
||||||
|
self._user = CURRENT_USER
|
||||||
|
self._data_dir_name = self.get_data_dir_name_from_suffix()
|
||||||
|
self._data_dir = Path.home().joinpath(f"{self._data_dir_name}_data")
|
||||||
|
self._cfg_dir = self.data_dir.joinpath("config")
|
||||||
|
self._log_dir = self.data_dir.joinpath("logs")
|
||||||
|
self._comms_dir = self.data_dir.joinpath("comms")
|
||||||
|
self._sysd_dir = self.data_dir.joinpath("systemd")
|
||||||
|
self._gcodes_dir = self.data_dir.joinpath("gcodes")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_type(self) -> BaseInstance:
|
||||||
|
return self._instance_type
|
||||||
|
|
||||||
|
@instance_type.setter
|
||||||
|
def instance_type(self, value: BaseInstance) -> None:
|
||||||
|
self._instance_type = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suffix(self) -> str:
|
||||||
|
return self._suffix
|
||||||
|
|
||||||
|
@suffix.setter
|
||||||
|
def suffix(self, value: str) -> None:
|
||||||
|
self._suffix = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user(self) -> str:
|
||||||
|
return self._user
|
||||||
|
|
||||||
|
@user.setter
|
||||||
|
def user(self, value: str) -> None:
|
||||||
|
self._user = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_dir_name(self) -> str:
|
||||||
|
return self._data_dir_name
|
||||||
|
|
||||||
|
@data_dir_name.setter
|
||||||
|
def data_dir_name(self, value: str) -> None:
|
||||||
|
self._data_dir_name = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_dir(self) -> Path:
|
||||||
|
return self._data_dir
|
||||||
|
|
||||||
|
@data_dir.setter
|
||||||
|
def data_dir(self, value: Path) -> None:
|
||||||
|
self._data_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cfg_dir(self) -> Path:
|
||||||
|
return self._cfg_dir
|
||||||
|
|
||||||
|
@cfg_dir.setter
|
||||||
|
def cfg_dir(self, value: Path) -> None:
|
||||||
|
self._cfg_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_dir(self) -> Path:
|
||||||
|
return self._log_dir
|
||||||
|
|
||||||
|
@log_dir.setter
|
||||||
|
def log_dir(self, value: Path) -> None:
|
||||||
|
self._log_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comms_dir(self) -> Path:
|
||||||
|
return self._comms_dir
|
||||||
|
|
||||||
|
@comms_dir.setter
|
||||||
|
def comms_dir(self, value: Path) -> None:
|
||||||
|
self._comms_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sysd_dir(self) -> Path:
|
||||||
|
return self._sysd_dir
|
||||||
|
|
||||||
|
@sysd_dir.setter
|
||||||
|
def sysd_dir(self, value: Path) -> None:
|
||||||
|
self._sysd_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gcodes_dir(self) -> Path:
|
||||||
|
return self._gcodes_dir
|
||||||
|
|
||||||
|
@gcodes_dir.setter
|
||||||
|
def gcodes_dir(self, value: Path) -> None:
|
||||||
|
self._gcodes_dir = value
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create(self) -> None:
|
||||||
|
raise NotImplementedError("Subclasses must implement the create method")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete(self) -> None:
|
||||||
|
raise NotImplementedError("Subclasses must implement the delete method")
|
||||||
|
|
||||||
|
def create_folders(self, add_dirs: Optional[List[Path]] = None) -> None:
|
||||||
|
dirs = [
|
||||||
|
self.data_dir,
|
||||||
|
self.cfg_dir,
|
||||||
|
self.log_dir,
|
||||||
|
self.comms_dir,
|
||||||
|
self.sysd_dir,
|
||||||
|
]
|
||||||
|
|
||||||
|
if add_dirs:
|
||||||
|
dirs.extend(add_dirs)
|
||||||
|
|
||||||
|
for _dir in dirs:
|
||||||
|
_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def get_service_file_name(self, extension: bool = False) -> str:
|
||||||
|
name = f"{self.__class__.__name__.lower()}"
|
||||||
|
if self.suffix != "":
|
||||||
|
name += f"-{self.suffix}"
|
||||||
|
|
||||||
|
return name if not extension else f"{name}.service"
|
||||||
|
|
||||||
|
def get_service_file_path(self) -> Path:
|
||||||
|
return SYSTEMD.joinpath(self.get_service_file_name(extension=True))
|
||||||
|
|
||||||
|
def get_data_dir_name_from_suffix(self) -> str:
|
||||||
|
if self._suffix == "":
|
||||||
|
return "printer"
|
||||||
|
elif self._suffix.isdigit():
|
||||||
|
return f"printer_{self._suffix}"
|
||||||
|
else:
|
||||||
|
return self._suffix
|
||||||
231
kiauh/core/instance_manager/instance_manager.py
Normal file
231
kiauh/core/instance_manager/instance_manager.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Union, TypeVar
|
||||||
|
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from utils.constants import SYSTEMD
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
T = TypeVar(name="T", bound=BaseInstance, covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class InstanceManager:
|
||||||
|
def __init__(self, instance_type: T) -> None:
|
||||||
|
self._instance_type = instance_type
|
||||||
|
self._current_instance: Optional[T] = None
|
||||||
|
self._instance_suffix: Optional[str] = None
|
||||||
|
self._instance_service: Optional[str] = None
|
||||||
|
self._instance_service_full: Optional[str] = None
|
||||||
|
self._instance_service_path: Optional[str] = None
|
||||||
|
self._instances: List[T] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_type(self) -> T:
|
||||||
|
return self._instance_type
|
||||||
|
|
||||||
|
@instance_type.setter
|
||||||
|
def instance_type(self, value: T):
|
||||||
|
self._instance_type = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_instance(self) -> T:
|
||||||
|
return self._current_instance
|
||||||
|
|
||||||
|
@current_instance.setter
|
||||||
|
def current_instance(self, value: T) -> None:
|
||||||
|
self._current_instance = value
|
||||||
|
self.instance_suffix = value.suffix
|
||||||
|
self.instance_service = value.get_service_file_name()
|
||||||
|
self.instance_service_path = value.get_service_file_path()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_suffix(self) -> str:
|
||||||
|
return self._instance_suffix
|
||||||
|
|
||||||
|
@instance_suffix.setter
|
||||||
|
def instance_suffix(self, value: str):
|
||||||
|
self._instance_suffix = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_service(self) -> str:
|
||||||
|
return self._instance_service
|
||||||
|
|
||||||
|
@instance_service.setter
|
||||||
|
def instance_service(self, value: str):
|
||||||
|
self._instance_service = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_service_full(self) -> str:
|
||||||
|
return f"{self._instance_service}.service"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_service_path(self) -> str:
|
||||||
|
return self._instance_service_path
|
||||||
|
|
||||||
|
@instance_service_path.setter
|
||||||
|
def instance_service_path(self, value: str):
|
||||||
|
self._instance_service_path = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instances(self) -> List[T]:
|
||||||
|
return self.find_instances()
|
||||||
|
|
||||||
|
@instances.setter
|
||||||
|
def instances(self, value: List[T]):
|
||||||
|
self._instances = value
|
||||||
|
|
||||||
|
def create_instance(self) -> None:
|
||||||
|
if self.current_instance is not None:
|
||||||
|
try:
|
||||||
|
self.current_instance.create()
|
||||||
|
except (OSError, subprocess.CalledProcessError) as e:
|
||||||
|
Logger.print_error(f"Creating instance failed: {e}")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise ValueError("current_instance cannot be None")
|
||||||
|
|
||||||
|
def delete_instance(self) -> None:
|
||||||
|
if self.current_instance is not None:
|
||||||
|
try:
|
||||||
|
self.current_instance.delete()
|
||||||
|
except (OSError, subprocess.CalledProcessError) as e:
|
||||||
|
Logger.print_error(f"Removing instance failed: {e}")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise ValueError("current_instance cannot be None")
|
||||||
|
|
||||||
|
def enable_instance(self) -> None:
|
||||||
|
Logger.print_status(f"Enabling {self.instance_service_full} ...")
|
||||||
|
try:
|
||||||
|
command = [
|
||||||
|
"sudo",
|
||||||
|
"systemctl",
|
||||||
|
"enable",
|
||||||
|
self.instance_service_full,
|
||||||
|
]
|
||||||
|
if subprocess.run(command, check=True):
|
||||||
|
Logger.print_ok(f"{self.instance_service_full} enabled.")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error enabling service {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
|
||||||
|
def disable_instance(self) -> None:
|
||||||
|
Logger.print_status(f"Disabling {self.instance_service_full} ...")
|
||||||
|
try:
|
||||||
|
command = [
|
||||||
|
"sudo",
|
||||||
|
"systemctl",
|
||||||
|
"disable",
|
||||||
|
self.instance_service_full,
|
||||||
|
]
|
||||||
|
if subprocess.run(command, check=True):
|
||||||
|
Logger.print_ok(f"{self.instance_service_full} disabled.")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error disabling {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
|
||||||
|
def start_instance(self) -> None:
|
||||||
|
Logger.print_status(f"Starting {self.instance_service_full} ...")
|
||||||
|
try:
|
||||||
|
command = [
|
||||||
|
"sudo",
|
||||||
|
"systemctl",
|
||||||
|
"start",
|
||||||
|
self.instance_service_full,
|
||||||
|
]
|
||||||
|
if subprocess.run(command, check=True):
|
||||||
|
Logger.print_ok(f"{self.instance_service_full} started.")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error starting {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
|
||||||
|
def restart_instance(self) -> None:
|
||||||
|
Logger.print_status(f"Restarting {self.instance_service_full} ...")
|
||||||
|
try:
|
||||||
|
command = [
|
||||||
|
"sudo",
|
||||||
|
"systemctl",
|
||||||
|
"restart",
|
||||||
|
self.instance_service_full,
|
||||||
|
]
|
||||||
|
if subprocess.run(command, check=True):
|
||||||
|
Logger.print_ok(f"{self.instance_service_full} restarted.")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error restarting {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
|
||||||
|
def start_all_instance(self) -> None:
|
||||||
|
for instance in self.instances:
|
||||||
|
self.current_instance = instance
|
||||||
|
self.start_instance()
|
||||||
|
|
||||||
|
def restart_all_instance(self) -> None:
|
||||||
|
for instance in self.instances:
|
||||||
|
self.current_instance = instance
|
||||||
|
self.restart_instance()
|
||||||
|
|
||||||
|
def stop_instance(self) -> None:
|
||||||
|
Logger.print_status(f"Stopping {self.instance_service_full} ...")
|
||||||
|
try:
|
||||||
|
command = ["sudo", "systemctl", "stop", self.instance_service_full]
|
||||||
|
if subprocess.run(command, check=True):
|
||||||
|
Logger.print_ok(f"{self.instance_service_full} stopped.")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error stopping {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def stop_all_instance(self) -> None:
|
||||||
|
for instance in self.instances:
|
||||||
|
self.current_instance = instance
|
||||||
|
self.stop_instance()
|
||||||
|
|
||||||
|
def reload_daemon(self) -> None:
|
||||||
|
Logger.print_status("Reloading systemd manager configuration ...")
|
||||||
|
try:
|
||||||
|
command = ["sudo", "systemctl", "daemon-reload"]
|
||||||
|
if subprocess.run(command, check=True):
|
||||||
|
Logger.print_ok("Systemd manager configuration reloaded")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error("Error reloading systemd manager configuration:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def find_instances(self) -> List[T]:
|
||||||
|
name = self.instance_type.__name__.lower()
|
||||||
|
pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.service$")
|
||||||
|
excluded = self.instance_type.blacklist()
|
||||||
|
|
||||||
|
service_list = [
|
||||||
|
Path(SYSTEMD, service)
|
||||||
|
for service in SYSTEMD.iterdir()
|
||||||
|
if pattern.search(service.name)
|
||||||
|
and not any(s in service.name for s in excluded)
|
||||||
|
]
|
||||||
|
|
||||||
|
instance_list = [
|
||||||
|
self.instance_type(suffix=self._get_instance_suffix(service))
|
||||||
|
for service in service_list
|
||||||
|
]
|
||||||
|
|
||||||
|
return sorted(instance_list, key=lambda x: self._sort_instance_list(x.suffix))
|
||||||
|
|
||||||
|
def _get_instance_suffix(self, file_path: Path) -> str:
|
||||||
|
return file_path.stem.split("-")[-1] if "-" in file_path.stem else ""
|
||||||
|
|
||||||
|
def _sort_instance_list(self, s: Union[int, str, None]):
|
||||||
|
if s is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
return int(s) if s.isdigit() else s
|
||||||
8
kiauh/core/instance_manager/name_scheme.py
Normal file
8
kiauh/core/instance_manager/name_scheme.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from enum import unique, Enum
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class NameScheme(Enum):
|
||||||
|
SINGLE = "SINGLE"
|
||||||
|
INDEX = "INDEX"
|
||||||
|
CUSTOM = "CUSTOM"
|
||||||
35
kiauh/core/menus/__init__.py
Normal file
35
kiauh/core/menus/__init__.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Callable, Any, Union
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Option:
|
||||||
|
"""
|
||||||
|
Represents a menu option.
|
||||||
|
:param method: Method that will be used to call the menu option
|
||||||
|
:param menu: Flag for singaling that another menu will be opened
|
||||||
|
:param opt_index: Can be used to pass the user input to the menu option
|
||||||
|
:param opt_data: Can be used to pass any additional data to the menu option
|
||||||
|
"""
|
||||||
|
|
||||||
|
method: Union[Callable, None] = None
|
||||||
|
menu: bool = False
|
||||||
|
opt_index: str = ""
|
||||||
|
opt_data: Any = None
|
||||||
|
|
||||||
|
|
||||||
|
class FooterType(Enum):
|
||||||
|
QUIT = "QUIT"
|
||||||
|
BACK = "BACK"
|
||||||
|
BACK_HELP = "BACK_HELP"
|
||||||
|
BLANK = "BLANK"
|
||||||
96
kiauh/core/menus/advanced_menu.py
Normal file
96
kiauh/core/menus/advanced_menu.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper_firmware.menus.klipper_build_menu import (
|
||||||
|
KlipperBuildFirmwareMenu,
|
||||||
|
)
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_menu import (
|
||||||
|
KlipperFlashMethodMenu,
|
||||||
|
KlipperSelectMcuConnectionMenu,
|
||||||
|
)
|
||||||
|
from components.moonraker import MOONRAKER_DIR
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_YELLOW, RESET_FORMAT
|
||||||
|
from utils.git_utils import rollback_repository
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class AdvancedMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self):
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.klipper_rollback, menu=True),
|
||||||
|
"2": Option(method=self.moonraker_rollback, menu=True),
|
||||||
|
"3": Option(method=self.build, menu=True),
|
||||||
|
"4": Option(method=self.flash, menu=False),
|
||||||
|
"5": Option(method=self.build_flash, menu=False),
|
||||||
|
"6": Option(method=self.get_id, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Advanced Menu ] "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Repo Rollback: |
|
||||||
|
| 1) [Klipper] |
|
||||||
|
| 2) [Moonraker] |
|
||||||
|
| |
|
||||||
|
| Klipper Firmware: |
|
||||||
|
| 3) [Build] |
|
||||||
|
| 4) [Flash] |
|
||||||
|
| 5) [Build + Flash] |
|
||||||
|
| 6) [Get MCU ID] |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def klipper_rollback(self, **kwargs):
|
||||||
|
rollback_repository(KLIPPER_DIR, Klipper)
|
||||||
|
|
||||||
|
def moonraker_rollback(self, **kwargs):
|
||||||
|
rollback_repository(MOONRAKER_DIR, Moonraker)
|
||||||
|
|
||||||
|
def build(self, **kwargs):
|
||||||
|
KlipperBuildFirmwareMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def flash(self, **kwargs):
|
||||||
|
KlipperFlashMethodMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def build_flash(self, **kwargs):
|
||||||
|
KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run()
|
||||||
|
KlipperFlashMethodMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def get_id(self, **kwargs):
|
||||||
|
KlipperSelectMcuConnectionMenu(
|
||||||
|
previous_menu=self.__class__,
|
||||||
|
standalone=True,
|
||||||
|
).run()
|
||||||
107
kiauh/core/menus/backup_menu.py
Normal file
107
kiauh/core/menus/backup_menu.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.klipper.klipper_utils import backup_klipper_dir
|
||||||
|
from components.moonraker.moonraker_utils import (
|
||||||
|
backup_moonraker_dir,
|
||||||
|
backup_moonraker_db_dir,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
backup_client_data,
|
||||||
|
backup_client_config_data,
|
||||||
|
)
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.common import backup_printer_config_dir
|
||||||
|
from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class BackupMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.backup_klipper, menu=False),
|
||||||
|
"2": Option(method=self.backup_moonraker, menu=False),
|
||||||
|
"3": Option(method=self.backup_printer_config, menu=False),
|
||||||
|
"4": Option(method=self.backup_moonraker_db, menu=False),
|
||||||
|
"5": Option(method=self.backup_mainsail, menu=False),
|
||||||
|
"6": Option(method=self.backup_fluidd, menu=False),
|
||||||
|
"7": Option(method=self.backup_mainsail_config, menu=False),
|
||||||
|
"8": Option(method=self.backup_fluidd_config, menu=False),
|
||||||
|
"9": Option(method=self.backup_klipperscreen, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Backup Menu ] "
|
||||||
|
line1 = f"{COLOR_YELLOW}INFO: Backups are located in '~/kiauh-backups'{RESET_FORMAT}"
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line1:^62} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Klipper & Moonraker API: | Client-Config: |
|
||||||
|
| 1) [Klipper] | 7) [Mainsail-Config] |
|
||||||
|
| 2) [Moonraker] | 8) [Fluidd-Config] |
|
||||||
|
| 3) [Config Folder] | |
|
||||||
|
| 4) [Moonraker Database] | Touchscreen GUI: |
|
||||||
|
| | 9) [KlipperScreen] |
|
||||||
|
| Webinterface: | |
|
||||||
|
| 5) [Mainsail] | |
|
||||||
|
| 6) [Fluidd] | |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def backup_klipper(self, **kwargs):
|
||||||
|
backup_klipper_dir()
|
||||||
|
|
||||||
|
def backup_moonraker(self, **kwargs):
|
||||||
|
backup_moonraker_dir()
|
||||||
|
|
||||||
|
def backup_printer_config(self, **kwargs):
|
||||||
|
backup_printer_config_dir()
|
||||||
|
|
||||||
|
def backup_moonraker_db(self, **kwargs):
|
||||||
|
backup_moonraker_db_dir()
|
||||||
|
|
||||||
|
def backup_mainsail(self, **kwargs):
|
||||||
|
backup_client_data(MainsailData())
|
||||||
|
|
||||||
|
def backup_fluidd(self, **kwargs):
|
||||||
|
backup_client_data(FluiddData())
|
||||||
|
|
||||||
|
def backup_mainsail_config(self, **kwargs):
|
||||||
|
backup_client_config_data(MainsailData())
|
||||||
|
|
||||||
|
def backup_fluidd_config(self, **kwargs):
|
||||||
|
backup_client_config_data(FluiddData())
|
||||||
|
|
||||||
|
def backup_klipperscreen(self, **kwargs):
|
||||||
|
pass
|
||||||
216
kiauh/core/menus/base_menu.py
Normal file
216
kiauh/core/menus/base_menu.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import Type, Dict, Optional
|
||||||
|
|
||||||
|
from core.menus import FooterType, Option
|
||||||
|
from utils.constants import (
|
||||||
|
COLOR_GREEN,
|
||||||
|
COLOR_YELLOW,
|
||||||
|
COLOR_RED,
|
||||||
|
COLOR_CYAN,
|
||||||
|
RESET_FORMAT,
|
||||||
|
)
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def clear():
|
||||||
|
subprocess.call("clear", shell=True)
|
||||||
|
|
||||||
|
|
||||||
|
def print_header():
|
||||||
|
line1 = " [ KIAUH ] "
|
||||||
|
line2 = "Klipper Installation And Update Helper"
|
||||||
|
line3 = ""
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
header = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{line1:~^{count}}{RESET_FORMAT} |
|
||||||
|
| {color}{line2:^{count}}{RESET_FORMAT} |
|
||||||
|
| {color}{line3:~^{count}}{RESET_FORMAT} |
|
||||||
|
\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(header, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_quit_footer():
|
||||||
|
text = "Q) Quit"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
footer = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {color}{text:^{count}}{RESET_FORMAT} |
|
||||||
|
\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(footer, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_back_footer():
|
||||||
|
text = "B) « Back"
|
||||||
|
color = COLOR_GREEN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
footer = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {color}{text:^{count}}{RESET_FORMAT} |
|
||||||
|
\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(footer, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_back_help_footer():
|
||||||
|
text1 = "B) « Back"
|
||||||
|
text2 = "H) Help [?]"
|
||||||
|
color1 = COLOR_GREEN
|
||||||
|
color2 = COLOR_YELLOW
|
||||||
|
count = 34 - len(color1) - len(RESET_FORMAT)
|
||||||
|
footer = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {color1}{text1:^{count}}{RESET_FORMAT} | {color2}{text2:^{count}}{RESET_FORMAT} |
|
||||||
|
\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(footer, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_blank_footer():
|
||||||
|
print("\=======================================================/")
|
||||||
|
|
||||||
|
|
||||||
|
class PostInitCaller(type):
|
||||||
|
def __call__(cls, *args, **kwargs):
|
||||||
|
obj = type.__call__(cls, *args, **kwargs)
|
||||||
|
obj.__post_init__()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class BaseMenu(metaclass=PostInitCaller):
|
||||||
|
options: Dict[str, Option] = {}
|
||||||
|
options_offset: int = 0
|
||||||
|
default_option: Option = None
|
||||||
|
input_label_txt: str = "Perform action"
|
||||||
|
header: bool = False
|
||||||
|
previous_menu: Type[BaseMenu] = None
|
||||||
|
help_menu: Type[BaseMenu] = None
|
||||||
|
footer_type: FooterType = FooterType.BACK
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
if type(self) is BaseMenu:
|
||||||
|
raise NotImplementedError("BaseMenu cannot be instantiated directly.")
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.set_previous_menu(self.previous_menu)
|
||||||
|
self.set_options()
|
||||||
|
|
||||||
|
# conditionally add options based on footer type
|
||||||
|
if self.footer_type is FooterType.QUIT:
|
||||||
|
self.options["q"] = Option(method=self.__exit, menu=False)
|
||||||
|
if self.footer_type is FooterType.BACK:
|
||||||
|
self.options["b"] = Option(method=self.__go_back, menu=False)
|
||||||
|
if self.footer_type is FooterType.BACK_HELP:
|
||||||
|
self.options["b"] = Option(method=self.__go_back, menu=False)
|
||||||
|
self.options["h"] = Option(method=self.__go_to_help, menu=False)
|
||||||
|
# if defined, add the default option to the options dict
|
||||||
|
if self.default_option is not None:
|
||||||
|
self.options[""] = self.default_option
|
||||||
|
|
||||||
|
def __go_back(self, **kwargs):
|
||||||
|
self.previous_menu().run()
|
||||||
|
|
||||||
|
def __go_to_help(self, **kwargs):
|
||||||
|
self.help_menu(previous_menu=self).run()
|
||||||
|
|
||||||
|
def __exit(self, **kwargs):
|
||||||
|
Logger.print_ok("###### Happy printing!", False)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_options(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def print_footer(self) -> None:
|
||||||
|
if self.footer_type is FooterType.QUIT:
|
||||||
|
print_quit_footer()
|
||||||
|
elif self.footer_type is FooterType.BACK:
|
||||||
|
print_back_footer()
|
||||||
|
elif self.footer_type is FooterType.BACK_HELP:
|
||||||
|
print_back_help_footer()
|
||||||
|
elif self.footer_type is FooterType.BLANK:
|
||||||
|
print_blank_footer()
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def display_menu(self) -> None:
|
||||||
|
if self.header:
|
||||||
|
print_header()
|
||||||
|
self.print_menu()
|
||||||
|
self.print_footer()
|
||||||
|
|
||||||
|
def validate_user_input(self, usr_input: str) -> Option:
|
||||||
|
"""
|
||||||
|
Validate the user input and either return an Option, a string or None
|
||||||
|
:param usr_input: The user input in form of a string
|
||||||
|
:return: Option, str or None
|
||||||
|
"""
|
||||||
|
usr_input = usr_input.lower()
|
||||||
|
option = self.options.get(usr_input, Option(None, False, "", None))
|
||||||
|
|
||||||
|
# if option/usr_input is None/empty string, we execute the menus default option if specified
|
||||||
|
if (option is None or usr_input == "") and self.default_option is not None:
|
||||||
|
self.default_option.opt_index = usr_input
|
||||||
|
return self.default_option
|
||||||
|
|
||||||
|
# user selected a regular option
|
||||||
|
option.opt_index = usr_input
|
||||||
|
return option
|
||||||
|
|
||||||
|
def handle_user_input(self) -> Option:
|
||||||
|
"""Handle the user input, return the validated input or print an error."""
|
||||||
|
while True:
|
||||||
|
print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="")
|
||||||
|
usr_input = input().lower()
|
||||||
|
validated_input = self.validate_user_input(usr_input)
|
||||||
|
|
||||||
|
if validated_input.method is not None:
|
||||||
|
return validated_input
|
||||||
|
else:
|
||||||
|
Logger.print_error("Invalid input!", False)
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""Start the menu lifecycle. When this function returns, the lifecycle of the menu ends."""
|
||||||
|
try:
|
||||||
|
self.display_menu()
|
||||||
|
option = self.handle_user_input()
|
||||||
|
option.method(opt_index=option.opt_index, opt_data=option.opt_data)
|
||||||
|
self.run()
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"An unexpected error occured:\n{e}")
|
||||||
90
kiauh/core/menus/install_menu.py
Normal file
90
kiauh/core/menus/install_menu.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.klipper import klipper_setup
|
||||||
|
from components.moonraker import moonraker_setup
|
||||||
|
from components.webui_client import client_setup
|
||||||
|
from components.webui_client.client_config import client_config_setup
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.menus import Option
|
||||||
|
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_GREEN, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class InstallMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.install_klipper, menu=False),
|
||||||
|
"2": Option(method=self.install_moonraker, menu=False),
|
||||||
|
"3": Option(method=self.install_mainsail, menu=False),
|
||||||
|
"4": Option(method=self.install_fluidd, menu=False),
|
||||||
|
"5": Option(method=self.install_mainsail_config, menu=False),
|
||||||
|
"6": Option(method=self.install_fluidd_config, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Installation Menu ] "
|
||||||
|
color = COLOR_GREEN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Firmware & API: | Touchscreen GUI: |
|
||||||
|
| 1) [Klipper] | 7) [KlipperScreen] |
|
||||||
|
| 2) [Moonraker] | |
|
||||||
|
| | Android / iOS: |
|
||||||
|
| Webinterface: | 8) [Mobileraker] |
|
||||||
|
| 3) [Mainsail] | |
|
||||||
|
| 4) [Fluidd] | Webcam Streamer: |
|
||||||
|
| | 9) [Crowsnest] |
|
||||||
|
| Client-Config: | |
|
||||||
|
| 5) [Mainsail-Config] | |
|
||||||
|
| 6) [Fluidd-Config] | |
|
||||||
|
| | |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def install_klipper(self, **kwargs):
|
||||||
|
klipper_setup.install_klipper()
|
||||||
|
|
||||||
|
def install_moonraker(self, **kwargs):
|
||||||
|
moonraker_setup.install_moonraker()
|
||||||
|
|
||||||
|
def install_mainsail(self, **kwargs):
|
||||||
|
client_setup.install_client(MainsailData())
|
||||||
|
|
||||||
|
def install_mainsail_config(self, **kwargs):
|
||||||
|
client_config_setup.install_client_config(MainsailData())
|
||||||
|
|
||||||
|
def install_fluidd(self, **kwargs):
|
||||||
|
client_setup.install_client(FluiddData())
|
||||||
|
|
||||||
|
def install_fluidd_config(self, **kwargs):
|
||||||
|
client_config_setup.install_client_config(FluiddData())
|
||||||
171
kiauh/core/menus/main_menu.py
Normal file
171
kiauh/core/menus/main_menu.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.klipper.klipper_utils import get_klipper_status
|
||||||
|
from components.log_uploads.menus.log_upload_menu import LogUploadMenu
|
||||||
|
from components.moonraker.moonraker_utils import get_moonraker_status
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
get_client_status,
|
||||||
|
get_current_client_config,
|
||||||
|
)
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.menus import FooterType
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
from core.menus.backup_menu import BackupMenu
|
||||||
|
from core.menus.base_menu import BaseMenu, Option
|
||||||
|
from extensions.extensions_menu import ExtensionsMenu
|
||||||
|
from core.menus.install_menu import InstallMenu
|
||||||
|
from core.menus.remove_menu import RemoveMenu
|
||||||
|
from core.menus.settings_menu import SettingsMenu
|
||||||
|
from core.menus.update_menu import UpdateMenu
|
||||||
|
from utils.constants import (
|
||||||
|
COLOR_MAGENTA,
|
||||||
|
COLOR_CYAN,
|
||||||
|
RESET_FORMAT,
|
||||||
|
COLOR_RED,
|
||||||
|
COLOR_GREEN,
|
||||||
|
COLOR_YELLOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class MainMenu(BaseMenu):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.header = True
|
||||||
|
self.footer_type = FooterType.QUIT
|
||||||
|
|
||||||
|
self.kl_status = ""
|
||||||
|
self.kl_repo = ""
|
||||||
|
self.mr_status = ""
|
||||||
|
self.mr_repo = ""
|
||||||
|
self.ms_status = ""
|
||||||
|
self.fl_status = ""
|
||||||
|
self.ks_status = ""
|
||||||
|
self.mb_status = ""
|
||||||
|
self.cn_status = ""
|
||||||
|
self.cc_status = ""
|
||||||
|
self.init_status()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
"""MainMenu does not have a previous menu"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"0": Option(method=self.log_upload_menu, menu=True),
|
||||||
|
"1": Option(method=self.install_menu, menu=True),
|
||||||
|
"2": Option(method=self.update_menu, menu=True),
|
||||||
|
"3": Option(method=self.remove_menu, menu=True),
|
||||||
|
"4": Option(method=self.advanced_menu, menu=True),
|
||||||
|
"5": Option(method=self.backup_menu, menu=True),
|
||||||
|
"e": Option(method=self.extension_menu, menu=True),
|
||||||
|
"s": Option(method=self.settings_menu, menu=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
def init_status(self) -> None:
|
||||||
|
status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn"]
|
||||||
|
for var in status_vars:
|
||||||
|
setattr(
|
||||||
|
self,
|
||||||
|
f"{var}_status",
|
||||||
|
f"{COLOR_RED}Not installed!{RESET_FORMAT}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def fetch_status(self) -> None:
|
||||||
|
# klipper
|
||||||
|
klipper_status = get_klipper_status()
|
||||||
|
kl_status = klipper_status.get("status")
|
||||||
|
kl_code = klipper_status.get("status_code")
|
||||||
|
kl_instances = f" {klipper_status.get('instances')}" if kl_code == 1 else ""
|
||||||
|
self.kl_status = self.format_status_by_code(kl_code, kl_status, kl_instances)
|
||||||
|
self.kl_repo = f"{COLOR_CYAN}{klipper_status.get('repo')}{RESET_FORMAT}"
|
||||||
|
# moonraker
|
||||||
|
moonraker_status = get_moonraker_status()
|
||||||
|
mr_status = moonraker_status.get("status")
|
||||||
|
mr_code = moonraker_status.get("status_code")
|
||||||
|
mr_instances = f" {moonraker_status.get('instances')}" if mr_code == 1 else ""
|
||||||
|
self.mr_status = self.format_status_by_code(mr_code, mr_status, mr_instances)
|
||||||
|
self.mr_repo = f"{COLOR_CYAN}{moonraker_status.get('repo')}{RESET_FORMAT}"
|
||||||
|
# mainsail
|
||||||
|
self.ms_status = get_client_status(MainsailData())
|
||||||
|
# fluidd
|
||||||
|
self.fl_status = get_client_status(FluiddData())
|
||||||
|
# client-config
|
||||||
|
self.cc_status = get_current_client_config([MainsailData(), FluiddData()])
|
||||||
|
|
||||||
|
def format_status_by_code(self, code: int, status: str, count: str) -> str:
|
||||||
|
if code == 1:
|
||||||
|
return f"{COLOR_GREEN}{status}{count}{RESET_FORMAT}"
|
||||||
|
elif code == 2:
|
||||||
|
return f"{COLOR_RED}{status}{count}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
return f"{COLOR_YELLOW}{status}{count}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
self.fetch_status()
|
||||||
|
|
||||||
|
header = " [ Main Menu ] "
|
||||||
|
footer1 = "KIAUH v6.0.0"
|
||||||
|
footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}"
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) [Log-Upload] | Klipper: {self.kl_status:<32} |
|
||||||
|
| | Repo: {self.kl_repo:<32} |
|
||||||
|
| 1) [Install] |------------------------------------|
|
||||||
|
| 2) [Update] | Moonraker: {self.mr_status:<32} |
|
||||||
|
| 3) [Remove] | Repo: {self.mr_repo:<32} |
|
||||||
|
| 4) [Advanced] |------------------------------------|
|
||||||
|
| 5) [Backup] | Mainsail: {self.ms_status:<26} |
|
||||||
|
| | Fluidd: {self.fl_status:<26} |
|
||||||
|
| S) [Settings] | Client-Config: {self.cc_status:<26} |
|
||||||
|
| | |
|
||||||
|
| Community: | KlipperScreen: {self.ks_status:<26} |
|
||||||
|
| E) [Extensions] | Mobileraker: {self.mb_status:<26} |
|
||||||
|
| | Crowsnest: {self.cn_status:<26} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def log_upload_menu(self, **kwargs):
|
||||||
|
LogUploadMenu().run()
|
||||||
|
|
||||||
|
def install_menu(self, **kwargs):
|
||||||
|
InstallMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def update_menu(self, **kwargs):
|
||||||
|
UpdateMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def remove_menu(self, **kwargs):
|
||||||
|
RemoveMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def advanced_menu(self, **kwargs):
|
||||||
|
AdvancedMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def backup_menu(self, **kwargs):
|
||||||
|
BackupMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def settings_menu(self, **kwargs):
|
||||||
|
SettingsMenu().run()
|
||||||
|
|
||||||
|
def extension_menu(self, **kwargs):
|
||||||
|
ExtensionsMenu(previous_menu=self.__class__).run()
|
||||||
84
kiauh/core/menus/remove_menu.py
Normal file
84
kiauh/core/menus/remove_menu.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu
|
||||||
|
from components.moonraker.menus.moonraker_remove_menu import (
|
||||||
|
MoonrakerRemoveMenu,
|
||||||
|
)
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from components.webui_client.menus.client_remove_menu import ClientRemoveMenu
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_RED, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class RemoveMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self):
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.remove_klipper, menu=True),
|
||||||
|
"2": Option(method=self.remove_moonraker, menu=True),
|
||||||
|
"3": Option(method=self.remove_mainsail, menu=True),
|
||||||
|
"4": Option(method=self.remove_fluidd, menu=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Remove Menu ] "
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| INFO: Configurations and/or any backups will be kept! |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Firmware & API: | Webcam Streamer: |
|
||||||
|
| 1) [Klipper] | 6) [Crowsnest] |
|
||||||
|
| 2) [Moonraker] | 7) [MJPG-Streamer] |
|
||||||
|
| | |
|
||||||
|
| Klipper Webinterface: | Other: |
|
||||||
|
| 3) [Mainsail] | 8) [PrettyGCode] |
|
||||||
|
| 4) [Fluidd] | 9) [Telegram Bot] |
|
||||||
|
| | 10) [Obico for Klipper] |
|
||||||
|
| Touchscreen GUI: | 11) [OctoEverywhere] |
|
||||||
|
| 5) [KlipperScreen] | 12) [Mobileraker] |
|
||||||
|
| | 13) [NGINX] |
|
||||||
|
| | |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def remove_klipper(self, **kwargs):
|
||||||
|
KlipperRemoveMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def remove_moonraker(self, **kwargs):
|
||||||
|
MoonrakerRemoveMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def remove_mainsail(self, **kwargs):
|
||||||
|
ClientRemoveMenu(previous_menu=self.__class__, client=MainsailData()).run()
|
||||||
|
|
||||||
|
def remove_fluidd(self, **kwargs):
|
||||||
|
ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run()
|
||||||
43
kiauh/core/menus/settings_menu.py
Normal file
43
kiauh/core/menus/settings_menu.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class SettingsMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
print("self")
|
||||||
|
|
||||||
|
def execute_option_p(self):
|
||||||
|
# Implement the functionality for Option P
|
||||||
|
print("Executing Option P")
|
||||||
|
|
||||||
|
def execute_option_q(self):
|
||||||
|
# Implement the functionality for Option Q
|
||||||
|
print("Executing Option Q")
|
||||||
|
|
||||||
|
def execute_option_r(self):
|
||||||
|
# Implement the functionality for Option R
|
||||||
|
print("Executing Option R")
|
||||||
195
kiauh/core/menus/update_menu.py
Normal file
195
kiauh/core/menus/update_menu.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Type, Optional
|
||||||
|
|
||||||
|
from components.klipper.klipper_setup import update_klipper
|
||||||
|
from components.klipper.klipper_utils import (
|
||||||
|
get_klipper_status,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker_setup import update_moonraker
|
||||||
|
from components.moonraker.moonraker_utils import get_moonraker_status
|
||||||
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
|
update_client_config,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_setup import update_client
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
get_local_client_version,
|
||||||
|
get_remote_client_version,
|
||||||
|
get_client_config_status,
|
||||||
|
)
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import (
|
||||||
|
COLOR_GREEN,
|
||||||
|
RESET_FORMAT,
|
||||||
|
COLOR_YELLOW,
|
||||||
|
COLOR_WHITE,
|
||||||
|
COLOR_RED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class UpdateMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
self.kl_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.kl_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.mr_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.mr_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.ms_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.ms_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.fl_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.fl_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.mc_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
self.mainsail_client = MainsailData()
|
||||||
|
self.fluidd_client = FluiddData()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"0": Option(self.update_all, menu=False),
|
||||||
|
"1": Option(self.update_klipper, menu=False),
|
||||||
|
"2": Option(self.update_moonraker, menu=False),
|
||||||
|
"3": Option(self.update_mainsail, menu=False),
|
||||||
|
"4": Option(self.update_fluidd, menu=False),
|
||||||
|
"5": Option(self.update_mainsail_config, menu=False),
|
||||||
|
"6": Option(self.update_fluidd_config, menu=False),
|
||||||
|
"7": Option(self.update_klipperscreen, menu=False),
|
||||||
|
"8": Option(self.update_mobileraker, menu=False),
|
||||||
|
"9": Option(self.update_crowsnest, menu=False),
|
||||||
|
"10": Option(self.upgrade_system_packages, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
self.fetch_update_status()
|
||||||
|
|
||||||
|
header = " [ Update Menu ] "
|
||||||
|
color = COLOR_GREEN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) Update all | | |
|
||||||
|
| | Current: | Latest: |
|
||||||
|
| Klipper & API: |---------------|---------------|
|
||||||
|
| 1) Klipper | {self.kl_local:<22} | {self.kl_remote:<22} |
|
||||||
|
| 2) Moonraker | {self.mr_local:<22} | {self.mr_remote:<22} |
|
||||||
|
| | | |
|
||||||
|
| Webinterface: |---------------|---------------|
|
||||||
|
| 3) Mainsail | {self.ms_local:<22} | {self.ms_remote:<22} |
|
||||||
|
| 4) Fluidd | {self.fl_local:<22} | {self.fl_remote:<22} |
|
||||||
|
| | | |
|
||||||
|
| Client-Config: |---------------|---------------|
|
||||||
|
| 5) Mainsail-Config | {self.mc_local:<22} | {self.mc_remote:<22} |
|
||||||
|
| 6) Fluidd-Config | {self.fc_local:<22} | {self.fc_remote:<22} |
|
||||||
|
| | | |
|
||||||
|
| Other: |---------------|---------------|
|
||||||
|
| 7) KlipperScreen | | |
|
||||||
|
| 8) Mobileraker | | |
|
||||||
|
| 9) Crowsnest | | |
|
||||||
|
| |-------------------------------|
|
||||||
|
| 10) System | |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def update_all(self, **kwargs):
|
||||||
|
print("update_all")
|
||||||
|
|
||||||
|
def update_klipper(self, **kwargs):
|
||||||
|
update_klipper()
|
||||||
|
|
||||||
|
def update_moonraker(self, **kwargs):
|
||||||
|
update_moonraker()
|
||||||
|
|
||||||
|
def update_mainsail(self, **kwargs):
|
||||||
|
update_client(self.mainsail_client)
|
||||||
|
|
||||||
|
def update_mainsail_config(self, **kwargs):
|
||||||
|
update_client_config(self.mainsail_client)
|
||||||
|
|
||||||
|
def update_fluidd(self, **kwargs):
|
||||||
|
update_client(self.fluidd_client)
|
||||||
|
|
||||||
|
def update_fluidd_config(self, **kwargs):
|
||||||
|
update_client_config(self.fluidd_client)
|
||||||
|
|
||||||
|
def update_klipperscreen(self, **kwargs): ...
|
||||||
|
|
||||||
|
def update_mobileraker(self, **kwargs): ...
|
||||||
|
|
||||||
|
def update_crowsnest(self, **kwargs): ...
|
||||||
|
|
||||||
|
def upgrade_system_packages(self, **kwargs): ...
|
||||||
|
|
||||||
|
def fetch_update_status(self):
|
||||||
|
# klipper
|
||||||
|
kl_status = get_klipper_status()
|
||||||
|
self.kl_local = self.format_local_status(
|
||||||
|
kl_status.get("local"), kl_status.get("remote")
|
||||||
|
)
|
||||||
|
self.kl_remote = kl_status.get("remote")
|
||||||
|
self.kl_remote = f"{COLOR_GREEN}{kl_status.get('remote')}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
# moonraker
|
||||||
|
mr_status = get_moonraker_status()
|
||||||
|
self.mr_local = self.format_local_status(
|
||||||
|
mr_status.get("local"), mr_status.get("remote")
|
||||||
|
)
|
||||||
|
self.mr_remote = f"{COLOR_GREEN}{mr_status.get('remote')}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
# mainsail
|
||||||
|
ms_local_ver = get_local_client_version(self.mainsail_client)
|
||||||
|
ms_remote_ver = get_remote_client_version(self.mainsail_client)
|
||||||
|
self.ms_local = self.format_local_status(ms_local_ver, ms_remote_ver)
|
||||||
|
self.ms_remote = f"{COLOR_GREEN if ms_remote_ver != 'ERROR' else COLOR_RED}{ms_remote_ver}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
# fluidd
|
||||||
|
fl_local_ver = get_local_client_version(self.fluidd_client)
|
||||||
|
fl_remote_ver = get_remote_client_version(self.fluidd_client)
|
||||||
|
self.fl_local = self.format_local_status(fl_local_ver, fl_remote_ver)
|
||||||
|
self.fl_remote = f"{COLOR_GREEN if fl_remote_ver != 'ERROR' else COLOR_RED}{fl_remote_ver}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
# mainsail-config
|
||||||
|
mc_status = get_client_config_status(self.mainsail_client)
|
||||||
|
self.mc_local = self.format_local_status(
|
||||||
|
mc_status.get("local"), mc_status.get("remote")
|
||||||
|
)
|
||||||
|
self.mc_remote = f"{COLOR_GREEN}{mc_status.get('remote')}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
# fluidd-config
|
||||||
|
fc_status = get_client_config_status(self.fluidd_client)
|
||||||
|
self.fc_local = self.format_local_status(
|
||||||
|
fc_status.get("local"), fc_status.get("remote")
|
||||||
|
)
|
||||||
|
self.fc_remote = f"{COLOR_GREEN}{fc_status.get('remote')}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
def format_local_status(self, local_version, remote_version) -> str:
|
||||||
|
if local_version == remote_version:
|
||||||
|
return f"{COLOR_GREEN}{local_version}{RESET_FORMAT}"
|
||||||
|
return f"{COLOR_YELLOW}{local_version}{RESET_FORMAT}"
|
||||||
0
kiauh/core/repo_manager/__init__.py
Normal file
0
kiauh/core/repo_manager/__init__.py
Normal file
171
kiauh/core/repo_manager/repo_manager.py
Normal file
171
kiauh/core/repo_manager/repo_manager.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class RepoManager:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
repo: str,
|
||||||
|
target_dir: str,
|
||||||
|
branch: str = None,
|
||||||
|
):
|
||||||
|
self._repo = repo
|
||||||
|
self._branch = branch
|
||||||
|
self._method = self._get_method()
|
||||||
|
self._target_dir = target_dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def repo(self) -> str:
|
||||||
|
return self._repo
|
||||||
|
|
||||||
|
@repo.setter
|
||||||
|
def repo(self, value) -> None:
|
||||||
|
self._repo = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def branch(self) -> str:
|
||||||
|
return self._branch
|
||||||
|
|
||||||
|
@branch.setter
|
||||||
|
def branch(self, value) -> None:
|
||||||
|
self._branch = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def method(self) -> str:
|
||||||
|
return self._method
|
||||||
|
|
||||||
|
@method.setter
|
||||||
|
def method(self, value) -> None:
|
||||||
|
self._method = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_dir(self) -> str:
|
||||||
|
return self._target_dir
|
||||||
|
|
||||||
|
@target_dir.setter
|
||||||
|
def target_dir(self, value) -> None:
|
||||||
|
self._target_dir = value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_repo_name(repo: Path) -> str:
|
||||||
|
"""
|
||||||
|
Helper method to extract the organisation and name of a repository |
|
||||||
|
:param repo: repository to extract the values from
|
||||||
|
:return: String in form of "<orga>/<name>"
|
||||||
|
"""
|
||||||
|
if not repo.exists() and not repo.joinpath(".git").exists():
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = ["git", "-C", repo, "config", "--get", "remote.origin.url"]
|
||||||
|
result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
|
||||||
|
return "/".join(result.decode().strip().split("/")[-2:])
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_local_commit(repo: Path) -> str:
|
||||||
|
if not repo.exists() and not repo.joinpath(".git").exists():
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = f"cd {repo} && git describe HEAD --always --tags | cut -d '-' -f 1,2"
|
||||||
|
return subprocess.check_output(cmd, shell=True, text=True).strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_remote_commit(repo: Path) -> str:
|
||||||
|
if not repo.exists() and not repo.joinpath(".git").exists():
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# get locally checked out branch
|
||||||
|
branch_cmd = f"cd {repo} && git branch | grep -E '\*'"
|
||||||
|
branch = subprocess.check_output(branch_cmd, shell=True, text=True)
|
||||||
|
branch = branch.split("*")[-1].strip()
|
||||||
|
cmd = f"cd {repo} && git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2"
|
||||||
|
return subprocess.check_output(cmd, shell=True, text=True).strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
def clone_repo(self):
|
||||||
|
log = f"Cloning repository from '{self.repo}' with method '{self.method}'"
|
||||||
|
Logger.print_status(log)
|
||||||
|
try:
|
||||||
|
if Path(self.target_dir).exists():
|
||||||
|
question = f"'{self.target_dir}' already exists. Overwrite?"
|
||||||
|
if not get_confirm(question, default_choice=False):
|
||||||
|
Logger.print_info("Skip cloning of repository ...")
|
||||||
|
return
|
||||||
|
shutil.rmtree(self.target_dir)
|
||||||
|
|
||||||
|
self._clone()
|
||||||
|
self._checkout()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
log = "An unexpected error occured during cloning of the repository."
|
||||||
|
Logger.print_error(log)
|
||||||
|
return
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Error removing existing repository: {e.strerror}")
|
||||||
|
return
|
||||||
|
|
||||||
|
def pull_repo(self) -> None:
|
||||||
|
Logger.print_status(f"Updating repository '{self.repo}' ...")
|
||||||
|
try:
|
||||||
|
self._pull()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
log = "An unexpected error occured during updating the repository."
|
||||||
|
Logger.print_error(log)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _clone(self):
|
||||||
|
try:
|
||||||
|
command = ["git", "clone", self.repo, self.target_dir]
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
|
||||||
|
Logger.print_ok("Clone successful!")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log = f"Error cloning repository {self.repo}: {e.stderr.decode()}"
|
||||||
|
Logger.print_error(log)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _checkout(self):
|
||||||
|
if self.branch is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = ["git", "checkout", f"{self.branch}"]
|
||||||
|
subprocess.run(command, cwd=self.target_dir, check=True)
|
||||||
|
|
||||||
|
Logger.print_ok("Checkout successful!")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log = f"Error checking out branch {self.branch}: {e.stderr.decode()}"
|
||||||
|
Logger.print_error(log)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _pull(self) -> None:
|
||||||
|
try:
|
||||||
|
command = ["git", "pull"]
|
||||||
|
subprocess.run(command, cwd=self.target_dir, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log = f"Error on git pull: {e.stderr.decode()}"
|
||||||
|
Logger.print_error(log)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _get_method(self) -> str:
|
||||||
|
return "ssh" if self.repo.startswith("git") else "https"
|
||||||
12
kiauh/extensions/__init__.py
Normal file
12
kiauh/extensions/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
EXTENSION_ROOT = Path(__file__).resolve().parents[1].joinpath("extensions")
|
||||||
29
kiauh/extensions/base_extension.py
Normal file
29
kiauh/extensions/base_extension.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from abc import abstractmethod, ABC
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class BaseExtension(ABC):
|
||||||
|
def __init__(self, metadata: Dict[str, str]):
|
||||||
|
self.metadata = metadata
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def update_extension(self, **kwargs) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
159
kiauh/extensions/extensions_menu.py
Normal file
159
kiauh/extensions/extensions_menu.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Type, Dict, Optional
|
||||||
|
|
||||||
|
from core.menus import Option
|
||||||
|
from extensions import EXTENSION_ROOT
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import RESET_FORMAT, COLOR_CYAN, COLOR_YELLOW
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class ExtensionsMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.extensions: Dict[str, BaseExtension] = self.discover_extensions()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
i: Option(
|
||||||
|
self.extension_submenu, menu=True, opt_data=self.extensions.get(i)
|
||||||
|
)
|
||||||
|
for i in self.extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
def discover_extensions(self) -> Dict[str, BaseExtension]:
|
||||||
|
ext_dict = {}
|
||||||
|
|
||||||
|
for ext in EXTENSION_ROOT.iterdir():
|
||||||
|
metadata_json = Path(ext).joinpath("metadata.json")
|
||||||
|
if not metadata_json.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(metadata_json, "r") as m:
|
||||||
|
# read extension metadata from json
|
||||||
|
metadata = json.load(m).get("metadata")
|
||||||
|
module_name = metadata.get("module")
|
||||||
|
module_path = f"kiauh.extensions.{ext.name}.{module_name}"
|
||||||
|
|
||||||
|
# get the class name of the extension
|
||||||
|
ext_class: Type[BaseExtension] = inspect.getmembers(
|
||||||
|
importlib.import_module(module_path),
|
||||||
|
predicate=lambda o: inspect.isclass(o)
|
||||||
|
and issubclass(o, BaseExtension)
|
||||||
|
and o != BaseExtension,
|
||||||
|
)[0][1]
|
||||||
|
|
||||||
|
# instantiate the extension with its metadata and add to dict
|
||||||
|
ext_instance: BaseExtension = ext_class(metadata)
|
||||||
|
ext_dict[f"{metadata.get('index')}"] = ext_instance
|
||||||
|
|
||||||
|
except (IOError, json.JSONDecodeError, ImportError) as e:
|
||||||
|
print(f"Failed loading extension {ext}: {e}")
|
||||||
|
|
||||||
|
return dict(sorted(ext_dict.items()))
|
||||||
|
|
||||||
|
def extension_submenu(self, **kwargs):
|
||||||
|
ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run()
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Extensions Menu ] "
|
||||||
|
color = COLOR_CYAN
|
||||||
|
line1 = f"{COLOR_YELLOW}Available Extensions:{RESET_FORMAT}"
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line1:<62} |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
for extension in self.extensions.values():
|
||||||
|
index = extension.metadata.get("index")
|
||||||
|
name = extension.metadata.get("display_name")
|
||||||
|
row = f"{index}) {name}"
|
||||||
|
print(f"| {row:<53} |")
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class ExtensionSubmenu(BaseMenu):
|
||||||
|
def __init__(
|
||||||
|
self, extension: BaseExtension, previous_menu: Optional[Type[BaseMenu]] = None
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.extension = extension
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else ExtensionsMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options["1"] = Option(self.extension.install_extension, menu=False)
|
||||||
|
if self.extension.metadata.get("updates"):
|
||||||
|
self.options["2"] = Option(self.extension.update_extension, menu=False)
|
||||||
|
self.options["3"] = Option(self.extension.remove_extension, menu=False)
|
||||||
|
else:
|
||||||
|
self.options["2"] = Option(self.extension.remove_extension, menu=False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = f" [ {self.extension.metadata.get('display_name')} ] "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
|
||||||
|
wrapper = textwrap.TextWrapper(55, initial_indent="| ", subsequent_indent="| ")
|
||||||
|
lines = wrapper.wrap(self.extension.metadata.get("description"))
|
||||||
|
formatted_lines = [f"{line:<55} |" for line in lines]
|
||||||
|
description_text = "\n".join(formatted_lines)
|
||||||
|
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
menu += f"{description_text}\n"
|
||||||
|
menu += textwrap.dedent(
|
||||||
|
"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 1) Install |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if self.extension.metadata.get("updates"):
|
||||||
|
menu += "| 2) Update |\n"
|
||||||
|
menu += "| 3) Remove |\n"
|
||||||
|
else:
|
||||||
|
menu += "| 2) Remove |\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
19
kiauh/extensions/gcode_shell_cmd/__init__.py
Normal file
19
kiauh/extensions/gcode_shell_cmd/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
EXT_MODULE_NAME = "gcode_shell_command.py"
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
MODULE_ASSETS = MODULE_PATH.joinpath("assets")
|
||||||
|
KLIPPER_DIR = Path.home().joinpath("klipper")
|
||||||
|
KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras")
|
||||||
|
EXTENSION_SRC = MODULE_ASSETS.joinpath(EXT_MODULE_NAME)
|
||||||
|
EXTENSION_TARGET_PATH = KLIPPER_EXTRAS.joinpath(EXT_MODULE_NAME)
|
||||||
|
EXAMPLE_CFG_SRC = MODULE_ASSETS.joinpath("shell_command.cfg")
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Run a shell command via gcode
|
||||||
|
#
|
||||||
|
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
|
||||||
|
#
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
class ShellCommand:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.name = config.get_name().split()[-1]
|
||||||
|
self.printer = config.get_printer()
|
||||||
|
self.gcode = self.printer.lookup_object("gcode")
|
||||||
|
cmd = config.get("command")
|
||||||
|
cmd = os.path.expanduser(cmd)
|
||||||
|
self.command = shlex.split(cmd)
|
||||||
|
self.timeout = config.getfloat("timeout", 2.0, above=0.0)
|
||||||
|
self.verbose = config.getboolean("verbose", True)
|
||||||
|
self.proc_fd = None
|
||||||
|
self.partial_output = ""
|
||||||
|
self.gcode.register_mux_command(
|
||||||
|
"RUN_SHELL_COMMAND",
|
||||||
|
"CMD",
|
||||||
|
self.name,
|
||||||
|
self.cmd_RUN_SHELL_COMMAND,
|
||||||
|
desc=self.cmd_RUN_SHELL_COMMAND_help,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_output(self, eventime):
|
||||||
|
if self.proc_fd is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = os.read(self.proc_fd, 4096)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
data = self.partial_output + data.decode()
|
||||||
|
if "\n" not in data:
|
||||||
|
self.partial_output = data
|
||||||
|
return
|
||||||
|
elif data[-1] != "\n":
|
||||||
|
split = data.rfind("\n") + 1
|
||||||
|
self.partial_output = data[split:]
|
||||||
|
data = data[:split]
|
||||||
|
else:
|
||||||
|
self.partial_output = ""
|
||||||
|
self.gcode.respond_info(data)
|
||||||
|
|
||||||
|
cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command"
|
||||||
|
|
||||||
|
def cmd_RUN_SHELL_COMMAND(self, params):
|
||||||
|
gcode_params = params.get("PARAMS", "")
|
||||||
|
gcode_params = shlex.split(gcode_params)
|
||||||
|
reactor = self.printer.get_reactor()
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
self.command + gcode_params,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("shell_command: Command {%s} failed" % (self.name))
|
||||||
|
raise self.gcode.error("Error running command {%s}" % (self.name))
|
||||||
|
if self.verbose:
|
||||||
|
self.proc_fd = proc.stdout.fileno()
|
||||||
|
self.gcode.respond_info("Running Command {%s}...:" % (self.name))
|
||||||
|
hdl = reactor.register_fd(self.proc_fd, self._process_output)
|
||||||
|
eventtime = reactor.monotonic()
|
||||||
|
endtime = eventtime + self.timeout
|
||||||
|
complete = False
|
||||||
|
while eventtime < endtime:
|
||||||
|
eventtime = reactor.pause(eventtime + 0.05)
|
||||||
|
if proc.poll() is not None:
|
||||||
|
complete = True
|
||||||
|
break
|
||||||
|
if not complete:
|
||||||
|
proc.terminate()
|
||||||
|
if self.verbose:
|
||||||
|
if self.partial_output:
|
||||||
|
self.gcode.respond_info(self.partial_output)
|
||||||
|
self.partial_output = ""
|
||||||
|
if complete:
|
||||||
|
msg = "Command {%s} finished\n" % (self.name)
|
||||||
|
else:
|
||||||
|
msg = "Command {%s} timed out" % (self.name)
|
||||||
|
self.gcode.respond_info(msg)
|
||||||
|
reactor.unregister_fd(hdl)
|
||||||
|
self.proc_fd = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_prefix(config):
|
||||||
|
return ShellCommand(config)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[gcode_shell_command hello_world]
|
||||||
|
command: echo hello world
|
||||||
|
timeout: 2.
|
||||||
|
verbose: True
|
||||||
|
[gcode_macro HELLO_WORLD]
|
||||||
|
gcode:
|
||||||
|
RUN_SHELL_COMMAND CMD=hello_world
|
||||||
127
kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py
Normal file
127
kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from extensions.gcode_shell_cmd import (
|
||||||
|
EXTENSION_TARGET_PATH,
|
||||||
|
EXTENSION_SRC,
|
||||||
|
KLIPPER_DIR,
|
||||||
|
EXAMPLE_CFG_SRC,
|
||||||
|
KLIPPER_EXTRAS,
|
||||||
|
)
|
||||||
|
from utils.filesystem_utils import check_file_exist
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class GcodeShellCmdExtension(BaseExtension):
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
install_example = get_confirm("Create an example shell command?", False, False)
|
||||||
|
|
||||||
|
klipper_dir_exists = check_file_exist(KLIPPER_DIR)
|
||||||
|
if not klipper_dir_exists:
|
||||||
|
Logger.print_warn(
|
||||||
|
"No Klipper directory found! Unable to install extension."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
extension_installed = check_file_exist(EXTENSION_TARGET_PATH)
|
||||||
|
overwrite = True
|
||||||
|
if extension_installed:
|
||||||
|
overwrite = get_confirm(
|
||||||
|
"Extension seems to be installed already. Overwrite?",
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not overwrite:
|
||||||
|
Logger.print_warn("Installation aborted due to user request.")
|
||||||
|
return
|
||||||
|
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
im.stop_all_instance()
|
||||||
|
|
||||||
|
try:
|
||||||
|
Logger.print_status(f"Copy extension to '{KLIPPER_EXTRAS}' ...")
|
||||||
|
shutil.copy(EXTENSION_SRC, EXTENSION_TARGET_PATH)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to install extension: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if install_example:
|
||||||
|
self.install_example_cfg(im.instances)
|
||||||
|
|
||||||
|
im.start_all_instance()
|
||||||
|
|
||||||
|
Logger.print_ok("Installing G-Code Shell Command extension successful!")
|
||||||
|
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
extension_installed = check_file_exist(EXTENSION_TARGET_PATH)
|
||||||
|
if not extension_installed:
|
||||||
|
Logger.print_info("Extension does not seem to be installed! Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
question = "Do you really want to remove the extension?"
|
||||||
|
if get_confirm(question, True, False):
|
||||||
|
try:
|
||||||
|
Logger.print_status(f"Removing '{EXTENSION_TARGET_PATH}' ...")
|
||||||
|
os.remove(EXTENSION_TARGET_PATH)
|
||||||
|
Logger.print_ok("Extension successfully removed!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to remove extension: {e}")
|
||||||
|
|
||||||
|
Logger.print_warn("PLEASE NOTE:")
|
||||||
|
Logger.print_warn(
|
||||||
|
"Remaining gcode shell command will cause Klipper to throw an error."
|
||||||
|
)
|
||||||
|
Logger.print_warn("Make sure to remove them from the printer.cfg!")
|
||||||
|
|
||||||
|
def install_example_cfg(self, instances: List[Klipper]):
|
||||||
|
cfg_dirs = [instance.cfg_dir for instance in instances]
|
||||||
|
# copy extension to klippy/extras
|
||||||
|
for cfg_dir in cfg_dirs:
|
||||||
|
Logger.print_status(f"Create shell_command.cfg in '{cfg_dir}' ...")
|
||||||
|
if check_file_exist(cfg_dir.joinpath("shell_command.cfg")):
|
||||||
|
Logger.print_info("File already exists! Skipping ...")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
shutil.copy(EXAMPLE_CFG_SRC, cfg_dir)
|
||||||
|
Logger.print_ok("Done!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.warn(f"Unable to create example config: {e}")
|
||||||
|
|
||||||
|
# backup each printer.cfg before modification
|
||||||
|
bm = BackupManager()
|
||||||
|
for instance in instances:
|
||||||
|
bm.backup_file(
|
||||||
|
instance.cfg_file,
|
||||||
|
custom_filename=f"{instance.suffix}.printer.cfg",
|
||||||
|
)
|
||||||
|
|
||||||
|
# add section to printer.cfg if not already defined
|
||||||
|
section = "include shell_command.cfg"
|
||||||
|
cfg_files = [instance.cfg_file for instance in instances]
|
||||||
|
for cfg_file in cfg_files:
|
||||||
|
Logger.print_status(f"Include shell_command.cfg in '{cfg_file}' ...")
|
||||||
|
cm = ConfigManager(cfg_file)
|
||||||
|
if cm.config.has_section(section):
|
||||||
|
Logger.print_info("Section already defined! Skipping ...")
|
||||||
|
continue
|
||||||
|
cm.config.add_section(section)
|
||||||
|
cm.write_config()
|
||||||
|
Logger.print_ok("Done!")
|
||||||
9
kiauh/extensions/gcode_shell_cmd/metadata.json
Normal file
9
kiauh/extensions/gcode_shell_cmd/metadata.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"index": 1,
|
||||||
|
"module": "gcode_shell_cmd_extension",
|
||||||
|
"maintained_by": "dw-0",
|
||||||
|
"display_name": "G-Code Shell Command",
|
||||||
|
"description": "Allows to run a shell command from gcode."
|
||||||
|
}
|
||||||
|
}
|
||||||
18
kiauh/extensions/klipper_backup/__init__.py
Normal file
18
kiauh/extensions/klipper_backup/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet #
|
||||||
|
# https://github.com/Staubgeborener/klipper-backup #
|
||||||
|
# #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
EXT_MODULE_NAME = "klipper_backup_extension.py"
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
MOONRAKER_CONF = Path.home().joinpath("printer_data", "config", "moonraker.conf")
|
||||||
|
KLIPPERBACKUP_DIR = Path.home().joinpath("klipper-backup")
|
||||||
|
KLIPPERBACKUP_CONFIG_DIR = Path.home().joinpath("config_backup")
|
||||||
|
KLIPPERBACKUP_REPO_URL = "https://github.com/staubgeborener/klipper-backup"
|
||||||
192
kiauh/extensions/klipper_backup/klipper_backup_extension.py
Normal file
192
kiauh/extensions/klipper_backup/klipper_backup_extension.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet #
|
||||||
|
# https://github.com/Staubgeborener/klipper-backup #
|
||||||
|
# #
|
||||||
|
# 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 os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from extensions.klipper_backup import (
|
||||||
|
KLIPPERBACKUP_REPO_URL,
|
||||||
|
KLIPPERBACKUP_DIR,
|
||||||
|
KLIPPERBACKUP_CONFIG_DIR,
|
||||||
|
MOONRAKER_CONF,
|
||||||
|
)
|
||||||
|
|
||||||
|
from utils.filesystem_utils import check_file_exist
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperbackupExtension(BaseExtension):
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
if not KLIPPERBACKUP_DIR.exists():
|
||||||
|
subprocess.run(
|
||||||
|
["git", "clone", str(KLIPPERBACKUP_REPO_URL), str(KLIPPERBACKUP_DIR)]
|
||||||
|
)
|
||||||
|
# subprocess.run(["git", "-C", str(KLIPPERBACKUP_DIR), "checkout", "installer-dev"]) # Only for testing
|
||||||
|
subprocess.run(["chmod", "+x", str(KLIPPERBACKUP_DIR / "install.sh")])
|
||||||
|
subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh")])
|
||||||
|
|
||||||
|
def update_extension(self, **kwargs) -> None:
|
||||||
|
extension_installed = check_file_exist(KLIPPERBACKUP_DIR)
|
||||||
|
if not extension_installed:
|
||||||
|
Logger.print_info("Extension does not seem to be installed! Skipping ...")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh"), "check_updates"])
|
||||||
|
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
def is_service_installed(service_name):
|
||||||
|
command = ["systemctl", "status", service_name]
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True)
|
||||||
|
# Doesn't matter whether the service is active or not, what matters is whether it is installed. So let's search for "Loaded:" in stdout
|
||||||
|
if "Loaded:" in result.stdout:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def uninstall_service(service_name):
|
||||||
|
try:
|
||||||
|
subprocess.run(["sudo", "systemctl", "stop", service_name], check=True)
|
||||||
|
subprocess.run(
|
||||||
|
["sudo", "systemctl", "disable", service_name], check=True
|
||||||
|
)
|
||||||
|
subprocess.run(["sudo", "systemctl", "daemon-reload"], check=True)
|
||||||
|
service_path = f"/etc/systemd/system/{service_name}"
|
||||||
|
os.system(f"sudo rm {service_path}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_crontab_entry(entry):
|
||||||
|
try:
|
||||||
|
crontab_content = subprocess.check_output(
|
||||||
|
["crontab", "-l"], stderr=subprocess.DEVNULL, text=True
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
for line in crontab_content.splitlines():
|
||||||
|
if entry in line:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
extension_installed = check_file_exist(KLIPPERBACKUP_DIR)
|
||||||
|
if not extension_installed:
|
||||||
|
Logger.print_info("Extension does not seem to be installed! Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
def remove_moonraker_entry():
|
||||||
|
original_file_path = MOONRAKER_CONF
|
||||||
|
comparison_file_path = os.path.join(
|
||||||
|
str(KLIPPERBACKUP_DIR), "install-files", "moonraker.conf"
|
||||||
|
)
|
||||||
|
if not os.path.exists(original_file_path) or not os.path.exists(
|
||||||
|
comparison_file_path
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
with open(original_file_path, "r") as original_file, open(
|
||||||
|
comparison_file_path, "r"
|
||||||
|
) as comparison_file:
|
||||||
|
original_content = original_file.read()
|
||||||
|
comparison_content = comparison_file.read()
|
||||||
|
if comparison_content in original_content:
|
||||||
|
modified_content = original_content.replace(
|
||||||
|
comparison_content, ""
|
||||||
|
).strip()
|
||||||
|
modified_content = "\n".join(
|
||||||
|
line for line in modified_content.split("\n") if line.strip()
|
||||||
|
)
|
||||||
|
with open(original_file_path, "w") as original_file:
|
||||||
|
original_file.write(modified_content)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
question = "Do you really want to remove the extension?"
|
||||||
|
if get_confirm(question, True, False):
|
||||||
|
# Remove Klipper-Backup services
|
||||||
|
service_names = [
|
||||||
|
"klipper-backup-on-boot.service",
|
||||||
|
"klipper-backup-filewatch.service",
|
||||||
|
]
|
||||||
|
for service_name in service_names:
|
||||||
|
try:
|
||||||
|
Logger.print_status(
|
||||||
|
f"Check whether the service {service_name} is installed ..."
|
||||||
|
)
|
||||||
|
if is_service_installed(service_name):
|
||||||
|
Logger.print_info(f"Service {service_name} detected.")
|
||||||
|
if uninstall_service(service_name):
|
||||||
|
Logger.print_ok(
|
||||||
|
f"The service {service_name} has been successfully uninstalled."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Error uninstalling the service {service_name}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
f"The service {service_name} is not installed. Skipping ..."
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
Logger.print_error(f"Unable to remove the service {service_name}")
|
||||||
|
|
||||||
|
# Remove Klipper-Backup cron
|
||||||
|
Logger.print_status("Check for Klipper-Backup cron entry ...")
|
||||||
|
entry_to_check = "/klipper-backup/script.sh"
|
||||||
|
try:
|
||||||
|
if check_crontab_entry(entry_to_check):
|
||||||
|
crontab_content = subprocess.check_output(
|
||||||
|
["crontab", "-l"], text=True
|
||||||
|
)
|
||||||
|
modified_content = "\n".join(
|
||||||
|
line
|
||||||
|
for line in crontab_content.splitlines()
|
||||||
|
if entry_to_check not in line
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["crontab", "-"], input=modified_content, text=True, check=True
|
||||||
|
)
|
||||||
|
Logger.print_ok(
|
||||||
|
"The Klipper-Backup entry has been removed from the crontab."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
"The Klipper-Backup entry is not present in the crontab. Skipping ..."
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
Logger.print_error("Unable to remove the Klipper-Backup cron entry")
|
||||||
|
|
||||||
|
# Remove Moonraker entry
|
||||||
|
Logger.print_status(f"Check for Klipper-Backup moonraker entry ...")
|
||||||
|
try:
|
||||||
|
if remove_moonraker_entry():
|
||||||
|
Logger.print_ok("Klipper-Backup entry in moonraker.conf removed")
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
"Klipper-Backup entry not found in moonraker.conf. Skipping ..."
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
Logger.print_error(
|
||||||
|
"Unknown error, either the moonraker.conf is not found or the Klipper-Backup entry under ~/klipper-backup/install-files/moonraker.conf. Skipping ..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove Klipper-Backup
|
||||||
|
Logger.print_status(f"Removing '{KLIPPERBACKUP_DIR}' ...")
|
||||||
|
try:
|
||||||
|
shutil.rmtree(KLIPPERBACKUP_DIR)
|
||||||
|
config_backup_exists = check_file_exist(KLIPPERBACKUP_CONFIG_DIR)
|
||||||
|
if config_backup_exists:
|
||||||
|
shutil.rmtree(KLIPPERBACKUP_CONFIG_DIR)
|
||||||
|
Logger.print_ok("Extension Klipper-Backup successfully removed!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to remove extension: {e}")
|
||||||
10
kiauh/extensions/klipper_backup/metadata.json
Normal file
10
kiauh/extensions/klipper_backup/metadata.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"index": 3,
|
||||||
|
"module": "klipper_backup_extension",
|
||||||
|
"maintained_by": "Staubgeborener",
|
||||||
|
"display_name": "Klipper-Backup",
|
||||||
|
"description": "Backup all your klipper files in GitHub",
|
||||||
|
"updates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import shutil
|
||||||
|
import textwrap
|
||||||
|
import urllib.request
|
||||||
|
from typing import List, Union
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper.klipper_dialogs import (
|
||||||
|
print_instance_overview,
|
||||||
|
DisplayType,
|
||||||
|
)
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from core.repo_manager.repo_manager import RepoManager
|
||||||
|
from utils.constants import COLOR_YELLOW, COLOR_CYAN, RESET_FORMAT
|
||||||
|
from utils.input_utils import get_selection_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
class ThemeData(TypedDict):
|
||||||
|
name: str
|
||||||
|
short_note: str
|
||||||
|
author: str
|
||||||
|
repo: str
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class MainsailThemeInstallerExtension(BaseExtension):
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
instances: List[Klipper] = im.instances
|
||||||
|
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
install_menu = MainsailThemeInstallMenu(self.instances)
|
||||||
|
install_menu.run()
|
||||||
|
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
print_instance_overview(
|
||||||
|
self.instances,
|
||||||
|
display_type=DisplayType.PRINTER_NAME,
|
||||||
|
show_headline=True,
|
||||||
|
show_index=True,
|
||||||
|
show_select_all=True,
|
||||||
|
)
|
||||||
|
printer_list = get_printer_selection(self.instances, True)
|
||||||
|
if printer_list is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
for printer in printer_list:
|
||||||
|
Logger.print_status(f"Uninstalling theme from {printer.cfg_dir} ...")
|
||||||
|
theme_dir = printer.cfg_dir.joinpath(".theme")
|
||||||
|
if not theme_dir.exists():
|
||||||
|
Logger.print_info(f"{theme_dir} not found. Skipping ...")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
shutil.rmtree(theme_dir)
|
||||||
|
Logger.print_ok("Theme successfully uninstalled!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error("Unable to uninstall theme")
|
||||||
|
Logger.print_error(e)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class MainsailThemeInstallMenu(BaseMenu):
|
||||||
|
THEMES_URL: str = (
|
||||||
|
"https://raw.githubusercontent.com/mainsail-crew/gb-docs/main/_data/themes.csv"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, instances: List[Klipper]):
|
||||||
|
super().__init__()
|
||||||
|
self.themes: List[ThemeData] = self.load_themes()
|
||||||
|
options = {f"{index}": self.install_theme for index in range(len(self.themes))}
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
self.instances = instances
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " [ Mainsail Theme Installer ] "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
line1 = f"{COLOR_CYAN}A preview of each Mainsail theme can be found here:{RESET_FORMAT}"
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line1:<62} |
|
||||||
|
| https://docs.mainsail.xyz/theming/themes |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
for i, theme in enumerate(self.themes):
|
||||||
|
i = f" {i}" if i < 10 else f"{i}"
|
||||||
|
row = f"{i}) [{theme.get('name')}]"
|
||||||
|
menu += f"| {row:<53} |\n"
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def load_themes(self) -> List[ThemeData]:
|
||||||
|
with urllib.request.urlopen(self.THEMES_URL) as response:
|
||||||
|
themes: List[ThemeData] = []
|
||||||
|
csv_data: str = response.read().decode().splitlines()
|
||||||
|
csv_reader = csv.DictReader(csv_data, delimiter=",")
|
||||||
|
for row in csv_reader:
|
||||||
|
row: ThemeData = row
|
||||||
|
themes.append(row)
|
||||||
|
|
||||||
|
return themes
|
||||||
|
|
||||||
|
def install_theme(self, **kwargs):
|
||||||
|
index = int(kwargs.get("opt_index"))
|
||||||
|
theme_data: ThemeData = self.themes[index]
|
||||||
|
theme_author: str = theme_data.get("author")
|
||||||
|
theme_repo: str = theme_data.get("repo")
|
||||||
|
theme_repo_url: str = f"https://github.com/{theme_author}/{theme_repo}"
|
||||||
|
|
||||||
|
print_instance_overview(
|
||||||
|
self.instances,
|
||||||
|
display_type=DisplayType.PRINTER_NAME,
|
||||||
|
show_headline=True,
|
||||||
|
show_index=True,
|
||||||
|
show_select_all=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
printer_list = get_printer_selection(self.instances, True)
|
||||||
|
if printer_list is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
repo_manager = RepoManager(theme_repo_url, "")
|
||||||
|
for printer in printer_list:
|
||||||
|
repo_manager.target_dir = printer.cfg_dir.joinpath(".theme")
|
||||||
|
repo_manager.clone_repo()
|
||||||
|
|
||||||
|
if len(theme_data.get("short_note", "")) > 1:
|
||||||
|
Logger.print_warn("Info from the creator:", prefix=False, start="\n")
|
||||||
|
Logger.print_info(theme_data.get("short_note"), prefix=False, end="\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def get_printer_selection(
|
||||||
|
instances: List[BaseInstance], is_install: bool
|
||||||
|
) -> Union[List[BaseInstance], None]:
|
||||||
|
options = [str(i) for i in range(len(instances))]
|
||||||
|
options.extend(["a", "A", "b", "B"])
|
||||||
|
|
||||||
|
if is_install:
|
||||||
|
q = "Select the printer to install the theme for"
|
||||||
|
else:
|
||||||
|
q = "Select the printer to remove the theme from"
|
||||||
|
selection = get_selection_input(q, options)
|
||||||
|
|
||||||
|
install_for = []
|
||||||
|
if selection == "b".lower():
|
||||||
|
return None
|
||||||
|
elif selection == "a".lower():
|
||||||
|
install_for.extend(instances)
|
||||||
|
else:
|
||||||
|
instance = instances[int(selection)]
|
||||||
|
install_for.append(instance)
|
||||||
|
|
||||||
|
return install_for
|
||||||
9
kiauh/extensions/mainsail_theme_installer/metadata.json
Normal file
9
kiauh/extensions/mainsail_theme_installer/metadata.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"index": 2,
|
||||||
|
"module": "mainsail_theme_installer_extension",
|
||||||
|
"maintained_by": "dw-0",
|
||||||
|
"display_name": "Mainsail Theme Installer",
|
||||||
|
"description": "Install Mainsail Themes maintained by the community."
|
||||||
|
}
|
||||||
|
}
|
||||||
18
kiauh/main.py
Normal file
18
kiauh/main.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
MainMenu().run()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
Logger.print_ok("\nHappy printing!\n", prefix=False)
|
||||||
21
kiauh/utils/__init__.py
Normal file
21
kiauh/utils/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
INVALID_CHOICE = "Invalid choice. Please select a valid value."
|
||||||
|
PRINTER_CFG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-cfg-backups")
|
||||||
|
|
||||||
|
# ================== NGINX =====================#
|
||||||
|
NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available")
|
||||||
|
NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled")
|
||||||
|
NGINX_CONFD = Path("/etc/nginx/conf.d")
|
||||||
6
kiauh/utils/assets/common_vars.conf
Normal file
6
kiauh/utils/assets/common_vars.conf
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# /etc/nginx/conf.d/common_vars.conf
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
95
kiauh/utils/assets/nginx_cfg
Normal file
95
kiauh/utils/assets/nginx_cfg
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
server {
|
||||||
|
listen %PORT%;
|
||||||
|
# uncomment the next line to activate IPv6
|
||||||
|
# listen [::]:%PORT%;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/%NAME%-access.log;
|
||||||
|
error_log /var/log/nginx/%NAME%-error.log;
|
||||||
|
|
||||||
|
# disable this section on smaller hardware like a pi zero
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_comp_level 4;
|
||||||
|
gzip_buffers 16 8k;
|
||||||
|
gzip_http_version 1.1;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/json application/xml;
|
||||||
|
|
||||||
|
# web_path from %NAME% static files
|
||||||
|
root %ROOT_DIR%;
|
||||||
|
|
||||||
|
index index.html;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# disable max upload size checks
|
||||||
|
client_max_body_size 0;
|
||||||
|
|
||||||
|
# disable proxy request buffering
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /websocket {
|
||||||
|
proxy_pass http://apiserver/websocket;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(printer|api|access|machine|server)/ {
|
||||||
|
proxy_pass http://apiserver$request_uri;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Scheme $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /webcam/ {
|
||||||
|
postpone_output 0;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_ignore_headers X-Accel-Buffering;
|
||||||
|
access_log off;
|
||||||
|
error_log off;
|
||||||
|
proxy_pass http://mjpgstreamer1/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /webcam2/ {
|
||||||
|
postpone_output 0;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_ignore_headers X-Accel-Buffering;
|
||||||
|
access_log off;
|
||||||
|
error_log off;
|
||||||
|
proxy_pass http://mjpgstreamer2/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /webcam3/ {
|
||||||
|
postpone_output 0;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_ignore_headers X-Accel-Buffering;
|
||||||
|
access_log off;
|
||||||
|
error_log off;
|
||||||
|
proxy_pass http://mjpgstreamer3/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /webcam4/ {
|
||||||
|
postpone_output 0;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_ignore_headers X-Accel-Buffering;
|
||||||
|
access_log off;
|
||||||
|
error_log off;
|
||||||
|
proxy_pass http://mjpgstreamer4/;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
kiauh/utils/assets/upstreams.conf
Normal file
25
kiauh/utils/assets/upstreams.conf
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# /etc/nginx/conf.d/upstreams.conf
|
||||||
|
upstream apiserver {
|
||||||
|
ip_hash;
|
||||||
|
server 127.0.0.1:7125;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream mjpgstreamer1 {
|
||||||
|
ip_hash;
|
||||||
|
server 127.0.0.1:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream mjpgstreamer2 {
|
||||||
|
ip_hash;
|
||||||
|
server 127.0.0.1:8081;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream mjpgstreamer3 {
|
||||||
|
ip_hash;
|
||||||
|
server 127.0.0.1:8082;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream mjpgstreamer4 {
|
||||||
|
ip_hash;
|
||||||
|
server 127.0.0.1:8083;
|
||||||
|
}
|
||||||
135
kiauh/utils/common.py
Normal file
135
kiauh/utils/common.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Literal, List, Type, Union
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils import PRINTER_CFG_BACKUP_DIR
|
||||||
|
from utils.constants import (
|
||||||
|
COLOR_CYAN,
|
||||||
|
RESET_FORMAT,
|
||||||
|
COLOR_YELLOW,
|
||||||
|
COLOR_GREEN,
|
||||||
|
COLOR_RED,
|
||||||
|
)
|
||||||
|
from utils.filesystem_utils import check_file_exist
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.system_utils import check_package_install, install_system_packages
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_date() -> Dict[Literal["date", "time"], str]:
|
||||||
|
"""
|
||||||
|
Get the current date |
|
||||||
|
:return: Dict holding a date and time key:value pair
|
||||||
|
"""
|
||||||
|
now: datetime = datetime.today()
|
||||||
|
date: str = now.strftime("%Y%m%d")
|
||||||
|
time: str = now.strftime("%H%M%S")
|
||||||
|
|
||||||
|
return {"date": date, "time": time}
|
||||||
|
|
||||||
|
|
||||||
|
def check_install_dependencies(deps: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Common helper method to check if dependencies are installed
|
||||||
|
and if not, install them automatically |
|
||||||
|
:param deps: List of strings of package names to check if installed
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
requirements = check_package_install(deps)
|
||||||
|
if requirements:
|
||||||
|
Logger.print_status("Installing dependencies ...")
|
||||||
|
Logger.print_info("The following packages need installation:")
|
||||||
|
for _ in requirements:
|
||||||
|
print(f"{COLOR_CYAN}● {_}{RESET_FORMAT}")
|
||||||
|
install_system_packages(requirements)
|
||||||
|
|
||||||
|
|
||||||
|
def get_install_status_common(
|
||||||
|
instance_type: Type[BaseInstance], repo_dir: Path, env_dir: Path
|
||||||
|
) -> Dict[Literal["status", "status_code", "instances"], Union[str, int]]:
|
||||||
|
"""
|
||||||
|
Helper method to get the installation status of software components,
|
||||||
|
which only consist of 3 major parts and if those parts exist, the
|
||||||
|
component can be considered as "installed". Typically, Klipper or
|
||||||
|
Moonraker match that criteria.
|
||||||
|
:param instance_type: The component type
|
||||||
|
:param repo_dir: the repository directory
|
||||||
|
:param env_dir: the python environment directory
|
||||||
|
:return: Dictionary with status string, statuscode and instance count
|
||||||
|
"""
|
||||||
|
im = InstanceManager(instance_type)
|
||||||
|
instances_exist = len(im.instances) > 0
|
||||||
|
status = [repo_dir.exists(), env_dir.exists(), instances_exist]
|
||||||
|
if all(status):
|
||||||
|
return {
|
||||||
|
"status": "Installed:",
|
||||||
|
"status_code": 1,
|
||||||
|
"instances": len(im.instances),
|
||||||
|
}
|
||||||
|
elif not any(status):
|
||||||
|
return {
|
||||||
|
"status": "Not installed!",
|
||||||
|
"status_code": 2,
|
||||||
|
"instances": len(im.instances),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"status": "Incomplete!",
|
||||||
|
"status_code": 3,
|
||||||
|
"instances": len(im.instances),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_install_status_webui(
|
||||||
|
install_dir: Path, nginx_cfg: Path, upstreams_cfg: Path, common_cfg: Path
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Helper method to get the installation status of webuis
|
||||||
|
like Mainsail or Fluidd |
|
||||||
|
:param install_dir: folder of the static webui files
|
||||||
|
:param nginx_cfg: the webuis NGINX config
|
||||||
|
:param upstreams_cfg: the required upstreams.conf
|
||||||
|
:param common_cfg: the required common_vars.conf
|
||||||
|
:return: formatted string, containing the status
|
||||||
|
"""
|
||||||
|
dir_exist = install_dir.exists()
|
||||||
|
nginx_cfg_exist = check_file_exist(nginx_cfg)
|
||||||
|
upstreams_cfg_exist = check_file_exist(upstreams_cfg)
|
||||||
|
common_cfg_exist = check_file_exist(common_cfg)
|
||||||
|
status = [dir_exist, nginx_cfg_exist]
|
||||||
|
general_nginx_status = [upstreams_cfg_exist, common_cfg_exist]
|
||||||
|
|
||||||
|
if all(status) and all(general_nginx_status):
|
||||||
|
return f"{COLOR_GREEN}Installed!{RESET_FORMAT}"
|
||||||
|
elif not all(status):
|
||||||
|
return f"{COLOR_RED}Not installed!{RESET_FORMAT}"
|
||||||
|
else:
|
||||||
|
return f"{COLOR_YELLOW}Incomplete!{RESET_FORMAT}"
|
||||||
|
|
||||||
|
|
||||||
|
def backup_printer_config_dir():
|
||||||
|
# local import to prevent circular import
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
instances: List[Klipper] = im.instances
|
||||||
|
bm = BackupManager()
|
||||||
|
|
||||||
|
for instance in instances:
|
||||||
|
name = f"config-{instance.data_dir_name}"
|
||||||
|
bm.backup_directory(
|
||||||
|
name,
|
||||||
|
source=instance.cfg_dir,
|
||||||
|
target=PRINTER_CFG_BACKUP_DIR,
|
||||||
|
)
|
||||||
24
kiauh/utils/constants.py
Normal file
24
kiauh/utils/constants.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pwd
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# text colors and formats
|
||||||
|
COLOR_WHITE = "\033[37m" # white
|
||||||
|
COLOR_MAGENTA = "\033[35m" # magenta
|
||||||
|
COLOR_GREEN = "\033[92m" # bright green
|
||||||
|
COLOR_YELLOW = "\033[93m" # bright yellow
|
||||||
|
COLOR_RED = "\033[91m" # bright red
|
||||||
|
COLOR_CYAN = "\033[96m" # bright cyan
|
||||||
|
RESET_FORMAT = "\033[0m" # reset format
|
||||||
|
# current user
|
||||||
|
CURRENT_USER = pwd.getpwuid(os.getuid())[0]
|
||||||
|
SYSTEMD = Path("/etc/systemd/system")
|
||||||
304
kiauh/utils/filesystem_utils.py
Normal file
304
kiauh/utils/filesystem_utils.py
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from typing import List, TypeVar, Tuple, Optional
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils import (
|
||||||
|
NGINX_SITES_AVAILABLE,
|
||||||
|
MODULE_PATH,
|
||||||
|
NGINX_CONFD,
|
||||||
|
NGINX_SITES_ENABLED,
|
||||||
|
)
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
B = TypeVar("B", Klipper, Moonraker)
|
||||||
|
ConfigOption = Tuple[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
def check_file_exist(file_path: Path, sudo=False) -> bool:
|
||||||
|
"""
|
||||||
|
Helper function for checking the existence of a file |
|
||||||
|
:param file_path: the absolute path of the file to check
|
||||||
|
:param sudo: use sudo if required
|
||||||
|
:return: True, if file exists, otherwise False
|
||||||
|
"""
|
||||||
|
if sudo:
|
||||||
|
try:
|
||||||
|
command = ["sudo", "find", file_path]
|
||||||
|
subprocess.check_output(command, stderr=subprocess.DEVNULL)
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if file_path.exists():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_symlink(source: Path, target: Path, sudo=False) -> None:
|
||||||
|
try:
|
||||||
|
cmd = ["ln", "-sf", source, target]
|
||||||
|
if sudo:
|
||||||
|
cmd.insert(0, "sudo")
|
||||||
|
subprocess.run(cmd, stderr=subprocess.PIPE, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Failed to create symlink: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def remove_file(file_path: Path, sudo=False) -> None:
|
||||||
|
try:
|
||||||
|
cmd = f"{'sudo ' if sudo else ''}rm -f {file_path}"
|
||||||
|
subprocess.run(cmd, stderr=subprocess.PIPE, check=True, shell=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log = f"Cannot remove file {file_path}: {e.stderr.decode()}"
|
||||||
|
Logger.print_error(log)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def unzip(filepath: Path, target_dir: Path) -> None:
|
||||||
|
"""
|
||||||
|
Helper function to unzip a zip-archive into a target directory |
|
||||||
|
:param filepath: the path to the zip-file to unzip
|
||||||
|
:param target_dir: the target directory to extract the files into
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
with ZipFile(filepath, "r") as _zip:
|
||||||
|
_zip.extractall(target_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_upstream_nginx_cfg() -> None:
|
||||||
|
"""
|
||||||
|
Creates an upstream.conf in /etc/nginx/conf.d
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
source = MODULE_PATH.joinpath("assets/upstreams.conf")
|
||||||
|
target = NGINX_CONFD.joinpath("upstreams.conf")
|
||||||
|
try:
|
||||||
|
command = ["sudo", "cp", source, target]
|
||||||
|
subprocess.run(command, stderr=subprocess.PIPE, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log = f"Unable to create upstreams.conf: {e.stderr.decode()}"
|
||||||
|
Logger.print_error(log)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def copy_common_vars_nginx_cfg() -> None:
|
||||||
|
"""
|
||||||
|
Creates a common_vars.conf in /etc/nginx/conf.d
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
source = MODULE_PATH.joinpath("assets/common_vars.conf")
|
||||||
|
target = NGINX_CONFD.joinpath("common_vars.conf")
|
||||||
|
try:
|
||||||
|
command = ["sudo", "cp", source, target]
|
||||||
|
subprocess.run(command, stderr=subprocess.PIPE, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log = f"Unable to create upstreams.conf: {e.stderr.decode()}"
|
||||||
|
Logger.print_error(log)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def create_nginx_cfg(name: str, port: int, root_dir: Path) -> None:
|
||||||
|
"""
|
||||||
|
Creates an NGINX config from a template file and replaces all placeholders
|
||||||
|
:param name: name of the config to create
|
||||||
|
:param port: listen port
|
||||||
|
:param root_dir: directory of the static files
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
tmp = Path.home().joinpath(f"{name}.tmp")
|
||||||
|
shutil.copy(MODULE_PATH.joinpath("assets/nginx_cfg"), tmp)
|
||||||
|
with open(tmp, "r+") as f:
|
||||||
|
content = f.read()
|
||||||
|
content = content.replace("%NAME%", name)
|
||||||
|
content = content.replace("%PORT%", str(port))
|
||||||
|
content = content.replace("%ROOT_DIR%", str(root_dir))
|
||||||
|
f.seek(0)
|
||||||
|
f.write(content)
|
||||||
|
f.truncate()
|
||||||
|
|
||||||
|
target = NGINX_SITES_AVAILABLE.joinpath(name)
|
||||||
|
try:
|
||||||
|
command = ["sudo", "mv", tmp, target]
|
||||||
|
subprocess.run(command, stderr=subprocess.PIPE, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log = f"Unable to create '{target}': {e.stderr.decode()}"
|
||||||
|
Logger.print_error(log)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def read_ports_from_nginx_configs() -> List[str]:
|
||||||
|
"""
|
||||||
|
Helper function to iterate over all NGINX configs and read all ports defined for listen
|
||||||
|
:return: A sorted list of listen ports
|
||||||
|
"""
|
||||||
|
if not NGINX_SITES_ENABLED.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
port_list = []
|
||||||
|
for config in NGINX_SITES_ENABLED.iterdir():
|
||||||
|
with open(config, "r") as cfg:
|
||||||
|
lines = cfg.readlines()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.replace("default_server", "")
|
||||||
|
line = re.sub(r"[;:\[\]]", "", line.strip())
|
||||||
|
if line.startswith("listen") and line.split()[-1] not in port_list:
|
||||||
|
port_list.append(line.split()[-1])
|
||||||
|
|
||||||
|
return sorted(port_list, key=lambda x: int(x))
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_port(port: str, ports_in_use: List[str]) -> bool:
|
||||||
|
return port.isdigit() and port not in ports_in_use
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_free_port(ports_in_use: List[str]) -> str:
|
||||||
|
valid_ports = set(range(80, 7125))
|
||||||
|
used_ports = set(map(int, ports_in_use))
|
||||||
|
|
||||||
|
return str(min(valid_ports - used_ports))
|
||||||
|
|
||||||
|
|
||||||
|
def add_config_section(
|
||||||
|
section: str,
|
||||||
|
instances: List[B],
|
||||||
|
options: Optional[List[ConfigOption]] = None,
|
||||||
|
) -> None:
|
||||||
|
for instance in instances:
|
||||||
|
cfg_file = instance.cfg_file
|
||||||
|
Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...")
|
||||||
|
|
||||||
|
if not Path(cfg_file).exists():
|
||||||
|
Logger.print_warn(f"'{cfg_file}' not found!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
cm = ConfigManager(cfg_file)
|
||||||
|
if cm.config.has_section(section):
|
||||||
|
Logger.print_info("Section already exist. Skipped ...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
cm.config.add_section(section)
|
||||||
|
|
||||||
|
if options is not None:
|
||||||
|
for option in options:
|
||||||
|
cm.config.set(section, option[0], option[1])
|
||||||
|
|
||||||
|
cm.write_config()
|
||||||
|
|
||||||
|
|
||||||
|
def add_config_section_at_top(section: str, instances: List[B]):
|
||||||
|
for instance in instances:
|
||||||
|
tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
||||||
|
tmp_cfg_path = Path(tmp_cfg.name)
|
||||||
|
cmt = ConfigManager(tmp_cfg_path)
|
||||||
|
cmt.config.add_section(section)
|
||||||
|
cmt.write_config()
|
||||||
|
tmp_cfg.close()
|
||||||
|
|
||||||
|
cfg_file = instance.cfg_file
|
||||||
|
with open(cfg_file, "r") as org:
|
||||||
|
org_content = org.readlines()
|
||||||
|
with open(tmp_cfg_path, "a") as tmp:
|
||||||
|
tmp.writelines(org_content)
|
||||||
|
|
||||||
|
cfg_file.unlink()
|
||||||
|
tmp_cfg_path.rename(cfg_file)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_config_section(section: str, instances: List[B]) -> None:
|
||||||
|
for instance in instances:
|
||||||
|
cfg_file = instance.cfg_file
|
||||||
|
Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...")
|
||||||
|
|
||||||
|
if not Path(cfg_file).exists():
|
||||||
|
Logger.print_warn(f"'{cfg_file}' not found!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
cm = ConfigManager(cfg_file)
|
||||||
|
if not cm.config.has_section(section):
|
||||||
|
Logger.print_info("Section does not exist. Skipped ...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
cm.config.remove_section(section)
|
||||||
|
cm.write_config()
|
||||||
|
|
||||||
|
|
||||||
|
def patch_moonraker_conf(
|
||||||
|
moonraker_instances: List[Moonraker],
|
||||||
|
name: str,
|
||||||
|
section_name: str,
|
||||||
|
template_file: str,
|
||||||
|
) -> None:
|
||||||
|
for instance in moonraker_instances:
|
||||||
|
cfg_file = instance.cfg_file
|
||||||
|
Logger.print_status(f"Add {name} update section to '{cfg_file}' ...")
|
||||||
|
|
||||||
|
if not Path(cfg_file).exists():
|
||||||
|
Logger.print_warn(f"'{cfg_file}' not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
cm = ConfigManager(cfg_file)
|
||||||
|
if cm.config.has_section(section_name):
|
||||||
|
Logger.print_info("Section already exist. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
template = MODULE_PATH.joinpath("assets", template_file)
|
||||||
|
with open(template, "r") as t:
|
||||||
|
template_content = "\n"
|
||||||
|
template_content += t.read()
|
||||||
|
|
||||||
|
with open(cfg_file, "a") as f:
|
||||||
|
f.write(template_content)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_nginx_config(name: str) -> None:
|
||||||
|
Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...")
|
||||||
|
try:
|
||||||
|
remove_file(NGINX_SITES_AVAILABLE.joinpath(name), True)
|
||||||
|
remove_file(NGINX_SITES_ENABLED.joinpath(name), True)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log = f"Unable to remove NGINX config '{name}':\n{e.stderr.decode()}"
|
||||||
|
Logger.print_error(log)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_nginx_logs(name: str) -> None:
|
||||||
|
Logger.print_status(f"Removing NGINX logs for {name.capitalize()} ...")
|
||||||
|
try:
|
||||||
|
remove_file(Path(f"/var/log/nginx/{name}-access.log"), True)
|
||||||
|
remove_file(Path(f"/var/log/nginx/{name}-error.log"), True)
|
||||||
|
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
instances: List[Klipper] = im.instances
|
||||||
|
if not instances:
|
||||||
|
return
|
||||||
|
|
||||||
|
for instance in instances:
|
||||||
|
remove_file(instance.log_dir.joinpath(f"{name}-access.log"))
|
||||||
|
remove_file(instance.log_dir.joinpath(f"{name}-error.log"))
|
||||||
|
|
||||||
|
except (OSError, subprocess.CalledProcessError) as e:
|
||||||
|
Logger.print_error(f"Unable to remove NGINX logs:\n{e}")
|
||||||
91
kiauh/utils/git_utils.py
Normal file
91
kiauh/utils/git_utils.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
from http.client import HTTPResponse
|
||||||
|
from json import JSONDecodeError
|
||||||
|
from subprocess import CalledProcessError, PIPE, run
|
||||||
|
from typing import List, Type
|
||||||
|
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.input_utils import get_number_input, get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_tags(repo_path: str) -> List[str]:
|
||||||
|
try:
|
||||||
|
url = f"https://api.github.com/repos/{repo_path}/tags"
|
||||||
|
with urllib.request.urlopen(url) as r:
|
||||||
|
response: HTTPResponse = r
|
||||||
|
if response.getcode() != 200:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Error retrieving tags: HTTP status code {response.getcode()}"
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
data = json.loads(response.read())
|
||||||
|
return [item["name"] for item in data]
|
||||||
|
except (JSONDecodeError, TypeError) as e:
|
||||||
|
Logger.print_error(f"Error while processing the response: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_tag(repo_path: str) -> str:
|
||||||
|
"""
|
||||||
|
Gets the latest stable tag of a GitHub repostiory
|
||||||
|
:param repo_path: path of the GitHub repository - e.g. `<owner>/<name>`
|
||||||
|
:return: tag or empty string
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if len(latest_tag := get_tags(repo_path)) > 0:
|
||||||
|
return latest_tag[0]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error("Error while getting the latest tag")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_unstable_tag(repo_path: str) -> str:
|
||||||
|
"""
|
||||||
|
Gets the latest unstable (alpha, beta, rc) tag of a GitHub repository
|
||||||
|
:param repo_path: path of the GitHub repository - e.g. `<owner>/<name>`
|
||||||
|
:return: tag or empty string
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if len(unstable_tags := [t for t in get_tags(repo_path) if "-" in t]) > 0:
|
||||||
|
return unstable_tags[0]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error("Error while getting the latest unstable tag")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def rollback_repository(repo_dir: str, instance: Type[BaseInstance]) -> None:
|
||||||
|
q1 = "How many commits do you want to roll back"
|
||||||
|
amount = get_number_input(q1, 1, allow_go_back=True)
|
||||||
|
|
||||||
|
im = InstanceManager(instance)
|
||||||
|
|
||||||
|
Logger.print_warn("Do not continue if you have ongoing prints!", start="\n")
|
||||||
|
Logger.print_warn(
|
||||||
|
f"All currently running {im.instance_type.__name__} services will be stopped!"
|
||||||
|
)
|
||||||
|
if not get_confirm(
|
||||||
|
f"Roll back {amount} commit{'s' if amount > 1 else ''}",
|
||||||
|
default_choice=False,
|
||||||
|
allow_go_back=True,
|
||||||
|
):
|
||||||
|
Logger.print_info("Aborting roll back ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
im.stop_all_instance()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = ["git", "reset", "--hard", f"HEAD~{amount}"]
|
||||||
|
run(cmd, cwd=repo_dir, check=True, stdout=PIPE, stderr=PIPE)
|
||||||
|
Logger.print_ok(f"Rolled back {amount} commits!", start="\n")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"An error occured during repo rollback:\n{e}")
|
||||||
|
|
||||||
|
im.start_all_instance()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user