Mini Shell
# _input-output-functions.sh
#
# NOTE:
# This is the first file to be sourced (because of _ in the name) which is why
# it contains some special stuff like EXIT_TASKS that I want to be available everywhere.
# input-output functions for Relax-and-Recover
# plus some special stuff that should be available everywhere.
#
# This file is part of Relax-and-Recover, licensed under the GNU General
# Public License. Refer to the included COPYING for full text of license.
# The sequence $'...' is an special bash expansion with backslash-escaped characters
# see "Words of the form $'string' are treated specially" in "man bash"
# that works at least down to bash 3.1 in SLES10:
LF=$'\n'
# Keep PID of main process (i.e. the main script that the user had launched as 'rear'):
readonly MASTER_PID=$$
# Collect exit tasks in this array.
# Without the empty string as initial value ${EXIT_TASKS[@]} would be an unbound variable
# that would result an error exit if 'set -eu' is used:
EXIT_TASKS=("")
# Add $* as an exit task to be done at the end:
function AddExitTask () {
# NOTE: We add the task at the beginning to make sure that they are executed in reverse order.
# I use $* on purpose because I want to get one string from all args!
EXIT_TASKS=( "$*" "${EXIT_TASKS[@]}" )
Debug "Added '$*' as an exit task"
}
# Add $* as an exit task to be done at the end but do not output a debug message.
# TODO: I <jsmeix@suse.de> wonder why debug messages are suppressed at all?
# I.e. I wonder about the reason behind why QuietAddExitTask is needed?
function QuietAddExitTask () {
# NOTE: We add the task at the beginning to make sure that they are executed in reverse order.
# I use $* on purpose because I want to get one string from all args!
EXIT_TASKS=( "$*" "${EXIT_TASKS[@]}" )
}
# Remove $* from the exit tasks list:
function RemoveExitTask () {
local removed="" exit_tasks=""
for (( c=0 ; c<${#EXIT_TASKS[@]} ; c++ )) ; do
if test "${EXIT_TASKS[c]}" = "$*" ; then
# the ' ' protect from bash expansion, however unlikely to have a file named EXIT_TASKS in pwd...
unset 'EXIT_TASKS[c]'
removed=yes
Debug "Removed '$*' from the list of exit tasks"
fi
done
if ! test "$removed" = "yes" ; then
exit_tasks="$( for task in "${EXIT_TASKS[@]}" ; do echo "$task" ; done )"
Log "Could not remove exit task '$*' (not found). Exit Tasks: '$exit_tasks'"
fi
}
# Output PIDs of all descendant processes of a parent process PID (specified as $1)
# i.e. the parent and its direct children plus recursively all subsequent children of children
# (i.e. parent PID, children PIDs, grandchildren PIDs, great-grandchildren PIDs, and so on)
# where each PID is output on a separated line.
# Calling "ps --ppid $parent_pid -o pid=" recursively is needed
# because otherwise it does not work on all systems.
# E.g. on SLES10 and SLES11 it would work to simply call "ps -g $parent_pid -o pid="
# # sleep 20 | grep foo & ( sleep 30 | grep bar & ) ; sleep 1 ; ps f -g $$
# [1] 3622
# PID TTY STAT TIME COMMAND
# 3372 pts/0 Ss 0:00 -bash
# 3621 pts/0 S 0:00 \_ sleep 20
# 3622 pts/0 S 0:00 \_ grep foo
# 3627 pts/0 R+ 0:00 \_ ps f -g 3372
# 3625 pts/0 S 0:00 grep bar
# 3624 pts/0 S 0:00 sleep 30
# but this way it does no longer work e.g. on SLES12 or openSUSE Leap 42.3
# # sleep 20 | grep foo & ( sleep 30 | grep bar & ) ; sleep 1 ; ps f -g $$ ; ps --ppid $$ -o pid,args
# [1] 6518
# PID TTY STAT TIME COMMAND
# PID COMMAND
# 6517 sleep 20
# 6518 grep --color=auto foo
# 6524 ps --ppid 2674 -o pid,args
# where there is really no longer any output of the "ps f -g $$" command.
# Because of the recursion the output of the deepest nested call appears first
# so that it lists latest descendants PIDs first and the initial parent PID last
# (i.e. great-grandchildren PIDs, grandchildren PIDs, children PIDs, parent PID)
# so that the output ordering is already the right ordering to cleanly terminate
# a sub-tree of processes below a parent process and finally the parent process
# (i.e. first terminate great-grandchildren processes, then grandchildren processes,
# then children processes, and finally terminate the parent process itself).
# This termination functionality is used in the DoExitTasks() function.
function descendants_pids () {
local parent_pid=$1
# Successfully ignore PIDs that do not exist or do no longer exist:
kill -0 $parent_pid 2>/dev/null || return 0
# Recursively call this function for the actual children:
local child_pid=""
for child_pid in $( ps --ppid $parent_pid -o pid= ) ; do
# At least the sub-shell of the $( ps --ppid $parent_pid -o pid= )
# is always reported as a child_pid so that the following test avoids
# that descendants_pids is uselessly recursively called for it:
kill -0 $child_pid 2>/dev/null && descendants_pids $child_pid
done
# Only show PIDs that actually still exist which skips PIDs of children
# that were running a short time (like called programs by this function)
# and had already finished here:
kill -0 $parent_pid 2>/dev/null && echo $parent_pid || return 0
}
# Show descendant processes PIDs with their commands in the log
# so that later the plain PIDs in the log get more comprehensible
# (e.g. when terminate_descendants_pids is called afterwards):
function log_descendants_pids () {
# What works sufficiently on all systems is "pstree -Aplau MASTER_PID"
# but the pstree command is not available by default in the ReaR recovery system
# (cf. https://github.com/rear/rear/issues/1755) so that the ps command is used as fallback.
# Because "ps f -g MASTER_PID -o pid,args" only works on older systems like SLES10 and SLES11
# (cf. the above comment for the descendants_pids function)
# a last resort fallback "ps --ppid MASTER_PID -o pid,args" is used for newer systems like SLES12
# (at least on SLES12 "ps f -g MASTER_PID -o pid,args" results non-zero exit code when nothing is shown):
Log "$( pstree -Aplau $MASTER_PID || ps f -g $MASTER_PID -o pid,args || ps --ppid $MASTER_PID -o pid,args )"
}
# Terminate all still running descendant processes of MASTER_PID but do not terminate the MASTER_PID process itself.
# First terminate great-grandchildren processes, then grandchildren processes, then children processes.
# This termination functionality is used in the DoExitTasks() function.
function terminate_descendants_from_grandchildren_to_children () {
# Some descendant processes commands could be much too long (e.g. a 'tar ...' command)
# to be usefully shown completely in the below LogPrint information (could be many lines)
# so that the descendant process command output is truncated after at most remaining_columns.
# We reserve 40 characters for the log prefix and show at most 40 characters of the command.
# The shell variable COLUMNS is not defined in noninteractive bash, so we set a fallback
# cf. https://github.com/rear/rear/pull/1720#discussion_r328686592
local remaining_columns
test $COLUMNS && remaining_columns=$COLUMNS || remaining_columns=80
remaining_columns=$(( remaining_columns - 40 ))
test $remaining_columns -ge 40 || remaining_columns=40
# Terminate all still running descendant processes of MASTER_PID
# but do not terminate the MASTER_PID process itself because
# the MASTER_PID process must run the exit tasks below:
local descendant_pid=""
local not_yet_terminated_pids=""
# Send SIGTERM to all still running descendant processes of MASTER_PID:
for descendant_pid in $( descendants_pids $MASTER_PID ) ; do
# The descendant_pids() function outputs at least MASTER_PID
# plus the PID of the subshell of the $( descendants_pids MASTER_PID )
# so that it is tested that a descendant_pid is not MASTER_PID
# and that a descendant_pid is still running before SIGTERM is sent:
test $MASTER_PID -eq $descendant_pid && continue
kill -0 $descendant_pid || continue
LogPrint "Terminating descendant process $descendant_pid $( ps -p $descendant_pid -o args= | cut -b-$remaining_columns )"
kill -SIGTERM $descendant_pid 1>&2
# For each descendant process wait one second to let it terminate to be on the safe side
# that e.g. grandchildren can actually cleanly terminate before children get SIGTERM sent
# i.e. every child process can cleanly terminate before its parent gets SIGTERM:
sleep 1
if kill -0 $descendant_pid ; then
# Keep the current ordering also in not_yet_terminated_pids
# i.e. grandchildren before children:
not_yet_terminated_pids="$not_yet_terminated_pids $descendant_pid"
LogPrint "Descendant process $descendant_pid not yet terminated"
fi
done
# No need to kill a descendant processes if all were already terminated:
test "$not_yet_terminated_pids" || return 0
# Kill all not yet terminated descendant processes:
for descendant_pid in $not_yet_terminated_pids ; do
if kill -0 $descendant_pid ; then
LogPrint "Killing descendant process $descendant_pid $( ps -p $descendant_pid -o args= | cut -b-$remaining_columns )"
kill -SIGKILL $descendant_pid 1>&2
# For each killed descendant process wait one second to let it die to be on the safe side
# that e.g. grandchildren were actually removed by the kernel before children get SIGKILL sent
# i.e. every child process is already gone before its parent process may get SIGKILL so that
# the parent (that may wait for its child) has a better chance to still cleanly terminate:
sleep 1
kill -0 $descendant_pid && LogPrint "Killed descendant process $descendant_pid still there"
else
# Show a counterpart message to the above 'not yet terminated' message
# e.g. after a child process was killed its parent may have terminated on its own:
LogPrint "Descendant process $descendant_pid terminated"
fi
done
}
# Terminate all still running descendant processes of MASTER_PID but do not terminate the MASTER_PID process itself.
# First children processes, then grandchildren processes, then great-grandchildren processes.
# This termination functionality is used in the Error() function.
# The following code is basically the same as in terminate_descendants_from_grandchildren_to_children (see there for explanatory comments)
# except small but crucial differences here which is the reason why that kind of code exists two times.
function terminate_descendants_from_children_to_grandchildren () {
# Some descendant processes commands could be much too long (e.g. a 'tar ...' command):
local remaining_columns
test $COLUMNS && remaining_columns=$COLUMNS || remaining_columns=80
remaining_columns=$(( remaining_columns - 40 ))
test $remaining_columns -ge 40 || remaining_columns=40
# Terminate all still running descendant processes of MASTER_PID
# but do not terminate the MASTER_PID process itself because
# the MASTER_PID process must run the exit tasks below
# and do not terminate the current process that runs this code here.
local current_pid=""
local descendant_pid=""
local not_yet_terminated_pids=""
local descendant_pids_from_children_to_parent="$( descendants_pids $MASTER_PID )"
# Reverse the ordering of the PIDs to get them from parent to children:
local descendant_pids_from_parent_to_children=""
for descendant_pid in $descendant_pids_from_children_to_parent ; do
descendant_pids_from_parent_to_children="$descendant_pid $descendant_pids_from_parent_to_children"
done
# Send SIGTERM to all still running descendant processes of MASTER_PID
# except the current process that runs this code here which is usually MASTER_PID
# but this code here could be also run within a (possibly deeply nested) subshell:
if test "$BASHPID" ; then
current_pid=$BASHPID
else
# When there is no BASHPID we need to determine our current PID indirectly.
# Things like https://stackoverflow.com/questions/20725925/get-pid-of-current-subshell
# to get the current PID by calling a subshell like "( : ; bash -c 'echo $PPID' )"
# do not work when the current PID is already a (possibly deeply nested) subshell.
# Interestingly on command line "mypid=$( bash -c 'echo $PPID' )"
# works even in a nested subshell but it does no longer work when it is used in a sourced script.
# One way that works is that our current PID is the parent PID of a command that is called directly here
# (without any indirection via another subshell like "current_pid=$( whatever_command )" or when using a pipe)
# like "tmpfile=$( mktemp ) ; cat /proc/self/stat >$tmpfile ; current_pid=$( cut -d ' ' -f4 $tmpfile ) ; rm $tmpfile"
# but the simplest way is using the bash builtin 'read' to get our current PID directly from /proc/self/stat
# (our current PID is the first field in /proc/self/stat and our parent PID is the fourth field):
read current_pid junk </proc/self/stat
fi
for descendant_pid in $descendant_pids_from_parent_to_children ; do
# Test that a descendant_pid is not MASTER_PID or the current process that runs this code here
# and that a descendant_pid is still running before SIGTERM is sent:
test $MASTER_PID -eq $descendant_pid && continue
test $current_pid -eq $descendant_pid && continue
kill -0 $descendant_pid || continue
LogPrint "Terminating child process $descendant_pid $( ps -p $descendant_pid -o args= | cut -b-$remaining_columns )"
kill -SIGTERM $descendant_pid 1>&2
done
# In contrast to the terminate_descendants_from_grandchildren_to_children function above
# we do not wait here one second for each processes when it gets SIGTERM above
# because we send SIGTERM first to children then to grandchildren
# so that it does not make sense to give a grandchild one second
# to let it cleanly terminate before its paretnt child gets SIGTERM.
# Wait one second to let the above processes that got SIGTERM actually terminate
# before determining which did not yet terminate and should get a SIGKILL:
sleep 1
# Determine which of the above processes that got SIGTERM did not yet terminate
# except MASTER_PID and the current process that runs this code here:
for descendant_pid in $descendant_pids_from_parent_to_children ; do
test $MASTER_PID -eq $descendant_pid && continue
test $current_pid -eq $descendant_pid && continue
if kill -0 $descendant_pid ; then
# Keep the current ordering also in not_yet_terminated_pids
# i.e. children before grandchildren:
not_yet_terminated_pids="$not_yet_terminated_pids $descendant_pid"
LogPrint "Child process $descendant_pid not yet terminated"
fi
done
# No need to kill a descendant processes if all were already terminated:
test "$not_yet_terminated_pids" || return 0
# Kill all not yet terminated descendant processes that already got SIGTERM above:
for descendant_pid in $not_yet_terminated_pids ; do
if kill -0 $descendant_pid ; then
LogPrint "Killing child process $descendant_pid $( ps -p $descendant_pid -o args= | cut -b-$remaining_columns )"
kill -SIGKILL $descendant_pid 1>&2
else
# Show a counterpart message to the above 'not yet terminated' message:
LogPrint "Child process $descendant_pid terminated"
fi
done
# In contrast to the terminate_descendants_from_grandchildren_to_children function above
# we do not wait here one second for each processes when it gets SIGKILL above
# with the same reasoning behind as above where SIGTERM was sent.
# Wait one second the let the killed descendant processes actually die:
sleep 1
# Show which killed descendant processes are still there:
for descendant_pid in $not_yet_terminated_pids ; do
kill -0 $descendant_pid && LogPrint "Killed child process $descendant_pid still there"
done
}
# Do all exit tasks:
function DoExitTasks () {
# First of all restore the ReaR default bash flags and options (see usr/sbin/rear)
# because otherwise in case of a bash error exit when e.g. "set -e -u -o pipefail" was set
# all the exit tasks related code would also run with "set -e -u -o pipefail" still set
# which may abort exit tasks related code anywhere with a "sudden death" bash error exit
# where in particular no longer the EXIT_FAIL_MESSAGE (cf. below) would be shown
# so that for the user ReaR would "just somehow silently abort" in this case
# cf. https://github.com/rear/rear/issues/1747#issuecomment-371055121
# and https://github.com/rear/rear/issues/700#issuecomment-327755633
# To avoid useless 'set -x' debug output for the apply_bash_flags_and_options_commands call
# run it in the current shell environment where stderr is redirected to /dev/null before:
{ apply_bash_flags_and_options_commands "$DEFAULT_BASH_FLAGS_AND_OPTIONS_COMMANDS" ; } 2>/dev/null
# Apply debugscript mode also for the exit tasks:
test "$DEBUGSCRIPTS" && set -$DEBUGSCRIPTS_ARGUMENT
LogPrint "Exiting $PROGRAM $WORKFLOW (PID $MASTER_PID) and its descendant processes ..."
# Wait some time to let descendant processes terminate on their own
# e.g. after Ctrl+C by the user descendant processes should terminate on their own
# at least the "foreground processes" (with the current terminal process group ID)
# but "background processes" would not terminate on their own after Ctrl+C
# cf. https://github.com/rear/rear/issues/1712
# and also the Error function terminates descendant processes on its own via
# terminate_descendants_from_children_to_grandchildren that sleeps two times one second
# so that we wait here three seconds to be on the safe side that a possibly running
# terminate_descendants_from_children_to_grandchildren has done its job and finished
# to avoid that two functions run in parallel that terminate descendant processes:
sleep 3
# Show descendant processes PIDs with their commands in the log
# so that the plain PIDs in the log get more comprehensible
# when terminate_descendants_from_grandchildren_to_children is called afterwards:
log_descendants_pids
# Terminate all still running descendant processes of MASTER_PID
# but do not terminate the MASTER_PID process itself because
# the MASTER_PID process must run the exit tasks below:
terminate_descendants_from_grandchildren_to_children
# Finally run the exit tasks:
LogPrint "Running exit tasks"
local exit_task=""
for exit_task in "${EXIT_TASKS[@]}" ; do
Debug "Exit task '$exit_task'"
eval "$exit_task"
done
}
# The command (actually the function) DoExitTasks is executed on exit from the shell.
# Avoid SC2218 "This function is only defined later. Move the definition up."
# because it seems ShellCheck falsely thinks 'trap' is the below defined function
# (i.e. it seems ShellCheck does not recognize 'builtin')
# shellcheck disable=SC2218
builtin trap "DoExitTasks" EXIT
# Prepare that STDIN STDOUT and STDERR can be later redirected to anywhere
# (e.g. both STDOUT and STDERR can be later redirected to the log file).
# To be able to output on the original STDOUT and STDERR when 'rear' was launched and
# to be able to input (i.e. 'read') from the original STDIN when 'rear' was launched
# (which is usually the keyboard and display of the user who launched 'rear')
# the original STDIN STDOUT and STDERR file descriptors are saved as fd6 fd7 and fd8
# so that ReaR functions for actually intended user messages can use fd7 and fd8
# to show messages to the user regardless where to STDOUT and STDERR are redirected
# and fd6 to get input from the user regardless where to STDIN is redirected.
# Duplicate STDIN to fd6 to be used by 'read' in the UserInput function
# cf. http://tldp.org/LDP/abs/html/x17974.html
exec 6<&0
# Close fd6 when exiting:
QuietAddExitTask "exec 6<&-"
# Duplicate STDOUT to fd7 to be used by the Print and UserOutput functions:
exec 7>&1
# Close fd7 when exiting:
QuietAddExitTask "exec 7>&-"
# Duplicate STDERR to fd8 to be used by the PrintError function:
exec 8>&2
# Close fd8 when exiting:
QuietAddExitTask "exec 8>&-"
# TODO: I <jsmeix@suse.de> wonder if it is really needed to explicitly close stuff when exiting
# because during exit all open files (and file descriptors) should be closed automatically.
# Verbose exit in case of errors which is in particular needed when 'set -e' is active because
# otherwise a 'set -e' error exit would happen silently which could look as if all was o.k.
# cf. https://github.com/rear/rear/issues/700#issuecomment-327755633
# The separated EXIT_FAIL_MESSAGE variable is used to denote a failure exit.
# One cannot use EXIT_CODE for that because there are cases where a non-zero exit code
# is the intended outcome (e.g. in the 'checklayout' workflow, cf. usr/sbin/rear):
QuietAddExitTask "(( EXIT_FAIL_MESSAGE )) && echo '${MESSAGE_PREFIX}$PROGRAM $WORKFLOW failed, check $RUNTIME_LOGFILE for details' 1>&8"
# USR1 is used to abort on errors.
# It is not using PrintError but does direct output to the original STDERR.
# Set EXIT_FAIL_MESSAGE to 0 to avoid an additional failed message via the QuietAddExitTask above.
# Avoid SC2218 "This function is only defined later. Move the definition up."
# because it seems ShellCheck falsely thinks 'trap' is the below defined function
# (i.e. it seems ShellCheck does not recognize 'builtin')
# shellcheck disable=SC2218
builtin trap "EXIT_FAIL_MESSAGE=0 ; echo '${MESSAGE_PREFIX}Aborting due to an error, check $RUNTIME_LOGFILE for details' 1>&8 ; kill $MASTER_PID" USR1
# Make sure nobody else can use trap:
function trap () {
BugError "Forbidden usage of trap with '$*'. Use AddExitTask instead."
}
# For actually intended user messages output to the original STDOUT
# but only when the user launched 'rear -v' in verbose mode:
function Print () {
# It is crucial to append to /dev/$DISPENSABLE_OUTPUT_DEV when $DISPENSABLE_OUTPUT_DEV is not 'null'.
# In debugscript mode $DISPENSABLE_OUTPUT_DEV is 'stderr' (see usr/sbin/rear)
# and /dev/stderr is fd2 which is redirected to append to RUNTIME_LOGFILE (see usr/sbin/rear)
# so that 2>/dev/stderr would truncate RUNTIME_LOGFILE to zero size (see 'REDIRECTION' in "man bash")
# but 2>>/dev/stderr does not change things so that fd2 output is still appended to RUNTIME_LOGFILE:
{ test "$VERBOSE" && echo "${MESSAGE_PREFIX}$*" 1>&7 || true ; } 2>>/dev/$DISPENSABLE_OUTPUT_DEV
}
# For normal output messages that are intended for user dialogs.
# For error messages that are intended for the user use 'PrintError'.
# In contrast to the 'Print' function output to the original STDOUT
# regardless whether or not the user launched 'rear' in verbose mode
# but output to the original STDOUT without a MESSAGE_PREFIX because
# MESSAGE_PREFIX is not helpful in normal user dialog output messages:
function UserOutput () {
{ echo "$*" 1>&7 || true ; } 2>>/dev/$DISPENSABLE_OUTPUT_DEV
}
# For actually intended user error messages output to the original STDERR
# regardless whether or not the user launched 'rear' in verbose mode:
function PrintError () {
{ echo "${MESSAGE_PREFIX}$*" 1>&8 || true ; } 2>>/dev/$DISPENSABLE_OUTPUT_DEV
}
# For messages that should only appear in the log file output to the current STDERR
# because (usually) the current STDERR is redirected to the log file:
function Log () {
# RUNTIME_LOGFILE does not yet exists in case of early Error() in usr/sbin/rear
test -w "$RUNTIME_LOGFILE" || return 0
# Have a timestamp with nanoseconds precision in any case
# so that any subsequent Log() calls get logged with precise timestamps:
{ local timestamp=$( date +"%Y-%m-%d %H:%M:%S.%N " )
local prefix="${MESSAGE_PREFIX}${timestamp}"
# prefix_blanks has the printable characters in prefix replaced with blanks for indentation:
local prefix_blanks="$( tr '[:print:]' ' ' <<<"$prefix" )"
local message=""
local log_message=""
test $# -gt 0 && message="$*" || message="$( cat )"
# The first line of message is prefixed with MESSAGE_PREFIX and timestamp
# and all subsequent lines in message are indented by prefix_blanks
# via bash parameter expansion ${message//$LF/$LF$prefix_blanks}
# ${...} - interpret ... using parameter expansion
# message - name of the variable containing the content
# //... - replace all instances of ...
# $LF - the literal newline character (see 'LF' above)
# /... - replace with ...
# $LF$prefix_blanks - the literal newline character followed by the indentation blanks
# cf. https://superuser.com/questions/955935/how-can-i-replace-a-newline-with-its-escape-sequence
# that uses the literal newline character inline as in ${...//$'\n'/...}
# but that results partially wrong parameter expansion with bash version 3.1.17 in SLES10
# that seems to get somehow confused by the single quotes within parameter expansion:
# # MESSAGE_PREFIX="message prefix "
# # timestamp=$( date +"%Y-%m-%d %H:%M:%S.%N " )
# # message="$( echo -e 'fist line\nsecond line\nthird line')"
# # prefix="${MESSAGE_PREFIX}${timestamp}"
# # prefix_blanks="$( tr '[:print:]' ' ' <<<"$prefix" )"
# # log_message="${MESSAGE_PREFIX}${timestamp}${message//$'\n'/$'\n'$prefix_blanks}"
# # echo "$log_message"
# message prefix 2021-06-24 10:49:39.824719000 fist line'
# ' second line'
# ' third line
# so we use the LF variable (cf. how LF is set above)
# # LF=$'\n'
# # log_message="${MESSAGE_PREFIX}${timestamp}${message//$LF/$LF$prefix_blanks}"
# # echo "$log_message"
# message prefix 2021-06-24 10:49:39.824719000 fist line
# second line
# third line
# to make that parameter expansion also works with bash version 3.1.17 in SLES10:
log_message="${MESSAGE_PREFIX}${timestamp}${message//$LF/$LF$prefix_blanks}"
} 2>>/dev/$DISPENSABLE_OUTPUT_DEV
# Append the log message explicitly to the log file to ensure that intended log messages
# actually appear in the log file even inside { ... } 2>>/dev/$DISPENSABLE_OUTPUT_DEV
# e.g. as in { COMMAND || Log "COMMAND failed" ; } 2>>/dev/$DISPENSABLE_OUTPUT_DEV
# cf. the 2>>/dev/$DISPENSABLE_OUTPUT_DEV usage in the RequiredSharedObjects function
# and in build/GNU/Linux/100_copy_as_is.sh and build/GNU/Linux/390_copy_binaries_libraries.sh
echo "$log_message" >>"$RUNTIME_LOGFILE" || true
}
# For messages that should only appear in the log file when the user launched 'rear -d' in debug mode:
function Debug () {
test "$DEBUG" && Log "$@" || true
}
# For messages that should appear in the log file when the user launched 'rear -d' in debug mode and
# that also appear on the user's terminal (in debug mode the verbose mode is set automatically):
function DebugPrint () {
Debug "$@"
test "$DEBUG" && Print "$@" || true
}
# For messages that should appear in the log file and also
# on the user's terminal when the user launched 'rear -v' in verbose mode:
function LogPrint () {
Log "$@"
Print "$@"
}
# For output plus logging that is intended for user dialogs.
# 'LogUserOutput' belongs to 'UserOutput' like 'LogPrint' belongs to 'Print':
function LogUserOutput () {
Log "$@"
UserOutput "$@"
}
# For important messages that should appear in the log file and also
# on the user's terminal regardless whether or not the user launched 'rear' in verbose mode.
# LogPrintError does not error out (the Error function is meant to error out).
# LogPrintError is meant to show error messages when we do not want to error out,
# (for example when at the end of "rear recover" it failed to install a bootloader).
# Real error messages should be prefixed with 'Error: ' in the LogPrintError message.
# LogPrintError is also meant to show important "error-like" messages to the user
# (for example when the user must decide if that means a real error in his case)
# and other important messages that must appear on the user's terminal.
# In particular when there was already a LogPrintError message
# subsequent messages that are related to this LogPrintError message
# must also be shown to the user as LogPrintError messages
# to ensure the user gets them on his terminal regardless
# whether or not he launched 'rear' in verbose mode.
# Messages that are no real error messages should not be prefixed with 'Warning: '
# cf. https://blog.schlomo.schapiro.org/2015/04/warning-is-waste-of-my-time.html
function LogPrintError () {
Log "$@"
PrintError "$@"
}
# For messages that should only appear in the syslog:
function LogToSyslog () {
# Send a line to syslog or messages file with input string with the tag 'rear':
logger -t rear -i "${MESSAGE_PREFIX}$*"
}
# Check if any of the arguments is executable (logical OR condition).
# Using plain "type" without any option because has_binary is intended
# to know if there is a program that one can call regardless if it is
# an alias, builtin, function, or a disk file that would be executed
# see https://github.com/rear/rear/issues/729
function has_binary () {
for bin in "$@" ; do
# Suppress success output via stdout which is crucial when has_binary is called
# in other functions that provide their intended function results via stdout
# to not pollute intended function results with intermixed has_binary stdout
# (e.g. the RequiredSharedObjects function) but keep failure output via stderr:
type $bin 1>/dev/null && return 0
done
return 1
}
# Get the name of the disk file that would be executed.
# In contrast to "type -p" that returns nothing for an alias, builtin, or function,
# "type -P" forces a PATH search for each NAME, even if it is an alias, builtin,
# or function, and returns the name of the disk file that would be executed
# see https://github.com/rear/rear/issues/729
function get_path () {
type -P $1
}
# Output the source file of the actual caller script and its line number:
function CallerSource () {
# Get the source file of actual caller script.
# Usually this is ${BASH_SOURCE[1]} but CallerSource is also called
# from functions in this script like BugError and UserInput below
# and BugError is again called from BugIfError in this script.
# When BugIfError is called the actual caller is the script
# that had called BugIfError which is ${BASH_SOURCE[3]}
# because when BugIfError is called from a script
# ${BASH_SOURCE[0]} is '_input-output-functions.sh' for the CallerSource call
# ${BASH_SOURCE[1]} is '_input-output-functions.sh' for the BugError call
# ${BASH_SOURCE[2]} is '_input-output-functions.sh' for the BugIfError call
# ${BASH_SOURCE[3]} is the script that had called BugIfError.
# Currently it is sufficient to inspect the execution call stack up to ${BASH_SOURCE[3]}
# (i.e. currently there are at most three indirections as described above).
# With bash >= 3 the BASH_SOURCE array variable is supported and even
# for older bash it should be fail-safe when unset variables evaluate to empty:
local this_script="${BASH_SOURCE[0]}"
# Note the "off by one" for the BASH_LINENO array index because
# https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html
# reads (excerpt):
# ${BASH_LINENO[$i]} is the line number in the source file (${BASH_SOURCE[$i+1]}) where ${FUNCNAME[$i]} was called
# (or ${BASH_LINENO[$i-1]} if referenced within another shell function). Use LINENO to obtain the current line number.
local caller_source="${BASH_SOURCE[1]}"
local caller_source_lineno="${BASH_LINENO[0]}"
if test "$caller_source" = "$this_script" ; then
caller_source="${BASH_SOURCE[2]}"
caller_source_lineno="${BASH_LINENO[1]}"
fi
if test "$caller_source" = "$this_script" ; then
caller_source="${BASH_SOURCE[3]}"
caller_source_lineno="${BASH_LINENO[2]}"
fi
if test "$caller_source" ; then
echo "$caller_source line $caller_source_lineno"
return 0
fi
# Fallback output:
echo "Relax-and-Recover"
}
# Error exit:
# It is an Error when the cause is not in ReaR's code
# for example when the user specified something wrong
# or when a called program exits with a fatal error.
function Error () {
# Get the last sourced script out of the log file:
# Using the CallerSource function is not sufficient here because CallerSource results
# the file where this Error function is called which can also be a lib/*-functions.sh
# but showing *-functions.sh would not be as helpful for the user as the last actual script.
# Each sourced script gets logged as 'timestamp Including sub-path/to/script_file_name.sh' and
# valid script files names are of the form NNN_script_name.sh (i.e. with leading 3-digit number)
# but also the outdated scripts with leading 2-digit number get sourced
# see the SourceStage function in lib/framework-functions.sh
# so that we grep for script files names with two or more leading numbers:
if test -s "$RUNTIME_LOGFILE" ; then
{ local last_sourced_script_log_entry=( $( grep -o ' Including .*/[0-9][0-9].*\.sh' $RUNTIME_LOGFILE | tail -n 1 ) )
# The last_sourced_script_log_entry contains: Including sub-path/to/script_file_name.sh
local last_sourced_script_sub_path="${last_sourced_script_log_entry[1]}"
local last_sourced_script_filename="$( basename $last_sourced_script_sub_path )"
# When it errors out in sbin/rear last_sourced_script_filename is empty which would result bad looking output
# cf. https://github.com/rear/rear/issues/1965#issuecomment-439437868
test "$last_sourced_script_filename" || last_sourced_script_filename="$SCRIPT_FILE"
} 2>>/dev/$DISPENSABLE_OUTPUT_DEV
fi
# Do not log the error message right now but after the currently last log messages were shown:
PrintError "ERROR: $*"
# Show some additional hopefully meaningful output on the user's terminal
# (no need to log that again here because it is already in the log file)
# in particular the normal stdout and stderr messages of the last called programs
# to make the root cause more obvious to the user without the need to analyze the log file
# cf. https://github.com/rear/rear/issues/1875#issuecomment-407039065
# Extract lines starting when the last script was sourced (logged as 'Including sub-path/to/script.sh')
# but do not use last_sourced_script_sub_path because it contains '/' characters that let sed fail with
# sed: -e expression #1, char ...: extra characters after command
# because the '/' characters would need to be escaped in the sed expression so that
# we simply use last_sourced_script_filename in the sed expression.
# Extract at most up to a line that is usually logged as '++ Error ...' or '++ BugError ...'
# (but do not stop at lines that are logged like '++ StopIfError ...' or '++ PrintError ...')
# if such a '+ Error' or '+ BugError' line exists, otherwise sed proceeds to the end
# (the sed pattern '[Bug]*Error' is fuzzy because it would also match things like 'uuggError').
# The reason to stop at a line that contains '+ [Bug]*Error ' is that in debugscript mode '-D'
# a BugError or Error function call with a multi line error message (e.g. BugError does that)
# results 'set -x' debug output of that function call in the log file that looks like:
# ++ [Bug]Error 'first error message line
# second error message line
# third error message line
# ...
# last error message line'
# Because of the newlines in the error message subsequent lines appear without a leading '+' character
# so that those debug output lines are indistinguishable from normal stdout/stderr output of programs,
# cf. https://github.com/rear/rear/pull/1877
# Thereafter ('+ [Bug]*Error ' lines were needed before) skip 'set -x' lines (lines that start with a '+' character)
# and skip the initial 'Including sub-path/to/script.sh' line that is always found
# to keep only the actual stdout and stderr messages of the last called programs
# so we can test if messages were actually found via 'test "string of messages"' for emptiness.
# Show at most the last 8 lines because too much before the actual error may cause more confusion than help.
# Add two spaces indentation for better readability what those extracted log file lines are.
# Some messages could be too long to be usefully shown on the user's terminal so that they are truncated after 200 bytes:
if test -s "$RUNTIME_LOGFILE" ; then
{ local last_sourced_script_log_messages="$( sed -n -e "/Including .*$last_sourced_script_filename/,/+ [Bug]*Error /p" $RUNTIME_LOGFILE | egrep -v "^\+|Including .*$last_sourced_script_filename" | tail -n 8 | sed -e 's/^/ /' | cut -b-200 )" ; } 2>>/dev/$DISPENSABLE_OUTPUT_DEV
if test "$last_sourced_script_log_messages" ; then
PrintError "Some latest log messages since the last called script $last_sourced_script_filename:"
PrintError "$last_sourced_script_log_messages"
fi
fi
# In non-debug modes stdout and stderr are redirected to STDOUT_STDERR_FILE="$TMP_DIR/rear.$WORKFLOW.stdout_stderr" if possible
# but in certain cases (e.g. for the 'help' workflow where no $TMP_DIR exists) STDOUT_STDERR_FILE=/dev/null
# so we extract some latest messages only if STDOUT_STDERR_FILE is a regular file:
if test -f "$STDOUT_STDERR_FILE" ; then
# We use the same extraction pipe as above because STDOUT_STDERR_FILE may also contain 'set -x' and things like that
# because scripts could use 'set -x' and things like that as needed (e.g. diskrestore.sh runs with 'set -x'):
{ local last_sourced_script_stdout_stderr_messages="$( sed -n -e "/Including .*$last_sourced_script_filename/,/+ [Bug]*Error /p" $STDOUT_STDERR_FILE | egrep -v "^\+|Including .*$last_sourced_script_filename" | tail -n 8 | sed -e 's/^/ /' | cut -b-200 )" ; } 2>>/dev/$DISPENSABLE_OUTPUT_DEV
if test "$last_sourced_script_stdout_stderr_messages" ; then
# When stdout and stderr are redirected to STDOUT_STDERR_FILE messages of the last called programs cannot be in the log
# so we use LogPrintError and 'echo "string of messages" >>$RUNTIME_LOGFILE' (the latter avoids the timestamp prefix)
# to have the extracted messages stored in the log so that they are later available (in contrast to terminal output).
# The full stdout and stderr messages are available in STDOUT_STDERR_FILE:
LogPrintError "Some messages from $STDOUT_STDERR_FILE since the last called script $last_sourced_script_filename:"
PrintError "$last_sourced_script_stdout_stderr_messages"
echo "$last_sourced_script_stdout_stderr_messages" >>"$RUNTIME_LOGFILE"
fi
fi
# Show some generic info about debugging:
if test "$DEBUG" ; then
# We are in debug mode but not in debugscript mode:
test "$DEBUGSCRIPTS" || PrintError "You may use debugscript mode '-D' for full debug messages with 'set -x' output"
else
# We are not in debug mode:
PrintError "Use debug mode '-d' for some debug messages or debugscript mode '-D' for full debug messages with 'set -x' output"
fi
# Log the error message:
Log "ERROR: $*"
LogToSyslog "ERROR: $*"
# Print stack strace in reverse order to the current STDERR which is (usually) the log file:
( echo "===== ${MESSAGE_PREFIX}Stack trace ====="
local c=0;
while caller $((c++)) ; do
:
done | awk ' { l[NR]=$3":"$1" "$2 }
END { for (i=NR; i>0;) print "Trace "NR-i": "l[i--] }
'
echo "=== ${MESSAGE_PREFIX}End stack trace ==="
) 1>&2
# Make sure Error exits the master process, even if called from child processes.
# We must send USR1 to MASTER_PID before we terminate all still running descendant processes of MASTER_PID below
# because when the Error function is called from a subshell we are one of those still running descendant processes:
kill -USR1 $MASTER_PID
# That USR1 has a trap (see above) that does 'kill MASTER_PID' whicht triggers another trap on EXIT that calls DoExitTasks().
# When the Error function is called from within a subshell (cf. layout/save/GNU/Linux/230_filesystem_layout.sh) like
# ( echo "additional content for file" || Error "failed to append content to file" ) >> file
# the Error function does not let MASTER_PID exit because the parent shell waits until its subshell has finished
# so that the USR1 that was sent above to MASTER_PID will be processed only after the subshell has finished
# cf. https://github.com/rear/rear/issues/2089#issuecomment-474260332 that reads (excerpts)
# A nice clean reproducer on plain command line (needs a recent bash that supports BASHPID):
# # export MASTERPID=$BASHPID
# # trap "echo $MASTERPID got USR1" USR1
# # ( echo begin subshell $BASHPID parent $MASTERPID
# pstree -Aplau $MASTERPID
# kill -USR1 $MASTERPID
# echo sent USR1 to $MASTERPID in subshell
# echo other stuff in subshell
# echo subshell done )
# Running that reproducer results:
# begin subshell 26109 parent 26108
# bash,26108
# `-bash,26109
# `-pstree,26110 -Aplau 26108
# sent USR1 to 26108 in subshell
# other stuff in subshell
# subshell done
# 26108 got USR1
# It shows that the parent waits until its subshell child has finished and then the parent processes the signal.
# This behaviour matches what "man bash" reads for "SIGNALS":
# If bash is waiting for a command to complete
# and receives a signal for which a trap has been set,
# the trap will not be executed until the command completes.
# This means when the Error function is called from within a subshell only USR1 is sent to MASTER_PID
# and the subshell continues with all its code after the Error function until the subshell finishes.
# This would result unintendedly executed code (with all its unexpected messages in the log file) and
# also further Error function calls with error messages on the user's terminal from subsequent failures
# after the initial error, e.g. see https://github.com/rear/rear/issues/2087#issue-421604286 that shows
# ERROR: Partition number '0' of partition mmcblk0boot0 is not a valid number.
# ERROR: Partition number '' of partition mmcblk0rpmb is not a valid number.
# ERROR: Partition mmcblk0rpmb is numbered ''. More than 128 partitions is not supported.
# Aborting due to an error, check /var/log/rear/rear-testvm02.log for details
# where only the first error message should have been shown and a direct abort should have happened.
# This is the reason why we have to terminate all still running descendant processes of MASTER_PID
# but do not terminate the MASTER_PID process itself because the MASTER_PID process must run
# the exit tasks via DoExitTasks via trap on EXIT via trap on USR1 (see above).
# How to cleanly error out from within a lower level of nested subshells as in this code:
# ( LogPrint "Begin first subshell"
# ( LogPrint "Begin second subshell"
# Error "First error"
# Error "Second error"
# LogPrint "End second subshell"
# )
# LogPrint "Code in first subshell after second subshell"
# LogPrint "End of first subshell"
# )
# It should error out at "First error" and not execute any code after that.
# If we terminate the second subshell here (i.e. the one that currently runs this 'Error "First error"' function)
# we could avoid that the second subshell unintendedly continues and runs the 'Error "Second error"' function
# but its parent (i.e. the first subshell that has waited for its second subshell child to finish)
# would then continue and unintendedly execute the "Code in first subshell after second subshell".
# Therefore we terminate all still running processes (except MASTER_PID) starting with children to grandchildren
# so that we terminate first the first subshell and then the second subshell.
# This way when the second subshell gets terminated its parent was already terminated
# so that in the end there will be no unintendedly executed code after the "First error".
# The following code is basically the same as in DoExitTasks (see there for explanatory comments)
# except small but crucial differences here which is the reason why that kind of code exists two times.
# First of all restore the ReaR default bash flags and options of MASTER_PID (i.e. of usr/sbin/rear):
{ apply_bash_flags_and_options_commands "$DEFAULT_BASH_FLAGS_AND_OPTIONS_COMMANDS" ; } 2>/dev/null
# Keep debugscript mode also here if it was used before:
test "$DEBUGSCRIPTS" && set -$DEBUGSCRIPTS_ARGUMENT
LogPrint "Error exit of $PROGRAM $WORKFLOW (PID $MASTER_PID) and its descendant processes"
# Show descendant processes PIDs with their commands in the log
# so that the plain PIDs in the log get more comprehensible
# when terminate_descendants_from_children_to_grandchildren is called afterwards:
log_descendants_pids
# Terminate all still running descendant processes of MASTER_PID
# but do not terminate the MASTER_PID process itself because
# the MASTER_PID process must run the exit tasks via DoExitTasks (see above)
# and do not terminate the current process that runs this code here
# because the terminate_descendants_from_children_to_grandchildren function
# should run to its end because it may have to kill descendant processes:
terminate_descendants_from_children_to_grandchildren
# Now only the process that runs this code here is left.
# If that process is MASTER_PID all is o.k. but if that process is run within a subshell
# we must not return here from the Error funtion to its caller because that would let
# the subshell continue with all its code after the Error function until the subshell finishes
# so that if we are in a subshell here we exit from that subshell here:
if test $BASH_SUBSHELL -gt 0 ; then
LogPrint "Exiting subshell $BASH_SUBSHELL (where the actual error happened)"
test $EXIT_CODE -gt 1 && exit $EXIT_CODE || exit 1
fi
}
# Exit if there is a bug inside ReaR:
# It is a BugError when the cause is in ReaR's code,
# for example when a ReaR function is called with wrong
# or missing required parameters and things like that.
function BugError () {
{ local caller_source="$( CallerSource )" ; } 2>>/dev/$DISPENSABLE_OUTPUT_DEV
Error "
====================
BUG in $caller_source:
'$*'
--------------------
Please report it at $BUG_REPORT_SITE
and include all related parts from $RUNTIME_LOGFILE
preferably the whole debug information via 'rear -D $WORKFLOW'
===================="
}
# Using the ...IfError functions can result unexpected behaviour in certain cases.
#
# Using $? in an ...IfError function message like
# COMMAND
# StopIfError "COMMAND failed with error code $?"
# lets $? evaluate to an unintended value (usually 0) for example as in
# # cat QQQ ; if (( $? != 0 )) ; then echo "ERROR $?" ; fi
# cat: QQQ: No such file or directory
# ERROR 0
# In contrast using bash directly as in
# # cat QQQ || echo "ERROR $?"
# cat: QQQ: No such file or directory
# ERROR 1
# works as expected.
#
# The ...IfError functions fail when 'set -e' is set
# cf. https://github.com/rear/rear/issues/534
# for example code like
# set -e
# COMMAND
# StopIfError "COMMAND failed"
# cannot work because 'set -e' exits the script when COMMAND results non-zero exit code
# so that the subsequent StopIfError is never reached.
# In contrast using bash directly as in
# set -e
# COMMAND || Error "COMMAND failed"
# works as expected.
#
# The ...IfError functions fail when $( COMMAND )
# command substitution is used in the ...IfError functions message
# cf. https://github.com/rear/rear/issues/1415#issuecomment-315692391
# for example code like
# COMMAND1
# StopIfError "... $( COMMAND2 ) ..."
# cannot work because $? that is tested in the ...IfError functions
# will be the one from COMMAND2 and not the one from COMMAND1
# so that StopIfError errors out when COMMAND2 fails but not when COMMAND1 fails.
# In contrast using bash directly as in
# COMMAND1 || Error "... $( COMMAND2 ) ..."
# works as expected when $? is not used in the Error function message.
# When $? should be used in the Error function message it must be before $( COMMAND2 ).
# When $? is before $( COMMAND2 ) it evaluates to the exit code of COMMAND1.
# When $? is after $( COMMAND2 ) it evaluates to the exit code of COMMAND2.
# At least with bash-4.4 in openSUSE Leap 15.1 one gets
# # cat QQQ || echo "ERROR $? $( grep -Q '/' /etc/fstab ) $?"
# cat: QQQ: No such file or directory
# grep: invalid option -- 'Q'
# Usage: grep [OPTION]... PATTERN [FILE]...
# Try 'grep --help' for more information.
# ERROR 1 2
# when COMMAND1 fails with exit code 1 and COMMAND2 fails with exit code 2
# versus
# # cat QQQ || echo "ERROR $? $( grep -q '/' /etc/fstab ) $?"
# cat: QQQ: No such file or directory
# ERROR 1 0
# when COMMAND1 fails with exit code 1 and COMMAND2 succeeds.
# If return code is non-zero, bail out:
function StopIfError () {
if (( $? != 0 )) ; then
Error "$@"
fi
}
# If return code is non-zero, there is a bug in ReaR:
function BugIfError () {
if (( $? != 0 )) ; then
BugError "$@"
fi
}
# Show the user if there is an error:
function PrintIfError () {
# If return code is non-zero, show that on the user's terminal
# regardless whether or not the user launched 'rear' in verbose mode:
if (( $? != 0 )) ; then
PrintError "$@"
fi
}
# Log if there is an error;
function LogIfError () {
if (( $? != 0 )) ; then
Log "$@"
fi
}
# Log if there is an error and also show it to the user:
function LogPrintIfError () {
# If return code is non-zero, show that on the user's terminal
# regardless whether or not the user launched 'rear' in verbose mode:
if (( $? != 0 )) ; then
LogPrintError "$@"
fi
}
function cleanup_build_area_and_end_program () {
# Cleanup build area
local mounted_in_BUILD_DIR
Log "Finished $PROGRAM $WORKFLOW in $(( $( date +%s ) - START_SECONDS )) seconds"
# is_true is in lib/global-functions.sh which is not yet sourced in case of early Error() in usr/sbin/rear
if has_binary is_true && is_true "$KEEP_BUILD_DIR" ; then
mounted_in_BUILD_DIR="$( mount | grep "$BUILD_DIR" | sed -e 's/^/ /' )"
if test "$mounted_in_BUILD_DIR" ; then
LogPrintError "Caution - there is something mounted within the build area"
LogPrintError "$mounted_in_BUILD_DIR"
LogPrintError "You must manually umount that before you may remove the build area"
fi
# Show this message also inside the recovery system (e.g. at the end of "rear -D recover")
# because there may be a reason why manually removing the build area is wanted
# (e.g. some additional manual things need be done before rebooting).
# In any case one must be careful if one wants to remove the build area because
# e.g. the NFS share with the backup.tar.gz may still be erroneously mounted therein.
LogPrint "To remove the build area you may use (with caution): rm -Rf --one-file-system $BUILD_DIR"
else
Log "Removing build area $BUILD_DIR"
# Use '--one-file-system' to be safe against also deleting by accident
# all mounted things below mountpoints in TMP_DIR or ROOTFS_DIR
# (regardless if mountpoints in TMP_DIR or ROOTFS_DIR may happen):
rm -Rf --one-file-system $TMP_DIR || LogPrintError "Failed to 'rm -Rf --one-file-system $TMP_DIR'"
rm -Rf --one-file-system $ROOTFS_DIR || LogPrintError "Failed to 'rm -Rf --one-file-system $ROOTFS_DIR'"
# Before removing BUILD_DIR check that outputfs is gone (i.e. check that nothing is mounted there):
if mountpoint -q "$BUILD_DIR/outputfs" ; then
# If still mounted wait a bit (perhaps some ongoing umount needs more time) then try lazy umount:
sleep 2
# umount_mountpoint_lazy is in lib/global-functions.sh
# which is not yet sourced in case of early Error() in usr/sbin/rear
has_binary umount_mountpoint_lazy && umount_mountpoint_lazy $BUILD_DIR/outputfs
fi
# remove_temporary_mountpoint is in lib/global-functions.sh
# which is not yet sourced in case of early Error() in usr/sbin/rear
if has_binary remove_temporary_mountpoint ; then
# It is a bug in ReaR if BUILD_DIR/outputfs was not properly umounted and made empty by the scripts before:
remove_temporary_mountpoint "$BUILD_DIR/outputfs" || BugError "Directory $BUILD_DIR/outputfs not empty, cannot remove"
fi
if ! rmdir $v "$BUILD_DIR" ; then
LogPrintError "Could not remove build area $BUILD_DIR (something still exists therein)"
mounted_in_BUILD_DIR="$( mount | grep "$BUILD_DIR" | sed -e 's/^/ /' )"
if test "$mounted_in_BUILD_DIR" ; then
LogPrintError "Something is still mounted within the build area"
LogPrintError "$mounted_in_BUILD_DIR"
LogPrintError "You must manually umount it, then you could manually remove the build area"
fi
LogPrintError "To manually remove the build area use (with caution): rm -Rf --one-file-system $BUILD_DIR"
fi
fi
Log "End of program '$PROGRAM' reached"
}
# UserInput is a general function that is intended for basically any user input.
# Output happens via the original STDOUT and STDERR when 'rear' was launched
# (which is usually the terminal of the user who launched 'rear') and
# input is read from the original STDIN when 'rear' was launched
# (which is usually the keyboard of the user who launched 'rear').
# Synopsis:
# UserInput -I user_input_ID [-C] [-r] [-s] [-t timeout] [-p prompt] [-a input_words_array_name] [-n input_max_chars] [-d input_delimiter] [-D default_input] [choices]
# The options -r -s -t -p -a -n -d match the ones for the 'read' bash builtin.
# The option [choices] are the values that are shown to the user as available choices like if a 'select' bash keyword was used.
# The option [-D default_input] specifies what is used as default response when the user does not enter something.
# Usually this is one of the choice values or one of the a choice numbers '1' '2' '3' ...
# that are shown to the user (the choice numbers are shown as in 'select' (i.e. starting at 1)
# but the default input can be anything else (in particular for free input without predefined choices)
# so that e.g. '-D 0' is not the first choice but lets the default input be '0' (regardles of choices).
# The option '-I user_input_ID' is required so that UserInput can work full automated (e.g. when ReaR runs unattended)
# via user-specified variables that get named USER_INPUT_user_input_ID (i.e. prefixed with 'USER_INPUT_')
# so that the user can (as he needs it) predefine user input values like
# USER_INPUT_FOO_CONFIRMATION='input for UserInput -I FOO_CONFIRMATION'
# USER_INPUT_BAR_CHOICE='input for UserInput -I BAR_CHOICE'
# USER_INPUT_BAZ_DIALOG='input for UserInput -I BAZ_DIALOG'
# (with actually meaningful words for FOO, BAR, and BAZ)
# that will be autoresponded with the value of the matching USER_INPUT_user_input_ID variable.
# Accordingly only a valid variable name can be used as user_input_ID value.
# Different UserInput calls must use different '-I user_input_ID' option values but
# same UserInput calls in different scripts can use same '-I user_input_ID' option values.
# It is recommended to use meaningful and explanatory user_input_ID values
# which helps the user to specify automated input via meaningful USER_INPUT_user_input_ID variables
# and it avoids that different UserInput calls accidentally use same user_input_ID values.
# It is required to use uppercase user_input_ID values because the USER_INPUT_user_input_ID variables
# are user configuration variables and all user configuration variables have uppercase letters.
# The option [-C] specifies confidential user input mode. In this mode no input values are logged.
# This means that neither the actual user input nor the default input nor the choices values are logged but
# the prompt, the actual input, the default value, and the choices are still shown on the user's terminal.
# In confidential user input mode the actual input coming from the user's terminal is still echoed
# on the user's terminal unless also the -s option is specified.
# When usr/sbin/rear is run in debugscript mode (which runs the scripts with 'set -x') arbitrary values
# appear in the log file so that the confidential user input mode does not help in debugscript mode.
# If confidential user input is needed also in debugscript mode the caller of the UserInput function
# must call it in an appropriate (temporary) environment e.g. with STDERR redirected to /dev/null like
# { password="$( UserInput -I PASSWORD -C -r -s -p 'Enter the pasword' )" ; } 2>/dev/null
# The redirection must be done via a compound group command { confidential_command ; } 2>/dev/null
# even for a single confidential command to ensure STDERR is redirected to /dev/null also for 'set -x'
# otherwise the confidential command and its arguments would be shown in the log file, for example
# { openssl des3 -salt -k secret_passphrase ; } 2>/dev/null
# where the secret passphrase must not appear in the log, cf. https://github.com/rear/rear/issues/2155
# Result:
# Any actual user input or an automated user input or the default response is output via STDOUT.
# Return code:
# The UserInput return code is the return code of the 'read' bash builtin that is called to get user input.
# When the UserInput function is called with right syntax its return code is 0
# for any actual user input and in case of any (non empty) automated user input.
# The return code is 1 when the 'read' call timed out (i.e. when there was no actual user input)
# so that one can distinguish between an explicitly provided user input and no actual user input
# even if the explicitly provided user input is the same as the default so that it makes a difference
# whether or not the user explicitly chose and confirmed that the default is what he actually wants
# or if he let things "just happen" inattentively via timeout where it is important to have a big timeout
# so that an attentive user will actively provide user input to proceed even if it is same as the default.
# Usage examples:
# * Wait endlessly until the user hits the [Enter] key (without '-t 0' a default timeout is used):
# UserInput -I WAIT_UNTIL_ENTER -t 0 -p 'Press [Enter] to continue'
# * Wait up to 30 seconds until the user hits the [Enter] key (i.e. proceed automatically after 30 seconds):
# UserInput -I WAIT_FOR_ENTER_OR_TIMEOUT -t 30 -p 'Press [Enter] to continue'
# * Get an input value from the user (proceed automatically with empty input_value after the default timeout).
# Leading and trailing spaces are cut from the actual user input:
# input_value="$( UserInput -I FOO_INPUT -p 'Enter the input value' )"
# * Get an input value from the user (proceed automatically with the 'default input' after 2 minutes).
# The timeout interrupts ongoing user input so that 'default input' is used when the user
# does not hit the [Enter] key to finish his input before the timeout happens:
# input_value="$( UserInput -I FOO_INPUT -t 120 -p 'Enter the input value' -D 'default input' )"
# * Get an input value from the user by offering him possible choices (proceed with the default choice after the default timeout).
# The shown choice numbers start with 1 so that '-D 2' specifies the second choice as default choice:
# input_value="$( UserInput -I BAR_CHOICE -p 'Select a choice' -D 2 'first choice' 'second choice' 'third choice' )"
# * When the user enters an arbitrary value like 'foo bar' this actual user input is used as input_value.
# The UserInput function provides the actual user input and its caller needs to check the actual user input.
# To enforce that the actual user input is one of the choices an endless retrying loop could be used like:
# choices=( 'first choice' 'second choice' 'third choice' )
# until IsInArray "$input_value" "${choices[@]}" ; do
# input_value="$( UserInput -I BAR_CHOICE -p 'Select a choice' -D 'second choice' "${choices[@]}" )"
# done
# Because the default choice is one of the choices the endless loop does not contradict that ReaR can run unattended.
# When that code runs unattended (i.e. without actual user input) the default choice is used after the default timeout.
# * The default choice can be anything as in:
# input_value="$( UserInput -I BAR_CHOICE -p 'Select a choice' -D 'fallback value' -n 1 'first choice' 'second choice' 'third choice' )"
# The caller needs to check the actual input_value which could be 'fallback value' when the user hits the [Enter] key
# or one of 'first choice' 'second choice' 'third choice' when the user hits the [1] [2] or [3] key respectively
# or any other character as actual user input ('-n 1' limits the actual user input to one single character).
# * When up to 9 possible choices are shown using '-n 1' lets the user choose one by only pressing a [1] ... [9] key
# without the additional [Enter] key that is normally needed to submit the input. With an endless loop that retries
# when the actual user input is not one of the choices it is possible to implement valid and convenient user input:
# choices=( 'default choice' 'first alternative choice' 'second alternative choice' )
# until IsInArray "$choice" "${choices[@]}" ; do
# choice="$( UserInput -I BAZ_CHOICE -t 60 -p 'Hit a choice number key' -D 1 -n 1 "${choices[@]}" )"
# done
# * To to let UserInput autorespond full automated a predefined user input value specify the user input value
# with a matching USER_INPUT_user_input_ID variable (e.g. specify that it in your local.conf file) like
# USER_INPUT_BAR_CHOICE='third choice'
# which lets a 'UserInput -I BAR_CHOICE' call autorespond with 'third choice'.
# No USER_INPUT_BAR_CHOICE variable should exist to get real user input for a 'UserInput -I BAR_CHOICE' call
# or the user can interupt any automated response within a relatively short time (minimum is only 1 second).
function UserInput () {
# First and foremost log that UserInput was called (but be confidential here):
local caller_source="$( CallerSource )"
Log "UserInput: called in $caller_source"
# Set defaults or fallback values:
# Have a relatively big default timeout of 5 minutes to avoid that the timeout interrupts ongoing user input:
local timeout=300
# Avoid stderr if USER_INPUT_TIMEOUT is not set or empty and ignore wrong USER_INPUT_TIMEOUT:
test "$USER_INPUT_TIMEOUT" -ge 0 2>/dev/null && timeout=$USER_INPUT_TIMEOUT
# Have some seconds (at least one second) delay when an automated user input is used to be fail-safe against
# a possibly false specified predefined user input value for an endless retrying loop of UserInput calls
# that would (without the delay) run in a tight loop that wastes resources (CPU, diskspace, and memory)
# and fills up the ReaR log file (and the disk - which is a ramdisk for 'rear recover')
# with some KiB data each second that may let 'rear recover' fail with 'out of diskspace/memory'.
# The default automated input interrupt timeout is 5 seconds to give the user a reasonable chance
# to recognize the right automated input on his screen and interrupt it when needed:
local automated_input_interrupt_timeout=5
# Avoid stderr if USER_INPUT_INTERRUPT_TIMEOUT is not set or empty and ignore wrong USER_INPUT_INTERRUPT_TIMEOUT:
test "$USER_INPUT_INTERRUPT_TIMEOUT" -ge 1 2>/dev/null && automated_input_interrupt_timeout=$USER_INPUT_INTERRUPT_TIMEOUT
local default_prompt="enter your input"
local prompt="$default_prompt"
# Avoid stderr if USER_INPUT_PROMPT is not set or empty:
test "$USER_INPUT_PROMPT" 2>/dev/null && prompt="$USER_INPUT_PROMPT"
local input_words_array_name=""
local input_max_chars=0
# Avoid stderr if USER_INPUT_MAX_CHARS is not set or empty and ignore useless '0' or wrong USER_INPUT_MAX_CHARS:
test "$USER_INPUT_MAX_CHARS" -ge 1 2>/dev/null && input_max_chars=$USER_INPUT_MAX_CHARS
local input_delimiter=""
local default_input=""
local user_input_ID=""
local confidential_mode="no"
local raw_input="no"
local silent_input="no"
# Get the options and their arguments:
local option=""
# Resetting OPTIND is necessary if getopts was used previously in the script
# and because we are in a function we can even make OPTIND local:
local OPTIND=1
while getopts ":t:p:a:n:d:D:I:Crs" option ; do
case $option in
(t)
# Avoid stderr if OPTARG is not set or empty or not an integer value:
test "$OPTARG" -ge 0 2>/dev/null && timeout=$OPTARG || Log "UserInput: Invalid -$option argument '$OPTARG' using fallback '$timeout'"
;;
(p)
prompt="$OPTARG"
;;
(a)
input_words_array_name="$OPTARG"
;;
(n)
# Avoid stderr if OPTARG is not set or empty or not an integer value:
test "$OPTARG" -ge 0 2>/dev/null && input_max_chars=$OPTARG || Log "UserInput: Invalid -$option argument '$OPTARG' using fallback '$input_max_chars'"
;;
(d)
input_delimiter="$OPTARG"
;;
(D)
default_input="$OPTARG"
;;
(I)
user_input_ID="$OPTARG"
;;
(C)
confidential_mode="yes"
;;
(r)
raw_input="yes"
;;
(s)
silent_input="yes"
;;
(\?)
BugError "UserInput: Invalid option: -$OPTARG"
;;
(:)
BugError "UserInput: Option -$OPTARG requires an argument"
;;
esac
done
test $user_input_ID || BugError "UserInput: Option '-I user_input_ID' required"
test "$( echo $user_input_ID | tr -c -d '[:lower:]' )" && BugError "UserInput: Option '-I' argument '$user_input_ID' must not contain lower case letters"
declare $user_input_ID="dummy" || BugError "UserInput: Option '-I' argument '$user_input_ID' not a valid variable name"
# Shift away the options and arguments:
shift "$(( OPTIND - 1 ))"
# Everything that is now left in "$@" is neither an option nor an option argument
# so that now "$@" contains the trailing mass-arguments (POSIX calls them operands):
local choices=( "$@" )
local choice=""
local choice_index=0
local choice_number=1
if test "${choices:=}" ; then
if test "$default_input" ; then
# Avoid stderr if default_input is not set or empty or not an integer value:
if test "$default_input" -ge 1 2>/dev/null ; then
choice_index=$(( default_input - 1 ))
# It is possible (it is no error) to specify a number as default input that has no matching choice:
test "${choices[$choice_index]:=}" && Log "UserInput: Default input not in choices"
else
# When the default input is no number try to find it in the choices
# and if found use its choice number as default input:
for choice in "${choices[@]}" ; do
if test "$default_input" = "$choice" ; then
Log "UserInput: Default input in choices - using choice number $choice_number as default input"
default_input=$choice_number
break
fi
(( choice_number += 1 ))
done
# It is possible (it is no error) to specify anything as default input.
# Avoid stderr if default_input is not set or empty or not an integer value:
test "$default_input" -ge 1 2>/dev/null || Log "UserInput: Default input not found in choices"
fi
fi
# Use a better default prompt if no prompt was specified when there are choices:
test "$default_prompt" = "$prompt" && prompt="enter a choice number"
else
# It is possible (it is no error) to specify no choices:
Log "UserInput: No choices specified"
fi
# Prepare what to show as default and/or timeout:
local default_and_timeout=""
# Avoid stderr if default_input or timeout is not set or empty or not an integer value:
if test "$default_input" -o "$timeout" -ge 1 2>/dev/null ; then
test "$default_input" && default_and_timeout="default '$default_input'"
# Avoid stderr if timeout is not set or empty or not an integer value:
if test "$timeout" -ge 1 2>/dev/null ; then
if test "$default_and_timeout" ; then
default_and_timeout="$default_and_timeout timeout $timeout seconds"
else
default_and_timeout="timeout $timeout seconds"
fi
fi
fi
# The actual work:
# In debug mode show the user the script that called UserInput and what user_input_ID was specified
# so that the user can prepare an automated response for that UserInput call (without digging in the code):
DebugPrint "UserInput -I $user_input_ID needed in $caller_source"
# First of all show the prompt unless an empty prompt was specified (via -p '')
# so that the prompt can be used as some kind of header line that introduces the user input
# and separates the following user input from arbitrary other output lines before:
test "$prompt" && LogUserOutput "$prompt"
# List the choices (if exists):
if test "${choices:=}" ; then
# This comment contains the opening parentheses ( ( ( to keep paired parentheses:
# Show the choices with leading choice numbers 1) 2) 3) ... as in 'select' (i.e. starting at 1):
choice_number=1
for choice in "${choices[@]}" ; do
# This comment contains the opening parenthesis ( to keep paired parenthesis:
is_true "$confidential_mode" && UserOutput "$choice_number) $choice" || LogUserOutput "$choice_number) $choice"
(( choice_number += 1 ))
done
fi
# Finally show the default and/or the timeout (if exists):
if test "$default_and_timeout" ; then
is_true "$confidential_mode" && UserOutput "($default_and_timeout)" || LogUserOutput "($default_and_timeout)"
fi
# Prepare the 'read' call:
local read_options_and_arguments=""
is_true "$raw_input" && read_options_and_arguments="$read_options_and_arguments -r"
is_true "$silent_input" && read_options_and_arguments="$read_options_and_arguments -s"
# When a zero timeout was specified (via -t 0) do not use it.
# Avoid stderr if timeout is not set or empty or not an integer value:
test "$timeout" -ge 1 2>/dev/null && read_options_and_arguments="$read_options_and_arguments -t $timeout"
# When no input_words_array_name was specified (via -a myarr) do not use it:
test "$input_words_array_name" && read_options_and_arguments="$read_options_and_arguments -a $input_words_array_name"
# When zero input_max_chars was specified (via -n 0) do not use it.
# Avoid stderr if input_max_chars is not set or empty or not an integer value:
test "$input_max_chars" -ge 1 2>/dev/null && read_options_and_arguments="$read_options_and_arguments -n $input_max_chars"
# When no input_delimiter was specified (via -d x) do not use it:
test "$input_delimiter" && read_options_and_arguments="$read_options_and_arguments -d $input_delimiter"
# Get the actual user input value:
local input_string=""
# When a predefined user input value exists use that as automated user input:
local predefined_input_variable_name="USER_INPUT_$user_input_ID"
if test "${!predefined_input_variable_name:-}" ; then
if is_true "$confidential_mode" ; then
if is_true "$silent_input" ; then
UserOutput "UserInput: Will use predefined input in $predefined_input_variable_name"
else
UserOutput "UserInput: Will use predefined input in $predefined_input_variable_name='${!predefined_input_variable_name}'"
fi
else
LogUserOutput "UserInput: Will use predefined input in $predefined_input_variable_name='${!predefined_input_variable_name}'"
fi
# Let the user interrupt the automated user input:
LogUserOutput "Hit any key to interrupt the automated input (timeout $automated_input_interrupt_timeout seconds)"
# automated_input_interrupt_timeout is at least 1 second (see above) and do not echo the input (it is meaningless here)
# and STDOUT is also meaningless (not used) and STDERR can still go into the log (no 'read -p prompt' is used):
if read -t $automated_input_interrupt_timeout -n 1 -s 0<&6 ; then
Log "UserInput: automated input interrupted by user"
# Show the prompt again (or at least the default prompt) to signal the user that now he can and must enter something:
test "$prompt" && LogUserOutput "$prompt" || LogUserOutput "$default_prompt"
if test "$default_and_timeout" ; then
is_true "$confidential_mode" && UserOutput "($default_and_timeout)" || LogUserOutput "($default_and_timeout)"
fi
else
input_string="${!predefined_input_variable_name}"
# When a (non empty) input_words_array_name was specified it must contain all user input words:
test "$input_words_array_name" && read -a "$input_words_array_name" <<<"$input_string"
fi
fi
# When there is no (non empty) automated user input read the user input:
local return_code=0
if ! contains_visible_char "$input_string" ; then
# Read the user input from the original STDIN that is saved as fd6 (see above).
# STDOUT is meaningless because 'read' echoes input from a terminal directly onto the terminal (not via STDOUT) and
# STDERR can still go into the log because no 'read' prompt is used (the prompt is already shown via LogUserOutput):
if read $read_options_and_arguments input_string 0<&6 ; then
is_true "$confidential_mode" && Log "UserInput: 'read' got user input" || Log "UserInput: 'read' got as user input '$input_string'"
else
return_code=1
# Continue in any case because in case of errors the default input is used.
# Avoid stderr if timeout is not set or empty or not an integer value:
if test "$timeout" -ge 1 2>/dev/null ; then
Log "UserInput: 'read' timed out with non-zero exit code"
else
Log "UserInput: 'read' finished with non-zero exit code"
fi
fi
fi
# When an input_words_array_name was specified it contains all user input words
# so that the words in input_words_array_name are copied into input_string:
if test "$input_words_array_name" ; then
# Regarding how to get all array elements when the array name is in a variable, see
# https://unix.stackexchange.com/questions/60584/how-to-use-a-variable-as-part-of-an-array-name
# Assume input_words_array_name="myarr" then input_words_array_name_dereferenced="myarr[*]"
# and "${!input_words_array_name_dereferenced}" becomes "${myarr[*]}"
# Avoid ShellCheck false error indication for code like
# string_appended="$string[*]"
# ^-- SC1087: Use braces when expanding arrays, e.g. ${array[idx]}
# by appending '[*]' to a string variable in a separated command:
local input_words_array_name_dereferenced="$input_words_array_name"
input_words_array_name_dereferenced+='[*]'
input_string="${!input_words_array_name_dereferenced}"
fi
# When there is no user input or when the user input is only spaces use the "best" fallback or default that exists.
if ! contains_visible_char "$input_string" ; then
# There is no real user input (user input is empty or only spaces):
if ! contains_visible_char "$default_input" ; then
# There is neither real user input nor a real default input:
DebugPrint "UserInput: Neither real user input nor real default input (both empty or only spaces) results ''"
echo ""
return $return_code
fi
# When there is a real default input but no real user input use the default input as user input:
DebugPrint "UserInput: No real user input (empty or only spaces) - using default input"
input_string="$default_input"
fi
# Now there is real input in input_string (neither empty nor only spaces):
# When there are no choices result the input as is:
if ! test "$choices" ; then
is_true "$confidential_mode" || DebugPrint "UserInput: No choices - result is '$input_string'"
echo "$input_string"
return $return_code
fi
# When there are choices:
# Avoid stderr if input_string is not set or empty or not an integer value:
if test "$input_string" -ge 1 2>/dev/null ; then
# There are choices and the user input is a positive integer value:
choice_index=$(( input_string - 1 ))
if test "${choices[$choice_index]:=}" ; then
# The user input is a valid choice number:
is_true "$confidential_mode" || DebugPrint "UserInput: Valid choice number result '${choices[$choice_index]}'"
echo "${choices[$choice_index]}"
return $return_code
fi
fi
# When the input is not a a valid choice number or
# when the input is an existing choice string or
# when the input is anything else:
is_true "$confidential_mode" || DebugPrint "UserInput: Result is '$input_string'"
echo "$input_string"
return $return_code
}
# Setup dummy progress subsystem as a default.
# Progress stuff replaced by dummy/noop
# cf. https://github.com/rear/rear/issues/887
function ProgressStart () {
: ;
}
function ProgressStop () {
: ;
}
function ProgressError () {
: ;
}
function ProgressStep () {
: ;
}
function ProgressInfo () {
: ;
}
Zerion Mini Shell 1.0