openclaw/scripts/lib/docker-e2e-container.sh
Peter Steinberger 3cb68020b6
refactor(scripts): deslop shared tooling
## What Problem This Solves

Repository scripts retain private copies of shared tooling helpers and an unused translation subprocess runner, increasing maintenance work across sibling scripts.

## User Impact

Internal tooling cleanup: existing flags, diagnostics, generated bytes, JSON formats, exit codes, and cleanup policies stay the same. The build wrapper additionally detects forbidden dynamic-import warnings when compiler output splits the warning marker across chunks.

## Why This Change Was Made

- Remove the retired i18n process runner. Its final production caller moved to the shared formatter in #95534; retain real CLI/privacy/provider tests and run their subprocesses with the existing dependency.
- Share Docker resource, signal-trap, platform, workspace-staging, and plugin-selection helpers while retaining each caller's environment precedence and lifecycle decisions.
- Reuse existing E2E fixture JSON/assertion and package-path helpers; retain distinct write modes, recovery ordering, and mounted/frozen harness contracts.
- Share macOS guest desktop-user/home resolution through its existing owner while preserving per-caller timeout policy.
- Route release/mobile flags through the existing version flag specifications, preserving split-only values, duplicate/mode rules, help timing, and error text. Reuse existing retry sleep and comparator owners and remove an unreachable iOS output branch.
- Reuse existing guard entrypoint, failure-trailer, diagnostic-line, and metadata-normalization owners.

Measured reduction: **732 net production lines**, with test changes counted separately. PR tooling and its protected import closure, CI planners/shards, baselines, and generated artifacts are unchanged.

## Fixes Found Along the Way

The tsdown scanner checked each raw output chunk for its warning marker. It now checks the existing combined-line buffer. The regression exercises every split inside the marker and fails on the original source for the intended missing-warning assertion.

## Evidence

- Blacksmith Testbox `tbx_01m3tp4sabwd0807kj9jkqqmbr`: frozen dependency install and candidate source-byte verification; all nine changed test files and 59 selected sibling files passed, including real CLI, package fixture, recovery, Docker harness, and Parallels transport contracts.
- Generated channel metadata is byte-identical; differential metadata normalization cases passed.
- SDK surface check passed. Independent isolated Codex review completed with no P0–P2 findings.
- Initial changed checks caught an invalid direct source import in a proposed snapshot-distance consolidation; that independent change was withdrawn. No boundary exception was added. The final changed checks pass, including script/test lint and Docker shell/scheduler checks; madge reports 0 cycles and the runtime import check reports 0 cycles.
- Per-file single-worker wall time for changed suites: translation 9.92s; Docker helper 38.88s; live Docker auth 1.71s; mobile ref 2.36s; mobile release 6.23s; release preparation 2.42s; version 2.04s; installer 15.01s; tsdown 2.70s. The existing Docker helper suite executes shell/process cleanup and container-command boundary fixtures; new assertions reuse those fixtures. New parser/scanner cases use no sleeps or polling.

Hosted CI will be verified against this PR's exact pushed head. No live deployment or release was performed.
2026-10-01 05:14:07 +00:00

283 lines
8 KiB
Bash

