diff --git a/cmake/tests-q4nx-cache-timer.cmake b/cmake/tests-q4nx-cache-timer.cmake new file mode 100644 index 000000000..8c930a3d1 --- /dev/null +++ b/cmake/tests-q4nx-cache-timer.cmake @@ -0,0 +1,20 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# engine#2263: scripts/install-q4nx-cache-timer.sh generates a systemd --user sweep for a box that never serves. Its +# --self-test checks the generated units, the refusals (--max-gib 0 means OFF, an empty --interval never fires) and the +# capability probe that stops a timer being installed against a binary older than engine#2251 -- all with stubs, so no +# systemd, no cache and no binary are touched. +add_test(NAME q4nx_cache_timer_self COMMAND ${CMAKE_SOURCE_DIR}/scripts/install-q4nx-cache-timer.sh --self-test) diff --git a/docs/npu.md b/docs/npu.md index 5020928e8..8c956db64 100644 --- a/docs/npu.md +++ b/docs/npu.md @@ -311,6 +311,16 @@ every day outlives a one-off imported yesterday. The value is read as a whole nu number too large to be a byte count is the largest bound that is one (it does not wrap to a small one), and a value that is not a number gets the default rather than an unbounded cache. +**A box that never serves never sweeps (engine#2263).** Both triggers — the write-time sweep and the manual +`1bit q4nx-cache evict` — need the binary to run, so a review, CI or build box that holds the cache and never starts a +serve keeps every entry it ever wrote. Measured on this box: **59 GiB in 12 h** with the command merged and nothing +invoking it, which put free space 3 GiB under the 175 GiB build gate; one manual 24 h age cut took it to 24 GiB and it +regrew. `scripts/install-q4nx-cache-timer.sh` installs the invoker as a `systemd --user` timer — hourly, +`--max-gib 40 --min-age 86400`, opt-in, nothing outside `~/.config/systemd/user` — and refuses to install against a +binary that predates the command (`1bit q4nx-cache` printing *unknown command*), because a timer naming a command the +binary lacks fails hourly and silently. `--dry-run` prints the units and verifies them without changing anything; +`--uninstall` removes them and leaves the cache alone. + **The packed lm-head is cached beside it (engine#1716).** A model whose head runs as int8 tiles packs those tiles on the host — gather `kpad × T` weights per tile, quantise each column to int8 with its own scale — and until this cache that happened on every serve. The image is a function diff --git a/onnx/bench_npu.sh b/onnx/bench_npu.sh index 947b3e249..a8a7cfa91 100755 --- a/onnx/bench_npu.sh +++ b/onnx/bench_npu.sh @@ -45,6 +45,10 @@ # PORT serve port (default 52625) set -uo pipefail +# usage(): the header comment as prose, anchored on the comment block rather than a line range -- several of these ranges +# had drifted past the end of their header and printed code to whoever asked for help (the same defect engine PR #2258 fixed in scripts/). +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; } + BIN="${ONEBIT_BIN:-$HOME/src/engine-laya-npu/build-npu/1bit}" MODEL="${ONEBIT_MODEL:-$HOME/.config/flm/models/Qwen3-0.6B-NPU2}" KERNELS="${ONEBIT_DX_KERNELS:-}" @@ -60,7 +64,7 @@ while [ $# -gt 0 ]; do --kernels) KERNELS="$2"; shift 2 ;; --port) PORT="$2"; shift 2 ;; --lens|--gen|-w|--warmup|-r|--repeat|--mode|--endpoint) BENCH_ARGS+=("$1" "$2"); shift 2 ;; - -h|--help) sed -n '2,30p' "$0"; exit 0 ;; + -h|--help) usage; exit 0 ;; *) echo "bench_npu.sh: unknown option $1" >&2; exit 2 ;; esac done diff --git a/os/v2/mkdebs.sh b/os/v2/mkdebs.sh index 2dd8b0c4e..dba240388 100755 --- a/os/v2/mkdebs.sh +++ b/os/v2/mkdebs.sh @@ -42,7 +42,11 @@ # prefix (bin/lemond, bin/lemonade, bin/resources). is the release tag, with or without the # leading v: v2026.43 becomes 2026.43-1. set -euo pipefail -[ $# -ge 4 ] || { sed -n '22,45p' "$0"; exit 2; } + +# usage(): the header comment as prose, anchored on the comment block rather than a line range -- several of these ranges +# had drifted past the end of their header and printed code to whoever asked for help (the same defect engine PR #2258 fixed in scripts/). +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; } +[ $# -ge 4 ] || { usage; exit 2; } stage_in=$1 lemon=$2 version=${3#v} out=$4 keyring=${5:-} kstage=${KERNEL_STAGE:-} kver= here=$(cd "$(dirname "$0")" && pwd) src=$(cd "$here/../.." && pwd) diff --git a/os/v2/mkrepo.sh b/os/v2/mkrepo.sh index 4d3331fd6..2c51bd6a8 100755 --- a/os/v2/mkrepo.sh +++ b/os/v2/mkrepo.sh @@ -21,7 +21,11 @@ # Release file is produced unsigned and the script says so - useful for a local run, never for # publishing. The result is a directory of static files: publish it to 1bit.gg/apt as it is. set -euo pipefail -[ $# -ge 2 ] || { sed -n '21,27p' "$0"; exit 2; } + +# usage(): the header comment as prose, anchored on the comment block rather than a line range -- several of these ranges +# had drifted past the end of their header and printed code to whoever asked for help (the same defect engine PR #2258 fixed in scripts/). +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; } +[ $# -ge 2 ] || { usage; exit 2; } debs=$1 repo=$2 gnupg=${3:-} keyid=${4:-} [ -d "$debs" ] || { echo "mkrepo: no $debs"; exit 2; } dist=$repo/dists/stable diff --git a/os/v2/mkrootfs.sh b/os/v2/mkrootfs.sh index b3ec24e87..a963095f8 100755 --- a/os/v2/mkrootfs.sh +++ b/os/v2/mkrootfs.sh @@ -30,7 +30,11 @@ # The repository goes in at /srv/1bit-apt and apt reads it as file://, so the signed-by chain is the # same one https would use: an unsigned or wrongly signed Release file fails the build here. set -euo pipefail -[ $# -ge 2 ] || { sed -n '21,32p' "$0"; exit 2; } + +# usage(): the header comment as prose, anchored on the comment block rather than a line range -- several of these ranges +# had drifted past the end of their header and printed code to whoever asked for help (the same defect engine PR #2258 fixed in scripts/). +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; } +[ $# -ge 2 ] || { usage; exit 2; } repo=$(cd "$1" && pwd) rootfs=$2 sshkey=${3:-} here=$(cd "$(dirname "$0")" && pwd) . "$here/pin.txt" diff --git a/os/v2/proto.sh b/os/v2/proto.sh index 637efe572..383f6b842 100755 --- a/os/v2/proto.sh +++ b/os/v2/proto.sh @@ -25,7 +25,11 @@ # directory to seed the data partition with (so the VM needs no network for Lemonade's backends), and # [model.gguf] a small model for the chat row. set -euo pipefail -[ $# -ge 3 ] || { sed -n '21,32p' "$0"; exit 2; } + +# usage(): the header comment as prose, anchored on the comment block rather than a line range -- several of these ranges +# had drifted past the end of their header and printed code to whoever asked for help (the same defect engine PR #2258 fixed in scripts/). +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; } +[ $# -ge 3 ] || { usage; exit 2; } w=$1 stage=$2 lemon=$3 backends=${4:-} model=${5:-$HOME/models/Qwen3-0.6B-Q4_K_M.gguf} here=$(cd "$(dirname "$0")" && pwd) version=v2026.43 diff --git a/os/v2/vmtest-v2.sh b/os/v2/vmtest-v2.sh index be5c64de9..1826b9557 100755 --- a/os/v2/vmtest-v2.sh +++ b/os/v2/vmtest-v2.sh @@ -29,7 +29,11 @@ # # Prints VMTEST-V2 PASS or FAIL. set -uo pipefail -[ $# -ge 2 ] || { sed -n '21,34p' "$0"; exit 2; } + +# usage(): the header comment as prose, anchored on the comment block rather than a line range -- several of these ranges +# had drifted past the end of their header and printed code to whoever asked for help (the same defect engine PR #2258 fixed in scripts/). +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; } +[ $# -ge 2 ] || { usage; exit 2; } rootfs=$(cd "$1" && pwd) seed=$(cd "$2" && pwd) key=${3:-} # an mmdebstrap rootfs is root-owned with root-only files: populating a filesystem image from it and # handing it to qemu needs root, so the whole test runs as root rather than half of it diff --git a/os/v3/puppy/demo-seq-probe.sh b/os/v3/puppy/demo-seq-probe.sh index bee7bb3ca..569565d1e 100755 --- a/os/v3/puppy/demo-seq-probe.sh +++ b/os/v3/puppy/demo-seq-probe.sh @@ -43,6 +43,10 @@ # 1 when any did not, 2 on usage/setup error. set -uo pipefail +# usage(): the header comment as prose, anchored on the comment block rather than a line range -- several of these ranges +# had drifted past the end of their header and printed code to whoever asked for help (the same defect engine PR #2258 fixed in scripts/). +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; } + rounds=10 url=${LEMONADE:-http://1bit-os.local:13305/api/v1} npu=${NPU_MODEL:-Qwen3-0.6B-NPU-1bit} @@ -56,7 +60,7 @@ while [ $# -gt 0 ]; do --big) big=${2:?}; shift 2 ;; --dmesg) dmesg_file=${2:?}; shift 2 ;; --state) state=${2:?}; shift 2 ;; - -h | --help) sed -n '28,48p' "$0"; exit 0 ;; + -h | --help) usage; exit 0 ;; *[!0-9]*) echo "demo-seq-probe: unknown argument: $1" >&2; exit 2 ;; *) rounds=$1; shift ;; esac diff --git a/scripts/install-q4nx-cache-timer.sh b/scripts/install-q4nx-cache-timer.sh new file mode 100755 index 000000000..e2ef3bd0b --- /dev/null +++ b/scripts/install-q4nx-cache-timer.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# install-q4nx-cache-timer.sh: install a systemd --user timer that runs `1bit q4nx-cache evict` on a schedule +# (engine#2263). +# +# The cache's own sweep runs at SERVE time: after writing an entry, `1bit serve` evicts least-recently-used entries +# down to ONEBIT_Q4NX_CACHE_MAX_GIB. A box that never serves - a review, CI or build lane - therefore never sweeps, +# and ~/.cache/1bit/q4nx grows until it pushes the box under the 175 GiB build gate. Measured on this box: 59 GiB in +# 12 h with the evict command merged and nothing invoking it, then 24 GiB after one manual age cut, which regrew. +# +# This installs the invoker. It is OPT-IN and touches nothing outside the calling user's systemd --user directory: no +# root, no /etc, no repo state. Nothing in the repository runs it for you, by design - engine#2263's fix is a box +# configuration change, and this is the one command that makes it. +# +# scripts/install-q4nx-cache-timer.sh # install, hourly, 40 GiB, 24 h age floor +# scripts/install-q4nx-cache-timer.sh --dry-run # print the units and verify them, change nothing +# scripts/install-q4nx-cache-timer.sh --uninstall # disable and remove the units +# scripts/install-q4nx-cache-timer.sh --self-test # the argument and unit generation, no systemd, no cache +# +# Options: --max-gib N (default 40; 0 is refused, it would evict everything eligible), --min-age SECONDS (default +# 86400), --interval HOURLY|DAILY|*:0/30 (a systemd OnCalendar expression, default hourly), --1bit PATH. +# +# Exit: 0 done, 1 refused or failed, 2 usage. +set -uo pipefail + +usage() { + awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0" +} + +max_gib=40 +min_age=86400 +interval=hourly +bin_arg="" +dry=0 +uninstall=0 + +die() { printf 'install-q4nx-cache-timer: %s\n' "$*" >&2; exit 1; } + +# The units, generated from the arguments and printed by both the real install and --dry-run, so a dry run shows exactly +# what would be written rather than a description of it. +service_unit() { # <1bit path> + cat < /dev/null 2>&1; then command -v 1bit; return; fi + if [ -x /opt/1bit/1bit ]; then printf '/opt/1bit/1bit'; return; fi + printf '' +} + +# Every flag that takes a value is checked for that value BEFORE `shift 2`. `shift 2` on a one-argument list does not shift +# at all -- it fails and leaves $# unchanged -- so a value flag given last used to spin this loop at full CPU for ever +# (measured: `timeout 5 script --dry-run --max-gib` exited 124, the timeout killing it). +needs_value() { [ $# -ge 2 ] || die "$1 needs a value"; } + +while [ $# -gt 0 ]; do + case "$1" in + --max-gib) needs_value "$@"; max_gib=$2; shift 2 ;; + --min-age) needs_value "$@"; min_age=$2; shift 2 ;; + --interval) needs_value "$@"; interval=$2; shift 2 ;; + --1bit) needs_value "$@"; bin_arg=$2; shift 2 ;; + --dry-run) dry=1; shift ;; + --uninstall) uninstall=1; shift ;; + --self-test) self=1; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument '$1' (try --help)" ;; + esac +done + +[ "$uninstall" = 0 ] || [ "${self:-0}" = 0 ] || die "--uninstall and --self-test are different jobs" + +if [ "${self:-0}" = 1 ]; then + tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT + fail=0 + ok() { printf 'ok %s\n' "$1"; } + bad() { printf 'FAIL %s\n' "$1"; fail=1; } + + s=$(service_unit /opt/1bit/1bit) + case "$s" in *'ExecStart="/opt/1bit/1bit" q4nx-cache evict --max-gib 40 --min-age 86400'*) + ok "the service carries the command and both bounds" ;; + *) bad "the service ExecStart is wrong: $s" ;; + esac + t=$(timer_unit) + case "$t" in *"OnCalendar=hourly"*) ok "the timer fires hourly by default" ;; *) bad "the timer is not hourly" ;; esac + case "$t" in *"WantedBy=timers.target"*) ok "and is installed into timers.target" ;; *) bad "no [Install] section" ;; esac + case "$t" in *"Persistent=true"*) ok "Persistent=true, so a box asleep at the hour still sweeps once it wakes" ;; *) bad "not persistent" ;; esac + + # 0 is the one value that must never be installed: evicting to a 0 GiB bound removes every eligible entry, i.e. the + # whole cache, which is the opposite of housekeeping. + max_gib=0 + out=$(bash "$0" --max-gib 0 --dry-run 2>&1); rc=$? + [ "$rc" != 0 ] && ok "--max-gib 0 is refused (rc $rc)" || bad "--max-gib 0 should be refused, got rc 0" + case "$out" in *"disables the sweep"*) ok "...and says why: 0 means OFF in the CLI, so the timer would fire and do nothing" ;; *) bad "the refusal does not explain: $out" ;; esac + max_gib=40 + + out=$(bash "$0" --interval "" --dry-run 2>&1); rc=$? + [ "$rc" != 0 ] && ok "--interval with no value is refused (an empty OnCalendar never fires)" \ + || bad "--interval with no value should be refused" + + # capability probe: a binary that has the command passes, one that predates it is refused (measured on this box: the + # deployed rc-2026-11-03-c22 says "unknown command 'q4nx-cache'", so a timer installed against it would fail hourly) + good=$tmp/good-1bit; printf '#!/bin/sh\necho "usage: 1bit q4nx-cache evict [--root DIR] [--max-gib N]" >&2\nexit 2\n' > "$good"; chmod +x "$good" + out=$(bash "$0" --dry-run --1bit "$good" 2>&1); rc=$? + [ "$rc" = 0 ] && ok "a binary with the command is accepted ($rc)" || bad "a capable binary was refused: $out" + old=$tmp/old-1bit; printf '#!/bin/sh\necho "1bit: unknown command '"'"'q4nx-cache'"'"'" >&2\nexit 1\n' > "$old"; chmod +x "$old" + out=$(bash "$0" --dry-run --1bit "$old" 2>&1); rc=$? + [ "$rc" != 0 ] && case "$out" in *"predates engine#2251"*) ok "a binary that predates the command is refused, and named" ;; *) bad "refused for the wrong reason: $out" ;; esac \ + || bad "a binary without q4nx-cache should be refused, got rc 0" + + # a value flag given LAST must exit with a message rather than spin: `shift 2` on a one-argument list does not shift, + # so the loop used to run for ever (timeout 5 -> 124). + for flag in --max-gib --min-age --interval --1bit; do + out=$(timeout 5 bash "$0" --dry-run "$flag" 2>&1); rc=$? + case "$rc:$out" in + 124:*) bad "$flag given last SPUN (timeout killed it) -- the shift-2 bug is back" ;; + 0:*) bad "$flag given last exited 0; it must refuse" ;; + *"$flag needs a value"*) ok "$flag given last refuses: needs a value (rc $rc)" ;; + *) bad "$flag given last refused for the wrong reason: $out" ;; + esac + done + + # a relative --1bit is refused where the mistake is, not by systemd inside the unit + out=$(bash "$0" --dry-run --1bit relative/1bit 2>&1); rc=$? + case "$out" in *"wants an absolute path"*) ok "a relative --1bit is refused, naming the reason" ;; *) bad "relative --1bit: $out" ;; esac + + # the dry run writes nothing + before=$(ls -A "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" 2>/dev/null | grep -c 'q4nx' || true) + bash "$0" --dry-run > /dev/null 2>&1 + after=$(ls -A "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" 2>/dev/null | grep -c 'q4nx' || true) + [ "$before" = "$after" ] && ok "--dry-run installs nothing ($before q4nx unit(s) before and after)" \ + || bad "--dry-run changed the unit directory ($before -> $after)" + + [ "$fail" = 0 ] && { printf 'install_q4nx_cache_timer self-test: ok\n'; exit 0; } + printf 'install_q4nx_cache_timer self-test: FAILED\n'; exit 1 +fi + +case "$max_gib" in + ''|*[!0-9]*) die "--max-gib needs a whole number of GiB, got '$max_gib'" ;; +esac +if [ "$max_gib" -eq 0 ]; then + # The CLI treats 0 as "sweep disabled" (app/q4nx_cache.cpp's usage text), not as "evict everything" -- so a timer + # installed with it would fire hourly and do nothing, which is the failure this whole script exists to remove. + die "--max-gib 0 disables the sweep in the CLI, so the timer would fire and do nothing; give a positive bound" +fi +case "$min_age" in + ''|*[!0-9]*) die "--min-age needs a whole number of seconds, got '$min_age'" ;; +esac +# An empty OnCalendar= is a timer that never fires, and systemd accepts the unit anyway -- so refuse it here rather than +# install something that looks scheduled and is not. +[ -n "$interval" ] || die "--interval needs an OnCalendar expression, e.g. hourly or *:0/30" + +unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" +svc=$unit_dir/1bit-q4nx-cache.service +tmr=$unit_dir/1bit-q4nx-cache.timer + +if [ "$uninstall" = 1 ]; then + systemctl --user disable --now 1bit-q4nx-cache.timer 2>/dev/null || true + rm -f "$svc" "$tmr" + systemctl --user daemon-reload 2>/dev/null || true + printf 'removed %s and %s (the cache itself is untouched)\n' "$svc" "$tmr" + exit 0 +fi + +bin=$(resolve_1bit) +[ -n "$bin" ] || die "no 1bit binary: not on PATH and not at /opt/1bit/1bit (pass --1bit PATH)" +# A relative path reaches systemd as `ExecStart=build1336/1bit`, and systemd answers "Neither a valid executable name nor an +# absolute path" -- a unit-file complaint about a flag. Say it here instead, on the line the mistake was made on, and BEFORE +# the executability test so that a relative path gets this message rather than "is not executable". +case "$bin" in + /*) ;; + *) die "--1bit wants an absolute path, got '$bin' (systemd requires an absolute ExecStart)" ;; +esac +[ -x "$bin" ] || die "$bin is not executable" + +# A timer whose ExecStart names a command the binary does not have fails hourly and silently. The command landed in +# engine#2251, so a box still running an older 1bit has the cache and not the sweep: probe for it, with the subcommand +# that prints usage and returns 2, and refuse rather than schedule a no-op. +probe=$("$bin" q4nx-cache 2>&1 || true) +case "$probe" in + *"q4nx-cache evict"*) ;; + *) die "$bin has no q4nx-cache command (it predates engine#2251); install a current 1bit first. It said: $(printf '%s' "$probe" | head -1)" ;; +esac + +if [ "$dry" = 1 ]; then + printf '# would write %s\n%s\n# would write %s\n%s\n' "$svc" "$(service_unit "$bin")" "$tmr" "$(timer_unit)" + if command -v systemd-analyze > /dev/null 2>&1; then + tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT + service_unit "$bin" > "$tmp/1bit-q4nx-cache.service" + timer_unit > "$tmp/1bit-q4nx-cache.timer" + # The dry run's promise is that the units VERIFY, so say what systemd said. `is not executable` is dropped because a + # dry run against a not-yet-installed binary is the normal case; every other complaint is passed through and decides + # the exit code, because a unit systemd refuses would be installed and then never run. + if verdict=$(systemd-analyze verify "$tmp/1bit-q4nx-cache.service" "$tmp/1bit-q4nx-cache.timer" 2>&1); then + printf 'verify: systemd accepts both units\n' + else + # systemd adds "is not executable" for any ExecStart it cannot stat -- true of a dry run against a + # placeholder, and also the exact message a space-riddled path produces. Filtering it out left the + # operator with a bare "FAILED" for the one failure this check exists to catch, so print it all. + printf 'verify: FAILED\n%s\n' "$verdict" + printf 'dry run: nothing was changed, and nothing would be installed until this passes\n' + exit 1 + fi + printf 'dry run: nothing was changed\n' + fi + exit 0 +fi + +mkdir -p "$unit_dir" +service_unit "$bin" > "$svc" +timer_unit > "$tmr" +systemctl --user daemon-reload || die "systemctl --user daemon-reload failed (is a user manager running for $USER?)" +systemctl --user enable --now 1bit-q4nx-cache.timer || die "could not enable 1bit-q4nx-cache.timer" +printf 'installed %s\n %s\n\n' "$svc" "$tmr" +systemctl --user list-timers 1bit-q4nx-cache.timer --no-pager 2>/dev/null || true +printf '\nThe sweep runs %s: `%s q4nx-cache evict --max-gib %s --min-age %s`.\n' \ + "$interval" "$bin" "$max_gib" "$min_age" diff --git a/tools/xhci_keepawake_check.sh b/tools/xhci_keepawake_check.sh index 6aea65fe9..e255c083a 100755 --- a/tools/xhci_keepawake_check.sh +++ b/tools/xhci_keepawake_check.sh @@ -36,6 +36,10 @@ # 2 = usage/IO problem. Never 0 by accident: a machine where the rule does not apply says so. set -u +# usage(): the header comment as prose, anchored on the comment block rather than a line range -- several of these ranges +# had drifted past the end of their header and printed code to whoever asked for help (the same defect engine PR #2258 fixed in scripts/). +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; } + SYSFS=${SYSFS:-/sys} MODE=report @@ -56,7 +60,7 @@ while [ $# -gt 0 ]; do --apply) MODE=apply; shift;; --revert) MODE=revert; shift;; --sysfs-root) SYSFS=${2:?--sysfs-root needs a directory}; shift 2;; - -h|--help) sed -n '17,40p' "$0"; exit 0;; + -h|--help) usage; exit 0;; *) echo "xhci_keepawake_check: unknown argument '$1'" >&2; exit 2;; esac done