diff --git a/pkgs/by-name/br/breakpointHook/attach.sh b/pkgs/by-name/br/breakpointHook/attach.sh new file mode 100755 index 000000000000..06ccdc3eee06 --- /dev/null +++ b/pkgs/by-name/br/breakpointHook/attach.sh @@ -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 diff --git a/pkgs/by-name/br/breakpointHook/breakpoint-hook.sh b/pkgs/by-name/br/breakpointHook/breakpoint-hook.sh new file mode 100644 index 000000000000..6c79f58604cd --- /dev/null +++ b/pkgs/by-name/br/breakpointHook/breakpoint-hook.sh @@ -0,0 +1,20 @@ +breakpointHook() { + local red='\033[0;31m' + local cyan='\033[0;36m' + local green='\033[0;32m' + local no_color='\033[0m' + + # 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) diff --git a/pkgs/by-name/br/breakpointHook/package.nix b/pkgs/by-name/br/breakpointHook/package.nix new file mode 100644 index 000000000000..a94ef881a430 --- /dev/null +++ b/pkgs/by-name/br/breakpointHook/package.nix @@ -0,0 +1,38 @@ +{ + 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