逾期本來沒實作,監控頁上就寫著「上面的判定只講到期,不講逾期」。 到期與逾期分兩欄,不合成一個判定:到期是「該跑了」,逾期是「due 欄那個 截止時間過了」。一筆七天一次的檢查項天天都可能到期,卻永遠不會逾期; 一筆有截止時間的交辦可能早就逾期,卻因為排定點還沒到而判成還沒到。 合成一個值就把兩種情況混成同一格。 due.sh 多第十二欄 overdue,值是減號(沒有截止時間)、no、逾期的秒數, 或 bad。bad 算成欄位不合法回 2——一個解不開的截止時間跟沒有截止時間 在畫面上長得一模一樣,而前者是登錄的時候就填錯了。done 那幾筆不算逾期; 被暫停的那幾筆照算,因為暫停中止的是到期判定,不是日曆,而一筆被暫停 又逾期的交辦正好是最容易被忘掉的那一種。 另加唯讀子命令 due.sh overdue 給狀態查詢那一路走。走 scan 會推進快照, 那一輪之後的事件判定會全部落空;走 events 又判不到逾期;讓呼叫端自己 拿 due 欄去比更糟,那是第二套判定,漂移的那一天兩邊都說自己對。 patrol.sh 在逾期或連續失敗筆數大於 0 時各列一筆待人處理,每一輪都列。 兩種都不會自己好:截止時間過了不會因為過更久就不逾期,連續失敗的項目 每一輪都會再試一次然後再失敗一次。「提過了」不是「處理過了」,而助理 不替人按暫停,那個狀態留給人設。 監控頁那一節逐筆點名連續失敗的那幾筆——id、失敗幾次、最後一次執行、 標題。原本只印一個總數,而失敗次數這個欄位存在的理由是指出「有一筆壞掉 的項目每輪重試而沒人知道」,不說出是哪一筆,那個理由只完成一半。 目錄頁刻意不加欄位。那一頁一台機器一個區塊、各自那一輪寫,新加一條只有 跑到新版的機器寫得出來,讀的人分不出「這台沒有逾期」與「這台還沒寫這 一條」——理由與當初把「本輪非 ok 事件數」擋在那一頁外面的一樣。 欄位數從 11 變 12,run-due.sh 那道欄位數檢核跟著改成 12。那道檢核擋的 不是第 12 欄本身,是「兩支不是同一版」——版本不同的時候,前 11 欄的 順序也不能假設還是原來那樣。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
476 lines
25 KiB
Bash
Executable File
476 lines
25 KiB
Bash
Executable File
#!/usr/bin/env sh
|
||
# run-due.sh — 把到期的內建檢查項真的跑一遍,成敗回寫待辦簿。
|
||
#
|
||
# 用法:
|
||
# run-due.sh run [--rows {檔案}] [--root {字面絕對根目錄}] [--now {epoch 秒}] [--dry-run]
|
||
# run-due.sh plan [--rows {檔案}] [--root {字面絕對根目錄}] [--now {epoch 秒}]
|
||
#
|
||
# run 逐筆執行並回寫待辦簿。--dry-run 只印要跑什麼、不執行也不回寫
|
||
# plan 等同 run --dry-run,另取一個名字是為了讓唯讀那一路在指令列上看得出來
|
||
#
|
||
# 結束碼:
|
||
# 0 這一輪跑完了。零筆到期、每一筆都代不出目標,都算跑完
|
||
# 1 至少一支指令回非零。那一筆已經記成失敗、失敗次數加一,其餘各筆照跑
|
||
# 2 到期清單讀不到或欄位對不上,**一筆都沒跑**。判到期那一支沒跑過,或它的輸出換了格式
|
||
# 4 至少一筆的回寫失敗。指令跑過了,但待辦簿沒記到,下一輪會再跑一次同一筆
|
||
# 5 檔案系統失敗:暫存檔寫不進去
|
||
# 6 用法錯誤:不認得的子命令或選項、選項缺值、--root 不是絕對路徑
|
||
# 同時命中好幾碼時,回報順序是 5、2、4、1:前面的蓋掉後面的。4 排在 1 前面是因為
|
||
# 「跑了但沒記到」會讓同一件事每輪重跑,比「跑了而且失敗」更需要人知道。
|
||
#
|
||
# --- 這一支只跑指令型,不叫技能也不送提醒 ---
|
||
#
|
||
# 待辦簿的動作欄有三種值:一行指令、技能名、remind。這一支只跑第一種。
|
||
# 技能名那一種不跑的理由:叫用一整支技能的代價與風險都大得多——它會寫 wiki、開 PR、改檔案,
|
||
# 而這一支跑在沒有人看的那一輪裡。唯讀盤點指令失敗最多就是回一個非零碼,那一筆記一次失敗;
|
||
# 一支技能中途失敗可能留下寫到一半的頁或開錯的 PR。兩種代價不同,就該分兩次判、分兩次接。
|
||
# remind 那一種不跑的理由更直接:它本來就沒有東西可跑,提醒怎麼送到前景是另一件待辦。
|
||
#
|
||
# --- 只跑內建項,使用者交辦的一律不碰 ---
|
||
#
|
||
# 只有 spec_key 非空的那幾筆會被執行,也就是依委派清單種入的內建項。
|
||
# 理由是那幾筆的指令經過種入那一支的檢核:路徑一定是 {root}/jsc-{domain}/ 開頭、不含金錢符號
|
||
# 與波浪號、代入點只認得三個、腳本一定在這台機器上。使用者親手登錄的那幾筆沒有經過那道關,
|
||
# 動作欄想寫什麼都行,在無人值守那一輪把它送進殼是另一回事,要另外判。
|
||
# 執行這一刻再驗一次同樣那幾項,不因為種入時驗過就省掉:待辦檔是純文字,中間可能被改。
|
||
#
|
||
# --- 權限閘門與這一支的關係,講白 ---
|
||
#
|
||
# 這一支自己執行那些指令,所以權限層只看得到 run-due.sh 這一條指令,看不到裡面跑了什麼。
|
||
# 那不是繞過閘門,是閘門本來就不是沙箱:它是一份「可信入口」的白名單,而清單上的每一支腳本
|
||
# 本來就做得了它該做的事——巡檢那一支會寫 wiki 頁,部署那一支會改整台機器的外掛。
|
||
# 防線改由三層擋:一、只跑 spec_key 非空的內建項,那幾筆的來源是版本控管、要走 PR 的委派清單;
|
||
# 二、執行前再驗一次指令形狀,不信任待辦檔的內容;三、代不出來的目標一律不跑。
|
||
# 這一段寫在這裡是因為下一個維護的人一定會問,而「為什麼不會被擋」跟「為什麼不需要被擋」
|
||
# 是兩個不同的答案,後者才是對的那一個。
|
||
#
|
||
# --- 代不出目標不算失敗 ---
|
||
#
|
||
# {cli} 由偵測到的 CLI 代號代入,{repo} 由 scan-repos.sh 掃到的存取庫工作目錄代入。
|
||
# 兩個代入點都代得出來才跑;任一個代不出來就印一行 held= 跳過,**不動那一筆的 last_run,
|
||
# 也不加失敗次數**。
|
||
# 那一筆沒有做錯任何事:代不出目標是環境還缺一塊,記成失敗會讓一個沒有人修得動的計數
|
||
# 一路往上爬,而那個計數存在的理由是指出「有一筆壞掉的項目每輪重試而沒人知道」。
|
||
# 把「還沒接上」記成「壞掉」,等於用假的壞掉把真的壞掉蓋掉。
|
||
#
|
||
# {repo} 代不出來最常見的成因是 JSC_ASSIST_SCAN_ROOT 沒設。那不是預設值漏填,是掃描那一支
|
||
# 刻意不猜:猜錯掃描起點的後果是在猜錯的那些目錄底下跑指令,而那一輪沒有人看得到它跑到
|
||
# 哪裡去了。所以這裡照它回的話原樣記進 held= 的理由欄,人才看得出要去設哪一個變數。
|
||
set -u
|
||
|
||
usage() {
|
||
echo 'usage: run-due.sh {run|plan} [--rows 檔案] [--root 絕對路徑] [--now epoch] [--dry-run]' >&2
|
||
exit 6
|
||
}
|
||
|
||
die() { _c=$1; shift; printf '[jsc][助理執行][ERR]:%s\n' "$*" >&2; exit "$_c"; }
|
||
|
||
# 取第一行、截到指定長度。cut 數的是位元組不是字元,中文字剛好被切成一半時尾巴就變成
|
||
# 替代字元——實測踩到:一行中文錯誤訊息截在第 200 個位元組,報告上留下一個亂碼字。
|
||
# 截完再過一次 iconv -c 把那個不完整的序列丟掉。iconv 不在這台機器上就退回原樣,
|
||
# 不自己拼一套 UTF-8 邊界判定:那是在錯誤訊息這條路上加一個會出錯的新東西。
|
||
clip() { # $1=位元組上限;讀標準輸入
|
||
_raw=$(head -n1 | cut -c"1-$1")
|
||
_cln=$(printf '%s' "$_raw" | iconv -c -f UTF-8 -t UTF-8 2>/dev/null)
|
||
if [ -n "$_cln" ]; then printf '%s' "$_cln"; else printf '%s' "$_raw"; fi
|
||
}
|
||
note() { printf '[jsc][助理執行]:%s\n' "$*" >&2; }
|
||
warn() { printf '[jsc][助理執行][WARN]:%s\n' "$*" >&2; }
|
||
|
||
[ "$#" -ge 1 ] || usage
|
||
MODE=$1; shift
|
||
case "$MODE" in
|
||
run) DRYRUN=0 ;;
|
||
plan) DRYRUN=1 ;;
|
||
*) usage ;;
|
||
esac
|
||
|
||
ROWS=''
|
||
OPT_ROOT=''
|
||
NOW=''
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--rows) [ "$#" -ge 2 ] || usage; ROWS=$2; shift 2 ;;
|
||
--root) [ "$#" -ge 2 ] || usage; OPT_ROOT=$2; shift 2 ;;
|
||
--now) [ "$#" -ge 2 ] || usage; NOW=$2; shift 2 ;;
|
||
--dry-run) DRYRUN=1; shift ;;
|
||
*) usage ;;
|
||
esac
|
||
done
|
||
|
||
case "$OPT_ROOT" in
|
||
''|/*) ;;
|
||
*) die 6 "--root 要給字面絕對路徑,收到的是「$OPT_ROOT」。" ;;
|
||
esac
|
||
|
||
# 助理狀態目錄。判到期那一支把 rows.txt 放在這底下,兩支要對得上同一個位置。
|
||
JSC_HOME_RESOLVED=${JSC_HOME:-${HOME:-}/.jsc}
|
||
case "$JSC_HOME_RESOLVED" in
|
||
/*) ;;
|
||
*) die 6 'JSC_HOME 與 HOME 都解不出絕對路徑,找不到助理狀態目錄。' ;;
|
||
esac
|
||
# --rows 沒有預設值,一律要指定。
|
||
#
|
||
# 原本有預設,指向助理狀態目錄底下那一份。那個預設是這一支上線之後最嚴重的一個缺陷:
|
||
# 判到期那一支是被巡檢用 --out 叫的,輸出寫進那一輪自己的暫存目錄,跟這個預設不是同一個
|
||
# 位置。於是這一支每一輪讀的都是「上一次有人手動跑 scan 留下的那一份」。實測那份清單在
|
||
# 機器上放了六小時,這一支每一輪都對它動手,還跑了一筆已經被移除的待辦。
|
||
# 而它看起來完全正常——那份舊清單產生的當下是對的,內容剛好沒變。**錯了六小時才顯形。**
|
||
#
|
||
# 所以不留預設。少帶這個選項就回用法錯誤,吵一次總比安靜地對舊資料動手好。呼叫端要拿
|
||
# collect 印的 due_rows_file= 餵進來,那是同一輪產生的那一份。
|
||
if [ -z "$ROWS" ]; then
|
||
die 6 '沒有帶 --rows。這一支不猜到期清單的位置:判到期那一支的輸出寫在叫它的人指定的目錄裡,猜一個預設就會讀到別人留下的舊檔案,而舊檔案讀起來跟新的一模一樣。請把 collect 印的 due_rows_file= 餵進來。'
|
||
fi
|
||
|
||
[ -f "$ROWS" ] || die 2 "到期清單讀不到:$ROWS。請先跑 due.sh scan——沒跑過判定,跟「都沒到期」不是同一件事。"
|
||
|
||
TAB=$(printf '\t')
|
||
TMPD=$(mktemp -d 2>/dev/null) || die 5 '暫存目錄建不起來。'
|
||
trap 'rm -rf "$TMPD"' EXIT
|
||
|
||
[ -n "$NOW" ] || NOW=$(date -u +%s)
|
||
NOW_ISO=$(date -u -d "@$NOW" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
|
||
|| NOW_ISO=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||
|
||
# --- 找同一組工具 ---
|
||
|
||
HERE=$(CDPATH= cd -P -- "$(dirname -- "$0")" && pwd -P)
|
||
ROOT="$OPT_ROOT"
|
||
# 沒帶 --root 就從自己的位置往上推兩層。呼叫端該餵進來,這只是開發時跑得動的退路。
|
||
[ -n "$ROOT" ] || ROOT=$(CDPATH= cd -- "$HERE/../.." 2>/dev/null && pwd -L) || ROOT=''
|
||
|
||
find_tool() { # $1=domain $2=相對路徑
|
||
for _d in "jsc-$1" "$1"; do
|
||
[ -n "$ROOT" ] && [ -f "$ROOT/$_d/$2" ] && { printf '%s' "$ROOT/$_d/$2"; return 0; }
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# 清單太舊就拒跑。判到期與執行之間隔著呼叫端,那一段有可能斷掉——collect 失敗了而那一輪
|
||
# 照樣往下走,或呼叫端餵進上一輪的路徑。兩種都會讓這一支對著一份不描述現況的清單動手,
|
||
# 而那正是它上線之後六小時裡在做的事。
|
||
# 門檻取心跳的過期門檻:那是這台機器認定「一輪跑完的紀錄還算新鮮」的長度,一份比它還舊的
|
||
# 到期清單本來就不可能是這一輪產生的。讀不到門檻就退回 300 秒,跟心跳那一支的預設一致。
|
||
_ttl=''
|
||
if _hb=$(find_tool hooks hooks/heartbeat.sh 2>/dev/null); then
|
||
_ttl=$("$_hb" report 2>/dev/null | sed -n 's/.*[[:space:]]ttl=\([0-9]*\).*/\1/p' | head -n1)
|
||
fi
|
||
case "${_ttl:-}" in
|
||
''|*[!0-9]*) _ttl=300 ;;
|
||
esac
|
||
_mtime=$(date -r "$ROWS" +%s 2>/dev/null) || _mtime=''
|
||
if [ -n "$_mtime" ]; then
|
||
_age=$((NOW - _mtime))
|
||
if [ "$_age" -gt "$_ttl" ]; then
|
||
die 2 "到期清單是 $_age 秒前產生的,超過門檻 $_ttl 秒,這一支不對它動手:$ROWS。一份比心跳門檻還舊的清單不可能描述這一輪,照著跑會動到已經不存在的待辦。請確認判到期那一步這一輪真的跑成功了,再把它印的路徑餵進來。"
|
||
fi
|
||
fi
|
||
|
||
TASKS_SH="$HERE/tasks.sh"
|
||
[ -f "$TASKS_SH" ] || TASKS_SH=$(find_tool assist tools/tasks.sh) \
|
||
|| die 2 '找不到 tasks.sh,成敗沒有地方回寫。待辦簿只有一個寫入者,缺了它這一輪不該跑。'
|
||
DUE_SH="$HERE/due.sh"
|
||
[ -f "$DUE_SH" ] || DUE_SH=$(find_tool assist tools/due.sh) || DUE_SH=''
|
||
|
||
# --- {cli} 的代入來源 ---
|
||
|
||
# 偵測到的 CLI 代號,一行一個。取不到就當成代不出來,帶 {cli} 的那幾筆這一輪跳過。
|
||
CLIS="$TMPD/clis.txt"
|
||
: >"$CLIS"
|
||
if _det=$(find_tool cli tools/detect-clis.sh); then
|
||
"$_det" 2>/dev/null | awk -F"$TAB" 'NF>0 && $1 != "" {print $1}' >"$CLIS" 2>/dev/null || : >"$CLIS"
|
||
fi
|
||
N_CLI=$(awk 'END{print NR+0}' "$CLIS")
|
||
|
||
# 跟排程條目安裝當下記下的那一份比對。少了哪一支要點名,不是只印個數字。
|
||
#
|
||
# 為什麼要比:偵測那一支純看 PATH,分不出「裝了但執行檔不在 PATH 上」。而條目帶的 PATH 是
|
||
# 安裝當下拍的,其中某些 CLI 裝在會過期的目錄底下——實測有一支裝在帶行程編號的多殼層目錄,
|
||
# 重開機之後那條路徑就不在了。
|
||
# 沒有基準的後果不是報錯,是安靜地少做一支:這一輪照樣把偵測到的每一支都跑過、照樣全部
|
||
# 成功、照樣回報「都跑過了」。實測那一輪報「對 4 支 CLI 都跑過,全部成功」,而機器上裝了
|
||
# 5 支——報告讀起來完全正常。
|
||
# 只報不修:那條路徑本質上是暫時的,真正的解法是那支 CLI 該裝在穩定位置,或者重跑一次
|
||
# 排程安裝重新拍一份快照。兩件都不是這一支該替人決定的。
|
||
CLI_MISSING=''
|
||
if [ -n "${JSC_ASSIST_CLIS:-}" ]; then
|
||
for _want in $JSC_ASSIST_CLIS; do
|
||
grep -qxF "$_want" "$CLIS" 2>/dev/null || CLI_MISSING="${CLI_MISSING:+$CLI_MISSING、}$_want"
|
||
done
|
||
fi
|
||
|
||
# --- {repo} 的代入來源 ---
|
||
|
||
# 掃到的存取庫工作目錄,一行一條。掃描規則、掃描起點與排除清單全在 scan-repos.sh 那一支,
|
||
# 這裡不自己再判一次:兩邊各有一套掃描規則就會漂移,而漂移的那一天沒有人會收到通知。
|
||
# 兩份:REPOS 是路徑清單,給沒綁存取庫的那幾筆代成每一個目標;REPOMAP 多帶一欄
|
||
# {owner}/{repo},給綁了存取庫的那幾筆對出它綁的是哪一條路徑。
|
||
REPOS="$TMPD/repos.txt"
|
||
REPOMAP="$TMPD/repomap.txt"
|
||
: >"$REPOS"; : >"$REPOMAP"
|
||
REPO_WHY=''
|
||
REPO_SCAN='-'
|
||
SCAN_SH="$HERE/scan-repos.sh"
|
||
[ -f "$SCAN_SH" ] || SCAN_SH=$(find_tool assist tools/scan-repos.sh) || SCAN_SH=''
|
||
if [ -z "$SCAN_SH" ]; then
|
||
REPO_WHY='找不到 scan-repos.sh,{repo} 沒有代入來源'
|
||
REPO_SCAN='missing'
|
||
else
|
||
"$SCAN_SH" list --root "$ROOT" >"$TMPD/scan.out" 2>"$TMPD/scan.err"
|
||
_src=$?
|
||
REPO_SCAN="rc$_src"
|
||
case "$_src" in
|
||
# 0 是全部都對得出盤點頁頁名,1 是有幾個對不出來或路徑被拒——後者交出來的那幾條照用,
|
||
# 對不出頁名不影響唯讀盤點跑得動。其餘各碼一律當成沒有來源,並把它印的理由帶上。
|
||
0|1)
|
||
sed -n 's/^repo=\([^ ]*\) slug=\([^ ]*\).*/\1'"$TAB"'\2/p' "$TMPD/scan.out" >"$REPOMAP"
|
||
cut -f1 "$REPOMAP" >"$REPOS" ;;
|
||
*) REPO_WHY=$(sed -n 's/^\[jsc\]\[助理掃描\]\[ERR\]://p' "$TMPD/scan.err" 2>/dev/null | clip 200)
|
||
[ -n "$REPO_WHY" ] || REPO_WHY="存取庫掃描回結束碼 $_src,{repo} 代不出目標" ;;
|
||
esac
|
||
# 掃描那一支的逐項判定行被導進暫存檔,所以外面看不到。原樣轉出來:被拒的那幾個存取庫是
|
||
# 這一輪「掃到但沒交出去」的名單,那份名單不能只留在暫存檔裡。
|
||
sed -n 's/^\(rejected=\|unnamed=\|excluded=\|skipped_link=\|not_a_repo=\)/repo_scan_note=\1/p' \
|
||
"$TMPD/scan.out" "$TMPD/scan.err" 2>/dev/null
|
||
fi
|
||
N_REPOS=$(awk 'END{print NR+0}' "$REPOS")
|
||
if [ -z "$REPO_WHY" ] && [ "$N_REPOS" -eq 0 ]; then
|
||
REPO_WHY='存取庫掃描一個都沒掃到,{repo} 代不出目標'
|
||
fi
|
||
|
||
# 這一筆綁的存取庫對出一條掃到的路徑。綁的值可以是工作目錄路徑,也可以是 {owner}/{repo}——
|
||
# 待辦簿的欄位說明只寫「這一筆綁哪一個存取庫」,兩種寫法都有人會填,所以兩種都認。
|
||
# 對不出來一律回失敗,不退回「全部存取庫」:那一筆指名了一個目標,代成全部就是跑了
|
||
# 十九個沒有人要它跑的地方。
|
||
repo_path_of() { # $1=綁定值
|
||
awk -F"$TAB" -v want="$1" '$1==want || $2==want {print $1; found=1; exit} END{exit !found}' "$REPOMAP"
|
||
}
|
||
|
||
# --- 判斷動作是哪一種 ---
|
||
|
||
# 技能名的形狀是 jsc-{domain}:{技能名}。比對整串,不用前綴比對:一行以 jsc- 開頭的指令
|
||
# 路徑不該被當成技能名。
|
||
is_skill_name() { # $1=action
|
||
case "$1" in
|
||
jsc-*:*)
|
||
case "$1" in
|
||
*' '*|*/*) return 1 ;;
|
||
esac
|
||
return 0 ;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
# 執行前再驗一次指令形狀。種入那一支驗過同樣幾項,這裡不省:待辦檔是純文字,中間可能被改。
|
||
# 驗不過就當成代不出來,跳過不跑,不記失敗——這一筆的內容有問題,那是清單或待辦檔的事。
|
||
cmd_shape_ok() { # $1=指令
|
||
case "$1" in
|
||
*'$'*|*'`'*|*'~'*) BAD_WHY='指令裡有金錢符號、反引號或波浪號'; return 1 ;;
|
||
*';'*|*'|'*|*'&'*) BAD_WHY='指令裡有分號、管線或連接符號,一筆內建項只該是一行單一指令'; return 1 ;;
|
||
esac
|
||
# 路徑一定要落在這一輪的根目錄底下。字面絕對路徑是權限層唯一認得的形狀,也是唯一
|
||
# 看得出「這支腳本是不是我們自己的」的形狀。
|
||
case "$1" in
|
||
*"$ROOT"/*) ;;
|
||
*) BAD_WHY="指令裡沒有這一輪的根目錄 $ROOT"; return 1 ;;
|
||
esac
|
||
return 0
|
||
}
|
||
|
||
# 把一個代入點換成清單裡的每一個值,一個值一行。清單是空的又剛好命中代入點,那一行就
|
||
# 整行消失——所以呼叫端要先確定清單非空,別靠這裡回錯:目標數變成 0 的那一筆會被當成
|
||
# 「沒有東西要跑」而算成功,而它其實一次都沒跑。
|
||
expand_over() { # $1=代入點 $2=值清單檔 $3=輸入檔 $4=輸出檔
|
||
: >"$4" 2>/dev/null || return 1
|
||
while IFS= read -r _line; do
|
||
[ -n "$_line" ] || continue
|
||
case "$_line" in
|
||
*"$1"*)
|
||
while IFS= read -r _v; do
|
||
[ -n "$_v" ] || continue
|
||
printf '%s\n' "$(printf '%s' "$_line" | sed "s|$1|$_v|g")" >>"$4"
|
||
done <"$2" ;;
|
||
*) printf '%s\n' "$_line" >>"$4" ;;
|
||
esac
|
||
done <"$3"
|
||
return 0
|
||
}
|
||
|
||
# --- 逐筆處理 ---
|
||
|
||
N_DUE=0; N_RUN=0; N_OK=0; N_FAIL=0; N_HELD=0; N_SKIP=0; N_WRITE_BAD=0
|
||
RC_CMD=0; RC_WRITE=0
|
||
|
||
# 欄位順序取自 due.sh 印的 rows_columns=。這裡寫死同一個順序,換了就對不上——所以先驗一次
|
||
# 欄位數,對不上一筆都不跑,而不是照舊讀進錯的欄位。
|
||
_cols=$(awk -F"$TAB" 'NF>1{print NF; exit}' "$ROWS" 2>/dev/null)
|
||
case "${_cols:-0}" in
|
||
12) ;;
|
||
0) note '到期清單是空的,這一輪沒有任何一筆要跑。'; _cols=12 ;;
|
||
*) die 2 "到期清單的欄位數是 ${_cols},這一支認得的是 12 欄。判到期那一支的輸出格式換過了,先對齊再跑——照舊讀下去會把指令讀成別的欄位。第 12 欄是逾期判定,跟這一支要跑什麼無關,但欄位數不對就代表兩支不是同一版,那時候前 11 欄的順序也不能假設還是原來那樣。" ;;
|
||
esac
|
||
|
||
while IFS="$TAB" read -r c_id c_verdict c_state c_kind c_action c_trigger c_recur c_next c_rearm c_why c_rest; do
|
||
[ -n "${c_id:-}" ] || continue
|
||
[ "${c_verdict:-}" = "due" ] || continue
|
||
N_DUE=$((N_DUE + 1))
|
||
|
||
# 只跑內建項。使用者交辦的那幾筆沒有經過種入那一支的檢核,動作欄想寫什麼都行。
|
||
_spec=$(sed -n 's/^spec_key=//p' "$JSC_HOME_RESOLVED/assistant/tasks/$c_id" 2>/dev/null | head -n1)
|
||
if [ -z "$_spec" ]; then
|
||
N_SKIP=$((N_SKIP + 1))
|
||
printf 'skip=%s reason=不是委派清單種入的內建項,這一支不跑使用者交辦的那幾筆\n' "$c_id"
|
||
continue
|
||
fi
|
||
|
||
# 三種動作,這一輪只跑第一種。
|
||
if [ "$c_action" = "remind" ]; then
|
||
N_SKIP=$((N_SKIP + 1))
|
||
printf 'skip=%s spec=%s reason=動作是只提醒,沒有東西可跑\n' "$c_id" "$_spec"
|
||
continue
|
||
fi
|
||
if is_skill_name "$c_action"; then
|
||
N_SKIP=$((N_SKIP + 1))
|
||
printf 'skip=%s spec=%s reason=動作是技能名,叫用整支技能這一批還沒接 action=%s\n' \
|
||
"$c_id" "$_spec" "$c_action"
|
||
continue
|
||
fi
|
||
|
||
BAD_WHY=''
|
||
if ! cmd_shape_ok "$c_action"; then
|
||
N_HELD=$((N_HELD + 1))
|
||
printf 'held=%s spec=%s reason=%s action=%s\n' "$c_id" "$_spec" "$BAD_WHY" "$c_action"
|
||
continue
|
||
fi
|
||
|
||
# 代入點展開:一個目標一行,寫進暫存檔。
|
||
_targets="$TMPD/targets.$c_id"
|
||
: >"$_targets" 2>/dev/null || die 5 "暫存檔寫不進去:$_targets。"
|
||
_hold=''
|
||
# 沒綁存取庫的那一筆代成掃到的每一個;綁了的只代那一個。
|
||
_repovals="$REPOS"
|
||
case "$c_action" in
|
||
*'{repo}'*)
|
||
if [ -n "$REPO_WHY" ]; then
|
||
_hold="$REPO_WHY"
|
||
else
|
||
_bind=$(sed -n 's/^repo=//p' "$JSC_HOME_RESOLVED/assistant/tasks/$c_id" 2>/dev/null | head -n1)
|
||
if [ -n "$_bind" ]; then
|
||
if _bp=$(repo_path_of "$_bind"); then
|
||
_repovals="$TMPD/repoval.$c_id"
|
||
printf '%s\n' "$_bp" >"$_repovals" 2>/dev/null || die 5 "暫存檔寫不進去:$_repovals。"
|
||
else
|
||
_hold="這一筆綁的存取庫「$_bind」這一輪沒掃到,代不出目標"
|
||
fi
|
||
fi
|
||
fi ;;
|
||
esac
|
||
case "$c_action" in
|
||
*'{cli}'*) [ "$N_CLI" -gt 0 ] || _hold='這台機器偵測不到任何一支 CLI,{cli} 代不出目標' ;;
|
||
esac
|
||
if [ -z "$_hold" ]; then
|
||
# 兩個代入點分兩段代,一段一個暫存檔。同一筆同時帶 {repo} 與 {cli} 時,目標數是兩邊
|
||
# 的乘積——三個存取庫乘五支 CLI 就是十五個目標,一個目標跑一次。
|
||
printf '%s\n' "$c_action" >"$TMPD/x0.$c_id"
|
||
expand_over '{repo}' "$_repovals" "$TMPD/x0.$c_id" "$TMPD/x1.$c_id" \
|
||
|| die 5 "暫存檔寫不進去:$TMPD/x1.$c_id。"
|
||
expand_over '{cli}' "$CLIS" "$TMPD/x1.$c_id" "$_targets" \
|
||
|| die 5 "暫存檔寫不進去:$_targets。"
|
||
# 代入之後再驗一次禁止字元。種入那一支與上面的 cmd_shape_ok 驗的是還沒代入的字面,
|
||
# 代進去的值是這一輪現場算出來的——路徑與 CLI 代號都有可能帶進新的字元,而帶進來的
|
||
# 那一刻就是送進殼的前一刻。
|
||
if grep -q '[$`;|&~]' "$_targets" 2>/dev/null; then
|
||
_hold='代入之後出現金錢符號、反引號、分號、管線、連接符號或波浪號,代入來源有問題'
|
||
: >"$_targets"
|
||
fi
|
||
fi
|
||
if [ -n "$_hold" ]; then
|
||
N_HELD=$((N_HELD + 1))
|
||
printf 'held=%s spec=%s reason=%s action=%s\n' "$c_id" "$_spec" "$_hold" "$c_action"
|
||
continue
|
||
fi
|
||
|
||
# 代完之後還留著大括號就是還有認不得的代入點。原樣送進殼會跑到一個沒有人寫過的地方。
|
||
if grep -q '[{}]' "$_targets" 2>/dev/null; then
|
||
N_HELD=$((N_HELD + 1))
|
||
printf 'held=%s spec=%s reason=代完之後還留著大括號,認不得的代入點 action=%s\n' \
|
||
"$c_id" "$_spec" "$c_action"
|
||
continue
|
||
fi
|
||
|
||
_n=$(awk 'END{print NR+0}' "$_targets")
|
||
if [ "$DRYRUN" -eq 1 ]; then
|
||
N_RUN=$((N_RUN + 1))
|
||
printf 'would_run=%s spec=%s targets=%s\n' "$c_id" "$_spec" "$_n"
|
||
while IFS= read -r _t; do printf ' target=%s\n' "$_t"; done <"$_targets"
|
||
continue
|
||
fi
|
||
|
||
# 真的跑。一個目標一次,全部成功這一筆才算成功——一支 CLI 接線壞掉就是一筆要人看的發現。
|
||
N_RUN=$((N_RUN + 1))
|
||
_entry_rc=0
|
||
_first_err=''
|
||
while IFS= read -r _t; do
|
||
[ -n "$_t" ] || continue
|
||
_out=$(sh -c "$_t" </dev/null 2>&1); _rc=$?
|
||
if [ "$_rc" -eq 0 ]; then
|
||
printf 'target_ok=%s rc=0 cmd=%s\n' "$c_id" "$_t"
|
||
else
|
||
_entry_rc=1
|
||
[ -n "$_first_err" ] || _first_err=$(printf '%s' "$_out" | clip 160)
|
||
printf 'target_fail=%s rc=%s cmd=%s detail=%s\n' \
|
||
"$c_id" "$_rc" "$_t" "$(printf '%s' "$_out" | clip 160)"
|
||
fi
|
||
done <"$_targets"
|
||
|
||
# 回寫。下一次什麼時候到期由判定那一支算,這一支不自己算——兩邊各算一次就會漂移。
|
||
if [ "$_entry_rc" -eq 0 ]; then
|
||
_next=''
|
||
if [ -n "$DUE_SH" ]; then
|
||
_next=$("$DUE_SH" next --trigger "$c_trigger" --recur "$c_recur" \
|
||
--last-run "$NOW_ISO" --now "$NOW" 2>/dev/null | sed -n 's/^next_run=//p' | head -n1)
|
||
fi
|
||
if [ -n "$_next" ]; then
|
||
"$TASKS_SH" done "$c_id" --last-run "$NOW_ISO" --next-run "$_next" >/dev/null 2>&1
|
||
else
|
||
"$TASKS_SH" done "$c_id" --last-run "$NOW_ISO" >/dev/null 2>&1
|
||
fi
|
||
_wrc=$?
|
||
if [ "$_wrc" -eq 0 ]; then
|
||
N_OK=$((N_OK + 1))
|
||
printf 'done=%s spec=%s targets=%s next_run=%s\n' "$c_id" "$_spec" "$_n" "${_next:--}"
|
||
else
|
||
N_WRITE_BAD=$((N_WRITE_BAD + 1)); RC_WRITE=1
|
||
printf 'write_failed=%s spec=%s op=done rc=%s\n' "$c_id" "$_spec" "$_wrc"
|
||
fi
|
||
else
|
||
RC_CMD=1
|
||
"$TASKS_SH" fail "$c_id" --last-run "$NOW_ISO" >/dev/null 2>&1; _wrc=$?
|
||
if [ "$_wrc" -eq 0 ]; then
|
||
N_FAIL=$((N_FAIL + 1))
|
||
printf 'failed=%s spec=%s targets=%s first_error=%s\n' "$c_id" "$_spec" "$_n" "${_first_err:--}"
|
||
else
|
||
N_WRITE_BAD=$((N_WRITE_BAD + 1)); RC_WRITE=1
|
||
printf 'write_failed=%s spec=%s op=fail rc=%s\n' "$c_id" "$_spec" "$_wrc"
|
||
fi
|
||
fi
|
||
done <"$ROWS"
|
||
|
||
printf 'mode=%s rows=%s root=%s due=%s ran=%s ok=%s failed=%s held=%s skipped=%s write_failed=%s clis=%s cli_missing=%s repos=%s repo_scan=%s\n' \
|
||
"$([ "$DRYRUN" -eq 1 ] && echo plan || echo run)" "$ROWS" "${ROOT:--}" \
|
||
"$N_DUE" "$N_RUN" "$N_OK" "$N_FAIL" "$N_HELD" "$N_SKIP" "$N_WRITE_BAD" "$N_CLI" "${CLI_MISSING:--}" \
|
||
"$N_REPOS" "$REPO_SCAN"
|
||
|
||
[ -n "$REPO_WHY" ] && note "存取庫清單這一輪是空的:$REPO_WHY。帶 {repo} 代入點的那幾筆全部跳過,逐筆印在上面的 held= 那幾行。"
|
||
[ "$REPO_SCAN" = 'rc1' ] && note "存取庫掃描回 1:掃到的存取庫裡有幾個對不出 REPO_{HASH} 盤點頁頁名,或路徑帶了空白與殼層特殊字元而被拒。交出來的那幾條照用,被拒的那幾個這一輪沒有被盤點到——理由逐個印在掃描那一支的 rejected= 與 unnamed= 那幾行。"
|
||
[ -n "$CLI_MISSING" ] && warn "排程條目記著這台機器有 $JSC_ASSIST_CLIS,但這一輪只偵測到 $N_CLI 支,少了:$CLI_MISSING。帶 {cli} 代入點的那幾筆這一輪少跑了那幾支,而且**全部成功也不代表全部跑過**。成因通常是那支 CLI 裝在會過期的目錄底下(例如帶行程編號的多殼層目錄),條目帶的 PATH 是安裝當下拍的,那條路徑現在不在了。重跑一次 schedule.sh install patrol 會重新拍一份快照;那支 CLI 反覆消失就要考慮把它裝到穩定位置。"
|
||
[ "$N_HELD" -gt 0 ] && note "有 $N_HELD 筆代不出目標或指令形狀不對,這一輪跳過,逐筆印在上面的 held= 那幾行。**那幾筆的執行紀錄與失敗次數一個字都沒動**——代不出目標不是那一筆做錯了什麼,記成失敗會讓一個沒有人修得動的計數一路往上爬。"
|
||
[ "$N_SKIP" -gt 0 ] && note "有 $N_SKIP 筆這一批不跑:動作是只提醒的、動作是技能名的、還有不是內建項的,逐筆印在上面的 skip= 那幾行。"
|
||
[ "$RC_WRITE" -ne 0 ] && warn "有 $N_WRITE_BAD 筆的回寫失敗。指令跑過了,但待辦簿沒記到,下一輪會再跑一次同一筆,逐筆印在上面的 write_failed= 那幾行,各自帶了 tasks.sh 的結束碼。"
|
||
[ "$RC_CMD" -ne 0 ] && warn "有 $N_FAIL 筆的指令回非零,已經記成失敗、失敗次數加一。逐筆印在上面的 failed= 與 target_fail= 那幾行。"
|
||
|
||
[ "$RC_WRITE" -eq 0 ] || exit 4
|
||
[ "$RC_CMD" -eq 0 ] || exit 1
|
||
exit 0
|