# Parabola Install Wizard - common functions # # Copyright (C) 2020,2022 bill-auger # # SPDX-License-Identifier: GPL-3.0-or-later # # This file is part of Parabola Install Wizard. # # Parabola Install Wizard is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Parabola Install Wizard is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Parabola Install Wizard. If not, see . cd $(dirname ${BASH_SOURCE[0]}) source ./constants.sh.inc ## sanity checks ## if (( ! IN_CHROOT )) then which dialog &> /dev/null || ! echo "${DIALOG_ERR_MSG}" || exit (( ! $EUID )) || ! echo "${PRIVILEGE_ERR_MSG}" || exit [[ -w ./.session_state ]] || ! echo "${STATEFILE_ERR_MSG}" || exit fi ## state helpers ## SetStateVar() # (var_name value*) { local var_name=$1 ; shift ; local value="$*" sed -i "/^${var_name}=.*/d" ./.session_state echo "${var_name}=${value}" >> ./.session_state } GetStateVar() # (var_name [def_value]) { local var_name=$1 local def_value=$2 local stored_value=$(grep "${var_name}=" ./.session_state | cut -d '=' -f 2) if [[ -n "${stored_value}" ]] && [[ "${stored_value}" != '_UNDEFINED_' ]] then echo ${stored_value} else echo ${def_value} fi } AreConfiguredStateVars() # ( state_var [ state_var ] ... ) { # for state_var in $* ; do echo -n "AreConfiguredStateVars() state_var=${state_var}=$(GetStateVar ${state_var})=" ; [[ -n "$(GetStateVar ${state_var})" ]] && echo "OK" || ! echo "NFG" ; done ; # TODO: replace each call to this function with proper validations for state_var in $* ; do [[ -n "$(GetStateVar ${state_var})" ]] || return 1 ; done ; return 0 } ## dialog prompt helpers ## # WizardDlg() notes: # the primary purpose of the WizardDlg() function, is to simplify and normalize # creation of various dialog boxes, such that they can all be handled uniformly. # # in order for this to work properly, callers of WizardDlg() should pass the # box-type argument (eg: --menu, --yesno, --inputbox) as positional parameter #2, # followed by it's box-specific options. additional dialog options may be set # via the 'DIALOGOPTS' environment variable. # # when the "Cancel" button or key is pressed, in a 'msgbox', # 'dialog' exits with a non-zero status; and no result is printed. # install.sh runs under `set -e`; so we can not propogate the non-zero status, # otherwise, all callers of these helpers would need to supress it. # callers should instead, exit upon the empty result, when a selection is mandatory. # # because an 'inputbox' may be blank, when the "OK" button is pressed, we emit a space, # in order to maintain the invariant, such that the "OK" result is never empty. # # 'dialog' is quirky; and it was difficult to acheive complete uniformity. # so unfortunately, there are some caveats: # * 'yesno' dialogs exit with a non-zero status upon the "No" button. # the "Cancel" and "No" buttons are identical, and indistinguishable at run-time. # the $YESNO_YES or $YESNO_NO values are printed explicitly for 'yesno' dialogs, # so that callers can handle the 'yesno' dialogs in the same way as 'msgbox' dialogs. # * 'checklist' dialogs print no result if nothing was selected, # which would be indistinguishable from a cancel operation (eg: key). # for that reason, $CANCEL_SENTINEL will be printed explicitly, upon a cancel operation # for 'checklist' dialogs, so that interested callers may handle it. readonly CANCEL_STATUS=1 readonly CANCEL_SENTINEL='CANCEL' # dialog prompt for session-init.sh (with auto-accept timeout) InitDlg() # (title prompt default_option options*) { local title="$1" local prompt="$2" local default_option="$3" local options=("${@:4}") dialog --stdout --sleep 1 --no-tags --no-cancel --timeout 30 \ --backtitle "${title}" \ --default-item "${default_option}" \ --menu "${prompt}" 15 42 10 \ "${options[@]}" } # dialog prompts for install.sh WizardDlg() # ("title" box_type "boxargs"* options*) { local title="$1" local dialog_type=$2 local boxargs_and_options=("${@:3}") local opts=( $( [[ "${dialog_type}" == '--progressbox' ]] || [[ "${dialog_type}" == '--infobox' ]] || echo '--erase-on-exit' ) ) local result local status result=$( DIALOG_ESC=${CANCEL_STATUS} \ dialog --stdout --insecure --sleep 1 \ --colors ${opts[*]} \ --backtitle "${TR[wizard-${Lang}]}" \ --title "${title}" \ ${dialog_type} "${boxargs_and_options[@]}" ) && status=$? || status=$? # NOTE: a failure on the following LOC, indicates a bug in the caller code (( ! status || status == CANCEL_STATUS )) if [[ "${dialog_type}" == '--menu' ]] then echo "${result}" elif [[ "${dialog_type}" == '--yesno' ]] then echo $(( ! status )) elif [[ "${dialog_type}" == '--inputbox' ]] || [[ "${dialog_type}" == '--passwordbox' ]] then if (( status != CANCEL_STATUS )) then [[ -n "${result}" ]] && echo "${result}" || echo ' ' fi elif [[ "${dialog_type}" == '--checklist' ]] then (( status != CANCEL_STATUS )) && echo "${result}" || echo ${CANCEL_SENTINEL} elif [[ "${dialog_type}" == '--infobox' ]] || [[ "${dialog_type}" == '--gauge' ]] || [[ "${dialog_type}" == '--msgbox' ]] || [[ "${dialog_type}" == '--progressbox' ]] then echo '' else echo "unhandled dialog_type: ${dialog_type}" >&2 fi (( status == CANCEL_STATUS )) && status=0 || true ; return ${status} } ExitFail() # ("exit_msg") { WizardDlg "${TR[exit_fail-${Lang}]}" --infobox "$(echo -e $@)" 0 0 exit } ## input validation helpers ## ValidateId() # (login) { local login="$*" [[ "${login}" =~ ^[[:space:]]*[^[:space:]]+[[:space:]]*$ ]] # TODO: improve this } ## partitioning helpers ## readonly DISK_RECORD_SEP='|' readonly DISK_FIELD_SEP=':' declare -A PartsData # raw data declare -a DlgParams # dialog options UnmountAll() { mkdir -p /mnt umount /mnt/boot &> /dev/null || true umount /mnt/home &> /dev/null || true umount /mnt &> /dev/null || true } GetDisksPartsData() { local line local dev local size local part_n local format # example parted output (GPT): # BYT; # /dev/sda:1074MB:scsi:512:512:gpt:ATA QEMU HARDDISK:; # QEMU device # 1:1049kB:106MB:105MB:ext4::; # EXT4 partition # 2:106MB:211MB:105MB:::; # un-formatted partition # 3:211MB:316MB:105MB:::swap; # un-formatted swap partition # 4:316MB:421MB:105MB:linux-swap(v1)::swap; # formatted swap partition # # <-- ~600MB unallocated space # # example parted output (MBR): # BYT; # /dev/sdb:250GB:scsi:512:512:msdos:ATA ST3250318AS:; # hardware device # 1:1049kB:525MB:524MB:ntfs::boot; # windows boot partition # 2:525MB:20.5GB:20.0GB:ntfs::; # windows system partition # 4:20.5GB:249GB:228GB:::lvm; # LLVM partition # 3:249GB:250GB:1049MB:ntfs::msftres; # windows data partition # # NOTE: the example /dev/sdb is a typical windows partition scheme, where partition #2 was shrunken, # partition #3 was deleted, and a *nix partition (#4) was added # # example parted output (multiple devices, no partition table on sdb): # BYT; # /dev/sda:42.9GB:scsi:512:512:msdos:ATA QEMU HARDDISK:; # 1:1049kB:42.9GB:42.9GB:ext4::; # # BYT; # /dev/sdb:1074MB:scsi:512:512:unknown:ATA QEMU HARDDISK:; # # NOTE: in the 'BYT;' line of the example /dev/sdb, # all of those spaces and a stray CR are present in the output # ' \rBYT;' # # example GetDisksPartsData() output: # /dev/sda:1074MB|/dev/sda1:105MB:ext4|/dev/sda2:105MB # /dev/sdb:250GB|/dev/sdb1:524MB:ntfs|/dev/sdb2:20.0GB:ntfs|/dev/sdb4:228GB:|/dev/sdb3:1049MB:ntfs # # TODO: the above knowledge could be encapsulated as test mock data # * make GetDisksPartsData() store into a cache instead of returning results # * call GetDisksPartsData() only once # * replace GetDisksPartsData() in callers with the cache # * write a test suite against these partitioning helpers, # mocking the GetDisksPartsData() cache with the data above # * capture the 7th field (partition type) # * allow automatically format un-formatted swap partitions in Partition() # # TODO: filter out mounted filesystems UnmountAll parted --list --machine 2> /dev/null | tr -d '\r' | \ while read line do if [[ "$line" =~ ^BYT\;\ *$ ]] # device delimiter then echo ' ' elif [[ "${line}" =~ ^(/dev/sd[a-z]):([^:]*): ]] # device specifier then dev="${BASH_REMATCH[1]}" size="${BASH_REMATCH[2]}" echo -n "${dev}${DISK_FIELD_SEP}${size}" elif [[ "${line}" =~ ^([0-9]):[^:]*:[^:]*:([^:]*):([^:]*): ]] # partition specifier then part_n=${BASH_REMATCH[1]} size=${BASH_REMATCH[2]} format=$( [[ -n "${BASH_REMATCH[3]}" ]] && echo ${BASH_REMATCH[3]} || : ) echo -n "${DISK_RECORD_SEP}${dev}${part_n}" echo -n "${DISK_FIELD_SEP}${size}" echo -n "${DISK_FIELD_SEP}${format}" fi done } GetDiskData() # (disk_data_n) { # example GetDiskData() output: # /dev/sda 42.9GB if [[ "$1" =~ ^[0-9]+$ ]] then local disk_data_n=$1 local disks_parts_data=( $(GetDisksPartsData) ) local disk_data=${disks_parts_data[${disk_data_n}]%%${DISK_RECORD_SEP}*} tr "${DISK_FIELD_SEP}" ' ' <<<${disk_data} fi } GetDiskPartsData() # (disk_data_n) { # example GetDiskPartsData() output: # /dev/sdb1:968MB:ext4 /dev/sdb2:105MB:linux-swap(v1) local disk_data_n=$1 local disks_parts_data=( $(GetDisksPartsData) ) local disk_parts_data=${disks_parts_data[${disk_data_n}]#*${DISK_RECORD_SEP}} tr "${DISK_RECORD_SEP}" ' ' <<<${disk_parts_data} } GetDiskPartData() # (disk_data_n part_data_n) { # example GetDiskPartData() output: # /dev/sdb1 968MB ext4 local disk_data_n=$1 local part_data_n=$2 local disk_parts_data=( $(GetDiskPartsData ${disk_data_n}) ) local disk_part_data=${disk_parts_data[${part_data_n}]} tr "${DISK_FIELD_SEP}" ' ' <<<${disk_part_data} } GetDevice() # (disk_data_n) { # example GetDevice() output: # /dev/sda local disk_data_n=$1 local disk_data=$(GetDiskData ${disk_data_n}) local device=${disk_data/ *} echo ${device} } PopulatePartsData() # (disk_data_n) # sets $PartsData { local disk_data_n=$(( $1 - 1 )) # dialog device_n is 1-based local disk_parts_data=( $(GetDiskPartsData ${disk_data_n}) ) local part_data_n local disk_part_data local dlg_part_n PartsData=() # populate storage array for partition data - example PartsData: # [1]="/dev/sdb1 9662MB ext4" [2]="/dev/sdb2 1048kB" for (( part_data_n=0 ; part_data_n < ${#disk_parts_data[@]} ; ++part_data_n )) do disk_part_data="$(GetDiskPartData ${disk_data_n} ${part_data_n})" dlg_part_n=$(( ${part_data_n} + 1 )) # sparse array for selective deletion if ! [[ "${disk_part_data}" =~ linux-swap ]] && # ignore swap ! mount | grep "^${disk_part_data/ *} " > /dev/null # ignore mounted then PartsData[${dlg_part_n}]="${disk_part_data}" fi done } PopulateDisksOptions() # sets $DlgParams { local disks_part_data=( $(GetDisksPartsData) ) local disk_data_n DlgParams=() # populate params array for dialog - example DlgParams: # ( 1 "/dev/sda 42.9GB" 2 "/dev/sdb 9664MB" ) for (( disk_data_n=0 ; disk_data_n < ${#disks_part_data[@]} ; ++disk_data_n )) do DlgParams+=( $(( ${disk_data_n} + 1 )) "$(GetDiskData ${disk_data_n})" ) done } PopulatePartOptions() # sets $DlgParams, assumes $PartsData { local part_data_n DlgParams=() # populate params array for dialog - example DlgParams: # ( 1 "/dev/sdb1 9662MB ext4" 2 "/dev/sdb2 1048kB" ) for part_data_n in $(tr ' ' '\n' <<<${!PartsData[@]} | sort) do DlgParams+=( ${part_data_n} "${PartsData[${part_data_n}]}" ) done } RemovePartOption() # (part_n) # modifies $PartsData { local dlg_part_n=$1 unset 'PartsData['"${dlg_part_n}"']' PopulatePartOptions } ## error logging helpers ## LogError() # (source_file func_name line_n) { local SOURCE_FILE="${BASH_SOURCE[1]}" local FUNC_NAME="$( [[ -n "$1" ]] && echo "$1" || echo "FUNC_NAME" )" local LINE_N=$2 local N_CONTEXT_LINES=3 local N_LINES=$(( 1 + (2 * N_CONTEXT_LINES) )) local BEGIN_LINE_N=$(( LINE_N - N_CONTEXT_LINES )) local END_LINE_N=$(( LINE_N + N_CONTEXT_LINES )) local marker line (( BEGIN_LINE_N < 0 )) && BEGIN_LINE_N=0 echo "ERROR: in ${SOURCE_FILE} ${FUNC_NAME}()::${LINE_N}" >&2 sed 's|\\$||' "${SOURCE_FILE}" | pr -tn | \ tail -n +${BEGIN_LINE_N} | head -n ${N_LINES} | tr '\n' '\n' | \ while read line do line_n=$(sed -E 's|([0-9]+).*|\1|' <<<${line}) (( line_n == LINE_N )) && marker='==>' || marker=' ' printf "%s %s\n" "${marker}" "${line}" >&2 done } ## debugging helpers ## MOCK_INIT() { SetStateVar 'XKBMAP' ${DEF_KEYMAP} ; SetStateVar 'LANG' ${DEF_LANG} ; SetStateVar 'TR_KEY' ${DEF_TRKEY} ; SetStateVar 'INSTALL' ${DEF_INSTALL} ; } MOCK_SELECTDEFAULTS() { SetStateVar 'INSTALL' ${DEF_INSTALL} ; SetStateVar 'BASE' ${DEF_PKG_SET} ; SetStateVar 'INIT' ${DEF_INIT} ; SetStateVar 'WMDE' ${DEF_WMDE} ; SetStateVar 'HOSTNAME' ${DEF_HOSTNAME} ; SetStateVar 'TIMEZONE' ${DEF_TIMEZONE} ; SetStateVar 'KEYMAP' $(GetStateVar 'XKBMAP') ; SetStateVar 'LOCALES' $(GetStateVar 'LANG') ; } MOCK_SELECTDEFAULTS_NO() { : ; } MOCK_SELECTLOGINS() { SetStateVar 'ROOT_PASS' ROOT_PASS ; MOCK_SELECTLOGINS_USER ; } MOCK_SELECTLOGINS_USER() { SetStateVar 'USER_LOGIN' USER_LOGIN ; SetStateVar 'USER_PASS' USER_PASS ; } MOCK_SELECTBASE() { SetStateVar 'INSTALL' ${DEF_INSTALL} ; SetStateVar 'BASE' ${DEF_PKG_SET} ; SetStateVar 'INIT' ${DEF_INIT} ; } MOCK_SELECTBOOT() { SetStateVar 'BOOT' 'grub' ; } MOCK_SELECTWMDE() { [[ -n "$(GetStateVar 'WMDE')" ]] || SetStateVar 'WMDE' 'cli' ; } MOCK_SELECTENV() { SetStateVar 'HOSTNAME' ${DEF_HOSTNAME} ; SetStateVar 'TIMEZONE' ${DEF_TIMEZONE} ; SetStateVar 'KEYMAP' $(GetStateVar 'XKBMAP') ; SetStateVar 'LOCALES' $(GetStateVar 'LANG') ; } ## translations for user-facing strings ## readonly SWAP_SED_RX='/(\[part_auto-[a-z][a-z]\]=".*) [^ ]+ ([^ ]+\)")/{s//\1 '$((SWAP_MB / 1000))'GB \2/ ; h} ; ${x ; /./{x ; q0} ; x ; q1} ;' readonly SWAP_SED_ERR_MSG="SWAP_SED_RX failed on translations.sh.inc" readonly TRANSLATIONS_ERR_MSG="failed to source translations.sh.inc" if (( ! IN_CHROOT )) then # inject size of automatic swap partition into translations, then load the translations sed -i -E "${SWAP_SED_RX}" ./translations.sh.inc || ! echo "${SWAP_SED_ERR_MSG}" || exit 1 source ./translations.sh.inc || ! echo "${TRANSLATIONS_ERR_MSG}" || exit 1 fi # helper for install.sh SetLang() { Lang=$( (( ${TRANSLATIONS[$1]} )) && echo $1 || echo ${DEF_TRKEY} ) ; } # set initial installer language Lang='' ; SetLang $(GetStateVar 'TR_KEY' ${DEF_TRKEY}) ;