Plain-text replies usually end with one, and the ID check counted it as part of the ID, so a report that was sent was said to have failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
207 lines
7.4 KiB
Bash
Executable File
207 lines
7.4 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"
|
|
# Without the newline that usually ends a reply
|
|
body="${body%"${body##*[![:space:]]}"}"
|
|
# 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
|