#!/bin/sh
# gdb for a seat: started from a directory that exists, and given a sysroot
# whenever it is asked to attach to a pid.
#
# Safe to be `gdb` on PATH, and the image installs it as one. Both bugs below
# hit *every* tool that shells out to gdb in a seat — debugpy's attach-to-pid
# is only the one that was caught — and neither reports anything a reader can
# act on.
#
# 1. A cwd that no longer exists kills gdb during startup.
#
# VS Code's cpptools launches gdb as a child and it inherits cpptools' own cwd,
# which is the extension directory. VS Code replaces that directory wholesale on
# extension update, so a long-lived cpptools ends up sitting in a *deleted*
# directory. gdb links libpython, cpptools always sends `-enable-pretty-printing`,
# CPython's init calls getcwd(), and gdb dies during startup with
#
#   gdb: warning: error finding working directory: No such file or directory
#   Fatal signal:
#   A fatal error internal to GDB has been detected...
#
# — no signal name and no backtrace, because it crashes before it can format
# either. VS Code surfaces only "Unable to start debugging. GDB exited
# unexpectedly", which points at the attach rather than at startup.
#
# `podbench dbg` never hits this: it does not enable pretty-printing, so gdb's
# Python is never initialised. That is why the CLI works on a seat where the VS
# Code debugger cannot start at all. See `podbench debug-config`, which points
# miDebuggerPath here.
#
# Reproduce the bug in any seat with:
#   mkdir -p /tmp/gone && cd /tmp/gone && rmdir /tmp/gone
#   printf -- "-enable-pretty-printing\n-gdb-exit\n" | gdb --interpreter=mi
#
# 2. `gdb --pid <n>` with no sysroot reads the wrong libraries.
#
# A seat and its target share a PID namespace but not a mount namespace, so a
# bare attach loads *this* container's libc for *that* container's process:
#
#   Error reading attached process's symbol file.
#   Error while mapping shared library sections:
#
# and, when it does not error, a plausible and wrong backtrace (report 3.3).
# `podbench dbg` and `podbench debug-config` both set `set sysroot
# /proc/<pid>/root` themselves; a third-party caller does not know it has to. So
# the pid is taken from argv here and the setting supplied.
#
# It MUST be `-iex` and not `-ex`: `--pid` attaches during *startup*, so an
# `-ex` command runs after the attach and is too late — the same
# sysroot-before-attach ordering report 3.3 made load-bearing, and the anti-
# pattern docs/how-to/debug-with-gdb.md already names.
#
# The pid is read from `--pid N`, `--pid=N` and `-p N`, which is every spelling
# gdb accepts; debugpy uses the first. Anything non-numeric, or a pid with no
# /proc entry, is left alone rather than guessed at — gdb's own error for a
# bad pid is better than one from a wrapper.
#
# The liveness test is `-d /proc/<pid>` and deliberately NOT `-e
# /proc/<pid>/root`: resolving that symlink needs PTRACE_MODE_READ, so on the
# degraded rung it fails for a process that exists, and the wrapper would then
# skip the sysroot exactly where it matters most — silently.
#
# 3. A sysroot is not enough for the *executable* (issue #90), and it is not
#    the only line the attach needs.
#
# Left to itself, `gdb --pid <n>` finds the exec file from /proc/<n>/exe and
# canonicalises the name — and /proc/<n>/root canonicalises to `/`, so the
# sysroot is erased and BFD reads this container's binary of the same name:
#
#   BFD: /python/…/python3.11: .gnu.version_r invalid entry
#   warning: Can't read symbols from /python/…/python3.11: bad value
#   Error reading attached process's symbol file.
#
# That is the text issue #90 was filed with, reproduced here from exactly this
# argv. The seat image and any uv-managed workload both install an interpreter
# at /python/cpython-<version>-<triple>/, so a Python target collides by
# construction. The cure is a `file` command naming a path nothing of ours
# shadows.
#
# The whole sequence is asked of podbench rather than worked out here, per
# deviation 2 in image/README.md. It used to be two lines copied out of
# `attach_commands` by hand, and the copy fell behind twice without anything
# saying so: `add-auto-load-safe-path` was never in it, so a sysrooted gdb
# declined to auto-load libthread_db and every caller in the seat — cpptools,
# debugpy's attach-to-pid, `gdb -p` — silently lost thread debugging; and
# `handle SIGURG nostop noprint pass` arrived later still, which pins a default
# rather than repairing a flood - this image's gdb 13.1 already answers
# `SIGURG No No Yes`, as it does for SIGCHLD and SIGWINCH, so the three words
# are its class for a routine signal - so that no differently configured gdb can
# turn an attached Go target into a wall of `Program received signal SIGURG`
# (finding 17.6). Re-read that with `/usr/bin/gdb`, never a bare `gdb`: `gdb` on
# PATH here is this wrapper, which issues the command being measured.
# `podbench dbg <pid> --print-startup-commands` prints exactly the lines
# `podbench dbg` would run before its own `attach`, so there is now one author
# and nothing left in sh to fall behind.
#
# The answer is optional — a podbench that is missing, older, or cannot read
# /proc/<pid>/exe prints nothing, and the sysroot below is then supplied on its
# own, which is what this wrapper did before any of it existed.
pid=
previous=
for argument in "$@"; do
	case "$previous" in
	--pid | -p) pid=$argument ;;
	esac
	case "$argument" in
	--pid=*) pid=${argument#--pid=} ;;
	esac
	previous=$argument
done
case "$pid" in
'' | *[!0-9]*) pid= ;;
esac
[ -n "$pid" ] && [ -d "/proc/$pid" ] || pid=

