The progress modal faked its steps on a timer because the worker only wrote to syslog. Now every updates_log line is appended to <job>.log (UPDATES_JOB_LOG), the running state advertises log_path, the log survives job completion until the TTL sweep, and the new component_action_log CLI mode serves it from a byte offset. The UI polls it into a monospace panel with auto-scroll, exposes a Copy log button, and the modal cannot be closed while the job runs (only the disabled progress button is shown).
116 lines
4.8 KiB
Bash
116 lines
4.8 KiB
Bash
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
|
|
# The UI shows a live log of a running component action. The worker's progress
|
|
# messages used to go to syslog only, so the modal had nothing real to display
|
|
# and faked its steps on a timer. The contract: the worker appends every
|
|
# progress line to <job>.log (UPDATES_JOB_LOG), the running state advertises the
|
|
# path, the log outlives the job (only the age-based sweep removes it), and the
|
|
# component-action-log CLI mode serves it from an arbitrary byte offset.
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
UPDATES_UC="$ROOT_DIR/tachyon/files/usr/lib/components/updates.uc"
|
|
ACTION_UC="$ROOT_DIR/tachyon/files/usr/lib/components/action.uc"
|
|
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
|
|
|
fail() {
|
|
printf 'FAIL: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
code_only() {
|
|
sed 's://.*::'
|
|
}
|
|
|
|
# The worker must hand the action module a dedicated log file path.
|
|
worker="$(sed -n '/^function component_action_worker/,/^}/p' "$UPDATES_UC" | code_only)"
|
|
[ -n "$worker" ] || fail "components/updates.uc must define component_action_worker"
|
|
grep -Fq 'UPDATES_JOB_LOG' <<<"$worker" ||
|
|
fail "component_action_worker must set UPDATES_JOB_LOG for the action module"
|
|
grep -Eq 'UPDATES_JOB_LOG[^=]*=\s*.*\.log' <<<"$worker" ||
|
|
fail "component_action_worker must point UPDATES_JOB_LOG at a .log file"
|
|
|
|
# The log path must be derived from the job id, the same way the .out file is.
|
|
grep -Fq 'function component_job_log_path' "$UPDATES_UC" ||
|
|
fail "components/updates.uc must define component_job_log_path"
|
|
grep -Fq '.log' "$UPDATES_UC" ||
|
|
fail "component_job_log_path must produce a .log path"
|
|
|
|
# The running state must carry the log path so the UI can find the file.
|
|
grep -Fq 'log_path' "$UPDATES_UC" ||
|
|
fail "the component job state must expose log_path"
|
|
|
|
# finish_component_job removes the result channel and stderr, but the log is the
|
|
# UI's evidence of what happened: it must survive until the TTL sweep.
|
|
finish="$(sed -n '/^function finish_component_job/,/^}/p' "$UPDATES_UC" | code_only)"
|
|
[ -n "$finish" ] || fail "components/updates.uc must define finish_component_job"
|
|
grep -Fq 'remove_file(output_file + ".stderr")' <<<"$finish" ||
|
|
fail "finish_component_job must still remove the .stderr file"
|
|
if grep -Fq '"log"' <<<"$finish"; then
|
|
fail "finish_component_job must not remove the job .log file"
|
|
fi
|
|
|
|
# Only the age-based orphan sweep may drop .log files.
|
|
grep -Fq '"*.log"' "$UPDATES_UC" ||
|
|
fail "the age-based orphan cleanup must cover *.log"
|
|
|
|
# The progress line must reach both syslog and the job log file.
|
|
updates_log="$(sed -n '/^function updates_log/,/^}/p' "$ACTION_UC" | code_only)"
|
|
[ -n "$updates_log" ] || fail "components/action.uc must define updates_log"
|
|
grep -Fq 'log_message(' <<<"$updates_log" ||
|
|
fail "updates_log must keep writing to syslog"
|
|
grep -Fq 'job_log_append(' <<<"$updates_log" ||
|
|
fail "updates_log must append to the job log file"
|
|
grep -Fq 'getenv("UPDATES_JOB_LOG")' "$ACTION_UC" ||
|
|
fail "action.uc must read the UPDATES_JOB_LOG env var"
|
|
|
|
# The CLI must expose the log mode.
|
|
grep -Fq 'component_action_log' "$TACHYON_BIN" ||
|
|
fail "/usr/bin/tachyon must declare component_action_log"
|
|
|
|
# --- behavioural: offset reads, clamping, missing file -----------------------
|
|
|
|
if command -v ucode >/dev/null 2>&1; then
|
|
WORK_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
|
|
|
mkdir -p "$WORK_DIR/jobs"
|
|
printf 'line1\nline2\nline3\n' >"$WORK_DIR/jobs/abc-1_2.log"
|
|
|
|
run_log() {
|
|
UPDATES_JOB_DIR="$WORK_DIR/jobs" \
|
|
ucode -L "$ROOT_DIR/tachyon/files/usr/lib" \
|
|
"$UPDATES_UC" component-action-log "$@"
|
|
}
|
|
|
|
full="$(run_log abc-1_2 0)"
|
|
grep -Fq '"success": true' <<<"$full" ||
|
|
fail "component-action-log must report success for an existing log: $full"
|
|
grep -Fq '"log": "line1\nline2\nline3\n"' <<<"$full" ||
|
|
fail "component-action-log offset 0 must return the whole file: $full"
|
|
grep -Fq '"offset": 18' <<<"$full" ||
|
|
fail "component-action-log must report the total byte length: $full"
|
|
|
|
tail="$(run_log abc-1_2 12)"
|
|
grep -Fq '"log": "line3\n"' <<<"$tail" ||
|
|
fail "component-action-log must read from the requested byte offset: $tail"
|
|
grep -Fq '"offset": 18' <<<"$tail" ||
|
|
fail "component-action-log must report the new offset after a partial read: $tail"
|
|
|
|
clamped="$(run_log abc-1_2 999)"
|
|
grep -Fq '"log": ""' <<<"$clamped" ||
|
|
fail "component-action-log must clamp an offset past the end of the file: $clamped"
|
|
grep -Fq '"offset": 18' <<<"$clamped" ||
|
|
fail "component-action-log must clamp the reported offset to the file length: $clamped"
|
|
|
|
set +e
|
|
missing="$(run_log nope 0 2>/dev/null)"
|
|
status=$?
|
|
set -e
|
|
[ "$status" -ne 0 ] || fail "component-action-log must exit non-zero for a missing log file"
|
|
grep -Fq '"success": false' <<<"$missing" ||
|
|
fail "component-action-log must report failure for a missing log file: $missing"
|
|
fi
|
|
|
|
printf 'component action log checks passed\n'
|