#!/usr/bin/env bash
#
# Shared helpers for Docker E2E scripts that keep a named container running
# while polling readiness from the host.
DOCKER_E2E_CONTAINER_LIB_DIR="${BASH_SOURCE[0]}"
if [[ "$DOCKER_E2E_CONTAINER_LIB_DIR" == */* ]]; then
DOCKER_E2E_CONTAINER_LIB_DIR="${DOCKER_E2E_CONTAINER_LIB_DIR%/*}"
else
DOCKER_E2E_CONTAINER_LIB_DIR=.
fi
DOCKER_E2E_CONTAINER_LIB_DIR="$(cd "$DOCKER_E2E_CONTAINER_LIB_DIR" && pwd)"
source "$DOCKER_E2E_CONTAINER_LIB_DIR/docker-e2e-resource-diagnostics.sh"
docker_e2e_timeout_bin() {
if command -v timeout >/dev/null 2>&1; then
printf '%s\n' timeout
elif command -v gtimeout >/dev/null 2>&1; then
printf '%s\n' gtimeout
else
return 1
fi
}
docker_e2e_timeout_cmd() {
local timeout_value="$1"
shift
local timeout_bin
if ! timeout_bin="$(docker_e2e_timeout_bin)"; then
if command -v node >/dev/null 2>&1; then
echo "timeout command not found; using Node watchdog for Docker command timeout ${timeout_value}" >&2
# Keep argv unchanged: Node historically parses option-like timeout values.
local helper_source="$DOCKER_E2E_CONTAINER_LIB_DIR/docker-e2e-watchdog.mjs"
helper_source="${helper_source//\\/\\\\}"
helper_source="${helper_source//\"/\\\"}"
helper_source="${helper_source//$'\n'/\\n}"
helper_source="${helper_source//$'\r'/\\r}"
node --input-type=module -e "
import { pathToFileURL } from 'node:url';
const { runWatchdog } = await import(pathToFileURL(\"$helper_source\").href);
const [, timeoutValue, ...args] = process.argv;
await runWatchdog('docker', timeoutValue, args);
" "$timeout_value" "$@"
return "$?"
fi
echo "timeout command not found; cannot bound Docker command after ${timeout_value}" >&2
return 127
fi
if "$timeout_bin" --kill-after=1s 1s true >/dev/null 2>&1; then
"$timeout_bin" --kill-after=30s "$timeout_value" "$@"
else
"$timeout_bin" "$timeout_value" "$@"
fi
}
docker_e2e_docker_cmd() {
local timeout_value="${DOCKER_COMMAND_TIMEOUT:-600s}"
if [ "${1:-}" = "run" ]; then
shift
docker_e2e_docker_run_resource_args "$@" || return $?
docker_e2e_docker_run_with_resource_diagnostics "$timeout_value" "$@"
# A bare return in an EXIT trap can restore the trap's original status.
return "$?"
fi
docker_e2e_timeout_cmd "$timeout_value" docker "$@"
}
docker_e2e_cleanup_container_run() {
docker_e2e_docker_cmd rm -f "$1" >/dev/null 2>&1 || true
rm -f "$2"
}
docker_e2e_docker_run_cmd() {
local timeout_value="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}"
if [ "${1:-}" = "run" ]; then
shift
docker_e2e_docker_run_resource_args "$@" || return $?
docker_e2e_docker_run_with_resource_diagnostics "$timeout_value" "$@"
return "$?"
fi
docker_e2e_timeout_cmd "$timeout_value" docker "$@"
}
docker_e2e_resource_limits_disabled() {
case "${OPENCLAW_DOCKER_E2E_DISABLE_RESOURCE_LIMITS:-}" in
1 | true | TRUE | yes | YES | on | ON)
return 0
;;
esac
return 1
}
docker_e2e_resource_value_disabled() {
case "${1:-}" in
"" | 0 | none | NONE | off | OFF | false | FALSE)
return 0
;;
esac
return 1
}
docker_e2e_detect_available_cpus() {
local available="${1:-${OPENCLAW_DOCKER_E2E_AVAILABLE_CPUS:-}}"
if [ -n "$available" ]; then
printf '%s\n' "$available"
return 0
fi
if command -v nproc >/dev/null 2>&1; then
nproc
return 0
fi
if command -v getconf >/dev/null 2>&1; then
getconf _NPROCESSORS_ONLN
return 0
fi
return 1
}
docker_e2e_resolve_cpus() {
local requested="$1"
local available=""
available="$(docker_e2e_detect_available_cpus "${2:-}" 2>/dev/null || true)"
if [[ "$requested" =~ ^[0-9]+$ ]] && [[ "$available" =~ ^[0-9]+$ ]] && [ "$requested" -gt "$available" ]; then
printf '%s\n' "$available"
return 0
fi
printf '%s\n' "$requested"
}
docker_e2e_run_arg_present() {
local option="$1"
shift
local arg
for arg in "$@"; do
if [ "$arg" = "$option" ] || [[ "$arg" == "$option="* ]]; then
return 0
fi
case "$option:$arg" in
--memory:-m | --memory:-m=*)
return 0
;;
esac
done
return 1
}
docker_e2e_resolve_pids_limit() {
local pids_limit="$1"
local env_name="${2:-OPENCLAW_DOCKER_E2E_PIDS_LIMIT}"
if [[ ! "$pids_limit" =~ ^[0-9]+$ ]] || (( 10#$pids_limit < 1 )); then
echo "invalid $env_name: $pids_limit" >&2
return 2
fi
printf '%s\n' "$((10#$pids_limit))"
}
docker_e2e_docker_run_resource_args() {
DOCKER_E2E_RUN_RESOURCE_ARGS=()
docker_e2e_append_update_check_suppression "$@"
if docker_e2e_resource_limits_disabled; then
return 0
fi
local memory="${OPENCLAW_DOCKER_E2E_MEMORY:-8g}"
local cpus="${OPENCLAW_DOCKER_E2E_CPUS:-16}"
local pids_limit="${OPENCLAW_DOCKER_E2E_PIDS_LIMIT:-2048}"
cpus="$(docker_e2e_resolve_cpus "$cpus")"
if ! docker_e2e_resource_value_disabled "$memory" && ! docker_e2e_run_arg_present --memory "$@"; then
DOCKER_E2E_RUN_RESOURCE_ARGS+=(--memory "$memory")
fi
if ! docker_e2e_resource_value_disabled "$cpus" && ! docker_e2e_run_arg_present --cpus "$@"; then
DOCKER_E2E_RUN_RESOURCE_ARGS+=(--cpus "$cpus")
fi
if ! docker_e2e_resource_value_disabled "$pids_limit" && ! docker_e2e_run_arg_present --pids-limit "$@"; then
pids_limit="$(docker_e2e_resolve_pids_limit "$pids_limit")" || return $?
DOCKER_E2E_RUN_RESOURCE_ARGS+=(--pids-limit "$pids_limit")
fi
}
docker_e2e_run_env_present() {
local name="$1"
shift
local arg
local expect_value=0
for arg in "$@"; do
if [ "$expect_value" = 1 ]; then
expect_value=0
case "$arg" in
"$name" | "$name"=*)
return 0
;;
esac
continue
fi
case "$arg" in
-e | --env)
expect_value=1
;;
-e"$name" | -e"$name"=* | --env="$name" | --env="$name"=*)
return 0
;;
esac
done
return 1
}
# CI containers are not installations. The runner's own CI variable does not
# cross into `docker run`, so without this every E2E container reports a daily
# update check and drowns real operators in the telemetry aggregates. Callers
# that exercise update behavior pass their own value and keep it.
docker_e2e_append_update_check_suppression() {
if docker_e2e_run_env_present OPENCLAW_NO_AUTO_UPDATE "$@"; then
return 0
fi
DOCKER_E2E_RUN_RESOURCE_ARGS+=(-e OPENCLAW_NO_AUTO_UPDATE=1)
}
docker_e2e_container_running() {
local container_name="$1"
[ "$(docker_e2e_docker_cmd inspect -f '{{.State.Running}}' "$container_name" 2>/dev/null || echo false)" = "true" ]
}
docker_e2e_container_exec_bash() {
local container_name="$1"
shift
docker_e2e_docker_cmd exec "$container_name" bash -lc "$*"
}
docker_e2e_wait_for_proof() {
local container_name="$1"
local attempts="$2"
for _ in $(seq 1 "$attempts"); do
if docker exec "$container_name" test -f /tmp/openclaw-proof-ready; then
return 0
fi
if [ "$(docker inspect --format '{{.State.Running}}' "$container_name")" != "true" ]; then
docker logs "$container_name" >&2
return 1
fi
sleep 1
done
docker logs "$container_name" >&2
return 1
}
docker_e2e_wait_container_bash() {
local container_name="$1"
shift
docker_e2e_wait_container_bash_while_running "$container_name" "$container_name" "$@"
}
docker_e2e_wait_container_bash_while_running() {
local running_container_name="$1"
local exec_container_name="$2"
local attempts="$3"
local sleep_seconds="$4"
shift 4
local probe="$*"
for _ in $(seq 1 "$attempts"); do
if ! docker_e2e_container_running "$running_container_name"; then
return 1
fi
if docker_e2e_container_exec_bash "$exec_container_name" "$probe" >/dev/null 2>&1; then
return 0
fi
sleep "$sleep_seconds"
done
return 1
}
docker_e2e_tail_container_file_if_running() {
local container_name="$1"
local file_path="$2"
local lines="${3:-120}"
if docker_e2e_container_running "$container_name"; then
docker_e2e_container_exec_bash "$container_name" "tail -n $lines $file_path" || true
else
docker_e2e_docker_cmd logs "$container_name" 2>&1 | tail -n "$lines" || true
fi
}