脚本:电源唤醒管理awake

原始信息

[cat@max2-nixos:~]$ # ① 所有唤醒位 + 当前状态(enabled=能唤醒 / disabled=不能)
for p in $(find /sys/bus/*/devices/*/power/wakeup /sys/class/power_supply/*/power/wakeup 2>/dev/null); do
  printf '%-9s %s\n' "$(cat "$p")" "$p"
done | sort
echo "--- 计数 ---"
for p in $(find /sys/bus/*/devices/*/power/wakeup /sys/class/power_supply/*/power/wakeup 2>/dev/null); do cat "$p"; done | sort | uniq -c

# ② 定时器总闸(26 号核验项)
for u in $(systemctl list-units --type=timer --all --no-legend --plain | awk '{print $1}'); do
  printf '%s\t%s\n' "$(systemctl show "$u" -p WakeSystem --value)" "$u"
done

# ③ 另一个口径(注意:awake list【不读】它,别拿来直接对表)
cat /proc/acpi/wakeup
disabled  /sys/bus/acpi/devices/PNP0C0A:00/power/wakeup
disabled  /sys/bus/acpi/devices/PNP0C0C:00/power/wakeup
disabled  /sys/bus/acpi/devices/PNP0C0E:00/power/wakeup
disabled  /sys/bus/i2c/devices/i2c-GXTP7385:00/power/wakeup
disabled  /sys/bus/pci/devices/0000:00:01.2/power/wakeup
disabled  /sys/bus/pci/devices/0000:00:02.2/power/wakeup
disabled  /sys/bus/pci/devices/0000:00:02.3/power/wakeup
disabled  /sys/bus/pci/devices/0000:00:02.4/power/wakeup
disabled  /sys/bus/pci/devices/0000:00:04.1/power/wakeup
disabled  /sys/bus/pci/devices/0000:00:08.1/power/wakeup
disabled  /sys/bus/pci/devices/0000:00:08.2/power/wakeup
disabled  /sys/bus/pci/devices/0000:00:08.3/power/wakeup
disabled  /sys/bus/pci/devices/0000:01:00.0/power/wakeup
disabled  /sys/bus/pci/devices/0000:02:00.0/power/wakeup
disabled  /sys/bus/pci/devices/0000:03:00.0/power/wakeup
disabled  /sys/bus/pci/devices/0000:04:00.0/power/wakeup
disabled  /sys/bus/pci/devices/0000:65:00.0/power/wakeup
disabled  /sys/bus/pci/devices/0000:65:00.1/power/wakeup
disabled  /sys/bus/pci/devices/0000:65:00.2/power/wakeup
disabled  /sys/bus/pci/devices/0000:65:00.4/power/wakeup
disabled  /sys/bus/pci/devices/0000:65:00.6/power/wakeup
disabled  /sys/bus/pci/devices/0000:66:00.0/power/wakeup
disabled  /sys/bus/pci/devices/0000:66:00.1/power/wakeup
disabled  /sys/bus/pci/devices/0000:67:00.0/power/wakeup
disabled  /sys/bus/pci/devices/0000:67:00.6/power/wakeup
disabled  /sys/bus/platform/devices/alarmtimer.0.auto/power/wakeup
disabled  /sys/bus/platform/devices/PNP0B00:00/power/wakeup
disabled  /sys/bus/thunderbolt/devices/0-0/power/wakeup
disabled  /sys/bus/thunderbolt/devices/domain0/power/wakeup
disabled  /sys/bus/usb/devices/1-5/power/wakeup
disabled  /sys/bus/usb/devices/3-1/power/wakeup
disabled  /sys/bus/usb/devices/7-1.1/power/wakeup
disabled  /sys/bus/usb/devices/7-1.4/power/wakeup
disabled  /sys/bus/usb/devices/7-1/power/wakeup
disabled  /sys/bus/usb/devices/usb1/power/wakeup
disabled  /sys/bus/usb/devices/usb2/power/wakeup
disabled  /sys/bus/usb/devices/usb3/power/wakeup
disabled  /sys/bus/usb/devices/usb4/power/wakeup
disabled  /sys/bus/usb/devices/usb5/power/wakeup
disabled  /sys/bus/usb/devices/usb6/power/wakeup
disabled  /sys/bus/usb/devices/usb7/power/wakeup
disabled  /sys/bus/usb/devices/usb8/power/wakeup
disabled  /sys/class/power_supply/ADP1/power/wakeup
disabled  /sys/class/power_supply/hid-0003:17EF:60EE.0015-battery/power/wakeup
enabled   /sys/bus/acpi/devices/PNP0C0D:00/power/wakeup
enabled   /sys/bus/pci/devices/0000:65:00.3/power/wakeup
enabled   /sys/bus/pci/devices/0000:67:00.3/power/wakeup
enabled   /sys/bus/pci/devices/0000:67:00.4/power/wakeup
enabled   /sys/bus/serio/devices/serio0/power/wakeup
enabled   /sys/bus/usb/devices/7-1.4.2/power/wakeup
--- 计数 ---
     44 disabled
      6 enabled
