Name the install medium when pacstrap rejects a package off it (#120)
* Name the install medium when pacstrap rejects a package off it A failed install showed "Omarchy installation stopped", the pacstrap error, a log tail, and a link to Discord. Somewhere in that tail was the whole answer -- pacman rejecting a package as corrupt -- and nothing on the screen connected it to the USB stick it came off. Nothing else can put it there. The offline mirror is bind-mounted onto the target's package cache for the duration of pacstrap, so a package rejected under that path was read straight off the medium; nothing else ever writes there. That is worth saying in the words that name the fix rather than leaving it as a package name nobody recognises. Which way the medium failed is a separate question, and usually unanswerable after the fact: pacstrap runs with --noconfirm, which answers yes to pacman's "delete it?", so the evidence is unlinked through the bind mount before anything can weigh it. The usual verdict therefore names the medium and gives both remedies in the order worth trying -- verify the download first, because it is cheap and rules out the common case. When the package does survive, the sha256 repo-add recorded at build time separates a bad write from a bad read, and the wording follows. Co-Authored-By: Codex XHigh <codex@openai.com> * Keep the media diagnosis whole and print it once Two things only visible by looking at the rendered screen. The diagnosis was recorded in the support log before the failure screen was drawn, and the log tail on that screen reads the same file -- so the verdict appeared twice, once as the banner and again eight lines below it. The same finding printed twice reads as two findings. It is recorded after the render now, and the tail carries the installer's own output the way it did before. The package name shared a line with the sentence explaining it, and the screen is only as wide as the logo. npth's name already pushed the mismatch wording past the edge, and a firmware package would have taken the whole sentence with it. Every verdict puts the name on a line of its own, and the test walks all three with a sixty-three character package name to keep them there. Co-Authored-By: Codex XHigh <codex@openai.com> * Break the media diagnosis between what happened and what to do The package name and the remedy beneath it ran together as one block, so the line telling someone to verify their download read as another clause about the package rather than the first thing to go and do. A blank line separates them in all three verdicts. The renderer dropped blank lines, on the assumption that any it saw were slack rather than shape, so the break is drawn now instead of skipped. * Give the log tail back the rows the diagnosis spends The tail sizes itself from the terminal height less a constant for everything drawn above it, and the diagnosis is seven rows that constant never knew about. On a forty-row console the failure screen came to forty-one and scrolled its own logo off the top. The tail is the part that should give way, so it is told what was spent above it. The test that came with this first asserted the logo was still on screen, which it cannot: it reads the output stream, and the stream keeps every row ever emitted whether or not it stayed visible. It passed with the fix reverted. It checks the mechanism instead -- a tail that reaches fewer lines of the log when a diagnosis is present -- against two logs identical line for line but for the one path that decides whether there is a diagnosis at all. --------- Co-authored-by: Codex XHigh <codex@openai.com>
This commit is contained in:
1 parent
6d02625f7f
commit
268bac16d3
4 files changed
+641
-7
No files matched your search
@@ -571,10 +571,13 @@ truncate_for_width() {
|
||||
}
|
||||
|
||||
render_failure_log_tail() {
|
||||
local rows lines width line tail_log label
|
||||
# The caller passes the rows it spent above this point that the constant below
|
||||
# does not know about, so the tail gives way rather than pushing the top of
|
||||
# the screen off a short console.
|
||||
local reserved="${1:-0}" rows lines width line tail_log label
|
||||
rows=$(term_size)
|
||||
rows=${rows%% *}
|
||||
lines=$((rows - LOGO_HEIGHT - 18))
|
||||
lines=$((rows - LOGO_HEIGHT - 18 - reserved))
|
||||
(( lines < 6 )) && lines=6
|
||||
(( lines > 14 )) && lines=14
|
||||
width=$((CONTENT_WIDTH - 4))
|
||||
@@ -643,8 +646,52 @@ view_failure_log() {
|
||||
fi
|
||||
}
|
||||
|
||||
# A failure the medium explains is worth saying before the log tail, because
|
||||
# the tail is where "invalid or corrupted package" already scrolled past
|
||||
# without ever naming the USB stick it came off.
|
||||
diagnose_media() {
|
||||
local logs=("$LOG_FILE")
|
||||
|
||||
[[ -f /mnt/var/log/omarchy-install.log ]] && logs+=(/mnt/var/log/omarchy-install.log)
|
||||
[[ -n ${OMARCHY_FAILURE_TAIL_LOG:-} ]] && logs+=("$OMARCHY_FAILURE_TAIL_LOG")
|
||||
|
||||
omarchy-install-diagnose-media "${logs[@]}" 2>/dev/null
|
||||
}
|
||||
|
||||
# Rows the diagnosis occupies, its leading blank included.
|
||||
media_diagnosis_rows() {
|
||||
local diagnosis="$1"
|
||||
|
||||
if [[ -z $diagnosis ]]; then
|
||||
printf '0\n'
|
||||
else
|
||||
printf '%s\n' "$(( $(grep -c '' <<<"$diagnosis") + 1 ))"
|
||||
fi
|
||||
}
|
||||
|
||||
render_media_diagnosis() {
|
||||
local diagnosis="$1" line first=1
|
||||
|
||||
[[ -n $diagnosis ]] || return 0
|
||||
blank_line
|
||||
while IFS= read -r line; do
|
||||
# A blank line inside the diagnosis is deliberate: it separates what
|
||||
# happened from what to do about it, so it is drawn rather than skipped.
|
||||
if [[ -z $line ]]; then
|
||||
blank_line
|
||||
continue
|
||||
fi
|
||||
if (( first )); then
|
||||
center "${RED}$(truncate_for_width "$line" "$CONTENT_WIDTH")${RESET}" "$CONTENT_WIDTH"
|
||||
first=0
|
||||
else
|
||||
center "$(truncate_for_width "$line" "$CONTENT_WIDTH")" "$CONTENT_WIDTH"
|
||||
fi
|
||||
done <<<"$diagnosis"
|
||||
}
|
||||
|
||||
render_failure() {
|
||||
local status="$1" state_summary="${2:-}" line
|
||||
local status="$1" state_summary="${2:-}" media_diagnosis="${3:-}" line
|
||||
printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
|
||||
blank_line
|
||||
render_logo
|
||||
@@ -652,6 +699,8 @@ render_failure() {
|
||||
center "${RED}Omarchy installation stopped${RESET}" "$CONTENT_WIDTH"
|
||||
center "Installer exited with status $status" "$CONTENT_WIDTH"
|
||||
|
||||
render_media_diagnosis "$media_diagnosis"
|
||||
|
||||
if [[ -n $state_summary ]]; then
|
||||
blank_line
|
||||
while IFS= read -r line; do
|
||||
@@ -661,7 +710,7 @@ render_failure() {
|
||||
fi
|
||||
|
||||
blank_line
|
||||
render_failure_log_tail
|
||||
render_failure_log_tail "$(media_diagnosis_rows "$media_diagnosis")"
|
||||
blank_line
|
||||
center "${DIM}Get help at https://omarchy.org/discord${RESET}" "$CONTENT_WIDTH"
|
||||
blank_line
|
||||
@@ -702,11 +751,11 @@ failure_menu() {
|
||||
case "$choice" in
|
||||
"Upload log for support")
|
||||
upload_failure_log
|
||||
render_failure "${failure_status:-1}" "${failure_summary:-}" >"$TTY_PATH" 2>/dev/null || true
|
||||
render_failure "${failure_status:-1}" "${failure_summary:-}" "${failure_media_diagnosis:-}" >"$TTY_PATH" 2>/dev/null || true
|
||||
;;
|
||||
"View full log")
|
||||
view_failure_log
|
||||
render_failure "${failure_status:-1}" "${failure_summary:-}" >"$TTY_PATH" 2>/dev/null || true
|
||||
render_failure "${failure_status:-1}" "${failure_summary:-}" "${failure_media_diagnosis:-}" >"$TTY_PATH" 2>/dev/null || true
|
||||
;;
|
||||
"Reboot")
|
||||
reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
|
||||
@@ -792,7 +841,15 @@ if (( status != 0 )); then
|
||||
done <<<"$failure_summary"
|
||||
fi
|
||||
fi
|
||||
render_failure "$failure_status" "$failure_summary" >"$TTY_PATH" 2>/dev/null || true
|
||||
failure_media_diagnosis="$(diagnose_media || true)"
|
||||
render_failure "$failure_status" "$failure_summary" "$failure_media_diagnosis" >"$TTY_PATH" 2>/dev/null || true
|
||||
# Recorded after the screen is drawn, not before: the log tail on that screen
|
||||
# reads this same file, and the diagnosis printed twice reads as two findings.
|
||||
if [[ -n $failure_media_diagnosis ]]; then
|
||||
while IFS= read -r line; do
|
||||
[[ -n $line ]] && dashboard_log "$line"
|
||||
done <<<"$failure_media_diagnosis"
|
||||
fi
|
||||
failure_menu
|
||||
exit "$failure_status"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Say whether an install failure was the install medium, and say it in words
|
||||
# that name the fix.
|
||||
#
|
||||
# The ISO's offline mirror is bind-mounted onto the target's package cache for
|
||||
# the duration of pacstrap (see _mount_offline_package_cache), so a package
|
||||
# pacman calls corrupt under that path is a byte that came off the medium --
|
||||
# nothing else ever writes there. That much is certain, and it is the part
|
||||
# worth saying out loud, because the failure screen otherwise ends at "get help
|
||||
# at discord" with the word USB nowhere on it.
|
||||
#
|
||||
# What is not certain is which way the medium failed. Bytes written wrong and
|
||||
# bytes read wrong produce the same checksum mismatch, and pacstrap runs with
|
||||
# --noconfirm, which answers yes to pacman's "delete it?" and unlinks the
|
||||
# package through the bind mount before anything here can weigh it. So the
|
||||
# usual verdict names the medium and gives both remedies in the order worth
|
||||
# trying. When the package does survive, its checksum in the mirror's database
|
||||
# tells the two apart, and then this says which.
|
||||
#
|
||||
# Prints the diagnosis on stdout, one line per line, headline first. Exits 1
|
||||
# with no output when there is nothing here to diagnose, so the caller can ask
|
||||
# unconditionally.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
TARGET_PACKAGE_CACHE="${OMARCHY_TARGET_PACKAGE_CACHE:-/mnt/var/cache/pacman/pkg}"
|
||||
OFFLINE_MIRROR="${OMARCHY_OFFLINE_MIRROR:-/var/cache/omarchy/mirror/offline}"
|
||||
|
||||
if (( $# == 0 )); then
|
||||
echo "Usage: omarchy-install-diagnose-media <log-file>..." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Only packages under the target cache count. A corrupt package anywhere else
|
||||
# was not read off this ISO, and guessing wrong here sends someone to buy a USB
|
||||
# stick they did not need.
|
||||
corrupt_package() {
|
||||
local log match package=""
|
||||
|
||||
for log in "$@"; do
|
||||
[[ -f $log ]] || continue
|
||||
match=$(sed -n "s|.*${TARGET_PACKAGE_CACHE}/\([^ ]*\) is corrupted.*|\1|p" "$log" | tail -n 1)
|
||||
[[ -n $match ]] && package="$match"
|
||||
done
|
||||
|
||||
printf '%s' "$package"
|
||||
}
|
||||
|
||||
expected_checksum() {
|
||||
local package="$1" reader
|
||||
|
||||
[[ -f $OFFLINE_MIRROR/offline.db ]] || return 1
|
||||
|
||||
if command -v bsdtar >/dev/null 2>&1; then
|
||||
reader=(bsdtar -xOf "$OFFLINE_MIRROR/offline.db")
|
||||
else
|
||||
reader=(tar -xOzf "$OFFLINE_MIRROR/offline.db")
|
||||
fi
|
||||
|
||||
# repo-add writes %FILENAME% before %SHA256SUM% in every desc record, so the
|
||||
# name in hand when a checksum arrives is the name it belongs to. The wanted
|
||||
# name comes through the environment rather than -v, which would read a
|
||||
# backslash in a package version as an escape sequence.
|
||||
"${reader[@]}" 2>/dev/null | want="$package" awk '
|
||||
BEGIN { want = ENVIRON["want"] }
|
||||
$0 == "%FILENAME%" { getline name; next }
|
||||
$0 == "%SHA256SUM%" { getline sha; if (name == want) { print sha; exit } }
|
||||
'
|
||||
}
|
||||
|
||||
actual_checksum() {
|
||||
local package="$1" sum
|
||||
|
||||
[[ -f $OFFLINE_MIRROR/$package ]] || return 1
|
||||
sum=$(sha256sum "$OFFLINE_MIRROR/$package" 2>/dev/null) || return 1
|
||||
printf '%s' "${sum%% *}"
|
||||
}
|
||||
|
||||
package=$(corrupt_package "$@")
|
||||
if [[ -z $package ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The name reaches a terminal, and a package version may legally hold a
|
||||
# backslash that the renderer would read as an escape. Strip anything a package
|
||||
# filename has no business containing before printing it.
|
||||
display_package="${package//[^A-Za-z0-9._+:@%-]/}"
|
||||
|
||||
expected=$(expected_checksum "$package")
|
||||
actual=$(actual_checksum "$package")
|
||||
|
||||
# The package name goes on a line of its own throughout. Names run past fifty
|
||||
# characters and the screen is only as wide as the logo, so a name sharing a
|
||||
# line with the sentence that explains it truncates the explanation away. The
|
||||
# blank line after it separates what happened from what to do about it.
|
||||
if [[ -n $expected && -n $actual ]]; then
|
||||
if [[ $expected == "$actual" ]]; then
|
||||
# Intact now, so the bytes were misread rather than written wrong, and
|
||||
# re-downloading fixes neither of the things that could have done it.
|
||||
echo "The install medium was misread"
|
||||
echo "This package is intact on this ISO, so the download and the write were fine:"
|
||||
echo "$display_package"
|
||||
echo
|
||||
echo "Try another USB port or another stick, and test this machine's memory"
|
||||
else
|
||||
echo "The install medium is damaged"
|
||||
echo "This package no longer matches the checksum it was built with:"
|
||||
echo "$display_package"
|
||||
echo
|
||||
echo "Re-download the ISO, verify it with sha256sum, and write the USB again"
|
||||
echo "If a verified ISO fails here too, the USB stick itself is the problem"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The usual case: pacman deleted the package it rejected, so the medium is the
|
||||
# answer but the manner of its failure is not recoverable from here.
|
||||
echo "The install medium is damaged or misread"
|
||||
echo "pacman rejected this package, read straight off this ISO:"
|
||||
echo "$display_package"
|
||||
echo
|
||||
echo "Verify your download with sha256sum, then write the USB again"
|
||||
echo "If the download verifies, the stick or this machine's memory is at fault"
|
||||
@@ -39,6 +39,7 @@ file_permissions=(
|
||||
["/usr/local/bin/omarchy-cidata-load"]="0:0:755"
|
||||
["/usr/local/bin/omarchy-iso-cleanup-disk"]="0:0:755"
|
||||
["/usr/local/bin/omarchy-install-dashboard"]="0:0:755"
|
||||
["/usr/local/bin/omarchy-install-diagnose-media"]="0:0:755"
|
||||
["/usr/local/bin/omarchy-iso-install"]="0:0:755"
|
||||
["/usr/local/bin/omarchy-upload-log"]="0:0:755"
|
||||
["/var/cache/omarchy/mirror/offline/"]="0:0:775"
|
||||
|
||||
Executable
+452
@@ -0,0 +1,452 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Unit tests for omarchy-install-diagnose-media and the failure screen that
|
||||
# renders it. Every case builds a throwaway offline mirror -- a package file
|
||||
# and a real repo-add-shaped offline.db naming its sha256 -- so the three
|
||||
# verdicts are told apart by the same evidence the live ISO has.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
DIAGNOSE="$ROOT/configs/airootfs/usr/local/bin/omarchy-install-diagnose-media"
|
||||
DASHBOARD="$ROOT/configs/airootfs/usr/local/bin/omarchy-install-dashboard"
|
||||
PACKAGE="npth-1.8-1-x86_64.pkg.tar.zst"
|
||||
TARGET_CACHE="/mnt/var/cache/pacman/pkg"
|
||||
|
||||
pass() {
|
||||
printf 'ok - %s\n' "$1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
local description="$1"
|
||||
local detail="${2:-}"
|
||||
|
||||
[[ -n $detail ]] && printf '%s\n' "$detail" >&2
|
||||
printf 'not ok - %s\n' "$description" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
mirror="$work/mirror"
|
||||
mkdir -p "$mirror"
|
||||
|
||||
# repo-add writes one desc record per package, %FILENAME% before %SHA256SUM%.
|
||||
# Building a real gzipped tar of that shape keeps the parser honest.
|
||||
write_mirror_db() {
|
||||
local checksum="$1" filename="${2:-$PACKAGE}" staging="$work/db"
|
||||
|
||||
rm -rf "$staging"
|
||||
mkdir -p "$staging/npth-1.8-1"
|
||||
{
|
||||
printf '%%FILENAME%%\n%s\n\n' "$filename"
|
||||
printf '%%NAME%%\nnpth\n\n'
|
||||
printf '%%VERSION%%\n1.8-1\n\n'
|
||||
printf '%%SHA256SUM%%\n%s\n\n' "$checksum"
|
||||
} >"$staging/npth-1.8-1/desc"
|
||||
|
||||
# A second package proves the lookup matches on name rather than on order.
|
||||
mkdir -p "$staging/zlib-1.3-1"
|
||||
{
|
||||
printf '%%FILENAME%%\nzlib-1.3-1-x86_64.pkg.tar.zst\n\n'
|
||||
printf '%%NAME%%\nzlib\n\n'
|
||||
printf '%%SHA256SUM%%\n%s\n\n' "$(printf 'f%.0s' {1..64})"
|
||||
} >"$staging/zlib-1.3-1/desc"
|
||||
|
||||
tar -czf "$mirror/offline.db" -C "$staging" npth-1.8-1 zlib-1.3-1
|
||||
}
|
||||
|
||||
write_log() {
|
||||
local log="$1" path="${2:-$TARGET_CACHE/$PACKAGE}"
|
||||
|
||||
{
|
||||
echo "[dashboard] checking package integrity..."
|
||||
echo "[dashboard] :: File $path is corrupted (invalid or corrupted package)."
|
||||
echo "[dashboard] error: failed to commit transaction (invalid or corrupted package)"
|
||||
echo "[dashboard] ==> ERROR: Failed to install packages to new root"
|
||||
} >"$log"
|
||||
}
|
||||
|
||||
diagnose() {
|
||||
OMARCHY_OFFLINE_MIRROR="$mirror" OMARCHY_TARGET_PACKAGE_CACHE="$TARGET_CACHE" \
|
||||
"$DIAGNOSE" "$@"
|
||||
}
|
||||
|
||||
log="$work/install.log"
|
||||
write_log "$log"
|
||||
|
||||
# The medium was written wrong: the package on the ISO no longer hashes to what
|
||||
# the build recorded for it.
|
||||
printf 'a package, but damaged\n' >"$mirror/$PACKAGE"
|
||||
write_mirror_db "$(printf '0%.0s' {1..64})"
|
||||
|
||||
set +e
|
||||
output=$(diagnose "$log")
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
(( status == 0 )) || fail "a checksum mismatch is diagnosed" "exit status $status"
|
||||
[[ $(head -n 1 <<<"$output") == "The install medium is damaged" ]] ||
|
||||
fail "a checksum mismatch is diagnosed" "$output"
|
||||
grep -qF "$PACKAGE" <<<"$output" ||
|
||||
fail "the diagnosis names the package" "$output"
|
||||
grep -qF "no longer matches the checksum" <<<"$output" ||
|
||||
fail "the diagnosis says the checksum disagrees" "$output"
|
||||
grep -qF "sha256sum" <<<"$output" ||
|
||||
fail "the diagnosis says how to verify a fresh download" "$output"
|
||||
pass "a checksum mismatch says the medium is damaged and names the package"
|
||||
|
||||
# The medium reads clean now, so the bytes were misread rather than written
|
||||
# wrong. Re-downloading fixes nothing, and the advice has to say so.
|
||||
write_mirror_db "$(sha256sum "$mirror/$PACKAGE" | cut -d " " -f 1)"
|
||||
|
||||
set +e
|
||||
output=$(diagnose "$log")
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
(( status == 0 )) || fail "an intact package is still diagnosed" "exit status $status"
|
||||
[[ $(head -n 1 <<<"$output") == "The install medium was misread" ]] ||
|
||||
fail "an intact package is diagnosed as a misread" "$output"
|
||||
grep -qF "another USB port" <<<"$output" ||
|
||||
fail "a misread points at the hardware" "$output"
|
||||
if grep -qF "Re-download" <<<"$output"; then
|
||||
fail "a misread does not send anyone to re-download" "$output"
|
||||
fi
|
||||
pass "an intact package on the medium is diagnosed as a misread, not a bad download"
|
||||
|
||||
# The usual case. pacstrap runs with --noconfirm, which answers yes to pacman's
|
||||
# "delete it?" and unlinks the package through the bind mount, so by the time
|
||||
# anything here looks there is nothing left to weigh. The verdict must name the
|
||||
# medium without claiming a comparison it never made, and must offer both
|
||||
# remedies: a bad write and a bad read leave identical evidence.
|
||||
rm "$mirror/$PACKAGE"
|
||||
|
||||
set +e
|
||||
output=$(diagnose "$log")
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
(( status == 0 )) || fail "a deleted package is still diagnosed" "exit status $status"
|
||||
[[ $(head -n 1 <<<"$output") == "The install medium is damaged or misread" ]] ||
|
||||
fail "a deleted package is diagnosed without over-claiming" "$output"
|
||||
grep -qF "read straight off this ISO" <<<"$output" ||
|
||||
fail "a deleted package still names the medium" "$output"
|
||||
grep -qF "sha256sum" <<<"$output" ||
|
||||
fail "a deleted package offers the download remedy" "$output"
|
||||
grep -qF "memory is at fault" <<<"$output" ||
|
||||
fail "a deleted package offers the hardware remedy too" "$output"
|
||||
if grep -qF "no longer matches the checksum" <<<"$output"; then
|
||||
fail "a deleted package claims no comparison" "$output"
|
||||
fi
|
||||
pass "a deleted package names the medium without claiming which way it failed"
|
||||
|
||||
# A record with no %SHA256SUM% must not borrow the next record's, which would
|
||||
# compare a real file against another package's checksum.
|
||||
printf 'a package, but damaged\n' >"$mirror/$PACKAGE"
|
||||
staging="$work/db"
|
||||
rm -rf "$staging"
|
||||
mkdir -p "$staging/npth-1.8-1" "$staging/zlib-1.3-1"
|
||||
printf '%%FILENAME%%\n%s\n\n%%NAME%%\nnpth\n\n' "$PACKAGE" >"$staging/npth-1.8-1/desc"
|
||||
{
|
||||
printf '%%FILENAME%%\nzlib-1.3-1-x86_64.pkg.tar.zst\n\n'
|
||||
printf '%%SHA256SUM%%\n%s\n\n' "$(sha256sum "$mirror/$PACKAGE" | cut -d " " -f 1)"
|
||||
} >"$staging/zlib-1.3-1/desc"
|
||||
tar -czf "$mirror/offline.db" -C "$staging" npth-1.8-1 zlib-1.3-1
|
||||
|
||||
set +e
|
||||
output=$(diagnose "$log")
|
||||
set -e
|
||||
|
||||
[[ $(head -n 1 <<<"$output") == "The install medium is damaged or misread" ]] ||
|
||||
fail "a record without a checksum borrows nobody else's" "$output"
|
||||
pass "a record without a checksum borrows nobody else's"
|
||||
|
||||
# A package version may legally hold a backslash. It must not reach a terminal
|
||||
# as an escape sequence, and awk must look it up literally rather than as one.
|
||||
escaped='demo-1\033[2J-1-x86_64.pkg.tar.zst'
|
||||
write_log "$work/escaped.log" "$TARGET_CACHE/$escaped"
|
||||
printf 'a package, but damaged\n' >"$mirror/$escaped"
|
||||
write_mirror_db "$(printf '0%.0s' {1..64})" "$escaped"
|
||||
|
||||
set +e
|
||||
output=$(diagnose "$work/escaped.log")
|
||||
set -e
|
||||
|
||||
# The dashboard renders these lines through printf %b, which turns a surviving
|
||||
# backslash into whatever escape it spells. None may reach it.
|
||||
if grep -qF '\' <<<"$output"; then
|
||||
fail "a backslash in a package name never reaches the renderer" "$(cat -v <<<"$output")"
|
||||
fi
|
||||
grep -qF "demo-1033" <<<"$output" ||
|
||||
fail "the sanitized name still identifies the package" "$output"
|
||||
# The database record is found despite the backslash, so the verdict is the
|
||||
# mismatch rather than the one that means no comparison could be made.
|
||||
[[ $(head -n 1 <<<"$output") == "The install medium is damaged" ]] ||
|
||||
fail "a backslash in a package name is looked up literally" "$output"
|
||||
pass "a backslash in a package name is looked up literally and never rendered as an escape"
|
||||
rm "$mirror/$escaped"
|
||||
|
||||
# The live ISO has bsdtar through pacman's libarchive, but the tar fallback is
|
||||
# the one that runs if that ever stops being true.
|
||||
printf 'a package, but damaged\n' >"$mirror/$PACKAGE"
|
||||
write_mirror_db "$(sha256sum "$mirror/$PACKAGE" | cut -d " " -f 1)"
|
||||
no_bsdtar="$work/no-bsdtar"
|
||||
mkdir -p "$no_bsdtar"
|
||||
for tool in sed awk tar gzip sha256sum cat grep head tail cut printf; do
|
||||
command -v "$tool" >/dev/null && ln -sf "$(command -v "$tool")" "$no_bsdtar/$tool"
|
||||
done
|
||||
|
||||
set +e
|
||||
output=$(PATH="$no_bsdtar" diagnose "$log")
|
||||
set -e
|
||||
|
||||
[[ $(head -n 1 <<<"$output") == "The install medium was misread" ]] ||
|
||||
fail "the tar fallback reads the database when bsdtar is absent" "$output"
|
||||
pass "the tar fallback reads the database when bsdtar is absent"
|
||||
|
||||
printf 'a package, but damaged\n' >"$mirror/$PACKAGE"
|
||||
write_mirror_db "$(printf '0%.0s' {1..64})"
|
||||
|
||||
# A corrupt package that was never read off this ISO is somebody else's
|
||||
# problem. Guessing here sends people to buy a USB stick they did not need.
|
||||
write_log "$work/elsewhere.log" "/var/tmp/build/$PACKAGE"
|
||||
|
||||
set +e
|
||||
output=$(diagnose "$work/elsewhere.log")
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
(( status == 1 )) || fail "corruption outside the target cache is not diagnosed" "exit $status: $output"
|
||||
[[ -z $output ]] || fail "corruption outside the target cache prints nothing" "$output"
|
||||
pass "corruption outside the target cache is not blamed on the medium"
|
||||
|
||||
# An ordinary failure has no media diagnosis, and the caller asks
|
||||
# unconditionally, so silence has to be the answer.
|
||||
printf 'error: something else went wrong entirely\n' >"$work/other.log"
|
||||
|
||||
set +e
|
||||
output=$(diagnose "$work/other.log")
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
(( status == 1 )) || fail "an unrelated failure is not diagnosed" "exit $status: $output"
|
||||
[[ -z $output ]] || fail "an unrelated failure prints nothing" "$output"
|
||||
pass "an unrelated failure produces no diagnosis"
|
||||
|
||||
set +e
|
||||
output=$(diagnose "$work/does-not-exist.log")
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
(( status == 1 )) || fail "a missing log is not diagnosed" "exit $status: $output"
|
||||
pass "a missing log produces no diagnosis"
|
||||
|
||||
# End to end: the dashboard runs a failing installer and the diagnosis has to
|
||||
# reach both the screen and the support log, above the tail it explains.
|
||||
#
|
||||
# The screen goes through a real pty rather than a file. The dashboard's exit
|
||||
# handler reopens its TTY with a truncating redirect, which does nothing to a
|
||||
# character device and empties a regular file, so a file would capture nothing.
|
||||
stubs="$work/stubs"
|
||||
mkdir -p "$stubs"
|
||||
{
|
||||
echo '#!/bin/bash'
|
||||
echo "OMARCHY_OFFLINE_MIRROR='$mirror' OMARCHY_TARGET_PACKAGE_CACHE='$TARGET_CACHE' exec '$DIAGNOSE' \"\$@\""
|
||||
} >"$stubs/omarchy-install-diagnose-media"
|
||||
chmod +x "$stubs/omarchy-install-diagnose-media"
|
||||
|
||||
state_file="$work/state.json"
|
||||
screen="$work/screen"
|
||||
|
||||
# A ten-line logo, the height of the real one. Without it the dashboard falls
|
||||
# back to a single centred word and the screen can never be tall enough to
|
||||
# overflow, which is exactly the case worth testing.
|
||||
omarchy_share="$work/share"
|
||||
mkdir -p "$omarchy_share"
|
||||
# 81 columns wide, like the real logo: the dashboard takes its content width
|
||||
# from the logo, and a narrow one would truncate every line under test.
|
||||
{
|
||||
printf 'LOGO TOP ROW%*s\n' 69 ''
|
||||
for n in 2 3 4 5 6 7 8 9; do printf 'logo row %s%*s\n' "$n" 71 ''; done
|
||||
printf 'logo bottom row%*s\n' 66 ''
|
||||
} >"$omarchy_share/logo.txt"
|
||||
|
||||
run_dashboard() {
|
||||
local install_log="$1"
|
||||
|
||||
# A real failure carries the failed phase as well as the current one, and
|
||||
# that second summary line is another row the screen has to find space for.
|
||||
cat >"$state_file" <<'STATE'
|
||||
{"current_phase": "Installing Arch + Omarchy",
|
||||
"phases": [{"name": "Installing Arch + Omarchy", "status": "failed",
|
||||
"error": "Pacstrap failed. See /var/log/archinstall.log"}]}
|
||||
STATE
|
||||
: >"$screen"
|
||||
script -qefc "stty rows 40 cols 120; PATH='$stubs:$PATH' OMARCHY_PATH='$omarchy_share' OMARCHY_UI_INTERACTIVE=no OMARCHY_UI_FAILURE_ACTION=exit OMARCHY_FAILURE_TAIL_LOG='$install_log' '$DASHBOARD' '$install_log' '$state_file' -- bash -c 'exit 1'" \
|
||||
"$screen" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
visible_screen() {
|
||||
# Carriage returns as well as escapes: the pty ends every line with one, and
|
||||
# a row that looks blank still carries it.
|
||||
sed -E $'s/\x1b\\[[0-9;?]*[A-Za-z]//g; s/\r//g' "$screen"
|
||||
}
|
||||
|
||||
dashboard_log="$work/dashboard.log"
|
||||
write_log "$dashboard_log"
|
||||
|
||||
# A real install log is longer than the tail can show, so the tail actually
|
||||
# spends its whole budget. A short one hides an over-tall screen.
|
||||
{
|
||||
for n in $(seq 1 20); do printf '[dashboard] installing package %s\n' "$n"; done
|
||||
cat "$dashboard_log"
|
||||
} >"$dashboard_log.padded"
|
||||
mv "$dashboard_log.padded" "$dashboard_log"
|
||||
|
||||
# State the case rather than inheriting whichever verdict the previous one left
|
||||
# behind: this is the shape a real install fails in, with pacman having deleted
|
||||
# the package it rejected.
|
||||
rm -f "$mirror/$PACKAGE"
|
||||
write_mirror_db "$(printf '0%.0s' {1..64})"
|
||||
|
||||
set +e
|
||||
run_dashboard "$dashboard_log"
|
||||
dashboard_status=$?
|
||||
set -e
|
||||
|
||||
(( dashboard_status == 1 )) ||
|
||||
fail "the dashboard still exits with the installer's status" "exit $dashboard_status"
|
||||
visible_screen | grep -qF "The install medium is damaged" ||
|
||||
fail "the failure screen shows the diagnosis" "$(visible_screen | tail -n 25)"
|
||||
visible_screen | grep -qF "$PACKAGE" ||
|
||||
fail "the failure screen names the package" "$(visible_screen | tail -n 25)"
|
||||
grep -qF "[dashboard] The install medium is damaged" "$dashboard_log" ||
|
||||
fail "the diagnosis is written to the support log" "$(tail -n 20 "$dashboard_log")"
|
||||
|
||||
# Once on the screen. The log tail rendered underneath reads the same file the
|
||||
# diagnosis is recorded in, and the same finding printed twice reads as two.
|
||||
headline_count=$(visible_screen | grep -cF "The install medium is damaged or misread" || true)
|
||||
(( headline_count == 1 )) ||
|
||||
fail "the diagnosis appears once on the screen" "found $headline_count times"
|
||||
|
||||
# The renderer used to drop blank lines, so the break has to be checked where
|
||||
# it matters: on the screen, not only in the text the helper produced.
|
||||
package_row=$(visible_screen | grep -nF "$PACKAGE" | grep -v "dashboard" | head -n 1 | cut -d: -f1)
|
||||
[[ -n $package_row ]] ||
|
||||
fail "the screen names the package on its own row" "$(visible_screen | tail -n 25)"
|
||||
next_row=$(visible_screen | sed -n "$((package_row + 1))p")
|
||||
[[ -z ${next_row// /} ]] ||
|
||||
fail "the screen breaks after the package" "next row was: $next_row"
|
||||
pass "the dashboard renders the diagnosis once, with the break, and logs it"
|
||||
|
||||
# The diagnosis costs rows the log tail's budget knew nothing about, so a
|
||||
# screen carrying one scrolled its own logo off the top of the console. The
|
||||
# tail gives those rows back now, which is visible as a shorter tail: count the
|
||||
# filler lines it reaches, with a diagnosis and without one.
|
||||
#
|
||||
# Asserting the logo is still on screen would be the direct test, and it cannot
|
||||
# be written here: this reads the output stream, which keeps every row ever
|
||||
# emitted whether or not it stayed visible. Only a terminal emulator could tell
|
||||
# the difference, so the mechanism is what gets checked.
|
||||
tail_filler_rows() {
|
||||
visible_screen | grep -c "installing package" || true
|
||||
}
|
||||
|
||||
with_diagnosis=$(tail_filler_rows)
|
||||
|
||||
# The same log line for line, differing only in the one path that decides
|
||||
# whether there is a diagnosis at all. Two differently shaped logs would show
|
||||
# different amounts of filler for reasons that have nothing to do with the
|
||||
# budget, and the comparison would prove nothing.
|
||||
elsewhere_log="$work/elsewhere-padded.log"
|
||||
write_log "$elsewhere_log" "/var/tmp/build/$PACKAGE"
|
||||
{
|
||||
for n in $(seq 1 20); do printf '[dashboard] installing package %s\n' "$n"; done
|
||||
cat "$elsewhere_log"
|
||||
} >"$elsewhere_log.padded"
|
||||
mv "$elsewhere_log.padded" "$elsewhere_log"
|
||||
|
||||
set +e
|
||||
run_dashboard "$elsewhere_log"
|
||||
set -e
|
||||
without_diagnosis=$(tail_filler_rows)
|
||||
|
||||
(( with_diagnosis < without_diagnosis )) ||
|
||||
fail "the tail gives up rows to the diagnosis" \
|
||||
"with: $with_diagnosis, without: $without_diagnosis"
|
||||
pass "the log tail gives up rows to the diagnosis so the screen still fits"
|
||||
|
||||
# The screen is only as wide as the logo and the dashboard truncates to it with
|
||||
# an ellipsis. Every sentence has to survive that, whatever the package is
|
||||
# called, or the advice arrives with its second half cut off.
|
||||
long_package="linux-firmware-nvidia-tegra-20250917.0e800e46-1-any.pkg.tar.zst"
|
||||
long_log="$work/long.log"
|
||||
write_log "$long_log" "$TARGET_CACHE/$long_package"
|
||||
|
||||
check_widths() {
|
||||
local label="$1" line
|
||||
while IFS= read -r line; do
|
||||
(( ${#line} <= 81 )) ||
|
||||
fail "no diagnosis line outruns the screen ($label)" "${#line} chars: $line"
|
||||
done < <(diagnose "$long_log")
|
||||
}
|
||||
|
||||
# The package line and the remedy that follows it are two different thoughts,
|
||||
# so a blank line separates them in every verdict.
|
||||
check_break() {
|
||||
local label="$1" package="$2" out
|
||||
out=$(diagnose "$long_log")
|
||||
local -a lines
|
||||
mapfile -t lines <<<"$out"
|
||||
|
||||
local i
|
||||
for i in "${!lines[@]}"; do
|
||||
[[ ${lines[i]} == "$package" ]] || continue
|
||||
[[ -z ${lines[i + 1]-x} ]] ||
|
||||
fail "a break follows the package ($label)" "$out"
|
||||
[[ -n ${lines[i + 2]-} ]] ||
|
||||
fail "the remedy follows the break ($label)" "$out"
|
||||
return 0
|
||||
done
|
||||
fail "the package is on a line of its own ($label)" "$out"
|
||||
}
|
||||
|
||||
# All three verdicts, since each writes its own sentences.
|
||||
rm -f "$mirror/$long_package"
|
||||
check_widths "deleted"
|
||||
check_break "deleted" "$long_package"
|
||||
printf 'these are not the bytes that were built\n' >"$mirror/$long_package"
|
||||
write_mirror_db "$(printf '0%.0s' {1..64})" "$long_package"
|
||||
check_widths "damaged"
|
||||
check_break "damaged" "$long_package"
|
||||
write_mirror_db "$(sha256sum "$mirror/$long_package" | cut -d " " -f 1)" "$long_package"
|
||||
check_widths "misread"
|
||||
check_break "misread" "$long_package"
|
||||
rm -f "$mirror/$long_package"
|
||||
pass "no diagnosis line outruns the screen, even with a long package name"
|
||||
pass "a break separates the package from the remedy in every verdict"
|
||||
|
||||
# A failure with no media explanation must leave the screen as it was, rather
|
||||
# than gaining a banner or an empty gap where the diagnosis would go.
|
||||
printf 'error: something else went wrong entirely\n' >"$work/plain.log"
|
||||
|
||||
set +e
|
||||
run_dashboard "$work/plain.log"
|
||||
set -e
|
||||
|
||||
if visible_screen | grep -qF "install medium"; then
|
||||
fail "an unrelated failure gets no media banner" "$(visible_screen | tail -n 25)"
|
||||
fi
|
||||
visible_screen | grep -qF "Omarchy installation stopped" ||
|
||||
fail "an unrelated failure still renders the normal failure screen" "$(visible_screen | tail -n 25)"
|
||||
pass "an unrelated failure renders the failure screen unchanged"
|
||||
|
||||
# The failure menu redraws the screen after uploading or viewing the log, and a
|
||||
# redraw that drops the diagnosis loses it exactly when someone went looking.
|
||||
# Those paths need gum and a person at the keyboard, so they are checked here.
|
||||
missing_redraw=$(grep -n 'render_failure "' "$DASHBOARD" | grep -v 'failure_media_diagnosis' || true)
|
||||
[[ -z $missing_redraw ]] ||
|
||||
fail "every failure-screen redraw carries the diagnosis" "$missing_redraw"
|
||||
pass "every failure-screen redraw carries the diagnosis"
|
||||
Reference in new issue
Block a user