#!/bin/sh # Wait for a sustained quiet macOS window, then replace this process with COMMAND. set -eu if [ "$#" -lt 3 ] || [ "$2" != -- ]; then echo "usage: $0 RECEIPT.ndjson -- COMMAND [ARG ...]" >&2 exit 2 fi receipt=$1 shift 2 interval=${GEM_CLEAN_HOST_INTERVAL_SECONDS:-30} consecutive_required=${GEM_CLEAN_HOST_CONSECUTIVE_SAMPLES:-3} max_wait=${GEM_CLEAN_HOST_MAX_WAIT_SECONDS:-28800} max_load=${GEM_CLEAN_HOST_MAX_LOAD_PER_CPU:-1.0} max_busy=${GEM_CLEAN_HOST_MAX_SHORT_WINDOW_BUSY_CPU_PERCENT:-10.0} fixture=${GEM_CLEAN_HOST_FIXTURE_FILE:-} [ ! -e "$receipt" ] && [ ! -L "$receipt" ] || { echo "gem: clean-host receipt already exists: $receipt" >&2 exit 2 } [ -d "$(dirname "$receipt")" ] || { echo "gem: clean-host receipt parent does not exist: $(dirname "$receipt")" >&2 exit 2 } case $interval in ''|*[!0-9]*) echo "gem: invalid sample interval" >&2; exit 2 ;; esac case $consecutive_required in ''|*[!0-9]*) echo "gem: invalid consecutive sample count" >&2; exit 2 ;; esac case $max_wait in ''|*[!0-9]*) echo "gem: invalid maximum wait" >&2; exit 2 ;; esac [ "$consecutive_required" -gt 0 ] && [ "$max_wait" -gt 0 ] || { echo "gem: clean-host counts and maximum wait must be positive" >&2 exit 2 } for value in "$max_load" "$max_busy"; do awk -v value="$value" 'BEGIN { exit !(value ~ /^[0-9]+([.][0-9]+)?$/ && value > 0) }' || { echo "gem: clean-host thresholds must be positive numbers" >&2; exit 2; } done if ! command -v jq >/dev/null 2>&1; then echo "gem: clean-host launcher requires jq" >&2 exit 2 fi if [ -n "$fixture" ]; then [ -f "$fixture" ] || { echo "gem: clean-host fixture is missing" >&2; exit 2; } exec 3< "$fixture" elif [ "$(uname -s)" != Darwin ] || ! command -v sysctl >/dev/null 2>&1 || ! command -v pmset >/dev/null 2>&1 || ! command -v top >/dev/null 2>&1; then echo "gem: clean-host launcher requires macOS host-state tools" >&2 exit 2 fi if [ ! -x "$1" ] && ! command -v "$1" >/dev/null 2>&1; then echo "gem: clean-host target is not executable: $1" >&2 exit 2 fi start_epoch=$(date +%s) attempt=0 consecutive=0 while :; do attempt=$((attempt + 1)) timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ') if [ -n "$fixture" ]; then if ! IFS= read -r sample <&3; then echo "gem: clean-host fixture exhausted before a clean window" >&2 exit 75 fi else load_average=$(sysctl -n vm.loadavg) load_1m=$(printf '%s\n' "$load_average" | awk '{print $2}') logical_cpus=$(sysctl -n hw.logicalcpu) short_busy=$(LC_ALL=C top -l 2 -s 1 -n 0 -stats pid,cpu,command | awk -F'[:,%]' '/^CPU usage:/ {idle=$6} END { gsub(/^[[:space:]]+|[[:space:]]+$/, "", idle) if (idle !~ /^[0-9]+([.][0-9]+)?$/) exit 1 printf "%.3f", 100 - idle }') || { echo "gem: could not sample short-window macOS CPU state" >&2 exit 1 } power_state=$(LC_ALL=C pmset -g batt 2>&1 || true) thermal_state=$(LC_ALL=C pmset -g therm 2>&1 || true) ac_power=false case $power_state in *"Now drawing from 'AC Power'"*) ac_power=true ;; esac no_thermal_warning=false if printf '%s\n' "$thermal_state" | grep -Fq 'No thermal warning level has been recorded' && printf '%s\n' "$thermal_state" | grep -Fq 'No performance warning level has been recorded'; then no_thermal_warning=true elif printf '%s\n' "$thermal_state" | grep -Eq 'CPU_Scheduler_Limit[[:space:]]*=[[:space:]]*100' && printf '%s\n' "$thermal_state" | grep -Eq 'CPU_Speed_Limit[[:space:]]*=[[:space:]]*100'; then no_thermal_warning=true fi top_processes=$(ps -Ao pid,pcpu,pmem,etime,comm -r | sed -n '1,24p') sample=$(jq -cn --arg load_average "$load_average" --argjson load_1m "$load_1m" \ --argjson logical_cpus "$logical_cpus" --argjson short_busy "$short_busy" \ --arg power_state "$power_state" --argjson ac_power "$ac_power" \ --arg thermal_state "$thermal_state" \ --argjson no_thermal_warning "$no_thermal_warning" \ --arg top_processes "$top_processes" \ '{load_average: $load_average, load_1m: $load_1m, logical_cpus: $logical_cpus, short_window_busy_cpu_percent: $short_busy, power_state: $power_state, ac_power: $ac_power, thermal_state: $thermal_state, no_thermal_warning: $no_thermal_warning, top_processes: $top_processes}') fi if ! printf '%s\n' "$sample" | jq -e ' (.load_1m | type) == "number" and .load_1m >= 0 and (.logical_cpus | type) == "number" and .logical_cpus > 0 and (.short_window_busy_cpu_percent | type) == "number" and .short_window_busy_cpu_percent >= 0 and (.ac_power | type) == "boolean" and (.no_thermal_warning | type) == "boolean" ' >/dev/null; then echo "gem: invalid clean-host sample" >&2 exit 2 fi load_per_cpu=$(printf '%s\n' "$sample" | jq -r '.load_1m / .logical_cpus') clean=true awk -v actual="$load_per_cpu" -v maximum="$max_load" \ 'BEGIN {exit !(actual <= maximum)}' || clean=false short_busy=$(printf '%s\n' "$sample" | jq -r '.short_window_busy_cpu_percent') awk -v actual="$short_busy" -v maximum="$max_busy" \ 'BEGIN {exit !(actual <= maximum)}' || clean=false [ "$(printf '%s\n' "$sample" | jq -r '.ac_power')" = true ] || clean=false [ "$(printf '%s\n' "$sample" | jq -r '.no_thermal_warning')" = true ] || clean=false if [ "$clean" = true ]; then consecutive=$((consecutive + 1)) status=pass else consecutive=0 status=fail fi printf '%s\n' "$sample" | jq -c \ --arg timestamp "$timestamp" --arg status "$status" \ --argjson attempt "$attempt" --argjson load_per_cpu "$load_per_cpu" \ --argjson max_load "$max_load" --argjson max_busy "$max_busy" \ --argjson consecutive "$consecutive" \ --argjson required "$consecutive_required" ' {schema_version: 1, timestamp_utc: $timestamp, attempt: $attempt, status: $status, load_average: (.load_average // null), load_1m, logical_cpus, load_per_logical_cpu: $load_per_cpu, max_load_per_logical_cpu: $max_load, short_window_busy_cpu_percent, max_short_window_busy_cpu_percent: $max_busy, power_state: (.power_state // null), ac_power, thermal_state: (.thermal_state // null), no_thermal_warning, top_processes: (.top_processes // null), consecutive_clean_samples: $consecutive, required_consecutive_clean_samples: $required}' >> "$receipt" if [ "$consecutive" -ge "$consecutive_required" ]; then echo "gem: sustained clean-host window found after $attempt samples" >&2 exec "$@" fi now_epoch=$(date +%s) if [ $((now_epoch - start_epoch)) -ge "$max_wait" ]; then echo "gem: timed out waiting for a sustained clean-host window" >&2 exit 75 fi sleep "$interval" done