From fa4b171a436559787cfcebd4046a1354a1f5cacf Mon Sep 17 00:00:00 2001 From: Bruce Ashfield Date: Tue, 17 Feb 2026 13:27:23 +0000 Subject: vxn: add per-container DomU lifecycle and memres persistent DomU Per-container DomU lifecycle: - run -d: per-container DomU with daemon loop and PTY-based IPC - ps: show Running vs Exited(code) via ===STATUS=== PTY query - exec/stop/rm: send commands to per-container DomU - logs: retrieve entrypoint output from running DomU - Entrypoint death detection with configurable grace period - Graceful error messages for ~25 unsupported commands - Command quoting fix: word-count+cut preserves internal spaces Memres (persistent DomU for fast container dispatch): - vxn memres start/stop/status/list for persistent DomU management - vxn run auto-dispatches to memres via xl block-attach + RUN_CONTAINER - Guest daemon loop handles ===RUN_CONTAINER===: mount hot-plugged xvdb, extract OCI rootfs, chroot exec entrypoint, unmount, report - Falls back to ephemeral mode when memres is occupied (PING timeout) - Xen-specific memres list shows xl domains and orphan detection Tested: vxn memres start + vxn run --rm alpine echo hello + vxn run --rm hello-world both produce correct output. Signed-off-by: Bruce Ashfield --- .../vcontainer/files/vcontainer-common.sh | 433 ++++++++++++++++++--- .../vcontainer/files/vrunner-backend-xen.sh | 301 ++++++++++++-- recipes-containers/vcontainer/files/vrunner.sh | 80 +++- recipes-containers/vcontainer/files/vxn-init.sh | 262 +++++++++++-- 4 files changed, 922 insertions(+), 154 deletions(-) (limited to 'recipes-containers/vcontainer') diff --git a/recipes-containers/vcontainer/files/vcontainer-common.sh b/recipes-containers/vcontainer/files/vcontainer-common.sh index cb48e1bb..7f3b8945 100755 --- a/recipes-containers/vcontainer/files/vcontainer-common.sh +++ b/recipes-containers/vcontainer/files/vcontainer-common.sh @@ -758,6 +758,9 @@ build_runner_args() { [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER") [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS") + # Xen: pass exit grace period + [ -n "${VXN_EXIT_GRACE_PERIOD:-}" ] && args+=("--exit-grace-period" "$VXN_EXIT_GRACE_PERIOD") + echo "${args[@]}" } @@ -1386,6 +1389,78 @@ parse_and_prepare_volumes() { done } +# ============================================================================ +# VXN Container State Helpers (Xen per-container DomU) +# ============================================================================ + +# VXN container state directory +vxn_container_dir() { echo "$HOME/.vxn/containers/$1"; } + +vxn_container_is_running() { + local cdir="$(vxn_container_dir "$1")" + [ -f "$cdir/daemon.domname" ] || return 1 + local domname=$(cat "$cdir/daemon.domname") + xl list "$domname" >/dev/null 2>&1 +} + +# Query entrypoint status from a running vxn container. +# Returns: "Running", "Exited ()", or "Unknown" +vxn_container_status() { + local name="$1" + local cdir="$(vxn_container_dir "$name")" + + # DomU not alive at all + if ! vxn_container_is_running "$name"; then + echo "Exited" + return + fi + + # Query the guest via PTY for entrypoint status + local pty_file="$cdir/daemon.pty" + if [ -f "$pty_file" ]; then + local pty + pty=$(cat "$pty_file") + if [ -c "$pty" ]; then + # Open PTY, send STATUS query, read response + local status_line="" + exec 4<>"$pty" + # Drain pending output + while IFS= read -t 0.3 -r _discard <&4; do :; done + echo "===STATUS===" >&4 + while IFS= read -t 3 -r status_line <&4; do + status_line=$(echo "$status_line" | tr -d '\r') + case "$status_line" in + *"===RUNNING==="*) + exec 4<&- 4>&- 2>/dev/null + echo "Running" + return + ;; + *"===EXITED="*"==="*) + local code=$(echo "$status_line" | sed 's/.*===EXITED=\([0-9]*\)===/\1/') + exec 4<&- 4>&- 2>/dev/null + echo "Exited ($code)" + return + ;; + esac + done + exec 4<&- 4>&- 2>/dev/null + fi + fi + + # Could not determine — DomU is alive but status query failed + echo "Running" +} + +# Xen: error helper for unsupported commands +vxn_unsupported() { + echo "${VCONTAINER_RUNTIME_NAME}: '$1' is not supported (VM is the container, no runtime inside)" >&2 + exit 1 +} +vxn_not_yet() { + echo "${VCONTAINER_RUNTIME_NAME}: '$1' is not yet supported" >&2 + exit 1 +} + # Handle commands case "$COMMAND" in image) @@ -1394,6 +1469,7 @@ case "$COMMAND" in # docker image rm → docker rmi # docker image pull → docker pull # etc. + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "image" if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} image requires a subcommand (ls, rm, pull, inspect, tag, push, prune)" >&2 exit 1 @@ -1448,11 +1524,13 @@ case "$COMMAND" in ;; images) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "images" # runtime images run_runtime_command "$VCONTAINER_RUNTIME_CMD images ${COMMAND_ARGS[*]}" ;; pull) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "pull" # runtime pull # Daemon mode already has networking enabled, so this works via daemon if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then @@ -1474,6 +1552,7 @@ case "$COMMAND" in ;; load) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "load" # runtime load -i # Parse -i argument INPUT_FILE="" @@ -1508,6 +1587,7 @@ case "$COMMAND" in ;; import) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "import" # runtime import [name:tag] - matches Docker/Podman's import exactly # Only accepts tarballs (rootfs archives), not OCI directories if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then @@ -1536,6 +1616,7 @@ case "$COMMAND" in ;; vimport) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "vimport" # Extended import: handles OCI directories, tarballs, and plain directories # Auto-detects format if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then @@ -1629,6 +1710,7 @@ case "$COMMAND" in ;; save) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "save" # runtime save -o OUTPUT_FILE="" IMAGE_NAME="" @@ -1685,12 +1767,27 @@ case "$COMMAND" in ;; tag|rmi) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "$COMMAND" # Commands that work with existing images run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}" ;; # Container lifecycle commands ps) + # Xen: list per-container DomUs + if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then + printf "%-15s %-25s %-15s %-20s\n" "NAME" "IMAGE" "STATUS" "STARTED" + for cdir in "$HOME/.vxn/containers"/*/; do + [ -d "$cdir" ] || continue + name=$(basename "$cdir") + ctr_image=$(grep '^IMAGE=' "$cdir/container.meta" 2>/dev/null | cut -d= -f2-) + ctr_started=$(grep '^STARTED=' "$cdir/container.meta" 2>/dev/null | cut -d= -f2-) + status=$(vxn_container_status "$name") + printf "%-15s %-25s %-15s %-20s\n" "$name" "${ctr_image:-unknown}" "$status" "${ctr_started:-unknown}" + done + exit 0 + fi + # List containers and show port forwards if daemon is running run_runtime_command "$VCONTAINER_RUNTIME_CMD ps ${COMMAND_ARGS[*]}" PS_EXIT=$? @@ -1719,6 +1816,24 @@ case "$COMMAND" in ;; rm) + # Xen: remove per-container DomU state + if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then + for arg in "${COMMAND_ARGS[@]}"; do + case "$arg" in -*) continue ;; esac + cdir="$(vxn_container_dir "$arg")" + if [ -d "$cdir" ]; then + # Stop if still running + if vxn_container_is_running "$arg"; then + RUNNER_ARGS=$(build_runner_args) + "$RUNNER" $RUNNER_ARGS --daemon-socket-dir "$cdir" --state-dir "$cdir" --daemon-stop 2>/dev/null + fi + rm -rf "$cdir" + echo "$arg" + fi + done + exit 0 + fi + # Remove containers and cleanup any registered port forwards for arg in "${COMMAND_ARGS[@]}"; do # Skip flags like -f, --force, etc. @@ -1734,21 +1849,51 @@ case "$COMMAND" in ;; logs) + # Xen: retrieve entrypoint log from DomU + if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then + if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then + echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} logs requires " >&2 + exit 1 + fi + cname="${COMMAND_ARGS[0]}" + cdir="$(vxn_container_dir "$cname")" + if [ -d "$cdir" ] && vxn_container_is_running "$cname"; then + RUNNER_ARGS=$(build_runner_args) + "$RUNNER" $RUNNER_ARGS --daemon-socket-dir "$cdir" --state-dir "$cdir" --daemon-send -- "cat /tmp/entrypoint.log 2>/dev/null" + exit $? + fi + echo "Container $cname not running" >&2 + exit 1 + fi + # View container logs run_runtime_command "$VCONTAINER_RUNTIME_CMD logs ${COMMAND_ARGS[*]}" ;; inspect) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "inspect" # Inspect container or image run_runtime_command "$VCONTAINER_RUNTIME_CMD inspect ${COMMAND_ARGS[*]}" ;; start|restart|kill|pause|unpause) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "$COMMAND" # Container state commands (no special handling needed) run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}" ;; stop) + # Xen: stop per-container DomU + if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && [ -n "${COMMAND_ARGS[0]:-}" ]; then + cname="${COMMAND_ARGS[0]}" + cdir="$(vxn_container_dir "$cname")" + if [ -d "$cdir" ]; then + RUNNER_ARGS=$(build_runner_args) + "$RUNNER" $RUNNER_ARGS --daemon-socket-dir "$cdir" --state-dir "$cdir" --daemon-stop + exit $? + fi + fi + # Stop container and cleanup any registered port forwards if [ ${#COMMAND_ARGS[@]} -ge 1 ]; then STOP_CONTAINER_NAME="${COMMAND_ARGS[0]}" @@ -1762,33 +1907,39 @@ case "$COMMAND" in # Image commands commit) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "commit" # Commit container to image run_runtime_command "$VCONTAINER_RUNTIME_CMD commit ${COMMAND_ARGS[*]}" ;; history) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "history" # Show image history run_runtime_command "$VCONTAINER_RUNTIME_CMD history ${COMMAND_ARGS[*]}" ;; # Registry commands push) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "push" # Push image to registry run_runtime_command "$VCONTAINER_RUNTIME_CMD push ${COMMAND_ARGS[*]}" ;; search) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "search" # Search registries run_runtime_command "$VCONTAINER_RUNTIME_CMD search ${COMMAND_ARGS[*]}" ;; login) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "login" # Login to registry - may need credentials via stdin # For non-interactive: runtime login -u user -p pass registry run_runtime_command "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}" ;; logout) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "logout" # Logout from registry run_runtime_command "$VCONTAINER_RUNTIME_CMD logout ${COMMAND_ARGS[*]}" ;; @@ -1800,6 +1951,21 @@ case "$COMMAND" in exit 1 fi + # Xen: exec in per-container DomU + if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then + cname="${COMMAND_ARGS[0]}" + cdir="$(vxn_container_dir "$cname")" + if [ -d "$cdir" ] && vxn_container_is_running "$cname"; then + shift_args=("${COMMAND_ARGS[@]:1}") + exec_cmd="${shift_args[*]}" + RUNNER_ARGS=$(build_runner_args) + "$RUNNER" $RUNNER_ARGS --daemon-socket-dir "$cdir" --state-dir "$cdir" --daemon-send -- "$exec_cmd" + exit $? + fi + echo "Container $cname not running" >&2 + exit 1 + fi + # Check for interactive flags EXEC_INTERACTIVE=false EXEC_ARGS=() @@ -1839,6 +2005,7 @@ case "$COMMAND" in # VM shell - interactive shell into the VM itself (not a container) vshell) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "vshell" # Opens a shell directly in the vdkr/vpdmn VM for debugging # This runs /bin/sh in the VM, not inside a container # Useful for: @@ -1858,6 +2025,7 @@ case "$COMMAND" in # Runtime cp - copy files to/from container cp) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "cp" if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} cp requires " >&2 echo "Usage: $VCONTAINER_RUNTIME_NAME cp : " >&2 @@ -2025,14 +2193,17 @@ case "$COMMAND" in ;; info) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "info" run_runtime_command "$VCONTAINER_RUNTIME_CMD info" ;; version) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "version" run_runtime_command "$VCONTAINER_RUNTIME_CMD version" ;; system) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "system" # Passthrough to runtime system commands (df, prune, events, etc.) if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} system requires a subcommand: df, prune, events, info" >&2 @@ -2181,6 +2352,7 @@ case "$COMMAND" in ;; vrun) + [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "vrun" # Extended run: run a command in a container (runtime-like syntax) # Usage: vrun [options] [command] [args...] # Options: @@ -2389,6 +2561,13 @@ case "$COMMAND" in exit 1 fi + # vxn (Xen): ephemeral mode by default. + # Detached mode (-d) uses per-container DomU with daemon loop instead. + if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then + # Detached flag is parsed below; defer NO_DAEMON until after flag parsing + : + fi + # Check if any volume mounts, network, port forwards, or detach are present RUN_HAS_VOLUMES=false RUN_HAS_NETWORK=false @@ -2431,6 +2610,15 @@ case "$COMMAND" in i=$((i + 1)) done + # Xen: non-detached runs use ephemeral mode unless memres is running + if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && [ "$RUN_IS_DETACHED" != "true" ]; then + if daemon_is_running; then + NO_DAEMON=false + else + NO_DAEMON=true + fi + fi + # Volume mounts require daemon mode if [ "$RUN_HAS_VOLUMES" = "true" ] && ! daemon_is_running; then echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Volume mounts require daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2 @@ -2469,7 +2657,7 @@ case "$COMMAND" in if [ "$INTERACTIVE" = "true" ]; then # Interactive mode with volumes still needs to stop daemon (volumes use share dir) # Interactive mode without volumes can use daemon_interactive (faster) - if [ "$RUN_HAS_VOLUMES" = "false" ] && daemon_is_running; then + if [ "$NO_DAEMON" != "true" ] && [ "$RUN_HAS_VOLUMES" = "false" ] && daemon_is_running; then # Use daemon interactive mode - keeps daemon running [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon interactive mode" >&2 RUNNER_ARGS=$(build_runner_args) @@ -2505,6 +2693,66 @@ case "$COMMAND" in else # Non-interactive - use daemon mode when available + # Xen detached mode: per-container DomU with daemon loop + if [ "$RUN_IS_DETACHED" = "true" ] && [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then + # Generate name if not provided + [ -z "$RUN_CONTAINER_NAME" ] && RUN_CONTAINER_NAME="$(cat /proc/sys/kernel/random/uuid | cut -c1-8)" + + # Per-container state dir + VXN_CTR_DIR="$HOME/.vxn/containers/$RUN_CONTAINER_NAME" + mkdir -p "$VXN_CTR_DIR" + + # Build runner args with per-container state/socket dir + RUNNER_ARGS=$(build_runner_args) + RUNNER_ARGS="$RUNNER_ARGS --daemon-socket-dir $VXN_CTR_DIR --state-dir $VXN_CTR_DIR" + RUNNER_ARGS="$RUNNER_ARGS --container-name $RUN_CONTAINER_NAME" + + # Start per-container DomU (daemon mode + initial command) + "$RUNNER" $RUNNER_ARGS --daemon-start -- "$RUNTIME_CMD" + + if [ $? -eq 0 ]; then + # Save metadata — extract image name (last positional arg before any cmd) + local_image="" + local_found_image=false + local_skip_next=false + for arg in "${COMMAND_ARGS[@]}"; do + if [ "$local_skip_next" = "true" ]; then + local_skip_next=false + continue + fi + case "$arg" in + --rm|--detach|-d|-i|--interactive|-t|--tty|--privileged|-it) ;; + -p|--publish|-v|--volume|-e|--env|--name|--network|-w|--workdir|--entrypoint|-m|--memory|--cpus) + local_skip_next=true ;; + --publish=*|--volume=*|--env=*|--name=*|--network=*|--workdir=*|--entrypoint=*|--memory=*|--cpus=*) ;; + -*) ;; + *) + if [ "$local_found_image" = "false" ]; then + local_image="$arg" + local_found_image=true + fi + ;; + esac + done + echo "IMAGE=${local_image}" > "$VXN_CTR_DIR/container.meta" + echo "COMMAND=$RUNTIME_CMD" >> "$VXN_CTR_DIR/container.meta" + echo "STARTED=$(date -Iseconds)" >> "$VXN_CTR_DIR/container.meta" + echo "$RUN_CONTAINER_NAME" + else + rm -rf "$VXN_CTR_DIR" + exit 1 + fi + exit 0 + fi + + # Xen memres mode: dispatch container to persistent DomU + if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && [ "$NO_DAEMON" != "true" ] && daemon_is_running; then + [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using memres DomU" >&2 + RUNNER_ARGS=$(build_runner_args) + "$RUNNER" $RUNNER_ARGS --daemon-run -- "$RUNTIME_CMD" + exit $? + fi + # For detached containers with port forwards, add them dynamically via QMP if [ "$RUN_IS_DETACHED" = "true" ] && [ "$RUN_HAS_PORT_FORWARDS" = "true" ] && daemon_is_running; then # Generate container name if not provided (needed for port tracking) @@ -2676,71 +2924,144 @@ case "$COMMAND" in echo "" found=0 tracked_pids="" - for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do - [ -f "$pid_file" ] || continue - pid=$(cat "$pid_file" 2>/dev/null) - if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then - instance_dir=$(dirname "$pid_file") - instance_name=$(basename "$instance_dir") - echo " ${CYAN}$instance_name${NC}" - echo " PID: $pid" - echo " State: $instance_dir" - if [ -f "$instance_dir/qemu.log" ]; then - # Try to extract port forwards from qemu command line - ports=$(grep -o 'hostfwd=[^,]*' "$instance_dir/qemu.log" 2>/dev/null | sed 's/hostfwd=tcp:://g; s/-/:/' | tr '\n' ' ') - [ -n "$ports" ] && echo " Ports: $ports" + + # Xen: check for vxn domains via xl list + if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then + for domname_file in "$DEFAULT_STATE_DIR"/*/daemon.domname; do + [ -f "$domname_file" ] || continue + domname=$(cat "$domname_file" 2>/dev/null) + if [ -n "$domname" ] && xl list "$domname" >/dev/null 2>&1; then + instance_dir=$(dirname "$domname_file") + instance_name=$(basename "$instance_dir") + echo " ${CYAN}$instance_name${NC}" + echo " Domain: $domname" + echo " State: $instance_dir" + if [ -f "$instance_dir/daemon.pty" ]; then + echo " PTY: $(cat "$instance_dir/daemon.pty")" + fi + echo "" + found=$((found + 1)) fi - echo "" - found=$((found + 1)) - tracked_pids="$tracked_pids $pid" + done + + # Also check per-container DomUs + if [ -d "$HOME/.vxn/containers" ]; then + for meta_file in "$HOME/.vxn/containers"/*/container.meta; do + [ -f "$meta_file" ] || continue + ctr_dir=$(dirname "$meta_file") + ctr_name=$(basename "$ctr_dir") + if vxn_container_is_running "$ctr_name"; then + image=$(grep '^IMAGE=' "$meta_file" 2>/dev/null | cut -d= -f2) + started=$(grep '^STARTED=' "$meta_file" 2>/dev/null | cut -d= -f2) + echo " ${CYAN}$ctr_name${NC} (per-container DomU)" + echo " Image: ${image:-(unknown)}" + echo " Started: ${started:-(unknown)}" + echo " State: $ctr_dir" + echo "" + found=$((found + 1)) + fi + done fi - done - if [ $found -eq 0 ]; then - echo " (none)" - fi - # Check for zombie/orphan QEMU processes (vdkr or vpdmn) - echo "" - echo "Checking for orphan QEMU processes..." - zombies="" - for qemu_pid in $(pgrep -f "qemu-system.*runtime=(docker|podman)" 2>/dev/null || true); do - # Skip if this PID is already tracked - if echo "$tracked_pids" | grep -qw "$qemu_pid"; then - continue + if [ $found -eq 0 ]; then + echo " (none)" fi - # Also check other tool's state dirs - other_tracked=false - for vpid_file in "$OTHER_STATE_DIR"/*/daemon.pid; do - [ -f "$vpid_file" ] || continue - vpid=$(cat "$vpid_file" 2>/dev/null) - if [ "$vpid" = "$qemu_pid" ]; then - other_tracked=true - break + + # Check for orphan vxn domains + echo "" + echo "Checking for orphan Xen domains..." + orphans="" + for domname in $(xl list 2>/dev/null | awk '/^vxn-/{print $1}'); do + tracked=false + for df in "$DEFAULT_STATE_DIR"/*/daemon.domname "$HOME"/.vxn/containers/*/daemon.domname; do + [ -f "$df" ] || continue + if [ "$(cat "$df" 2>/dev/null)" = "$domname" ]; then + tracked=true + break + fi + done + if [ "$tracked" != "true" ]; then + orphans="$orphans $domname" fi done - if [ "$other_tracked" = "true" ]; then - continue + + if [ -n "$orphans" ]; then + echo -e "${YELLOW}Orphan vxn domains found:${NC}" + for odom in $orphans; do + echo " ${RED}$odom${NC}" + echo " Destroy with: xl destroy $odom" + done + else + echo " (no orphans found)" + fi + else + # QEMU: check PID files + for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do + [ -f "$pid_file" ] || continue + pid=$(cat "$pid_file" 2>/dev/null) + if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then + instance_dir=$(dirname "$pid_file") + instance_name=$(basename "$instance_dir") + echo " ${CYAN}$instance_name${NC}" + echo " PID: $pid" + echo " State: $instance_dir" + if [ -f "$instance_dir/qemu.log" ]; then + # Try to extract port forwards from qemu command line + ports=$(grep -o 'hostfwd=[^,]*' "$instance_dir/qemu.log" 2>/dev/null | sed 's/hostfwd=tcp:://g; s/-/:/' | tr '\n' ' ') + [ -n "$ports" ] && echo " Ports: $ports" + fi + echo "" + found=$((found + 1)) + tracked_pids="$tracked_pids $pid" + fi + done + if [ $found -eq 0 ]; then + echo " (none)" fi - zombies="$zombies $qemu_pid" - done - if [ -n "$zombies" ]; then + # Check for zombie/orphan QEMU processes (vdkr or vpdmn) echo "" - echo -e "${YELLOW}Orphan QEMU processes found:${NC}" - for zpid in $zombies; do - # Extract runtime from cmdline - cmdline=$(cat /proc/$zpid/cmdline 2>/dev/null | tr '\0' ' ') - runtime=$(echo "$cmdline" | grep -o 'runtime=[a-z]*' | cut -d= -f2) - state_dir=$(echo "$cmdline" | grep -o 'path=[^,]*daemon.sock' | sed 's|path=||; s|/daemon.sock||') - echo "" - echo " ${RED}PID $zpid${NC} (${runtime:-unknown})" - [ -n "$state_dir" ] && echo " State: $state_dir" - echo " Kill with: kill $zpid" + echo "Checking for orphan QEMU processes..." + zombies="" + for qemu_pid in $(pgrep -f "qemu-system.*runtime=(docker|podman)" 2>/dev/null || true); do + # Skip if this PID is already tracked + if echo "$tracked_pids" | grep -qw "$qemu_pid"; then + continue + fi + # Also check other tool's state dirs + other_tracked=false + for vpid_file in "$OTHER_STATE_DIR"/*/daemon.pid; do + [ -f "$vpid_file" ] || continue + vpid=$(cat "$vpid_file" 2>/dev/null) + if [ "$vpid" = "$qemu_pid" ]; then + other_tracked=true + break + fi + done + if [ "$other_tracked" = "true" ]; then + continue + fi + zombies="$zombies $qemu_pid" done - echo "" - echo -e "To kill all orphans: ${CYAN}kill$zombies${NC}" - else - echo " (no orphans found)" + + if [ -n "$zombies" ]; then + echo "" + echo -e "${YELLOW}Orphan QEMU processes found:${NC}" + for zpid in $zombies; do + # Extract runtime from cmdline + cmdline=$(cat /proc/$zpid/cmdline 2>/dev/null | tr '\0' ' ') + runtime=$(echo "$cmdline" | grep -o 'runtime=[a-z]*' | cut -d= -f2) + state_dir=$(echo "$cmdline" | grep -o 'path=[^,]*daemon.sock' | sed 's|path=||; s|/daemon.sock||') + echo "" + echo " ${RED}PID $zpid${NC} (${runtime:-unknown})" + [ -n "$state_dir" ] && echo " State: $state_dir" + echo " Kill with: kill $zpid" + done + echo "" + echo -e "To kill all orphans: ${CYAN}kill$zombies${NC}" + else + echo " (no orphans found)" + fi fi ;; clean-ports) diff --git a/recipes-containers/vcontainer/files/vrunner-backend-xen.sh b/recipes-containers/vcontainer/files/vrunner-backend-xen.sh index 9f423dc6..55e87bd1 100644 --- a/recipes-containers/vcontainer/files/vrunner-backend-xen.sh +++ b/recipes-containers/vcontainer/files/vrunner-backend-xen.sh @@ -13,8 +13,8 @@ # Key differences from QEMU backend: # - Block devices appear as /dev/xvd* instead of /dev/vd* # - Network uses bridge + iptables NAT instead of QEMU slirp -# - Console uses PV console (hvc0/hvc1) instead of virtio-serial -# - 9p file sharing uses trans=xen instead of trans=virtio +# - Console uses PV console (hvc0) with serial='pty' for PTY on Dom0 +# - Daemon IPC uses direct PTY I/O (no socat bridge needed) # - VM tracking uses domain name instead of PID # ============================================================================ @@ -43,8 +43,12 @@ hv_setup_arch() { ;; esac - # Xen domain name (unique per instance) - HV_DOMNAME="vxn-$$" + # Xen domain name: use container name if set, otherwise PID-based + if [ -n "${CONTAINER_NAME:-}" ]; then + HV_DOMNAME="vxn-${CONTAINER_NAME}" + else + HV_DOMNAME="vxn-$$" + fi HV_VM_PID="" # Xen domain config path (generated at runtime) @@ -97,6 +101,8 @@ hv_prepare_container() { fi # Parse image name and any trailing command from "docker run [opts] [cmd...]" + # Uses word counting + cut to extract the user command portion from the + # original string, preserving internal spaces (e.g., -c "echo hello && sleep 5"). local args args=$(echo "$DOCKER_CMD" | sed 's/^[a-z]* run //') @@ -104,16 +110,16 @@ hv_prepare_container() { local user_cmd="" local skip_next=false local found_image=false + local word_count=0 for arg in $args; do + if [ "$found_image" = "true" ]; then + break + fi + word_count=$((word_count + 1)) if [ "$skip_next" = "true" ]; then skip_next=false continue fi - if [ "$found_image" = "true" ]; then - # Everything after image name is the user command - user_cmd="$user_cmd $arg" - continue - fi case "$arg" in --rm|--detach|-d|-i|--interactive|-t|--tty|--privileged|-it) ;; @@ -130,7 +136,21 @@ hv_prepare_container() { ;; esac done - user_cmd=$(echo "$user_cmd" | sed 's/^ *//') + + # Extract user command from original string using cut (preserves internal spaces) + if [ "$found_image" = "true" ]; then + user_cmd=$(echo "$args" | cut -d' ' -f$((word_count + 1))-) + # If cut returns the whole string (no fields after image), clear it + [ "$user_cmd" = "$args" ] && [ "$word_count" -ge "$(echo "$args" | wc -w)" ] && user_cmd="" + fi + + # Strip /bin/sh -c wrapper from user command — the guest already wraps + # with /bin/sh -c in exec_in_container_background(), so passing it through + # would create nested shells with broken quoting. + case "$user_cmd" in + "/bin/sh -c "*) user_cmd="${user_cmd#/bin/sh -c }" ;; + "sh -c "*) user_cmd="${user_cmd#sh -c }" ;; + esac if [ -z "$image" ]; then log "DEBUG" "hv_prepare_container: no image found in DOCKER_CMD" @@ -245,22 +265,17 @@ hv_build_network_opts() { hv_build_9p_opts() { local share_dir="$1" local share_tag="$2" - # For Xen, 9p is configured in the domain config, not as a command-line option - # We accumulate these and include them in the config file - _XEN_9P+=("{ 'tag': '$share_tag', 'path': '$share_dir', 'security_model': 'none' }") - # Return empty string since Xen doesn't use command-line 9p options - echo "" + # Xen 9p (xen_9pfsd) is not reliable in all environments (e.g. nested + # QEMU→Xen). Keep the interface for future use but don't depend on it + # for daemon IPC — we use serial/PTY + socat instead. + _XEN_9P+=("'tag=$share_tag,path=$share_dir,security_model=none,type=xen_9pfsd'") } hv_build_daemon_opts() { HV_DAEMON_OPTS="" - # Xen uses PV console (hvc1) for daemon command channel - # The init scripts already have /dev/hvc1 as a fallback for the daemon port - # No extra config needed - hvc1 is automatically available in PV guests - # - # For the host-side socket, we'll use xl console with a pipe - # The daemon socket is handled differently for Xen: - # We create a socat bridge between the Xen console and a unix socket + # Xen daemon mode uses hvc0 with serial='pty' for bidirectional IPC. + # The PTY is created on Dom0 and bridged to a Unix socket via socat. + # This is the same approach runx used (serial_start). } hv_build_vm_cmd() { @@ -316,6 +331,8 @@ extra = "console=hvc0 quiet loglevel=0 init=/init vcontainer.blk=xvd vcontainer. disk = [ $disk_array ] vif = [ $vif_array ] +serial = 'pty' + on_poweroff = "destroy" on_reboot = "destroy" on_crash = "destroy" @@ -353,16 +370,36 @@ hv_start_vm_background() { # Create the domain xl create "$HV_XEN_CFG" >> "$log_file" 2>&1 - # For background monitoring, we need a PID-like concept - # Use the domain name as VM identifier - HV_VM_PID="$$" # Use our PID as a placeholder for compatibility + # Xen domains don't have a PID on Dom0 — xl manages them by name. + # For daemon mode, start a lightweight monitor process that stays alive + # while the domain exists. This gives vcontainer-common.sh a real PID + # to check in /proc/$pid for daemon_is_running(). + HV_VM_PID="$$" if [ "$DAEMON_MODE" = "start" ]; then - # Daemon mode: bridge xl console (hvc1) to the daemon unix socket - # xl console -n 1 connects to the second PV console (hvc1) - socat "UNIX-LISTEN:$DAEMON_SOCKET,fork" "EXEC:xl console -n 1 $HV_DOMNAME" & - _XEN_SOCAT_PID=$! - log "DEBUG" "Console-socket bridge started (PID: $_XEN_SOCAT_PID)" + # Daemon mode: get the domain's hvc0 PTY for direct I/O. + # serial='pty' in the xl config creates a PTY on Dom0. + # We read/write this PTY directly — no socat bridge needed. + local domid + domid=$(xl domid "$HV_DOMNAME" 2>/dev/null) + if [ -n "$domid" ]; then + _XEN_PTY=$(xenstore-read "/local/domain/$domid/console/tty" 2>/dev/null) + if [ -n "$_XEN_PTY" ]; then + log "DEBUG" "Domain $HV_DOMNAME (domid $domid) console PTY: $_XEN_PTY" + echo "$_XEN_PTY" > "$DAEMON_SOCKET_DIR/daemon.pty" + else + log "ERROR" "Could not read console PTY from xenstore for domid $domid" + fi + else + log "ERROR" "Could not get domid for $HV_DOMNAME" + fi + + # Monitor process: stays alive while domain exists. + # vcontainer-common.sh checks /proc/$pid → alive means daemon running. + # When domain dies (xl destroy, guest reboot), monitor exits. + local _domname="$HV_DOMNAME" + (while xl list "$_domname" >/dev/null 2>&1; do sleep 10; done) & + HV_VM_PID=$! else # Ephemeral mode: capture guest console (hvc0) to log file # so the monitoring loop in vrunner.sh can see output markers @@ -413,11 +450,6 @@ hv_destroy_vm() { if [ -n "${_XEN_CONSOLE_PID:-}" ]; then kill $_XEN_CONSOLE_PID 2>/dev/null || true fi - - # Clean up console bridge (daemon mode) - if [ -n "${_XEN_SOCAT_PID:-}" ]; then - kill $_XEN_SOCAT_PID 2>/dev/null || true - fi } hv_get_vm_id() { @@ -524,6 +556,181 @@ hv_daemon_is_running() { [ -n "$HV_DOMNAME" ] && xl list "$HV_DOMNAME" >/dev/null 2>&1 } +# PTY-based daemon readiness check. +# The guest emits ===PONG=== on hvc0 at daemon startup and in response to PING. +# We read the PTY (saved by hv_start_vm_background) looking for this marker. +hv_daemon_ping() { + local pty_file="$DAEMON_SOCKET_DIR/daemon.pty" + [ -f "$pty_file" ] || return 1 + local pty + pty=$(cat "$pty_file") + [ -c "$pty" ] || return 1 + + # Open PTY for read/write on fd 3 + exec 3<>"$pty" + + # Send PING (guest also emits PONG at startup) + echo "===PING===" >&3 + + # Read lines looking for PONG (skip boot messages, log lines) + local line + while IFS= read -t 5 -r line <&3; do + line=$(echo "$line" | tr -d '\r') + case "$line" in + *"===PONG==="*) exec 3<&- 3>&-; return 0 ;; + esac + done + + exec 3<&- 3>&- 2>/dev/null + return 1 +} + +# PTY-based daemon command send. +# Writes base64-encoded command to PTY, reads response with markers. +# Same protocol as socat-based daemon_send in vrunner.sh. +hv_daemon_send() { + local cmd="$1" + local pty_file="$DAEMON_SOCKET_DIR/daemon.pty" + [ -f "$pty_file" ] || { log "ERROR" "No daemon PTY file"; return 1; } + local pty + pty=$(cat "$pty_file") + [ -c "$pty" ] || { log "ERROR" "PTY $pty not a character device"; return 1; } + + # Update activity timestamp + touch "$DAEMON_SOCKET_DIR/activity" 2>/dev/null || true + + # Encode command + local cmd_b64 + cmd_b64=$(echo -n "$cmd" | base64 -w0) + + # Open PTY for read/write on fd 3 + exec 3<>"$pty" + + # Drain any pending output (boot messages, prior log lines) + while IFS= read -t 0.5 -r _discard <&3; do :; done + + # Send command + echo "$cmd_b64" >&3 + + # Read response with markers + local EXIT_CODE=0 + local in_output=false + local line + while IFS= read -t 60 -r line <&3; do + line=$(echo "$line" | tr -d '\r') + case "$line" in + *"===OUTPUT_START==="*) + in_output=true + ;; + *"===OUTPUT_END==="*) + in_output=false + ;; + *"===EXIT_CODE="*"==="*) + EXIT_CODE=$(echo "$line" | sed 's/.*===EXIT_CODE=\([0-9]*\)===/\1/') + ;; + *"===END==="*) + break + ;; + *) + if [ "$in_output" = "true" ]; then + echo "$line" + fi + ;; + esac + done + + exec 3<&- 3>&- 2>/dev/null + return ${EXIT_CODE:-0} +} + +# Run a container in a memres (persistent) DomU. +# Hot-plugs an input disk, sends ===RUN_CONTAINER=== command via PTY, +# reads output, detaches disk. +# Usage: hv_daemon_run_container +hv_daemon_run_container() { + local cmd="$1" + local input_disk="$2" + + hv_daemon_load_state + if [ -z "$HV_DOMNAME" ]; then + log "ERROR" "No memres domain name" + return 1 + fi + + # Hot-plug the input disk as xvdb (read-only) + if [ -n "$input_disk" ] && [ -f "$input_disk" ]; then + log "INFO" "Hot-plugging container disk to $HV_DOMNAME..." + xl block-attach "$HV_DOMNAME" "format=raw,vdev=xvdb,access=ro,target=$input_disk" 2>/dev/null || { + log "ERROR" "Failed to attach block device" + return 1 + } + sleep 1 # Let the kernel register the device + fi + + # Build the command line: ===RUN_CONTAINER=== + local raw_line="===RUN_CONTAINER===" + if [ -n "$cmd" ]; then + raw_line="${raw_line}$(echo -n "$cmd" | base64 -w0)" + fi + + # Send via PTY and read response (same protocol as hv_daemon_send) + local pty_file="$DAEMON_SOCKET_DIR/daemon.pty" + [ -f "$pty_file" ] || { log "ERROR" "No daemon PTY file"; return 1; } + local pty + pty=$(cat "$pty_file") + [ -c "$pty" ] || { log "ERROR" "PTY $pty not a character device"; return 1; } + + touch "$DAEMON_SOCKET_DIR/activity" 2>/dev/null || true + + exec 3<>"$pty" + + # Drain pending output + while IFS= read -t 0.5 -r _discard <&3; do :; done + + # Send command + echo "$raw_line" >&3 + + # Read response with markers + local EXIT_CODE=0 + local in_output=false + local line + while IFS= read -t 120 -r line <&3; do + line=$(echo "$line" | tr -d '\r') + case "$line" in + *"===OUTPUT_START==="*) + in_output=true + ;; + *"===OUTPUT_END==="*) + in_output=false + ;; + *"===EXIT_CODE="*"==="*) + EXIT_CODE=$(echo "$line" | sed 's/.*===EXIT_CODE=\([0-9]*\)===/\1/') + ;; + *"===ERROR==="*) + in_output=true + ;; + *"===END==="*) + break + ;; + *) + if [ "$in_output" = "true" ]; then + echo "$line" + fi + ;; + esac + done + + exec 3<&- 3>&- 2>/dev/null + + # Detach the input disk + if [ -n "$input_disk" ] && [ -f "$input_disk" ]; then + log "DEBUG" "Detaching container disk from $HV_DOMNAME..." + xl block-detach "$HV_DOMNAME" xvdb 2>/dev/null || true + fi + + return ${EXIT_CODE:-0} +} + hv_daemon_stop() { hv_daemon_load_state if [ -z "$HV_DOMNAME" ]; then @@ -532,10 +739,15 @@ hv_daemon_stop() { log "INFO" "Shutting down Xen domain $HV_DOMNAME..." - # Send shutdown command via socket first (graceful guest shutdown) - if [ -S "$DAEMON_SOCKET" ]; then - echo "===SHUTDOWN===" | socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true - sleep 2 + # Send shutdown command via PTY (graceful guest shutdown) + local pty_file="$DAEMON_SOCKET_DIR/daemon.pty" + if [ -f "$pty_file" ]; then + local pty + pty=$(cat "$pty_file") + if [ -c "$pty" ]; then + echo "===SHUTDOWN===" > "$pty" 2>/dev/null || true + sleep 2 + fi fi # Try graceful xl shutdown @@ -554,11 +766,14 @@ hv_daemon_stop() { xl destroy "$HV_DOMNAME" 2>/dev/null || true fi - # Clean up console bridge - if [ -n "${_XEN_SOCAT_PID:-}" ]; then - kill $_XEN_SOCAT_PID 2>/dev/null || true + # Kill monitor process (PID stored in daemon.pid) + local pid_file="$DAEMON_SOCKET_DIR/daemon.pid" + if [ -f "$pid_file" ]; then + local mpid + mpid=$(cat "$pid_file" 2>/dev/null) + [ -n "$mpid" ] && kill "$mpid" 2>/dev/null || true fi - rm -f "$(_xen_domname_file)" + rm -f "$(_xen_domname_file)" "$pty_file" "$pid_file" log "INFO" "Xen domain stopped" } diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh index 04f0e39e..aaaaeb61 100755 --- a/recipes-containers/vcontainer/files/vrunner.sh +++ b/recipes-containers/vcontainer/files/vrunner.sh @@ -323,6 +323,7 @@ BATCH_IMPORT="false" DAEMON_MODE="" # start, send, stop, status DAEMON_SOCKET_DIR="" # Directory for daemon socket/PID files IDLE_TIMEOUT="1800" # Default: 30 minutes +EXIT_GRACE_PERIOD="" # Entrypoint exit grace period (vxn) while [ $# -gt 0 ]; do case $1 in @@ -443,6 +444,10 @@ while [ $# -gt 0 ]; do DAEMON_MODE="interactive" shift ;; + --daemon-run) + DAEMON_MODE="run" + shift + ;; --daemon-stop) DAEMON_MODE="stop" shift @@ -459,11 +464,20 @@ while [ $# -gt 0 ]; do IDLE_TIMEOUT="$2" shift 2 ;; + --container-name) + CONTAINER_NAME="$2" + export CONTAINER_NAME + shift 2 + ;; --no-daemon) # Placeholder for CLI wrapper - vrunner.sh itself doesn't use this # but we accept it so callers can pass it through shift ;; + --exit-grace-period) + EXIT_GRACE_PERIOD="$2" + shift 2 + ;; --verbose|-v) VERBOSE="true" shift @@ -597,6 +611,12 @@ daemon_send() { exit 1 fi + # Use backend-specific send if available (e.g. Xen PTY-based IPC) + if type hv_daemon_send >/dev/null 2>&1; then + hv_daemon_send "$cmd" + return $? + fi + if [ ! -S "$DAEMON_SOCKET" ]; then log "ERROR" "Daemon socket not found: $DAEMON_SOCKET" exit 1 @@ -704,6 +724,14 @@ daemon_interactive() { return 1 fi + # PTY-based backends don't support interactive daemon mode + # (file-descriptor polling isn't practical for interactive I/O) + if type hv_daemon_send >/dev/null 2>&1; then + log "ERROR" "Interactive daemon mode not supported with ${VCONTAINER_HYPERVISOR} backend" + log "ERROR" "Use: ${TOOL_NAME} -it --no-daemon run ... for interactive mode" + return 1 + fi + if [ ! -S "$DAEMON_SOCKET" ]; then log "ERROR" "Daemon socket not found: $DAEMON_SOCKET" return 1 @@ -1032,6 +1060,25 @@ if [ -n "$INPUT_PATH" ] && [ "$INPUT_TYPE" != "none" ]; then log "DEBUG" "Input disk: $(ls -lh "$INPUT_IMG" | awk '{print $5}')" fi +# Daemon run mode: try to use memres DomU, fall back to ephemeral +# This runs after input disk creation so we have the container disk ready +if [ "$DAEMON_MODE" = "run" ]; then + if type hv_daemon_ping >/dev/null 2>&1 && hv_daemon_ping; then + # Memres DomU is responsive — use it + log "INFO" "Memres DomU is idle, dispatching container..." + INPUT_IMG_PATH="" + if [ -n "$DISK_OPTS" ]; then + INPUT_IMG_PATH=$(echo "$DISK_OPTS" | sed -n 's/.*file=\([^,]*\).*/\1/p') + fi + hv_daemon_run_container "$DOCKER_CMD" "$INPUT_IMG_PATH" + exit $? + else + # Memres DomU is occupied or not responding — fall through to ephemeral + log "INFO" "Memres occupied or not responding, using ephemeral mode" + DAEMON_MODE="" + fi +fi + # Create state disk for persistent storage (--state-dir) # Xen backend skips this: DomU Docker storage lives in the guest's overlay # filesystem and persists as long as the domain is running (daemon mode). @@ -1175,6 +1222,11 @@ if [ "$INTERACTIVE" = "true" ]; then KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_interactive=1" fi +# Exit grace period for entrypoint death detection (vxn) +if [ -n "${EXIT_GRACE_PERIOD:-}" ]; then + KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_exit_grace=$EXIT_GRACE_PERIOD" +fi + # Build VM configuration via hypervisor backend # Drive ordering is important: # rootfs.img (read-only), input disk (if any), state disk (if any) @@ -1187,15 +1239,15 @@ if [ "$BATCH_IMPORT" = "true" ]; then BATCH_SHARE_DIR="$TEMP_DIR/share" mkdir -p "$BATCH_SHARE_DIR" SHARE_TAG="${TOOL_NAME}_share" - HV_OPTS="$HV_OPTS $(hv_build_9p_opts "$BATCH_SHARE_DIR" "$SHARE_TAG")" + hv_build_9p_opts "$BATCH_SHARE_DIR" "$SHARE_TAG" KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_9p=1" log "INFO" "Using 9p for fast storage output" fi # Daemon mode: add serial channel for command I/O if [ "$DAEMON_MODE" = "start" ]; then - # Check for required tools - if ! command -v socat >/dev/null 2>&1; then + # Check for required tools (socat needed unless backend provides PTY-based IPC) + if ! type hv_daemon_send >/dev/null 2>&1 && ! command -v socat >/dev/null 2>&1; then log "ERROR" "Daemon mode requires 'socat' but it is not installed." log "ERROR" "Install with: sudo apt install socat" exit 1 @@ -1210,15 +1262,6 @@ if [ "$DAEMON_MODE" = "start" ]; then # Create socket directory mkdir -p "$DAEMON_SOCKET_DIR" - # Create shared directory for file I/O (9p) - DAEMON_SHARE_DIR="$DAEMON_SOCKET_DIR/share" - mkdir -p "$DAEMON_SHARE_DIR" - - # Add 9p for shared directory access - SHARE_TAG="${TOOL_NAME}_share" - HV_OPTS="$HV_OPTS $(hv_build_9p_opts "$DAEMON_SHARE_DIR" "$SHARE_TAG")" - KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_9p=1" - # Add daemon command channel (backend-specific: virtio-serial or PV console) hv_build_daemon_opts HV_OPTS="$HV_OPTS $HV_DAEMON_OPTS" @@ -1266,11 +1309,18 @@ if [ "$DAEMON_MODE" = "start" ]; then log "INFO" "VM started (PID: $HV_VM_PID)" - # Wait for socket to appear (container runtime starting) + # Wait for daemon to be ready (backend-specific or socket-based) log "INFO" "Waiting for daemon to be ready..." READY=false for i in $(seq 1 120); do - if [ -S "$DAEMON_SOCKET" ]; then + # Backend-specific readiness check (e.g. Xen PTY-based) + if type hv_daemon_ping >/dev/null 2>&1; then + if hv_daemon_ping; then + log "DEBUG" "Got PONG response (backend)" + READY=true + break + fi + elif [ -S "$DAEMON_SOCKET" ]; then RESPONSE=$( { echo "===PING==="; sleep 3; } | timeout 10 socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true) if echo "$RESPONSE" | grep -q "===PONG==="; then log "DEBUG" "Got PONG response" @@ -1356,7 +1406,7 @@ if [ -n "$CA_CERT" ] && [ -f "$CA_CERT" ]; then cp "$CA_CERT" "$CA_SHARE_DIR/ca.crt" SHARE_TAG="${TOOL_NAME}_share" - HV_OPTS="$HV_OPTS $(hv_build_9p_opts "$CA_SHARE_DIR" "$SHARE_TAG" "readonly=on")" + hv_build_9p_opts "$CA_SHARE_DIR" "$SHARE_TAG" "readonly=on" KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_9p=1" log "DEBUG" "CA certificate available via 9p" fi diff --git a/recipes-containers/vcontainer/files/vxn-init.sh b/recipes-containers/vcontainer/files/vxn-init.sh index 93e631e1..4b9b630a 100755 --- a/recipes-containers/vcontainer/files/vxn-init.sh +++ b/recipes-containers/vcontainer/files/vxn-init.sh @@ -22,9 +22,10 @@ # docker_output= Output type: text (default: text) # docker_network=1 Enable networking # docker_interactive=1 Interactive mode (suppress boot messages) -# docker_daemon=1 Daemon mode (command loop on hvc1) +# docker_daemon=1 Daemon mode (command loop on hvc0 via serial PTY) +# docker_exit_grace= Grace period (seconds) after entrypoint exits [default: 300] # -# Version: 1.0.0 +# Version: 1.2.0 # Set runtime-specific parameters before sourcing common code VCONTAINER_RUNTIME_NAME="vxn" @@ -32,7 +33,7 @@ VCONTAINER_RUNTIME_CMD="chroot" VCONTAINER_RUNTIME_PREFIX="docker" VCONTAINER_STATE_DIR="/var/lib/vxn" VCONTAINER_SHARE_NAME="vxn_share" -VCONTAINER_VERSION="1.0.0" +VCONTAINER_VERSION="1.2.0" # Source common init functions . /vcontainer-init-common.sh @@ -344,7 +345,7 @@ exec_in_container() { export TERM=linux printf '\r\033[K' if [ "$use_sh" = "true" ]; then - chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; exec $cmd" + chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; $cmd" else chroot "$rootfs" $cmd fi @@ -354,7 +355,7 @@ exec_in_container() { EXEC_OUTPUT="/tmp/container_output.txt" EXEC_EXIT_CODE=0 if [ "$use_sh" = "true" ]; then - chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; exec $cmd" \ + chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; $cmd" \ > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$? else chroot "$rootfs" $cmd \ @@ -375,38 +376,158 @@ exec_in_container() { umount "$rootfs/dev" 2>/dev/null || true } +# Execute a command inside the container rootfs in the background. +# Used by detached mode: start entrypoint, then enter daemon loop. +exec_in_container_background() { + local rootfs="$1" + local cmd="$2" + local workdir="${OCI_WORKDIR:-/}" + + # Mount essential filesystems inside the container rootfs + mkdir -p "$rootfs/proc" "$rootfs/sys" "$rootfs/dev" "$rootfs/tmp" 2>/dev/null || true + mount -t proc proc "$rootfs/proc" 2>/dev/null || true + mount -t sysfs sysfs "$rootfs/sys" 2>/dev/null || true + mount --bind /dev "$rootfs/dev" 2>/dev/null || true + + # Copy resolv.conf for DNS + if [ -f /etc/resolv.conf ]; then + mkdir -p "$rootfs/etc" 2>/dev/null || true + cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || true + fi + + # Run in background, save PID + # Note: no 'exec' — compound commands (&&, ||, ;) need the wrapper shell + chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; $cmd" > /tmp/entrypoint.log 2>&1 & + ENTRYPOINT_PID=$! + echo "$ENTRYPOINT_PID" > /tmp/entrypoint.pid + log "Entrypoint PID: $ENTRYPOINT_PID" +} + # ============================================================================ -# Daemon Mode (vxn-specific) +# Memres: Run Container from Hot-Plugged Disk # ============================================================================ -# In daemon mode, commands come via the hvc1 console channel -# and are executed in the container rootfs via chroot. -run_vxn_daemon_mode() { - log "=== vxn Daemon Mode ===" - log "Container rootfs: ${CONTAINER_ROOT:-(none)}" - log "Idle timeout: ${RUNTIME_IDLE_TIMEOUT}s" - - # Find the command channel (prefer hvc1 for Xen) - DAEMON_PORT="" - for port in /dev/hvc1 /dev/vport0p1 /dev/vport1p1 /dev/virtio-ports/vxn; do - if [ -c "$port" ]; then - DAEMON_PORT="$port" - log "Found command channel: $port" +# Run a container from a hot-plugged /dev/xvdb block device. +# Called by the daemon loop in response to ===RUN_CONTAINER=== command. +# Mounts the device, finds rootfs, executes entrypoint, unmounts. +# Output follows the daemon command protocol (===OUTPUT_START/END/EXIT_CODE/END===). +run_container_from_disk() { + local user_cmd="$1" + + # Wait for hot-plugged block device + log "Waiting for input device..." + local found=false + for i in $(seq 1 30); do + if [ -b /dev/xvdb ]; then + found=true break fi + sleep 0.5 done - if [ -z "$DAEMON_PORT" ]; then - log "ERROR: No command channel for daemon mode" - ls -la /dev/hvc* /dev/vport* /dev/virtio-ports/ 2>/dev/null || true - sleep 5 - reboot -f + if [ "$found" != "true" ]; then + echo "===ERROR===" + echo "Input device /dev/xvdb not found after 15s" + echo "===END===" + return fi - # Open bidirectional FD - exec 3<>"$DAEMON_PORT" + # Mount input disk + mkdir -p /mnt/input + if ! mount /dev/xvdb /mnt/input 2>/dev/null; then + echo "===ERROR===" + echo "Failed to mount /dev/xvdb" + echo "===END===" + return + fi + log "Input disk mounted" - log "Daemon ready, waiting for commands..." + # Find container rootfs + if ! find_container_rootfs; then + echo "===ERROR===" + echo "No container rootfs found on input disk" + echo "===END===" + umount /mnt/input 2>/dev/null || true + return + fi + + # Parse OCI config for entrypoint/env/workdir + parse_oci_config + setup_container_env + + # Determine command: user-supplied takes priority, then OCI config + if [ -n "$user_cmd" ]; then + RUNTIME_CMD="$user_cmd" + else + RUNTIME_CMD="" + fi + local exec_cmd + exec_cmd=$(determine_exec_command) + + log "Executing container: $exec_cmd" + + # Execute in container rootfs (blocking) + local rootfs="$CONTAINER_ROOT" + local workdir="${OCI_WORKDIR:-/}" + local exit_code=0 + + # Mount essential filesystems inside container + mkdir -p "$rootfs/proc" "$rootfs/sys" "$rootfs/dev" "$rootfs/tmp" 2>/dev/null || true + mount -t proc proc "$rootfs/proc" 2>/dev/null || true + mount -t sysfs sysfs "$rootfs/sys" 2>/dev/null || true + mount --bind /dev "$rootfs/dev" 2>/dev/null || true + [ -f /etc/resolv.conf ] && { + mkdir -p "$rootfs/etc" 2>/dev/null || true + cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || true + } + + local output_file="/tmp/container_run_output.txt" + if [ -x "$rootfs/bin/sh" ]; then + chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; $exec_cmd" \ + > "$output_file" 2>&1 || exit_code=$? + else + chroot "$rootfs" $exec_cmd \ + > "$output_file" 2>&1 || exit_code=$? + fi + + echo "===OUTPUT_START===" + cat "$output_file" + echo "===OUTPUT_END===" + echo "===EXIT_CODE=$exit_code===" + + # Clean up container mounts + umount "$rootfs/proc" 2>/dev/null || true + umount "$rootfs/sys" 2>/dev/null || true + umount "$rootfs/dev" 2>/dev/null || true + umount /mnt/input 2>/dev/null || true + rm -f "$output_file" + + # Reset container state for next run + CONTAINER_ROOT="" + OCI_ENTRYPOINT="" + OCI_CMD="" + OCI_ENV="" + OCI_WORKDIR="" + RUNTIME_CMD="" + # Clean up extracted OCI rootfs (if any) + rm -rf /mnt/container 2>/dev/null || true + + echo "===END===" + log "Container finished (exit code: $exit_code)" +} + +# ============================================================================ +# Daemon Mode (vxn-specific) +# ============================================================================ + +# Daemon mode: command loop on hvc0 (stdin/stdout). +# The host bridges the domain's console PTY to a Unix socket via socat. +# Commands arrive as base64-encoded lines on stdin, responses go to stdout. +# This is the same model runx used (serial='pty' + serial_start). +run_vxn_daemon_mode() { + log "=== vxn Daemon Mode ===" + log "Container rootfs: ${CONTAINER_ROOT:-(none)}" + log "Idle timeout: ${RUNTIME_IDLE_TIMEOUT}s" ACTIVITY_FILE="/tmp/.daemon_activity" touch "$ACTIVITY_FILE" @@ -415,10 +536,17 @@ run_vxn_daemon_mode() { trap 'log "Shutdown signal"; sync; reboot -f' TERM trap 'rm -f "$ACTIVITY_FILE"; exit' INT - # Command loop + log "Using hvc0 console for daemon IPC" + log "Daemon ready, waiting for commands..." + + # Emit readiness marker so the host can detect daemon is ready + # without needing to send PING first (host reads PTY for this) + echo "===PONG===" + + # Command loop: read from stdin (hvc0), write to stdout (hvc0) while true; do CMD_B64="" - read -r CMD_B64 <&3 + read -r CMD_B64 READ_EXIT=$? if [ $READ_EXIT -eq 0 ] && [ -n "$CMD_B64" ]; then @@ -426,20 +554,41 @@ run_vxn_daemon_mode() { case "$CMD_B64" in "===PING===") - echo "===PONG===" | cat >&3 + echo "===PONG===" + continue + ;; + "===STATUS===") + if [ -f /tmp/entrypoint.exit_code ]; then + echo "===EXITED=$(cat /tmp/entrypoint.exit_code)===" + else + echo "===RUNNING===" + fi continue ;; "===SHUTDOWN===") log "Received shutdown command" - echo "===SHUTTING_DOWN===" | cat >&3 + echo "===SHUTTING_DOWN===" break ;; + "===RUN_CONTAINER==="*) + # Memres: run a container from a hot-plugged disk + _rc_cmd_b64="${CMD_B64#===RUN_CONTAINER===}" + _rc_cmd="" + if [ -n "$_rc_cmd_b64" ]; then + _rc_cmd=$(echo "$_rc_cmd_b64" | base64 -d 2>/dev/null) + fi + log "RUN_CONTAINER: cmd='$_rc_cmd'" + run_container_from_disk "$_rc_cmd" + continue + ;; esac # Decode command CMD=$(echo "$CMD_B64" | base64 -d 2>/dev/null) if [ -z "$CMD" ]; then - printf "===ERROR===\nFailed to decode command\n===END===\n" | cat >&3 + echo "===ERROR===" + echo "Failed to decode command" + echo "===END===" continue fi @@ -455,13 +604,11 @@ run_vxn_daemon_mode() { eval "$CMD" > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$? fi - { - echo "===OUTPUT_START===" - cat "$EXEC_OUTPUT" - echo "===OUTPUT_END===" - echo "===EXIT_CODE=$EXEC_EXIT_CODE===" - echo "===END===" - } | cat >&3 + echo "===OUTPUT_START===" + cat "$EXEC_OUTPUT" + echo "===OUTPUT_END===" + echo "===EXIT_CODE=$EXEC_EXIT_CODE===" + echo "===END===" log "Command completed (exit code: $EXEC_EXIT_CODE)" else @@ -469,7 +616,6 @@ run_vxn_daemon_mode() { fi done - exec 3>&- log "Daemon shutting down..." } @@ -494,6 +640,15 @@ setup_cgroups # Parse kernel command line parse_cmdline +# Parse vxn-specific kernel parameters +ENTRYPOINT_GRACE_PERIOD="300" +for param in $(cat /proc/cmdline); do + case "$param" in + docker_exit_grace=*) ENTRYPOINT_GRACE_PERIOD="${param#docker_exit_grace=}" ;; + esac +done +log "Entrypoint grace period: ${ENTRYPOINT_GRACE_PERIOD}s" + # Detect and configure disks detect_disks @@ -525,6 +680,33 @@ parse_oci_config setup_container_env if [ "$RUNTIME_DAEMON" = "1" ]; then + # If we also have a command, run it in background first (detached container) + if [ -n "$RUNTIME_CMD" ] && [ "$RUNTIME_CMD" != "1" ]; then + EXEC_CMD=$(determine_exec_command) + if [ -n "$EXEC_CMD" ] && [ -n "$CONTAINER_ROOT" ]; then + log "Starting entrypoint in background: $EXEC_CMD" + exec_in_container_background "$CONTAINER_ROOT" "$EXEC_CMD" + + # Monitor entrypoint: when it exits, record exit code and + # schedule DomU shutdown after grace period. + # During the grace period, exec and logs still work. + if [ -n "$ENTRYPOINT_PID" ]; then + ( + wait $ENTRYPOINT_PID 2>/dev/null + EP_EXIT=$? + echo "$EP_EXIT" > /tmp/entrypoint.exit_code + log "Entrypoint exited (code: $EP_EXIT), grace period: ${ENTRYPOINT_GRACE_PERIOD}s" + if [ "$ENTRYPOINT_GRACE_PERIOD" -gt 0 ] 2>/dev/null; then + sleep "$ENTRYPOINT_GRACE_PERIOD" + fi + log "Grace period expired, shutting down" + reboot -f + ) & + ENTRYPOINT_MONITOR_PID=$! + log "Entrypoint monitor started (PID: $ENTRYPOINT_MONITOR_PID)" + fi + fi + fi run_vxn_daemon_mode else # Determine command to execute -- cgit v1.2.3-54-g00ecf