no      fstrim.timer
no      fwupd-refresh.timer
no      logrotate.timer
no      nix-gc.timer
no      systemd-tmpfiles-clean.timer
Device  S-state   Status   Sysfs node
GPP0      S4    *disabled
GPP6      S4    *disabled  pci:0000:00:02.2
GPP7      S4    *disabled  pci:0000:00:02.3
GP11      S4    *disabled
SWUS      S4    *disabled
GP12      S4    *disabled  pci:0000:00:04.1
SWUS      S4    *disabled
XHC0      S4    *enabled   pci:0000:65:00.3
XHC1      S4    *disabled  pci:0000:65:00.4
XHC2      S3    *disabled  pci:0000:67:00.0
XHC3      S3    *enabled   pci:0000:67:00.3
XHC4      S3    *enabled   pci:0000:67:00.4
NHI0      S4    *disabled
NHI1      S3    *disabled  pci:0000:67:00.6

awake list 列表数据结构设计

按照 power.wakeup(enabled和yes在前,disabled和no在后,path值作为第二排序字段)
path 													uevent.DRIVER	power.wakeup	power.wakeup_last_time_ms	conf.id	conf.wakeup	conf.desc
/sys/bus/acpi/devices/PNP0C0A:00						battery			enabled			yyyy-MM-dd hh:mm:ss	1自增	disable	备注描述可空
/sys/bus/platform/devices/alarmtimer.0.auto				alarmtimer		disabled		yyyy-MM-dd hh:mm:ss	2自增	disable	备注描述可空
/sys/bus/usb/devices/1-5								usb				disabled		yyyy-MM-dd hh:mm:ss	null	null	null
/sys/class/power_supply/hid-0003:17EF:60EE.0015-battery	battery			disabled		null				null	null	null
fstrim.timer											timer			no				null				2自增	no		Discard unused filesystem blocks once a week

awake.conf配置数据文件设计

path 								uevent.DRIVER	conf.id	conf.wakeup	conf.desc
/sys/bus/acpi/devices/PNP0C0A:00	battery			1自增	disable		备注描述可空,ai处理时
fstrim.timer						timer			2自增	no			Discard unused filesystem blocks once a week

命令说明

awake —— 管理「谁能唤醒这台机器」的事项清单(清单文件 + 系统运行时)

  awake list              运行时全量清单(含清单注解列;能唤醒的在前)
  awake setting           读清单:两组(能唤醒 / 不能唤醒)
  sudo awake on  <id…>    设为「能唤醒」—— 只改运行时,不动清单
  sudo awake off <id…>    设为「不能唤醒」—— 只改运行时,不动清单
  sudo awake sync         把运行时实况写回清单(新项插入并分配 id)⇒ 下次 rebuild 成为持久策略
  awake log [N] [-f]      唤醒日志(睡多久 + 唤醒原因)

  机制:清单只有一个文件 logind-data/awake-<机器码>.conf
        (机器码 = hardware-configuration.nix 里 fileSystems."/boot" 的 UUID,原文大小写)。
        nix 读它进本世代快照、rebuild 时按它重放运行时;on/off 只改运行时;
        sync 把运行时写回清单。设备写 sysfs 的 power/wakeup;timer 写 /run 下 drop-in。
  需要 root 的:on / off / sync(写 sysfs、/run 与清单文件)。

/etc/nixos/modules/system/service/logind.sh

#!/usr/bin/env bash
# logind.sh —— 「谁能唤醒这台机器」的事项清单管理工具(由 logind.nix 包装成命令 awake)
#
# 三个对象:
#   ① nix 配置层   logind.nix:logind 睡眠策略 + 唤醒位默认策略(udev)+ 重放单元(无轮询)
#   ② 事项清单     logind-data/awake-<机器码>.conf —— 唯一数据源
#                  机器码 = /etc/nixos/hardware-configuration.nix 里 fileSystems."/boot" 设备的 UUID(原文大小写)
#                  nix 读它(rebuild → 本世代快照 → apply 重放运行时);awake list/setting 读它;
#                  awake sync 把运行时实况写回它 ⇒ 下次 rebuild 成为持久策略
#   ③ 系统运行时   设备 <dir>/power/wakeup(enabled/disabled);timer 的 WakeSystem(yes/no)
#
# 清单列(TAB 分隔):path | driver | id | wakeup | desc
#   path   设备 = /sys/.../power/wakeup 所在的【设备目录】;timer = systemd unit 名(.timer 结尾)
#   driver 设备 = /sys/.../driver 符号链接(回退 uevent 的 DRIVER=,再取不到留空);timer = 固定 timer
#   id     自增编号(sync 新增行取 max+1,删行不重排)—— awake on/off 用它
#   wakeup 设备 = enabled/disabled(sysfs 原值);timer = yes/no(WakeSystem 原值)
#   desc   备注(设备可空;timer 默认取 unit 的 Description)
#
# 命令:
#   awake list             运行时全量 + 清单注解:path | driver | 运行时 | last_time | id | conf.wakeup | conf.desc
#   awake setting          清单的两组(能唤醒 / 不能唤醒)
#   awake on|off <id…>     只改【运行时】:设备写 sysfs;timer 写 /run 下 drop-in + daemon-reload
#   awake sync             运行时 → 清单(更新已有行;清单里没有的运行时项插入新行 + 分配 id)
#   awake log [N] [-f]     看唤醒日志
# 内部动词(供 nix 侧与睡眠钩子调用):
#   awake --apply          按【本世代清单快照】($AWAKE_SNAPSHOT)重放运行时
#   awake --snapshot       睡前快照唤醒源计数    awake --record-wake  醒后记账
#
# 退出码:0 成功 / 1 用法或参数错 / 2 需要 root
set -uo pipefail