# Only when getcwd() itself fails. Once this wrapper is `gdb` on PATH it is no
# longer only cpptools calling it — an interactive `gdb ./a.out` or `gdb -c
# core` resolves its relative arguments against the cwd, and moving to /root
# unconditionally would turn those into "No such file or directory". getcwd is
# also the exact call that kills gdb above, so probing it tests the real
# condition rather than a proxy for it.
#
# `/bin/pwd`, not the `pwd` builtin: dash's builtin answers from $PWD and so
# reports success for a directory that has been unlinked, which is precisely
# the case being detected.
/bin/pwd >/dev/null 2>&1 || cd /root 2>/dev/null || cd / || exit 1
if [ -n "$pid" ]; then
	# Whether podbench can run at all is asked first, and asked quietly. Missing
	# it is a case this fallback is *for*, but the shell announces it on our own
	# stderr — `gdb-podbench: 123: /usr/local/bin/podbench: not found`, a raw
	# shell error immediately before a session that then works fine. `--version`
	# rather than `[ -x ]` because /usr/local/bin/podbench is itself a two-line
	# shim over /app/.venv/bin/podbench: a seat whose venv is broken passes the
	# file test and leaks the same message one script down. It is the probe the
	# image build already runs for the same question.
	#
	# The real call keeps its stderr, and must: podbench's warnings there are
	# how a reader learns that /proc/<pid>/exe could not be read, and which file
	# gdb was pointed at instead (issue #90).
	commands=
	if /usr/local/bin/podbench --version >/dev/null 2>&1; then
		commands=$(/usr/local/bin/podbench dbg "$pid" --print-startup-commands) ||
			commands=
	fi
	if [ -n "$commands" ]; then
		# Every line becomes `-iex`, never `-ex`: --pid attaches during
		# *startup*, so an -ex command runs after the attach and is too late —
		# the sysroot-before-attach ordering report 3.3 made load-bearing.
		#
		# sh has no arrays, so the pairs are appended to the caller's own
		# arguments and the caller's are then rotated to the end, which leaves
		# `-iex … -iex … "$@"` in that order. A here-document rather than a
		# pipe: dash runs the right-hand side of a pipeline in a subshell,
		# where `set --` would be discarded.
		given=$#
		while IFS= read -r command; do
			[ -n "$command" ] || continue
			set -- "$@" -iex "$command"
		done <<EOF
$commands
EOF
		while [ "$given" -gt 0 ]; do
			argument=$1
			shift
			set -- "$@" "$argument"
			given=$((given - 1))
		done
		exec /usr/bin/gdb "$@"
	fi
	# podbench could not answer. The sysroot alone is still the difference
	# between this container's libraries and the target's (report 3.3), and it
	# is the one line short enough to be worth keeping a copy of.
	exec /usr/bin/gdb -iex "set sysroot /proc/$pid/root" "$@"
fi
exec /usr/bin/gdb "$@"
