Two mechanical sweeps, no behaviour change on a single-root install. The 14 `[[ "$p" == "$containers_dir"* ]]` prefix tests that decide manager-vs-container-user elevation become pathIsContainerData, so a file on a second storage root is no longer misclassified as manager-owned — which would have written it with the wrong owner and failed later, far from the cause. The 65 references to the WebUI's own tree become webuiDir(), which is pinned to the primary root by design. Two traps found while doing it: run_privileged.sh is sourced directly by init.sh without paths.sh, so it needs a fallback. Defining one named pathIsContainerData was wrong: generate_function_manifest.sh indexes top-level definitions, and the resulting autoload stub would have shadowed the real multi-root implementation with the primary-only fallback — silently classifying every file on a second disk as manager-owned, which is exactly the bug this sweep exists to prevent. Renamed to _runCfgIsContainerPath, which delegates when the real one is loaded. setup_lock.sh built its path in a top-level assignment, so it was evaluated at source time and needed the file flagged eager. Made it a function instead: the path resolves on call, and the file drops off LP_EAGER_FILES entirely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
7.6 KiB
Bash
189 lines
7.6 KiB
Bash
#!/bin/bash
|
|
|
|
# Routes a CLI command through the existing libreportal task processor and
|
|
# follows its log in the foreground, so the terminal sees the same output
|
|
# the WebUI sees and Ctrl-C detaches cleanly without killing the task.
|
|
#
|
|
# Usage:
|
|
# cliTaskRun "libreportal app install dashy" install dashy
|
|
# cliTaskRun "libreportal app install dashy" install dashy --detach
|
|
#
|
|
# Sets up a task file that's identical to what the WebUI's "install" button
|
|
# produces — same format, same task dir, same processor. The CLI just adds
|
|
# itself as a viewer.
|
|
#
|
|
# How the command bypass works: the recursive command in the task file is
|
|
# prefixed with `LIBREPORTAL_TASK_EXEC=1`, and the caller (the app install
|
|
# handler) honours that by running inline instead of re-enqueueing. No
|
|
# processor changes required — the bypass travels with the task.
|
|
|
|
# Resolve task dir, FIFO, the manifest of tasks dir/log paths. Mirrors what
|
|
# crontab_task_processor.sh and webui_task_create.sh use; if those move, both
|
|
# this file and they have to update together.
|
|
_taskDir() {
|
|
echo "$(webuiDir)/frontend/data/tasks"
|
|
}
|
|
|
|
_genTaskId() {
|
|
local rand
|
|
rand=$(openssl rand -hex 4 2>/dev/null || printf '%08x' $RANDOM)
|
|
echo "task_$(date +%s)_${rand}"
|
|
}
|
|
|
|
# Write the task file + append to queue.json. Identical shape to
|
|
# createTaskFile in webui_task_create.sh, but inlined here so the CLI doesn't
|
|
# have to source the WebUI generators when LP_LAZY=1 hasn't pulled them in.
|
|
_writeTaskFile() {
|
|
local task_id="$1" command="$2" task_type="$3" app_name="$4"
|
|
local task_dir; task_dir=$(_taskDir)
|
|
local task_file="$task_dir/${task_id}.json"
|
|
# ISO-8601 UTC with milliseconds. Matches the format the WebUI's
|
|
# tasks-manager.js emits (Date.now() → toISOString()), so the tasks
|
|
# panel's `new Date(task.createdAt).toLocaleString()` renders correctly.
|
|
# Falls back to seconds-precision if %3N isn't supported (busybox).
|
|
local created_at
|
|
created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ" 2>/dev/null) \
|
|
|| created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
[[ -d "$task_dir" ]] || runFileOp mkdir -p "$task_dir"
|
|
|
|
# CamelCase initial fields match the WebUI's task shape; processor adds
|
|
# snake_case status fields on top as it runs. Both task-manager.js and
|
|
# tasks-manager.js read camelCase directly (no alias needed for createdAt).
|
|
local task_json
|
|
if command -v jq >/dev/null 2>&1; then
|
|
task_json=$(jq -n \
|
|
--arg id "$task_id" \
|
|
--arg command "$command" \
|
|
--arg status queued \
|
|
--arg createdAt "$created_at" \
|
|
--arg type "${task_type:-}" \
|
|
--arg app "${app_name:-}" \
|
|
'{id:$id, command:$command, status:$status, createdAt:$createdAt,
|
|
startedAt:null, completedAt:null, heartbeatAt:null,
|
|
exitCode:null, errorMessage:null}
|
|
+ (if $type != "" then {type:$type} else {} end)
|
|
+ (if $app != "" then {app:$app} else {} end)')
|
|
else
|
|
# Plain string interpolation — command is the only field that needs
|
|
# escaping. Strip control chars; the dispatcher uses ascii only so
|
|
# this is sufficient without pulling jq in.
|
|
local esc="${command//\\/\\\\}"; esc="${esc//\"/\\\"}"
|
|
task_json="{\"id\":\"$task_id\",\"command\":\"$esc\",\"status\":\"queued\",\"createdAt\":\"$created_at\""
|
|
task_json+=",\"startedAt\":null,\"completedAt\":null,\"heartbeatAt\":null"
|
|
task_json+=",\"exitCode\":null,\"errorMessage\":null"
|
|
[[ -n "$task_type" ]] && task_json+=",\"type\":\"$task_type\""
|
|
[[ -n "$app_name" ]] && task_json+=",\"app\":\"$app_name\""
|
|
task_json+="}"
|
|
fi
|
|
|
|
printf '%s' "$task_json" | runFileWrite "$task_file"
|
|
runFileOp chmod 644 "$task_file" 2>/dev/null
|
|
|
|
local queue="$task_dir/queue.json"
|
|
[[ -f "$queue" ]] || printf '[]' | runFileWrite "$queue"
|
|
if command -v jq >/dev/null 2>&1; then
|
|
local updated; updated=$(jq --arg id "$task_id" '. + [$id]' "$queue" 2>/dev/null || echo "[\"$task_id\"]")
|
|
printf '%s' "$updated" | runFileWrite "$queue"
|
|
else
|
|
# Best-effort fallback.
|
|
local cur; cur=$(cat "$queue" 2>/dev/null)
|
|
if [[ "$cur" == "[]" || -z "$cur" ]]; then
|
|
printf '["%s"]' "$task_id" | runFileWrite "$queue"
|
|
else
|
|
printf '%s' "${cur%]}, \"$task_id\"]" | runFileWrite "$queue"
|
|
fi
|
|
fi
|
|
|
|
# Poke the FIFO so the processor dispatches immediately instead of
|
|
# waiting up to IDLE_POLL_SECS (3s) for its next read timeout.
|
|
local fifo="$task_dir/.queue.fifo"
|
|
[[ -p "$fifo" ]] && printf '%s\n' "$task_id" >> "$fifo" 2>/dev/null || true
|
|
}
|
|
|
|
# Follow a task's log + status until terminal state.
|
|
# Returns the task's exit_code (0 on completed, non-zero on failed/cancelled).
|
|
# Ctrl-C from the user detaches the follower without cancelling the task.
|
|
cliTaskFollow() {
|
|
local task_id="$1"
|
|
local task_dir; task_dir=$(_taskDir)
|
|
local task_file="$task_dir/${task_id}.json"
|
|
local log_file="$task_dir/${task_id}.log"
|
|
|
|
[[ -f "$task_file" ]] || { isError "Task $task_id not found."; return 1; }
|
|
|
|
# Ctrl-C detaches us, NOT the task. Print a hint and exit 0 — the
|
|
# processor keeps running in the background and the WebUI continues
|
|
# to show the task. `libreportal task log $task_id` (future) can
|
|
# reattach.
|
|
local detached=0
|
|
trap 'detached=1' INT
|
|
|
|
# tail -F (capital): retry-on-error, so we can start tailing before
|
|
# the log file exists. Processor creates it just before exec.
|
|
tail -n +1 -F "$log_file" 2>/dev/null &
|
|
local tail_pid=$!
|
|
|
|
# Poll the task file until status hits a terminal state OR the user
|
|
# detaches. 0.5s polling — bash + jq is cheap and the UX feels live.
|
|
local status="queued" exit_code=0
|
|
while (( detached == 0 )); do
|
|
if command -v jq >/dev/null 2>&1; then
|
|
status=$(jq -r '.status // "queued"' "$task_file" 2>/dev/null)
|
|
else
|
|
status=$(grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]+"' "$task_file" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/')
|
|
fi
|
|
case "$status" in
|
|
completed|failed|cancelled) break ;;
|
|
esac
|
|
sleep 0.5
|
|
done
|
|
trap - INT
|
|
|
|
# Let tail flush the final output buffered behind the log writer, then
|
|
# kill it cleanly.
|
|
sleep 0.3
|
|
kill "$tail_pid" 2>/dev/null
|
|
wait "$tail_pid" 2>/dev/null
|
|
|
|
if (( detached )); then
|
|
echo ""
|
|
isNotice "Detached from $task_id. Task continues in the background; the WebUI tasks panel will show its progress."
|
|
return 0
|
|
fi
|
|
|
|
if command -v jq >/dev/null 2>&1; then
|
|
exit_code=$(jq -r '.exit_code // 0' "$task_file" 2>/dev/null)
|
|
fi
|
|
case "$status" in
|
|
completed) return "${exit_code:-0}" ;;
|
|
failed) return "${exit_code:-1}" ;;
|
|
cancelled) return 130 ;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# Enqueue a command and follow until terminal state (or --detach).
|
|
# Args: command_string task_type app_name [--detach]
|
|
cliTaskRun() {
|
|
local command="$1" task_type="$2" app_name="$3" mode="$4"
|
|
|
|
if [[ -z "$command" ]]; then
|
|
isError "cliTaskRun: empty command."
|
|
return 1
|
|
fi
|
|
|
|
local task_id; task_id=$(_genTaskId)
|
|
_writeTaskFile "$task_id" "$command" "$task_type" "$app_name"
|
|
|
|
if [[ "$mode" == "--detach" ]]; then
|
|
isSuccessful "Task queued: $task_id"
|
|
isNotice "Detached — track progress in the WebUI tasks panel or via 'libreportal task log $task_id' (future)."
|
|
return 0
|
|
fi
|
|
|
|
isNotice "Queued task $task_id ($task_type${app_name:+ $app_name}). Following live output — Ctrl-C detaches without cancelling."
|
|
echo ""
|
|
cliTaskFollow "$task_id"
|
|
}
|