Skip to content

Commit

Permalink
added automatic temporary swap file usage
Browse files Browse the repository at this point in the history
  • Loading branch information
Frix-x committed Sep 29, 2024
1 parent 69bbe2f commit 923b6ac
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 15 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ Follow these steps to install Shake&Tune on your printer:
# RAM, and should work for everyone. However, if you are using a powerful computer, you may
# wish to increase this value to keep more measurements in memory (e.g., 15-20) before writing
# the chunk and avoid stressing the SD card too much.
# temporary_swap_size: 0
# This allows to specify the size of an additional temporary swap file that will be dynamically
# created on the system to avoid running out of memory. This should help mitigating Klipper Timer
# Too Close errors that can occur on low end devices with little RAM like the CB1 when processing
# large measurements. If you want to use this setting, be sure to have enough disk space available
# in your home folder, and a value like 512 or 1024 should be enough in most cases.
# dpi: 300
# Controls the resolution of the generated graphs. The default value of 300 dpi was optimized
# and strikes a balance between performance and readability, ensuring that graphs are clear
Expand Down
46 changes: 38 additions & 8 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/bin/bash

# This script is used to install the Shake&Tune module on a Klipper machine


USER_CONFIG_PATH="${HOME}/printer_data/config"
MOONRAKER_CONFIG="${HOME}/printer_data/config/moonraker.conf"
KLIPPER_PATH="${HOME}/klipper"
Expand All @@ -11,7 +14,6 @@ K_SHAKETUNE_PATH="${HOME}/klippain_shaketune"
set -eu
export LC_ALL=C


function preflight_checks {
if [ "$EUID" -eq 0 ]; then
echo "[PRE-CHECK] This script must not be run as root!"
Expand All @@ -23,7 +25,7 @@ function preflight_checks {
exit -1
fi

if [ "$(sudo systemctl list-units --full -all -t service --no-legend | grep -F 'klipper.service')" ]; then
if sudo systemctl is-active --quiet klipper; then
printf "[PRE-CHECK] Klipper service found! Continuing...\n\n"
else
echo "[ERROR] Klipper service not found, please install Klipper first!"
Expand All @@ -40,7 +42,7 @@ function is_package_installed {
}

function install_package_requirements {
packages=("libopenblas-dev" "libatlas-base-dev")
packages=("libopenblas-dev" "libatlas-base-dev" "sudo")
packages_to_install=""

for package in "${packages[@]}"; do
Expand Down Expand Up @@ -76,14 +78,20 @@ function check_download {
fi
}

function setup_shaketune_sudo {
echo "[SETUP] Setting up sudo permissions for Shake&Tune swap file management..."
chmod +x ${K_SHAKETUNE_PATH}/install_swap_access.sh
${K_SHAKETUNE_PATH}/install_swap_access.sh
}

function setup_venv {
if [ ! -d "${KLIPPER_VENV_PATH}" ]; then
echo "[ERROR] Klipper's Python virtual environment not found!"
exit -1
fi

if [ -d "${OLD_K_SHAKETUNE_VENV}" ]; then
echo "[INFO] Old K-Shake&Tune virtual environement found, cleaning it!"
echo "[INFO] Old K-Shake&Tune virtual environment found, cleaning it!"
rm -rf "${OLD_K_SHAKETUNE_VENV}"
fi

Expand All @@ -101,12 +109,12 @@ function link_extension {
if [ -d "${HOME}/klippain_config" ] && [ -f "${USER_CONFIG_PATH}/.VERSION" ]; then
if [ -d "${USER_CONFIG_PATH}/scripts/K-ShakeTune" ]; then
echo "[INFO] Old K-Shake&Tune macro folder found, cleaning it!"
rm -d "${USER_CONFIG_PATH}/scripts/K-ShakeTune"
rm -rf "${USER_CONFIG_PATH}/scripts/K-ShakeTune"
fi
else
if [ -d "${USER_CONFIG_PATH}/K-ShakeTune" ]; then
echo "[INFO] Old K-Shake&Tune macro folder found, cleaning it!"
rm -d "${USER_CONFIG_PATH}/K-ShakeTune"
rm -rf "${USER_CONFIG_PATH}/K-ShakeTune"
fi
fi
}
Expand Down Expand Up @@ -147,9 +155,31 @@ printf "=============================================\n\n"
# Run steps
preflight_checks
check_download
setup_shaketune_sudo
setup_venv
link_extension
link_module
add_updater
restart_klipper
restart_moonraker


echo "[POST-INSTALL] Shake&Tune installation complete!"

# Ask the user if he want to reboot now
read -p "Do you want to reboot now? [y/N] " answer1
case $answer1 in
[yY][eE][sS]|[yY])
sudo reboot
;;
*)
# Ask the user if he still want to restart Klipper and Moonraker
read -p "Ok, but do you want to at least restart Klipper and Moonraker? [y/N] " answer2
case $answer2 in
[yY][eE][sS]|[yY])
restart_klipper
restart_moonraker
;;
*)
echo "Ok, but just a heads-up: Shake&Tune won't be available until you restart your printer!"
esac
;;
esac
93 changes: 93 additions & 0 deletions install_swap_access.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/bin/bash

