Compare commits

...

175 Commits

Author SHA1 Message Date
dw-0
03b940e569 Merge ad56b51e70 into 8e7d4db988 2024-01-28 22:27:38 +01:00
dw-0
ad56b51e70 feat(LogUpload): implement log upload feature
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-28 22:27:38 +01:00
dw-0
c6999f1990 refactor(kiauh): if self.options is an empty dict, return invalid input error message.
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-28 22:26:54 +01:00
dw-0
bc30cf418b refactor(kiauh): add option index parameter to method calls from menus
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-28 22:25:22 +01:00
dw-0
ee81ee4c0c fix(Mainsail): correctly handle invalid config value for default_port
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-28 15:35:00 +01:00
dw-0
35911604af chore(kiauh): update copyright
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-28 12:54:37 +01:00
dw-0
77f1089041 chore(kiauh): reformat code / rename method
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-28 12:54:35 +01:00
Anna
8e7d4db988 fix: OctoPrint logo in README.md (#424)
* Fix OctoPrint logo in Readme

Fixed the broken link to the OctoPrint logo in the Readme under the Sources section. Now uses the logo present in the octoprint docs directory.

* Update OctoPrint logo to "raw" cdn link

* Changed ?raw=true link to raw.githubusercontent.com

---------

Co-authored-by: dw-0 <th33xitus@gmail.com>
2024-01-23 22:47:37 +01:00
jneilliii
8f960495ba fix: OctoPrint logo in README.md (#423) 2024-01-22 17:57:37 +01:00
dw-0
095823bf28 chore(kiauh): update FUNDING.yml
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-01 20:02:29 +01:00
dw-0
397038e43e chore(kiauh): add .idea to .gitignore
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-01 19:55:47 +01:00
dw-0
061e222664 chore(kiauh): update copyright
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-01 19:54:13 +01:00
dw-0
3f5ff50d69 refactor(kiauh): use sudo true instead of sudo -v to prevent password prompts
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-01-01 19:51:56 +01:00
dw-0
7820155094 refactor(Klipper): add some exception handling
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-29 19:47:45 +01:00
dw-0
c28d5c28b9 refactor(KIAUH): use pythons own venv module to create a venv
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-29 19:39:45 +01:00
dw-0
cda6d31a7c fix(RepoManager): check if git dir exists
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-29 18:57:53 +01:00
dw-0
9a657daffd feat(KIAUH): show Mainsail in UpdateMenu
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-28 18:05:43 +01:00
dw-0
85b4b68f16 refactor(Klipper/Moonraker): install new packages during updates
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-28 13:49:28 +01:00
dw-0
dfbce3b489 feat(KIAUH): show commit in UpdateMenu
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-28 13:38:24 +01:00
dw-0
f3b0e45e39 refactor(Klipper): refactor klipper_setup to reduce cognitive complexity
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-27 23:49:34 +01:00
dw-0
83e5d9c0d5 refactor(Klipper/Moonraker): remove obsolete method calls
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-27 15:58:37 +01:00
dw-0
8f44187568 feat(Moonraker): enable Mainsail remote mode after multi instance setup
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-27 15:42:46 +01:00
dw-0
625a808484 fix(InstanceManager): return an updated list when getting the instances property
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-27 14:09:51 +01:00
dw-0
ad0dbf63b8 refactor(Mainsail): enable remote mode if moonraker multi instance
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-27 00:11:11 +01:00
dw-0
9dedf38079 refactor(KIAUH): big refactor of instance handling
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-26 23:37:35 +01:00
dw-0
1b4c76d080 fix(KIAUH): more file path handling improvements
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 22:31:18 +01:00
dw-0
d20d82aeac fix(Mainsail): proper check if config exists
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 22:06:40 +01:00
dw-0
16a28ffda0 fix(Klipper/Moonraker): config files now always have a Path, are never None anymore
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 22:03:44 +01:00
dw-0
a9367cc064 fix(Klipper): remove obsolete method parameter
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 22:01:18 +01:00
dw-0
b165d88855 fix(Moonraker): missing return statement if all requirements met
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 20:57:22 +01:00
dw-0
6c59d58193 refactor(KIAUH): use red dash instead of "Unknown" if repo info not available
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 20:44:04 +01:00
dw-0
b4f5c3c1ac refactor(Mainsail): remove mainsail.zip after extracting content
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 20:16:38 +01:00
dw-0
b69ecbc9b5 fix(KIAUH): wrong logic in status detection
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 19:56:43 +01:00
dw-0
fc9fa39eee refactor(Mainsail): use same wording in MainsailRemoveMenu
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 19:43:30 +01:00
dw-0
142b4498a3 refactor(Klipper): rework remove process
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 19:39:19 +01:00
dw-0
012b6c4bb7 refactor(Moonraker): rework remove process
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 15:12:08 +01:00
dw-0
8aeb01aca0 refactor(KIAUH): use pathlib instead of os where possible. consistent use of pathlib.
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-25 01:16:16 +01:00
dw-0
da533fdd67 refactor(KIAUH): use util functions for Klipper and Moonraker to get their status
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-24 14:35:25 +01:00
dw-0
8cb0754296 feat(KIAUH): show Mainsail install status
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-24 14:23:57 +01:00
dw-0
7a6590e86a refactor(Mainsail): rework config.json backup
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-24 13:53:55 +01:00
dw-0
2f0feb317e refactor(BackupManager): rework backup structure and implement single file backup method
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-24 12:58:41 +01:00
dw-0
b9479db766 feat(KIAUH): show installation status of Klipper and Moonraker in MainMenu
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-23 19:59:39 +01:00
dw-0
14132fc34b refactor(ConfigManager): automatically read config upon ConfigManager instance init
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-23 19:36:03 +01:00
dw-0
3d5e83d5ab refactor(Mainsail): remove specific methods and replace by generic ones
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-22 22:54:29 +01:00
dw-0
edd5f5c6fd refactor(KIAUH): refactor RemoveMenu
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-22 22:43:29 +01:00
dw-0
8ff0b9d81d refactor(Mainsail): refactor methods for removing and checking files
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-22 22:39:43 +01:00
dw-0
22e8e314db fix(Mainsail): implement missing mainsail cfg symlinking
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-22 21:41:15 +01:00
dw-0
12bd8eb799 feat(KIAUH): move filesystem related methods to own module
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-22 21:25:20 +01:00
dw-0
4915896099 feat(Mainsail): remove Mainsail
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-21 22:53:41 +01:00
dw-0
cd38970bbd refactor(Mainsail): move some functions to a mainsail utils module
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-20 22:35:29 +01:00
dw-0
b8640f45a6 refactor(Klipper): refactor example printer.cfg creation
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 23:30:38 +01:00
dw-0
5fb4444f03 refactor(Moonraker): refactor example moonraker.conf creation
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 23:30:15 +01:00
dw-0
926ba1acb4 feat(ConfigManager): implement own ConfigParser write() method
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 21:49:09 +01:00
dw-0
c2e7ee98df feat(Mainsail): implement Mainsail installer
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 18:08:18 +01:00
dw-0
3865266da1 refactor(RepoManager): default to master branch if none is provided
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 18:07:18 +01:00
dw-0
b83f642a13 refactor(ConfigManager): logging can be silenced
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 18:03:42 +01:00
dw-0
30b4414469 feat(Klipper): create example printer.cfg if wanted
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 18:00:16 +01:00
dw-0
1178d3c730 refactor(Moonraker): skip selection dialog if there is only 1 klipper instance
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 16:18:44 +01:00
dw-0
59d8867c8c fix(kiauh): copy&paste issue in repo url for Moonraker
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 16:12:43 +01:00
dw-0
80a953a587 fix(Moonraker): typo in python version check
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 16:10:20 +01:00
dw-0
a80f0bb0e8 feat(utils): add several util methods
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 14:42:53 +01:00
dw-0
78cefddb2e feat(InstanceManager): add restart service method
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 13:26:22 +01:00
dw-0
b20613819e feat(Logger): add "start" parameter
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 13:00:46 +01:00
dw-0
5ebe941125 readme: don't recommend a specific architecture anymore
Fixes #403 

Both 32bit and 64bit images were reported to work fine. So we don't need a strict 32bit recommendation anymore which may confuses users, why they should pick 32bit over 64bit.

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 12:18:04 +01:00
dw-0
f5eb9486cc refactor(kiauh): use sudo true instead of sudo -v to prevent password prompts
Fixes #395 

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 12:10:36 +01:00
dw-0
1836beab42 feat(klipper): add getter for specific properties
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-17 00:01:25 +01:00
dw-0
545397f598 feat(kiauh): fix typo in check_package_install
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-16 17:38:01 +01:00
dw-0
f709cf84e7 feat(kiauh): add helper methods for downloading files
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-16 15:41:48 +01:00
dw-0
f62c10dc8b feat(kiauh): add helper methods to check for installed packages
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-16 15:38:23 +01:00
Stefan Dej
7a9e752f9c fix: fix typo in example.printer.cfg (#412)
rename virtual_sd_card to virtual_sdcard

Signed-off-by: Stefan Dej <meteyou@gmail.com>
2023-12-12 08:21:51 +01:00
CODeRUS
30bc56b198 feat(advanced): add selection between flash/serialflash methods (#410) 2023-12-10 16:57:59 +01:00
dw-0
b2567995de feat(klipper): add virtual_sd_card_block to example printer.cfg (#411) 2023-12-10 15:12:28 +01:00
dw-0
e121ba8a62 feat(Moonraker): add python version check
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-04 22:35:36 +01:00
dw-0
9a1a66aa64 docs(utils): add docstrings
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-04 21:35:28 +01:00
dw-0
420b193f4b feat(Moonraker): implement Moonraker
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-03 23:13:28 +01:00
dw-0
de20f0c412 refactor(ConfigManager): allow to take in any config file
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-03 23:10:43 +01:00
dw-0
57f34b07c6 refactor(utils): add more util functions
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-03 23:09:43 +01:00
dw-0
e35e44a76a refactor(kiauh): move create_folders to BaseInstance
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-03 23:08:41 +01:00
dw-0
bfb10c742b refactor(kiauh): reword print_info to print_status and implement new print_info method
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-03 23:06:30 +01:00
dw-0
458c89a78a fix(InstanceManager): print service name instead of suffix only
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-12-02 16:52:30 +01:00
dw-0
6128e35d45 refactor(kiauh): rework menu formatting logic
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-15 22:58:54 +01:00
dw-0
279d000bb0 refactor(kiauh): specify python3 in shebang
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-14 21:31:25 +01:00
dw-0
a4a3d5eecb feat(BackupManager): implement simple backup manager
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-14 21:28:13 +01:00
dw-0
1392ca9f82 refactor(klipper): pass the script path as a Path to the parse function
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-13 20:07:21 +01:00
dw-0
47121f6875 refactor(utils): clean up, add comments
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-13 20:06:48 +01:00
dw-0
d0d2404132 refactor(kiauh): move core modules to core package
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-12 23:28:05 +01:00
dw-0
6ed5395f17 feat(klipper): check for brltty-udev too
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-12 01:26:43 +01:00
dw-0
be805c169b feat(klipper): allow keeping klipper and klipper-env dir during uninstall
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-12 01:13:39 +01:00
dw-0
eaf12db27e fix(klipper): allow go back when asked for new instances
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-12 00:39:10 +01:00
dw-0
fe8767113b refactor(klipper): rework dialogs
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-12 00:37:25 +01:00
dw-0
2148d95cf4 fix(InstanceManager): return None for suffix if there is none
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-12 00:10:52 +01:00
dw-0
682be48e8d fix(InstanceManager): instance_service should be service file name
remove debug prints

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-12 00:01:21 +01:00
dw-0
68369753fd refactor(InstanceManager): rework
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-11 23:56:27 +01:00
dw-0
44ed3b6ddf feat(kiauh): add .iml to gitignore
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-11 23:53:42 +01:00
dw-0
e12e578098 refactor(klipper): rewrite dialogs
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-05 23:13:16 +01:00
dw-0
515a42f098 feat(klipper): implement update function
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-05 16:15:19 +01:00
dw-0
f9ecad0eca refactor(klipper): use name "klipper" for single instance setup
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-05 16:12:17 +01:00
dw-0
fb09acf660 refactor(utils): reduce complexity
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-02 22:52:34 +01:00
dw-0
093da73dd1 refactor(klipper): use constants for commonly used strings
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-11-02 22:37:17 +01:00
dw-0
c9e8c4807e feat(klipper): convert single to multi instance
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-31 20:54:44 +01:00
dw-0
6fcd7a3f08 refactor(klipper): improve disruptive package handling
fix #293

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-31 15:09:31 +01:00
dw-0
09e874214b feat(ConfigManager): implement ConfigManager
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-29 17:19:26 +01:00
dw-0
623bd7553b feat(RepoManager): implement RepoManager
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-29 13:25:21 +01:00
dw-0
1e0c74b549 style: rename input functions
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-29 01:29:27 +02:00
dw-0
358c666da9 feat(style): use black code style / formatter
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-29 00:31:34 +02:00
dw-0
84a530be7d fix(klipper): handle disruptive system packages/services
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-28 23:52:51 +02:00
dw-0
bfff3019cb fix(InstanceManager): fix TypeError if instance name is None
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-28 22:10:07 +02:00
dw-0
2a100c2934 feat(klipper): check for required user-groups
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-28 21:31:31 +02:00
dw-0
25dfbb83df fix(kiauh): display custom repo dialog again after user interaction
fix #394

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-28 20:48:57 +02:00
dw-0
ce0daa52ae feat(klipper): implement instance manager and klipper installer in python
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-10-26 13:58:31 +02:00
Sineos
899b204dc7 refactor: show full path of mcu (#391) 2023-10-15 10:08:41 +02:00
Kenneth Jiang
5cf4b018fc refactor: change wording now that moonraker-obico can link printer using auto-discovery (#389) 2023-10-12 20:26:30 +02:00
dw-0
ae9d1b98da feat(klipper): remove/disable brltty and ModemManager during installation (#387) 2023-10-01 18:30:01 +02:00
Kenneth Jiang
16d3388ff2 refactor: make the 'Obico for Klipper' name consistent (#385) 2023-09-29 20:20:21 +02:00
th33xitus
b88d0085ba fix(backups): fix broken config target folder name
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-09-04 21:17:10 +02:00
th33xitus
0b6613e464 fix(backups): fix broken moonraker database target folder name
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-09-04 21:02:22 +02:00
th33xitus
d99cda544a fix(backups): add backup before update function for KlipperScreen
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-08-30 19:48:55 +02:00
th33xitus
a50dce20de fix(backups): add backup function for OctoEverywhere
fix #380

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-08-30 19:48:53 +02:00
th33xitus
f45da66e9e fix(crowsnest): uninstaller exited KIAUH if crowsnest not installed
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-08-26 18:31:37 +02:00
th33xitus
2822499344 refactor(copyright): update copyright comment
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-08-26 18:11:00 +02:00
th33xitus
c777ba3e6b refactor(log-upload): update log upload functions and re-enable access
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-08-26 18:06:14 +02:00
th33xitus
9f410450d7 refactor(backups): update backup functions for config and moonraker database and enable backup-before-update again
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-08-23 23:22:23 +02:00
dw-0
0497d49066 Update README.md 2023-08-15 16:42:38 +02:00
dw-0
229da227b0 Update FUNDING.yml
use updated ko-fi username
2023-08-14 23:06:44 +02:00
th33xitus
65854c8da6 fix(updates): make update all function also update system again
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-07-22 23:24:29 +02:00
Kenneth Jiang
5985646633 refactor(obico): move dependency handling to obico's own install script (#361) 2023-07-10 17:44:05 +02:00
th33xitus
979c39dc02 refactor(mainsail/fluidd): allow reading of version from release_info.json
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-07-01 20:59:46 +02:00
th33xitus
197058bd00 changelog: update Changelog
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-06-17 21:19:49 +02:00
th33xitus
d3b5122ebb refactor(UI): move version and added changelog link to own row in main menu
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-06-17 21:11:23 +02:00
th33xitus
8ce4daf403 refactor(klipper): pre-select python3 for klipper install
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-06-17 19:51:41 +02:00
th33xitus
b0a65fe14e refactor(UI): remove warn message
more than half a year is gone, the warning should not be that important anymore

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-06-17 19:49:57 +02:00
Patrick Schmidt
98866caefa feat: add Mobileraker (#343)
Co-authored-by: th33xitus <th33xitus@googlemail.com>
2023-06-17 19:34:26 +02:00
phizev
345b7b66a3 refactor: use service specific directories in templates (#355) 2023-06-17 18:22:16 +02:00
cravl-dev
8eb2924832 refactor: update package lists only when stale (#346)
Co-authored-by: th33xitus <th33xitus@googlemail.com>
2023-06-17 18:08:24 +02:00
Kyriel Abad
5d7debd65e readme: fix typos in README.md (#352) 2023-06-10 23:00:23 +02:00
th33xitus
7df3dd489f fix(mainsail/fluidd): show correct version number in update menu
fixes #350

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-06-07 21:48:17 +02:00
marbocub
0cd058320f feat: allow to install Mainsail/Fluidd without Moonraker (#347)
Co-authored-by: th33xitus <th33xitus@googlemail.com>
2023-06-06 21:09:47 +02:00
th33xitus
bcbb185bd7 readme: update README.md
also fixes #327
2023-06-04 21:57:56 +02:00
CODeRUS
477f3ca72c feat: flash DFU device in HID mode (#337)
Co-authored-by: th33xitus <th33xitus@googlemail.com>
2023-06-04 21:23:32 +02:00
th33xitus
c19acb1694 fix(mainsail/fluidd): fall back to latest stable url if fetching tags fails (#348) 2023-06-04 19:12:53 +02:00
Jookia
8228943850 fix(klipper): if set, use custom branch to check for update (#332) 2023-04-16 08:56:18 +02:00
Stefan Dej
5b890fb0fb refactor(mainsail): update themes.csv url for mainsail themes (#329) 2023-04-13 17:59:14 +02:00
Piotr Banasik
7989cec8d4 fix(crowsnest): override installers BASE_USER to current user (#317) 2023-03-19 11:50:06 +01:00
th33xitus
858301aa9a fix: temporary disable backup_before_update
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-03-18 18:15:11 +01:00
Quinn Damerell
ae9e79c579 feat(octoeverywhere): add OE to the "update all" action (#311) 2023-03-11 23:52:46 +01:00
th33xitus
1215446a6c feat(octoeverywhere): implement update function (#310) 2023-03-11 20:25:17 +01:00
th33xitus
8526acf8b6 chore: update copyright notice
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-03-10 20:06:06 +01:00
th33xitus
cc27aaec7c readme: add prerequisites to readme (#309) 2023-03-08 20:07:00 +01:00
Quinn Damerell
1e9493461c feat: add OctoEverywhere for Klipper (#300)
Co-authored-by: th33xitus <th33xitus@googlemail.com>
2023-03-05 17:19:20 +01:00
th33xitus
31616ebad5 fix(crowsnest): silence grep error output in main menu
fixes #308

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-03-04 16:01:06 +01:00
Stephan Wendel
faf56ed1b1 refactor(crowsnest): improve performance in crowsnest.sh (#306) 2023-02-28 18:37:43 +01:00
th33xitus
d6837af2a2 refactor(klipper): implement blacklisted service names (#304) 2023-02-28 18:29:21 +01:00
Pedro Lamas
afe6f7499a feat(fluidd): use fluidd-config repo for downloading fluidd macros (#305) 2023-02-28 16:33:32 +01:00
Pedro Lamas
e3ed223b5c fix: always use system home directory (#303) 2023-02-28 15:30:42 +01:00
th33xitus
fd27db28d4 feat(mainsail): use mainsail-config repo for downloading mainsail macros (#301) 2023-02-26 16:29:01 +01:00
Michael Bravo
68a02ad3f5 fix(mainsail): set increased read timeout on API endpoint (#294) 2023-02-25 12:59:31 +01:00
CODeRUS
99b7672dc9 fix(telegram bot): Fix service restart (#296) 2023-02-14 19:40:11 +01:00
th33xitus
bb3ec79756 revert(mjpg-streamer): re-add uninstall option for mjpg-streamer
fix #291

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-02-05 08:57:15 +01:00
Kenneth Jiang
ce595abd60 refactor(obico): remove "-s" option from moonraker-obico/install.sh invocation (#292) 2023-02-05 08:50:06 +01:00
th33xitus
c79dc280e3 refactor(moonraker): update cors domains in moonraker.conf template
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-02-05 08:47:46 +01:00
Stephan Wendel
7aa186e8b9 feat: replace mjpg-streamer with crowsnest (#286)
Co-authored-by: th33xitus <th33xitus@googlemail.com>
2023-02-03 21:59:59 +01:00
th33xitus
8493269c6f refactor: hide currently still disabled functions from the menus
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2023-01-08 11:05:14 +01:00
th33xitus
150ef0142f refactor(mainsail/fluidd): move tag urls from globals to respective files
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2022-12-14 19:40:12 +01:00
th33xitus
f70faa52cc fix(klipper): update python dialog description
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2022-12-08 19:25:58 +01:00
CODeRUS
e796f74640 refactor(klipper): recommend Python3 for Klipper installation (#246) 2022-12-07 18:42:51 +01:00
Thomas Lété
2c9f5bed60 fix: pull mainsail.cfg from correct location (#272) 2022-12-07 17:34:01 +01:00
th33xitus
e9c23ca93e fix(klipper): use correct value for py_ver variable in update function
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2022-11-14 19:50:55 +01:00
th33xitus
67afa26ed7 refactor(klipper): full refactor of klipper install/remove part (#264) 2022-11-13 15:51:22 +01:00
th33xitus
54be7e4e21 refactor(obico): update obico installer (#254) 2022-10-31 16:06:45 +01:00
th33xitus
811c071b74 refactor(telegram-bot): update telegram-bot installer (#251) 2022-10-30 18:30:20 +01:00
th33xitus
6116fc92cf fix: allow for any amount of whitespaces after config section in update manager
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2022-10-30 18:27:13 +01:00
th33xitus
5524a40f04 refactor(klipperscreen): re-enable klipperscreen installer
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2022-10-25 15:24:25 +02:00
th33xitus
cb3661b8b5 refactor(prettygcode): re-enable pretty gcode installer
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2022-10-25 15:23:57 +02:00
th33xitus
2cec90b29c refactor(octoprint): update octoprint installer
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2022-10-24 10:35:30 +02:00
th33xitus
d2c009df9a fix(klipper): network.target is now network-online.target
Signed-off-by: Dominik Willner th33xitus@gmail.com
2022-10-22 23:28:01 +02:00
118 changed files with 7870 additions and 1038 deletions

4
.github/FUNDING.yml vendored
View File

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

9
.gitignore vendored
View File

@@ -1 +1,8 @@
klipper_repos.txt .idea
.vscode
.idea
.pytest_cache
.kiauh-env
*.code-workspace
*.iml
kiauh.cfg

145
README.md
View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a> <a>
<img src="https://raw.githubusercontent.com/th33xitus/kiauh/master/resources/screenshots/kiauh.png" alt="KIAUH logo" height="181"> <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> <h1 align="center">Klipper Installation And Update Helper</h1>
</a> </a>
</p> </p>
@@ -10,42 +10,96 @@
</p> </p>
<p align="center"> <p align="center">
<a><img src="https://img.shields.io/github/license/th33xitus/kiauh"></a> <a><img src="https://img.shields.io/github/license/dw-0/kiauh"></a>
<a><img src="https://img.shields.io/github/stars/th33xitus/kiauh"></a> <a><img src="https://img.shields.io/github/stars/dw-0/kiauh"></a>
<a><img src="https://img.shields.io/github/forks/th33xitus/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/th33xitus/kiauh?logo=gnubash&logoColor=white"></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/th33xitus/kiauh"></a> <a><img src="https://img.shields.io/github/v/tag/dw-0/kiauh"></a>
<br /> <br />
<a><img src="https://img.shields.io/github/last-commit/th33xitus/kiauh"></a> <a><img src="https://img.shields.io/github/last-commit/dw-0/kiauh"></a>
<a><img src="https://img.shields.io/github/contributors/th33xitus/kiauh"></a> <a><img src="https://img.shields.io/github/contributors/dw-0/kiauh"></a>
</p> </p>
## **🛠️ Instructions:** <hr>
For downloading this script it is necessary to have git installed.\ <h2 align="center">
If you haven't, please run `sudo apt-get install git -y` to install git first.\ 📄️ Instructions 📄
After git is installed, use the following commands in the given order to download and execute the script: </h2>
```shell ### 📋 Prerequisites
cd ~ 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.
git clone https://github.com/th33xitus/kiauh.git * 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>
./kiauh/kiauh.sh * 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
```
## **❗ Notes:** * **Step 2:** \
Once git is installed, use the following command to download KIAUH into your home-directory:
**📋 Please see the [Changelog](docs/changelog.md) for possible important changes!** ```shell
cd ~ && git clone https://github.com/dw-0/kiauh.git
```
- Tested **only** on Raspberry Pi OS Lite (Debian 10 Buster) * **Step 3:** \
- Other Debian based distributions can work Finally, start KIAUH by running the next command:
- Reported to work on Armbian too
- During the use of this script you might be asked for your sudo password. There are several functions involved which need sudo privileges.
## **🌐 Sources & Further Information** ```shell
./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.
<hr>
<h2 align="center">❗ Notes ❗</h2>
### **📋 Please see the [Changelog](docs/changelog.md) for possible important changes!**
- 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.
<hr>
<h2 align="center">🌐 Sources & Further Information</h2>
<table> <table>
<tr> <tr>
@@ -71,33 +125,66 @@ git clone https://github.com/th33xitus/kiauh.git
<tr> <tr>
<th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th> <th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th> <th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th>
<th><img src="https://camo.githubusercontent.com/627be7fc67195b626b298af9b9677d7c58e698c67305e54324cffbe06130d4a4/68747470733a2f2f6f63746f7072696e742e6f72672f6173736574732f696d672f6c6f676f2e706e67" alt="OctoPrint Logo" height="64"></th> <th><img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint/master/docs/images/octoprint-logo.png" alt="OctoPrint Logo" height="64"></th>
</tr> </tr>
<tr> <tr>
<th>by <a href="https://github.com/fluidd-core">fluidd-core</a></th> <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/jordanruthe">jordanruthe</a></th>
<th>by <a href="https://github.com/OctoPrint">OctoPrint</a></th> <th>by <a href="https://github.com/OctoPrint">OctoPrint</a></th>
</tr> </tr>
<tr>
<th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th> <th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th>
<th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></th> <th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></th>
<th><h3><a href="https://github.com/TheSpaghettiDetective/moonraker-obico">Obico for Klipper</a></h3></th> <th><h3><a href="https://github.com/TheSpaghettiDetective/moonraker-obico">Obico for Klipper</a></h3></th>
<tr>
</tr> </tr>
<tr> <tr>
<th><img src="https://avatars.githubusercontent.com/u/52351624?v=4" alt="nlef avatar" height="64"></th> <th><img src="https://avatars.githubusercontent.com/u/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/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> <th><img src="https://avatars.githubusercontent.com/u/46323662?s=200&v=4" alt="Obico logo" height="64"></th>
</tr> </tr>
<tr> <tr>
<th>by <a href="https://github.com/nlef">nlef</a></th> <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/Kragrathea">Kragrathea</a></th>
<th>by <a href="https://github.com/TheSpaghettiDetective">Obico</a></th> <th>by <a href="https://github.com/TheSpaghettiDetective">Obico</a></th>
</tr> </tr>
<tr>
<th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th>
<th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th>
<th><h3></h3></th>
</tr>
<tr>
<th><a href="https://github.com/Clon1998/mobileraker_companion"><img src="https://raw.githubusercontent.com/Clon1998/mobileraker/master/assets/icon/mr_appicon.png" alt="OctoEverywhere Logo" height="64"></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></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></th>
</tr>
</table> </table>
## **Credits** <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>

View File

@@ -2,6 +2,24 @@
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 ### 2022-10-20
KIAUH has now reached major version 5 ! KIAUH has now reached major version 5 !

View File

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

16
kiauh.cfg.example Normal file
View File

@@ -0,0 +1,16 @@
[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]
default_port: 80
unstable_releases: False

15
kiauh.py Normal file
View 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()

171
kiauh.sh
View File

@@ -1,97 +1,108 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
# TODO: mjpg-streamer
# TODO: moonraker-telegram-bot
# TODO: obico
# TODO: pretty_gcode
# TODO: upload_log
# TODO: all backup functions
# TODO: octoprint
# TODO: doublecheck that nothing got missed!
set -e set -e
clear clear
### sourcing all additional scripts function main() {
KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")" local python_command
for script in "${KIAUH_SRCDIR}/scripts/"*.sh; do . "${script}"; done local entrypoint
for script in "${KIAUH_SRCDIR}/scripts/ui/"*.sh; do . "${script}"; done
#===================================================# if command -v python3 &>/dev/null; then
#=================== UPDATE KIAUH ==================# python_command="python3"
#===================================================# elif command -v python &>/dev/null; then
python_command="python"
function update_kiauh() { else
status_msg "Updating KIAUH ..." echo "Python is not installed. Please install Python and try again."
exit 1
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 fi
entrypoint=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
${python_command} "${entrypoint}/kiauh.py"
} }
function kiauh_update_dialog() { main
[[ ! $(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 #### sourcing all additional scripts
read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn #KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")"
while true; do #for script in "${KIAUH_SRCDIR}/scripts/"*.sh; do . "${script}"; done
case "${yn}" in #for script in "${KIAUH_SRCDIR}/scripts/ui/"*.sh; do . "${script}"; done
Y|y|Yes|yes|"") #
do_action "update_kiauh" ##===================================================#
break;; ##=================== UPDATE KIAUH ==================#
N|n|No|no) ##===================================================#
break;; #
*) #function update_kiauh() {
deny_action "kiauh_update_dialog";; # status_msg "Updating KIAUH ..."
esac #
done # cd "${KIAUH_SRCDIR}"
} # git reset --hard && git pull
#
check_euid # ok_msg "Update complete! Please restart KIAUH."
init_logfile # exit 0
set_globals #}
kiauh_update_dialog #
main_menu ##===================================================#
##=================== 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

16
kiauh/__init__.py Normal file
View File

@@ -0,0 +1,16 @@
#!/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 pathlib import Path
APPLICATION_ROOT = Path(__file__).resolve().parent.parent
KIAUH_CFG = APPLICATION_ROOT.joinpath("kiauh.cfg")
KIAUH_BACKUP_DIR = Path.home().joinpath("kiauh-backups")

0
kiauh/core/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,72 @@
#!/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 shutil
from pathlib import Path
from typing import List
from kiauh import KIAUH_BACKUP_DIR
from kiauh.utils.common import get_current_date
from kiauh.utils.logger import Logger
# noinspection PyMethodMayBeStatic
class BackupManager:
def __init__(self, backup_root_dir: Path = KIAUH_BACKUP_DIR):
self._backup_root_dir = backup_root_dir
@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
def backup_file(
self, files: List[Path] = None, target: Path = None, custom_filename=None
):
if not files:
raise ValueError("Parameter 'files' cannot be None or an empty List!")
target = self.backup_root_dir if target is None else target
for file in files:
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))
except OSError as e:
Logger.print_error(f"Unable to backup '{file}':\n{e}")
continue
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
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}-{date}-{time}"))
except OSError as e:
Logger.print_error(f"Unable to backup directory '{source}':\n{e}")
return
Logger.print_ok("Backup successfull!")

View File

View File

@@ -0,0 +1,85 @@
#!/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 configparser
from pathlib import Path
from typing import Union
from kiauh.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")

View File

View File

@@ -0,0 +1,161 @@
#!/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 abc import abstractmethod, ABC
from pathlib import Path
from typing import List, Type, TypeVar
from kiauh.utils.constants import SYSTEMD, CURRENT_USER
B = TypeVar(name="B", bound="BaseInstance", covariant=True)
class BaseInstance(ABC):
@classmethod
def blacklist(cls) -> List[str]:
return []
def __init__(
self,
suffix: str,
instance_type: B = B,
):
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) -> Type["BaseInstance"]:
return self._instance_type
@instance_type.setter
def instance_type(self, value: Type["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: str) -> None:
self._data_dir = value
@property
def cfg_dir(self) -> Path:
return self._cfg_dir
@cfg_dir.setter
def cfg_dir(self, value: str) -> None:
self._cfg_dir = value
@property
def log_dir(self) -> Path:
return self._log_dir
@log_dir.setter
def log_dir(self, value: str) -> None:
self._log_dir = value
@property
def comms_dir(self) -> Path:
return self._comms_dir
@comms_dir.setter
def comms_dir(self, value: str) -> None:
self._comms_dir = value
@property
def sysd_dir(self) -> Path:
return self._sysd_dir
@sysd_dir.setter
def sysd_dir(self, value: str) -> None:
self._sysd_dir = value
@property
def gcodes_dir(self) -> Path:
return self._gcodes_dir
@gcodes_dir.setter
def gcodes_dir(self, value: str) -> 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: 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

View File

@@ -0,0 +1,214 @@
#!/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 subprocess
from pathlib import Path
from typing import List, Optional, Union, TypeVar
from kiauh.core.instance_manager.base_instance import BaseInstance
from kiauh.utils.constants import SYSTEMD
from kiauh.utils.logger import Logger
I = TypeVar(name="I", bound=BaseInstance, covariant=True)
# noinspection PyMethodMayBeStatic
class InstanceManager:
def __init__(self, instance_type: I) -> None:
self._instance_type = instance_type
self._current_instance: Optional[I] = 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[I] = []
@property
def instance_type(self) -> I:
return self._instance_type
@instance_type.setter
def instance_type(self, value: I):
self._instance_type = value
@property
def current_instance(self) -> I:
return self._current_instance
@current_instance.setter
def current_instance(self, value: I) -> 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[I]:
return self.find_instances()
@instances.setter
def instances(self, value: List[I]):
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[I]:
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

View File

@@ -0,0 +1,8 @@
from enum import unique, Enum
@unique
class NameScheme(Enum):
SINGLE = "SINGLE"
INDEX = "INDEX"
CUSTOM = "CUSTOM"

View File

@@ -0,0 +1,14 @@
#!/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 #
# ======================================================================= #
QUIT_FOOTER = "quit"
BACK_FOOTER = "back"
BACK_HELP_FOOTER = "back_help"

View File

@@ -0,0 +1,42 @@
#!/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 textwrap
from kiauh.core.menus import BACK_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.utils.constants import COLOR_YELLOW, RESET_FORMAT
class AdvancedMenu(BaseMenu):
def __init__(self):
super().__init__(header=True, options={}, footer_type=BACK_FOOTER)
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} |
|-------------------------------------------------------|
| Klipper & API: | Mainsail: |
| 0) [Rollback] | 5) [Theme installer] |
| | |
| Firmware: | System: |
| 1) [Build only] | 6) [Change hostname] |
| 2) [Flash only] | |
| 3) [Build + Flash] | Extras: |
| 4) [Get MCU ID] | 7) [G-Code Shell Command] |
"""
)[1:]
print(menu, end="")

View File

@@ -0,0 +1,183 @@
#!/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 subprocess
import sys
import textwrap
from abc import abstractmethod, ABC
from typing import Dict, Any, Literal
from kiauh.core.menus import QUIT_FOOTER, BACK_FOOTER, BACK_HELP_FOOTER
from kiauh.utils.constants import (
COLOR_GREEN,
COLOR_YELLOW,
COLOR_RED,
COLOR_CYAN,
RESET_FORMAT,
)
from kiauh.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="")
class BaseMenu(ABC):
NAVI_OPTIONS = {"quit": ["q"], "back": ["b"], "back_help": ["b", "h"]}
def __init__(
self,
options: Dict[int, Any],
options_offset: int = 0,
header: bool = True,
footer_type: Literal[
"QUIT_FOOTER", "BACK_FOOTER", "BACK_HELP_FOOTER"
] = QUIT_FOOTER,
):
self.options = options
self.options_offset = options_offset
self.header = header
self.footer_type = footer_type
@abstractmethod
def print_menu(self):
raise NotImplementedError("Subclasses must implement the print_menu method")
def print_footer(self):
footer_type_map = {
QUIT_FOOTER: print_quit_footer,
BACK_FOOTER: print_back_footer,
BACK_HELP_FOOTER: print_back_help_footer,
}
footer_function = footer_type_map.get(self.footer_type, print_quit_footer)
footer_function()
def display(self):
# clear()
if self.header:
print_header()
self.print_menu()
self.print_footer()
def handle_user_input(self):
while True:
choice = input(f"{COLOR_CYAN}###### Perform action: {RESET_FORMAT}")
if choice.isdigit() and 0 <= int(choice) < len(self.options):
return choice
elif choice.isalpha() and (
self.footer_type in self.NAVI_OPTIONS
and choice.lower() in self.NAVI_OPTIONS[self.footer_type]
):
return choice
else:
error_msg = (
"Invalid input!"
if choice.isalpha() or (not self.options and len(self.options) < 1)
else f"Invalid input! Select a number between {min(self.options)} and {max(self.options)}."
)
Logger.print_error(error_msg, False)
def start(self):
while True:
self.display()
choice = self.handle_user_input()
if choice == "q":
Logger.print_ok("###### Happy printing!", False)
sys.exit(0)
elif choice == "b":
return
elif choice == "p":
print("help!")
else:
self.execute_option(int(choice))
def execute_option(self, choice):
option = self.options.get(choice, None)
if isinstance(option, type) and issubclass(option, BaseMenu):
self.navigate_to_submenu(option)
elif callable(option):
option(opt_index=choice)
elif option is None:
raise NotImplementedError(f"No implementation for option {choice}")
else:
raise TypeError(
f"Type {type(option)} of option {choice} not of type BaseMenu or Method"
)
def navigate_to_submenu(self, submenu_class):
submenu = submenu_class()
submenu.previous_menu = self
submenu.start()

View File

@@ -0,0 +1,98 @@
#!/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 textwrap
from kiauh.core.menus import BACK_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.modules.klipper import klipper_setup
from kiauh.modules.mainsail import mainsail_setup
from kiauh.modules.moonraker import moonraker_setup
from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class InstallMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={
1: self.install_klipper,
2: self.install_moonraker,
3: self.install_mainsail,
4: self.install_fluidd,
5: self.install_klipperscreen,
6: self.install_pretty_gcode,
7: self.install_telegram_bot,
8: self.install_obico,
9: self.install_octoeverywhere,
10: self.install_mobileraker,
11: self.install_crowsnest,
},
footer_type=BACK_FOOTER,
)
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: | Other: |
| 1) [Klipper] | 6) [PrettyGCode] |
| 2) [Moonraker] | 7) [Telegram Bot] |
| | 8) $(obico_install_title) |
| Klipper Webinterface: | 9) [OctoEverywhere] |
| 3) [Mainsail] | 10) [Mobileraker] |
| 4) [Fluidd] | |
| | Webcam Streamer: |
| Touchscreen GUI: | 11) [Crowsnest] |
| 5) [KlipperScreen] | |
"""
)[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):
mainsail_setup.install_mainsail()
def install_fluidd(self, **kwargs):
print("install_fluidd")
def install_klipperscreen(self, **kwargs):
print("install_klipperscreen")
def install_pretty_gcode(self, **kwargs):
print("install_pretty_gcode")
def install_telegram_bot(self, **kwargs):
print("install_telegram_bot")
def install_obico(self, **kwargs):
print("install_obico")
def install_octoeverywhere(self, **kwargs):
print("install_octoeverywhere")
def install_mobileraker(self, **kwargs):
print("install_mobileraker")
def install_crowsnest(self, **kwargs):
print("install_crowsnest")

View File

@@ -0,0 +1,127 @@
#!/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 textwrap
from kiauh.core.menus import QUIT_FOOTER
from kiauh.core.menus.advanced_menu import AdvancedMenu
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.core.menus.install_menu import InstallMenu
from kiauh.core.menus.remove_menu import RemoveMenu
from kiauh.core.menus.settings_menu import SettingsMenu
from kiauh.core.menus.update_menu import UpdateMenu
from kiauh.modules.klipper.klipper_utils import get_klipper_status
from kiauh.modules.log_uploads.menus.log_upload_menu import LogUploadMenu
from kiauh.modules.mainsail.mainsail_utils import get_mainsail_status
from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status
from kiauh.utils.constants import (
COLOR_MAGENTA,
COLOR_CYAN,
RESET_FORMAT,
COLOR_RED,
COLOR_GREEN,
COLOR_YELLOW,
)
class MainMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={
0: LogUploadMenu,
1: InstallMenu,
2: UpdateMenu,
3: RemoveMenu,
4: AdvancedMenu,
5: None,
6: SettingsMenu,
},
footer_type=QUIT_FOOTER,
)
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.tg_status = ""
self.ob_status = ""
self.oe_status = ""
self.init_status()
def init_status(self) -> None:
status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn", "tg", "ob", "oe"]
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_mainsail_status()
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} |
| 6) [Settings] | KlipperScreen: {self.ks_status:<26} |
| | Mobileraker: {self.mb_status:<26} |
| | |
| | Crowsnest: {self.cn_status:<26} |
| | Telegram Bot: {self.tg_status:<26} |
| | Obico: {self.ob_status:<26} |
| | OctoEverywhere: {self.oe_status:<26} |
|-------------------------------------------------------|
| {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} |
"""
)[1:]
print(menu, end="")

View File

@@ -0,0 +1,104 @@
#!/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 textwrap
from kiauh.core.menus import BACK_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.modules.klipper.menus.klipper_remove_menu import KlipperRemoveMenu
from kiauh.modules.mainsail.menus.mainsail_remove_menu import MainsailRemoveMenu
from kiauh.modules.moonraker.menus.moonraker_remove_menu import MoonrakerRemoveMenu
from kiauh.utils.constants import COLOR_RED, RESET_FORMAT
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class RemoveMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={
1: KlipperRemoveMenu,
2: MoonrakerRemoveMenu,
3: MainsailRemoveMenu,
5: self.remove_fluidd,
6: self.remove_klipperscreen,
7: self.remove_crowsnest,
8: self.remove_mjpgstreamer,
9: self.remove_pretty_gcode,
10: self.remove_telegram_bot,
11: self.remove_obico,
12: self.remove_octoeverywhere,
13: self.remove_mobileraker,
14: self.remove_nginx,
},
footer_type=BACK_FOOTER,
)
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_fluidd(self, **kwargs):
print("remove_fluidd")
def remove_fluidd_config(self, **kwargs):
print("remove_fluidd_config")
def remove_klipperscreen(self, **kwargs):
print("remove_klipperscreen")
def remove_crowsnest(self, **kwargs):
print("remove_crowsnest")
def remove_mjpgstreamer(self, **kwargs):
print("remove_mjpgstreamer")
def remove_pretty_gcode(self, **kwargs):
print("remove_pretty_gcode")
def remove_telegram_bot(self, **kwargs):
print("remove_telegram_bot")
def remove_obico(self, **kwargs):
print("remove_obico")
def remove_octoeverywhere(self, **kwargs):
print("remove_octoeverywhere")
def remove_mobileraker(self, **kwargs):
print("remove_mobileraker")
def remove_nginx(self, **kwargs):
print("remove_nginx")

View File

@@ -0,0 +1,33 @@
#!/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.core.menus.base_menu import BaseMenu
# noinspection PyMethodMayBeStatic
class SettingsMenu(BaseMenu):
def __init__(self):
super().__init__(header=True, options={})
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")

View File

@@ -0,0 +1,162 @@
#!/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 textwrap
from kiauh.core.menus import BACK_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.modules.klipper.klipper_setup import update_klipper
from kiauh.modules.klipper.klipper_utils import (
get_klipper_status,
)
from kiauh.modules.mainsail.mainsail_setup import update_mainsail
from kiauh.modules.mainsail.mainsail_utils import (
get_mainsail_local_version,
get_mainsail_remote_version,
)
from kiauh.modules.moonraker.moonraker_setup import update_moonraker
from kiauh.modules.moonraker.moonraker_utils import get_moonraker_status
from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_WHITE
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class UpdateMenu(BaseMenu):
def __init__(self):
super().__init__(
header=True,
options={
0: self.update_all,
1: self.update_klipper,
2: self.update_moonraker,
3: self.update_mainsail,
4: self.update_fluidd,
5: self.update_klipperscreen,
6: self.update_pgc_for_klipper,
7: self.update_telegram_bot,
8: self.update_moonraker_obico,
9: self.update_octoeverywhere,
10: self.update_mobileraker,
11: self.update_crowsnest,
12: self.upgrade_system_packages,
},
footer_type=BACK_FOOTER,
)
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}"
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} |
| | | |
| Klipper Webinterface: |---------------|---------------|
| 3) Mainsail | {self.ms_local:<22} | {self.ms_remote:<22} |
| 4) Fluidd | | |
| | | |
| Touchscreen GUI: |---------------|---------------|
| 5) KlipperScreen | | |
| | | |
| Other: |---------------|---------------|
| 6) PrettyGCode | | |
| 7) Telegram Bot | | |
| 8) Obico for Klipper | | |
| 9) OctoEverywhere | | |
| 10) Mobileraker | | |
| 11) Crowsnest | | |
| |-------------------------------|
| 12) 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_mainsail()
def update_fluidd(self, **kwargs):
print("update_fluidd")
def update_klipperscreen(self, **kwargs):
print("update_klipperscreen")
def update_pgc_for_klipper(self, **kwargs):
print("update_pgc_for_klipper")
def update_telegram_bot(self, **kwargs):
print("update_telegram_bot")
def update_moonraker_obico(self, **kwargs):
print("update_moonraker_obico")
def update_octoeverywhere(self, **kwargs):
print("update_octoeverywhere")
def update_mobileraker(self, **kwargs):
print("update_mobileraker")
def update_crowsnest(self, **kwargs):
print("update_crowsnest")
def upgrade_system_packages(self, **kwargs):
print("upgrade_system_packages")
def fetch_update_status(self):
# klipper
kl_status = get_klipper_status()
self.kl_local = kl_status.get("local")
self.kl_remote = kl_status.get("remote")
if self.kl_local == self.kl_remote:
self.kl_local = f"{COLOR_GREEN}{self.kl_local}{RESET_FORMAT}"
else:
self.kl_local = f"{COLOR_YELLOW}{self.kl_local}{RESET_FORMAT}"
self.kl_remote = f"{COLOR_GREEN}{self.kl_remote}{RESET_FORMAT}"
# moonraker
mr_status = get_moonraker_status()
self.mr_local = mr_status.get("local")
self.mr_remote = mr_status.get("remote")
if self.mr_local == self.mr_remote:
self.mr_local = f"{COLOR_GREEN}{self.mr_local}{RESET_FORMAT}"
else:
self.mr_local = f"{COLOR_YELLOW}{self.mr_local}{RESET_FORMAT}"
self.mr_remote = f"{COLOR_GREEN}{self.mr_remote}{RESET_FORMAT}"
# mainsail
self.ms_local = get_mainsail_local_version()
self.ms_remote = get_mainsail_remote_version()
if self.ms_local == self.ms_remote:
self.ms_local = f"{COLOR_GREEN}{self.ms_local}{RESET_FORMAT}"
else:
self.ms_local = f"{COLOR_YELLOW}{self.ms_local}{RESET_FORMAT}"
self.ms_remote = f"{COLOR_GREEN}{self.ms_remote}{RESET_FORMAT}"

View File

View File

@@ -0,0 +1,170 @@
#!/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 shutil
import subprocess
from pathlib import Path
from kiauh.utils.input_utils import get_confirm
from kiauh.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 if branch is not None else "master"
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("Skipping re-clone 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 successfull!")
except subprocess.CalledProcessError as e:
log = f"Error cloning repository {self.repo}: {e.stderr.decode()}"
Logger.print_error(log)
raise
def _checkout(self):
try:
command = ["git", "checkout", f"{self.branch}"]
subprocess.run(command, cwd=self.target_dir, check=True)
Logger.print_ok("Checkout successfull!")
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"

20
kiauh/main.py Normal file
View File

@@ -0,0 +1,20 @@
#!/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.core.menus.main_menu import MainMenu
from kiauh.utils.logger import Logger
def main():
try:
MainMenu().start()
except KeyboardInterrupt:
Logger.print_ok("\nHappy printing!\n", prefix=False)

View File

View File

@@ -0,0 +1,21 @@
#!/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 pathlib import Path
MODULE_PATH = Path(__file__).resolve().parent
KLIPPER_DIR = Path.home().joinpath("klipper")
KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env")
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 ..."

View File

@@ -0,0 +1,154 @@
#!/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 subprocess
from pathlib import Path
from typing import List
from kiauh.core.instance_manager.base_instance import BaseInstance
from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH
from kiauh.utils.constants import SYSTEMD
from kiauh.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("res/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("res/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

View File

@@ -0,0 +1,136 @@
#!/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 textwrap
from typing import List
from kiauh.core.instance_manager.base_instance import BaseInstance
from kiauh.core.menus.base_menu import print_back_footer
from kiauh.utils.constants import COLOR_GREEN, RESET_FORMAT, COLOR_YELLOW, COLOR_CYAN
def print_instance_overview(
instances: List[BaseInstance], show_index=False, show_select_all=False
):
headline = f"{COLOR_GREEN}The following Klipper 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"
for i, s in enumerate(instances):
line = f"{COLOR_CYAN}{f'{i})' if show_index else ''} {s.get_service_file_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="")

View File

@@ -0,0 +1,132 @@
#!/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 shutil
from typing import List, Union
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.modules.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.klipper.klipper_dialogs import print_instance_overview
from kiauh.utils.filesystem_utils import remove_file
from kiauh.utils.input_utils import get_selection_input
from kiauh.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, True, 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)

View File

@@ -0,0 +1,185 @@
#!/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 pathlib import Path
from kiauh import KIAUH_CFG
from kiauh.core.backup_manager.backup_manager import BackupManager
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.modules.klipper import (
EXIT_KLIPPER_SETUP,
DEFAULT_KLIPPER_REPO_URL,
KLIPPER_DIR,
KLIPPER_ENV_DIR,
KLIPPER_REQUIREMENTS_TXT,
)
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.klipper.klipper_dialogs import print_update_warn_dialog
from kiauh.modules.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,
)
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.modules.moonraker.moonraker import Moonraker
from kiauh.utils.input_utils import get_confirm
from kiauh.utils.logger import Logger
from kiauh.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:
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]
# 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"):
bm = BackupManager()
bm.backup_directory("klipper", KLIPPER_DIR)
bm.backup_directory("klippy-env", KLIPPER_ENV_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:
create_example_printer_cfg(new_instance)
kl_im.start_instance()

View File

@@ -0,0 +1,278 @@
#!/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 os
import re
import grp
import shutil
import subprocess
import textwrap
from pathlib import Path
from typing import List, Union, Literal, Dict
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.base_instance import BaseInstance
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.core.instance_manager.name_scheme import NameScheme
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.modules.klipper import MODULE_PATH, KLIPPER_DIR, KLIPPER_ENV_DIR
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.klipper.klipper_dialogs import (
print_missing_usergroup_dialog,
print_instance_overview,
print_select_instance_count_dialog,
print_select_custom_name_dialog,
)
from kiauh.modules.moonraker.moonraker import Moonraker
from kiauh.modules.moonraker.moonraker_utils import moonraker_to_multi_conversion
from kiauh.utils.common import get_install_status_common
from kiauh.utils.constants import CURRENT_USER
from kiauh.utils.input_utils import get_confirm, get_string_input, get_number_input
from kiauh.utils.logger import Logger
from kiauh.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
# remove the old single instance
im.stop_instance()
im.disable_instance()
im.delete_instance()
# create a new klipper instance with the new name
im.current_instance = Klipper(suffix=new_name)
new_data_dir: Path = im.current_instance.data_dir
# rename the old data dir and use it for the new instance
Logger.print_status(f"Rename '{old_data_dir}' to '{new_data_dir}' ...")
if not new_data_dir.is_dir():
old_data_dir.rename(new_data_dir)
else:
Logger.print_info(f"'{new_data_dir}' already exist. Skipped ...")
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) -> 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("res/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))
cm.write_config()
Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'")

View File

View File

@@ -0,0 +1,110 @@
#!/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 textwrap
from kiauh.core.menus import BACK_HELP_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.modules.klipper import klipper_remove
from kiauh.modules.moonraker import moonraker_remove
from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN
# noinspection PyUnusedLocal
class KlipperRemoveMenu(BaseMenu):
def __init__(self):
super().__init__(
header=False,
options={
0: self.toggle_all,
1: self.toggle_remove_klipper_service,
2: self.toggle_remove_klipper_dir,
3: self.toggle_remove_klipper_env,
4: self.toggle_delete_klipper_logs,
5: self.run_removal_process,
},
footer_type=BACK_HELP_FOOTER,
)
self.remove_klipper_service = False
self.remove_klipper_dir = False
self.remove_klipper_env = False
self.delete_klipper_logs = 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 |
|-------------------------------------------------------|
| 5) 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

View File

@@ -0,0 +1 @@
KLIPPER_ARGS="%KLIPPER_DIR%/klippy/klippy.py %CFG% -I %SERIAL% -l %LOG% -a %UDS%"

View 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

View File

@@ -0,0 +1,11 @@
[mcu]
serial: /dev/serial/by-id/<your-mcu-id>
[virtual_sdcard]
path: %GCODES_DIR%
on_error_gcode: CANCEL_PRINT
[printer]
kinematics: none
max_velocity: 1000
max_accel: 1000

View File

@@ -0,0 +1,16 @@
#!/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 pathlib import Path
from typing import Dict, Union, Literal
FileKey = Literal["filepath", "display_name"]
LogFile = Dict[FileKey, Union[str, Path]]

View File

@@ -0,0 +1,57 @@
#!/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 typing import List
from pathlib import Path
import urllib.request
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.log_uploads import LogFile
from kiauh.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 successfull! Access it via the following link:")
Logger.print_ok(f">>>> {link}", False)
except Exception as e:
Logger.print_error(f"Uploading logfile failed!")
Logger.print_error(str(e))

View File

@@ -0,0 +1,54 @@
#!/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 textwrap
from kiauh.core.menus import BACK_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.modules.log_uploads.log_upload_utils import upload_logfile
from kiauh.modules.log_uploads.log_upload_utils import get_logfile_list
from kiauh.utils.constants import RESET_FORMAT, COLOR_YELLOW
# noinspection PyMethodMayBeStatic
class LogUploadMenu(BaseMenu):
def __init__(self):
self.logfile_list = get_logfile_list()
options = {index: self.upload for index in range(len(self.logfile_list))}
super().__init__(
header=True,
options=options,
footer_type=BACK_FOOTER,
)
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:]
logfile_list = get_logfile_list()
for logfile in enumerate(logfile_list):
line = f"{logfile[0]}) {logfile[1].get('display_name')}"
menu += f"| {line:<54}|\n"
print(menu, end="")
def upload(self, **kwargs):
upload_logfile(self.logfile_list[kwargs.get("opt_index")])

View File

@@ -0,0 +1,24 @@
#!/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 pathlib import Path
MODULE_PATH = Path(__file__).resolve().parent
MAINSAIL_DIR = Path(Path.home(), "mainsail")
MAINSAIL_CONFIG_DIR = Path(Path.home(), "mainsail-config")
MAINSAIL_CONFIG_JSON = Path(MAINSAIL_DIR, "config.json")
MAINSAIL_URL = (
"https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip"
)
MAINSAIL_UNSTABLE_URL = (
"https://github.com/mainsail-crew/mainsail/releases/download/%TAG%/mainsail.zip"
)
MAINSAIL_CONFIG_REPO_URL = "https://github.com/mainsail-crew/mainsail-config.git"

View File

@@ -0,0 +1,95 @@
#!/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 textwrap
from kiauh.core.menus.base_menu import print_back_footer
from kiauh.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_mainsail_already_installed_dialog():
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
line2 = f"{COLOR_YELLOW}Mainsail seems to be already installed!{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
| {line1:<63}|
| {line2:<63}|
|-------------------------------------------------------|
| If you continue, your current Mainsail installation |
| will be overwritten. You will not loose any printer |
| configurations and the Moonraker database will remain |
| untouched. |
"""
)[1:]
print(dialog, end="")
print_back_footer()
def print_install_mainsail_config_dialog():
dialog = textwrap.dedent(
f"""
/=======================================================\\
| It is recommended to use special macros in order to |
| have Mainsail fully functional and working. |
| |
| The recommended macros for Mainsail can be seen here: |
| https://github.com/mainsail-crew/mainsail-config |
| |
| 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="")
def print_mainsail_port_select_dialog(port: str):
port = f"{COLOR_CYAN}{port}{RESET_FORMAT}"
dialog = textwrap.dedent(
f"""
/=======================================================\\
| Please select the port, Mainsail should be served on. |
| If you are unsure what to select, hit Enter to apply |
| the suggested value of: {port:38} |
| |
| In case you need Mainsail to be served on a specific |
| port, you can set it now. Make sure the port is not |
| used by any other application on your system! |
\\=======================================================/
"""
)[1:]
print(dialog, end="")

View File

@@ -0,0 +1,164 @@
#!/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 shutil
import subprocess
from pathlib import Path
from typing import List
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.mainsail import MAINSAIL_DIR, MAINSAIL_CONFIG_DIR
from kiauh.modules.mainsail.mainsail_utils import backup_config_json
from kiauh.modules.moonraker.moonraker import Moonraker
from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
from kiauh.utils.filesystem_utils import remove_file
from kiauh.utils.logger import Logger
def run_mainsail_removal(
remove_mainsail: bool,
remove_ms_config: bool,
backup_ms_config_json: bool,
remove_mr_updater_section: bool,
remove_msc_printer_cfg_include: bool,
) -> None:
if backup_ms_config_json:
backup_config_json()
if remove_mainsail:
remove_mainsail_dir()
remove_nginx_config()
remove_nginx_logs()
if remove_mr_updater_section:
remove_updater_section("update_manager mainsail")
if remove_ms_config:
remove_mainsail_cfg_dir()
remove_mainsail_cfg_symlink()
if remove_mr_updater_section:
remove_updater_section("update_manager mainsail-config")
if remove_msc_printer_cfg_include:
remove_printer_cfg_include()
def remove_mainsail_dir() -> None:
Logger.print_status("Removing Mainsail ...")
if not MAINSAIL_DIR.exists():
Logger.print_info(f"'{MAINSAIL_DIR}' does not exist. Skipping ...")
return
try:
shutil.rmtree(MAINSAIL_DIR)
except OSError as e:
Logger.print_error(f"Unable to delete '{MAINSAIL_DIR}':\n{e}")
def remove_nginx_config() -> None:
Logger.print_status("Removing Mainsails NGINX config ...")
try:
remove_file(NGINX_SITES_AVAILABLE.joinpath("mainsail"), True)
remove_file(NGINX_SITES_ENABLED.joinpath("mainsail"), True)
except subprocess.CalledProcessError as e:
log = f"Unable to remove Mainsail NGINX config:\n{e.stderr.decode()}"
Logger.print_error(log)
def remove_nginx_logs() -> None:
Logger.print_status("Removing Mainsails NGINX logs ...")
try:
remove_file(Path("/var/log/nginx/mainsail-access.log"), True)
remove_file(Path("/var/log/nginx/mainsail-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("mainsail-access.log"))
remove_file(instance.log_dir.joinpath("mainsail-error.log"))
except (OSError, subprocess.CalledProcessError) as e:
Logger.print_error(f"Unable to NGINX logs:\n{e}")
def remove_updater_section(name: str) -> None:
Logger.print_status("Remove updater section from moonraker.conf ...")
im = InstanceManager(Moonraker)
instances: List[Moonraker] = im.instances
if not instances:
Logger.print_info("Moonraker not installed. Skipped ...")
return
for instance in instances:
Logger.print_status(f"Remove section '{name}' in '{instance.cfg_file}' ...")
if not instance.cfg_file.is_file():
Logger.print_info(f"'{instance.cfg_file}' does not exist. Skipped ...")
continue
cm = ConfigManager(instance.cfg_file)
if not cm.config.has_section(name):
Logger.print_info("Section not present. Skipped ...")
continue
cm.config.remove_section(name)
cm.write_config()
def remove_mainsail_cfg_dir() -> None:
Logger.print_status("Removing mainsail-config ...")
if not MAINSAIL_CONFIG_DIR.exists():
Logger.print_info(f"'{MAINSAIL_CONFIG_DIR}' does not exist. Skipping ...")
return
try:
shutil.rmtree(MAINSAIL_CONFIG_DIR)
except OSError as e:
Logger.print_error(f"Unable to delete '{MAINSAIL_CONFIG_DIR}':\n{e}")
def remove_mainsail_cfg_symlink() -> None:
Logger.print_status("Removing mainsail.cfg symlinks ...")
im = InstanceManager(Klipper)
instances: List[Klipper] = im.instances
for instance in instances:
Logger.print_status(f"Removing symlink from '{instance.cfg_file}' ...")
try:
remove_file(instance.cfg_dir.joinpath("mainsail.cfg"))
except subprocess.CalledProcessError:
Logger.print_error("Failed to remove symlink!")
def remove_printer_cfg_include() -> None:
Logger.print_status("Remove mainsail-config include from printer.cfg ...")
im = InstanceManager(Klipper)
instances: List[Klipper] = im.instances
if not instances:
Logger.print_info("Klipper not installed. Skipping ...")
return
for instance in instances:
log = f"Removing include from '{instance.cfg_file}' ..."
Logger.print_status(log)
if not instance.cfg_file.is_file():
continue
cm = ConfigManager(instance.cfg_file)
if not cm.config.has_section("include mainsail.cfg"):
Logger.print_info("Section not present. Skipped ...")
continue
cm.config.remove_section("include mainsail.cfg")
cm.write_config()

View File

@@ -0,0 +1,256 @@
#!/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 subprocess
from pathlib import Path
from typing import List
from kiauh import KIAUH_CFG
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.mainsail import (
MAINSAIL_URL,
MAINSAIL_DIR,
MAINSAIL_CONFIG_DIR,
MAINSAIL_CONFIG_REPO_URL,
MODULE_PATH,
)
from kiauh.modules.mainsail.mainsail_dialogs import (
print_moonraker_not_found_dialog,
print_mainsail_already_installed_dialog,
print_install_mainsail_config_dialog,
print_mainsail_port_select_dialog,
)
from kiauh.modules.mainsail.mainsail_utils import (
restore_config_json,
enable_mainsail_remotemode,
backup_config_json,
symlink_webui_nginx_log,
)
from kiauh.modules.moonraker.moonraker import Moonraker
from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
from kiauh.utils.common import check_install_dependencies
from kiauh.utils.filesystem_utils import (
unzip,
copy_upstream_nginx_cfg,
copy_common_vars_nginx_cfg,
create_nginx_cfg,
create_symlink,
remove_file,
)
from kiauh.utils.input_utils import get_confirm, get_number_input
from kiauh.utils.logger import Logger
from kiauh.utils.system_utils import (
download_file,
set_nginx_permissions,
get_ipv4_addr,
control_systemd_service,
)
def install_mainsail() -> None:
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("Continue Mainsail installation?", allow_go_back=True):
return
# if moonraker is not installed or multiple instances
# are installed we enable mainsails remote mode
if not mr_instances or len(mr_instances) > 1:
enable_remotemode = True
do_reinstall = False
if Path.home().joinpath("mainsail").exists():
print_mainsail_already_installed_dialog()
do_reinstall = get_confirm("Re-install Mainsail?", allow_go_back=True)
if do_reinstall:
backup_config_json(is_temp=True)
else:
return
kl_im = InstanceManager(Klipper)
kl_instances = kl_im.instances
install_ms_config = False
if kl_instances:
print_install_mainsail_config_dialog()
question = "Download the recommended macros?"
install_ms_config = get_confirm(question, allow_go_back=False)
# if a default port is configured in the kiauh.cfg, we use that for the port
# otherwise we default to port 80, but show the user a dialog to confirm/change that port
cm = ConfigManager(cfg_file=KIAUH_CFG)
default_port = cm.get_value("mainsail", "default_port")
is_valid_port = default_port and default_port.isdigit()
mainsail_port = default_port if is_valid_port else "80"
if not is_valid_port:
print_mainsail_port_select_dialog(mainsail_port)
mainsail_port = get_number_input(
"Configure Mainsail for port",
min_count=mainsail_port,
default=mainsail_port,
)
check_install_dependencies(["nginx"])
try:
download_mainsail()
if do_reinstall:
restore_config_json()
if enable_remotemode:
enable_mainsail_remotemode()
if mr_instances:
patch_moonraker_conf(
mr_instances,
"Mainsail",
"update_manager mainsail",
"mainsail-updater.conf",
)
mr_im.restart_all_instance()
if install_ms_config and kl_instances:
download_mainsail_cfg()
create_mainsail_cfg_symlink(kl_instances)
patch_moonraker_conf(
mr_instances,
"mainsail-config",
"update_manager mainsail-config",
"mainsail-config-updater.conf",
)
patch_printer_config(kl_instances)
kl_im.restart_all_instance()
copy_upstream_nginx_cfg()
copy_common_vars_nginx_cfg()
create_mainsail_nginx_cfg(mainsail_port)
if kl_instances:
symlink_webui_nginx_log(kl_instances)
control_systemd_service("nginx", "restart")
except Exception as e:
Logger.print_error(f"Mainsail installation failed!\n{e}")
return
log = f"Open Mainsail now on: http://{get_ipv4_addr()}:{mainsail_port}"
Logger.print_ok("Mainsail installation complete!", start="\n")
Logger.print_ok(log, prefix=False, end="\n\n")
def download_mainsail() -> None:
try:
Logger.print_status("Downloading Mainsail ...")
target = Path.home().joinpath("mainsail.zip")
download_file(MAINSAIL_URL, target, True)
Logger.print_ok("Download complete!")
Logger.print_status("Extracting mainsail.zip ...")
unzip(Path.home().joinpath("mainsail.zip"), MAINSAIL_DIR)
target.unlink(missing_ok=True)
Logger.print_ok("OK!")
except Exception:
Logger.print_error("Downloading Mainsail failed!")
raise
def update_mainsail() -> None:
Logger.print_status("Updating Mainsail ...")
backup_config_json(is_temp=True)
download_mainsail()
restore_config_json()
def download_mainsail_cfg() -> None:
try:
Logger.print_status("Downloading mainsail-config ...")
rm = RepoManager(MAINSAIL_CONFIG_REPO_URL, target_dir=MAINSAIL_CONFIG_DIR)
rm.clone_repo()
except Exception:
Logger.print_error("Downloading mainsail-config failed!")
raise
def create_mainsail_cfg_symlink(klipper_instances: List[Klipper]) -> None:
Logger.print_status("Create symlink of mainsail.cfg ...")
source = Path(MAINSAIL_CONFIG_DIR, "mainsail.cfg")
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!")
def create_mainsail_nginx_cfg(port: int) -> None:
root_dir = MAINSAIL_DIR
source = NGINX_SITES_AVAILABLE.joinpath("mainsail")
target = NGINX_SITES_ENABLED.joinpath("mainsail")
try:
Logger.print_status("Creating NGINX config for Mainsail ...")
remove_file(Path("/etc/nginx/sites-enabled/default"), True)
create_nginx_cfg("mainsail", port, root_dir)
create_symlink(source, target, True)
set_nginx_permissions()
Logger.print_ok("NGINX config for Mainsail successfully created.")
except Exception:
Logger.print_error("Creating NGINX config for Mainsail failed!")
raise
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("res", 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 patch_printer_config(klipper_instances: List[Klipper]) -> None:
for instance in klipper_instances:
cfg_file = instance.cfg_file
Logger.print_status(f"Including mainsail-config in '{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("include mainsail.cfg"):
Logger.print_info("Section already exist. Skipped ...")
return
with open(cfg_file, "a") as f:
f.write("\n[include mainsail.cfg]")

View File

@@ -0,0 +1,99 @@
#!/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 json
import shutil
import requests
from pathlib import Path
from typing import List
from kiauh.core.backup_manager.backup_manager import BackupManager
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.mainsail import MAINSAIL_CONFIG_JSON, MAINSAIL_DIR
from kiauh.utils import NGINX_SITES_AVAILABLE, NGINX_CONFD
from kiauh.utils.common import get_install_status_webui
from kiauh.utils.logger import Logger
def get_mainsail_status() -> str:
return get_install_status_webui(
MAINSAIL_DIR,
NGINX_SITES_AVAILABLE.joinpath("mainsail"),
NGINX_CONFD.joinpath("upstreams.conf"),
NGINX_CONFD.joinpath("common_vars.conf"),
)
def backup_config_json(is_temp=False) -> None:
Logger.print_status(f"Backup '{MAINSAIL_CONFIG_JSON}' ...")
bm = BackupManager()
if is_temp:
fn = Path.home().joinpath("config.json.kiauh.bak")
bm.backup_file([MAINSAIL_CONFIG_JSON], custom_filename=fn)
else:
bm.backup_file([MAINSAIL_CONFIG_JSON])
def restore_config_json() -> None:
try:
Logger.print_status(f"Restore '{MAINSAIL_CONFIG_JSON}' ...")
source = Path.home().joinpath("config.json.kiauh.bak")
shutil.copy(source, MAINSAIL_CONFIG_JSON)
except OSError:
Logger.print_info("Unable to restore config.json. Skipped ...")
def enable_mainsail_remotemode() -> None:
Logger.print_status("Enable Mainsails remote mode ...")
with open(MAINSAIL_CONFIG_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(MAINSAIL_CONFIG_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_mainsail_local_version() -> str:
relinfo_file = MAINSAIL_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_mainsail_remote_version() -> str:
url = "https://api.github.com/repos/mainsail-crew/mainsail/tags"
response = requests.get(url)
data = json.loads(response.text)
return data[0]["name"]

View File

View File

@@ -0,0 +1,122 @@
#!/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 textwrap
from kiauh.core.menus import BACK_HELP_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.modules.mainsail import mainsail_remove
from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN
# noinspection PyUnusedLocal
class MainsailRemoveMenu(BaseMenu):
def __init__(self):
super().__init__(
header=False,
options={
0: self.toggle_all,
1: self.toggle_remove_mainsail,
2: self.toggle_remove_ms_config,
3: self.toggle_backup_config_json,
4: self.toggle_remove_updater_section,
5: self.toggle_remove_printer_cfg_include,
6: self.run_removal_process,
},
footer_type=BACK_HELP_FOOTER,
)
self.remove_mainsail = False
self.remove_ms_config = False
self.backup_config_json = False
self.remove_updater_section = False
self.remove_printer_cfg_include = False
def print_menu(self) -> None:
header = " [ Remove Mainsail ] "
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
unchecked = "[ ]"
o1 = checked if self.remove_mainsail else unchecked
o2 = checked if self.remove_ms_config else unchecked
o3 = checked if self.backup_config_json else unchecked
o4 = checked if self.remove_updater_section else unchecked
o5 = checked if self.remove_printer_cfg_include 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 Mainsail |
| 2) {o2} Remove mainsail-config |
| 3) {o3} Backup config.json |
| |
| printer.cfg & moonraker.conf |
| 4) {o4} Remove Moonraker update section |
| 5) {o5} Remove printer.cfg include |
|-------------------------------------------------------|
| 6) Continue |
"""
)[1:]
print(menu, end="")
def toggle_all(self, **kwargs) -> None:
self.remove_mainsail = True
self.remove_ms_config = True
self.backup_config_json = True
self.remove_updater_section = True
self.remove_printer_cfg_include = True
def toggle_remove_mainsail(self, **kwargs) -> None:
self.remove_mainsail = not self.remove_mainsail
def toggle_remove_ms_config(self, **kwargs) -> None:
self.remove_ms_config = not self.remove_ms_config
def toggle_backup_config_json(self, **kwargs) -> None:
self.backup_config_json = not self.backup_config_json
def toggle_remove_updater_section(self, **kwargs) -> None:
self.remove_updater_section = not self.remove_updater_section
def toggle_remove_printer_cfg_include(self, **kwargs) -> None:
self.remove_printer_cfg_include = not self.remove_printer_cfg_include
def run_removal_process(self, **kwargs) -> None:
if (
not self.remove_mainsail
and not self.remove_ms_config
and not self.backup_config_json
and not self.remove_updater_section
and not self.remove_printer_cfg_include
):
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}"
print(error)
return
mainsail_remove.run_mainsail_removal(
remove_mainsail=self.remove_mainsail,
remove_ms_config=self.remove_ms_config,
backup_ms_config_json=self.backup_config_json,
remove_mr_updater_section=self.remove_updater_section,
remove_msc_printer_cfg_include=self.remove_printer_cfg_include,
)
self.remove_mainsail = False
self.remove_ms_config = False
self.backup_config_json = False
self.remove_updater_section = False
self.remove_printer_cfg_include = False

View File

@@ -0,0 +1,6 @@
[update_manager mainsail-config]
type: git_repo
primary_branch: master
path: ~/mainsail-config
origin: https://github.com/mainsail-crew/mainsail-config.git
managed_services: klipper

View File

@@ -0,0 +1,5 @@
[update_manager mainsail]
type: web
channel: stable
repo: mainsail-crew/mainsail
path: ~/mainsail

View File

@@ -0,0 +1,32 @@
#!/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 pathlib import Path
MODULE_PATH = Path(__file__).resolve().parent
MOONRAKER_DIR = Path.home().joinpath("moonraker")
MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env")
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 ..."

View File

@@ -0,0 +1,120 @@
#!/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 textwrap
from kiauh.core.menus import BACK_HELP_FOOTER
from kiauh.core.menus.base_menu import BaseMenu
from kiauh.modules.moonraker import moonraker_remove
from kiauh.utils.constants import RESET_FORMAT, COLOR_RED, COLOR_CYAN
# noinspection PyUnusedLocal
class MoonrakerRemoveMenu(BaseMenu):
def __init__(self):
super().__init__(
header=False,
options={
0: self.toggle_all,
1: self.toggle_remove_moonraker_service,
2: self.toggle_remove_moonraker_dir,
3: self.toggle_remove_moonraker_env,
4: self.toggle_remove_moonraker_polkit,
5: self.toggle_delete_moonraker_logs,
6: self.run_removal_process,
},
footer_type=BACK_HELP_FOOTER,
)
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 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 |
|-------------------------------------------------------|
| 6) 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

View File

@@ -0,0 +1,147 @@
#!/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 subprocess
from pathlib import Path
from typing import List, Union
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.base_instance import BaseInstance
from kiauh.modules.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR, MODULE_PATH
from kiauh.utils.constants import SYSTEMD
from kiauh.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.log = self.log_dir.joinpath("moonraker.log")
def create(self, create_example_cfg: bool = False) -> None:
Logger.print_status("Creating new Moonraker Instance ...")
service_template_path = MODULE_PATH.joinpath("res/moonraker.service")
env_template_file_path = MODULE_PATH.joinpath("res/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

View File

@@ -0,0 +1,72 @@
#!/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 textwrap
from typing import List
from kiauh.core.menus.base_menu import print_back_footer
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.moonraker.moonraker import Moonraker
from kiauh.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()

View File

@@ -0,0 +1,155 @@
#!/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 shutil
import subprocess
from typing import List, Union
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.modules.klipper.klipper_dialogs import print_instance_overview
from kiauh.modules.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR
from kiauh.modules.moonraker.moonraker import Moonraker
from kiauh.utils.filesystem_utils import remove_file
from kiauh.utils.input_utils import get_selection_input
from kiauh.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, True, 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)

View File

@@ -0,0 +1,233 @@
#!/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 subprocess
import sys
from pathlib import Path
from typing import List
from kiauh import KIAUH_CFG
from kiauh.core.backup_manager.backup_manager import BackupManager
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.modules.klipper.klipper import Klipper
from kiauh.modules.klipper.klipper_dialogs import print_instance_overview
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.modules.mainsail import MAINSAIL_DIR
from kiauh.modules.mainsail.mainsail_utils import enable_mainsail_remotemode
from kiauh.modules.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 kiauh.modules.moonraker.moonraker import Moonraker
from kiauh.modules.moonraker.moonraker_dialogs import print_moonraker_overview
from kiauh.modules.moonraker.moonraker_utils import create_example_moonraker_conf
from kiauh.utils.filesystem_utils import check_file_exist
from kiauh.utils.input_utils import (
get_confirm,
get_selection_input,
)
from kiauh.utils.logger import Logger
from kiauh.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:
create_example_moonraker_conf(current_instance, used_ports_map)
mr_im.start_instance()
mr_im.reload_daemon()
# if mainsail is installed, and we installed
# multiple moonraker instances, we enable mainsails remote mode
if MAINSAIL_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
mr_instance_count = len(InstanceManager(Moonraker).instances)
if mr_instance_count >= kl_instance_count:
Logger.print_warn("Unable to install more Moonraker instances!")
Logger.print_warn("More Klipper instances required.")
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 handle_existing_instances(instance_list: List[Klipper]) -> bool:
instance_count = len(instance_list)
if instance_count > 0:
print_instance_overview(instance_list)
if not get_confirm("Add new instances?", allow_go_back=True):
return False
return True
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"):
bm = BackupManager()
bm.backup_directory("moonraker", MOONRAKER_DIR)
bm.backup_directory("moonraker-env", MOONRAKER_ENV_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()

View File

@@ -0,0 +1,134 @@
#!/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 shutil
from typing import Dict, Literal, List, Union
from kiauh.core.config_manager.config_manager import ConfigManager
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.core.repo_manager.repo_manager import RepoManager
from kiauh.modules.mainsail import MAINSAIL_DIR
from kiauh.modules.mainsail.mainsail_utils import enable_mainsail_remotemode
from kiauh.modules.moonraker import (
DEFAULT_MOONRAKER_PORT,
MODULE_PATH,
MOONRAKER_DIR,
MOONRAKER_ENV_DIR,
)
from kiauh.modules.moonraker.moonraker import Moonraker
from kiauh.utils.common import get_install_status_common
from kiauh.utils.logger import Logger
from kiauh.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]
) -> 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("res/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)
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 klipper instance with the new name
im.current_instance = Moonraker(suffix=new_name)
# 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 MAINSAIL_DIR.exists() and len(im.instances) > 1:
enable_mainsail_remotemode()

View 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

View File

@@ -0,0 +1 @@
MOONRAKER_ARGS="%MOONRAKER_DIR%/moonraker/moonraker.py -d %PRINTER_DATA%"

View 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

20
kiauh/utils/__init__.py Normal file
View File

@@ -0,0 +1,20 @@
#!/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 pathlib import Path
MODULE_PATH = Path(__file__).resolve().parent
INVALID_CHOICE = "Invalid choice. Please select a valid value."
# ================== NGINX =====================#
NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available")
NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled")
NGINX_CONFD = Path("/etc/nginx/conf.d")

118
kiauh/utils/common.py Normal file
View File

@@ -0,0 +1,118 @@
#!/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 datetime import datetime
from pathlib import Path
from typing import Dict, Literal, List, Type, Union
from kiauh.core.instance_manager.base_instance import BaseInstance
from kiauh.core.instance_manager.instance_manager import InstanceManager
from kiauh.utils.constants import (
COLOR_CYAN,
RESET_FORMAT,
COLOR_YELLOW,
COLOR_GREEN,
COLOR_RED,
)
from kiauh.utils.filesystem_utils import check_file_exist
from kiauh.utils.logger import Logger
from kiauh.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}"

26
kiauh/utils/constants.py Normal file
View File

@@ -0,0 +1,26 @@
#!/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 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")

View File

@@ -0,0 +1,135 @@
#!/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 shutil
import subprocess
from pathlib import Path
from zipfile import ZipFile
from kiauh.utils import (
NGINX_SITES_AVAILABLE,
MODULE_PATH,
NGINX_CONFD,
)
from kiauh.utils.logger import Logger
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("res/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("res/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("res/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

148
kiauh/utils/input_utils.py Normal file
View File

@@ -0,0 +1,148 @@
#!/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 typing import Optional, List, Union
from kiauh.utils import INVALID_CHOICE
from kiauh.utils.constants import COLOR_CYAN, RESET_FORMAT
from kiauh.utils.logger import Logger
def get_confirm(
question: str, default_choice=True, allow_go_back=False
) -> Union[bool, None]:
"""
Helper method for validating confirmation (yes/no) user input. |
:param question: The question to display
:param default_choice: A default if input was submitted without input
:param allow_go_back: Navigate back to a previous dialog
:return: Either True or False, or None on go_back
"""
options_confirm = ["y", "yes"]
options_decline = ["n", "no"]
options_go_back = ["b", "B"]
if default_choice:
def_choice = "(Y/n)"
options_confirm.append("")
else:
def_choice = "(y/N)"
options_decline.append("")
while True:
choice = (
input(format_question(question + f" {def_choice}", None)).strip().lower()
)
if choice in options_confirm:
return True
elif choice in options_decline:
return False
elif allow_go_back and choice in options_go_back:
return None
else:
Logger.print_error(INVALID_CHOICE)
def get_number_input(
question: str, min_count: int, max_count=None, default=None, allow_go_back=False
) -> Union[int, None]:
"""
Helper method to get a number input from the user
:param question: The question to display
:param min_count: The lowest allowed value
:param max_count: The highest allowed value (or None)
:param default: Optional default value
:param allow_go_back: Navigate back to a previous dialog
:return: Either the validated number input, or None on go_back
"""
options_go_back = ["b", "B"]
_question = format_question(question, default)
while True:
_input = input(_question)
if allow_go_back and _input in options_go_back:
return None
if _input == "":
return default
try:
return validate_number_input(_input, min_count, max_count)
except ValueError:
Logger.print_error(INVALID_CHOICE)
def get_string_input(question: str, exclude=Optional[List], default=None) -> str:
"""
Helper method to get a string input from the user
:param question: The question to display
:param exclude: List of strings which are not allowed
:param default: Optional default value
:return: The validated string value
"""
while True:
_input = input(format_question(question, default)).strip()
if _input.isalnum() and _input.lower() not in exclude:
return _input
Logger.print_error(INVALID_CHOICE)
if _input in exclude:
Logger.print_error("This value is already in use/reserved.")
def get_selection_input(question: str, option_list: List, default=None) -> str:
"""
Helper method to get a selection from a list of options from the user
:param question: The question to display
:param option_list: The list of options the user can select from
:param default: Optional default value
:return: The option that was selected by the user
"""
while True:
_input = input(format_question(question, default)).strip()
if _input in option_list:
return _input
Logger.print_error(INVALID_CHOICE)
def format_question(question: str, default=None) -> str:
"""
Helper method to have a standardized formatting of questions |
:param question: The question to display
:param default: If defined, the default option will be displayed to the user
:return: The formatted question string
"""
formatted_q = question
if default is not None:
formatted_q += f" (default={default})"
return f"{COLOR_CYAN}###### {formatted_q}: {RESET_FORMAT}"
def validate_number_input(value: str, min_count: int, max_count: int) -> int:
"""
Helper method for a simple number input validation. |
:param value: The value to validate
:param min_count: The lowest allowed value
:param max_count: The highest allowed value (or None)
:return: The validated value as Integer
:raises: ValueError if value is invalid
"""
if max_count is not None:
if min_count <= int(value) <= max_count:
return int(value)
elif int(value) >= min_count:
return int(value)
raise ValueError

61
kiauh/utils/logger.py Normal file
View File

@@ -0,0 +1,61 @@
#!/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.utils.constants import (
COLOR_WHITE,
COLOR_GREEN,
COLOR_YELLOW,
COLOR_RED,
COLOR_MAGENTA,
RESET_FORMAT,
)
class Logger:
@staticmethod
def info(msg):
# log to kiauh.log
pass
@staticmethod
def warn(msg):
# log to kiauh.log
pass
@staticmethod
def error(msg):
# log to kiauh.log
pass
@staticmethod
def print_info(msg, prefix=True, start="", end="\n") -> None:
message = f"[INFO] {msg}" if prefix else msg
print(f"{COLOR_WHITE}{start}{message}{RESET_FORMAT}", end=end)
@staticmethod
def print_ok(msg, prefix=True, start="", end="\n") -> None:
message = f"[OK] {msg}" if prefix else msg
print(f"{COLOR_GREEN}{start}{message}{RESET_FORMAT}", end=end)
@staticmethod
def print_warn(msg, prefix=True, start="", end="\n") -> None:
message = f"[WARN] {msg}" if prefix else msg
print(f"{COLOR_YELLOW}{start}{message}{RESET_FORMAT}", end=end)
@staticmethod
def print_error(msg, prefix=True, start="", end="\n") -> None:
message = f"[ERROR] {msg}" if prefix else msg
print(f"{COLOR_RED}{start}{message}{RESET_FORMAT}", end=end)
@staticmethod
def print_status(msg, prefix=True, start="", end="\n") -> None:
message = f"\n###### {msg}" if prefix else msg
print(f"{COLOR_MAGENTA}{start}{message}{RESET_FORMAT}", end=end)

View File

@@ -0,0 +1,6 @@
# /etc/nginx/conf.d/common_vars.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

96
kiauh/utils/res/nginx_cfg Normal file
View File

@@ -0,0 +1,96 @@
# /etc/nginx/sites-available/%NAME%
server {
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;
proxy_read_timeout 600;
}
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/;
}
}

View 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;
}

328
kiauh/utils/system_utils.py Normal file
View File

@@ -0,0 +1,328 @@
#!/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 os
import shutil
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.request
import venv
from pathlib import Path
from typing import List, Literal
from kiauh.utils.input_utils import get_confirm
from kiauh.utils.logger import Logger
def kill(opt_err_msg: str = "") -> None:
"""
Kills the application |
:param opt_err_msg: an optional, additional error message
:return: None
"""
if opt_err_msg:
Logger.print_error(opt_err_msg)
Logger.print_error("A critical error has occured. KIAUH was terminated.")
sys.exit(1)
def parse_packages_from_file(source_file: Path) -> List[str]:
"""
Read the package names from bash scripts, when defined like:
PKGLIST="package1 package2 package3" |
:param source_file: path of the sourcefile to read from
:return: A list of package names
"""
packages = []
print("Reading dependencies...")
with open(source_file, "r") as file:
for line in file:
line = line.strip()
if line.startswith("PKGLIST="):
line = line.replace('"', "")
line = line.replace("PKGLIST=", "")
line = line.replace("${PKGLIST}", "")
packages.extend(line.split())
return packages
def create_python_venv(target: Path) -> None:
"""
Create a python 3 virtualenv at the provided target destination |
:param target: Path where to create the virtualenv at
:return: None
"""
Logger.print_status("Set up Python virtual environment ...")
if not target.exists():
try:
venv.create(target, with_pip=True)
Logger.print_ok("Setup of virtualenv successfull!")
except OSError as e:
Logger.print_error(f"Error setting up virtualenv:\n{e}")
raise
except subprocess.CalledProcessError as e:
Logger.print_error(f"Error setting up virtualenv:\n{e.output.decode()}")
raise
else:
if get_confirm("Virtualenv already exists. Re-create?", default_choice=False):
try:
shutil.rmtree(target)
create_python_venv(target)
except OSError as e:
log = f"Error removing existing virtualenv: {e.strerror}"
Logger.print_error(log, False)
raise
else:
Logger.print_info("Skipping re-creation of virtualenv ...")
def update_python_pip(target: Path) -> None:
"""
Updates pip in the provided target destination |
:param target: Path of the virtualenv
:return: None
"""
Logger.print_status("Updating pip ...")
try:
command = [target.joinpath("bin/pip"), "install", "-U", "pip"]
result = subprocess.run(command, stderr=subprocess.PIPE, text=True)
if result.returncode != 0 or result.stderr:
Logger.print_error(f"{result.stderr}", False)
Logger.print_error("Updating pip failed!")
return
Logger.print_ok("Updating pip successfull!")
except subprocess.CalledProcessError as e:
Logger.print_error(f"Error updating pip:\n{e.output.decode()}")
def install_python_requirements(target: Path, requirements: Path) -> None:
"""
Installs the python packages based on a provided requirements.txt |
:param target: Path of the virtualenv
:param requirements: Path to the requirements.txt file
:return: None
"""
update_python_pip(target)
Logger.print_status("Installing Python requirements ...")
try:
command = [target.joinpath("bin/pip"), "install", "-r", f"{requirements}"]
result = subprocess.run(command, stderr=subprocess.PIPE, text=True)
if result.returncode != 0 or result.stderr:
Logger.print_error(f"{result.stderr}", False)
Logger.print_error("Installing Python requirements failed!")
return
Logger.print_ok("Installing Python requirements successfull!")
except subprocess.CalledProcessError as e:
log = f"Error installing Python requirements:\n{e.output.decode()}"
Logger.print_error(log)
raise
def update_system_package_lists(silent: bool, rls_info_change=False) -> None:
"""
Updates the systems package list |
:param silent: Log info to the console or not
:param rls_info_change: Flag for "--allow-releaseinfo-change"
:return: None
"""
cache_mtime = 0
cache_files = [
Path("/var/lib/apt/periodic/update-success-stamp"),
Path("/var/lib/apt/lists"),
]
for cache_file in cache_files:
if cache_file.exists():
cache_mtime = max(cache_mtime, os.path.getmtime(cache_file))
update_age = int(time.time() - cache_mtime)
update_interval = 6 * 3600 # 48hrs
if update_age <= update_interval:
return
if not silent:
Logger.print_status("Updating package list...")
try:
command = ["sudo", "apt-get", "update"]
if rls_info_change:
command.append("--allow-releaseinfo-change")
result = subprocess.run(command, stderr=subprocess.PIPE, text=True)
if result.returncode != 0 or result.stderr:
Logger.print_error(f"{result.stderr}", False)
Logger.print_error("Updating system package list failed!")
return
Logger.print_ok("System package list updated successfully!")
except subprocess.CalledProcessError as e:
kill(f"Error updating system package list:\n{e.stderr.decode()}")
def check_package_install(packages: List[str]) -> List[str]:
"""
Checks the system for installed packages |
:param packages: List of strings of package names
:return: A list containing the names of packages that are not installed
"""
not_installed = []
for package in packages:
command = ["dpkg-query", "-f'${Status}'", "--show", package]
result = subprocess.run(
command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True
)
if "installed" not in result.stdout.strip("'").split():
not_installed.append(package)
else:
Logger.print_ok(f"{package} already installed.")
return not_installed
def install_system_packages(packages: List[str]) -> None:
"""
Installs a list of system packages |
:param packages: List of system package names
:return: None
"""
try:
command = ["sudo", "apt-get", "install", "-y"]
for pkg in packages:
command.append(pkg)
subprocess.run(command, stderr=subprocess.PIPE, check=True)
Logger.print_ok("Packages installed successfully.")
except subprocess.CalledProcessError as e:
kill(f"Error installing packages:\n{e.stderr.decode()}")
def mask_system_service(service_name: str) -> None:
"""
Mask a system service to prevent it from starting |
:param service_name: name of the service to mask
:return: None
"""
try:
command = ["sudo", "systemctl", "mask", service_name]
subprocess.run(command, stderr=subprocess.PIPE, check=True)
except subprocess.CalledProcessError as e:
log = f"Unable to mask system service {service_name}: {e.stderr.decode()}"
Logger.print_error(log)
raise
# this feels hacky and not quite right, but for now it works
# see: https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib
def get_ipv4_addr() -> str:
"""
Helper function that returns the IPv4 of the current machine
by opening a socket and sending a package to an arbitrary IP. |
:return: Local IPv4 of the current machine
"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(0)
try:
# doesn't even have to be reachable
s.connect(("192.255.255.255", 1))
return s.getsockname()[0]
except Exception:
return "127.0.0.1"
finally:
s.close()
def download_file(url: str, target: Path, show_progress=True) -> None:
"""
Helper method for downloading files from a provided URL |
:param url: the url to the file
:param target: the target path incl filename
:param show_progress: show download progress or not
:return: None
"""
try:
if show_progress:
urllib.request.urlretrieve(url, target, download_progress)
sys.stdout.write("\n")
else:
urllib.request.urlretrieve(url, target)
except urllib.error.HTTPError as e:
Logger.print_error(f"Download failed! HTTP error occured: {e}")
raise
except urllib.error.URLError as e:
Logger.print_error(f"Download failed! URL error occured: {e}")
raise
except Exception as e:
Logger.print_error(f"Download failed! An error occured: {e}")
raise
def download_progress(block_num, block_size, total_size) -> None:
"""
Reporthook method for urllib.request.urlretrieve() method call in download_file() |
:param block_num:
:param block_size:
:param total_size: total filesize in bytes
:return: None
"""
downloaded = block_num * block_size
percent = 100 if downloaded >= total_size else downloaded / total_size * 100
mb = 1024 * 1024
progress = int(percent / 5)
remaining = "-" * (20 - progress)
dl = f"\rDownloading: [{'#' * progress}{remaining}]{percent:.2f}% ({downloaded/mb:.2f}/{total_size/mb:.2f}MB)"
sys.stdout.write(dl)
sys.stdout.flush()
def set_nginx_permissions() -> None:
"""
Check if permissions of the users home directory
grant execution rights to group and other and set them if not set.
Required permissions for NGINX to be able to serve Mainsail/Fluidd.
This seems to have become necessary with Ubuntu 21+. |
:return: None
"""
cmd = f"ls -ld {Path.home()} | cut -d' ' -f1"
homedir_perm = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, text=True)
homedir_perm = homedir_perm.stdout
if homedir_perm.count("x") < 3:
Logger.print_status("Granting NGINX the required permissions ...")
subprocess.run(["chmod", "og+x", Path.home()])
Logger.print_ok("Permissions granted.")
def control_systemd_service(
name: str, action: Literal["start", "stop", "restart", "disable"]
) -> None:
"""
Helper method to execute several actions for a specific systemd service. |
:param name: the service name
:param action: Either "start", "stop", "restart" or "disable"
:return: None
"""
try:
Logger.print_status(f"{action.capitalize()} {name}.service ...")
command = ["sudo", "systemctl", action, f"{name}.service"]
subprocess.run(command, stderr=subprocess.PIPE, check=True)
Logger.print_ok("OK!")
except subprocess.CalledProcessError as e:
log = f"Failed to {action} {name}.service: {e.stderr.decode()}"
Logger.print_error(log)
raise

View File

@@ -1,18 +0,0 @@
# This file acts as an example file.
#
# 1) Make a copy of this file and rename it to 'klipper_repos.txt'
# 2) Add your custom Klipper repository to the bottom of that copy
# 3) Save the file
#
# Back in KIAUH you can now go into -> [Settings] and use action '2' to set a different Klipper repository
#
# Make sure to always separate the repository and the branch with a ','.
# <repository>,<branch> -> https://github.com/Klipper3d/klipper,master
# If you omit a branch, it will always default to 'master'
#
# You are allowed to omit the 'https://github.com/' part of the repository URL
# Down below are now a few examples of what is considered as valid:
https://github.com/Klipper3d/klipper,master
https://github.com/Klipper3d/klipper
Klipper3d/klipper,master
Klipper3d/klipper

13
pyproject.toml Normal file
View File

@@ -0,0 +1,13 @@
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
exclude = '''
(
\.git/
| \.github/
| docs/
| resources/
| scripts/
)
'''

View File

@@ -1,6 +1,10 @@
[mcu] [mcu]
serial: /dev/serial/by-id/<your-mcu-id> serial: /dev/serial/by-id/<your-mcu-id>
[virtual_sdcard]
path: %GCODES_DIR%
on_error_gcode: CANCEL_PRINT
[printer] [printer]
kinematics: none kinematics: none
max_velocity: 1000 max_velocity: 1000

View File

@@ -55,6 +55,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme; proxy_set_header X-Scheme $scheme;
proxy_read_timeout 600;
} }
location /webcam/ { location /webcam/ {

View File

@@ -1 +1 @@
KLIPPER_ARGS="/home/%USER%/klipper/klippy/klippy.py %CFG% -I %PRINTER% -l %LOG% -a %UDS%" KLIPPER_ARGS="%KLIPPER_DIR%/klippy/klippy.py %CFG% -I %PRINTER% -l %LOG% -a %UDS%"

View File

@@ -1,7 +1,7 @@
[Unit] [Unit]
Description=Klipper 3D Printer Firmware SV1 %INST% Description=Klipper 3D Printer Firmware SV1
Documentation=https://www.klipper3d.org/ Documentation=https://www.klipper3d.org/
After=network.target After=network-online.target
Wants=udev.target Wants=udev.target
[Install] [Install]
@@ -11,7 +11,7 @@ WantedBy=multi-user.target
Type=simple Type=simple
User=%USER% User=%USER%
RemainAfterExit=yes RemainAfterExit=yes
WorkingDirectory=/home/%USER%/klipper WorkingDirectory=%KLIPPER_DIR%
EnvironmentFile=%ENV_FILE% EnvironmentFile=%ENV_FILE%
ExecStart=%ENV%/bin/python $KLIPPER_ARGS ExecStart=%ENV%/bin/python $KLIPPER_ARGS
Restart=always Restart=always

View File

@@ -55,6 +55,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme; proxy_set_header X-Scheme $scheme;
proxy_read_timeout 600;
} }
location /webcam/ { location /webcam/ {

View File

@@ -0,0 +1 @@
TELEGRAM_BOT_ARGS="%TELEGRAM_BOT_DIR%/bot/main.py -c %CFG% -l %LOG%"

View File

@@ -1,19 +1,16 @@
#Systemd service file for Moonraker Telegram Bot
[Unit] [Unit]
Description=Starts Moonraker Telegram Bot instance %INST% on startup Description=Moonraker Telegram Bot SV1 %INST%
Documentation=https://github.com/nlef/moonraker-telegram-bot/wiki Documentation=https://github.com/nlef/moonraker-telegram-bot/wiki
After=network.target After=network-online.target
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
[Service] [Service]
Environment=TELEGRAM_CONF=%CFG%
Environment=TELEGRAM_LOG=%LOG%
Type=simple Type=simple
User=%USER% User=%USER%
RemainAfterExit=yes WorkingDirectory=%TELEGRAM_BOT_DIR%
ExecStart=%ENV%/bin/python %DIR%/bot/main.py -c ${TELEGRAM_CONF} -l ${TELEGRAM_LOG} EnvironmentFile=%ENV_FILE%
ExecStart=%ENV%/bin/python $TELEGRAM_BOT_ARGS
Restart=always Restart=always
RestartSec=10 RestartSec=10

View File

@@ -14,12 +14,12 @@ trusted_clients:
FE80::/10 FE80::/10
::1/128 ::1/128
cors_domains: cors_domains:
http://*.lan *.lan
http://*.local *.local
https://my.mainsail.xyz *://localhost
http://my.mainsail.xyz *://localhost:*
https://app.fluidd.xyz *://my.mainsail.xyz
http://app.fluidd.xyz *://app.fluidd.xyz
[octoprint_compat] [octoprint_compat]

View File

@@ -1 +1 @@
MOONRAKER_ARGS="/home/%USER%/moonraker/moonraker/moonraker.py -d %PRINTER_DATA%" MOONRAKER_ARGS="%MOONRAKER_DIR%/moonraker/moonraker.py -d %PRINTER_DATA%"

View File

@@ -12,7 +12,7 @@ Type=simple
User=%USER% User=%USER%
SupplementaryGroups=moonraker-admin SupplementaryGroups=moonraker-admin
RemainAfterExit=yes RemainAfterExit=yes
WorkingDirectory=/home/%USER%/moonraker WorkingDirectory=%MOONRAKER_DIR%
EnvironmentFile=%ENV_FILE% EnvironmentFile=%ENV_FILE%
ExecStart=%ENV%/bin/python $MOONRAKER_ARGS ExecStart=%ENV%/bin/python $MOONRAKER_ARGS
Restart=always Restart=always

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
@@ -31,21 +31,28 @@ function backup_before_update() {
backup_"${1}" backup_"${1}"
} }
function backup_klipper_config_dir() { function backup_config_dir() {
check_for_backup_dir check_for_backup_dir
local current_date config_folder_name local current_date config_pathes
if [[ -d "${KLIPPER_CONFIG}" ]]; then config_pathes=$(get_config_folders)
if [[ -n "${config_pathes}" ]]; then
current_date=$(get_date) current_date=$(get_date)
config_folder_name="$(echo "${KLIPPER_CONFIG}" | rev | cut -d"/" -f1 | rev)"
status_msg "Timestamp: ${current_date}" status_msg "Timestamp: ${current_date}"
status_msg "Create backup of the Klipper config directory ..."
mkdir -p "${BACKUP_DIR}/${config_folder_name}/${current_date}" local i=0 folder folder_name target_dir
cp -r "${KLIPPER_CONFIG}" "${_}" for folder in ${config_pathes}; do
status_msg "Create backup of ${folder} ..."
print_confirm "Configuration directory backup complete!" folder_name=$(echo "${folder}" | rev | cut -d"/" -f2 | rev)
target_dir="${BACKUP_DIR}/configs/${current_date}/${folder_name}"
mkdir -p "${target_dir}"
cp -r "${folder}" "${target_dir}"
i=$(( i + 1 ))
ok_msg "Backup created in:\n${target_dir}"
done
else else
ok_msg "No config directory found! Skipping backup ..." ok_msg "No config directory found! Skipping backup ..."
fi fi
@@ -53,27 +60,29 @@ function backup_klipper_config_dir() {
function backup_moonraker_database() { function backup_moonraker_database() {
check_for_backup_dir check_for_backup_dir
local current_date databases target_dir regex=".moonraker_database(_[0-9a-zA-Z]+)?" local current_date db_pathes
databases=$(find "${HOME}" -maxdepth 1 -type d -regextype posix-extended -regex "${HOME}/${regex}" | sort)
if [[ -n ${databases} ]]; then db_pathes=$(get_instance_folder_path "database")
if [[ -n ${db_pathes} ]]; then
current_date=$(get_date) current_date=$(get_date)
target_dir="${BACKUP_DIR}/moonraker_database_backup/${current_date}"
status_msg "Timestamp: ${current_date}" status_msg "Timestamp: ${current_date}"
mkdir -p "${target_dir}"
for database in ${databases}; do local i=0 database folder_name target_dir
for database in ${db_pathes}; do
status_msg "Create backup of ${database} ..." status_msg "Create backup of ${database} ..."
cp -r "${database}" "${target_dir}"
ok_msg "Done!"
done
print_confirm "Moonraker database backup complete!" folder_name=$(echo "${database}" | rev | cut -d"/" -f2 | rev)
target_dir="${BACKUP_DIR}/moonraker_databases/${current_date}/${folder_name}"
mkdir -p "${target_dir}"
cp -r "${database}" "${target_dir}"
i=$(( i + 1 ))
ok_msg "Backup created in:\n${target_dir}"
done
else else
print_error "No Moonraker database found! Skipping backup ..." print_error "No Moonraker database found! Skipping backup ..."
fi fi
return
} }
function backup_klipper() { function backup_klipper() {
@@ -186,3 +195,19 @@ function backup_telegram_bot() {
print_error "Can't back up MoonrakerTelegramBot directory!\n Not found!" print_error "Can't back up MoonrakerTelegramBot directory!\n Not found!"
fi fi
} }
function backup_octoeverywhere() {
local current_date
if [[ -d ${OCTOEVERYWHERE_DIR} ]] ; then
status_msg "Creating OctoEverywhere backup ..."
check_for_backup_dir
current_date=$(get_date)
status_msg "Timestamp: ${current_date}"
mkdir -p "${BACKUP_DIR}/OctoEverywhere-backups/${current_date}"
cp -r "${OCTOEVERYWHERE_DIR}" "${_}" && cp -r "${OCTOEVERYWHERE_ENV}" "${_}"
print_confirm "OctoEverywhere backup complete!"
else
print_error "Can't back up OctoEverywhere directory!\n Not found!"
fi
}

235
scripts/crowsnest.sh Normal file
View File

@@ -0,0 +1,235 @@
#!/usr/bin/env bash
#=======================================================================#
# 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 #
#=======================================================================#
#=======================================================================#
# Crowsnest Installer brought to you by KwadFan <me@stephanwe.de> #
# Copyright (C) 2022 KwadFan <me@stephanwe.de> #
# https://github.com/KwadFan/crowsnest #
#=======================================================================#
# Error Handling
set -e
# Helper messages
function multi_instance_message(){
echo -e "Crowsnest is NOT designed to support multi instances."
echo -e "A workaround for this is to choose the most used instance as a 'master'"
echo -e "Use this instance to set up your 'crowsnest.conf' and steering it's service.\n"
echo -e "Found the following instances:\n"
for i in ${1}; do
select_msg "${i}"
done
echo -e "\nLaunching crowsnest's configuration tool ..."
continue_config
}
# Helper funcs
function clone_crowsnest(){
$(command -v git) clone "${CROWSNEST_REPO}" -b master "${CROWSNEST_DIR}"
}
function check_multi_instance(){
local -a instances
readarray -t instances < <(find "${HOME}" -regex "${HOME}/[a-zA-Z0-9_]+_data/*" -printf "%P\n" 2> /dev/null | sort)
if [[ "${#instances[@]}" -gt 1 ]]; then
status_msg "Multi instance install detected ..."
multi_instance_message "${instances[*]}"
if [[ -d "${HOME}/crowsnest" ]]; then
pushd "${HOME}/crowsnest" &> /dev/null || exit 1
if ! make config ;then
error_msg "Something went wrong! Please try again..."
if [[ -f "tools/.config" ]]; then
rm -f tools/.config
fi
exit 1
fi
if [[ ! -f "tools/.config" ]]; then
log_error "failure while generating .config"
error_msg "Generating .config failed, installation aborted"
exit 1
fi
popd &> /dev/null || exit 1
fi
fi
}
function continue_config() {
local reply
while true; do
read -erp "${cyan}###### Continue with configuration? (y/N):${white} " reply
case "${reply}" in
Y|y|Yes|yes)
select_msg "Yes"
break;;
N|n|No|no|"")
select_msg "No"
warn_msg "Installation aborted by user ... Exiting!"
exit 1;;
*)
error_msg "Invalid Input!\n";;
esac
done
return 0
}
# Install func
function install_crowsnest(){
# Step 1: jump to home directory
pushd "${HOME}" &> /dev/null || exit 1
# Step 2: Clone crowsnest repo
status_msg "Cloning 'crowsnest' repository ..."
if [[ ! -d "${HOME}/crowsnest" && -z "$(ls -A "${HOME}/crowsnest" 2> /dev/null)" ]]; then
clone_crowsnest
else
ok_msg "crowsnest repository already exists ..."
fi
# Step 3: Install dependencies
dependency_check git make
# Step 4: Check for Multi Instance
check_multi_instance
# Step 5: Launch crowsnest installer
pushd "${HOME}/crowsnest" &> /dev/null || exit 1
title_msg "Installer will prompt you for sudo password!"
status_msg "Launching crowsnest installer ..."
if ! sudo make install BASE_USER=$USER; then
error_msg "Something went wrong! Please try again..."
exit 1
fi
# Step 5: Leave directory (twice due two pushd)
popd &> /dev/null || exit 1
popd &> /dev/null || exit 1
}
# Remove func
function remove_crowsnest(){
if [[ -d "${CROWSNEST_DIR}" ]]; then
pushd "${HOME}/crowsnest" &> /dev/null || exit 1
title_msg "Uninstaller will prompt you for sudo password!"
status_msg "Launching crowsnest uninstaller ..."
if ! make uninstall; then
error_msg "Something went wrong! Please try again..."
exit 1
fi
status_msg "Removing crowsnest directory ..."
rm -rf "${CROWSNEST_DIR}"
ok_msg "Directory removed!"
fi
print_confirm "Crowsnest successfully removed!"
}
# Status funcs
get_crowsnest_status(){
local -a files
local env_file
env_file="$(grep "EnvironmentFile" /etc/systemd/system/crowsnest.service 2>/dev/null | cut -d "=" -f2)"
files=(
"${CROWSNEST_DIR}"
"/usr/local/bin/crowsnest"
"/etc/logrotate.d/crowsnest"
"/etc/systemd/system/crowsnest.service"
"${env_file}"
)
local count
count=0
for file in "${files[@]}"; do
[[ -e "${file}" ]] && count=$(( count +1 ))
done
if [[ "${count}" -eq "${#files[*]}" ]]; then
echo "Installed"
elif [[ "${count}" -gt 0 ]]; then
echo "Incomplete!"
else
echo "Not installed!"
fi
}
# Update funcs
# Shameless stolen from KlipperScreen.sh
function get_local_crowsnest_commit() {
[[ ! -d ${CROWSNEST_DIR} || ! -d "${CROWSNEST_DIR}/.git" ]] && return
local commit
cd "${CROWSNEST_DIR}"
commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)"
echo "${commit}"
}
function get_remote_crowsnest_commit() {
[[ ! -d ${CROWSNEST_DIR} || ! -d "${CROWSNEST_DIR}/.git" ]] && return
local commit
cd "${CROWSNEST_DIR}" && git fetch origin -q
commit=$(git describe origin/master --always --tags | cut -d "-" -f 1,2)
echo "${commit}"
}
function compare_crowsnest_versions() {
local versions local_ver remote_ver
local_ver="$(get_local_crowsnest_commit)"
remote_ver="$(get_remote_crowsnest_commit)"
if [[ ${local_ver} != "${remote_ver}" ]]; then
versions="${yellow}$(printf " %-14s" "${local_ver}")${white}"
versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}"
# add moonraker to application_updates_available in kiauh.ini
add_to_application_updates "crowsnest"
else
versions="${green}$(printf " %-14s" "${local_ver}")${white}"
versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}"
fi
echo "${versions}"
}
function install_crowsnest_dependencies() {
local packages log_name="Crowsnest"
local install_script="${CROWSNEST_DIR}/tools/install.sh"
### read PKGLIST from official install-script
status_msg "Reading dependencies..."
# shellcheck disable=SC2016
packages="$(grep "PKGLIST=" "${install_script}" | cut -d'"' -f2 | sed 's/\${PKGLIST}//g' | tr -d '\n')"
echo "${cyan}${packages}${white}" | tr '[:space:]' '\n'
read -r -a packages <<< "${packages}"
### Update system package lists if stale
update_system_package_lists
### Install required packages
install_system_packages "${log_name}" "packages[@]"
}
function update_crowsnest() {
do_action_service "stop" "crowsnest"
if [[ ! -d ${CROWSNEST_DIR} ]]; then
clone_crowsnest
else
status_msg "Updating Crowsnest ..."
cd "${CROWSNEST_DIR}" && git pull
### read PKGLIST and install possible new dependencies
install_crowsnest_dependencies
fi
ok_msg "Update complete!"
do_action_service "restart" "crowsnest"
}

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
@@ -52,16 +52,57 @@ function init_flash_process() {
esac esac
done done
### step 2: select how the mcu is connected to the host ### step 2: select how the mcu is flashed (flash/serialflash)
select_flash_command
### step 3: select how the mcu is connected to the host
select_mcu_connection select_mcu_connection
### step 3: select which detected mcu should be flashed ### step 4: select which detected mcu should be flashed
select_mcu_id "${method}" select_mcu_id "${method}"
} }
#================================================# #================================================#
#=================== STEP 2 =====================# #=================== STEP 2 =====================#
#================================================# #================================================#
function select_flash_command() {
unset flash_command
top_border
echo -e "| How to flash MCU? |"
echo -e "| 1) make flash (default) |"
echo -e "| 2) make serialflash (stm32flash) |"
blank_line
back_help_footer
local choice
while true; do
read -p "${cyan}###### Flashing command:${white} " -i "1" -e choice
case "${choice}" in
1)
select_msg "Selected 'make flash' command"
flash_command="flash"
break;;
2)
select_msg "Selected 'make serialflash' command"
flash_command="serialflash"
break;;
B|b)
advanced_menu
break;;
H|h)
clear && print_header
show_mcu_flash_command_help
break;;
*)
error_msg "Invalid command!";;
esac
done
}
#================================================#
#=================== STEP 3 =====================#
#================================================#
function select_mcu_connection() { function select_mcu_connection() {
top_border top_border
echo -e "| ${yellow}Make sure that the controller board is connected now!${white} |" echo -e "| ${yellow}Make sure that the controller board is connected now!${white} |"
@@ -70,6 +111,7 @@ function select_mcu_connection() {
echo -e "| How is the controller board connected to the host? |" echo -e "| How is the controller board connected to the host? |"
echo -e "| 1) USB |" echo -e "| 1) USB |"
echo -e "| 2) UART |" echo -e "| 2) UART |"
echo -e "| 3) USB (DFU mode) |"
blank_line blank_line
back_help_footer back_help_footer
@@ -85,6 +127,10 @@ function select_mcu_connection() {
status_msg "Identifying MCU possibly connected via UART ...\n" status_msg "Identifying MCU possibly connected via UART ...\n"
get_uart_id || true # continue even after exit code 1 get_uart_id || true # continue even after exit code 1
break;; break;;
3)
status_msg "Identifying MCU connected via USB in DFU mode ...\n"
get_dfu_id || true # continue even after exit code 1
break;;
B|b) B|b)
advanced_menu advanced_menu
break;; break;;
@@ -107,7 +153,6 @@ function print_detected_mcu_to_screen() {
fi fi
for mcu in "${mcu_list[@]}"; do for mcu in "${mcu_list[@]}"; do
mcu=$(echo "${mcu}" | rev | cut -d"/" -f1 | rev)
echo -e " ● MCU #${i}: ${cyan}${mcu}${white}" echo -e " ● MCU #${i}: ${cyan}${mcu}${white}"
i=$(( i + 1 )) i=$(( i + 1 ))
done done
@@ -115,7 +160,7 @@ function print_detected_mcu_to_screen() {
} }
#================================================# #================================================#
#=================== STEP 3 =====================# #=================== STEP 4 =====================#
#================================================# #================================================#
function select_mcu_id() { function select_mcu_id() {
local i=0 sel_index=0 method=${1} local i=0 sel_index=0 method=${1}
@@ -190,7 +235,7 @@ function start_flash_mcu() {
local device=${1} local device=${1}
do_action_service "stop" "klipper" do_action_service "stop" "klipper"
if make flash FLASH_DEVICE="${device}"; then if make ${flash_command} FLASH_DEVICE="${device}"; then
ok_msg "Flashing successfull!" ok_msg "Flashing successfull!"
else else
warn_msg "Flashing failed!" warn_msg "Flashing failed!"
@@ -329,6 +374,16 @@ function get_uart_id() {
done done
} }
function get_dfu_id() {
unset mcu_list
sleep 1
mcus=$(lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null)
for mcu in ${mcus}; do
mcu_list+=("${mcu}")
done
}
function show_flash_method_help() { function show_flash_method_help() {
top_border top_border
echo -e "| ~~~~~~~~ < ? > Help: Flash MCU < ? > ~~~~~~~~ |" echo -e "| ~~~~~~~~ < ? > Help: Flash MCU < ? > ~~~~~~~~ |"
@@ -372,6 +427,36 @@ function show_flash_method_help() {
done done
} }
function show_mcu_flash_command_help() {
top_border
echo -e "| ~~~~~~~~ < ? > Help: Flash MCU < ? > ~~~~~~~~ |"
hr
echo -e "| ${cyan}make flash:${white} |"
echo -e "| The default command to flash controller board, it |"
echo -e "| will detect selected microcontroller and use suitable |"
echo -e "| tool for flashing it. |"
blank_line
echo -e "| ${cyan}make serialflash:${white} |"
echo -e "| Special command to flash STM32 microcontrollers in |"
echo -e "| DFU mode but connected via serial. stm32flash command |"
echo -e "| will be used internally. |"
blank_line
back_footer
local choice
while true; do
read -p "${cyan}###### Please select:${white} " choice
case "${choice}" in
B|b)
clear && print_header
select_flash_command
break;;
*)
error_msg "Invalid command!";;
esac
done
}
function show_mcu_connection_help() { function show_mcu_connection_help() {
top_border top_border
echo -e "| ~~~~~~~~ < ? > Help: Flash MCU < ? > ~~~~~~~~ |" echo -e "| ~~~~~~~~ < ? > Help: Flash MCU < ? > ~~~~~~~~ |"

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
@@ -16,10 +16,24 @@ set -e
#===================================================# #===================================================#
function install_fluidd() { function install_fluidd() {
### exit early if moonraker not found
if [[ -z $(moonraker_systemd) ]]; then if [[ -z $(moonraker_systemd) ]]; then
local error="Moonraker not installed! Please install Moonraker first!" local error="Moonraker not installed! It's recommended to install Moonraker first!"
print_error "${error}" && return print_error "${error}"
while true; do
local yn
read -p "${cyan}###### Proceed to install Fluidd without installing Moonraker? (y/N):${white} " yn
case "${yn}" in
Y|y|Yes|yes)
select_msg "Yes"
break;;
N|n|No|no|"")
select_msg "No"
abort_msg "Exiting Fluidd setup ...\n"
return;;
*)
error_msg "Invalid Input!";;
esac
done
fi fi
### checking dependencies ### checking dependencies
@@ -30,7 +44,7 @@ function install_fluidd() {
status_msg "Initializing Fluidd installation ..." status_msg "Initializing Fluidd installation ..."
### first, we create a backup of the full klipper_config dir - safety first! ### first, we create a backup of the full klipper_config dir - safety first!
backup_klipper_config_dir backup_config_dir
### check for other enabled web interfaces ### check for other enabled web interfaces
unset SET_LISTEN_PORT unset SET_LISTEN_PORT
@@ -39,30 +53,6 @@ function install_fluidd() {
### check if another site already listens to port 80 ### check if another site already listens to port 80
fluidd_port_check fluidd_port_check
# ### ask user to install mjpg-streamer
# local install_mjpg_streamer
# if [[ ! -f "${SYSTEMD}/webcamd.service" ]]; then
# while true; do
# echo
# top_border
# echo -e "| Install MJPG-Streamer for webcam support? |"
# bottom_border
# read -p "${cyan}###### Please select (y/N):${white} " yn
# case "${yn}" in
# Y|y|Yes|yes)
# select_msg "Yes"
# install_mjpg_streamer="true"
# break;;
# N|n|No|no|"")
# select_msg "No"
# install_mjpg_streamer="false"
# break;;
# *)
# error_msg "Invalid command!";;
# esac
# done
# fi
### download fluidd ### download fluidd
download_fluidd download_fluidd
@@ -82,9 +72,6 @@ function install_fluidd() {
### add fluidd to the update manager in moonraker.conf ### add fluidd to the update manager in moonraker.conf
patch_fluidd_update_manager patch_fluidd_update_manager
### install mjpg-streamer
# [[ ${install_mjpg_streamer} == "true" ]] && install_mjpg-streamer
fetch_webui_ports #WIP fetch_webui_ports #WIP
### confirm message ### confirm message
@@ -92,22 +79,21 @@ function install_fluidd() {
} }
function install_fluidd_macros() { function install_fluidd_macros() {
local yn
while true; do while true; do
echo echo
top_border top_border
echo -e "| It is recommended to have some important macros in |" echo -e "| It is recommended to use special macros in order to |"
echo -e "| your printer configuration to have Fluidd fully |" echo -e "| have Fluidd fully functional and working. |"
echo -e "| functional and working. |"
blank_line blank_line
echo -e "| The recommended macros for Fluidd can be found here: |" echo -e "| The recommended macros for Fluidd can be found here: |"
echo -e "| https://docs.fluidd.xyz/configuration/initial_setup |" echo -e "| https://github.com/fluidd-core/fluidd-config |"
blank_line blank_line
echo -e "| If you already have these macros in your config file, |" echo -e "| If you already use these macros skip this step. |"
echo -e "| skip this step and answer with 'no'. |"
echo -e "| Otherwise you should consider to answer with 'yes' to |" echo -e "| Otherwise you should consider to answer with 'yes' to |"
echo -e "| add the recommended example macros to your config. |" echo -e "| download the recommended macros. |"
bottom_border bottom_border
read -p "${cyan}###### Add the recommended macros? (Y/n):${white} " yn read -p "${cyan}###### Download the recommended macros? (Y/n):${white} " yn
case "${yn}" in case "${yn}" in
Y|y|Yes|yes|"") Y|y|Yes|yes|"")
select_msg "Yes" select_msg "Yes"
@@ -124,36 +110,64 @@ function install_fluidd_macros() {
} }
function download_fluidd_macros() { function download_fluidd_macros() {
local fluidd_cfg path configs regex local ms_cfg_repo path configs regex line gcode_dir
fluidd_cfg="https://raw.githubusercontent.com/fluidd-core/FluiddPI/master/src/modules/fluidd/filesystem/home/pi/klipper_config/fluidd.cfg" ms_cfg_repo="https://github.com/fluidd-core/fluidd-config.git"
regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/config\/printer\.cfg" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/printer\.cfg"
configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort)
if [[ -n ${configs} ]]; then if [[ -z ${configs} ]]; then
for config in ${configs}; do print_error "No printer.cfg found! Installation of Macros will be skipped ..."
path=$(echo "${config}" | rev | cut -d"/" -f2- | rev)
if [[ ! -f "${path}/fluidd.cfg" ]]; then
status_msg "Downloading fluidd.cfg to ${path} ..."
log_info "downloading fluidd.cfg to: ${path}"
wget "${fluidd_cfg}" -O "${path}/fluidd.cfg"
### replace user 'pi' with current username to prevent issues in cases where the user is not called 'pi'
log_info "modify fluidd.cfg"
sed -i "/^path: \/home\/pi\/gcode_files/ s/\/home\/pi/\/home\/${USER}/" "${path}/fluidd.cfg"
### write include to the very first line of the printer.cfg
if ! grep -Eq "^[include fluidd.cfg]$" "${path}/printer.cfg"; then
log_info "modify printer.cfg"
sed -i "1 i [include fluidd.cfg]" "${path}/printer.cfg"
fi
ok_msg "Done!"
fi
done
else
log_error "execution stopped! reason: no printer.cfg found" log_error "execution stopped! reason: no printer.cfg found"
return return
fi fi
status_msg "Cloning fluidd-config ..."
[[ -d "${HOME}/fluidd-config" ]] && rm -rf "${HOME}/fluidd-config"
if git clone --recurse-submodules "${ms_cfg_repo}" "${HOME}/fluidd-config"; then
for config in ${configs}; do
path=$(echo "${config}" | rev | cut -d"/" -f2- | rev)
if [[ -e "${path}/fluidd.cfg" && ! -h "${path}/fluidd.cfg" ]]; then
warn_msg "Attention! Existing fluidd.cfg detected!"
warn_msg "The file will be renamed to 'fluidd.bak.cfg' to be able to continue with the installation."
if ! mv "${path}/fluidd.cfg" "${path}/fluidd.bak.cfg"; then
error_msg "Renaming fluidd.cfg failed! Aborting installation ..."
return
fi
fi
if [[ -h "${path}/fluidd.cfg" ]]; then
warn_msg "Recreating symlink in ${path} ..."
rm -rf "${path}/fluidd.cfg"
fi
if ! ln -sf "${HOME}/fluidd-config/client.cfg" "${path}/fluidd.cfg"; then
error_msg "Creating symlink failed! Aborting installation ..."
return
fi
if ! grep -Eq "^\[include fluidd.cfg\]$" "${path}/printer.cfg"; then
log_info "${path}/printer.cfg"
sed -i "1 i [include fluidd.cfg]" "${path}/printer.cfg"
fi
line=$(($(grep -n "\[include fluidd.cfg\]" "${path}/printer.cfg" | tail -1 | cut -d: -f1) + 1))
gcode_dir=${path/config/gcodes}
if ! grep -Eq "^\[virtual_sdcard\]$" "${path}/printer.cfg"; then
log_info "${path}/printer.cfg"
sed -i "${line} i \[virtual_sdcard]\npath: ${gcode_dir}\non_error_gcode: CANCEL_PRINT\n" "${path}/printer.cfg"
fi
done
else
print_error "Cloning failed! Aborting installation ..."
log_error "execution stopped! reason: cloning failed"
return
fi
patch_fluidd_config_update_manager
ok_msg "Done!"
} }
function download_fluidd() { function download_fluidd() {
@@ -191,7 +205,7 @@ function remove_fluidd_dir() {
rm -rf "${FLUIDD_DIR}" && ok_msg "Directory removed!" rm -rf "${FLUIDD_DIR}" && ok_msg "Directory removed!"
} }
function remove_fluidd_config() { function remove_fluidd_nginx_config() {
if [[ -e "/etc/nginx/sites-available/fluidd" ]]; then if [[ -e "/etc/nginx/sites-available/fluidd" ]]; then
status_msg "Removing Fluidd configuration for Nginx ..." status_msg "Removing Fluidd configuration for Nginx ..."
sudo rm "/etc/nginx/sites-available/fluidd" && ok_msg "File removed!" sudo rm "/etc/nginx/sites-available/fluidd" && ok_msg "File removed!"
@@ -218,7 +232,7 @@ function remove_fluidd_logs() {
function remove_fluidd_log_symlinks() { function remove_fluidd_log_symlinks() {
local files regex local files regex
regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/logs\/fluidd-.*" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/fluidd-.*"
files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" 2> /dev/null | sort) files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" 2> /dev/null | sort)
if [[ -n ${files} ]]; then if [[ -n ${files} ]]; then
@@ -243,9 +257,18 @@ function remove_legacy_fluidd_log_symlinks() {
fi fi
} }
function remove_fluidd_config() {
if [[ -d "${HOME}/fluidd-config" ]]; then
status_msg "Removing ${HOME}/fluidd-config ..."
rm -rf "${HOME}/fluidd-config"
ok_msg "${HOME}/fluidd-config removed!"
print_confirm "Fluidd-Config successfully removed!"
fi
}
function remove_fluidd() { function remove_fluidd() {
remove_fluidd_dir remove_fluidd_dir
remove_fluidd_config remove_fluidd_nginx_config
remove_fluidd_logs remove_fluidd_logs
remove_fluidd_log_symlinks remove_fluidd_log_symlinks
remove_legacy_fluidd_log_symlinks remove_legacy_fluidd_log_symlinks
@@ -294,19 +317,25 @@ function get_fluidd_status() {
} }
function get_local_fluidd_version() { function get_local_fluidd_version() {
[[ ! -f "${FLUIDD_DIR}/.version" ]] && return local versionfile="${FLUIDD_DIR}/.version"
local relinfofile="${FLUIDD_DIR}/release_info.json"
local version local version
version=$(head -n 1 "${FLUIDD_DIR}/.version")
if [[ -f ${relinfofile} ]]; then
version=$(grep -o '"version":"[^"]*' "${relinfofile}" | grep -o '[^"]*$')
elif [[ -f ${versionfile} ]]; then
version=$(head -n 1 "${versionfile}")
fi
echo "${version}" echo "${version}"
} }
function get_remote_fluidd_version() { function get_remote_fluidd_version() {
[[ ! $(dpkg-query -f'${Status}' --show curl 2>/dev/null) = *\ installed ]] && return [[ ! $(dpkg-query -f'${Status}' --show curl 2>/dev/null) = *\ installed ]] && return
local version local tags
version=$(get_fluidd_download_url | rev | cut -d"/" -f2 | rev) tags=$(curl -s "https://api.github.com/repos/fluidd-core/fluidd/tags" | grep "name" | cut -d'"' -f4)
echo "${version}" echo "${tags}" | head -1
} }
function compare_fluidd_versions() { function compare_fluidd_versions() {
@@ -332,25 +361,28 @@ function compare_fluidd_versions() {
#================================================# #================================================#
function get_fluidd_download_url() { function get_fluidd_download_url() {
local tags latest_tag latest_url stable_tag stable_url url local releases_by_tag tags tag unstable_url url
tags=$(curl -s "${FLUIDD_TAGS}" | grep "name" | cut -d'"' -f4)
### latest download url including pre-releases (alpha, beta, rc) ### latest stable download url
latest_tag=$(echo "${tags}" | head -1) url="https://github.com/fluidd-core/fluidd/releases/latest/download/fluidd.zip"
latest_url="https://github.com/fluidd-core/fluidd/releases/download/${latest_tag}/fluidd.zip"
### get stable fluidd download url
stable_tag=$(echo "${tags}" | grep -E "^v([0-9]+\.?){3}$" | head -1)
stable_url="https://github.com/fluidd-core/fluidd/releases/download/${stable_tag}/fluidd.zip"
read_kiauh_ini "${FUNCNAME[0]}" read_kiauh_ini "${FUNCNAME[0]}"
if [[ ${fluidd_install_unstable} == "true" ]]; then if [[ ${fluidd_install_unstable} == "true" ]]; then
url="${latest_url}" releases_by_tag="https://api.github.com/repos/fluidd-core/fluidd/tags"
echo "${url}" tags=$(curl -s "${releases_by_tag}" | grep "name" | cut -d'"' -f4)
else tag=$(echo "${tags}" | head -1)
url="${stable_url}"
echo "${url}" ### latest unstable download url including pre-releases (alpha, beta, rc)
unstable_url="https://github.com/fluidd-core/fluidd/releases/download/${tag}/fluidd.zip"
if [[ ${unstable_url} == *"download//"* ]]; then
warn_msg "Download URL broken! Falling back to URL of latest stable release!"
else
url=${unstable_url}
fi
fi fi
echo "${url}"
} }
function fluidd_port_check() { function fluidd_port_check() {
@@ -387,7 +419,7 @@ function select_fluidd_port() {
blank_line blank_line
[[ ${MAINSAIL_PORT} == "80" ]] && echo "| ● Mainsail |" [[ ${MAINSAIL_PORT} == "80" ]] && echo "| ● Mainsail |"
blank_line blank_line
echo -e "| Make sure you don't choose a port which is already |" echo -e "| Make sure you don't choose a port which was already |"
echo -e "| assigned to another webinterface! |" echo -e "| assigned to another webinterface! |"
blank_line blank_line
echo -e "| Be aware: there is ${red}NO${white} sanity check for the following |" echo -e "| Be aware: there is ${red}NO${white} sanity check for the following |"
@@ -414,12 +446,12 @@ function select_fluidd_port() {
function patch_fluidd_update_manager() { function patch_fluidd_update_manager() {
local patched moonraker_configs regex local patched moonraker_configs regex
regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf"
moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort) moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort)
patched="false" patched="false"
for conf in ${moonraker_configs}; do for conf in ${moonraker_configs}; do
if ! grep -Eq "^\[update_manager fluidd\]$" "${conf}"; then if ! grep -Eq "^\[update_manager fluidd\]\s*$" "${conf}"; then
### add new line to conf if it doesn't end with one ### add new line to conf if it doesn't end with one
[[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}"
@@ -443,3 +475,36 @@ MOONRAKER_CONF
do_action_service "restart" "moonraker" do_action_service "restart" "moonraker"
fi fi
} }
function patch_fluidd_config_update_manager() {
local patched moonraker_configs regex
regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf"
moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort)
patched="false"
for conf in ${moonraker_configs}; do
if ! grep -Eq "^\[update_manager fluidd-config\]\s*$" "${conf}"; then
### add new line to conf if it doesn't end with one
[[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}"
### add Fluidds update manager section to moonraker.conf
status_msg "Adding Fluidd-Config to update manager in file:\n ${conf}"
/bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF
[update_manager fluidd-config]
type: git_repo
primary_branch: master
path: ~/fluidd-config
origin: https://github.com/fluidd-core/fluidd-config.git
managed_services: klipper
MOONRAKER_CONF
fi
patched="true"
done
if [[ ${patched} == "true" ]]; then
do_action_service "restart" "moonraker"
fi
}

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
@@ -103,10 +103,10 @@ function install_gcode_shell_command() {
function create_example_shell_command() { function create_example_shell_command() {
### create a backup of the config folder ### create a backup of the config folder
backup_klipper_config_dir backup_config_dir
local configs regex path local configs regex path
regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/config\/printer\.cfg" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/printer\.cfg"
configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort)
for cfg in ${configs}; do for cfg in ${configs}; do

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
@@ -42,13 +42,9 @@ function set_globals() {
#================= MAINSAIL ===================# #================= MAINSAIL ===================#
MAINSAIL_DIR="${HOME}/mainsail" MAINSAIL_DIR="${HOME}/mainsail"
MAINSAIL_REPO_API="https://api.github.com/repos/mainsail-crew/mainsail/releases"
MAINSAIL_TAGS="https://api.github.com/repos/mainsail-crew/mainsail/tags"
#================== FLUIDD ====================# #================== FLUIDD ====================#
FLUIDD_DIR="${HOME}/fluidd" FLUIDD_DIR="${HOME}/fluidd"
FLUIDD_REPO_API="https://api.github.com/repos/fluidd-core/fluidd/releases"
FLUIDD_TAGS="https://api.github.com/repos/fluidd-core/fluidd/tags"
#=============== KLIPPERSCREEN ================# #=============== KLIPPERSCREEN ================#
KLIPPERSCREEN_ENV="${HOME}/.KlipperScreen-env" KLIPPERSCREEN_ENV="${HOME}/.KlipperScreen-env"
@@ -72,4 +68,19 @@ function set_globals() {
#=============== MOONRAKER-OBICO ================# #=============== MOONRAKER-OBICO ================#
MOONRAKER_OBICO_DIR="${HOME}/moonraker-obico" MOONRAKER_OBICO_DIR="${HOME}/moonraker-obico"
MOONRAKER_OBICO_REPO="https://github.com/TheSpaghettiDetective/moonraker-obico.git" MOONRAKER_OBICO_REPO="https://github.com/TheSpaghettiDetective/moonraker-obico.git"
#=============== OCTOEVERYWHERE ================#
OCTOEVERYWHERE_ENV="${HOME}/octoeverywhere-env"
OCTOEVERYWHERE_DIR="${HOME}/octoeverywhere"
OCTOEVERYWHERE_REPO="https://github.com/QuinnDamerell/OctoPrint-OctoEverywhere.git"
#=============== Crowsnest ================#
CROWSNEST_DIR="${HOME}/crowsnest"
CROWSNEST_REPO="https://github.com/mainsail-crew/crowsnest.git"
#=============== Mobileraker ================#
MOBILERAKER_ENV="${HOME}/mobileraker-env"
MOBILERAKER_DIR="${HOME}/mobileraker_companion"
MOBILERAKER_REPO="https://github.com/Clon1998/mobileraker_companion.git"
} }

View File

@@ -1,39 +1,64 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
set -e set -e
#TODO (multi instance):
# if the klipper installer is started another time while other klipper
# instances are detected, ask if new instances should be added
#=================================================# #=================================================#
#================ INSTALL KLIPPER ================# #================ INSTALL KLIPPER ================#
#=================================================# #=================================================#
function klipper_setup_dialog() { ###
status_msg "Initializing Klipper installation ..." # this function detects all installed klipper
# systemd instances and returns their absolute path
function klipper_systemd() {
local services
local blacklist
local ignore
local match
local klipper_initd_service ###
# any service that uses "klipper" in its own name but isn't a full klipper service must be blacklisted using
# this variable, otherwise they will be falsely recognized as klipper instances. E.g. "klipper-mcu.service"
# is not a klipper service, but related to klippers linux mcu, which also requires its own service file, hence
# it must be blacklisted.
blacklist="mcu"
ignore="${SYSTEMD}/klipper-(${blacklist}).service"
match="${SYSTEMD}/klipper(-[0-9a-zA-Z]+)?.service"
services=$(find "${SYSTEMD}" -maxdepth 1 -regextype awk ! -regex "${ignore}" -regex "${match}" | sort)
echo "${services}"
}
function start_klipper_setup() {
local klipper_systemd_services local klipper_systemd_services
local python_version="${1}" user_input=() local python_version
local instance_count
local instance_names
local use_custom_names
local input
local regex
local blacklist
local error local error
klipper_initd_service=$(find_klipper_initd) status_msg "Initializing Klipper installation ...\n"
klipper_systemd_services=$(find_klipper_systemd)
user_input+=("${python_version}")
### return early if klipper already exists ### return early if klipper already exists
if [[ -n ${klipper_initd_service} ]]; then klipper_systemd_services=$(klipper_systemd)
error="Unsupported Klipper SysVinit service detected:"
error="${error}\n ➔ ${klipper_initd_service}" if [[ -n ${klipper_systemd_services} ]]; then
error="${error}\n Please re-install Klipper with KIAUH!"
log_info "Unsupported Klipper SysVinit service detected: ${klipper_initd_service}"
elif [[ -n ${klipper_systemd_services} ]]; then
error="At least one Klipper service is already installed:" error="At least one Klipper service is already installed:"
for s in ${klipper_systemd_services}; do for s in ${klipper_systemd_services}; do
@@ -41,10 +66,118 @@ function klipper_setup_dialog() {
error="${error}\n ➔ ${s}" error="${error}\n ➔ ${s}"
done done
fi fi
[[ -n ${error} ]] && print_error "${error}" && return [[ -n ${error} ]] && print_error "${error}" && return
### ask for amount of instances to create ### user selection for python version
print_dialog_user_select_python_version
while true; do
read -p "${cyan}###### Select Python version:${white} " -i "1" -e input
case "${input}" in
1)
select_msg "Python 3.x\n"
python_version=3
break;;
2)
select_msg "Python 2.7\n"
python_version=2
break;;
B|b)
clear; install_menu; break;;
*)
error_msg "Invalid Input!\n";;
esac
done && input=""
### user selection for instance count
print_dialog_user_select_instance_count
regex="^[1-9][0-9]*$"
while [[ ! ${input} =~ ${regex} ]]; do
read -p "${cyan}###### Number of Klipper instances to set up:${white} " -i "1" -e input
if [[ ${input} =~ ${regex} ]]; then
instance_count="${input}"
select_msg "Instance count: ${instance_count}\n"
break
elif [[ ${input} == "B" || ${input} == "b" ]]; then
install_menu
else
error_msg "Invalid Input!\n"
fi
done && input=""
### user selection for custom names
use_custom_names="false"
if (( instance_count > 1 )); then
print_dialog_user_select_custom_name_bool
while true; do
read -p "${cyan}###### Assign custom names? (y/N):${white} " input
case "${input}" in
Y|y|Yes|yes)
select_msg "Yes\n"
use_custom_names="true"
break;;
N|n|No|no|"")
select_msg "No\n"
break;;
B|b)
clear; install_menu; break;;
*)
error_msg "Invalid Input!\n";;
esac
done && input=""
else
instance_names+=("printer")
fi
### user selection for setting the actual custom names
shopt -s nocasematch
if (( instance_count > 1 )) && [[ ${use_custom_names} == "true" ]]; then
local i
i=1
regex="^[0-9a-zA-Z]+$"
blacklist="mcu"
while [[ ! ${input} =~ ${regex} || ${input} =~ ${blacklist} || ${i} -le ${instance_count} ]]; do
read -p "${cyan}###### Name for instance #${i}:${white} " input
if [[ ${input} =~ ${blacklist} ]]; then
error_msg "Name not allowed! You are trying to use a reserved name."
elif [[ ${input} =~ ${regex} && ! ${input} =~ ${blacklist} ]]; then
select_msg "Name: ${input}\n"
if [[ ${input} =~ ^[0-9]+$ ]]; then
instance_names+=("printer_${input}")
else
instance_names+=("${input}")
fi
i=$(( i + 1 ))
else
error_msg "Invalid Input!\n"
fi
done && input=""
elif (( instance_count > 1 )) && [[ ${use_custom_names} == "false" ]]; then
for (( i=1; i <= instance_count; i++ )); do
instance_names+=("printer_${i}")
done
fi
shopt -u nocasematch
(( instance_count > 1 )) && status_msg "Installing ${instance_count} Klipper instances ..."
(( instance_count == 1 )) && status_msg "Installing single Klipper instance ..."
run_klipper_setup "${python_version}" "${instance_names[@]}"
}
function print_dialog_user_select_python_version() {
top_border
echo -e "| Please select your preferred Python version. | "
echo -e "| The recommended version is Python 3.x. | "
hr
echo -e "| 1) [Python 3.x] (recommended) | "
echo -e "| 2) [Python 2.7] ${yellow}(legacy)${white} | "
back_footer
}
function print_dialog_user_select_instance_count() {
top_border top_border
echo -e "| Please select the number of Klipper instances to set |" echo -e "| Please select the number of Klipper instances to set |"
echo -e "| up. The number of Klipper instances will determine |" echo -e "| up. The number of Klipper instances will determine |"
@@ -52,100 +185,36 @@ function klipper_setup_dialog() {
blank_line blank_line
echo -e "| ${yellow}WARNING:${white} |" echo -e "| ${yellow}WARNING:${white} |"
echo -e "| ${yellow}Setting up too many instances may crash your system.${white} |" echo -e "| ${yellow}Setting up too many instances may crash your system.${white} |"
bottom_border back_footer
### ask for amount of instances
local klipper_count re="^[1-9][0-9]*$"
while [[ ! ${klipper_count} =~ ${re} ]]; do
read -p "${cyan}###### Number of Klipper instances to set up:${white} " -i "1" -e klipper_count
### break if input is valid
[[ ${klipper_count} =~ ${re} ]] && break
### error messages on invalid input
error_msg "Input not a number"
done && select_msg "${klipper_count}"
user_input+=("${klipper_count}")
### confirm instance amount
local yn
while true; do
read -p "${cyan}###### Install ${klipper_count} instance(s)? (Y/n):${white} " yn
case "${yn}" in
Y|y|Yes|yes|"")
select_msg "Yes"
break;;
N|n|No|no)
select_msg "No"
abort_msg "Exiting Klipper setup ...\n"
return;;
*)
error_msg "Invalid Input!";;
esac
done
### ask for custom names
if (( klipper_count > 1 )); then
local custom_names="false"
top_border
echo -e "| You can give each instance a custom name or skip. |"
echo -e "| If skipped, KIAUH will automatically assign an index |"
echo -e "| to each instance in ascending order, starting at '1'. |"
blank_line
echo -e "| Info: |"
echo -e "| Only alphanumeric characters will be allowed. |"
bottom_border
while true; do
read -p "${cyan}###### Use custom names? (y/N):${white} " yn
case "${yn}" in
Y|y|Yes|yes)
select_msg "Yes"
custom_names="true"
break;;
N|n|No|no|"")
select_msg "No"
break;;
*)
error_msg "Invalid Input!";;
esac
done
### get user input for custom names
if [[ ${custom_names} == "true" ]]; then
local i=1 name re="^[0-9a-zA-Z]+$"
while [[ ! ${name} =~ ${re} || ${i} -le ${klipper_count} ]]; do
read -p "${cyan}###### Name for instance #${i}:${white} " name
if [[ ${name} =~ ${re} ]]; then
select_msg "Name: ${name}"
user_input+=("${name}")
i=$(( i + 1 ))
else
error_msg "Invalid Input!"
fi
done
else
### if no custom names are used, add the respective amount of indices to the user_input array
for (( i=1; i <= klipper_count; i++ )); do
user_input+=("${i}")
done
fi
fi
(( klipper_count > 1 )) && status_msg "Installing ${klipper_count} Klipper instances ..."
(( klipper_count == 1 )) && status_msg "Installing single Klipper instance ..."
klipper_setup "${user_input[@]}"
} }
function klipper_setup() { function print_dialog_user_select_custom_name_bool() {
top_border
echo -e "| You can now assign a custom name to each instance. |"
echo -e "| If skipped, each instance will get an index assigned |"
echo -e "| in ascending order, starting at index '1'. |"
blank_line
echo -e "| Info: |"
echo -e "| Only alphanumeric characters for names are allowed! |"
back_footer
}
function run_klipper_setup() {
read_kiauh_ini "${FUNCNAME[0]}" read_kiauh_ini "${FUNCNAME[0]}"
### index 0: python version, index 1: instance count, index 2-n: instance names (optional)
local user_input=("${@}") local python_version=${1}
local python_version="${user_input[0]}" && unset "user_input[0]" local instance_names
local instance_arr=("${user_input[@]}") && unset "user_input[@]" local confirm
local custom_repo="${custom_klipper_repo}" local custom_repo
local custom_branch="${custom_klipper_repo_branch}" local custom_branch
local dep=(git) local dep
shift 1
read -r -a instance_names <<< "${@}"
custom_repo="${custom_klipper_repo}"
custom_branch="${custom_klipper_repo_branch}"
dep=(git)
### checking dependencies ### checking dependencies
dependency_check "${dep[@]}" dependency_check "${dep[@]}"
@@ -157,8 +226,10 @@ function klipper_setup() {
install_klipper_packages "${python_version}" install_klipper_packages "${python_version}"
create_klipper_virtualenv "${python_version}" create_klipper_virtualenv "${python_version}"
### step 3: configure and create klipper instances ### step 3: create klipper instances
configure_klipper_service "${instance_arr[@]}" for instance in "${instance_names[@]}"; do
create_klipper_service "${instance}"
done
### step 4: enable and start all instances ### step 4: enable and start all instances
do_action_service "enable" "klipper" do_action_service "enable" "klipper"
@@ -168,9 +239,12 @@ function klipper_setup() {
check_usergroups check_usergroups
### confirm message ### confirm message
local confirm="" (( ${#instance_names[@]} == 1 )) && confirm="Klipper has been set up!"
(( instance_arr[0] == 1 )) && confirm="Klipper has been set up!" (( ${#instance_names[@]} > 1 )) && confirm="${#instance_names[@]} Klipper instances have been set up!"
(( instance_arr[0] > 1 )) && confirm="${instance_arr[0]} Klipper instances have been set up!"
### finalizing the setup with writing instance names to the kiauh.ini
set_multi_instance_names
mask_disrupting_services
print_confirm "${confirm}" && return print_confirm "${confirm}" && return
} }
@@ -201,37 +275,18 @@ function clone_klipper() {
function create_klipper_virtualenv() { function create_klipper_virtualenv() {
local python_version="${1}" local python_version="${1}"
[[ ${python_version} == "python2" ]] && \
status_msg "Installing $(python2 -V) virtual environment..."
[[ ${python_version} == "python3" ]] && \
status_msg "Installing $(python3 -V) virtual environment..."
### remove klippy-env if it already exists
[[ -d ${KLIPPY_ENV} ]] && rm -rf "${KLIPPY_ENV}" [[ -d ${KLIPPY_ENV} ]] && rm -rf "${KLIPPY_ENV}"
if [[ ${python_version} == "python2" ]]; then status_msg "Installing $("python${python_version}" -V) virtual environment..."
if virtualenv -p python2 "${KLIPPY_ENV}"; then
"${KLIPPY_ENV}"/bin/pip install -r "${KLIPPER_DIR}"/scripts/klippy-requirements.txt
else
log_error "failure while creating python2 klippy-env"
error_msg "Creation of Klipper virtualenv failed!"
exit 1
fi
fi
if [[ ${python_version} == "python3" ]]; then if virtualenv -p "python${python_version}" "${KLIPPY_ENV}"; then
if virtualenv -p python3 "${KLIPPY_ENV}"; then (( python_version == 3 )) && "${KLIPPY_ENV}"/bin/pip install -U pip
"${KLIPPY_ENV}"/bin/pip install -U pip "${KLIPPY_ENV}"/bin/pip install -r "${KLIPPER_DIR}"/scripts/klippy-requirements.txt
"${KLIPPY_ENV}"/bin/pip install -r "${KLIPPER_DIR}"/scripts/klippy-requirements.txt else
else log_error "failure while creating python3 klippy-env"
log_error "failure while creating python3 klippy-env" error_msg "Creation of Klipper virtualenv failed!"
error_msg "Creation of Klipper virtualenv failed!" exit 1
exit 1
fi
fi fi
return
} }
### ###
@@ -241,7 +296,7 @@ function create_klipper_virtualenv() {
# @param {string}: python_version - klipper-env python version # @param {string}: python_version - klipper-env python version
# #
function install_klipper_packages() { function install_klipper_packages() {
local packages python_version="${1}" local packages log_name="Klipper" python_version="${1}"
local install_script="${KLIPPER_DIR}/scripts/install-debian.sh" local install_script="${KLIPPER_DIR}/scripts/install-debian.sh"
status_msg "Reading dependencies..." status_msg "Reading dependencies..."
@@ -252,10 +307,10 @@ function install_klipper_packages() {
### add dbus requirement for DietPi distro ### add dbus requirement for DietPi distro
[[ -e "/boot/dietpi/.version" ]] && packages+=" dbus" [[ -e "/boot/dietpi/.version" ]] && packages+=" dbus"
if [[ ${python_version} == "python3" ]]; then if (( python_version == 3 )); then
### replace python-dev with python3-dev if python3 was selected ### replace python-dev with python3-dev if python3 was selected
packages="${packages//python-dev/python3-dev}" packages="${packages//python-dev/python3-dev}"
elif [[ ${python_version} == "python2" ]]; then elif (( python_version == 2 )); then
### package name 'python-dev' is deprecated (-> no installation candidate) on more modern linux distros ### package name 'python-dev' is deprecated (-> no installation candidate) on more modern linux distros
packages="${packages//python-dev/python2-dev}" packages="${packages//python-dev/python2-dev}"
else else
@@ -267,112 +322,79 @@ function install_klipper_packages() {
echo "${cyan}${packages}${white}" | tr '[:space:]' '\n' echo "${cyan}${packages}${white}" | tr '[:space:]' '\n'
read -r -a packages <<< "${packages}" read -r -a packages <<< "${packages}"
### Update system package info ### Update system package lists if stale
status_msg "Updating package lists..." update_system_package_lists
if ! sudo apt-get update --allow-releaseinfo-change; then
log_error "failure while updating package lists"
error_msg "Updating package lists failed!"
exit 1
fi
### Install required packages ### Install required packages
status_msg "Installing required packages..." install_system_packages "${log_name}" "packages[@]"
if ! sudo apt-get install --yes "${packages[@]}"; then
log_error "failure while installing required klipper packages"
error_msg "Installing required packages failed!"
exit 1
fi
} }
function configure_klipper_service() { function create_klipper_service() {
local input=("${@}") local instance_name=${1}
local klipper_count=${input[0]} && unset "input[0]"
local names=("${input[@]}") && unset "input[@]"
local printer_data cfg_dir cfg log printer uds service env_file
if (( klipper_count == 1 )) && [[ ${#names[@]} -eq 0 ]]; then local printer_data
printer_data="${HOME}/printer_data" local cfg_dir
cfg_dir="${printer_data}/config" local gcodes_dir
cfg="${cfg_dir}/printer.cfg" local cfg
log="${printer_data}/logs/klippy.log" local log
printer="${printer_data}/comms/klippy.serial" local klippy_serial
uds="${printer_data}/comms/klippy.sock" local klippy_socket
service="${SYSTEMD}/klipper.service" local env_file
env_file="${printer_data}/systemd/klipper.env" local service
local service_template
local env_template
local suffix
### create required folder structure printer_data="${HOME}/${instance_name}_data"
create_required_folders "${printer_data}" cfg_dir="${printer_data}/config"
gcodes_dir="${printer_data}/gcodes"
### write single instance service cfg="${cfg_dir}/printer.cfg"
write_klipper_service "" "${cfg}" "${log}" "${printer}" "${uds}" "${service}" "${env_file}" log="${printer_data}/logs/klippy.log"
write_example_printer_cfg "${cfg_dir}" "${cfg}" klippy_serial="${printer_data}/comms/klippy.serial"
ok_msg "Klipper instance created!" klippy_socket="${printer_data}/comms/klippy.sock"
env_file="${printer_data}/systemd/klipper.env"
elif (( klipper_count >= 1 )) && [[ ${#names[@]} -gt 0 ]]; then
local j=0 re="^[1-9][0-9]*$"
for (( i=1; i <= klipper_count; i++ )); do
### overwrite config folder if name is only a number
if [[ ${names[j]} =~ ${re} ]]; then
printer_data="${HOME}/printer_${names[${j}]}_data"
else
printer_data="${HOME}/${names[${j}]}_data"
fi
cfg_dir="${printer_data}/config"
cfg="${cfg_dir}/printer.cfg"
log="${printer_data}/logs/klippy.log"
printer="${printer_data}/comms/klippy.serial"
uds="${printer_data}/comms/klippy.sock"
service="${SYSTEMD}/klipper-${names[${j}]}.service"
env_file="${printer_data}/systemd/klipper.env"
### create required folder structure
create_required_folders "${printer_data}"
### write multi instance service
write_klipper_service "${names[${j}]}" "${cfg}" "${log}" "${printer}" "${uds}" "${service}" "${env_file}"
write_example_printer_cfg "${cfg_dir}" "${cfg}"
ok_msg "Klipper instance 'klipper-${names[${j}]}' created!"
j=$(( j + 1 ))
done && unset j
if [[ ${instance_name} == "printer" ]]; then
suffix="${instance_name//printer/}"
else else
return 1 suffix="-${instance_name//printer_/}"
fi fi
}
function write_klipper_service() { create_required_folders "${printer_data}"
local i=${1} cfg=${2} log=${3} printer=${4} uds=${5} service=${6} env_file=${7}
local service_template="${KIAUH_SRCDIR}/resources/klipper.service" service_template="${KIAUH_SRCDIR}/resources/klipper.service"
local env_template="${KIAUH_SRCDIR}/resources/klipper.env" env_template="${KIAUH_SRCDIR}/resources/klipper.env"
service="${SYSTEMD}/klipper${suffix}.service"
### replace all placeholders
if [[ ! -f ${service} ]]; then if [[ ! -f ${service} ]]; then
status_msg "Creating Klipper Service ${i} ..." status_msg "Create Klipper service file ..."
sudo cp "${service_template}" "${service}" sudo cp "${service_template}" "${service}"
sudo cp "${env_template}" "${env_file}" sudo cp "${env_template}" "${env_file}"
[[ -z ${i} ]] && sudo sed -i "s| %INST%||" "${service}" sudo sed -i "s|%USER%|${USER}|g; s|%KLIPPER_DIR%|${KLIPPER_DIR}|; s|%ENV%|${KLIPPY_ENV}|; s|%ENV_FILE%|${env_file}|" "${service}"
[[ -n ${i} ]] && sudo sed -i "s|%INST%|${i}|" "${service}" sudo sed -i "s|%USER%|${USER}|; s|%KLIPPER_DIR%|${KLIPPER_DIR}|; s|%LOG%|${log}|; s|%CFG%|${cfg}|; s|%PRINTER%|${klippy_serial}|; s|%UDS%|${klippy_socket}|" "${env_file}"
sudo sed -i "s|%USER%|${USER}|g; s|%ENV%|${KLIPPY_ENV}|; s|%ENV_FILE%|${env_file}|" "${service}"
sudo sed -i "s|%USER%|${USER}|; s|%LOG%|${log}|; s|%CFG%|${cfg}|; s|%PRINTER%|${printer}|; s|%UDS%|${uds}|" "${env_file}" ok_msg "Klipper service file created!"
fi
if [[ ! -f ${cfg} ]]; then
write_example_printer_cfg "${cfg}" "${gcodes_dir}"
fi fi
} }
function write_example_printer_cfg() { function write_example_printer_cfg() {
local cfg_dir=${1} cfg=${2} local cfg=${1}
local cfg_template="${KIAUH_SRCDIR}/resources/example.printer.cfg" local gcodes_dir=${2}
local cfg_template
### create a config directory if it doesn't exist cfg_template="${KIAUH_SRCDIR}/resources/example.printer.cfg"
if [[ ! -d ${cfg_dir} ]]; then
status_msg "Creating '${cfg_dir}' ..."
mkdir -p "${cfg_dir}"
fi
### create a minimal config if there is no printer.cfg status_msg "Creating minimal example printer.cfg ..."
if [[ ! -f ${cfg} ]]; then if cp "${cfg_template}" "${cfg}"; then
status_msg "Creating minimal example printer.cfg ..." sed -i "s|%GCODES_DIR%|${gcodes_dir}|" "${cfg}"
cp "${cfg_template}" "${cfg}" ok_msg "Minimal example printer.cfg created!"
else
error_msg "Couldn't create minimal example printer.cfg!"
fi fi
} }
@@ -380,110 +402,53 @@ function write_example_printer_cfg() {
#================ REMOVE KLIPPER ================# #================ REMOVE KLIPPER ================#
#================================================# #================================================#
function remove_klipper_sysvinit() { function remove_klipper_service() {
[[ ! -e "${INITD}/klipper" ]] && return [[ -z $(klipper_systemd) ]] && return
status_msg "Removing Klipper SysVinit service ..." status_msg "Removing Klipper services ..."
sudo systemctl stop klipper
sudo update-rc.d -f klipper remove
sudo rm -f "${INITD}/klipper" "${ETCDEF}/klipper"
ok_msg "Klipper SysVinit service removed!"
}
function remove_klipper_systemd() { for service in $(klipper_systemd | cut -d"/" -f5); do
[[ -z $(find_klipper_systemd) ]] && return
status_msg "Removing Klipper Systemd Services ..."
for service in $(find_klipper_systemd | cut -d"/" -f5); do
status_msg "Removing ${service} ..." status_msg "Removing ${service} ..."
sudo systemctl stop "${service}" sudo systemctl stop "${service}"
sudo systemctl disable "${service}" sudo systemctl disable "${service}"
sudo rm -f "${SYSTEMD}/${service}" sudo rm -f "${SYSTEMD}/${service}"
ok_msg "Done!" sudo systemctl daemon-reload
sudo systemctl reset-failed
done done
### reloading units ok_msg "All Klipper services removed!"
sudo systemctl daemon-reload
sudo systemctl reset-failed
ok_msg "Klipper Service removed!"
} }
function remove_klipper_env_file() { function find_instance_files() {
local files regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/systemd\/klipper\.env" local target_folder=${1}
files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) local target_name=${2}
if [[ -n ${files} ]]; then
for file in ${files}; do
status_msg "Removing ${file} ..."
rm -f "${file}"
ok_msg "${file} removed!"
done
fi
}
function remove_klipper_logs() {
local files regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/logs\/klippy\.log.*"
files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort)
if [[ -n ${files} ]]; then
for file in ${files}; do
status_msg "Removing ${file} ..."
rm -f "${file}"
ok_msg "${file} removed!"
done
fi
}
function remove_legacy_klipper_logs() {
local files regex="klippy(-[0-9a-zA-Z]+)?\.log(.*)?"
files=$(find "${HOME}/klipper_logs" -maxdepth 1 -regextype posix-extended -regex "${HOME}/klipper_logs/${regex}" 2> /dev/null | sort)
if [[ -n ${files} ]]; then
for file in ${files}; do
status_msg "Removing ${file} ..."
rm -f "${file}"
ok_msg "${file} removed!"
done
fi
}
function remove_klipper_uds() {
local files regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/comms\/klippy\.sock"
files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort)
if [[ -n ${files} ]]; then
for file in ${files}; do
status_msg "Removing ${file} ..."
rm -f "${file}"
ok_msg "${file} removed!"
done
fi
}
function remove_klipper_printer() {
local files regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/comms\/klippy\.serial"
files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort)
if [[ -n ${files} ]]; then
for file in ${files}; do
status_msg "Removing ${file} ..."
rm -f "${file}"
ok_msg "${file} removed!"
done
fi
}
function remove_legacy_klipper_printer() {
local files local files
files=$(find /tmp -maxdepth 1 -regextype posix-extended -regex "/tmp/printer(-[0-9a-zA-Z]+)?" | sort)
if [[ -n ${files} ]]; then readarray -t files < <(find "${HOME}" -regex "${HOME}/[A-Za-z0-9_]+_data/${target_folder}/${target_name}" | sort)
for file in ${files}; do
status_msg "Removing ${file} ..." echo -e "${files[@]}"
rm -f "${file}" }
ok_msg "${file} removed!"
done function find_legacy_klipper_logs() {
fi local files
local regex="klippy(-[0-9a-zA-Z]+)?\.log(.*)?"
readarray -t files < <(find "${HOME}/klipper_logs" -maxdepth 1 -regextype posix-extended -regex "${HOME}/klipper_logs/${regex}" 2> /dev/null | sort)
echo -e "${files[@]}"
}
function find_legacy_klipper_uds() {
local files
readarray -t files < <(find /tmp -maxdepth 1 -regextype posix-extended -regex "/tmp/klippy_uds(-[0-9a-zA-Z]+)?" | sort)
echo -e "${files[@]}"
}
function find_legacy_klipper_printer() {
local files
readarray -t files < <(find /tmp -maxdepth 1 -regextype posix-extended -regex "/tmp/printer(-[0-9a-zA-Z]+)?" | sort)
echo -e "${files[@]}"
} }
function remove_klipper_dir() { function remove_klipper_dir() {
@@ -502,20 +467,38 @@ function remove_klipper_env() {
ok_msg "Directory removed!" ok_msg "Directory removed!"
} }
###
# takes in a string of space separated absolute
# filepaths and removes those files one after another
#
function remove_files() {
local files
read -r -a files <<< "${@}"
if (( ${#files[@]} > 0 )); then
for file in "${files[@]}"; do
status_msg "Removing ${file} ..."
rm -f "${file}"
ok_msg "${file} removed!"
done
fi
}
function remove_klipper() { function remove_klipper() {
remove_klipper_sysvinit remove_klipper_service
remove_klipper_systemd remove_files "$(find_instance_files "systemd" "klipper.env")"
remove_klipper_env_file remove_files "$(find_instance_files "logs" "klippy.log.*")"
remove_klipper_logs remove_files "$(find_instance_files "comms" "klippy.sock")"
remove_legacy_klipper_logs remove_files "$(find_instance_files "comms" "klippy.serial")"
remove_klipper_uds
remove_klipper_printer remove_files "$(find_legacy_klipper_logs)"
remove_legacy_klipper_printer remove_files "$(find_legacy_klipper_uds)"
remove_files "$(find_legacy_klipper_printer)"
remove_klipper_dir remove_klipper_dir
remove_klipper_env remove_klipper_env
local confirm="Klipper was successfully removed!" print_confirm "Klipper was successfully removed!" && return
print_confirm "${confirm}" && return
} }
#================================================# #================================================#
@@ -533,7 +516,7 @@ function update_klipper() {
local custom_repo="${custom_klipper_repo}" local custom_repo="${custom_klipper_repo}"
local custom_branch="${custom_klipper_repo_branch}" local custom_branch="${custom_klipper_repo_branch}"
py_ver="python$(get_klipper_python_ver)" py_ver=$(get_klipper_python_ver)
do_action_service "stop" "klipper" do_action_service "stop" "klipper"
@@ -560,13 +543,7 @@ function update_klipper() {
function get_klipper_status() { function get_klipper_status() {
local sf_count status py_ver local sf_count status py_ver
sf_count="$(find_klipper_systemd | wc -w)" sf_count="$(klipper_systemd | wc -w)"
### detect an existing "legacy" klipper init.d installation
if [[ $(find_klipper_systemd | wc -w) -eq 0 ]] \
&& [[ $(find_klipper_initd | wc -w) -ge 1 ]]; then
sf_count=1
fi
py_ver=$(get_klipper_python_ver) py_ver=$(get_klipper_python_ver)
@@ -608,8 +585,14 @@ function get_remote_klipper_commit() {
[[ ! -d ${KLIPPER_DIR} || ! -d "${KLIPPER_DIR}/.git" ]] && return [[ ! -d ${KLIPPER_DIR} || ! -d "${KLIPPER_DIR}/.git" ]] && return
local commit local commit
local branch
read_kiauh_ini "${FUNCNAME[0]}"
branch="${custom_klipper_repo_branch}"
[[ -z ${branch} ]] && branch="master"
cd "${KLIPPER_DIR}" && git fetch origin -q cd "${KLIPPER_DIR}" && git fetch origin -q
commit=$(git describe origin/master --always --tags | cut -d "-" -f 1,2) commit=$(git describe "origin/${branch}" --always --tags | cut -d "-" -f 1,2)
echo "${commit}" echo "${commit}"
} }
@@ -647,3 +630,34 @@ function get_klipper_python_ver() {
version=$("${KLIPPY_ENV}"/bin/python --version 2>&1 | cut -d" " -f2 | cut -d"." -f1) version=$("${KLIPPY_ENV}"/bin/python --version 2>&1 | cut -d" " -f2 | cut -d"." -f1)
echo "${version}" echo "${version}"
} }
function mask_disrupting_services() {
local brltty="false"
local brltty_udev="false"
local modem_manager="false"
[[ $(dpkg -s brltty 2>/dev/null | grep "Status") = *\ installed ]] && brltty="true"
[[ $(dpkg -s brltty-udev 2>/dev/null | grep "Status") = *\ installed ]] && brltty_udev="true"
[[ $(dpkg -s ModemManager 2>/dev/null | grep "Status") = *\ installed ]] && modem_manager="true"
status_msg "Installed brltty package detected, masking brltty service ..."
if [[ ${brltty} == "true" ]]; then
sudo systemctl stop brltty
sudo systemctl mask brltty
fi
ok_msg "brltty service masked!"
status_msg "Installed brltty-udev package detected, masking brltty-udev service ..."
if [[ ${brltty_udev} == "true" ]]; then
sudo systemctl stop brltty-udev
sudo systemctl mask brltty-udev
fi
ok_msg "brltty-udev service masked!"
status_msg "Installed ModemManager package detected, masking ModemManager service ..."
if [[ ${modem_manager} == "true" ]]; then
sudo systemctl stop ModemManager
sudo systemctl mask ModemManager
fi
ok_msg "ModemManager service masked!"
}

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
@@ -30,7 +30,7 @@ function install_klipperscreen() {
fi fi
### first, we create a backup of the full klipper_config dir - safety first! ### first, we create a backup of the full klipper_config dir - safety first!
backup_klipper_config_dir backup_config_dir
### install KlipperScreen ### install KlipperScreen
klipperscreen_setup klipperscreen_setup
@@ -119,6 +119,8 @@ function update_klipperscreen() {
old_md5=$(md5sum "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt" | cut -d " " -f1) old_md5=$(md5sum "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt" | cut -d " " -f1)
do_action_service "stop" "KlipperScreen" do_action_service "stop" "KlipperScreen"
backup_before_update "klipperscreen"
cd "${KLIPPERSCREEN_DIR}" cd "${KLIPPERSCREEN_DIR}"
git pull origin master -q && ok_msg "Fetch successfull!" git pull origin master -q && ok_msg "Fetch successfull!"
git checkout -f master && ok_msg "Checkout successfull" git checkout -f master && ok_msg "Checkout successfull"
@@ -207,7 +209,7 @@ function patch_klipperscreen_update_manager() {
moonraker_configs=$(find "${KLIPPER_CONFIG}" -type f -name "moonraker.conf" | sort) moonraker_configs=$(find "${KLIPPER_CONFIG}" -type f -name "moonraker.conf" | sort)
for conf in ${moonraker_configs}; do for conf in ${moonraker_configs}; do
if ! grep -Eq "^\[update_manager KlipperScreen\]$" "${conf}"; then if ! grep -Eq "^\[update_manager KlipperScreen\]\s*$" "${conf}"; then
### add new line to conf if it doesn't end with one ### add new line to conf if it doesn't end with one
[[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}"

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
@@ -16,10 +16,24 @@ set -e
#===================================================# #===================================================#
function install_mainsail() { function install_mainsail() {
### exit early if moonraker not found
if [[ -z $(moonraker_systemd) ]]; then if [[ -z $(moonraker_systemd) ]]; then
local error="Moonraker not installed! Please install Moonraker first!" local error="Moonraker not installed! It's recommended to install Moonraker first!"
print_error "${error}" && return print_error "${error}"
while true; do
local yn
read -p "${cyan}###### Proceed to install Mainsail without installing Moonraker? (y/N):${white} " yn
case "${yn}" in
Y|y|Yes|yes)
select_msg "Yes"
break;;
N|n|No|no|"")
select_msg "No"
abort_msg "Exiting Mainsail setup ...\n"
return;;
*)
error_msg "Invalid Input!";;
esac
done
fi fi
### checking dependencies ### checking dependencies
@@ -30,7 +44,7 @@ function install_mainsail() {
status_msg "Initializing Mainsail installation ..." status_msg "Initializing Mainsail installation ..."
### first, we create a backup of the full klipper_config dir - safety first! ### first, we create a backup of the full klipper_config dir - safety first!
backup_klipper_config_dir backup_config_dir
### check for other enabled web interfaces ### check for other enabled web interfaces
unset SET_LISTEN_PORT unset SET_LISTEN_PORT
@@ -39,30 +53,6 @@ function install_mainsail() {
### check if another site already listens to port 80 ### check if another site already listens to port 80
mainsail_port_check mainsail_port_check
# ### ask user to install mjpg-streamer
# local install_mjpg_streamer
# if [[ ! -f "${SYSTEMD}/webcamd.service" ]]; then
# while true; do
# echo
# top_border
# echo -e "| Install MJPG-Streamer for webcam support? |"
# bottom_border
# read -p "${cyan}###### Please select (y/N):${white} " yn
# case "${yn}" in
# Y|y|Yes|yes)
# select_msg "Yes"
# install_mjpg_streamer="true"
# break;;
# N|n|No|no|"")
# select_msg "No"
# install_mjpg_streamer="false"
# break;;
# *)
# error_msg "Invalid command!";;
# esac
# done
# fi
### download mainsail ### download mainsail
download_mainsail download_mainsail
@@ -82,9 +72,6 @@ function install_mainsail() {
### add mainsail to the update manager in moonraker.conf ### add mainsail to the update manager in moonraker.conf
patch_mainsail_update_manager patch_mainsail_update_manager
### install mjpg-streamer
# [[ ${install_mjpg_streamer} == "true" ]] && install_mjpg-streamer
fetch_webui_ports #WIP fetch_webui_ports #WIP
### confirm message ### confirm message
@@ -92,22 +79,21 @@ function install_mainsail() {
} }
function install_mainsail_macros() { function install_mainsail_macros() {
local yn
while true; do while true; do
echo echo
top_border top_border
echo -e "| It is recommended to have some important macros in |" echo -e "| It is recommended to use special macros in order to |"
echo -e "| your printer configuration to have Mainsail fully |" echo -e "| have Mainsail fully functional and working. |"
echo -e "| functional and working. |"
blank_line blank_line
echo -e "| The recommended macros for Mainsail can be seen here: |" echo -e "| The recommended macros for Mainsail can be seen here: |"
echo -e "| https://docs.mainsail.xyz/configuration#macros |" echo -e "| https://github.com/mainsail-crew/mainsail-config |"
blank_line blank_line
echo -e "| If you already have these macros in your config file, |" echo -e "| If you already use these macros skip this step. |"
echo -e "| skip this step and answer with 'no'. |"
echo -e "| Otherwise you should consider to answer with 'yes' to |" echo -e "| Otherwise you should consider to answer with 'yes' to |"
echo -e "| add the recommended example macros to your config. |" echo -e "| download the recommended macros. |"
bottom_border bottom_border
read -p "${cyan}###### Add the recommended macros? (Y/n):${white} " yn read -p "${cyan}###### Download the recommended macros? (Y/n):${white} " yn
case "${yn}" in case "${yn}" in
Y|y|Yes|yes|"") Y|y|Yes|yes|"")
select_msg "Yes" select_msg "Yes"
@@ -124,39 +110,68 @@ function install_mainsail_macros() {
} }
function download_mainsail_macros() { function download_mainsail_macros() {
local ms_cfg path configs regex local ms_cfg_repo path configs regex line gcode_dir
ms_cfg="https://raw.githubusercontent.com/mainsail-crew/MainsailOS/master/src/modules/mainsail/filesystem/home/pi/klipper_config/mainsail.cfg" ms_cfg_repo="https://github.com/mainsail-crew/mainsail-config.git"
regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/config\/printer\.cfg" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/printer\.cfg"
configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort)
if [[ -n ${configs} ]]; then if [[ -z ${configs} ]]; then
for config in ${configs}; do print_error "No printer.cfg found! Installation of Macros will be skipped ..."
path=$(echo "${config}" | rev | cut -d"/" -f2- | rev)
if [[ ! -f "${path}/mainsail.cfg" ]]; then
status_msg "Downloading mainsail.cfg to ${path} ..."
log_info "downloading mainsail.cfg to: ${path}"
wget "${ms_cfg}" -O "${path}/mainsail.cfg"
### replace user 'pi' with current username to prevent issues in cases where the user is not called 'pi'
log_info "modify mainsail.cfg"
sed -i "/^path: \/home\/pi\/gcode_files/ s/\/home\/pi/\/home\/${USER}/" "${path}/mainsail.cfg"
### write include to the very first line of the printer.cfg
if ! grep -Eq "^[include mainsail.cfg]$" "${path}/printer.cfg"; then
log_info "modify printer.cfg"
sed -i "1 i [include mainsail.cfg]" "${path}/printer.cfg"
fi
ok_msg "Done!"
fi
done
else
log_error "execution stopped! reason: no printer.cfg found" log_error "execution stopped! reason: no printer.cfg found"
return return
fi fi
status_msg "Cloning mainsail-config ..."
[[ -d "${HOME}/mainsail-config" ]] && rm -rf "${HOME}/mainsail-config"
if git clone "${ms_cfg_repo}" "${HOME}/mainsail-config"; then
for config in ${configs}; do
path=$(echo "${config}" | rev | cut -d"/" -f2- | rev)
if [[ -e "${path}/mainsail.cfg" && ! -h "${path}/mainsail.cfg" ]]; then
warn_msg "Attention! Existing mainsail.cfg detected!"
warn_msg "The file will be renamed to 'mainsail.bak.cfg' to be able to continue with the installation."
if ! mv "${path}/mainsail.cfg" "${path}/mainsail.bak.cfg"; then
error_msg "Renaming mainsail.cfg failed! Aborting installation ..."
return
fi
fi
if [[ -h "${path}/mainsail.cfg" ]]; then
warn_msg "Recreating symlink in ${path} ..."
rm -rf "${path}/mainsail.cfg"
fi
if ! ln -sf "${HOME}/mainsail-config/client.cfg" "${path}/mainsail.cfg"; then
error_msg "Creating symlink failed! Aborting installation ..."
return
fi
if ! grep -Eq "^\[include mainsail.cfg\]$" "${path}/printer.cfg"; then
log_info "${path}/printer.cfg"
sed -i "1 i [include mainsail.cfg]" "${path}/printer.cfg"
fi
line=$(($(grep -n "\[include mainsail.cfg\]" "${path}/printer.cfg" | tail -1 | cut -d: -f1) + 1))
gcode_dir=${path/config/gcodes}
if ! grep -Eq "^\[virtual_sdcard\]$" "${path}/printer.cfg"; then
log_info "${path}/printer.cfg"
sed -i "${line} i \[virtual_sdcard]\npath: ${gcode_dir}\non_error_gcode: CANCEL_PRINT\n" "${path}/printer.cfg"
fi
done
else
print_error "Cloning failed! Aborting installation ..."
log_error "execution stopped! reason: cloning failed"
return
fi
patch_mainsail_config_update_manager
ok_msg "Done!"
} }
function download_mainsail() { function download_mainsail() {
local services
local url local url
url=$(get_mainsail_download_url) url=$(get_mainsail_download_url)
@@ -179,8 +194,9 @@ function download_mainsail() {
exit 1 exit 1
fi fi
### check for moonraker multi-instance and if multi-instance was found, enable mainsails remoteMode ### check for moonraker multi-instance and if no-instance or multi-instance was found, enable mainsails remoteMode
if [[ $(moonraker_systemd | wc -w) -gt 1 ]]; then services=$(moonraker_systemd)
if [[ ( -z "${services}" ) || ( $(echo "${services}" | wc -w) -gt 1 ) ]]; then
enable_mainsail_remotemode enable_mainsail_remotemode
fi fi
} }
@@ -196,7 +212,7 @@ function remove_mainsail_dir() {
rm -rf "${MAINSAIL_DIR}" && ok_msg "Directory removed!" rm -rf "${MAINSAIL_DIR}" && ok_msg "Directory removed!"
} }
function remove_mainsail_config() { function remove_mainsail_nginx_config() {
if [[ -e "/etc/nginx/sites-available/mainsail" ]]; then if [[ -e "/etc/nginx/sites-available/mainsail" ]]; then
status_msg "Removing Mainsail configuration for Nginx ..." status_msg "Removing Mainsail configuration for Nginx ..."
sudo rm "/etc/nginx/sites-available/mainsail" && ok_msg "File removed!" sudo rm "/etc/nginx/sites-available/mainsail" && ok_msg "File removed!"
@@ -223,7 +239,7 @@ function remove_mainsail_logs() {
function remove_mainsail_log_symlinks() { function remove_mainsail_log_symlinks() {
local files regex local files regex
regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/logs\/mainsail-.*" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/mainsail-.*"
files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" 2> /dev/null | sort) files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" 2> /dev/null | sort)
if [[ -n ${files} ]]; then if [[ -n ${files} ]]; then
@@ -248,9 +264,18 @@ function remove_legacy_mainsail_log_symlinks() {
fi fi
} }
function remove_mainsail_config() {
if [[ -d "${HOME}/mainsail-config" ]]; then
status_msg "Removing ${HOME}/mainsail-config ..."
rm -rf "${HOME}/mainsail-config"
ok_msg "${HOME}/mainsail-config removed!"
print_confirm "Mainsail-Config successfully removed!"
fi
}
function remove_mainsail() { function remove_mainsail() {
remove_mainsail_dir remove_mainsail_dir
remove_mainsail_config remove_mainsail_nginx_config
remove_mainsail_logs remove_mainsail_logs
remove_mainsail_log_symlinks remove_mainsail_log_symlinks
remove_legacy_mainsail_log_symlinks remove_legacy_mainsail_log_symlinks
@@ -299,19 +324,25 @@ function get_mainsail_status() {
} }
function get_local_mainsail_version() { function get_local_mainsail_version() {
[[ ! -f "${MAINSAIL_DIR}/.version" ]] && return local versionfile="${MAINSAIL_DIR}/.version"
local relinfofile="${MAINSAIL_DIR}/release_info.json"
local version local version
version=$(head -n 1 "${MAINSAIL_DIR}/.version")
if [[ -f ${relinfofile} ]]; then
version=$(grep -o '"version":"[^"]*' "${relinfofile}" | grep -o '[^"]*$')
elif [[ -f ${versionfile} ]]; then
version=$(head -n 1 "${versionfile}")
fi
echo "${version}" echo "${version}"
} }
function get_remote_mainsail_version() { function get_remote_mainsail_version() {
[[ ! $(dpkg-query -f'${Status}' --show curl 2>/dev/null) = *\ installed ]] && return [[ ! $(dpkg-query -f'${Status}' --show curl 2>/dev/null) = *\ installed ]] && return
local version local tags
version=$(get_mainsail_download_url | rev | cut -d"/" -f2 | rev) tags=$(curl -s "https://api.github.com/repos/mainsail-crew/mainsail/tags" | grep "name" | cut -d'"' -f4)
echo "${version}" echo "${tags}" | head -1
} }
function compare_mainsail_versions() { function compare_mainsail_versions() {
@@ -349,7 +380,7 @@ function print_theme_list() {
function ms_theme_installer_menu() { function ms_theme_installer_menu() {
local theme_list theme_author theme_repo theme_name theme_note theme_url local theme_list theme_author theme_repo theme_name theme_note theme_url
local theme_csv_url="https://raw.githubusercontent.com/mainsail-crew/docs/master/_data/themes.csv" local theme_csv_url="https://raw.githubusercontent.com/mainsail-crew/gb-docs/main/_data/themes.csv"
theme_list=$(curl -s -L "${theme_csv_url}") theme_list=$(curl -s -L "${theme_csv_url}")
top_border top_border
@@ -460,7 +491,7 @@ function ms_theme_install() {
function ms_theme_delete() { function ms_theme_delete() {
local regex theme_folders target_folders=() local regex theme_folders target_folders=()
regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/config\/\.theme" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/\.theme"
theme_folders=$(find "${HOME}" -maxdepth 3 -type d -regextype posix-extended -regex "${regex}" | sort) theme_folders=$(find "${HOME}" -maxdepth 3 -type d -regextype posix-extended -regex "${regex}" | sort)
# theme_folders=$(find "${KLIPPER_CONFIG}" -mindepth 1 -type d -name ".theme" | sort) # theme_folders=$(find "${KLIPPER_CONFIG}" -mindepth 1 -type d -name ".theme" | sort)
@@ -502,25 +533,28 @@ function ms_theme_delete() {
#================================================# #================================================#
function get_mainsail_download_url() { function get_mainsail_download_url() {
local tags latest_tag latest_url stable_tag stable_url url local releases_by_tag tags tag unstable_url url
tags=$(curl -s "${MAINSAIL_TAGS}" | grep "name" | cut -d'"' -f4)
### latest download url including pre-releases (alpha, beta, rc) ### latest stable download url
latest_tag=$(echo "${tags}" | head -1) url="https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip"
latest_url="https://github.com/mainsail-crew/mainsail/releases/download/${latest_tag}/mainsail.zip"
### get stable mainsail download url
stable_tag=$(echo "${tags}" | grep -E "^v([0-9]+\.?){3}$" | head -1)
stable_url="https://github.com/mainsail-crew/mainsail/releases/download/${stable_tag}/mainsail.zip"
read_kiauh_ini "${FUNCNAME[0]}" read_kiauh_ini "${FUNCNAME[0]}"
if [[ ${mainsail_install_unstable} == "true" ]]; then if [[ ${mainsail_install_unstable} == "true" ]]; then
url="${latest_url}" releases_by_tag="https://api.github.com/repos/mainsail-crew/mainsail/tags"
echo "${url}" tags=$(curl -s "${releases_by_tag}" | grep "name" | cut -d'"' -f4)
else tag=$(echo "${tags}" | head -1)
url="${stable_url}"
echo "${url}" ### latest unstable download url including pre-releases (alpha, beta, rc)
unstable_url="https://github.com/mainsail-crew/mainsail/releases/download/${tag}/mainsail.zip"
if [[ ${unstable_url} == *"download//"* ]]; then
warn_msg "Download URL broken! Falling back to URL of latest stable release!"
else
url=${unstable_url}
fi
fi fi
echo "${url}"
} }
function mainsail_port_check() { function mainsail_port_check() {
@@ -592,12 +626,12 @@ function enable_mainsail_remotemode() {
function patch_mainsail_update_manager() { function patch_mainsail_update_manager() {
local patched moonraker_configs regex local patched moonraker_configs regex
regex="\/home\/${USER}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf"
moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort) moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort)
patched="false" patched="false"
for conf in ${moonraker_configs}; do for conf in ${moonraker_configs}; do
if ! grep -Eq "^\[update_manager mainsail\]$" "${conf}"; then if ! grep -Eq "^\[update_manager mainsail\]\s*$" "${conf}"; then
### add new line to conf if it doesn't end with one ### add new line to conf if it doesn't end with one
[[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}"
@@ -621,3 +655,36 @@ MOONRAKER_CONF
do_action_service "restart" "moonraker" do_action_service "restart" "moonraker"
fi fi
} }
function patch_mainsail_config_update_manager() {
local patched moonraker_configs regex
regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf"
moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort)
patched="false"
for conf in ${moonraker_configs}; do
if ! grep -Eq "^\[update_manager mainsail-config\]\s*$" "${conf}"; then
### add new line to conf if it doesn't end with one
[[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}"
### add Mainsails update manager section to moonraker.conf
status_msg "Adding Mainsail-Config to update manager in file:\n ${conf}"
/bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF
[update_manager mainsail-config]
type: git_repo
primary_branch: master
path: ~/mainsail-config
origin: https://github.com/mainsail-crew/mainsail-config.git
managed_services: klipper
MOONRAKER_CONF
fi
patched="true"
done
if [[ ${patched} == "true" ]]; then
do_action_service "restart" "moonraker"
fi
}

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#

247
scripts/mobileraker.sh Normal file
View File

@@ -0,0 +1,247 @@
#!/usr/bin/env bash
#=======================================================================#
# 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 #
#=======================================================================#
#
# This file is written and maintained by Patrick Schmidt author of Mobileraker
# It is based of the kliperscreen.sh install script!
set -e
#===================================================#
#========== INSTALL MOBILERAKER COMPANION ==========#
#===================================================#
function mobileraker_systemd() {
local services
services=$(find "${SYSTEMD}" -maxdepth 1 -regextype posix-extended -regex "${SYSTEMD}/mobileraker.service")
echo "${services}"
}
function install_mobileraker() {
### return early if python version check fails
if [[ $(python3_check) == "false" ]]; then
local error="Versioncheck failed! Python 3.7 or newer required!\n"
error="${error} Please upgrade Python."
print_error "${error}" && return
fi
### first, we create a backup of the full klipper_config dir - safety first!
backup_config_dir
### install Mobileraker's Companion
mobileraker_setup
### add Mobileraker's Companion to the update manager in moonraker.conf
patch_mobileraker_update_manager
do_action_service "restart" "mobileraker"
}
function mobileraker_setup() {
local dep=(wget curl unzip dfu-util)
dependency_check "${dep[@]}"
status_msg "Cloning Mobileraker's companion from ${MOBILERAKER_REPO} ..."
# force remove existing Mobileraker's companion dir
[[ -d ${MOBILERAKER_DIR} ]] && rm -rf "${MOBILERAKER_DIR}"
# clone into fresh Mobileraker's companion dir
cd "${HOME}" || exit 1
if ! git clone "${MOBILERAKER_REPO}" "${MOBILERAKER_DIR}"; then
print_error "Cloning mobileraker's companion from\n ${MOBILERAKER_REPO}\n failed!"
exit 1
fi
status_msg "Installing Mobileraker's companion ..."
if "${MOBILERAKER_DIR}"/scripts/install-mobileraker-companion.sh; then
ok_msg "Mobileraker's companion successfully installed!"
else
print_error "Mobileraker's companion installation failed!"
exit 1
fi
}
#===================================================#
#=========== REMOVE MOBILERAKER COMPANION ==========#
#===================================================#
function remove_mobileraker() {
### remove Mobileraker's companion dir
if [[ -d ${MOBILERAKER_DIR} ]]; then
status_msg "Removing Mobileraker's companion directory ..."
rm -rf "${MOBILERAKER_DIR}" && ok_msg "Directory removed!"
fi
### remove Mobileraker's companion VENV dir
if [[ -d ${MOBILERAKER_ENV} ]]; then
status_msg "Removing Mobileraker's companion VENV directory ..."
rm -rf "${MOBILERAKER_ENV}" && ok_msg "Directory removed!"
fi
### remove Mobileraker's companion service
if [[ -e "${SYSTEMD}/mobileraker.service" ]]; then
status_msg "Removing mobileraker service ..."
do_action_service "stop" "mobileraker"
do_action_service "disable" "mobileraker"
sudo rm -f "${SYSTEMD}/mobileraker.service"
###reloading units
sudo systemctl daemon-reload
sudo systemctl reset-failed
ok_msg "Mobileraker's companion Service removed!"
fi
remove_mobileraker_logs
print_confirm "Mobileraker's companion successfully removed!"
}
function remove_mobileraker_logs() {
local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/mobileraker\.log.*"
files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort)
if [[ -n ${files} ]]; then
for file in ${files}; do
status_msg "Removing ${file} ..."
rm -f "${file}"
ok_msg "${file} removed!"
done
fi
}
#===================================================#
#=========== UPDATE MOBILERAKER COMPANION ==========#
#===================================================#
function update_mobileraker() {
local old_md5
old_md5=$(md5sum "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt" | cut -d " " -f1)
do_action_service "stop" "mobileraker"
cd "${MOBILERAKER_DIR}"
git pull origin main -q && ok_msg "Fetch successfull!"
git checkout -f main && ok_msg "Checkout successfull"
if [[ $(md5sum "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt" | cut -d " " -f1) != "${old_md5}" ]]; then
status_msg "New dependecies detected..."
"${MOBILERAKER_ENV}"/bin/pip install -r "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt"
ok_msg "Dependencies have been installed!"
fi
ok_msg "Update complete!"
do_action_service "start" "mobileraker"
}
#===================================================#
#=========== MOBILERAKER COMPANION STATUS ==========#
#===================================================#
function get_mobileraker_status() {
local sf_count status
sf_count="$(mobileraker_systemd | wc -w)"
### remove the "SERVICE" entry from the data array if a moonraker service is installed
local data_arr=(SERVICE "${MOBILERAKER_DIR}" "${MOBILERAKER_ENV}")
(( sf_count > 0 )) && unset "data_arr[0]"
### count+1 for each found data-item from array
local filecount=0
for data in "${data_arr[@]}"; do
[[ -e ${data} ]] && filecount=$(( filecount + 1 ))
done
if (( filecount == ${#data_arr[*]} )); then
status="Installed!"
elif (( filecount == 0 )); then
status="Not installed!"
else
status="Incomplete!"
fi
echo "${status}"
}
function get_local_mobileraker_commit() {
[[ ! -d ${MOBILERAKER_DIR} || ! -d "${MOBILERAKER_DIR}/.git" ]] && return
local commit
cd "${MOBILERAKER_DIR}"
commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)"
echo "${commit}"
}
function get_remote_mobileraker_commit() {
[[ ! -d ${MOBILERAKER_DIR} || ! -d "${MOBILERAKER_DIR}/.git" ]] && return
local commit
cd "${MOBILERAKER_DIR}" && git fetch origin -q
commit=$(git describe origin/main --always --tags | cut -d "-" -f 1,2)
echo "${commit}"
}
function compare_mobileraker_versions() {
local versions local_ver remote_ver
local_ver="$(get_local_mobileraker_commit)"
remote_ver="$(get_remote_mobileraker_commit)"
if [[ ${local_ver} != "${remote_ver}" ]]; then
versions="${yellow}$(printf " %-14s" "${local_ver}")${white}"
versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}"
# add moonraker to application_updates_available in kiauh.ini
add_to_application_updates "mobileraker"
else
versions="${green}$(printf " %-14s" "${local_ver}")${white}"
versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}"
fi
echo "${versions}"
}
#================================================#
#=================== HELPERS ====================#
#================================================#
function patch_mobileraker_update_manager() {
local patched moonraker_configs regex
regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf"
moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort)
patched="false"
for conf in ${moonraker_configs}; do
if ! grep -Eq "^\[update_manager mobileraker\]\s*$" "${conf}"; then
### add new line to conf if it doesn't end with one
[[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}"
### add Mobileraker's Companion update manager section to moonraker.conf
status_msg "Adding Mobileraker's Companion to update manager in file:\n ${conf}"
/bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF
[update_manager mobileraker]
type: git_repo
path: ${HOME}/mobileraker_companion
origin: https://github.com/Clon1998/mobileraker_companion.git
primary_branch:main
managed_services: mobileraker
env: ${HOME}/mobileraker-env/bin/python
requirements: scripts/mobileraker-requirements.txt
install_script: scripts/install-mobileraker-companion.sh
MOONRAKER_CONF
fi
patched="true"
done
if [[ ${patched} == "true" ]]; then
do_action_service "restart" "moonraker"
fi
}

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#=======================================================================# #=======================================================================#
# Copyright (C) 2020 - 2022 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/th33xitus/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
#=======================================================================# #=======================================================================#
@@ -33,7 +33,7 @@ function telegram_bot_setup_dialog() {
status_msg "Initializing Telegram Bot installation ..." status_msg "Initializing Telegram Bot installation ..."
### first, we create a backup of the full klipper_config dir - safety first! ### first, we create a backup of the full klipper_config dir - safety first!
backup_klipper_config_dir backup_config_dir
local moonraker_count user_input=() moonraker_names=() local moonraker_count user_input=() moonraker_names=()
moonraker_count=$(echo "${moonraker_services}" | wc -w ) moonraker_count=$(echo "${moonraker_services}" | wc -w )
@@ -110,7 +110,7 @@ function telegram_bot_setup_dialog() {
} }
function install_telegram_bot_dependencies() { function install_telegram_bot_dependencies() {
local packages local packages log_name="Telegram Bot"
local install_script="${TELEGRAM_BOT_DIR}/scripts/install.sh" local install_script="${TELEGRAM_BOT_DIR}/scripts/install.sh"
### read PKGLIST from official install-script ### read PKGLIST from official install-script
@@ -121,21 +121,11 @@ function install_telegram_bot_dependencies() {
echo "${cyan}${packages}${white}" | tr '[:space:]' '\n' echo "${cyan}${packages}${white}" | tr '[:space:]' '\n'
read -r -a packages <<< "${packages}" read -r -a packages <<< "${packages}"
### Update system package info ### Update system package lists if stale
status_msg "Updating package lists..." update_system_package_lists
if ! sudo apt-get update --allow-releaseinfo-change; then
log_error "failure while updating package lists"
error_msg "Updating package lists failed!"
exit 1
fi
### Install required packages ### Install required packages
status_msg "Installing required packages..." install_system_packages "${log_name}" "packages[@]"
if ! sudo apt-get install --yes "${packages[@]}"; then
log_error "failure while installing required moonraker-telegram-bot packages"
error_msg "Installing required packages failed!"
exit 1
fi
} }
function create_telegram_bot_virtualenv() { function create_telegram_bot_virtualenv() {
@@ -203,29 +193,40 @@ function create_telegram_conf() {
local input=("${@}") local input=("${@}")
local telegram_bot_count=${input[0]} && unset "input[0]" local telegram_bot_count=${input[0]} && unset "input[0]"
local names=("${input[@]}") && unset "input[@]" local names=("${input[@]}") && unset "input[@]"
local log="${KLIPPER_LOGS}" local printer_data log_dir cfg cfg_dir
local cfg cfg_dir
if (( telegram_bot_count == 1 )); then if (( telegram_bot_count == 1 )); then
cfg_dir="${KLIPPER_CONFIG}" printer_data="${HOME}/printer_data"
log_dir="${printer_data}/logs"
cfg_dir="${printer_data}/config"
cfg="${cfg_dir}/telegram.conf" cfg="${cfg_dir}/telegram.conf"
### create required folder structure
create_required_folders "${printer_data}"
### write single instance config ### write single instance config
write_telegram_conf "${cfg_dir}" "${cfg}" "${log}" write_telegram_conf "${cfg_dir}" "${cfg}"
elif (( telegram_bot_count > 1 )); then elif (( telegram_bot_count > 1 )); then
local j=0 re="^[1-9][0-9]*$" local j=0 re="^[1-9][0-9]*$"
for (( i=1; i <= telegram_bot_count; i++ )); do for (( i=1; i <= telegram_bot_count; i++ )); do
### overwrite config folder if name is only a number
if [[ ${names[j]} =~ ${re} ]]; then printer_data="${HOME}/${names[${j}]}_data"
cfg_dir="${KLIPPER_CONFIG}/printer_${names[${j}]}" ### prefix instance name with "printer_" if it is only a number
else [[ ${names[j]} =~ ${re} ]] && printer_data="${HOME}/printer_${names[${j}]}_data"
cfg_dir="${KLIPPER_CONFIG}/${names[${j}]}"
fi
cfg_dir="${printer_data}/config"
cfg="${cfg_dir}/telegram.conf" cfg="${cfg_dir}/telegram.conf"
log_dir="${printer_data}/logs"
### create required folder structure
create_required_folders "${printer_data}"
### write multi instance config ### write multi instance config
write_telegram_conf "${cfg_dir}" "${cfg}" "${log}" write_telegram_conf "${cfg_dir}" "${cfg}"
j=$(( j + 1 )) j=$(( j + 1 ))
done && unset j done && unset j
@@ -235,17 +236,15 @@ function create_telegram_conf() {
} }
function write_telegram_conf() { function write_telegram_conf() {
local cfg_dir=${1} cfg=${2} log=${3} local cfg_dir=${1} cfg=${2}
local conf_template="${TELEGRAM_BOT_DIR}/scripts/base_install_template" local conf_template="${TELEGRAM_BOT_DIR}/scripts/base_install_template"
[[ ! -d ${cfg_dir} ]] && mkdir -p "${cfg_dir}"
if [[ ! -f ${cfg} ]]; then if [[ ! -f ${cfg} ]]; then
status_msg "Creating telegram.conf in ${cfg_dir} ..." status_msg "Creating telegram.conf in ${cfg_dir} ..."
cp "${conf_template}" "${cfg}" cp "${conf_template}" "${cfg}"
sed -i "s|some_log_path|${log}|g" "${cfg}"
ok_msg "telegram.conf created!" ok_msg "telegram.conf created!"
else else
status_msg "File '${cfg}' already exists!\nSkipping..." ok_msg "File '${cfg}' already exists! Skipping..."
fi fi
} }
@@ -253,16 +252,22 @@ function create_telegram_bot_service() {
local input=("${@}") local input=("${@}")
local instances=${input[0]} && unset "input[0]" local instances=${input[0]} && unset "input[0]"
local names=("${input[@]}") && unset "input[@]" local names=("${input[@]}") && unset "input[@]"
local cfg_dir cfg log service local printer_data cfg_dir cfg log service env_file
if (( instances == 1 )); then if (( instances == 1 )); then
cfg_dir="${KLIPPER_CONFIG}" printer_data="${HOME}/printer_data"
cfg_dir="${printer_data}/config"
cfg="${cfg_dir}/telegram.conf" cfg="${cfg_dir}/telegram.conf"
log="${KLIPPER_LOGS}/telegram.log" log="${printer_data}/logs/telegram.log"
service="${SYSTEMD}/moonraker-telegram-bot.service" service="${SYSTEMD}/moonraker-telegram-bot.service"
env_file="${printer_data}/systemd/moonraker-telegram-bot.env"
### create required folder structure
create_required_folders "${printer_data}"
### write single instance service ### write single instance service
write_telegram_bot_service "" "${cfg}" "${log}" "${service}" write_telegram_bot_service "" "${cfg}" "${log}" "${service}" "${env_file}"
ok_msg "Single Telegram Bot instance created!" ok_msg "Telegram Bot instance created!"
elif (( instances > 1 )); then elif (( instances > 1 )); then
local j=0 re="^[1-9][0-9]*$" local j=0 re="^[1-9][0-9]*$"
@@ -270,17 +275,27 @@ function create_telegram_bot_service() {
for (( i=1; i <= instances; i++ )); do for (( i=1; i <= instances; i++ )); do
### overwrite config folder if name is only a number ### overwrite config folder if name is only a number
if [[ ${names[j]} =~ ${re} ]]; then if [[ ${names[j]} =~ ${re} ]]; then
cfg_dir="${KLIPPER_CONFIG}/printer_${names[${j}]}" printer_data="${HOME}/printer_${names[${j}]}_data"
else else
cfg_dir="${KLIPPER_CONFIG}/${names[${j}]}" printer_data="${HOME}/${names[${j}]}_data"
fi fi
cfg_dir="${printer_data}/config"
cfg="${cfg_dir}/telegram.conf" cfg="${cfg_dir}/telegram.conf"
log="${KLIPPER_LOGS}/telegram-${names[${j}]}.log" log="${printer_data}/logs/telegram.log"
service="${SYSTEMD}/moonraker-telegram-bot-${names[${j}]}.service" service="${SYSTEMD}/moonraker-telegram-bot-${names[${j}]}.service"
env_file="${printer_data}/systemd/moonraker-telegram-bot.env"
### create required folder structure
create_required_folders "${printer_data}"
### write multi instance service ### write multi instance service
write_telegram_bot_service "${names[${j}]}" "${cfg}" "${log}" "${service}" if write_telegram_bot_service "${names[${j}]}" "${cfg}" "${log}" "${service}" "${env_file}"; then
ok_msg "Telegram Bot instance moonraker-telegram-bot-${names[${j}]} created!" ok_msg "Telegram Bot instance moonraker-telegram-bot-${names[${j}]} created!"
else
error_msg "An error occured during creation of instance moonraker-telegram-bot-${names[${j}]}!"
fi
j=$(( j + 1 )) j=$(( j + 1 ))
done && unset j done && unset j
@@ -290,17 +305,24 @@ function create_telegram_bot_service() {
} }
function write_telegram_bot_service() { function write_telegram_bot_service() {
local i=${1} cfg=${2} log=${3} service=${4} local i=${1} cfg=${2} log=${3} service=${4} env_file=${5}
local service_template="${KIAUH_SRCDIR}/resources/moonraker-telegram-bot.service" local service_template="${KIAUH_SRCDIR}/resources/moonraker-telegram-bot.service"
local env_template="${KIAUH_SRCDIR}/resources/moonraker-telegram-bot.env"
### replace all placeholders ### replace all placeholders
if [[ ! -f ${service} ]]; then if [[ ! -f ${service} ]]; then
status_msg "Creating Telegram Bot Service ${i} ..." status_msg "Creating service file for instance ${i} ..."
sudo cp "${service_template}" "${service}" sudo cp "${service_template}" "${service}"
[[ -z ${i} ]] && sudo sed -i "s|instance %INST% ||" "${service}" if [[ -z ${i} ]]; then
[[ -n ${i} ]] && sudo sed -i "s|%INST%|${i}|" "${service}" sudo sed -i "s| %INST%||" "${service}"
sudo sed -i "s|%USER%|${USER}|; s|%ENV%|${TELEGRAM_BOT_ENV}|; s|%DIR%|${TELEGRAM_BOT_DIR}|" "${service}" else
sudo sed -i "s|%CFG%|${cfg}|; s|%LOG%|${log}|" "${service}" sudo sed -i "s|%INST%|${i}|" "${service}"
fi
sudo sed -i "s|%USER%|${USER}|g; s|%TELEGRAM_BOT_DIR%|${TELEGRAM_BOT_DIR}|; s|%ENV%|${TELEGRAM_BOT_ENV}|; s|%ENV_FILE%|${env_file}|" "${service}"
status_msg "Creating environment file for instance ${i} ..."
cp "${env_template}" "${env_file}"
sed -i "s|%USER%|${USER}|; s|%TELEGRAM_BOT_DIR%|${TELEGRAM_BOT_DIR}|; s|%CFG%|${cfg}|; s|%LOG%|${log}|" "${env_file}"
fi fi
} }
@@ -343,9 +365,35 @@ function remove_telegram_bot_env() {
ok_msg "Directory removed!" ok_msg "Directory removed!"
} }
function remove_telegram_bot_env_file() {
local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/systemd\/moonraker-telegram-bot\.env"
files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort)
if [[ -n ${files} ]]; then
for file in ${files}; do
status_msg "Removing ${file} ..."
rm -f "${file}"
ok_msg "${file} removed!"
done
fi
}
function remove_telegram_bot_logs() { function remove_telegram_bot_logs() {
local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/telegram\.log.*"
files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort)
if [[ -n ${files} ]]; then
for file in ${files}; do
status_msg "Removing ${file} ..."
rm -f "${file}"
ok_msg "${file} removed!"
done
fi
}
function remove_legacy_telegram_bot_logs() {
local files regex="telegram(-[0-9a-zA-Z]+)?\.log(.*)?" local files regex="telegram(-[0-9a-zA-Z]+)?\.log(.*)?"
files=$(find "${KLIPPER_LOGS}" -maxdepth 1 -regextype posix-extended -regex "${KLIPPER_LOGS}/${regex}" | sort) files=$(find "${HOME}/klipper_logs" -maxdepth 1 -regextype posix-extended -regex "${HOME}/klipper_logs/${regex}" 2> /dev/null | sort)
if [[ -n ${files} ]]; then if [[ -n ${files} ]]; then
for file in ${files}; do for file in ${files}; do
@@ -360,7 +408,9 @@ function remove_telegram_bot() {
remove_telegram_bot_systemd remove_telegram_bot_systemd
remove_telegram_bot_dir remove_telegram_bot_dir
remove_telegram_bot_env remove_telegram_bot_env
remove_telegram_bot_env_file
remove_telegram_bot_logs remove_telegram_bot_logs
remove_legacy_telegram_bot_logs
local confirm="Moonraker-Telegram-Bot was successfully removed!" local confirm="Moonraker-Telegram-Bot was successfully removed!"
print_confirm "${confirm}" && return print_confirm "${confirm}" && return
@@ -458,12 +508,13 @@ function compare_telegram_bot_versions() {
#================================================# #================================================#
function patch_telegram_bot_update_manager() { function patch_telegram_bot_update_manager() {
local patched="false" local patched moonraker_configs regex
local moonraker_configs regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf"
moonraker_configs=$(find "${KLIPPER_CONFIG}" -type f -name "moonraker.conf" | sort) moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort)
patched="false"
for conf in ${moonraker_configs}; do for conf in ${moonraker_configs}; do
if ! grep -Eq "^\[update_manager moonraker-telegram-bot\]$" "${conf}"; then if ! grep -Eq "^\[update_manager moonraker-telegram-bot\]\s*$" "${conf}"; then
### add new line to conf if it doesn't end with one ### add new line to conf if it doesn't end with one
[[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}"

Some files were not shown because too many files have changed in this diff Show More