DATA_DIR="${AWAKE_DATA_DIR:-/etc/nixos/modules/system/service/logind-data}"
HWCONF="${AWAKE_HWCONF:-/etc/nixos/hardware-configuration.nix}"
LOG="${AWAKE_LOG:-/var/log/logind-awake.log}"
RUN="${AWAKE_RUN:-/run/logind-awake}"
SNAPSHOT="${AWAKE_SNAPSHOT:-}"       # nix 注入的 store 快照;只有 --apply 用它
DROPIN=/run/systemd/system           # timer 的运行时改动落这里(tmpfs ⇒ 重启自清)

die() { printf 'awake: %s\n' "$*" >&2; exit 1; }
need_root() { [ "$(id -u)" = 0 ] || { printf 'awake: %s 需要 root —— 请用 sudo awake %s\n' "$1" "$2" >&2; exit 2; }; }

usage() {
  cat <<'EOF'
awake —— 管理「谁能唤醒这台机器」的事项清单(清单文件 + 系统运行时)

  awake list              运行时全量清单(含清单注解列;能唤醒的在前)
  awake setting           读清单:两组(能唤醒 / 不能唤醒)
  sudo awake on  <id…>    设为「能唤醒」—— 只改运行时,不动清单
  sudo awake off <id…>    设为「不能唤醒」—— 只改运行时,不动清单
  sudo awake sync         把运行时实况写回清单(新项插入并分配 id)⇒ 下次 rebuild 成为持久策略
  awake log [N] [-f]      唤醒日志(睡多久 + 唤醒原因)

  机制:清单只有一个文件 logind-data/awake-<机器码>.conf
        (机器码 = hardware-configuration.nix 里 fileSystems."/boot" 的 UUID,原文大小写)。
        nix 读它进本世代快照、rebuild 时按它重放运行时;on/off 只改运行时;
        sync 把运行时写回清单。设备写 sysfs 的 power/wakeup;timer 写 /run 下 drop-in。
  需要 root 的:on / off / sync(写 sysfs、/run 与清单文件)。
EOF
}

# ───────────────────────── 路径与取值 ─────────────────────────
SRC_CACHE=''
machine_id() {   # 机器码 = /boot 分区设备的 UUID(原文大小写;取自同一个文件,nix/sh 同一条规则)
  local id
  id=$(grep -A4 'fileSystems\."/boot"' "$HWCONF" 2>/dev/null | grep -o 'by-uuid/[A-Za-z0-9-]*' | head -1 | cut -d/ -f2)
  [ -n "$id" ] || die "取不到机器码:$HWCONF 里没有 fileSystems.\"/boot\" 的 by-uuid 设备"
  printf '%s' "$id"
}
src() {          # 源清单文件(命令面读写的那个)
  [ -n "$SRC_CACHE" ] && { printf '%s' "$SRC_CACHE"; return; }
  SRC_CACHE="$DATA_DIR/awake-$(machine_id).conf"
  printf '%s' "$SRC_CACHE"
}
conf_rows() { grep -vE '^[[:space:]]*(#|$)' "${1:-$(src)}" 2>/dev/null; }   # $1 = 文件

is_timer() { case "$1" in *.timer) return 0;; *) return 1;; esac; }
dev_wake()    { cat "$1/power/wakeup" 2>/dev/null; }
dev_last_ms() { cat "$1/power/wakeup_last_time_ms" 2>/dev/null; }
dev_driver() {
  local d=$1 drv=''
  [ -e "$d/driver" ] && drv=$(basename "$(readlink -f "$d/driver" 2>/dev/null)" 2>/dev/null)
  [ -n "$drv" ] || drv=$(awk -F= '/^DRIVER=/{print $2; exit}' "$d/uevent" 2>/dev/null)
  printf '%s' "$drv"
}
timer_wake() { systemctl show "$1" -p WakeSystem --value 2>/dev/null; }
unit_loaded() { [ "$(systemctl show "$1" -p LoadState --value 2>/dev/null)" = loaded ]; }

runtime_state()  { if is_timer "$1"; then timer_wake "$1"; else dev_wake "$1"; fi; }        # $1 = anchor
runtime_driver() { if is_timer "$1"; then printf 'timer'; else dev_driver "$1"; fi; }

