#!/usr/bin/env bash # # SPDX-License-Identifier: GPL-2.0 # # Copyright (c) 2013-2023 Igor Pecovnik, igor@armbian.com # # This file is a part of the Armbian Build Framework # https://github.com/armbian/build/ # Initialize and prepare the trap managers, one for each of ERR, INT, TERM and EXIT traps. # Bash goes insane regarding line numbers and other stuff if we try to overwrite the traps. # This also implements the custom "cleanup" handlers, which always run at the end of build, or when exiting prematurely for any reason. function traps_init() { # shellcheck disable=SC2034 # Array of cleanup handlers. declare -g -a trap_manager_cleanup_handlers=() # shellcheck disable=SC2034 # Global to avoid doubly reporting ERR/EXIT pairs. declare -g -i trap_manager_error_handled=0 trap 'main_trap_handler "ERR" "$?"' ERR trap 'main_trap_handler "EXIT" "$?"' EXIT trap 'main_trap_handler "INT" "$?"' INT trap 'main_trap_handler "TERM" "$?"' TERM } # This is setup early in compile.sh as a trap handler for ERR, EXIT and INT signals. # There are arrays trap_manager_error_handlers=() trap_manager_exit_handlers=() trap_manager_int_handlers=() # that will receive the actual handlers. # First param is the type of trap, the second is the value of "$?" # In order of occurrence. # 1) Ctrl-C causes INT [stack unreliable], then ERR, then EXIT with trap_exit_code > 0 # 2) Stuff failing causes ERR [stack OK], then EXIT with trap_exit_code > 0 # 3) exit_with_error causes EXIT [stack OK, with extra frame] directly with trap_exit_code == 43 # 4) EXIT can also be called directly [stack unreliable], with trap_exit_code == 0 if build successful. # So the EXIT trap will do: # - show stack, if not previously shown (trap_manager_error_handled==0), and if trap_exit_code > 0 # - allow for debug shell, if trap_exit_code > 0 # - call all the cleanup functions (always) function main_trap_handler() { local trap_type="${1}" local trap_exit_code="${2}" local stack_caller short_stack stack_caller="$(show_caller_full)" short_stack="${BASH_SOURCE[1]}:${BASH_LINENO[0]}" display_alert "main_trap_handler" "${trap_type} and ${trap_exit_code} trap_manager_error_handled:${trap_manager_error_handled} short_stack:${short_stack}" "trap" case "${trap_type}" in TERM | INT) display_alert "Build interrupted" "Build interrupted by SIG${trap_type}" "warn" trap_manager_error_handled=1 return # Nothing else to do here. Let the ERR trap show the stack, and the EXIT trap do cleanups. ;; ERR) # If error occurs in subshell (eg: inside $()), we would show the error twice. # Determine if we're in a subshell, and if so, output a single message. # BASHPID is the current subshell; $$ is parent shell pid if [[ "${BASHPID}" == "${$}" ]]; then # Not in subshell, dump the error, complete with log, and show the stack. if [[ ! ${trap_manager_error_handled} -gt 0 ]]; then logging_error_show_log display_alert "Error ${trap_exit_code} occurred in main shell" "at ${short_stack}\n${stack_caller}\n" "err" fi else # In a subshell. This trap will run again in the parent shell, so just output a message about it; # When the parent shell trap runs, it will show the stack and log. display_alert "Error ${trap_exit_code} occurred in SUBSHELL" "SUBSHELL at ${short_stack}" "err" fi trap_manager_error_handled=1 return # Nothing else to do here, let the EXIT trap do the cleanups. ;; EXIT) if [[ ${trap_manager_error_handled} -lt 1 ]] && [[ ${trap_exit_code} -gt 0 ]]; then logging_error_show_log display_alert "Exiting with error ${trap_exit_code}" "at ${short_stack}\n${stack_caller}\n" "err" trap_manager_error_handled=1 fi if [[ ${trap_exit_code} -gt 0 ]] && [[ "${ERROR_DEBUG_SHELL}" == "yes" ]]; then declare -g ERROR_DEBUG_SHELL=no # dont do it twice display_alert "MOUNT" "${MOUNT}" "debug" display_alert "SDCARD" "${SDCARD}" "debug" display_alert "ERROR_DEBUG_SHELL=yes, starting a shell." "ERROR_DEBUG_SHELL; exit to cleanup." "debug" bash < /dev/tty >&2 || true fi # Run the cleanup handlers, always. pass it the exit code so it keep the red theme of errors in its messages. cleanup_exit_code="${trap_exit_code}" run_cleanup_handlers || true # If global_final_exit_code is set, use it as the exit code. (used by docker CLI handler) if [[ -n "${global_final_exit_code}" ]]; then display_alert "Final exit code" "Final exit code ${global_final_exit_code}" "debug" # disable the trap, so we don't get called again. trap - EXIT exit "${global_final_exit_code}" fi ;; *) display_alert "main_trap_handler" "Unknown trap type '${trap_type}'" "err" ;; esac } # Run the cleanup handlers, if any, and clean the cleanup list. function run_cleanup_handlers() { display_alert "run_cleanup_handlers! list:" "${trap_manager_cleanup_handlers[*]}" "cleanup" if [[ ${#trap_manager_cleanup_handlers[@]} -lt 1 ]]; then return 0 # No handlers set, just return. else if [[ ${cleanup_exit_code:-0} -gt 0 ]]; then # No use polluting GHA/CI with notices about cleanup, if we're already failing. skip_ci_special="yes" skip_ci_special="yes" display_alert "Cleaning up" "please wait for cleanups to finish" "error" else display_alert "Cleaning up" "please wait for cleanups to finish" "info" fi fi # Loop over the handlers, execute one by one. Ignore errors. # IMPORTANT: cleanups are added first to the list, so cleanups run in the reverse order they were added. local one_cleanup_handler for one_cleanup_handler in "${trap_manager_cleanup_handlers[@]}"; do run_one_cleanup_handler "${one_cleanup_handler}" done # Clear the cleanup handler list, so they don't accidentally run again. trap_manager_cleanup_handlers=() } # Adds a callback for trap types; first and only argument is eval code to call during cleanup. If such, that needs proper quoting (@Q) function add_cleanup_handler() { if [[ $# -gt 1 ]]; then exit_with_error "add_cleanup_handler: too many params" fi local callback="$1" # simple function name or @Q quoted eval code # validate if [[ -z "${callback}" ]]; then exit_with_error "add_cleanup_handler: no callback specified" fi display_alert "Add callback as cleanup handler" "${callback}" "cleanup" # IMPORTANT: cleanups are added first to the list, so they're executed in reverse order. trap_manager_cleanup_handlers=("${callback}" "${trap_manager_cleanup_handlers[@]}") } function execute_and_remove_cleanup_handler() { local callback="$1" display_alert "Execute and remove cleanup handler" "${callback}" "cleanup" local remaining_cleanups=() for one_cleanup_handler in "${trap_manager_cleanup_handlers[@]}"; do if [[ "${one_cleanup_handler}" != "${callback}" ]]; then remaining_cleanups+=("${one_cleanup_handler}") else run_one_cleanup_handler "${one_cleanup_handler}" fi done trap_manager_cleanup_handlers=("${remaining_cleanups[@]}") } function run_one_cleanup_handler() { declare one_cleanup_handler="$1" display_alert "Running cleanup handler" "${one_cleanup_handler}" "cleanup" eval "${one_cleanup_handler}" || { display_alert "Cleanup handler failed, this is a severe bug in the build system or extensions" "${one_cleanup_handler}" "err" } } function remove_all_trap_handlers() { # @TODO find usages and kill display_alert "calling obsolete method remove_all_trap_handlers()" "not doing anything" "warning" } # exit_with_error # a way to terminate build process with verbose error message function exit_with_error() { # Log the error and exit. # Everything else will be done by shared trap handling, above. local _file="${BASH_SOURCE[1]}" local _function=${FUNCNAME[1]} local _line="${BASH_LINENO[0]}" display_alert "error!" "${1} ${2}" "err" #display_alert "Build terminating..." "please wait for cleanups to finish" "err" # @TODO: move this into trap handler # @TODO: integrate both overlayfs and the FD locking with cleanup logic overlayfs_wrapper "cleanup" ## This does not really make sense. wtf? ## unlock loop device access in case of starvation # @TODO: hmm, say that again? #exec {FD}> /var/lock/armbian-debootstrap-losetup #flock -u "${FD}" # do NOT close the fd 13 here, otherwise the error will not be logged to logfile... exit 43 } # terminate build with a specific error code (44), meaning "target not supported". # this is exactly like exit_with_error, but will be handled differently by the build pipeline. function exit_with_target_not_supported_error() { # Log the error and exit. # Everything else will be done by shared trap handling, above. local _file="${BASH_SOURCE[1]}" local _function=${FUNCNAME[1]} local _line="${BASH_LINENO[0]}" display_alert "target not supported! " "${1} ${2}" "err" overlayfs_wrapper "cleanup" exit 44 }