# Sets up sudo permissions for the shaketune module to allow
# the user to create and delete a swap file without requiring a password


set -e

SUDOERS_DIR='/etc/sudoers.d'
SUDOERS_FILE='020-sudo-for-shaketune'
NEW_GROUP='shaketunesudo'


verify_ready() {
if [ "$EUID" -eq 0 ]; then
echo "This script must not run as root"
exit -1
fi
}

create_sudoers_file() {
SCRIPT_TEMP_PATH=/tmp

echo "Creating ${SUDOERS_FILE} ..."
sudo rm -f $SCRIPT_TEMP_PATH/$SUDOERS_FILE
sudo tee $SCRIPT_TEMP_PATH/$SUDOERS_FILE > /dev/null << EOF
Cmnd_Alias SWAP_CREATE = /usr/bin/fallocate -l * /home/*/shaketune_swap, /bin/dd if=/dev/zero of=/home/*/shaketune_swap bs=* count=*
Cmnd_Alias SWAP_SETUP = /sbin/mkswap /home/*/shaketune_swap, /sbin/swapon /home/*/shaketune_swap
Cmnd_Alias SWAP_REMOVE = /sbin/swapoff /home/*/shaketune_swap, /bin/rm /home/*/shaketune_swap
%${NEW_GROUP} ALL=(root) NOPASSWD: SWAP_CREATE, SWAP_SETUP, SWAP_REMOVE
EOF
}

verify_syntax() {
if command -v visudo &> /dev/null; then
echo "Verifying syntax of ${SUDOERS_FILE}..."
if sudo visudo -cf $SCRIPT_TEMP_PATH/$SUDOERS_FILE; then
VERIFY_STATUS=0
echo "Syntax OK"
else
echo "Syntax Error: Check file at $SCRIPT_TEMP_PATH/$SUDOERS_FILE"
exit 1
fi
else
VERIFY_STATUS=0
echo "Command 'visudo' not found. Skipping syntax verification."
fi
}

install_sudoers_file() {
verify_syntax
if [ $VERIFY_STATUS -eq 0 ]; then
echo "Installing sudoers file..."
sudo chmod 0440 $SCRIPT_TEMP_PATH/$SUDOERS_FILE
sudo cp $SCRIPT_TEMP_PATH/$SUDOERS_FILE $SUDOERS_DIR/$SUDOERS_FILE
else
exit 1
fi
}

add_new_group() {
if ! getent group $NEW_GROUP &> /dev/null; then
echo "Creating group ${NEW_GROUP}..."
sudo groupadd --system $NEW_GROUP
else
echo "Group ${NEW_GROUP} already exists."
fi
}

add_user_to_group() {
if groups $USER | grep -qw $NEW_GROUP; then
echo "User ${USER} is already in group ${NEW_GROUP}."
else
echo "Adding user ${USER} to group ${NEW_GROUP}..."
sudo usermod -aG $NEW_GROUP $USER
fi
}

