breakpointHook: re-implement using nsenter (#370184)

This commit is contained in:
Jörg Thalheim
2025-01-05 16:18:52 +01:00
committed by GitHub
6 changed files with 111 additions and 7 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
# breakpointHook {#breakpointhook}
This hook will make a build pause instead of stopping when a failure happens. It prevents nix from cleaning up the build environment immediately and allows the user to attach to a build environment using the `cntr` command. Upon build error it will print instructions on how to use `cntr`, which can be used to enter the environment for debugging. Installing cntr and running the command will provide shell access to the build sandbox of failed build. At `/var/lib/cntr` the sandboxed filesystem is mounted. All commands and files of the system are still accessible within the shell. To execute commands from the sandbox use the cntr exec subcommand. `cntr` is only supported on Linux-based platforms. To use it first add `cntr` to your `environment.systemPackages` on NixOS or alternatively to the root user on non-NixOS systems. Then in the package that is supposed to be inspected, add `breakpointHook` to `nativeBuildInputs`.
This hook makes a build pause instead of stopping when a failure occurs. It prevents Nix from cleaning up the build environment immediately and allows the user to attach to the build environment. Upon a build error, it will print instructions that can be used to enter the environment for debugging. breakpointHook is only available on Linux. To use it, add `breakpointHook` to `nativeBuildInputs` in the package to be inspected.
```nix
{
@@ -8,10 +8,10 @@ This hook will make a build pause instead of stopping when a failure happens. It
}
```
When a build failure happens there will be an instruction printed that shows how to attach with `cntr` to the build sandbox.
When a build failure occurs, an instruction will be printed showing how to attach to the build sandbox.
::: {.note}
Caution with remote builds
This wont work with remote builds as the build environment is on a different machine and cant be accessed by `cntr`. Remote builds can be turned off by setting `--option builders ''` for `nix-build` or `--builders ''` for `nix build`.
For remote builds, the printed instructions need to be run on the remote machine, as the build sandbox is only accessible on the machine running the builds. Remote builds can be turned off by setting `--option builders ''` for `nix-build` or `--builders ''` for `nix build`. :::
:::
+46
View File
@@ -0,0 +1,46 @@
#! /usr/bin/env bash
set -eu -o pipefail
id="$1"
pids="$(pgrep -f "sleep $id" || :)"
if [ -z "$pids" ]; then
echo "Error: No process found for 'sleep $id'. The build must still be running in order to attach. Also make sure it's not on a remote builder." >&2
exit 1
elif [ "$(echo "$pids" | wc -l)" -ne 1 ]; then
echo "Error: Multiple processes found matching 'sleep $id'" >&2
exit 1
fi
pid="$(echo "$pids" | head -n1)"
# helper to extract variables from the build env
getVar(){
while IFS= read -r -d $'\0' line; do
case "$line" in
*"$1="* )
echo "$line"
;;
esac
done < /proc/$pid/environ \
| cut -d "=" -f 2
}
# bash is needed to load the env vars, as we do not know the syntax of the debug shell.
# bashInteractive is used instead of bash, as we depend on it anyways, due to it being
# the default debug shell
bashInteractive="$(getVar bashInteractive)"
# the debug shell will be started as interactive shell after loading the env vars
debugShell="$(getVar debugShell)"
# to drop the user into the working directory at the point of failure
pwd="$(readlink /proc/$pid/cwd)"
# enter the namespace of the failed build
exec nsenter --mount --net --target "$pid" "$bashInteractive" -c "
set -eu -o pipefail
while IFS= read -r -d \$'\0' line; do
export \"\$line\"
done <&3
exec 3>&-
cd \"$pwd\"
exec \"$debugShell\"
" 3< /proc/$pid/environ
@@ -1,9 +1,20 @@
breakpointHook() {
local red='\033[0;31m'
local cyan='\033[0;36m'
local green='\033[0;32m'
local no_color='\033[0m'
echo -e "${red}build failed in ${curPhase} with exit code ${exitCode}${no_color}"
printf "To attach install cntr and run the following command as root:\n\n"
sh -c "echo ' cntr attach -t command cntr-${out}'; while true; do sleep 99999999; done"
# provide the user with an interactive shell for better experience
export bashInteractive="@bashInteractive@"
if [ -z "$debugShell" ]; then
export debugShell="@bashInteractive@"
fi
local id
id="$(shuf -i 999999-9999999 -n1)"
echo -e "${red}build for ${cyan}${name:-unknown}${red} failed in ${curPhase:-unknown} with exit code ${exitCode:-unknown}${no_color}"
echo -e "${green}To attach, run the following command:${no_color}"
echo -e "${green} sudo @attach@ $id${no_color}"
sleep "$id"
}
failureHooks+=(breakpointHook)
+33 -1
View File
@@ -1,6 +1,38 @@
{ stdenv, makeSetupHook }:
{
lib,
stdenv,
bash,
bashInteractive,
coreutils,
makeSetupHook,
procps,
util-linux,
writeShellScriptBin,
}:
let
attach = writeShellScriptBin "attach" ''
export PATH="${
lib.makeBinPath [
bash
coreutils
procps # needed for pgrep
util-linux # needed for nsenter
]
}"
exec bash ${./attach.sh} "$@"
'';
in
makeSetupHook {
name = "breakpoint-hook";
meta.broken = !stdenv.buildPlatform.isLinux;
substitutions = {
attach = "${attach}/bin/attach";
# The default interactive shell in case $debugShell is not set in the derivation.
# Can be overridden to zsh or fish, etc.
# This shell is also used to load the env variables before the $debugShell is started.
bashInteractive = lib.getExe bashInteractive;
};
} ./breakpoint-hook.sh
@@ -0,0 +1,9 @@
breakpointHook() {
local red='\033[0;31m'
local no_color='\033[0m'
echo -e "${red}build failed in ${curPhase} with exit code ${exitCode}${no_color}"
printf "To attach install cntr and run the following command as root:\n\n"
sh -c "echo ' cntr attach -t command cntr-${out}'; while true; do sleep 99999999; done"
}
failureHooks+=(breakpointHook)
@@ -0,0 +1,6 @@
{ stdenv, makeSetupHook }:
makeSetupHook {
name = "breakpoint-hook";
meta.broken = !stdenv.buildPlatform.isLinux;
} ./breakpoint-hook.sh