Add a wrapper for generating external thread-dump files.
Change-Id: I2a2262a341ce6e981b713dc88b4aa4c1e0f1970e
diff --git a/tools/kustvakt-wrapper.sh b/tools/kustvakt-wrapper.sh
new file mode 100755
index 0000000..0f2c2c3
--- /dev/null
+++ b/tools/kustvakt-wrapper.sh
@@ -0,0 +1,164 @@
+# This script is automatocally generated by GH Copilot to wrap the command
+# to run Kustvakt server and creates thread dumps on SIGQUIT/SIGTERM at
+# data/threaddumps. Only fallback kill -3 since jstack and jcmd are only
+# available in jdk.
+#
+# The script is intended to be used with and be placed at the same location as
+# a compose.yaml.
+
+#!/bin/sh
+set -eu
+
+# POSIX-compatible wrapper to start the original command and capture thread
+# dumps into /kustvakt/data/threaddumps when the container receives SIGQUIT
+# or SIGTERM. This avoids bash-specific syntax so it works with /bin/sh.
+
+DUMP_DIR="/kustvakt/data/threaddumps"
+mkdir -p "$DUMP_DIR"
+
+# File where we collect the JVM's stderr stream so kill -3 output can be
+# extracted when jcmd/jstack are not available.
+STDERR_LOG="/kustvakt/data/logs/jvm-stderr.log"
+
+child_pid=0
+
+on_quit() {
+ TS=$(date +"%Y%m%d-%H%M%S")
+ FILE="$DUMP_DIR/threaddump-$TS.txt"
+ echo "[wrapper] Creating thread dump to $FILE" >&2
+
+ if command -v jcmd >/dev/null 2>&1; then
+ echo "[wrapper] Running: jcmd $child_pid Thread.print" >&2
+ if jcmd "$child_pid" Thread.print > "$FILE" 2>&1; then
+ echo "[wrapper] jcmd wrote $FILE" >&2
+ else
+ echo "[wrapper] jcmd failed" >&2
+ fi
+ elif command -v jstack >/dev/null 2>&1; then
+ echo "[wrapper] Running: jstack -l $child_pid" >&2
+ if jstack -l "$child_pid" > "$FILE" 2>&1; then
+ echo "[wrapper] jstack wrote $FILE" >&2
+ else
+ echo "[wrapper] jstack failed" >&2
+ fi
+ else
+ # If we don't have jcmd/jstack, trigger JVM to print a thread dump to
+ # stderr (kill -3) and capture the appended portion from the stderr log.
+ MARKER="===THREAD_DUMP_MARKER $TS $child_pid ==="
+ printf "%s\n" "$MARKER" >> "$STDERR_LOG" || true
+ kill -3 "$child_pid" 2>/dev/null || true
+ # give JVM a moment to write to stderr
+ sleep 1
+
+ # Extract the portion after the MARKER into the dump file.
+ # Find the last occurrence of the marker and tail from the next line.
+ if marker_line=$(grep -nF "$MARKER" "$STDERR_LOG" 2>/dev/null | tail -n 1 | cut -d: -f1); then
+ if [ -n "$marker_line" ]; then
+ # tail from marker_line+1
+ start=$((marker_line + 1))
+ if tail -n +$start "$STDERR_LOG" > "$FILE" 2>/dev/null; then
+ echo "[wrapper] Extracted JVM stderr to $FILE" >&2
+ else
+ echo "[wrapper] Failed to extract JVM stderr to $FILE" >&2
+ fi
+ else
+ # nothing found; create empty file so caller sees some output
+ : > "$FILE" || true
+ fi
+ else
+ : > "$FILE" || true
+ fi
+ fi
+
+ # If the dump file is empty or missing, fall back to writing last 2000 lines
+ if [ ! -s "$FILE" ]; then
+ echo "[wrapper] $FILE is empty; writing fallback from $STDERR_LOG" >&2
+ printf "[wrapper] Fallback thread dump (no jcmd/jstack output)\n" > "$FILE" || true
+ tail -n 2000 "$STDERR_LOG" >> "$FILE" 2>/dev/null || true
+ echo "[wrapper] Wrote fallback content to $FILE" >&2
+ fi
+
+ # Forward quit/term to child so it can shutdown
+ kill -s QUIT "$child_pid" 2>/dev/null || kill -s TERM "$child_pid" 2>/dev/null || true
+}
+
+trap 'on_quit' QUIT TERM
+
+if [ "$#" -eq 0 ]; then
+ echo "[wrapper] No command provided to start Kustvakt" >&2
+ exit 1
+fi
+
+first="$1"
+shift
+
+# Determine how to invoke the target process.
+if command -v "$first" >/dev/null 2>&1; then
+ # executable available in PATH
+ set -- "$first" "$@"
+else
+ case "$first" in
+ *.jar)
+ if [ -f "/kustvakt/$first" ]; then
+ set -- java -jar "/kustvakt/$first" "$@"
+ else
+ set -- java -jar "$first" "$@"
+ fi
+ ;;
+ /*)
+ if [ -f "$first" ]; then
+ case "$first" in
+ *.jar)
+ set -- java -jar "$first" "$@"
+ ;;
+ *)
+ set -- "$first" "$@"
+ ;;
+ esac
+ else
+ set -- "$first" "$@"
+ fi
+ ;;
+ *)
+ if [ -f "/kustvakt/$first" ]; then
+ case "/kustvakt/$first" in
+ *.jar)
+ set -- java -jar "/kustvakt/$first" "$@"
+ ;;
+ *)
+ set -- "/kustvakt/$first" "$@"
+ ;;
+ esac
+ else
+ set -- java -jar "$first" "$@"
+ fi
+ ;;
+ esac
+fi
+
+WRAPPER_LOG="/kustvakt/data/logs/kustvakt-wrapper.log"
+timestamp() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
+
+echo "[wrapper] Starting child: $*" | tee -a "$WRAPPER_LOG" >&2
+
+# Start the child in background and capture its PID.
+# Prefer starting the child in a separate session so it does not receive
+# the initial container signal (which docker sends to PID 1). We then
+# explicitly forward QUIT/TERM to the child after creating the dump.
+if command -v setsid >/dev/null 2>&1; then
+ echo "[$(timestamp)] wrapper: starting child with setsid" >> "$WRAPPER_LOG" || true
+ setsid "$@" >>"$STDERR_LOG" 2>&1 &
+else
+ echo "[$(timestamp)] wrapper: setsid not available; starting child normally" >> "$WRAPPER_LOG" || true
+ "$@" >>"$STDERR_LOG" 2>&1 &
+fi
+child_pid=$!
+
+# Record the child's PID to a file and wrapper log for easier debugging
+echo "$child_pid" > /kustvakt/data/kustvakt-child.pid 2>/dev/null || true
+echo "[$(timestamp)] wrapper: child_pid=$child_pid (pid file: /kustvakt/data/kustvakt-child.pid)" >> "$WRAPPER_LOG" || true
+
+# Wait for the child and propagate exit code
+wait "$child_pid"
+exit_code=$?
+exit $exit_code
\ No newline at end of file