feat: add support for backtracing in Bash scripts
This change introduces `backtrace.sh`, a small script providing four
utility functions:
- `dump_stack`, which dumps the call stack
- `dump_process_tree`, which dumps the process tree
- `dump`, which dumps both of the above
- `backtrace`, a backtracing trap handler for errors
This change also introduces an ERR-trapped backtracing handler into the
`utils.sh` script, such that any detected errors will dump the script
state.
An example backtrace looks like the following:
ERROR: Command at /workspaces/tf-a/tf-a-ci-scripts/script/run_local_ci.sh:445 exited with error 2:
ERROR:
ERROR: MAKEFLAGS= make -r -j "$parallel" ${keep_going+-k} 5>&1 &> "make.log"
ERROR:
ERROR: Process tree:
ERROR:
ERROR: /bin/bash --init-file /vscode/vscode-server/bin/linux-arm64/6445d93c81ebe42c4cbd7a60712e0b17d9463e97/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh
ERROR: \_ bash /workspaces/tf-a/run-tests.sh
ERROR: \_ bash /workspaces/tf-a/tf-a-ci-scripts/script/run_local_ci.sh
ERROR: \_ bash /workspaces/tf-a/tf-a-ci-scripts/script/run_local_ci.sh
ERROR: | \_ bash /workspaces/tf-a/tf-a-ci-scripts/script/run_local_ci.sh
ERROR: | | \_ ps --forest --no-headers --format command
ERROR: | \_ bash /workspaces/tf-a/tf-a-ci-scripts/script/run_local_ci.sh
ERROR: \_ bash /workspaces/tf-a/tf-a-ci-scripts/script/run_local_ci.sh
ERROR:
ERROR: Call stack:
ERROR:
ERROR: [1]: /workspaces/tf-a/tf-a-ci-scripts/backtrace.sh:78 (backtrace)
ERROR: [2]: /workspaces/tf-a/tf-a-ci-scripts/script/run_local_ci.sh:445 (main)
ERROR: [3]: <unknown>:0 (<unknown>)
Change-Id: Id2c9450400027437fcc082d6849fca8326b33b1c
Signed-off-by: Chris Kay <chris.kay@arm.com>
diff --git a/backtrace.sh b/backtrace.sh
new file mode 100644
index 0000000..9d96bca
--- /dev/null
+++ b/backtrace.sh
@@ -0,0 +1,86 @@
+#!/usr/bin/env bash
+
+#
+# Copyright (c) 2023-2024, Arm Limited and Contributors. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+# Dump the current call stack.
+#
+# This function takes no arguments, and prints the call stack of the script at
+# the point at which it was called.
+dump_stack() {(
+ set +x
+
+ for ((i = 1; i < ${#FUNCNAME[@]}; i++)); do
+ local function="${FUNCNAME[$((i + 1))]:-<unknown>}"
+ local line="${BASH_LINENO[$i]}"
+ local source="${BASH_SOURCE[$((i + 1))]:-<unknown>}"
+
+ echo -e "[$i]: ${source}:${line} (${function})"
+ done
+)}
+
+# Dump the process tree.
+#
+# This function takes no arguments, and prints the process tree of the current
+# process. This can be useful for, for example, determining the arguments that
+# the script has been called with.
+dump_process_tree() {
+ ps --forest --no-headers --format command
+}
+
+# Dump the complete program state.
+#
+# This function takes no arguments, and combines the output of the `dump_stack`
+# and `dump_process_tree` functions.
+dump() {(
+ set +x
+
+ echo "Process tree:"
+ echo
+
+ dump_process_tree | while IFS= read -r line; do
+ echo " ${line}"
+ done
+
+ echo
+ echo "Call stack:"
+ echo
+
+ dump_stack | while read -r line; do
+ echo " ${line}"
+ done
+)}
+
+# Generate an error backtrace.
+#
+# This function dumps the backtrace at the point at which the function is
+# called, with additional information about the command that failed.
+#
+# This is best used as a trap handler (e.g. `trap backtrace ERR`) rather than by
+# being called directly. If you want to explicitly dump the script state, prefer
+# to use the `dump` function instead.
+backtrace() {
+ local error=$?
+ local command=${BASH_COMMAND}
+
+ (
+ set +x
+
+ echo "" >&2
+ echo "ERROR: Command at ${BASH_SOURCE[1]:-<unknown>}:${BASH_LINENO[0]} exited with error ${error}:" >&2
+ echo "ERROR:" >&2
+
+ echo "${command}" | while IFS= read -r line; do
+ echo "ERROR: ${line}" >&2
+ done
+
+ echo "ERROR:" >&2
+
+ dump | while IFS= read -r line; do
+ echo "ERROR: ${line}" >&2
+ done
+ )
+}