clean_temp() {
sudo rm -f $SCRIPT_TEMP_PATH/$SUDOERS_FILE
}


# Run steps
verify_ready
create_sudoers_file
install_sudoers_file
add_new_group
add_user_to_group
clean_temp

exit 0
34 changes: 27 additions & 7 deletions shaketune/shaketune.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@
from .helpers.console_output import ConsoleOutput
from .shaketune_config import ShakeTuneConfig
from .shaketune_process import ShakeTuneProcess
from .swap_manager import SwapManager

DEFAULT_FOLDER = '~/printer_data/config/ShakeTune_results'
DEFAULT_NUMBER_OF_RESULTS = 10
DEFAULT_KEEP_RAW_DATA = False
DEFAULT_DPI = 150
DEFAULT_TIMEOUT = 600
DEFAULT_SHOW_MACROS = True
DEFAULT_NUMBER_OF_RESULTS = 10 # Number of results to keep in the results folder before rotating them
DEFAULT_KEEP_RAW_DATA = False # Whether to also tore the .stdata files in the results folder
DEFAULT_DPI = 150 # Resolution of the generated graphs
DEFAULT_TIMEOUT = 600 # Maximum processing time (in seconds) to allow to Shake&Tune for generating graphs
DEFAULT_SHOW_MACROS = True # Whether to show the Shake&Tune macros in the web UI
DEFAULT_MEASUREMENTS_CHUNK_SIZE = 2 # Maximum number of measurements to keep in memory at once
DEFAULT_TEMP_SWAP_SIZE_MB = 0 # 0 means no swap file is created
ST_COMMANDS = {
'EXCITATE_AXIS_AT_FREQ': (
'Maintain a specified excitation frequency for a period '
Expand Down Expand Up @@ -82,10 +84,18 @@ def _initialize_config(self, config) -> None:
keep_raw_data = config.getboolean('keep_raw_data', default=DEFAULT_KEEP_RAW_DATA)
dpi = config.getint('dpi', default=DEFAULT_DPI, minval=100, maxval=500)
m_chunk_size = config.getint('measurements_chunk_size', default=DEFAULT_MEASUREMENTS_CHUNK_SIZE, minval=2)
self._st_config = ShakeTuneConfig(result_folder_path, keep_n_results, keep_raw_data, m_chunk_size, dpi)

temp_swap_size_mb = config.getint('temporary_swap_size', default=DEFAULT_TEMP_SWAP_SIZE_MB, minval=0)
self._st_config = ShakeTuneConfig(
result_folder_path,
keep_n_results,
keep_raw_data,
m_chunk_size,
temp_swap_size_mb,
dpi,
)
self.timeout = config.getfloat('timeout', DEFAULT_TIMEOUT, above=0.0)
self._show_macros = config.getboolean('show_macros_in_webui', default=DEFAULT_SHOW_MACROS)
self._swap_manager = SwapManager(temp_swap_size_mb)

# Create the Klipper commands to allow the user to run Shake&Tune's tools
def _register_commands(self) -> None:
Expand Down Expand Up @@ -160,7 +170,9 @@ def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None:
static_freq_graph_creator,
self.timeout,
)
self._swap_manager.add_swap()
excitate_axis_at_freq(gcmd, self._config, st_process)
self._swap_manager.remove_swap()

def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
Expand All @@ -171,7 +183,9 @@ def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
axes_map_graph_creator,
self.timeout,
)
self._swap_manager.add_swap()
axes_map_calibration(gcmd, self._config, st_process)
self._swap_manager.remove_swap()

def cmd_COMPARE_BELTS_RESPONSES(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
Expand All @@ -182,7 +196,9 @@ def cmd_COMPARE_BELTS_RESPONSES(self, gcmd) -> None:
belt_graph_creator,
self.timeout,
)
self._swap_manager.add_swap()
compare_belts_responses(gcmd, self._config, st_process)
self._swap_manager.remove_swap()

