mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-09 16:00:54 +00:00
polish: announce every wait and auto-recover a misconfigured registry
Two related papercuts: the user could neither tell when a long-running step had started, nor recover from a registry container created (in some past session) without `-p 127.0.0.1:5000:5000`. build.sh - Rewrite ensure_registry as a real state machine. Running-but- unresponsive triggers a remove-and-recreate after dumping diagnostics; stopped/etc start, then recreate if start didn't help. The "missing" path stays the same. Confirmed against a deliberately mis-bound container (registry on 5000/tcp only, no host port): script detects, recreates, push succeeds — no manual `docker rm -f` needed. - Single poll_registry helper so the cheap-once-then-poll pattern lives in one place, with an explicit "==> Waiting for registry on ..." banner the first time we don't get an immediate response. setup.rs (verify path) - Print before each phase: "Booting throwaway sandbox", "Running claude/opencode/codex --version inside the sandbox", "Stopping verify sandbox". The verify step used to disappear for ~3s after the "Verifying" banner with no indication of progress. run.rs (launcher) - Print "Booting sandbox from <image> (<mem> MiB, <cpus> vCPU; first run pulls layers, otherwise ~3s)" before create(), "Attaching to <cmd>" or "Running <cmd> in sandbox (no TTY; output appears after exit)" before the attach/exec branch, and "Stopping sandbox" before stop_and_wait().
This commit is contained in:
parent
bb3e7d0a18
commit
a57ed6d6ac
3 changed files with 86 additions and 42 deletions
|
|
@ -105,6 +105,9 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
"/root/.local/bin:/root/.claude/local/bin:/root/.opencode/bin:/usr/local/bin:/usr/bin:/bin",
|
||||
);
|
||||
|
||||
eprintln!(
|
||||
"==> Booting sandbox from {image} ({memory_mib} MiB, {cpus} vCPU; first run pulls layers, otherwise ~3s)"
|
||||
);
|
||||
let sandbox = builder
|
||||
.create()
|
||||
.await
|
||||
|
|
@ -113,6 +116,7 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
let cmd = agent.command();
|
||||
let agent_args = args.agent_args;
|
||||
let exit = if std::io::stdin().is_terminal() {
|
||||
eprintln!("==> Attaching to {cmd} (Ctrl-P Ctrl-Q to detach)");
|
||||
sandbox
|
||||
.attach(cmd, agent_args)
|
||||
.await
|
||||
|
|
@ -121,6 +125,7 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
// No host TTY (piped, redirected, smoke-tested under `sg`/`sudo` etc.).
|
||||
// attach() needs a real /dev/tty for raw-mode stdin, so fall back to
|
||||
// collected exec. Output is forwarded once the command exits.
|
||||
eprintln!("==> Running {cmd} in sandbox (no TTY; output appears after exit)");
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
let output = sandbox
|
||||
.exec_with(cmd, |e| e.args(agent_args).cwd("/workspace"))
|
||||
|
|
@ -135,6 +140,7 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
output.status().code
|
||||
};
|
||||
|
||||
eprintln!("==> Stopping sandbox");
|
||||
sandbox.stop_and_wait().await.ok();
|
||||
Sandbox::remove(&session.sandbox_name).await.ok();
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ fn build_script_path() -> Result<PathBuf> {
|
|||
|
||||
async fn verify_image(image: &str) -> Result<()> {
|
||||
println!("==> Verifying {image}");
|
||||
println!("==> Booting throwaway sandbox (this is the first VM cold-start; ~3s on a warm host)");
|
||||
let sandbox = Sandbox::builder("agent-vm-setup-verify")
|
||||
.image(image)
|
||||
.registry(|r| r.insecure())
|
||||
|
|
@ -76,6 +77,7 @@ async fn verify_image(image: &str) -> Result<()> {
|
|||
.await
|
||||
.context("booting verify sandbox")?;
|
||||
|
||||
println!("==> Running claude/opencode/codex --version inside the sandbox");
|
||||
let out = sandbox
|
||||
.shell("claude --version && opencode --version && codex --version")
|
||||
.await
|
||||
|
|
@ -83,6 +85,7 @@ async fn verify_image(image: &str) -> Result<()> {
|
|||
|
||||
println!("{}", out.stdout()?.trim_end());
|
||||
|
||||
println!("==> Stopping verify sandbox");
|
||||
sandbox.stop_and_wait().await.ok();
|
||||
Sandbox::remove("agent-vm-setup-verify").await.ok();
|
||||
|
||||
|
|
|
|||
119
images/build.sh
119
images/build.sh
|
|
@ -4,7 +4,8 @@
|
|||
# microsandbox pulls images from registries by reference, so we run a tiny
|
||||
# registry:2 container bound to 127.0.0.1:5000 and treat it as our local
|
||||
# image store. The registry is shared across `agent-vm setup` runs; we
|
||||
# create it on demand and never tear it down.
|
||||
# create it on demand and recover by hand if a prior session left it in a
|
||||
# bad state (running but no port published, crashed inside, etc.).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -14,46 +15,19 @@ IMAGE_TAG="${AGENT_VM_IMAGE_TAG:-localhost:${REGISTRY_PORT}/agent-vm:latest}"
|
|||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
ensure_registry() {
|
||||
local state
|
||||
state=$(docker inspect --type container -f '{{.State.Status}}' "${REGISTRY_NAME}" 2>/dev/null || echo missing)
|
||||
case "${state}" in
|
||||
running)
|
||||
;;
|
||||
missing)
|
||||
echo "==> Creating local registry ${REGISTRY_NAME} on 127.0.0.1:${REGISTRY_PORT}"
|
||||
docker run -d \
|
||||
--name "${REGISTRY_NAME}" \
|
||||
--restart=always \
|
||||
-p "127.0.0.1:${REGISTRY_PORT}:5000" \
|
||||
registry:2 >/dev/null
|
||||
;;
|
||||
*)
|
||||
# exited, created, paused, restarting, dead — all need a kick.
|
||||
echo "==> Starting existing registry container ${REGISTRY_NAME} (was: ${state})"
|
||||
docker start "${REGISTRY_NAME}" >/dev/null
|
||||
;;
|
||||
esac
|
||||
wait_for_registry
|
||||
}
|
||||
|
||||
# `docker start` returns when the container has launched, but registry:2's
|
||||
# HTTP listener takes another ~100ms to bind. A push fired immediately after
|
||||
# loses that race with ECONNREFUSED, so poll until the v2 API answers.
|
||||
#
|
||||
# On failure, dump the container's port bindings and recent logs so the
|
||||
# user can tell whether the registry process crashed, was bound to the
|
||||
# wrong port, or never started — instead of just a bare timeout.
|
||||
wait_for_registry() {
|
||||
local i
|
||||
for i in $(seq 1 50); do
|
||||
if curl -fsS "http://127.0.0.1:${REGISTRY_PORT}/v2/" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
# Returns 0 if /v2/ on the registry port answers within the timeout.
|
||||
# Quiet — caller decides whether to log.
|
||||
poll_registry() {
|
||||
local attempts="${1:-50}" i
|
||||
for ((i = 0; i < attempts; i++)); do
|
||||
curl -fsS "http://127.0.0.1:${REGISTRY_PORT}/v2/" >/dev/null 2>&1 && return 0
|
||||
sleep 0.2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
dump_registry_diagnostics() {
|
||||
{
|
||||
echo "Registry did not become reachable on 127.0.0.1:${REGISTRY_PORT} after 10s."
|
||||
echo "Container state:"
|
||||
docker ps -a --filter "name=${REGISTRY_NAME}" \
|
||||
--format ' status={{.Status}} ports={{.Ports}}' 2>/dev/null || true
|
||||
|
|
@ -62,11 +36,72 @@ wait_for_registry() {
|
|||
--format ' {{json .NetworkSettings.Ports}}' 2>/dev/null || true
|
||||
echo "Last 30 lines of container logs:"
|
||||
docker logs --tail 30 "${REGISTRY_NAME}" 2>&1 | sed 's/^/ /' || true
|
||||
echo
|
||||
echo "If the port mapping is wrong (a stale container from a prior session),"
|
||||
echo "delete it and rerun setup:"
|
||||
echo " docker rm -f ${REGISTRY_NAME} && agent-vm setup"
|
||||
} >&2
|
||||
}
|
||||
|
||||
create_registry() {
|
||||
echo "==> Creating local registry ${REGISTRY_NAME} on 127.0.0.1:${REGISTRY_PORT}"
|
||||
docker run -d \
|
||||
--name "${REGISTRY_NAME}" \
|
||||
--restart=always \
|
||||
-p "127.0.0.1:${REGISTRY_PORT}:5000" \
|
||||
registry:2 >/dev/null
|
||||
}
|
||||
|
||||
recreate_registry() {
|
||||
echo "==> Removing stale ${REGISTRY_NAME} container"
|
||||
dump_registry_diagnostics
|
||||
docker rm -f "${REGISTRY_NAME}" >/dev/null 2>&1 || true
|
||||
create_registry
|
||||
}
|
||||
|
||||
# Idempotent: bring the registry container into a "running and answering on
|
||||
# 127.0.0.1:${REGISTRY_PORT}" state, no matter how it was left.
|
||||
#
|
||||
# Cases:
|
||||
# - missing → create.
|
||||
# - running, healthy → nothing.
|
||||
# - running, unhealthy → recreate (was probably started in a past session
|
||||
# without the right `-p` mapping, or the registry process crashed).
|
||||
# - stopped/etc → start; recreate if still unhealthy after start.
|
||||
ensure_registry() {
|
||||
local state
|
||||
state=$(docker inspect --type container -f '{{.State.Status}}' "${REGISTRY_NAME}" 2>/dev/null || echo missing)
|
||||
|
||||
case "${state}" in
|
||||
running)
|
||||
if poll_registry 5; then
|
||||
return 0
|
||||
fi
|
||||
echo "==> ${REGISTRY_NAME} is running but 127.0.0.1:${REGISTRY_PORT} is unresponsive"
|
||||
recreate_registry
|
||||
;;
|
||||
missing)
|
||||
create_registry
|
||||
;;
|
||||
*)
|
||||
echo "==> Starting existing registry container ${REGISTRY_NAME} (was: ${state})"
|
||||
docker start "${REGISTRY_NAME}" >/dev/null
|
||||
# registry:2 takes ~100ms after start to bind 5000 — short poll
|
||||
# is normal. If the container was misconfigured (no port mapping)
|
||||
# this longer poll will time out, and we recreate from scratch.
|
||||
if poll_registry 25; then
|
||||
return 0
|
||||
fi
|
||||
echo "==> ${REGISTRY_NAME} did not become reachable after restart"
|
||||
recreate_registry
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "==> Waiting for registry on 127.0.0.1:${REGISTRY_PORT} to accept connections"
|
||||
if poll_registry 50; then
|
||||
return 0
|
||||
fi
|
||||
{
|
||||
echo "Registry did not become reachable on 127.0.0.1:${REGISTRY_PORT} after 10s."
|
||||
echo "This is past our auto-recovery — Docker itself is probably misbehaving."
|
||||
} >&2
|
||||
dump_registry_diagnostics
|
||||
return 1
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue