When update or install.sh fails and there's a terminal, ask whether to send its log to the CoreDesktop team, with the option of reading exactly what would be sent first. Nothing is sent without a yes, and nothing is asked without a terminal. The new report command sends a log later (report, report FILE) or shows what would be sent (report --view). Before sending, the log is cleaned of terminal codes, and the home folder, user name, host name, email addresses, IPv4 addresses and Tailscale names are replaced. CoreDesktop's version, the kernel's, the failed steps and sibling versions go along with it. The request format the receiver on federated.sh implements is described at the top of report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
205 lines
7.3 KiB
Bash
Executable File
205 lines
7.3 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Purpose : Send an install or update log to the CoreDesktop team, so they can
|
|
# work out what went wrong and fix it
|
|
#
|
|
# report Send the newest log, after showing what will be sent
|
|
# report FILE Send FILE
|
|
# report --view [FILE] Show what would be sent, and send nothing
|
|
# report --offer FILE [STEP...]
|
|
# What update and install.sh run when they fail: ask on
|
|
# the terminal whether to send FILE. STEP names what failed.
|
|
#
|
|
# Nothing is sent unless the user says yes. Before sending, the log is cleaned
|
|
# of terminal codes and personal details are replaced: the home folder, user
|
|
# name, host name, email addresses, IPv4 addresses and Tailscale names.
|
|
#
|
|
# What's sent (the receiver on federated.sh implements the other side): an
|
|
# HTTPS POST of multipart/form-data to $COREDESKTOP_REPORT_URL, with fields
|
|
# kind install or update, from the log's name
|
|
# version CoreDesktop's version, e.g. 2026.11.08 (a82bbfc)
|
|
# failed what failed, e.g. "71-siblings.sh, 46-neovim.sh" (may be empty)
|
|
# kernel the running kernel, from uname -r
|
|
# siblings the installed sibling projects, one "name version" per line
|
|
# log the cleaned, redacted log, gzipped (log.gz, application/gzip)
|
|
# A 2xx response's body is the report's ID, on one line: up to 40 letters,
|
|
# digits and dashes. Any other response's
|
|
# body is a message for the user.
|
|
|
|
COREDESKTOP="$HOME/.local/share/coredesktop"
|
|
LOGS="$HOME/.local/state/coredesktop/logs"
|
|
REPORT_URL="${COREDESKTOP_REPORT_URL:-https://federated.sh/report}"
|
|
TTY="${COREDESKTOP_TTY:-/dev/tty}"
|
|
# The receiver refuses bigger reports
|
|
MAX_BYTES=$(( 10 * 1024 * 1024 ))
|
|
|
|
usage() {
|
|
cat << EOF
|
|
Usage: report [FILE] Send a log (the newest, if none is named)
|
|
report --view [FILE] Show what would be sent, and send nothing
|
|
EOF
|
|
}
|
|
|
|
newest_log() {
|
|
local logs
|
|
mapfile -t logs < <(ls -t "$LOGS"/*.log 2> /dev/null)
|
|
[ "${#logs[@]}" -gt 0 ] && echo "${logs[0]}"
|
|
}
|
|
|
|
# Escape $1 for use in a sed regular expression
|
|
regex_escape() {
|
|
printf '%s' "$1" | sed 's/[][\.*^$/]/\\&/g'
|
|
}
|
|
|
|
# Print log $1 as it would be sent: without script(1)'s header and footer,
|
|
# terminal codes or progress redraws, and with personal details replaced.
|
|
# LC_ALL=C so the ranges mean bytes, whatever the locale. IPv4 addresses are
|
|
# replaced twice because a match takes the character after an address, which
|
|
# can be the one before the next; dotted versions such as 1.0.4.1.3 are kept.
|
|
redact() {
|
|
local home user host
|
|
home="$(regex_escape "$HOME")"
|
|
user="$(regex_escape "${USER:-$(id -un)}")"
|
|
host="$(regex_escape "$(uname -n)")"
|
|
LC_ALL=C sed -E \
|
|
-e '1{/^Script started on /d}' -e '/^Script done on .*COMMAND_EXIT_CODE/d' \
|
|
-e 's/\x1b\][^\x07\x1b]*(\x07|\x1b\\)//g' -e 's#\x1b\[[0-9;?]*[ -/]*[@-~]##g' \
|
|
-e 's/\x1b[()][0-9A-Za-z]//g' -e 's/\x1b[=>]//g' -e 's/\r+$//' -e 's/.*\r//' \
|
|
-e "s#${home}([/[:space:]'\":]|\$)#~\\1#g" \
|
|
-e 's/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/<email>/g' \
|
|
-e 's/[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.ts\.net/<tailscale-name>/g' \
|
|
-e 's/(^|[^0-9.])([0-9]{1,3}\.){3}[0-9]{1,3}([^0-9.]|$)/\1<ip>\3/g' \
|
|
-e 's/(^|[^0-9.])([0-9]{1,3}\.){3}[0-9]{1,3}([^0-9.]|$)/\1<ip>\3/g' \
|
|
-e "s/(^|[^A-Za-z0-9_-])${user}([^A-Za-z0-9_-]|\$)/\\1<user>\\2/g" \
|
|
-e "s/(^|[^A-Za-z0-9_-])${host}([^A-Za-z0-9_-]|\$)/\\1<host>\\2/g" \
|
|
"$1"
|
|
}
|
|
|
|
coredesktop_version() {
|
|
git -C "$COREDESKTOP" log -1 --format='%cd (%h)' --date=format:%Y.%m.%d 2> /dev/null || echo unknown
|
|
}
|
|
|
|
# "name version" for each installed sibling, without its URL, which can name a person
|
|
sibling_versions() {
|
|
[ -f "$COREDESKTOP/sibling" ] || return 0
|
|
bash "$COREDESKTOP/sibling" list 2> /dev/null | grep -v '^No sibling' \
|
|
| awk '{ $NF = ""; sub(/ +$/, ""); print }'
|
|
}
|
|
|
|
# show_redacted FILE: page through what would be sent
|
|
show_redacted() {
|
|
if command -v less > /dev/null && [ -t 1 ]; then
|
|
redact "$1" | less
|
|
else
|
|
redact "$1"
|
|
fi
|
|
}
|
|
|
|
# send FILE [STEP...]: send FILE's report and print its ID
|
|
send() {
|
|
local file="$1" kind tmp response code body
|
|
shift
|
|
kind="$(basename "$file")"
|
|
kind="${kind%%-*}"
|
|
[[ "$kind" == install || "$kind" == update ]] || kind=other
|
|
|
|
tmp="$(mktemp -d)" || return 1
|
|
redact "$file" | gzip -9 > "$tmp/log.gz"
|
|
if (( $(stat -c %s "$tmp/log.gz") > MAX_BYTES )); then
|
|
echo "This log is too big to send ($(( $(stat -c %s "$tmp/log.gz") / 1024 / 1024 )) MB compressed)."
|
|
rm -rf "$tmp"
|
|
return 1
|
|
fi
|
|
|
|
local failed
|
|
failed="$(printf '%s, ' "$@")"
|
|
failed="${failed%, }"
|
|
|
|
echo "Sending..."
|
|
# Values go in with -F name=<file, which sends them as they are: with
|
|
# name=value, curl would treat a value starting with @ or < as a file
|
|
printf '%s' "$kind" > "$tmp/kind"
|
|
coredesktop_version | tr -d '\n' > "$tmp/version"
|
|
printf '%s' "$failed" > "$tmp/failed"
|
|
uname -r | tr -d '\n' > "$tmp/kernel"
|
|
sibling_versions > "$tmp/siblings"
|
|
response="$(curl -sS --max-time 120 -A CoreDesktop-report \
|
|
-F "kind=<$tmp/kind" -F "version=<$tmp/version" -F "failed=<$tmp/failed" \
|
|
-F "kernel=<$tmp/kernel" -F "siblings=<$tmp/siblings" \
|
|
-F "log=@$tmp/log.gz;type=application/gzip;filename=log.gz" \
|
|
-w '\n%{http_code}' "$REPORT_URL" 2>&1)"
|
|
local status=$?
|
|
rm -rf "$tmp"
|
|
|
|
code="${response##*$'\n'}"
|
|
body="${response%$'\n'*}"
|
|
[[ "$response" == *$'\n'* ]] || body="$response"
|
|
# A 2xx that isn't a report ID is some other page, not the receiver
|
|
if (( status == 0 )) && [[ "$code" == 2?? && "$body" =~ ^[A-Za-z0-9-]{1,40}$ ]]; then
|
|
echo "Sent. Thank you! Your report's ID is $body; mention it if you contact us about this."
|
|
return 0
|
|
fi
|
|
echo "Couldn't send the report: ${body:-no response}"
|
|
echo "To try again later, run: report $file"
|
|
return 1
|
|
}
|
|
|
|
# Open the terminal once, on file descriptor 3, for ask to read answers from.
|
|
# Fails if there's no terminal to ask on.
|
|
open_tty() {
|
|
{ exec 3< "$TTY"; } 2> /dev/null
|
|
}
|
|
|
|
# ask QUESTION: read an answer from the terminal into $answer
|
|
ask() {
|
|
read -rp "$1" answer <&3
|
|
}
|
|
|
|
# offer FILE [STEP...]: ask whether to send FILE, show it first if asked
|
|
offer() {
|
|
local file="$1"
|
|
[ -f "$file" ] || return 0
|
|
open_tty || return 0
|
|
echo
|
|
echo "Send this log to the CoreDesktop team, so we can look into what went wrong?"
|
|
echo "Personal details such as your user name, host name and email addresses are"
|
|
echo "taken out first."
|
|
while ask "[v] view what would be sent, [y] send, [N] don't send: "; do
|
|
case "$answer" in
|
|
[vV]*) show_redacted "$file" ;;
|
|
[yY]*) send "$@"; return 0 ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
echo "Not sent. To send it later, run: report $file"
|
|
}
|
|
|
|
case "$1" in
|
|
-h|--help) usage ;;
|
|
--offer) shift; offer "$@" ;;
|
|
--view)
|
|
file="${2:-$(newest_log)}"
|
|
[ -f "$file" ] || { echo "No log to show."; exit 1; }
|
|
show_redacted "$file"
|
|
;;
|
|
-*) usage; exit 1 ;;
|
|
*)
|
|
file="${1:-$(newest_log)}"
|
|
[ -f "$file" ] || { echo "No log found in $LOGS."; exit 1; }
|
|
echo "Log: $file"
|
|
if ! open_tty; then
|
|
# No terminal to ask on: running report is itself the request to send
|
|
send "$file"
|
|
exit
|
|
fi
|
|
while true; do
|
|
ask "[v] view what would be sent, [y] send, [N] don't send: " || answer=n
|
|
case "$answer" in
|
|
[vV]*) show_redacted "$file" ;;
|
|
[yY]*) send "$file"; exit ;;
|
|
*) echo "Not sent."; exit 0 ;;
|
|
esac
|
|
done
|
|
;;
|
|
esac
|