def cmd_AXES_SHAPER_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
Expand All @@ -193,7 +209,9 @@ def cmd_AXES_SHAPER_CALIBRATION(self, gcmd) -> None:
shaper_graph_creator,
self.timeout,
)
self._swap_manager.add_swap()
axes_shaper_calibration(gcmd, self._config, st_process)
self._swap_manager.remove_swap()

def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
Expand All @@ -204,4 +222,6 @@ def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None:
vibration_profile_creator,
self.timeout,
)
self._swap_manager.add_swap()
create_vibrations_profile(gcmd, self._config, st_process)
self._swap_manager.remove_swap()
2 changes: 2 additions & 0 deletions shaketune/shaketune_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ def __init__(
keep_n_results: int = 10,
keep_raw_data: bool = False,
chunk_size: int = 2,
temp_swap_size_mb: int = 0,
dpi: int = 150,
) -> None:
self._result_folder = result_folder

self.keep_n_results = keep_n_results
self.keep_raw_data = keep_raw_data
self.chunk_size = chunk_size
self.temp_swap_size_mb = temp_swap_size_mb
self.dpi = dpi

self.klipper_folder = KLIPPER_FOLDER
Expand Down
79 changes: 79 additions & 0 deletions shaketune/swap_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Shake&Tune: 3D printer analysis tools
#
# Copyright (C) 2022 - 2024 Félix Boisselier <felix@fboisselier.fr> (Frix_x on Discord)
# Licensed under the GNU General Public License v3.0 (GPL-3.0)
#
# File: swap_manager.py
# Description: Implements the SwapManager class for managing the creation and
# activation of a temporary swap file on the system to avoid running
# out of memory when processing large files (useful for low end devices like CB1)

import shutil
import subprocess
from pathlib import Path

from .helpers.console_output import ConsoleOutput

SWAP_FILE_PATH = Path.home() / 'shaketune_swap'


class SwapManager:
def __init__(self, swap_size_mb: int = 0) -> None:
self._swap_size_mb = swap_size_mb
self._swap_file_path = SWAP_FILE_PATH
self._swap_activated = False

def is_swap_activated(self) -> bool:
return self._swap_activated

def add_swap(self) -> None:
if self._swap_size_mb <= 0:
return

# Check if swap file already exists and delete it if it does
if self._swap_file_path.exists():
ConsoleOutput.print(f'Warning: {self._swap_file_path} already exists. Replacing it...')
self.remove_swap()

# Check available disk space to be sure there is enough space for the swap file
total, used, free = shutil.disk_usage(self._swap_file_path.parent)
free_mb = free // (1024 * 1024)
if free_mb < self._swap_size_mb:
ConsoleOutput.print(
f'Warning: not enough disk space available ({free_mb} MB) to create the temporary swap file '
f'that you asked for ({self._swap_size_mb} MB). It will not be created for this run...'
)
return

# Create the swap file and activate it
try:
subprocess.run(
[
'sudo',
'dd',
'if=/dev/zero',
'of=' + str(self._swap_file_path),
'bs=1M',
'count=' + str(self._swap_size_mb),
],
check=True,
)
subprocess.run(['sudo', 'chmod', '600', str(self._swap_file_path)], check=True)
subprocess.run(['sudo', 'mkswap', str(self._swap_file_path)], check=True)
subprocess.run(['sudo', 'swapon', str(self._swap_file_path)], check=True)
self._swap_activated = True
ConsoleOutput.print(f'Temporary swap file of {self._swap_size_mb} MB activated')
except subprocess.CalledProcessError as err:
self.remove_swap()
raise RuntimeError('Failed to create and activate the temporary swap file!') from err

def remove_swap(self) -> None:
if not self._swap_file_path.exists():
return

try:
if self._swap_activated:
subprocess.run(['sudo', 'swapoff', str(self._swap_file_path)], check=True)
subprocess.run(['sudo', 'rm', str(self._swap_file_path)], check=True)
except subprocess.CalledProcessError as err:
raise RuntimeError('Failed to deactivate and delete the temporary swap file!') from err

0 comments on commit 923b6ac

Please sign in to comment.