runtime_anchors() {   # 运行时全量:设备目录 + timer unit
  local p u
  while IFS= read -r p; do [ -n "$p" ] && printf '%s\n' "${p%/power/wakeup}"; done \
    < <(find /sys/bus/*/devices/*/power/wakeup /sys/class/power_supply/*/power/wakeup 2>/dev/null)
  while IFS= read -r u; do [ -n "$u" ] && printf '%s\n' "$u"; done \
    < <(systemctl list-units --type=timer --all --no-legend --plain 2>/dev/null | awk '{print $1}')
}

ms_to_time() {   # 自开机起的毫秒 → yyyy-MM-dd HH:MM:SS;空/0 → null
  local ms=$1 up now
  { [ -n "$ms" ] && [ "$ms" != 0 ]; } || { printf 'null'; return; }
  up=$(cut -d' ' -f1 /proc/uptime); now=$(date +%s)
  date -d "@$(awk -v n="$now" -v u="$up" -v m="$ms" 'BEGIN{printf "%d", n-u+m/1000}')" '+%Y-%m-%d %H:%M:%S'
}

conf_lookup() {   # $1 = anchor, $2 = 文件 → 打印该行(TAB 分隔);无则返回 1
  awk -F'\t' -v a="$1" '!/^[[:space:]]*#/ && NF>=5 && $1==a {print; found=1} END{exit found?0:1}' "$2" 2>/dev/null
}
conf_maxid() { awk -F'\t' '!/^[[:space:]]*#/ && NF>=5 {if ($3+0>m) m=$3+0} END{print m+0}' "$1" 2>/dev/null; }
conf_proj() {   # 清单行投影成 '|' 分隔(空字段必须保住 —— bash 的 IFS=TAB 会把连续 TAB 折叠)
  awk -F'\t' -v OFS='|' '!/^[[:space:]]*#/ && NF>=5 {print $1,$2,$3,$4,$5}' "${1:-$(src)}" 2>/dev/null
}

# ───────────────────────── awake list ─────────────────────────
cmd_list() {
  local SRC; SRC=$(src)
  printf '%-46s %-13s %-9s %-19s %-4s %-9s %s\n' path driver 运行时 last_time id conf.wakeup conf.desc
  printf '%-46s %-13s %-9s %-19s %-4s %-9s %s\n' \
    ---------------------------------------------- ------------- --------- ------------------- ---- --------- --------------------------------
  local a st drv last row id cw cd key line out=''
  while IFS= read -r a; do
    [ -n "$a" ] || continue
    st=$(runtime_state "$a"); drv=$(runtime_driver "$a")
    if is_timer "$a"; then last=null; else last=$(ms_to_time "$(dev_last_ms "$a")"); fi
    id=null; cw=null; cd=null
    if row=$(conf_lookup "$a" "$SRC"); then
      id=$(printf '%s' "$row" | cut -f3); cw=$(printf '%s' "$row" | cut -f4); cd=$(printf '%s' "$row" | cut -f5)
      [ -n "$id" ] || id=null; [ -n "$cw" ] || cw=null; [ -n "$cd" ] || cd=null
    fi
    [ -n "$st" ] || st='—(不在位)'
    if [ "$st" = enabled ] || [ "$st" = yes ]; then key=0; else key=1; fi
    out="$out$key|$a|$drv|$st|$last|$id|$cw|$cd"$'\n'
  done < <(runtime_anchors)
  printf '%s' "$out" | LC_ALL=C sort -s -t'|' -k1,1n -k2,2 | while IFS='|' read -r key a drv st last id cw cd; do
    [ -n "$a" ] || continue
    printf '%-46s %-13s %-9s %-19s %-4s %-9s %s\n' "$a" "$drv" "$st" "$last" "$id" "$cw" "$cd"
  done

  # 清单里有、运行时不在位(拔掉的设备 / 卸载的 timer)
  local some=0
  while IFS='|' read -r a drv id cw cd; do
    [ -n "${a:-}" ] || continue
    if is_timer "$a"; then unit_loaded "$a" && continue
    else [ -e "$a/power/wakeup" ] && continue; fi
    if [ "$some" = 0 ]; then printf '\n清单里有、运行时不在位:\n'; some=1; fi
    printf '%-46s %-13s %-9s %-19s %-4s %-9s %s\n' "$a" "$drv" '—(不在位)' null "$id" "$cw" "$cd"
  done < <(conf_proj "$SRC")

  # 运行时能唤醒、但清单里没有(未登记)—— 单独点名,避免漏网
  local miss='' 
  while IFS= read -r a; do
    [ -n "$a" ] || continue
    case "$(runtime_state "$a")" in enabled|yes) ;; *) continue;; esac
    conf_lookup "$a" "$SRC" >/dev/null && continue
    miss="$miss$a"$'\n'
  done < <(runtime_anchors)
  if [ -n "$miss" ]; then
    printf '\n⚠ 运行时能唤醒、但清单里还没登记(先 sudo awake sync 登记后才能按 id 操作):\n'
    printf '%s' "$miss" | while IFS= read -r a; do [ -n "$a" ] && printf '  %s\n' "$a"; done
  fi

  # 定时器总闸:有几个 timer 设了 WakeSystem=yes
  local n=0 yes=0 u
  while IFS= read -r u; do
    [ -n "$u" ] || continue; n=$((n+1))
    [ "$(timer_wake "$u")" = yes ] && yes=$((yes+1))
  done < <(systemctl list-units --type=timer --all --no-legend --plain 2>/dev/null | awk '{print $1}')
  printf '\n定时器总闸:%s 个 timer 中 %s 个能唤醒(WakeSystem=yes)%s\n' "$n" "$yes" \
    "$([ "$yes" = 0 ] && printf '—— 合规(无定时任务能唤醒)' || printf '—— 不合规')"
  printf '(清单:%s)\n' "$SRC"
}

# ───────────────────────── awake setting ─────────────────────────
cmd_setting() {
  local SRC; SRC=$(src)
  echo "清单($SRC):"
  local a drv id w cd on=0 off=0
  printf '\n【能唤醒】\n'
  while IFS='|' read -r a drv id w cd; do
    [ -n "${a:-}" ] || continue
    case "$w" in enabled|yes) printf '  %-4s %-9s %s%s\n' "$id" "$w" "$a" "${cd:+   ($cd)}"; on=$((on+1));; esac
  done < <(conf_proj "$SRC")
  [ "$on" = 0 ] && printf '  (空)\n'
  printf '\n【不能唤醒】\n'
  while IFS='|' read -r a drv id w cd; do
    [ -n "${a:-}" ] || continue
    case "$w" in disabled|no) printf '  %-4s %-9s %s%s\n' "$id" "$w" "$a" "${cd:+   ($cd)}"; off=$((off+1));; esac
  done < <(conf_proj "$SRC")
  [ "$off" = 0 ] && printf '  (空)\n'
  printf '\n合计 %s 项(能唤醒 %s / 不能 %s)\n' "$((on+off))" "$on" "$off"
}

# ───────────────────── 写运行时(设备 / timer)─────────────────────
set_runtime() {   # $1 = anchor  $2 = on|off → 0 改到 / 1 运行时不在位 / 2 真失败
  local a=$1 mode=$2 target
  if is_timer "$a"; then
    if [ "$mode" = on ]; then target=yes; else target=no; fi
    unit_loaded "$a" || { printf '运行时未在位(unit 未加载)'; return 1; }
    [ "$(timer_wake "$a")" = "$target" ] && return 0    # 已一致 ⇒ 不落 drop-in、不 daemon-reload
    local d="$DROPIN/$a.d"
    mkdir -p "$d" || { printf '建不了 %s' "$d"; return 2; }
    printf '[Timer]\nWakeSystem=%s\n' "$target" > "$d/50-awake.conf" || { printf '写 %s/50-awake.conf 失败' "$d"; return 2; }
    systemctl daemon-reload >/dev/null 2>&1 || { printf 'daemon-reload 失败'; return 2; }
    [ "$(timer_wake "$a")" = "$target" ] || { printf '写入后读回不是 %s(可能需 systemctl restart %s)' "$target" "$a"; return 2; }
    return 0
  fi
  if [ "$mode" = on ]; then target=enabled; else target=disabled; fi
  local f="$a/power/wakeup"
  [ -e "$f" ] || { printf '运行时未在位(无 %s)' "$f"; return 1; }
  [ -w "$f" ] || { printf '%s 不可写(器件不声明 remote wakeup?)' "$f"; return 1; }
  printf '%s' "$target" > "$f" 2>/dev/null || { printf '写 %s 失败' "$f"; return 2; }
  [ "$(dev_wake "$a")" = "$target" ] || { printf '写入后读回不是 %s' "$target"; return 2; }
  return 0
}

# ───────────────────────── awake on / off ─────────────────────────
cmd_toggle() {   # $1 = on|off  $2.. = conf.id
  local mode=$1; shift
  [ $# -gt 0 ] || die "用法:sudo awake $mode <id…>(id 见 awake list / setting)"
  need_root "$mode" "$mode $*"
  local SRC; SRC=$(src)
  local id row a note rep ok=0 bad=0 miss=0
  for id in "$@"; do
    case "$id" in *[!0-9]*) die "id 必须是数字:$id";; esac
    row=$(awk -F'\t' -v n="$id" '!/^[[:space:]]*#/ && NF>=5 && $3+0==n {print; exit}' "$SRC")
    if [ -z "$row" ]; then printf '  [%s] ✗ 清单里没有这个 id\n' "$id"; bad=$((bad+1)); continue; fi
    a=$(printf '%s' "$row" | cut -f1)
    note=$(set_runtime "$a" "$mode"); rep=$?
    if [ "$rep" = 0 ]; then
      printf '  [%s] ✓ %s → %s\n' "$id" "$a" "$([ "$mode" = on ] && printf '能唤醒' || printf '不能唤醒')"
      ok=$((ok+1))
    elif [ "$rep" = 1 ]; then
      printf '  [%s] ↷ %s —— %s\n' "$id" "$a" "$note"; miss=$((miss+1))
    else
      printf '  [%s] ✗ %s —— %s\n' "$id" "$a" "$note" >&2; bad=$((bad+1))
    fi
  done
  printf '合计:成功 %s,未在位而跳过 %s,失败 %s(清单未改动;满意后跑 sudo awake sync 固化)\n' "$ok" "$miss" "$bad"
  [ "$bad" = 0 ] || return 2
}

# ───────────────────────── awake sync ─────────────────────────
cmd_sync() {
  need_root sync sync
  local SRC; SRC=$(src)
  [ -f "$SRC" ] || die "清单不存在:$SRC"
  cp -f "$SRC" "$SRC.bak-$(date +%Y%m%d-%H%M%S)" || die "备份清单失败($SRC)"
  local hdr rows='' a drv id w cd st newid upd=0 add=0 same=0 have line
  hdr=$(awk 'BEGIN{p=1} /^[[:space:]]*#/ || /^[[:space:]]*$/ {if(p) print; next} {p=0}' "$SRC")
  newid=$(conf_maxid "$SRC")
  # ① 已有行:运行时值不同则更新(保留 id / desc / 行序)
  while IFS='|' read -r a drv id w cd; do
    [ -n "${a:-}" ] || continue
    st=$(runtime_state "$a")
    if [ -z "$st" ] || [ "$st" = "$w" ]; then
      printf -v line '%s\t%s\t%s\t%s\t%s' "$a" "$drv" "$id" "$w" "$cd"; rows="$rows$line"$'\n'; same=$((same+1))
    else
      printf '  ✎ %s:%s → %s\n' "$a" "$w" "$st"
      printf -v line '%s\t%s\t%s\t%s\t%s' "$a" "$drv" "$id" "$st" "$cd"; rows="$rows$line"$'\n'; upd=$((upd+1))
    fi
  done < <(conf_proj "$SRC")
  # ② 运行时新项:插入新行(分配 id;driver 取运行时值;desc 留空)
  while IFS= read -r a; do
    [ -n "$a" ] || continue
    have=$(awk -F'\t' -v x="$a" '!/^[[:space:]]*#/ && NF>=5 && $1==x {print 1; exit}' "$SRC")
    [ -n "$have" ] && continue
    newid=$((newid+1)); drv=$(runtime_driver "$a"); st=$(runtime_state "$a")
    printf '  ➕ 新登记 [%s] %s(%s)\n' "$newid" "$a" "$st"
    printf -v line '%s\t%s\t%s\t%s\t' "$a" "$drv" "$newid" "$st"; rows="$rows$line"$'\n'; add=$((add+1))
  done < <(runtime_anchors)
  printf '%s\n' "$hdr" > "$SRC"
  printf '%s' "$rows" >> "$SRC"
  awk -F'\t' '!/^[[:space:]]*#/ && NF>0 && NF<5 {print "awake: 清单第 "NR" 行字段不足 5 列" > "/dev/stderr"; bad=1} END{exit bad?1:0}' "$SRC" \
    || die "写回后清单有非法行(备份在 $SRC.bak-*)"
  printf '已写回:更新 %s 行,新登记 %s 行,未变 %s 行(备份 %s.bak-*)\n' "$upd" "$add" "$same" "$SRC"
}

# ───────────────────── awake --apply(按清单重放)─────────────────────
cmd_apply() {
  [ "$(id -u)" = 0 ] || die "--apply 需要 root"
  local f="${SNAPSHOT:-$(src)}"
  [ -f "$f" ] || die "清单不存在:$f"
  local a drv id w cd note rep ok=0 skip=0 bad=0
  while IFS='|' read -r a w id; do
    [ -n "${a:-}" ] || continue
    case "$w" in
      enabled|yes) note=$(set_runtime "$a" on);;
      disabled|no) note=$(set_runtime "$a" off);;
      *) printf 'awake: 第 %s 行状态值非法(%s)—— 跳过\n' "${id:-?}" "$w" >&2; skip=$((skip+1)); continue;;
    esac
    rep=$?
    if [ "$rep" = 0 ]; then ok=$((ok+1))
    elif [ "$rep" = 1 ]; then skip=$((skip+1)); printf 'awake: 第 %s 行跳过 —— %s\n' "$id" "$note" >&2
    else bad=$((bad+1)); printf 'awake: 第 %s 行重放失败 —— %s\n' "$id" "$note" >&2
    fi
  done < <(awk -F'\t' -v OFS='|' '!/^[[:space:]]*#/ && NF>=5 {print $1,$4,$3}' "$f" 2>/dev/null)
  printf 'awake: 已按 %s 重放运行时(生效 %s 项,运行时无对象而跳过 %s 项,失败 %s 项)\n' "$f" "$ok" "$skip" "$bad"
  [ "$bad" = 0 ] || return 2
}

# ───────────────────────── 唤醒日志 ─────────────────────────
cmd_log() {
  local n=30 follow=0
  case "${1:-}" in
    '') ;;
    -f) n=10; follow=1;;
    *[!0-9]*) die "用法:awake log [数字] | awake log -f";;
    *) n=$1;;
  esac
  [ -f "$LOG" ] || { echo "还没有唤醒日志($LOG 不存在)—— 睡一次醒来就会有。"; return 0; }
  if [ "$follow" = 1 ]; then tail -n "$n" -f "$LOG"; else tail -n "$n" "$LOG"; fi
}

cmd_snapshot() {
  mkdir -p "$RUN" 2>/dev/null
  # 唤醒源计数只在 debugfs 里;本机 debugfs 不是声明式挂载的 ⇒ 用时自挂(幂等)
  if [ ! -r /sys/kernel/debug/wakeup_sources ]; then
    mountpoint -q /sys/kernel/debug 2>/dev/null || mount -t debugfs none /sys/kernel/debug 2>/dev/null
  fi
  cp -f /sys/kernel/debug/wakeup_sources "$RUN/wakeup_sources.before" 2>/dev/null
  # ACPI GPE 计数(不依赖 debugfs 的第二证据源;带名字,便于做差)
  acpi_dump() {
    local f
    for f in /sys/firmware/acpi/interrupts/*; do
      [ -r "$f" ] || continue
      printf '%s %s\n' "$(basename "$f")" "$(awk 'NR==1{print $1}' "$f" 2>/dev/null)"
    done
  }
  acpi_dump >"$RUN/acpi.before" 2>/dev/null
  date +%s >"$RUN/t0" 2>/dev/null
}

cmd_record() {
  local t0 slept entry irq irqname reason gpe f
  t0=$(cat "$RUN/t0" 2>/dev/null || echo 0)
  mkdir -p "$(dirname "$LOG")" 2>/dev/null
  slept='?'
  if [ "$t0" != 0 ]; then
    entry=$(journalctl -b -k -o short-unix --no-pager 2>/dev/null | grep 'PM: suspend entry' | tail -1 | cut -d. -f1)
    [ -n "$entry" ] && slept=$(( $(date +%s) - entry ))
  fi
  irq=$(cat /sys/power/pm_wakeup_irq 2>/dev/null || echo '?')
  irqname=$(awk -v n="$irq" '$1 == n":" {print $NF; exit}' /proc/interrupts 2>/dev/null)
  reason=$(awk '
    NR==FNR { for (i=1;i<=NF;i++) b[$1" "i]=$i; next }
    FNR==1 { next }
    { for (i=2;i<=NF;i++) { d=$i-(b[$1" "i]+0); if (d!=0) printf "%s%s%+d", (out++?", ":""), $1, d } }
  ' "$RUN/wakeup_sources.before" /sys/kernel/debug/wakeup_sources 2>/dev/null)
  [ -z "$reason" ] && reason='无唤醒源事件增量'
  # ACPI GPE 增量(第二证据源,标出是靠哪条 GPE 醒的)
  gpe=''
  if [ -r "$RUN/acpi.before" ]; then
    local now=''
    for f in /sys/firmware/acpi/interrupts/*; do
      [ -r "$f" ] || continue
      now="$now$(basename "$f") $(awk 'NR==1{print $1}' "$f" 2>/dev/null)
"
    done
    gpe=$(printf '%s' "$now" | awk '
      NR==FNR { b[$1]=$2; next }
      { d=$2-(b[$1]+0); if (d>0 && $1 ~ /^(gpe|sci|ff_)/) printf "%s%s%+d", (o++?", ":""), $1, d }
    ' "$RUN/acpi.before" -)
  fi
  printf '%s  slept=%ss  wake_irq=%s%s  src: %s%s\n' \
    "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$slept" "$irq" "${irqname:+($irqname)}" "$reason" \
    "${gpe:+   gpe: $gpe}" >>"$LOG"
}

# ───────────────────────── 派发 ─────────────────────────
case "${1:-}" in
  ''|-h|--help|help) usage; exit 0;;
  list|ls)          shift; cmd_list "$@";;
  setting|settings) shift; cmd_setting "$@";;
  on|enable|run|start|open)   shift; cmd_toggle on  "$@";;
  off|disable|stop|end|close) shift; cmd_toggle off "$@";;
  sync)             shift; cmd_sync "$@";;
  log|logs)         shift; cmd_log "$@";;
  --apply)          shift; cmd_apply "$@";;
  --snapshot)       shift; cmd_snapshot "$@";;
  --record-wake)    shift; cmd_record "$@";;
  *) die "未知子命令:$1(跑 awake 看说明)";;
esac

/etc/nixos/modules/system/service/logind-data/awake-8BE3-50EF.conf

# awake-8BE3-50EF.conf —— 「谁能唤醒这台机器」的事项清单(唯一数据源,按机器码分文件)
#
# 文件名的机器码 = /etc/nixos/hardware-configuration.nix 里 fileSystems."/boot".device 的 UUID(原文大小写)。
# 两侧同一条规则:nix 侧取 config.fileSystems."/boot".device 的 basename;sh 侧解析同一个文件。
#
# 列(TAB 分隔):path | driver | id | wakeup | desc
#   path   设备 = /sys/.../power/wakeup 所在的【设备目录】;timer = systemd unit 名(.timer 结尾)
#   driver 设备 = /sys/.../driver 符号链接(取不到回退 uevent 的 DRIVER=,再取不到留空);timer = 固定 timer
#   id     自增编号(sync 新增行取 max+1,删行不重排)—— awake on/off 用它
#   wakeup 设备 = enabled/disabled(sysfs 原值);timer = yes/no(WakeSystem 原值)
#   desc   备注(设备可空;timer 默认取 unit 的 Description)
#
# 语义:on/off 只改运行时(设备写 sysfs、timer 写 /run 下 drop-in);sync 把运行时实况写回本文件
#       ⇒ 下次 rebuild 成为持久策略;rebuild 时 logind-awake-apply 按本文件重放到运行时。
/sys/bus/acpi/devices/PNP0C0D:00	button	1	enabled	合盖开关
/sys/bus/pci/devices/0000:65:00.3	xhci_hcd	2	enabled	USB 出口 xHCI0(SoC)
/sys/bus/pci/devices/0000:67:00.3	xhci_hcd	3	enabled	USB 出口 xHCI3(雷雳坞)
/sys/bus/pci/devices/0000:67:00.4	xhci_hcd	4	enabled	USB 出口 xHCI4(雷雳坞)
/sys/bus/serio/devices/serio0	atkbd	5	enabled	内置键盘(PS/2)
/sys/bus/usb/devices/7-1.4.2	usb	6	enabled	USB 键鼠(HID 类:键盘/鼠标,当前插在 7-1.4.2)
/sys/bus/acpi/devices/PNP0C0A:00	battery	7	disabled	电池事件
/sys/bus/acpi/devices/PNP0C0C:00	button	8	disabled	电源键(ACPI 固定事件,未必真能屏蔽)
/sys/bus/acpi/devices/PNP0C0E:00	button	9	disabled	ACPI 睡眠键
/sys/bus/i2c/devices/i2c-GXTP7385:00	i2c_hid_acpi	10	disabled	触摸板 / 触摸屏
/sys/bus/pci/devices/0000:00:01.2	pcieport	11	disabled	
/sys/bus/pci/devices/0000:00:02.2	pcieport	12	disabled	无线网卡根端口
/sys/bus/pci/devices/0000:00:02.3	pcieport	13	disabled	读卡器根端口
/sys/bus/pci/devices/0000:00:02.4	pcieport	14	disabled	
/sys/bus/pci/devices/0000:00:04.1	pcieport	15	disabled	空根端口
/sys/bus/pci/devices/0000:00:08.1	pcieport	16	disabled	
/sys/bus/pci/devices/0000:00:08.2	pcieport	17	disabled	
/sys/bus/pci/devices/0000:00:08.3	pcieport	18	disabled	
/sys/bus/pci/devices/0000:01:00.0	nvme	19	disabled	
/sys/bus/pci/devices/0000:02:00.0	iwlwifi	20	disabled	
/sys/bus/pci/devices/0000:03:00.0	sdhci-pci	21	disabled	
/sys/bus/pci/devices/0000:04:00.0	nvme	22	disabled	
/sys/bus/pci/devices/0000:65:00.0	amdgpu	23	disabled	
/sys/bus/pci/devices/0000:65:00.1	snd_hda_intel	24	disabled	
/sys/bus/pci/devices/0000:65:00.2	ccp	25	disabled	
/sys/bus/pci/devices/0000:65:00.4	xhci_hcd	26	disabled	xHCI1(摄像头 + 指纹所在控制器)
/sys/bus/pci/devices/0000:65:00.6	snd_hda_intel	27	disabled	
/sys/bus/pci/devices/0000:66:00.0		28	disabled	
/sys/bus/pci/devices/0000:66:00.1	amdxdna	29	disabled	
/sys/bus/pci/devices/0000:67:00.0		30	disabled	
/sys/bus/pci/devices/0000:67:00.6	thunderbolt	31	disabled	雷雳 NHI
/sys/bus/platform/devices/PNP0B00:00	rtc_cmos	32	disabled	RTC 闹钟(定时唤醒)
/sys/bus/platform/devices/alarmtimer.0.auto	alarmtimer	33	disabled	alarmtimer(事件计时)
/sys/bus/thunderbolt/devices/0-0		34	disabled	雷雳根设备(据点 0-0)
/sys/bus/thunderbolt/devices/domain0		35	disabled	雷雳域(热插拔唤醒)
/sys/bus/usb/devices/1-5	usb	36	disabled	蓝牙适配器(Intel AX210)
/sys/bus/usb/devices/3-1	usb	37	disabled	USB 集线器 3-1
/sys/bus/usb/devices/7-1	usb	38	disabled	USB 集线器 7-1
/sys/bus/usb/devices/7-1.1	usb	39	disabled	USB 集线器 7-1.1
/sys/bus/usb/devices/7-1.4	usb	40	disabled	USB 集线器 7-1.4
/sys/bus/usb/devices/usb1	usb	41	disabled	USB 根集线器 usb1
/sys/bus/usb/devices/usb2	usb	42	disabled	USB 根集线器 usb2
/sys/bus/usb/devices/usb3	usb	43	disabled	USB 根集线器 usb3
/sys/bus/usb/devices/usb4	usb	44	disabled	USB 根集线器 usb4
/sys/bus/usb/devices/usb5	usb	45	disabled	USB 根集线器 usb5
/sys/bus/usb/devices/usb6	usb	46	disabled	USB 根集线器 usb6
/sys/bus/usb/devices/usb7	usb	47	disabled	USB 根集线器 usb7
/sys/bus/usb/devices/usb8	usb	48	disabled	USB 根集线器 usb8
/sys/class/power_supply/ADP1		49	disabled	交流适配器事件(拔插电)
/sys/class/power_supply/hid-0003:17EF:60EE.0015-battery		50	disabled	HID 设备电池事件(键鼠电量)
fstrim.timer	timer	51	no	Discard unused filesystem blocks once a week
fwupd-refresh.timer	timer	52	no	Refresh fwupd metadata regularly
logrotate.timer	timer	53	no	
nix-gc.timer	timer	54	no	
systemd-tmpfiles-clean.timer	timer	55	no	Daily Cleanup of Temporary Directories