From 54f9e12526a091d798ef1437c0ca686aced79544 Mon Sep 17 00:00:00 2001 From: David Young Date: Sun, 8 Nov 2026 11:00:31 -0700 Subject: [PATCH] Log each install and update, and keep only the recent logs Each run's output now goes to ~/.local/state/coredesktop/logs/, so what failed can be read after the terminal is gone. In a terminal, update runs itself under script(1), so everything it runs still sees a terminal, then strips color codes and progress redraws from the log; otherwise it uses tee. install.sh, usually piped into bash, uses tee, and runs configure straight on the terminal, outside the log. Both name the log when something fails. 95-cleanup.sh keeps the newest 10 update logs and 3 install logs. The sudo keepalive loops no longer share the logged output, so their sleep can't hold the log open after the run ends. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 ++ install.sh | 23 ++++++++++++++++++++--- tests/cleanup.bats | 32 ++++++++++++++++++++++++++++++++ tests/update.bats | 36 +++++++++++++++++++++++++++++++++++- update | 28 +++++++++++++++++++++++++++- updates.d/95-cleanup.sh | 14 ++++++++++++++ 6 files changed, 130 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d2d29fa..beb2bc0 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,8 @@ The script pulls the latest CoreDesktop, then runs each step in ~/.local/share/c If a step fails, the other steps still run, and the failed ones are listed at the end. +Each run's full output is saved in ~/.local/state/coredesktop/logs/, so you can read what went wrong after the terminal is gone. `update` keeps the logs of the last 10 updates and the last 3 installs or refreshes. + It is a good idea to reboot after an update especially if you see you have a new Linux kernel installed (and that happens frequently enough for Arch). Note: sometimes, for whatever reason, the update barfs. Take a look at [https://archlinux.org](https://archlinux.org) in the "Latest News" section for fixes. diff --git a/install.sh b/install.sh index 021fe17..1937b05 100755 --- a/install.sh +++ b/install.sh @@ -21,6 +21,14 @@ clear print_logo cd +# Log the install to ~/.local/state/coredesktop/logs (update deletes old ones). +# It's usually piped into bash, so it can't run itself again under script(1) +# the way update does; send its output through tee instead. +if mkdir -p -m 700 "$HOME/.local/state/coredesktop/logs"; then + INSTALL_LOG="$HOME/.local/state/coredesktop/logs/install-$(date +%Y-%m-%d-%H%M%S).log" + exec > >(tee -a "$INSTALL_LOG") 2>&1 +fi + # Exit on any error set -e @@ -51,9 +59,16 @@ if ! sudo -v; then echo "archinstall, make the user a superuser): https://wiki.archlinux.org/title/Sudo" exit 1 fi -while kill -0 $$ 2> /dev/null; do sudo -n -v; sleep 60; done 2> /dev/null & +while kill -0 $$ 2> /dev/null; do sudo -n -v; sleep 60; done > /dev/null 2>&1 & SUDO_KEEPALIVE_PID=$! -trap 'kill "$SUDO_KEEPALIVE_PID" 2> /dev/null' EXIT +on_exit() { + local status=$? + kill "$SUDO_KEEPALIVE_PID" 2> /dev/null + if (( status != 0 )) && [ -n "$INSTALL_LOG" ]; then + echo "The install stopped with an error. The full output is in $INSTALL_LOG" + fi +} +trap on_exit EXIT # Ensure git is available before attempting to clone anything if ! command -v git &> /dev/null; then @@ -283,7 +298,9 @@ if ~/.local/bin/configure --list | grep -q '^to do'; then echo "$CONFIGURE_HINT" if { : < /dev/tty; } 2> /dev/null && read -rp "Run configure now? [Y/n] " answer < /dev/tty \ && [[ "$answer" != [nN]* ]]; then - ~/.local/bin/configure < /dev/tty || true + # Straight to the terminal, outside the log: sign-ins need a terminal, and + # their codes and links don't belong in a log + ~/.local/bin/configure < /dev/tty > /dev/tty 2>&1 || true else echo "To do it later, run: configure" fi diff --git a/tests/cleanup.bats b/tests/cleanup.bats index 0918771..61dd7b9 100644 --- a/tests/cleanup.bats +++ b/tests/cleanup.bats @@ -31,3 +31,35 @@ esac' [ "$status" -eq 0 ] [ -z "$(grep 'pacman -R ' "$STUB_LOG")" ] } + +@test "95-cleanup keeps the newest 10 update logs and 3 install logs" { + stub pacman + stub sudo + stub flatpak + logs="$HOME/.local/state/coredesktop/logs" + mkdir -p "$logs" + for day in 01 02 03 04 05 06 07 08 09 10 11 12; do + touch "$logs/update-2026-11-$day-120000.log" "$logs/install-2026-11-$day-120000.log" + done + touch "$logs/notes.txt" + + run bash "$REPO/updates.d/95-cleanup.sh" < /dev/null + [ "$status" -eq 0 ] + [ "$(cd "$logs" && echo update-*)" = "$(printf 'update-2026-11-%s-120000.log ' 03 04 05 06 07 08 09 10 11 12 | sed 's/ $//')" ] + [ "$(cd "$logs" && echo install-*)" = "install-2026-11-10-120000.log install-2026-11-11-120000.log install-2026-11-12-120000.log" ] + [ -f "$logs/notes.txt" ] +} + +@test "95-cleanup is fine with no logs or only a few" { + stub pacman + stub sudo + stub flatpak + run bash "$REPO/updates.d/95-cleanup.sh" < /dev/null + [ "$status" -eq 0 ] + + mkdir -p "$HOME/.local/state/coredesktop/logs" + touch "$HOME/.local/state/coredesktop/logs/update-2026-11-01-120000.log" + run bash "$REPO/updates.d/95-cleanup.sh" < /dev/null + [ "$status" -eq 0 ] + [ -f "$HOME/.local/state/coredesktop/logs/update-2026-11-01-120000.log" ] +} diff --git a/tests/update.bats b/tests/update.bats index 41424ea..fb15624 100644 --- a/tests/update.bats +++ b/tests/update.bats @@ -16,7 +16,7 @@ setup() { done export RAN="$BATS_TEST_TMPDIR/ran" - unset UPDATE_REEXECED UPDATE_PULL_FAILED UPDATE_FORCE + unset UPDATE_REEXECED UPDATE_PULL_FAILED UPDATE_FORCE UPDATE_LOG stub clear stub git 'exit "${GIT_STATUS:-0}"' @@ -167,6 +167,40 @@ ran() { [ ! -e "$RAN" ] } +@test "update logs its output to one file, across re-running its new copy" { + hook 10-a 'echo "hook a ran"' + + run "$CD/update" + [ "$status" -eq 0 ] + logs=("$HOME"/.local/state/coredesktop/logs/update-*.log) + [ "${#logs[@]}" -eq 1 ] + grep -q "Updating CoreDesktop" "${logs[0]}" + grep -q "hook a ran" "${logs[0]}" + grep -q "^Done\.$" "${logs[0]}" + [ "$(stat -c %a "$HOME/.local/state/coredesktop/logs")" = 700 ] +} + +@test "when a hook fails, update says where its log is" { + hook 10-a 'return 1' + + run "$CD/update" + [ "$status" -eq 1 ] + [[ "$output" == *"The full output is in $HOME/.local/state/coredesktop/logs/update-"*".log"* ]] +} + +@test "clean_log leaves only the text of a terminal session's log" { + eval "$(sed -n '/^clean_log() {/,/^}/p' "$REPO/update")" + log="$BATS_TEST_TMPDIR/session.log" + printf '%s\n' 'Script started on 2026-11-08 10:00:00-07:00 [COMMAND="update"]' > "$log" + printf '\033]0;title\007\033[1;32mgreen\033[0m Ünïcode █\r\n' >> "$log" + printf '10%%\r50%%\r100%%\r\n' >> "$log" + printf '\033[H\033[2J\033[3Jdone\r\n' >> "$log" + printf '%s\n' 'Script done on 2026-11-08 10:01:00-07:00 [COMMAND_EXIT_CODE="0"]' >> "$log" + + clean_log "$log" + [ "$(cat "$log")" = "$(printf 'green Ünïcode █\n100%%\ndone')" ] +} + @test "an update from before 2026-10-19 that copies this one over itself hands over to it" { # 013acac's update: sudo cp overwrites the file bash is still reading, and # bash reads on in the new one from where it was diff --git a/update b/update index edea6c9..ee7139e 100755 --- a/update +++ b/update @@ -35,6 +35,31 @@ install_script() { tmp="$(mktemp "$2.XXXXXX")" && cp "$1" "$tmp" && chmod 755 "$tmp" && mv -f "$tmp" "$2" } +# Make a log written by script(1) readable: drop its header and footer, color +# and cursor codes, and all but the last redraw of each progress line. LC_ALL=C +# so the ranges below mean bytes, whatever the locale. +clean_log() { + LC_ALL=C sed -i -E '1{/^Script started on /d}; /^Script done on .*COMMAND_EXIT_CODE/d; + s/\x1b\][^\x07\x1b]*(\x07|\x1b\\)//g; s#\x1b\[[0-9;?]*[ -/]*[@-~]##g; + s/\x1b[()][0-9A-Za-z]//g; s/\x1b[=>]//g; s/\r+$//; s/.*\r//' "$1" +} + +# Log each update to ~/.local/state/coredesktop/logs (95-cleanup.sh deletes +# old ones). In a terminal, run under script(1), so everything still sees a +# terminal and colors, menus and prompts work as before; otherwise use tee. +# UPDATE_LOG tells the copy re-run below that it's already logging. +if [[ -z "$UPDATE_LOG" ]] && mkdir -p -m 700 "$HOME/.local/state/coredesktop/logs"; then + UPDATE_LOG="$HOME/.local/state/coredesktop/logs/update-$(date +%Y-%m-%d-%H%M%S).log" + export UPDATE_LOG + if [ -t 0 ] && [ -t 1 ] && command -v script > /dev/null; then + SHELL=/bin/bash script -q -e -f -c "$(printf '%q ' "$0" "$@")" "$UPDATE_LOG" + status=$? + clean_log "$UPDATE_LOG" + exit "$status" + fi + exec > >(tee -a "$UPDATE_LOG") 2>&1 +fi + # Print the logo print_logo() { cat << "EOF" @@ -88,7 +113,7 @@ fi # Ask for the password once, then keep sudo's cached credentials fresh until # update exits, so long-running hooks don't stop for a password again. sudo -v || exit 1 -while kill -0 $$ 2>/dev/null; do sudo -n -v; sleep 60; done 2>/dev/null & +while kill -0 $$ 2>/dev/null; do sudo -n -v; sleep 60; done > /dev/null 2>&1 & SUDO_KEEPALIVE_PID=$! trap 'kill "$SUDO_KEEPALIVE_PID" 2>/dev/null' EXIT @@ -119,6 +144,7 @@ done if (( ${#FAILED[@]} > 0 )); then echo "Done, with errors in: ${FAILED[*]}" + [ -n "$UPDATE_LOG" ] && echo "The full output is in $UPDATE_LOG" exit 1 fi diff --git a/updates.d/95-cleanup.sh b/updates.d/95-cleanup.sh index 518cda2..e8f2bc1 100755 --- a/updates.d/95-cleanup.sh +++ b/updates.d/95-cleanup.sh @@ -10,6 +10,20 @@ if command -v paccache &>/dev/null; then sudo paccache -ruk0 fi +# Keep the newest 10 update logs and 3 install logs (refresh runs install.sh +# too). Their names sort oldest first. +keep_newest() { + local keep="$1" + shift + (( $# > keep )) && rm -f -- "${@:1:$# - keep}" + return 0 +} +LOGS="$HOME/.local/state/coredesktop/logs" +shopt -s nullglob +keep_newest 10 "$LOGS"/update-*.log +keep_newest 3 "$LOGS"/install-*.log +shopt -u nullglob + # makepkg builds pkg-debug alongside an AUR package, and removing the package # leaves it behind (asdf-vm-debug). Without its package it's no use to anyone, # so remove it without asking. The exact name: pacman -Q herdr finds herdr-bin