omnilane 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +209 -0
- package/LICENSE +21 -0
- package/README.ja.md +345 -0
- package/README.ko.md +337 -0
- package/README.md +368 -0
- package/README.zh-CN.md +317 -0
- package/README.zh-TW.md +327 -0
- package/SECURITY.md +37 -0
- package/VERSION +1 -0
- package/bin/omnilane +72 -0
- package/completions/_omnilane +114 -0
- package/completions/omnilane.bash +123 -0
- package/local.sh.example +37 -0
- package/package.json +54 -0
- package/routing.local.yaml.example +41 -0
- package/routing.yaml +34 -0
- package/scripts/configure.sh +134 -0
- package/scripts/dispatch.sh +705 -0
- package/scripts/doctor.sh +166 -0
- package/scripts/jobs.sh +802 -0
- package/scripts/lib/common.sh +247 -0
- package/scripts/lib/i18n.sh +104 -0
- package/scripts/lib/job-timeout.pl +109 -0
- package/scripts/lib/job-worker.sh +23 -0
- package/scripts/release-audit.sh +314 -0
- package/scripts/runners/run-claude.sh +37 -0
- package/scripts/runners/run-codex.sh +51 -0
- package/scripts/runners/run-exec.sh +33 -0
- package/scripts/runners/run-gemini.sh +60 -0
- package/scripts/runners/run-grok.sh +56 -0
- package/scripts/runners/run-vote.sh +119 -0
- package/scripts/ui.py +1236 -0
- package/ui/app.js +1106 -0
- package/ui/index.html +192 -0
- package/ui/styles.css +1023 -0
package/scripts/jobs.sh
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane background-job helper.
|
|
4
|
+
# Usage: jobs.sh [--json] list | status JOB_ID | result JOB_ID | stats [--last N]
|
|
5
|
+
# jobs.sh wait JOB_ID [--timeout N]
|
|
6
|
+
# jobs.sh audit [--last N] [--json] | prune [--keep N] [--apply]
|
|
7
|
+
|
|
8
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
|
|
9
|
+
JOBS="$OMNILANE_HOME/jobs"
|
|
10
|
+
JOB_ID_PATTERN='^[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+$'
|
|
11
|
+
JSON_MODE=0
|
|
12
|
+
COMMAND="unknown"
|
|
13
|
+
|
|
14
|
+
json_escape() {
|
|
15
|
+
local s="$1" out="" ch escaped code i
|
|
16
|
+
for ((i = 0; i < ${#s}; i++)); do
|
|
17
|
+
ch="${s:i:1}"
|
|
18
|
+
case "$ch" in
|
|
19
|
+
'"') out="$out\\\"" ;;
|
|
20
|
+
'\\') out="$out\\\\" ;;
|
|
21
|
+
$'\b') out="$out\\b" ;;
|
|
22
|
+
$'\f') out="$out\\f" ;;
|
|
23
|
+
$'\n') out="$out\\n" ;;
|
|
24
|
+
$'\r') out="$out\\r" ;;
|
|
25
|
+
$'\t') out="$out\\t" ;;
|
|
26
|
+
*)
|
|
27
|
+
LC_CTYPE=C printf -v code '%d' "'$ch"
|
|
28
|
+
if [[ "$code" -ge 0 && "$code" -lt 32 ]]; then
|
|
29
|
+
printf -v escaped '\\u%04x' "$code"
|
|
30
|
+
out="$out$escaped"
|
|
31
|
+
else
|
|
32
|
+
out="$out$ch"
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
done
|
|
37
|
+
printf '%s' "$out"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
die() {
|
|
41
|
+
local rc="$1" message="$2"
|
|
42
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
43
|
+
printf '{"schema_version":1,"command":"%s","ok":false,"error":"%s"}\n' \
|
|
44
|
+
"$(json_escape "$COMMAND")" "$(json_escape "$message")"
|
|
45
|
+
else
|
|
46
|
+
printf '%s\n' "$message" >&2
|
|
47
|
+
fi
|
|
48
|
+
exit "$rc"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
USAGE_TEXT="usage: jobs.sh [--json] list|status ID|result ID|tail ID [--lines N]|retry ID [--background]|stats [--last N]|wait ID [--timeout N]|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
|
|
52
|
+
|
|
53
|
+
usage() {
|
|
54
|
+
die 2 "$USAGE_TEXT"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
validate_job_store() {
|
|
58
|
+
if [[ -L "$JOBS" || ( -e "$JOBS" && ! -d "$JOBS" ) ]]; then
|
|
59
|
+
die 1 "unsafe jobs store path (want a real directory)"
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
select_job() {
|
|
64
|
+
local id="${1:-}"
|
|
65
|
+
[[ -n "$id" ]] || usage
|
|
66
|
+
[[ "$id" =~ $JOB_ID_PATTERN ]] || {
|
|
67
|
+
die 2 "invalid job id"
|
|
68
|
+
}
|
|
69
|
+
JOB_DIR="$JOBS/$id"
|
|
70
|
+
# A job must be a real child directory, never a symlink outside the store.
|
|
71
|
+
[[ -d "$JOB_DIR" && ! -L "$JOB_DIR" ]] || {
|
|
72
|
+
die 1 "no such job"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
read_exit_code() {
|
|
77
|
+
local path="$1" size value length
|
|
78
|
+
RECORDED_EXIT=""
|
|
79
|
+
[[ -f "$path" && ! -L "$path" ]] || return 1
|
|
80
|
+
size="$(LC_ALL=C wc -c < "$path" | tr -d '[:space:]')" || return 2
|
|
81
|
+
[[ "$size" =~ ^[0-9]+$ && "$size" -le 4 ]] || return 2
|
|
82
|
+
value="$(cat "$path")" || return 2
|
|
83
|
+
length="${#value}"
|
|
84
|
+
# Accept digits with either no terminator or exactly one trailing newline.
|
|
85
|
+
[[ "$size" -eq "$length" || "$size" -eq $((length + 1)) ]] || return 2
|
|
86
|
+
[[ "$value" =~ ^(0|[1-9][0-9]{0,2})$ && "$value" -le 255 ]] || return 2
|
|
87
|
+
RECORDED_EXIT="$value"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
read_public_metadata() {
|
|
91
|
+
local path="$1" size value length
|
|
92
|
+
local LC_ALL=C
|
|
93
|
+
PUBLIC_METADATA=""
|
|
94
|
+
[[ -f "$path" && ! -L "$path" ]] || return 1
|
|
95
|
+
size="$(wc -c < "$path" | tr -d '[:space:]')" || return 2
|
|
96
|
+
[[ "$size" =~ ^[0-9]+$ && "$size" -le 4096 ]] || return 2
|
|
97
|
+
value="$(cat "$path")" || return 2
|
|
98
|
+
length="${#value}"
|
|
99
|
+
[[ "$length" -gt 0 ]] || return 2
|
|
100
|
+
# Metadata is generated as one JSON line. Reject extra lines and terminal
|
|
101
|
+
# controls instead of echoing corrupted local state to the user's terminal.
|
|
102
|
+
[[ "$size" -eq "$length" || "$size" -eq $((length + 1)) ]] || return 2
|
|
103
|
+
[[ "$value" != *$'\n'* ]] || return 2
|
|
104
|
+
if printf '%s' "$value" | grep -q '[[:cntrl:]]'; then
|
|
105
|
+
return 2
|
|
106
|
+
fi
|
|
107
|
+
PUBLIC_METADATA="$value"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
valid_utf8() {
|
|
111
|
+
command -v iconv >/dev/null 2>&1 || return 1
|
|
112
|
+
printf '%s' "$1" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
read_job_pid() {
|
|
116
|
+
local path="$1" size value length
|
|
117
|
+
local LC_ALL=C
|
|
118
|
+
RECORDED_PID=""
|
|
119
|
+
[[ -f "$path" && ! -L "$path" ]] || return 1
|
|
120
|
+
# 2>/dev/null before < : the file can vanish between the check above and
|
|
121
|
+
# these reads (a job being removed mid-wait). Redirections apply left to
|
|
122
|
+
# right, so stderr is already silenced when the open fails; the failure is
|
|
123
|
+
# classified by the caller and the noise must not leak into captured output.
|
|
124
|
+
size="$(wc -c 2>/dev/null < "$path" | tr -d '[:space:]')" || return 2
|
|
125
|
+
[[ "$size" =~ ^[0-9]+$ && "$size" -le 11 ]] || return 2
|
|
126
|
+
value="$(cat "$path" 2>/dev/null)" || return 2
|
|
127
|
+
length="${#value}"
|
|
128
|
+
[[ "$size" -eq "$length" || "$size" -eq $((length + 1)) ]] || return 2
|
|
129
|
+
[[ "$value" =~ ^[1-9][0-9]{0,9}$ ]] || return 2
|
|
130
|
+
RECORDED_PID="$value"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
parse_stats_metadata() {
|
|
134
|
+
local value="$1"
|
|
135
|
+
local metadata_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"(codex|claude|grok|gemini|exec)"(,|})'
|
|
136
|
+
STATS_LANE=""
|
|
137
|
+
STATS_VENDOR=""
|
|
138
|
+
[[ "$value" =~ $metadata_re ]] || return 1
|
|
139
|
+
STATS_LANE="${BASH_REMATCH[1]}"
|
|
140
|
+
STATS_VENDOR="${BASH_REMATCH[2]}"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
parse_audit_metadata() {
|
|
144
|
+
local value="$1" metadata_re
|
|
145
|
+
local json_string='([^"\\]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*'
|
|
146
|
+
metadata_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(codex|claude|grok|gemini|exec)\",\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work)\",\"workdir\":\"${json_string}\",\"candidate\":\"[1-9][0-9]*/[1-9][0-9]*\",\"started\":\"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\"\\}$"
|
|
147
|
+
[[ "$value" =~ $metadata_re ]]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
print_stat_counts() {
|
|
151
|
+
local label="$1"
|
|
152
|
+
shift
|
|
153
|
+
[[ $# -gt 0 ]] || return 0
|
|
154
|
+
printf '%s\n' "$@" | LC_ALL=C sort | uniq -c | while read -r count value; do
|
|
155
|
+
printf '%s %s %s\n' "$label" "$value" "$count"
|
|
156
|
+
done
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
json_stat_counts() {
|
|
160
|
+
local first=1 value count
|
|
161
|
+
[[ $# -gt 0 ]] || { printf '[]'; return 0; }
|
|
162
|
+
printf '['
|
|
163
|
+
while read -r count value; do
|
|
164
|
+
[[ "$first" -eq 1 ]] || printf ','
|
|
165
|
+
first=0
|
|
166
|
+
printf '{"name":"%s","count":%s}' "$(json_escape "$value")" "$count"
|
|
167
|
+
done < <(printf '%s\n' "$@" | LC_ALL=C sort | uniq -c)
|
|
168
|
+
printf ']'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
path_mode() {
|
|
172
|
+
if stat -f '%Lp' "$1" >/dev/null 2>&1; then
|
|
173
|
+
stat -f '%Lp' "$1"
|
|
174
|
+
else
|
|
175
|
+
stat -c '%a' "$1"
|
|
176
|
+
fi
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
args=()
|
|
180
|
+
for arg in "$@"; do
|
|
181
|
+
if [[ "$arg" == "--json" ]]; then
|
|
182
|
+
[[ "$JSON_MODE" -eq 0 ]] || usage
|
|
183
|
+
JSON_MODE=1
|
|
184
|
+
else
|
|
185
|
+
args+=("$arg")
|
|
186
|
+
fi
|
|
187
|
+
done
|
|
188
|
+
# ${args[@]} with zero remaining arguments is an unbound-variable error under
|
|
189
|
+
# set -u on Bash 3.2; a bare `jobs.sh` must reach usage, not crash.
|
|
190
|
+
set -- ${args[@]+"${args[@]}"}
|
|
191
|
+
COMMAND="${1:-unknown}"
|
|
192
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
193
|
+
case "$COMMAND" in
|
|
194
|
+
list|status|result|stats|audit) ;;
|
|
195
|
+
*) usage ;;
|
|
196
|
+
esac
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
validate_job_store
|
|
200
|
+
|
|
201
|
+
case "${1:-}" in
|
|
202
|
+
help|--help|-h)
|
|
203
|
+
[[ "$JSON_MODE" -eq 0 && $# -eq 1 ]] || usage
|
|
204
|
+
echo "$USAGE_TEXT" ;;
|
|
205
|
+
tail)
|
|
206
|
+
# Peek at the live public output stream of one job (running or done)
|
|
207
|
+
# without touching its exit contract. Only out.txt is shown; stderr and
|
|
208
|
+
# worker logs stay private to `result` and the job directory.
|
|
209
|
+
[[ "$JSON_MODE" -eq 0 && $# -ge 2 ]] || usage
|
|
210
|
+
tail_id="$2"
|
|
211
|
+
lines=20
|
|
212
|
+
shift 2
|
|
213
|
+
while [[ $# -gt 0 ]]; do
|
|
214
|
+
case "$1" in
|
|
215
|
+
--lines)
|
|
216
|
+
[[ $# -ge 2 ]] || usage
|
|
217
|
+
lines="$2"; shift 2 ;;
|
|
218
|
+
*) usage ;;
|
|
219
|
+
esac
|
|
220
|
+
done
|
|
221
|
+
[[ "$lines" =~ ^([1-9][0-9]{0,2}|1000)$ ]] || {
|
|
222
|
+
die 2 "invalid --lines value (want 1..1000)"
|
|
223
|
+
}
|
|
224
|
+
select_job "$tail_id"
|
|
225
|
+
out_path="$JOB_DIR/out.txt"
|
|
226
|
+
if [[ -L "$out_path" ]]; then
|
|
227
|
+
die 1 "unsafe output path (symlink)"
|
|
228
|
+
fi
|
|
229
|
+
if [[ ! -f "$out_path" ]]; then
|
|
230
|
+
die 1 "no output yet"
|
|
231
|
+
fi
|
|
232
|
+
tail -n "$lines" -- "$out_path" ;;
|
|
233
|
+
retry)
|
|
234
|
+
# Re-dispatch a COMPLETED job with its recorded route and original task
|
|
235
|
+
# text. Metadata parsing is fail-closed: any value that needed JSON
|
|
236
|
+
# escaping does not match the strict patterns below and refuses to retry,
|
|
237
|
+
# so corrupted or hand-edited metadata can never smuggle flags or paths.
|
|
238
|
+
[[ "$JSON_MODE" -eq 0 && $# -ge 2 ]] || usage
|
|
239
|
+
retry_id="$2"
|
|
240
|
+
retry_background=0
|
|
241
|
+
shift 2
|
|
242
|
+
while [[ $# -gt 0 ]]; do
|
|
243
|
+
case "$1" in
|
|
244
|
+
--background) retry_background=1; shift ;;
|
|
245
|
+
*) usage ;;
|
|
246
|
+
esac
|
|
247
|
+
done
|
|
248
|
+
select_job "$retry_id"
|
|
249
|
+
read_exit_code "$JOB_DIR/exit" || {
|
|
250
|
+
die 1 "retry needs a completed job (still running, or invalid exit metadata)"
|
|
251
|
+
}
|
|
252
|
+
read_public_metadata "$JOB_DIR/meta.json" || {
|
|
253
|
+
die 1 "cannot retry: unreadable job metadata"
|
|
254
|
+
}
|
|
255
|
+
retry_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"(codex|claude|grok|gemini|exec)","model":"([^"\\]*)","effort":"([^"\\]*)","timeout":([1-9][0-9]*),"job_timeout":([1-9][0-9]*|null),"mode":"(advise|work)","workdir":"([^"\\]*)",'
|
|
256
|
+
[[ "$PUBLIC_METADATA" =~ $retry_re ]] || {
|
|
257
|
+
die 1 "cannot retry: job metadata is not safely parseable"
|
|
258
|
+
}
|
|
259
|
+
retry_lane="${BASH_REMATCH[1]}"
|
|
260
|
+
retry_vendor="${BASH_REMATCH[2]}"
|
|
261
|
+
retry_model="${BASH_REMATCH[3]}"
|
|
262
|
+
retry_effort="${BASH_REMATCH[4]}"
|
|
263
|
+
retry_timeout="${BASH_REMATCH[5]}"
|
|
264
|
+
retry_job_timeout="${BASH_REMATCH[6]}"
|
|
265
|
+
retry_mode="${BASH_REMATCH[7]}"
|
|
266
|
+
retry_workdir="${BASH_REMATCH[8]}"
|
|
267
|
+
task_path="$JOB_DIR/task.txt"
|
|
268
|
+
[[ -f "$task_path" && ! -L "$task_path" ]] || {
|
|
269
|
+
die 1 "cannot retry: original task text is missing"
|
|
270
|
+
}
|
|
271
|
+
[[ -d "$retry_workdir" ]] || {
|
|
272
|
+
die 1 "cannot retry: original workdir no longer exists: $retry_workdir"
|
|
273
|
+
}
|
|
274
|
+
retry_args=(--mode "$retry_mode" --workdir "$retry_workdir" --timeout "$retry_timeout")
|
|
275
|
+
[[ "$retry_job_timeout" == "null" ]] || retry_args+=(--job-timeout "$retry_job_timeout")
|
|
276
|
+
# dispatch --vendor only accepts real CLI vendors; an exec gate is re-run
|
|
277
|
+
# through normal lane resolution plus the recorded --model script path.
|
|
278
|
+
[[ "$retry_vendor" == "exec" ]] || retry_args+=(--vendor "$retry_vendor")
|
|
279
|
+
[[ -z "$retry_model" ]] || retry_args+=(--model "$retry_model")
|
|
280
|
+
[[ -z "$retry_effort" ]] || retry_args+=(--effort "$retry_effort")
|
|
281
|
+
[[ "$retry_background" -eq 0 ]] || retry_args+=(--background)
|
|
282
|
+
exec bash "$OMNILANE_REPO/scripts/dispatch.sh" "${retry_args[@]}" \
|
|
283
|
+
"$retry_lane" - < "$task_path" ;;
|
|
284
|
+
audit)
|
|
285
|
+
limit=100
|
|
286
|
+
shift
|
|
287
|
+
while [[ $# -gt 0 ]]; do
|
|
288
|
+
case "$1" in
|
|
289
|
+
--last)
|
|
290
|
+
[[ $# -ge 2 ]] || usage
|
|
291
|
+
limit="$2"; shift 2 ;;
|
|
292
|
+
*) usage ;;
|
|
293
|
+
esac
|
|
294
|
+
done
|
|
295
|
+
[[ "$limit" =~ ^[1-9][0-9]{0,3}$ && "$limit" -le 10000 ]] || {
|
|
296
|
+
die 2 "invalid --last value (want 1..10000)"
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
findings=0
|
|
300
|
+
sampled=0
|
|
301
|
+
passed=0
|
|
302
|
+
failed=0
|
|
303
|
+
ids=()
|
|
304
|
+
audit_scopes=()
|
|
305
|
+
audit_codes=()
|
|
306
|
+
passed_ids=()
|
|
307
|
+
audit_emit() {
|
|
308
|
+
audit_scopes+=("$1")
|
|
309
|
+
audit_codes+=("$2")
|
|
310
|
+
[[ "$JSON_MODE" -eq 1 ]] || printf 'FAIL %s %s\n' "$1" "$2"
|
|
311
|
+
}
|
|
312
|
+
if [[ -d "$JOBS" ]]; then
|
|
313
|
+
mode="$(path_mode "$JOBS" 2>/dev/null || true)"
|
|
314
|
+
if [[ "$mode" != "700" ]]; then
|
|
315
|
+
audit_emit store unsafe-store-mode
|
|
316
|
+
findings=$((findings + 1))
|
|
317
|
+
fi
|
|
318
|
+
for job_dir in "$JOBS"/*; do
|
|
319
|
+
[[ -e "$job_dir" || -L "$job_dir" ]] || continue
|
|
320
|
+
if [[ -L "$job_dir" || ! -d "$job_dir" ]]; then
|
|
321
|
+
audit_emit store unsafe-job-entry
|
|
322
|
+
findings=$((findings + 1))
|
|
323
|
+
continue
|
|
324
|
+
fi
|
|
325
|
+
id="${job_dir##*/}"
|
|
326
|
+
if [[ ! "$id" =~ $JOB_ID_PATTERN ]]; then
|
|
327
|
+
audit_emit store invalid-job-name
|
|
328
|
+
findings=$((findings + 1))
|
|
329
|
+
continue
|
|
330
|
+
fi
|
|
331
|
+
ids+=("$id")
|
|
332
|
+
done
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
if [[ ${#ids[@]} -gt 0 ]]; then
|
|
336
|
+
while IFS= read -r id; do
|
|
337
|
+
sampled=$((sampled + 1))
|
|
338
|
+
job_failed=0
|
|
339
|
+
job_dir="$JOBS/$id"
|
|
340
|
+
if [[ ! -d "$job_dir" || -L "$job_dir" ]]; then
|
|
341
|
+
audit_emit "$id" changed-job-path
|
|
342
|
+
findings=$((findings + 1)); failed=$((failed + 1))
|
|
343
|
+
continue
|
|
344
|
+
fi
|
|
345
|
+
mode="$(path_mode "$job_dir" 2>/dev/null || true)"
|
|
346
|
+
if [[ "$mode" != "700" ]]; then
|
|
347
|
+
audit_emit "$id" unsafe-job-mode
|
|
348
|
+
findings=$((findings + 1)); job_failed=1
|
|
349
|
+
fi
|
|
350
|
+
for artifact in "$job_dir"/*; do
|
|
351
|
+
[[ -e "$artifact" || -L "$artifact" ]] || continue
|
|
352
|
+
if [[ -L "$artifact" ]]; then
|
|
353
|
+
audit_emit "$id" symlink-artifact
|
|
354
|
+
findings=$((findings + 1)); job_failed=1
|
|
355
|
+
elif [[ -d "$artifact" ]]; then
|
|
356
|
+
audit_emit "$id" nested-directory
|
|
357
|
+
findings=$((findings + 1)); job_failed=1
|
|
358
|
+
elif [[ -f "$artifact" ]]; then
|
|
359
|
+
mode="$(path_mode "$artifact" 2>/dev/null || true)"
|
|
360
|
+
if [[ "$mode" != "600" ]]; then
|
|
361
|
+
audit_emit "$id" unsafe-file-mode
|
|
362
|
+
findings=$((findings + 1)); job_failed=1
|
|
363
|
+
fi
|
|
364
|
+
else
|
|
365
|
+
audit_emit "$id" unsafe-artifact-type
|
|
366
|
+
findings=$((findings + 1)); job_failed=1
|
|
367
|
+
fi
|
|
368
|
+
done
|
|
369
|
+
if [[ ! -f "$job_dir/task.txt" || -L "$job_dir/task.txt" ]]; then
|
|
370
|
+
audit_emit "$id" missing-task
|
|
371
|
+
findings=$((findings + 1)); job_failed=1
|
|
372
|
+
fi
|
|
373
|
+
if ! read_public_metadata "$job_dir/meta.json" ||
|
|
374
|
+
! parse_audit_metadata "$PUBLIC_METADATA"; then
|
|
375
|
+
audit_emit "$id" invalid-metadata
|
|
376
|
+
findings=$((findings + 1)); job_failed=1
|
|
377
|
+
fi
|
|
378
|
+
if [[ -e "$job_dir/pid" || -L "$job_dir/pid" ]]; then
|
|
379
|
+
if ! read_job_pid "$job_dir/pid"; then
|
|
380
|
+
audit_emit "$id" invalid-pid
|
|
381
|
+
findings=$((findings + 1)); job_failed=1
|
|
382
|
+
fi
|
|
383
|
+
else
|
|
384
|
+
audit_emit "$id" missing-pid
|
|
385
|
+
findings=$((findings + 1)); job_failed=1
|
|
386
|
+
fi
|
|
387
|
+
if [[ -e "$job_dir/exit" || -L "$job_dir/exit" ]]; then
|
|
388
|
+
if ! read_exit_code "$job_dir/exit"; then
|
|
389
|
+
audit_emit "$id" invalid-exit
|
|
390
|
+
findings=$((findings + 1)); job_failed=1
|
|
391
|
+
fi
|
|
392
|
+
fi
|
|
393
|
+
if [[ "$job_failed" -eq 0 ]]; then
|
|
394
|
+
passed_ids+=("$id")
|
|
395
|
+
[[ "$JSON_MODE" -eq 1 ]] || printf 'PASS %s\n' "$id"
|
|
396
|
+
passed=$((passed + 1))
|
|
397
|
+
else
|
|
398
|
+
failed=$((failed + 1))
|
|
399
|
+
fi
|
|
400
|
+
done < <(printf '%s\n' "${ids[@]}" | LC_ALL=C sort -r | sed -n "1,${limit}p")
|
|
401
|
+
fi
|
|
402
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
403
|
+
printf '{"schema_version":1,"command":"audit","sampled":%s,"passed":%s,"failed":%s,"findings":[' \
|
|
404
|
+
"$sampled" "$passed" "$failed"
|
|
405
|
+
for ((index = 0; index < ${#audit_codes[@]}; index++)); do
|
|
406
|
+
[[ "$index" -eq 0 ]] || printf ','
|
|
407
|
+
printf '{"scope":"%s","code":"%s"}' \
|
|
408
|
+
"$(json_escape "${audit_scopes[$index]}")" "$(json_escape "${audit_codes[$index]}")"
|
|
409
|
+
done
|
|
410
|
+
printf '],"passed_ids":['
|
|
411
|
+
for ((index = 0; index < ${#passed_ids[@]}; index++)); do
|
|
412
|
+
[[ "$index" -eq 0 ]] || printf ','
|
|
413
|
+
printf '"%s"' "$(json_escape "${passed_ids[$index]}")"
|
|
414
|
+
done
|
|
415
|
+
printf ']}\n'
|
|
416
|
+
else
|
|
417
|
+
printf 'audit: sampled=%s passed=%s failed=%s findings=%s\n' \
|
|
418
|
+
"$sampled" "$passed" "$failed" "$findings"
|
|
419
|
+
fi
|
|
420
|
+
[[ "$findings" -eq 0 ]] || exit 1 ;;
|
|
421
|
+
stats)
|
|
422
|
+
limit=100
|
|
423
|
+
shift
|
|
424
|
+
while [[ $# -gt 0 ]]; do
|
|
425
|
+
case "$1" in
|
|
426
|
+
--last)
|
|
427
|
+
[[ $# -ge 2 ]] || usage
|
|
428
|
+
limit="$2"; shift 2 ;;
|
|
429
|
+
*) usage ;;
|
|
430
|
+
esac
|
|
431
|
+
done
|
|
432
|
+
[[ "$limit" =~ ^[1-9][0-9]{0,3}$ && "$limit" -le 10000 ]] || {
|
|
433
|
+
die 2 "invalid --last value (want 1..10000)"
|
|
434
|
+
}
|
|
435
|
+
sampled=0
|
|
436
|
+
succeeded=0
|
|
437
|
+
failed=0
|
|
438
|
+
running=0
|
|
439
|
+
invalid_exit=0
|
|
440
|
+
invalid_metadata=0
|
|
441
|
+
ids=()
|
|
442
|
+
lanes=()
|
|
443
|
+
vendors=()
|
|
444
|
+
if [[ -d "$JOBS" ]]; then
|
|
445
|
+
for job_dir in "$JOBS"/*; do
|
|
446
|
+
[[ -d "$job_dir" && ! -L "$job_dir" ]] || continue
|
|
447
|
+
id="${job_dir##*/}"
|
|
448
|
+
[[ "$id" =~ $JOB_ID_PATTERN ]] || continue
|
|
449
|
+
ids+=("$id")
|
|
450
|
+
done
|
|
451
|
+
fi
|
|
452
|
+
if [[ ${#ids[@]} -gt 0 ]]; then
|
|
453
|
+
while IFS= read -r id; do
|
|
454
|
+
sampled=$((sampled + 1))
|
|
455
|
+
job_dir="$JOBS/$id"
|
|
456
|
+
exit_path="$job_dir/exit"
|
|
457
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
458
|
+
if read_exit_code "$exit_path"; then
|
|
459
|
+
if [[ "$RECORDED_EXIT" -eq 0 ]]; then
|
|
460
|
+
succeeded=$((succeeded + 1))
|
|
461
|
+
else
|
|
462
|
+
failed=$((failed + 1))
|
|
463
|
+
fi
|
|
464
|
+
else
|
|
465
|
+
invalid_exit=$((invalid_exit + 1))
|
|
466
|
+
fi
|
|
467
|
+
else
|
|
468
|
+
running=$((running + 1))
|
|
469
|
+
fi
|
|
470
|
+
metadata_path="$job_dir/meta.json"
|
|
471
|
+
if read_public_metadata "$metadata_path" &&
|
|
472
|
+
parse_stats_metadata "$PUBLIC_METADATA"; then
|
|
473
|
+
lanes+=("$STATS_LANE")
|
|
474
|
+
vendors+=("$STATS_VENDOR")
|
|
475
|
+
else
|
|
476
|
+
invalid_metadata=$((invalid_metadata + 1))
|
|
477
|
+
fi
|
|
478
|
+
done < <(printf '%s\n' "${ids[@]}" | LC_ALL=C sort -r | sed -n "1,${limit}p")
|
|
479
|
+
fi
|
|
480
|
+
completed=$((succeeded + failed))
|
|
481
|
+
success_rate=0
|
|
482
|
+
[[ "$completed" -eq 0 ]] || success_rate=$((succeeded * 100 / completed))
|
|
483
|
+
valid_metadata=$((sampled - invalid_metadata))
|
|
484
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
485
|
+
lanes_json='[]'
|
|
486
|
+
vendors_json='[]'
|
|
487
|
+
if [[ "$valid_metadata" -gt 0 ]]; then
|
|
488
|
+
lanes_json="$(json_stat_counts "${lanes[@]}")"
|
|
489
|
+
vendors_json="$(json_stat_counts "${vendors[@]}")"
|
|
490
|
+
fi
|
|
491
|
+
printf '{"schema_version":1,"command":"stats","ok":true,"sampled":%s,"succeeded":%s,"failed":%s,"running":%s,"invalid_exit":%s,"success_rate":%s,"invalid_metadata":%s,"lanes":%s,"vendors":%s}\n' \
|
|
492
|
+
"$sampled" "$succeeded" "$failed" "$running" "$invalid_exit" "$success_rate" \
|
|
493
|
+
"$invalid_metadata" "$lanes_json" "$vendors_json"
|
|
494
|
+
else
|
|
495
|
+
printf 'jobs: sampled=%s succeeded=%s failed=%s running=%s invalid_exit=%s success_rate=%s%%\n' \
|
|
496
|
+
"$sampled" "$succeeded" "$failed" "$running" "$invalid_exit" "$success_rate"
|
|
497
|
+
printf 'invalid_metadata=%s\n' "$invalid_metadata"
|
|
498
|
+
if [[ "$valid_metadata" -gt 0 ]]; then
|
|
499
|
+
print_stat_counts lane "${lanes[@]}"
|
|
500
|
+
print_stat_counts vendor "${vendors[@]}"
|
|
501
|
+
fi
|
|
502
|
+
fi ;;
|
|
503
|
+
list)
|
|
504
|
+
[[ $# -eq 1 ]] || usage
|
|
505
|
+
if [[ ! -d "$JOBS" ]]; then
|
|
506
|
+
[[ "$JSON_MODE" -eq 0 ]] || printf '{"schema_version":1,"command":"list","ok":true,"jobs":[]}\n'
|
|
507
|
+
exit 0
|
|
508
|
+
fi
|
|
509
|
+
ids=()
|
|
510
|
+
for job_dir in "$JOBS"/*; do
|
|
511
|
+
[[ -d "$job_dir" && ! -L "$job_dir" ]] || continue
|
|
512
|
+
id="${job_dir##*/}"
|
|
513
|
+
[[ "$id" =~ $JOB_ID_PATTERN ]] || continue
|
|
514
|
+
ids+=("$id")
|
|
515
|
+
done
|
|
516
|
+
if [[ ${#ids[@]} -eq 0 ]]; then
|
|
517
|
+
[[ "$JSON_MODE" -eq 0 ]] || printf '{"schema_version":1,"command":"list","ok":true,"jobs":[]}\n'
|
|
518
|
+
exit 0
|
|
519
|
+
fi
|
|
520
|
+
[[ "$JSON_MODE" -eq 0 ]] || printf '{"schema_version":1,"command":"list","ok":true,"jobs":['
|
|
521
|
+
json_first=1
|
|
522
|
+
while IFS= read -r id; do
|
|
523
|
+
state="running"
|
|
524
|
+
exit_json="null"
|
|
525
|
+
exit_path="$JOBS/$id/exit"
|
|
526
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
527
|
+
if read_exit_code "$exit_path"; then
|
|
528
|
+
state="done(exit $RECORDED_EXIT)"
|
|
529
|
+
json_state="done"
|
|
530
|
+
exit_json="$RECORDED_EXIT"
|
|
531
|
+
else
|
|
532
|
+
state="done(invalid exit metadata)"
|
|
533
|
+
json_state="invalid"
|
|
534
|
+
fi
|
|
535
|
+
else
|
|
536
|
+
json_state="running"
|
|
537
|
+
fi
|
|
538
|
+
metadata=""
|
|
539
|
+
metadata_json="null"
|
|
540
|
+
metadata_status="missing"
|
|
541
|
+
metadata_path="$JOBS/$id/meta.json"
|
|
542
|
+
if [[ -f "$metadata_path" && ! -L "$metadata_path" ]]; then
|
|
543
|
+
if read_public_metadata "$metadata_path"; then
|
|
544
|
+
metadata="$PUBLIC_METADATA"
|
|
545
|
+
if [[ "$JSON_MODE" -eq 0 ]] || valid_utf8 "$PUBLIC_METADATA"; then
|
|
546
|
+
metadata_json="\"$(json_escape "$PUBLIC_METADATA")\""
|
|
547
|
+
metadata_status="valid"
|
|
548
|
+
else
|
|
549
|
+
metadata_status="invalid"
|
|
550
|
+
fi
|
|
551
|
+
else
|
|
552
|
+
metadata="[invalid metadata]"
|
|
553
|
+
metadata_status="invalid"
|
|
554
|
+
fi
|
|
555
|
+
elif [[ -e "$metadata_path" || -L "$metadata_path" ]]; then
|
|
556
|
+
metadata_status="invalid"
|
|
557
|
+
fi
|
|
558
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
559
|
+
[[ "$json_first" -eq 1 ]] || printf ','
|
|
560
|
+
json_first=0
|
|
561
|
+
printf '{"id":"%s","state":"%s","exit_code":%s,"metadata":%s,"metadata_status":"%s"}' \
|
|
562
|
+
"$(json_escape "$id")" "$json_state" "$exit_json" "$metadata_json" "$metadata_status"
|
|
563
|
+
else
|
|
564
|
+
printf '%s %s %s\n' "$id" "$state" "$metadata"
|
|
565
|
+
fi
|
|
566
|
+
done < <(printf '%s\n' "${ids[@]}" | LC_ALL=C sort -r | sed -n '1,20p')
|
|
567
|
+
[[ "$JSON_MODE" -eq 0 ]] || printf ']}\n' ;;
|
|
568
|
+
status)
|
|
569
|
+
[[ $# -eq 2 ]] || usage
|
|
570
|
+
select_job "$2"
|
|
571
|
+
exit_path="$JOB_DIR/exit"
|
|
572
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
573
|
+
read_exit_code "$exit_path" || {
|
|
574
|
+
die 1 "invalid recorded exit status"
|
|
575
|
+
}
|
|
576
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
577
|
+
printf '{"schema_version":1,"command":"status","ok":true,"job":{"id":"%s","state":"done","exit_code":%s}}\n' \
|
|
578
|
+
"$(json_escape "$2")" "$RECORDED_EXIT"
|
|
579
|
+
else
|
|
580
|
+
echo "done exit=$RECORDED_EXIT"
|
|
581
|
+
fi
|
|
582
|
+
else
|
|
583
|
+
pid=""; pid_state="missing"
|
|
584
|
+
if [[ -e "$JOB_DIR/pid" || -L "$JOB_DIR/pid" ]]; then
|
|
585
|
+
if read_job_pid "$JOB_DIR/pid"; then
|
|
586
|
+
pid="$RECORDED_PID"; pid_state="valid"
|
|
587
|
+
else
|
|
588
|
+
pid_state="invalid"
|
|
589
|
+
fi
|
|
590
|
+
fi
|
|
591
|
+
if [[ "$pid_state" == "invalid" ]]; then
|
|
592
|
+
state="dead"; reason="invalid pid metadata"
|
|
593
|
+
elif [[ "$pid_state" == "valid" ]] && ! kill -0 "$pid" 2>/dev/null; then
|
|
594
|
+
state="dead"; reason="worker gone, no exit recorded"
|
|
595
|
+
else
|
|
596
|
+
state="running"; reason=""
|
|
597
|
+
fi
|
|
598
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
599
|
+
if [[ -n "$reason" ]]; then
|
|
600
|
+
printf '{"schema_version":1,"command":"status","ok":true,"job":{"id":"%s","state":"%s","exit_code":null,"reason":"%s"}}\n' \
|
|
601
|
+
"$(json_escape "$2")" "$state" "$(json_escape "$reason")"
|
|
602
|
+
else
|
|
603
|
+
printf '{"schema_version":1,"command":"status","ok":true,"job":{"id":"%s","state":"running","exit_code":null}}\n' \
|
|
604
|
+
"$(json_escape "$2")"
|
|
605
|
+
fi
|
|
606
|
+
elif [[ -n "$reason" ]]; then
|
|
607
|
+
printf 'dead (%s)\n' "$reason"
|
|
608
|
+
else
|
|
609
|
+
echo "running"
|
|
610
|
+
fi
|
|
611
|
+
fi ;;
|
|
612
|
+
wait)
|
|
613
|
+
[[ "$JSON_MODE" -eq 0 && $# -ge 2 ]] || usage
|
|
614
|
+
wait_id="$2"
|
|
615
|
+
wait_timeout=600
|
|
616
|
+
shift 2
|
|
617
|
+
while [[ $# -gt 0 ]]; do
|
|
618
|
+
case "$1" in
|
|
619
|
+
--timeout)
|
|
620
|
+
[[ $# -ge 2 ]] || usage
|
|
621
|
+
wait_timeout="$2"; shift 2 ;;
|
|
622
|
+
*) usage ;;
|
|
623
|
+
esac
|
|
624
|
+
done
|
|
625
|
+
[[ "$wait_timeout" =~ ^(0|[1-9][0-9]{0,4})$ && "$wait_timeout" -le 86400 ]] || {
|
|
626
|
+
die 2 "invalid --timeout value (want 0..86400)"
|
|
627
|
+
}
|
|
628
|
+
select_job "$wait_id"
|
|
629
|
+
wait_started="$SECONDS"
|
|
630
|
+
while true; do
|
|
631
|
+
[[ -d "$JOB_DIR" && ! -L "$JOB_DIR" ]] || {
|
|
632
|
+
die 1 "job disappeared while waiting"
|
|
633
|
+
}
|
|
634
|
+
exit_path="$JOB_DIR/exit"
|
|
635
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
636
|
+
read_exit_code "$exit_path" || {
|
|
637
|
+
die 1 "invalid recorded exit status"
|
|
638
|
+
}
|
|
639
|
+
echo "done exit=$RECORDED_EXIT"
|
|
640
|
+
exit "$RECORDED_EXIT"
|
|
641
|
+
fi
|
|
642
|
+
|
|
643
|
+
pid=""; pid_state="missing"
|
|
644
|
+
if [[ -e "$JOB_DIR/pid" || -L "$JOB_DIR/pid" ]]; then
|
|
645
|
+
if read_job_pid "$JOB_DIR/pid"; then
|
|
646
|
+
pid="$RECORDED_PID"; pid_state="valid"
|
|
647
|
+
else
|
|
648
|
+
pid_state="invalid"
|
|
649
|
+
fi
|
|
650
|
+
fi
|
|
651
|
+
if [[ "$pid_state" == "invalid" ]]; then
|
|
652
|
+
# A pid file that vanished between the existence check and the bounded
|
|
653
|
+
# read is a job being removed, not corrupt metadata; loop again so the
|
|
654
|
+
# directory check reports the disappearance.
|
|
655
|
+
if [[ ! -e "$JOB_DIR/pid" && ! -L "$JOB_DIR/pid" ]]; then
|
|
656
|
+
continue
|
|
657
|
+
fi
|
|
658
|
+
echo "dead (invalid pid metadata)"
|
|
659
|
+
exit 125
|
|
660
|
+
elif [[ "$pid_state" == "valid" ]] && ! kill -0 "$pid" 2>/dev/null; then
|
|
661
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
662
|
+
continue
|
|
663
|
+
fi
|
|
664
|
+
echo "dead (worker gone, no exit recorded)"
|
|
665
|
+
exit 125
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
wait_elapsed=$((SECONDS - wait_started))
|
|
669
|
+
if [[ "$wait_elapsed" -ge "$wait_timeout" ]]; then
|
|
670
|
+
printf 'wait timeout after %ss\n' "$wait_timeout" >&2
|
|
671
|
+
exit 124
|
|
672
|
+
fi
|
|
673
|
+
sleep 1
|
|
674
|
+
done ;;
|
|
675
|
+
result)
|
|
676
|
+
[[ $# -eq 2 ]] || usage
|
|
677
|
+
select_job "$2"
|
|
678
|
+
exit_path="$JOB_DIR/exit"
|
|
679
|
+
[[ -e "$exit_path" || -L "$exit_path" ]] || {
|
|
680
|
+
die 1 "still running"
|
|
681
|
+
}
|
|
682
|
+
read_exit_code "$exit_path" || {
|
|
683
|
+
die 1 "invalid recorded exit status"
|
|
684
|
+
}
|
|
685
|
+
rc="$RECORDED_EXIT"
|
|
686
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
687
|
+
output_available=false
|
|
688
|
+
stderr_available=false
|
|
689
|
+
[[ -f "$JOB_DIR/out.txt" && ! -L "$JOB_DIR/out.txt" ]] && output_available=true
|
|
690
|
+
[[ -s "$JOB_DIR/out.txt.stderr.log" && ! -L "$JOB_DIR/out.txt.stderr.log" ]] && stderr_available=true
|
|
691
|
+
printf '{"schema_version":1,"command":"result","ok":true,"job":{"id":"%s","state":"done","exit_code":%s,"output_available":%s,"stderr_available":%s}}\n' \
|
|
692
|
+
"$(json_escape "$2")" "$rc" "$output_available" "$stderr_available"
|
|
693
|
+
else
|
|
694
|
+
# Guarded cat: under set -e a missing out.txt must not eat the real exit code.
|
|
695
|
+
[[ -f "$JOB_DIR/out.txt" && ! -L "$JOB_DIR/out.txt" ]] && cat "$JOB_DIR/out.txt"
|
|
696
|
+
if [[ -s "$JOB_DIR/out.txt.stderr.log" && ! -L "$JOB_DIR/out.txt.stderr.log" ]]; then
|
|
697
|
+
echo "--- stderr ---" >&2; cat "$JOB_DIR/out.txt.stderr.log" >&2
|
|
698
|
+
fi
|
|
699
|
+
fi
|
|
700
|
+
exit "$rc" ;;
|
|
701
|
+
prune)
|
|
702
|
+
keep=100
|
|
703
|
+
keep_given=0
|
|
704
|
+
older_than=""
|
|
705
|
+
apply=0
|
|
706
|
+
shift
|
|
707
|
+
while [[ $# -gt 0 ]]; do
|
|
708
|
+
case "$1" in
|
|
709
|
+
--keep)
|
|
710
|
+
[[ $# -ge 2 ]] || usage
|
|
711
|
+
keep="$2"; keep_given=1; shift 2 ;;
|
|
712
|
+
--older-than)
|
|
713
|
+
[[ $# -ge 2 ]] || usage
|
|
714
|
+
older_than="$2"; shift 2 ;;
|
|
715
|
+
--apply)
|
|
716
|
+
apply=1; shift ;;
|
|
717
|
+
*) usage ;;
|
|
718
|
+
esac
|
|
719
|
+
done
|
|
720
|
+
[[ "$keep" =~ ^(0|[1-9][0-9]{0,8})$ ]] || {
|
|
721
|
+
echo "invalid --keep value (want 0..999999999)" >&2
|
|
722
|
+
exit 2
|
|
723
|
+
}
|
|
724
|
+
cutoff=""
|
|
725
|
+
if [[ -n "$older_than" ]]; then
|
|
726
|
+
[[ "$older_than" =~ ^[1-9][0-9]{0,3}$ ]] || {
|
|
727
|
+
die 2 "invalid --older-than value (want 1..9999 days)"
|
|
728
|
+
}
|
|
729
|
+
# Age comes from the timestamp embedded in the job id (local time, the
|
|
730
|
+
# same clock that created it), not from mtime, which any tool can touch.
|
|
731
|
+
# The fixed-width YYYYMMDD-HHMMSS format makes string comparison exact.
|
|
732
|
+
if ! cutoff="$(date -v "-${older_than}d" +%Y%m%d-%H%M%S 2>/dev/null)" &&
|
|
733
|
+
! cutoff="$(date -d "${older_than} days ago" +%Y%m%d-%H%M%S 2>/dev/null)"; then
|
|
734
|
+
die 1 "cannot compute --older-than cutoff on this host's date command"
|
|
735
|
+
fi
|
|
736
|
+
[[ "$cutoff" =~ ^[0-9]{8}-[0-9]{6}$ ]] || {
|
|
737
|
+
die 1 "cannot compute --older-than cutoff on this host's date command"
|
|
738
|
+
}
|
|
739
|
+
# --older-than alone prunes purely by age; an explicit --keep composes
|
|
740
|
+
# as AND (a job must be both beyond the keep window and old enough).
|
|
741
|
+
[[ "$keep_given" -eq 1 ]] || keep=0
|
|
742
|
+
fi
|
|
743
|
+
completed=()
|
|
744
|
+
if [[ -d "$JOBS" ]]; then
|
|
745
|
+
for job_dir in "$JOBS"/*; do
|
|
746
|
+
[[ -d "$job_dir" && ! -L "$job_dir" ]] || continue
|
|
747
|
+
id="${job_dir##*/}"
|
|
748
|
+
[[ "$id" =~ $JOB_ID_PATTERN ]] || continue
|
|
749
|
+
read_exit_code "$job_dir/exit" || continue
|
|
750
|
+
completed+=("$id")
|
|
751
|
+
done
|
|
752
|
+
fi
|
|
753
|
+
candidates=()
|
|
754
|
+
index=0
|
|
755
|
+
if [[ ${#completed[@]} -gt 0 ]]; then
|
|
756
|
+
while IFS= read -r id; do
|
|
757
|
+
index=$((index + 1))
|
|
758
|
+
[[ "$index" -le "$keep" ]] && continue
|
|
759
|
+
if [[ -n "$cutoff" ]]; then
|
|
760
|
+
[[ "${id:0:15}" < "$cutoff" ]] || continue
|
|
761
|
+
fi
|
|
762
|
+
candidates+=("$id")
|
|
763
|
+
done < <(printf '%s\n' "${completed[@]}" | sort -r)
|
|
764
|
+
fi
|
|
765
|
+
|
|
766
|
+
deleted=0
|
|
767
|
+
# ${arr[@]} on an empty array is an unbound-variable error under set -u
|
|
768
|
+
# on Bash 3.2, so an empty candidate list must never reach the loop.
|
|
769
|
+
if [[ ${#candidates[@]} -gt 0 ]]; then
|
|
770
|
+
for id in "${candidates[@]}"; do
|
|
771
|
+
job_dir="$JOBS/$id"
|
|
772
|
+
if [[ "$apply" -eq 0 ]]; then
|
|
773
|
+
printf 'would delete %s\n' "$id"
|
|
774
|
+
continue
|
|
775
|
+
fi
|
|
776
|
+
# Re-check immediately before deletion so a replaced path or a job whose
|
|
777
|
+
# completion marker disappeared is never removed.
|
|
778
|
+
if [[ -d "$job_dir" && ! -L "$job_dir" ]] &&
|
|
779
|
+
read_exit_code "$job_dir/exit"; then
|
|
780
|
+
/bin/rm -rf "$job_dir"
|
|
781
|
+
printf 'deleted %s\n' "$id"
|
|
782
|
+
deleted=$((deleted + 1))
|
|
783
|
+
else
|
|
784
|
+
printf 'skipped changed job %s\n' "$id" >&2
|
|
785
|
+
fi
|
|
786
|
+
done
|
|
787
|
+
fi
|
|
788
|
+
if [[ "$apply" -eq 1 ]]; then
|
|
789
|
+
if [[ -n "$older_than" && "$keep_given" -eq 1 ]]; then
|
|
790
|
+
printf '%s jobs deleted; kept the newest %s completed jobs and everything newer than %s days\n' \
|
|
791
|
+
"$deleted" "$keep" "$older_than"
|
|
792
|
+
elif [[ -n "$older_than" ]]; then
|
|
793
|
+
printf '%s jobs deleted; completed jobs newer than %s days retained\n' \
|
|
794
|
+
"$deleted" "$older_than"
|
|
795
|
+
else
|
|
796
|
+
printf '%s jobs deleted; newest %s completed jobs retained\n' "$deleted" "$keep"
|
|
797
|
+
fi
|
|
798
|
+
else
|
|
799
|
+
printf '%s jobs eligible; rerun with --apply to delete\n' "${#candidates[@]}"
|
|
800
|
+
fi ;;
|
|
801
|
+
*) usage ;;
|
|
802
|
+
esac
|