#!/usr/bin/env bash # Goodly ディストロ図鑑 — 収集スクリプト(計測の手順 m1) # 手順: https://goodly.jp/distro-criteria/#cr-measure # # VM の中で、ユーザー goodly で動かします。出力(JSON)は検証記録の「状態の証拠」に使います。 # 手で写さないためのスクリプトです。中身は手順の版ごとに凍結しています(書き換えると受け付けません)。 # # curl -fsSLO https://goodly.jp/benchmark/collect-m1.sh (curl が無ければ wget) # bash collect-m1.sh fresh インストール直後: 識別・パッケージ数・使用量 # bash collect-m1.sh update OS の更新だけを流して、受信量と件数を測る(sudo を聞きます) # bash collect-m1.sh arm 更新後: 次のログインから3回、自動で測る。そのあと3回再起動する # bash collect-m1.sh status 自動の計測の進み具合 # bash collect-m1.sh updated 更新後のまとめ(3回そろってから) # bash collect-m1.sh send コード [file] 出力を Goodly に送る(コードは図鑑データの画面で出します) # bash collect-m1.sh reset 途中の記録を消す # # fresh と updated には --send コード を付けると、出したあとすぐ送ります。 set -u VERSION=1.0.0 PROTOCOL=m1 BENCH_SHA=c9c47575df67bbeaebf502147686be50da913bd654721479150d9fb55b054db0 BASE=${GOODLY_BASE:-https://goodly.jp} RUNS=3 IDLE_WAIT=180 BROWSER_WAIT=60 TEST=0 # テストのときだけ: /etc や /proc を読む根と、待ち時間を変えられる(出力に test: true が付き、受け付けられない)。 SYS="" if [ "${GOODLY_TEST:-0}" = "1" ]; then TEST=1 SYS=${GOODLY_SYSROOT:-} IDLE_WAIT=${GOODLY_IDLE_WAIT:-1} BROWSER_WAIT=${GOODLY_BROWSER_WAIT:-1} fi STATE=${XDG_STATE_HOME:-$HOME/.local/state}/goodly-m1 OUT=$HOME/goodly-m1 AUTOSTART=${XDG_CONFIG_HOME:-$HOME/.config}/autostart/goodly-m1.desktop say() { printf '%s\n' "$*" >&2; } die() { say "エラー: $*"; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } # ---------- JSON ---------- json_esc() { local s=$1 s=${s//\\/\\\\} s=${s//\"/\\\"} s=${s//$'\n'/\\n} s=${s//$'\r'/\\r} s=${s//$'\t'/\\t} printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037' } js() { printf '"%s"' "$(json_esc "${1:-}")"; } # 数(読めなければ null) jn() { case "${1:-}" in '' | *[!0-9.]* | *.*.*) printf 'null' ;; *) printf '%s' "$1" ;; esac } # ---------- 読む ---------- script_sha() { local self=$0 if [ -f "$self" ] && have sha256sum; then sha256sum "$self" | awk '{print $1}' fi } os_release_value() { local file="$SYS/etc/os-release" [ -f "$file" ] || file="$SYS/usr/lib/os-release" [ -f "$file" ] || return 0 awk -F= -v key="$1" -v q="'" '$1 == key { v = substr($0, length($1) + 2); c = substr(v, 1, 1); if (c == "\"" || c == q) v = substr(v, 2, length(v) - 2); print v; exit }' "$file" } os_release_json() { local first=1 key value printf '{' for key in ID ID_LIKE NAME VERSION VERSION_ID VERSION_CODENAME PRETTY_NAME BUILD_ID VARIANT VARIANT_ID IMAGE_ID IMAGE_VERSION; do value=$(os_release_value "$key") [ -n "$value" ] || continue [ $first -eq 1 ] || printf ',' first=0 printf '%s:%s' "$(js "$key")" "$(js "$value")" done printf '}' } mem_kb() { awk -v key="$1:" '$1 == key { print $2; exit }' "$SYS/proc/meminfo" } # MemTotal − MemAvailable(MB = 1024 KiB) mem_used_mb() { awk '$1 == "MemTotal:" { t = $2 } $1 == "MemAvailable:" { a = $2 } END { if (t && a) printf "%.0f", (t - a) / 1024 }' "$SYS/proc/meminfo" } rx_bytes() { awk 'NR > 2 { sub(/^[ \t]+/, ""); split($0, p, ":"); if (p[1] != "lo") { split(p[2], f, " "); s += f[1] } } END { printf "%.0f", s }' "$SYS/proc/net/dev" } init_name() { local comm="" [ -r "$SYS/proc/1/comm" ] && comm=$(tr -d '\n' <"$SYS/proc/1/comm") printf '%s' "$comm" } package_manager() { if have nixos-version || [ -e "$SYS/etc/NIXOS" ]; then printf 'nix' elif have rpm-ostree && rpm-ostree status >/dev/null 2>&1; then printf 'rpm-ostree' elif have dpkg-query; then printf 'dpkg' elif have rpm; then printf 'rpm' elif have pacman; then printf 'pacman' else printf '' fi } rpm_ostree_version() { have rpm-ostree || return 0 rpm-ostree status -b 2>/dev/null | awk '/^[[:space:]]*Version:/ { print $2; exit }' } nixos_version() { have nixos-version && nixos-version 2>/dev/null | head -n 1 } debian_version() { [ -r "$SYS/etc/debian_version" ] && head -n 1 "$SYS/etc/debian_version" } first_version() { grep -oE '[0-9]+(\.[0-9]+)+' | head -n 1; } desktop_json() { local raw=${XDG_CURRENT_DESKTOP:-${DESKTOP_SESSION:-}} name="" version="" case "$raw" in *GNOME*) name=GNOME; have gnome-shell && version=$(gnome-shell --version 2>/dev/null | first_version) ;; *KDE*) name='KDE Plasma'; have plasmashell && version=$(plasmashell --version 2>/dev/null | first_version) ;; *Cinnamon*) name=Cinnamon; have cinnamon && version=$(cinnamon --version 2>/dev/null | first_version) ;; *XFCE*) name=Xfce; have xfce4-session && version=$(xfce4-session --version 2>/dev/null | first_version) ;; *MATE*) name=MATE; have mate-session && version=$(mate-session --version 2>/dev/null | first_version) ;; *Budgie*) name=Budgie; have budgie-desktop && version=$(budgie-desktop --version 2>/dev/null | first_version) ;; *LXQt*) name=LXQt; have lxqt-session && version=$(lxqt-session --version 2>/dev/null | first_version) ;; *COSMIC*) name=COSMIC ;; *) name=$raw ;; esac printf '{"name":%s,"version":%s,"raw":%s,"session":%s}' "$(js "$name")" "$(js "$version")" "$(js "$raw")" "$(js "${XDG_SESSION_TYPE:-}")" } default_browser() { have xdg-settings && xdg-settings get default-web-browser 2>/dev/null | head -n 1 } secure_boot() { local var if have mokutil; then mokutil --sb-state 2>/dev/null | grep -qi 'enabled' && { printf 'enabled'; return; } mokutil --sb-state 2>/dev/null | grep -qi 'disabled' && { printf 'disabled'; return; } fi for var in "$SYS"/sys/firmware/efi/efivars/SecureBoot-*; do [ -r "$var" ] || continue if [ "$(od -An -t u1 "$var" | awk '{print $NF}')" = "1" ]; then printf 'enabled'; else printf 'disabled'; fi return done printf 'unknown' } env_json() { local virt="" product="" firmware=bios host user have systemd-detect-virt && virt=$(systemd-detect-virt 2>/dev/null) [ -r "$SYS/sys/class/dmi/id/product_name" ] && product=$(head -n 1 "$SYS/sys/class/dmi/id/product_name") [ -d "$SYS/sys/firmware/efi" ] && firmware=uefi host=$(cat "$SYS/proc/sys/kernel/hostname" 2>/dev/null || hostname 2>/dev/null) user=$(id -un 2>/dev/null) printf '{"virt":%s,"product":%s,"vcpu":%s,"mem_total_mb":%s,"firmware":%s,"secure_boot":%s,"hostname":%s,"user":%s}' \ "$(js "$virt")" "$(js "$product")" "$(jn "$(nproc 2>/dev/null)")" \ "$(jn "$(awk '$1 == "MemTotal:" { printf "%.0f", $2 / 1024; exit }' "$SYS/proc/meminfo")")" \ "$(js "$firmware")" "$(js "$(secure_boot)")" "$(js "$host")" "$(js "$user")" } identity_json() { printf '{"os_release":%s,"debian_version":%s,"rpm_ostree_version":%s,"nixos_version":%s,"kernel":%s,"init":%s,"package_manager":%s,"desktop":%s,"browser":%s}' \ "$(os_release_json)" "$(js "$(debian_version)")" "$(js "$(rpm_ostree_version)")" "$(js "$(nixos_version)")" \ "$(js "$(uname -r)")" "$(js "$(init_name)")" "$(js "$(package_manager)")" "$(desktop_json)" "$(js "$(default_browser)")" } fetch() { if have curl; then curl -fsSL --max-time 60 "$1" elif have wget; then wget -qO- --timeout=60 "$1" else return 1 fi } benchmark_json() { local url="$BASE/benchmark/$PROTOCOL.html" got="" got=$(fetch "$url" 2>/dev/null | sha256sum | awk '{print $1}') # 空の中身の sha256(取れなかった) [ "$got" = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ] && got="" local ok=false [ "$got" = "$BENCH_SHA" ] && ok=true printf '{"url":%s,"sha256":%s,"expected":%s,"ok":%s}' "$(js "$url")" "$(js "$got")" "$(js "$BENCH_SHA")" "$ok" } # ---------- 測る ---------- # パッケージ数 → "数 方法" count_packages() { case "$(package_manager)" in nix) printf '%s nix-store' "$(nix-store -qR /run/current-system 2>/dev/null | wc -l | tr -d ' ')" ;; dpkg) printf '%s dpkg-query' "$(dpkg-query -W -f='${db:Status-Abbrev}\n' 2>/dev/null | grep -c '^ii')" ;; rpm | rpm-ostree) printf '%s rpm' "$(rpm -qa 2>/dev/null | wc -l | tr -d ' ')" ;; pacman) printf '%s pacman' "$(pacman -Qq 2>/dev/null | wc -l | tr -d ' ')" ;; *) printf ' none' ;; esac } count_flatpak() { if have flatpak; then flatpak list --app --columns=application 2>/dev/null | awk 'NF { n++ } END { print n + 0 }'; else printf '0'; fi } count_snap() { if have snap; then snap list 2>/dev/null | awk 'NR > 1 && NF { n++ } END { print n + 0 }'; else printf '0'; fi } # OS が使うパーティションの使用量(バイト)→ "バイト 方法"。同じデバイスは1回だけ。Btrfs は filesystem usage の Used。 disk_used() { local total=0 method="" seen=" " src fstype used target b while read -r src fstype used target; do case "$src" in /dev/*) ;; *) continue ;; esac case "$seen" in *" $src "*) continue ;; esac seen="$seen$src " if [ "$fstype" = "btrfs" ] && have btrfs; then b=$(btrfs filesystem usage -b "$target" 2>/dev/null | awk '/^[[:space:]]*Used:/ { print $2; exit }') [ -n "$b" ] || b=$(sudo -n btrfs filesystem usage -b "$target" 2>/dev/null | awk '/^[[:space:]]*Used:/ { print $2; exit }') if [ -n "$b" ]; then used=$b; fstype=btrfs-usage; fi fi total=$((total + used)) case " $method " in *" $fstype "*) ;; *) method="$method $fstype" ;; esac done < <(LC_ALL=C df -B1 --output=source,fstype,used,target -x tmpfs -x devtmpfs -x squashfs -x overlay -x efivarfs 2>/dev/null | tail -n +2) printf '%s df(%s)' "$total" "$(printf '%s' "$method" | sed 's/^ //; s/ /,/g')" } gb() { awk -v b="$1" 'BEGIN { printf "%.2f", b / 1000000000 }'; } boot_seconds() { have systemctl || return 0 local us us=$(systemctl show -p FinishTimestampMonotonic --value 2>/dev/null) case "$us" in '' | 0 | *[!0-9]*) return 0 ;; esac awk -v u="$us" 'BEGIN { printf "%.2f", u / 1000000 }' } wait_boot_finished() { local i=0 while [ $i -lt 600 ]; do [ -n "$(boot_seconds)" ] && return 0 sleep 2 i=$((i + 2)) done return 1 } open_url() { if have xdg-open; then xdg-open "$1" >/dev/null 2>&1 & elif have gio; then gio open "$1" >/dev/null 2>&1 & else return 1 fi } # ---------- 出力 ---------- emit() { local phase=$1 measurements=$2 notes=${3:-} mkdir -p "$OUT" local file file="$OUT/$PROTOCOL-$phase-$(date +%Y%m%d-%H%M%S).json" { printf '{"format":"goodly-collect","protocol":%s,"script":{"version":%s,"sha256":%s},"test":%s,"phase":%s,"collected_at":%s,' \ "$(js "$PROTOCOL")" "$(js "$VERSION")" "$(js "$(script_sha)")" "$([ $TEST -eq 1 ] && printf true || printf false)" "$(js "$phase")" "$(js "$(date -Iseconds)")" printf '"identity":%s,"env":%s,"benchmark":%s,"measurements":{%s},"notes":[%s]}\n' \ "$(identity_json)" "$(env_json)" "$(benchmark_json)" "$measurements" "$notes" } >"$file" cat "$file" say "" say "保存しました: $file" LAST_FILE=$file } send_file() { local code=$1 file=${2:-} [ -n "$code" ] || die "コードを付けてください(図鑑データ → 検証記録 → 「VM から受け取る」で出します)。" if [ -z "$file" ]; then file=$(ls -1t "$OUT"/$PROTOCOL-*.json 2>/dev/null | head -n 1) fi [ -f "$file" ] || die "送る出力がありません。先に fresh か updated を動かしてください。" local url="$BASE/wp-json/linuxpro/v1/distro-db/collect-inbox?code=$code" res if have curl; then res=$(curl -sS --max-time 60 -H 'Content-Type: application/json' --data-binary "@$file" "$url") elif have wget; then res=$(wget -qO- --timeout=60 --header='Content-Type: application/json' --post-file="$file" "$url") else die "curl も wget もありません。" fi case "$res" in *'"received":true'*) say "送りました($(basename "$file"))。図鑑データの画面で「届いた出力」から選べます。" ;; *) die "送れませんでした: $res" ;; esac } # ---------- 段階 ---------- cmd_fresh() { local pk pk_n pk_m du du_b du_m m pk=$(count_packages) pk_n=${pk%% *} pk_m=${pk#* } du=$(disk_used) du_b=${du%% *} du_m=${du#* } m="" if [ -n "$pk_n" ]; then m="\"packages\":{\"runs\":[$(jn "$pk_n")],\"method\":$(js "$pk_m"),\"extra\":{\"flatpak\":$(jn "$(count_flatpak)"),\"snap\":$(jn "$(count_snap)")}}" fi [ -n "$m" ] && m="$m," m="$m\"disk_install_gb\":{\"runs\":[$(gb "$du_b")],\"method\":$(js "$du_m")}" emit fresh "$m" } cmd_update() { local pm id vid rx0 rx1 count="" method="" status=0 pm=$(package_manager) id=$(os_release_value ID) vid=$(os_release_value VERSION_ID) mkdir -p "$STATE" say "OS の更新だけを流します。ほかのアプリで通信しないでください(受信量に混ざります)。" rx0=$(rx_bytes) case "$pm" in dpkg) sudo apt-get update -qq || status=$? count=$(LC_ALL=C apt-get -s -o Debug::NoLocking=1 full-upgrade 2>/dev/null | grep -c '^Inst ') sudo env DEBIAN_FRONTEND=noninteractive apt-get -y full-upgrade || status=$? method="apt full-upgrade" ;; rpm) if have dnf; then count=$(LC_ALL=C dnf -q check-update 2>/dev/null | awk 'NF == 3 && $1 ~ /\./' | wc -l | tr -d ' ') sudo dnf -y upgrade --refresh || status=$? method="dnf upgrade" elif have zypper; then local verb=up case "$id $vid" in *tumbleweed* | *" "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]) verb=dup ;; esac count=$(sudo env LC_ALL=C zypper --non-interactive $verb --dry-run 2>/dev/null | grep -oE 'The following [0-9]+ package' | grep -oE '[0-9]+' | head -n 1) sudo zypper --non-interactive $verb || status=$? method="zypper $verb" else die "この環境の更新の仕方が分かりません(dnf / zypper がありません)。" fi ;; pacman) sudo pacman -Sy || status=$? count=$(LC_ALL=C pacman -Qu 2>/dev/null | wc -l | tr -d ' ') sudo pacman -Su --noconfirm || status=$? method="pacman -Syu" ;; rpm-ostree) sudo rpm-ostree upgrade || status=$? method="rpm-ostree upgrade" ;; nix) sudo nixos-rebuild switch --upgrade || status=$? method="nixos-rebuild switch --upgrade" ;; *) die "この環境の更新の仕方が分かりません。" ;; esac rx1=$(rx_bytes) [ $status -eq 0 ] || say "更新のコマンドが $status で終わりました。画面の表示を確かめてください。" local mb mb=$(awk -v a="$rx0" -v b="$rx1" 'BEGIN { printf "%.0f", (b - a) / 1000000 }') { printf '"updates_mb":{"runs":[%s],"method":%s}' "$(jn "$mb")" "$(js "/proc/net/dev ($method)")" [ -n "$count" ] && printf ',"updates_count":{"runs":[%s],"method":%s}' "$(jn "$count")" "$(js "$method")" } >"$STATE/updates.part" say "" say "受信量: ${mb}MB${count:+ ・ 件数: $count}" say "次: bash collect-m1.sh arm を実行してから、再起動してください(3回)。" } cmd_arm() { mkdir -p "$STATE/runs" "$OUT" "$(dirname "$AUTOSTART")" cp "$0" "$OUT/collect-$PROTOCOL.sh" 2>/dev/null || die "スクリプトを $OUT に写せませんでした。" printf '%s\n' "$BASE" >"$STATE/base" cat >"$AUTOSTART" <>"$STATE/auto.log" 2>&1 [ -f "$STATE/base" ] && BASE=$(head -n 1 "$STATE/base") local boot_id n boot_id=$(cat "$SYS/proc/sys/kernel/random/boot_id" 2>/dev/null || date +%s) [ -f "$STATE/runs/$boot_id.json" ] && exit 0 n=$(ls -1 "$STATE/runs" 2>/dev/null | grep -c '\.json$') if [ "$n" -ge $RUNS ]; then rm -f "$AUTOSTART" exit 0 fi wait_boot_finished || { say "起動の完了を待てませんでした。"; exit 1; } local boot idle browser="" boot=$(boot_seconds) sleep "$IDLE_WAIT" idle=$(mem_used_mb) if open_url "$BASE/benchmark/$PROTOCOL.html"; then sleep "$BROWSER_WAIT" browser=$(mem_used_mb) fi printf '{"boot_s":%s,"ram_idle_mb":%s,"ram_browser_mb":%s,"at":%s}\n' "$(jn "$boot")" "$(jn "$idle")" "$(jn "$browser")" "$(js "$(date -Iseconds)")" >"$STATE/runs/$boot_id.json" n=$((n + 1)) say "$(date -Iseconds) run $n: boot=$boot idle=$idle browser=$browser" if [ "$n" -ge $RUNS ]; then rm -f "$AUTOSTART" have notify-send && notify-send "Goodly m1" "3回そろいました。端末で bash ~/goodly-m1/collect-m1.sh updated を実行してください。" else have notify-send && notify-send "Goodly m1" "$n/$RUNS 回目が終わりました。再起動してください。" fi } runs_values() { local key=$1 f v out="" for f in "$STATE"/runs/*.json; do [ -f "$f" ] || continue v=$(sed -n "s/.*\"$key\":\([0-9.]*\).*/\1/p" "$f") [ -n "$v" ] || continue out="$out${out:+,}$v" done printf '%s' "$out" } cmd_status() { local n n=$(ls -1 "$STATE/runs" 2>/dev/null | grep -c '\.json$') say "自動の計測: $n / $RUNS 回" [ -f "$AUTOSTART" ] && say "自動起動: 有効(次のログインで測ります)" || say "自動起動: 無効" [ -f "$STATE/updates.part" ] && say "更新の受信量: 記録ずみ" || say "更新の受信量: まだ(bash collect-m1.sh update)" [ -f "$STATE/auto.log" ] && tail -n 5 "$STATE/auto.log" >&2 } cmd_updated() { local n du du_b du_m m="" n=$(ls -1 "$STATE/runs" 2>/dev/null | grep -c '\.json$') [ "$n" -ge $RUNS ] || die "自動の計測がまだ $n / $RUNS 回です(bash collect-m1.sh status)。" [ -f "$STATE/updates.part" ] && m="$(cat "$STATE/updates.part")," m="$m\"boot_s\":{\"runs\":[$(runs_values boot_s)],\"method\":\"systemd FinishTimestampMonotonic\"}" m="$m,\"ram_idle_mb\":{\"runs\":[$(runs_values ram_idle_mb)],\"method\":\"/proc/meminfo MemTotal-MemAvailable\",\"launched\":\"autostart\"}" [ -n "$(runs_values ram_browser_mb)" ] && m="$m,\"ram_browser_mb\":{\"runs\":[$(runs_values ram_browser_mb)],\"method\":$(js "/proc/meminfo $(default_browser)"),\"launched\":\"autostart\"}" du=$(disk_used) du_b=${du%% *} du_m=${du#* } m="$m,\"disk_updated_gb\":{\"runs\":[$(gb "$du_b")],\"method\":$(js "$du_m")}" emit updated "$m" } cmd_reset() { rm -rf "$STATE" rm -f "$AUTOSTART" say "途中の記録を消しました。" } main() { local cmd=${1:-help} send="" shift || true if [ "${1:-}" = "--send" ]; then send=${2:-} fi case "$cmd" in fresh) cmd_fresh ;; update) cmd_update ;; arm) cmd_arm ;; auto) cmd_auto ;; status) cmd_status ;; updated) cmd_updated ;; send) send_file "${1:-}" "${2:-}"; return ;; reset) cmd_reset ;; version) printf '%s %s\n' "$PROTOCOL" "$VERSION" ;; *) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//' >&2; return ;; esac if [ -n "$send" ] && [ -n "${LAST_FILE:-}" ]; then send_file "$send" "$LAST_FILE" fi } LAST_FILE="" main "$@"