#!/usr/bin/env bash #=======================================================================# # Copyright (C) 2020 - 2024 Dominik Willner # # # # This file is part of KIAUH - Klipper Installation And Update Helper # # https://github.com/dw-0/kiauh # # # # This file may be distributed under the terms of the GNU GPLv3 license # #=======================================================================# set -e #===================================================# #================= INSTALL MAINSAIL ================# #===================================================# function install_mainsail() { if [[ -z $(moonraker_systemd) ]]; then local error="Moonraker not installed! It's recommended to install Moonraker first!" 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 ### checking dependencies local dep=(wget nginx) dependency_check "${dep[@]}" ### detect conflicting Haproxy and Apache2 installations detect_conflicting_packages status_msg "Initializing Mainsail installation ..." ### first, we create a backup of the full klipper_config dir - safety first! backup_config_dir ### check for other enabled web interfaces unset SET_LISTEN_PORT detect_enabled_sites ### check if another site already listens to port 80 mainsail_port_check ### download mainsail download_mainsail ### ask user to install the recommended webinterface macros install_mainsail_macros ### create /etc/nginx/conf.d/upstreams.conf set_upstream_nginx_cfg ### create /etc/nginx/sites-available/ set_nginx_cfg "mainsail" ### nginx on ubuntu 21 and above needs special permissions to access the files set_nginx_permissions ### symlink nginx log symlink_webui_nginx_log "mainsail" ### add mainsail to the update manager in moonraker.conf patch_mainsail_update_manager fetch_webui_ports #WIP ### confirm message print_confirm "Mainsail has been set up!" } function install_mainsail_macros() { local yn while true; do echo top_border echo -e "| It is recommended to use special macros in order to |" echo -e "| have Mainsail fully functional and working. |" blank_line echo -e "| The recommended macros for Mainsail can be seen here: |" echo -e "| https://github.com/mainsail-crew/mainsail-config |" blank_line echo -e "| If you already use these macros skip this step. |" echo -e "| Otherwise you should consider to answer with 'yes' to |" echo -e "| download the recommended macros. |" bottom_border read -p "${cyan}###### Download the recommended macros? (Y/n):${white} " yn case "${yn}" in Y|y|Yes|yes|"") select_msg "Yes" download_mainsail_macros break;; N|n|No|no) select_msg "No" break;; *) print_error "Invalid command!";; esac done return } function download_mainsail_macros() { local ms_cfg_repo path configs regex line gcode_dir ms_cfg_repo="https://github.com/mainsail-crew/mainsail-config.git" regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/printer\.cfg" configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) if [[ -z ${configs} ]]; then print_error "No printer.cfg found! Installation of Macros will be skipped ..." log_error "execution stopped! reason: no printer.cfg found" return 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() { local services local url url=$(get_mainsail_download_url) status_msg "Downloading Mainsail from ${url} ..." if [[ -d ${MAINSAIL_DIR} ]]; then rm -rf "${MAINSAIL_DIR}" fi mkdir "${MAINSAIL_DIR}" && cd "${MAINSAIL_DIR}" if wget "${url}"; then ok_msg "Download complete!" status_msg "Extracting archive ..." unzip -q -o ./*.zip && ok_msg "Done!" status_msg "Remove downloaded archive ..." rm -rf ./*.zip && ok_msg "Done!" else print_error "Downloading Mainsail from\n ${url}\n failed!" exit 1 fi ### check for moonraker multi-instance and if no-instance or multi-instance was found, enable mainsails remoteMode services=$(moonraker_systemd) if [[ ( -z "${services}" ) || ( $(echo "${services}" | wc -w) -gt 1 ) ]]; then enable_mainsail_remotemode fi } #===================================================# #================= REMOVE MAINSAIL =================# #===================================================# function remove_mainsail_dir() { [[ ! -d ${MAINSAIL_DIR} ]] && return status_msg "Removing Mainsail directory ..." rm -rf "${MAINSAIL_DIR}" && ok_msg "Directory removed!" } function remove_mainsail_nginx_config() { if [[ -e "/etc/nginx/sites-available/mainsail" ]]; then status_msg "Removing Mainsail configuration for Nginx ..." sudo rm "/etc/nginx/sites-available/mainsail" && ok_msg "File removed!" fi if [[ -L "/etc/nginx/sites-enabled/mainsail" ]]; then status_msg "Removing Mainsail Symlink for Nginx ..." sudo rm "/etc/nginx/sites-enabled/mainsail" && ok_msg "File removed!" fi } function remove_mainsail_logs() { local files files=$(find /var/log/nginx -name "mainsail*" 2> /dev/null | sort) if [[ -n ${files} ]]; then for file in ${files}; do status_msg "Removing ${file} ..." sudo rm -f "${file}" ok_msg "${file} removed!" done fi } function remove_mainsail_log_symlinks() { local files regex regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/mainsail-.*" files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${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_legacy_mainsail_log_symlinks() { local files files=$(find "${HOME}/klipper_logs" -name "mainsail*" 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_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() { remove_mainsail_dir remove_mainsail_nginx_config remove_mainsail_logs remove_mainsail_log_symlinks remove_legacy_mainsail_log_symlinks ### remove mainsail_port from ~/.kiauh.ini sed -i "/^mainsail_port=/d" "${INI_FILE}" print_confirm "Mainsail successfully removed!" } #===================================================# #================= UPDATE MAINSAIL =================# #===================================================# function update_mainsail() { backup_before_update "mainsail" status_msg "Updating Mainsail ..." download_mainsail match_nginx_configs symlink_webui_nginx_log "mainsail" print_confirm "Mainsail successfully updated!" } #===================================================# #================= MAINSAIL STATUS =================# #===================================================# function get_mainsail_status() { local status local data_arr=("${MAINSAIL_DIR}" "${NGINX_SA}/mainsail" "${NGINX_SE}/mainsail") ### 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_mainsail_version() { local versionfile="${MAINSAIL_DIR}/.version" local relinfofile="${MAINSAIL_DIR}/release_info.json" local version if [[ -f ${relinfofile} ]]; then version=$(grep -o '"version":"[^"]*' "${relinfofile}" | grep -o '[^"]*$') elif [[ -f ${versionfile} ]]; then version=$(head -n 1 "${versionfile}") fi echo "${version}" } function get_remote_mainsail_version() { [[ ! $(dpkg-query -f'${Status}' --show curl 2>/dev/null) = *\ installed ]] && return local tags tags=$(curl -s "https://api.github.com/repos/mainsail-crew/mainsail/tags" | grep "name" | cut -d'"' -f4) echo "${tags}" | head -1 } function compare_mainsail_versions() { local versions local_ver remote_ver local_ver="$(get_local_mainsail_version)" remote_ver="$(get_remote_mainsail_version)" if [[ ${local_ver} != "${remote_ver}" && ${local_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 "mainsail" else versions="${green}$(printf " %-14s" "${local_ver}")${white}" versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" fi echo "${versions}" } #================================================# #=========== MAINSAIL THEME INSTALLER ===========# #================================================# function print_theme_list() { local i=0 theme_list=${1} while IFS="," read -r col1 col2 col3 col4; do if [[ ${col1} != "name" ]]; then printf "| ${i}) %-51s|\n" "[${col1}]" fi i=$(( i + 1 )) done <<< "${theme_list}" } function ms_theme_installer_menu() { local theme_list theme_author theme_repo theme_name theme_note theme_url local theme_csv_url="https://raw.githubusercontent.com/mainsail-crew/gb-docs/main/_data/themes.csv" theme_list=$(curl -s -L "${theme_csv_url}") top_border echo -e "| ${red}~~~~~~~~ [ Mainsail Theme Installer ] ~~~~~~~${white} |" hr echo -e "| ${cyan}A preview of each Mainsail theme can be found here:${white} |" echo -e "| https://docs.mainsail.xyz/theming/themes |" blank_line echo -e "| ${yellow}Important note:${white} |" echo -e "| Installing a theme from this menu will overwrite an |" echo -e "| already installed theme or modified custom.css file! |" hr print_theme_list "${theme_list}" echo -e "| |" echo -e "| R) [Remove Theme] |" back_footer while IFS="," read -r col1 col2 col3 col4; do theme_name+=("${col1}") theme_note+=("${col2}") theme_author+=("${col3}") theme_repo+=("${col4}") done <<< "${theme_list}" local option while true; do read -p "${cyan}Install theme:${white} " option if (( option > 0 && option < ${#theme_name[@]} )); then theme_url="https://github.com/${theme_author[${option}]}/${theme_repo[${option}]}" ms_theme_install "${theme_url}" "${theme_name[${option}]}" "${theme_note[${option}]}" break elif [[ ${option} == "R" || ${option} == "r" ]]; then ms_theme_delete break elif [[ ${option} == "B" || ${option} == "b" ]]; then advanced_menu break else error_msg "Invalid command!" fi done ms_theme_installer_menu } function ms_theme_install() { read_kiauh_ini "${FUNCNAME[0]}" local theme_url local theme_name local theme_note theme_url=${1} theme_name=${2} theme_note=${3} local folder_arr local folder_names="${multi_instance_names}" local target_folders=() IFS=',' read -r -a folder_arr <<< "${folder_names}" ### build theme target folder array if (( ${#folder_arr[@]} > 1 )); then for folder in "${folder_arr[@]}"; do ### instance names/identifier of only numbers need to be prefixed with 'printer_' if [[ ${folder} =~ ^[0-9]+$ ]]; then target_folders+=("${HOME}/printer_${folder}_data/config") else target_folders+=("${HOME}/${folder}_data/config") fi done else target_folders+=("${HOME}/printer_data/config") fi if (( ${#target_folders[@]} > 1 )); then top_border echo -e "| Please select the printer you want to apply the theme |" echo -e "| installation to: |" for (( i=0; i < ${#target_folders[@]}; i++ )); do folder=$(echo "${target_folders[${i}]}" | rev | cut -d "/" -f2 | cut -d"_" -f2- | rev) printf "|${cyan}%-55s${white}|\n" " ${i}) ${folder}" done bottom_border local target re="^[0-9]*$" while true; do read -p "${cyan}###### Select printer:${white} " target ### break while loop if input is valid, else display error [[ ${target} =~ ${re} && ${target} -lt ${#target_folders[@]} ]] && break error_msg "Invalid command!" done fi [[ -d "${target_folders[${target}]}/.theme" ]] && rm -rf "${target_folders[${target}]}/.theme" status_msg "Installing '${theme_name}' to ${target_folders[${target}]} ..." cd "${target_folders[${target}]}" if git clone "${theme_url}" ".theme"; then ok_msg "Theme installation complete!" [[ -n ${theme_note} ]] && echo "${yellow}###### Theme Info: ${theme_note}${white}" ok_msg "Please remember to delete your browser cache!\n" else error_msg "Theme installation failed!\n" fi } function ms_theme_delete() { local regex theme_folders target_folders=() 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 "${KLIPPER_CONFIG}" -mindepth 1 -type d -name ".theme" | sort) ### build target folder array for folder in ${theme_folders}; do target_folders+=("${folder}") done if (( ${#target_folders[@]} == 0 )); then status_msg "No Themes installed!\n" return elif (( ${#target_folders[@]} > 1 )); then top_border echo -e "| Please select the printer you want to remove the |" echo -e "| theme installation from. |" for (( i=0; i < ${#target_folders[@]}; i++ )); do folder=$(echo "${target_folders[${i}]}" | rev | cut -d "/" -f2 | rev) printf "|${cyan}%-55s${white}|\n" " ${i}) ${folder}" done bottom_border local target re="^[0-9]*$" while true; do read -p "${cyan}###### Select printer:${white} " target ### break while loop if input is valid, else display error [[ ${target} =~ ${re} && ${target} -lt ${#target_folders[@]} ]] && break error_msg "Invalid command!" done fi status_msg "Removing ${target_folders[${target}]} ..." rm -rf "${target_folders[${target}]}" && ok_msg "Theme removed!\n" return } #================================================# #=================== HELPERS ====================# #================================================# function get_mainsail_download_url() { local releases_by_tag tags tag unstable_url url ### latest stable download url url="https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" read_kiauh_ini "${FUNCNAME[0]}" if [[ ${mainsail_install_unstable} == "true" ]]; then releases_by_tag="https://api.github.com/repos/mainsail-crew/mainsail/tags" tags=$(curl -s "${releases_by_tag}" | grep "name" | cut -d'"' -f4) tag=$(echo "${tags}" | head -1) ### 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 echo "${url}" } function mainsail_port_check() { if [[ ${MAINSAIL_ENABLED} == "false" ]]; then if [[ ${SITE_ENABLED} == "true" ]]; then status_msg "Detected other enabled interfaces:" [[ ${FLUIDD_ENABLED} == "true" ]] && \ echo -e " ${cyan}● Fluidd - Port: ${FLUIDD_PORT}${white}" if [[ ${FLUIDD_PORT} == "80" ]]; then PORT_80_BLOCKED="true" select_mainsail_port fi else DEFAULT_PORT=$(grep listen "${KIAUH_SRCDIR}/resources/mainsail" | head -1 | sed 's/^\s*//' | cut -d" " -f2 | cut -d";" -f1) SET_LISTEN_PORT=${DEFAULT_PORT} fi SET_NGINX_CFG="true" else SET_NGINX_CFG="false" fi } function select_mainsail_port() { if [[ ${PORT_80_BLOCKED} == "true" ]]; then echo top_border echo -e "| ${red}!!!WARNING!!!${white} |" echo -e "| ${red}You need to choose a different port for Mainsail!${white} |" echo -e "| ${red}The following web interface is listening at port 80:${white} |" blank_line [[ ${FLUIDD_PORT} == "80" ]] && echo "| ● Fluidd |" blank_line echo -e "| Make sure you don't choose a port which was already |" echo -e "| assigned to another webinterface! |" blank_line echo -e "| Be aware: there is ${red}NO${white} sanity check for the following |" echo -e "| input. So make sure to choose a valid port! |" bottom_border local new_port re="^[0-9]+$" while true; do read -p "${cyan}Please enter a new Port:${white} " new_port if [[ ${new_port} =~ ${re} && ${new_port} != "${FLUIDD_PORT}" ]]; then select_msg "Setting port ${new_port} for Mainsail!" SET_LISTEN_PORT=${new_port} break else if [[ ! ${new_port} =~ ${re} ]]; then error_msg "Invalid input!" else error_msg "Port already taken! Select a different one!" fi fi done fi } function enable_mainsail_remotemode() { [[ ! -f "${MAINSAIL_DIR}/config.json" ]] && return status_msg "Setting instance storage location to 'browser' ..." sed -i 's|"instancesDB": "moonraker"|"instancesDB": "browser"|' "${MAINSAIL_DIR}/config.json" ok_msg "Done!" } function patch_mainsail_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\]\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 to update manager in file:\n ${conf}" /bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF [update_manager mainsail] type: web channel: stable repo: mainsail-crew/mainsail path: ~/mainsail MOONRAKER_CONF fi patched="true" done if [[ ${patched} == "true" ]]; then do_action_service "restart" "moonraker" 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 }