summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-02-17 13:27:23 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-26 01:05:01 +0000
commitfa4b171a436559787cfcebd4046a1354a1f5cacf (patch)
treeb974e470795e2715f45a6d666206118194f5711a
parentdec4b4dfc719e095c0b3dcbff638896282c389af (diff)
downloadmeta-virtualization-fa4b171a436559787cfcebd4046a1354a1f5cacf.tar.gz
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 <bruce.ashfield@gmail.com>
-rwxr-xr-xrecipes-containers/vcontainer/files/vcontainer-common.sh433
-rw-r--r--recipes-containers/vcontainer/files/vrunner-backend-xen.sh301
-rwxr-xr-xrecipes-containers/vcontainer/files/vrunner.sh80
-rwxr-xr-xrecipes-containers/vcontainer/files/vxn-init.sh262
4 files changed, 922 insertions, 154 deletions
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() {
758 [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER") 758 [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER")
759 [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS") 759 [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS")
760 760
761 # Xen: pass exit grace period
762 [ -n "${VXN_EXIT_GRACE_PERIOD:-}" ] && args+=("--exit-grace-period" "$VXN_EXIT_GRACE_PERIOD")
763
761 echo "${args[@]}" 764 echo "${args[@]}"
762} 765}
763 766
@@ -1386,6 +1389,78 @@ parse_and_prepare_volumes() {
1386 done 1389 done
1387} 1390}
1388 1391
1392# ============================================================================
1393# VXN Container State Helpers (Xen per-container DomU)
1394# ============================================================================
1395
1396# VXN container state directory
1397vxn_container_dir() { echo "$HOME/.vxn/containers/$1"; }
1398
1399vxn_container_is_running() {
1400 local cdir="$(vxn_container_dir "$1")"
1401 [ -f "$cdir/daemon.domname" ] || return 1
1402 local domname=$(cat "$cdir/daemon.domname")
1403 xl list "$domname" >/dev/null 2>&1
1404}
1405
1406# Query entrypoint status from a running vxn container.
1407# Returns: "Running", "Exited (<code>)", or "Unknown"
1408vxn_container_status() {
1409 local name="$1"
1410 local cdir="$(vxn_container_dir "$name")"
1411
1412 # DomU not alive at all
1413 if ! vxn_container_is_running "$name"; then
1414 echo "Exited"
1415 return
1416 fi
1417
1418 # Query the guest via PTY for entrypoint status
1419 local pty_file="$cdir/daemon.pty"
1420 if [ -f "$pty_file" ]; then
1421 local pty
1422 pty=$(cat "$pty_file")
1423 if [ -c "$pty" ]; then
1424 # Open PTY, send STATUS query, read response
1425 local status_line=""
1426 exec 4<>"$pty"
1427 # Drain pending output
1428 while IFS= read -t 0.3 -r _discard <&4; do :; done
1429 echo "===STATUS===" >&4
1430 while IFS= read -t 3 -r status_line <&4; do
1431 status_line=$(echo "$status_line" | tr -d '\r')
1432 case "$status_line" in
1433 *"===RUNNING==="*)
1434 exec 4<&- 4>&- 2>/dev/null
1435 echo "Running"
1436 return
1437 ;;
1438 *"===EXITED="*"==="*)
1439 local code=$(echo "$status_line" | sed 's/.*===EXITED=\([0-9]*\)===/\1/')
1440 exec 4<&- 4>&- 2>/dev/null
1441 echo "Exited ($code)"
1442 return
1443 ;;
1444 esac
1445 done
1446 exec 4<&- 4>&- 2>/dev/null
1447 fi
1448 fi
1449
1450 # Could not determine — DomU is alive but status query failed
1451 echo "Running"
1452}
1453
1454# Xen: error helper for unsupported commands
1455vxn_unsupported() {
1456 echo "${VCONTAINER_RUNTIME_NAME}: '$1' is not supported (VM is the container, no runtime inside)" >&2
1457 exit 1
1458}
1459vxn_not_yet() {
1460 echo "${VCONTAINER_RUNTIME_NAME}: '$1' is not yet supported" >&2
1461 exit 1
1462}
1463
1389# Handle commands 1464# Handle commands
1390case "$COMMAND" in 1465case "$COMMAND" in
1391 image) 1466 image)
@@ -1394,6 +1469,7 @@ case "$COMMAND" in
1394 # docker image rm → docker rmi 1469 # docker image rm → docker rmi
1395 # docker image pull → docker pull 1470 # docker image pull → docker pull
1396 # etc. 1471 # etc.
1472 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "image"
1397 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then 1473 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
1398 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} image requires a subcommand (ls, rm, pull, inspect, tag, push, prune)" >&2 1474 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} image requires a subcommand (ls, rm, pull, inspect, tag, push, prune)" >&2
1399 exit 1 1475 exit 1
@@ -1448,11 +1524,13 @@ case "$COMMAND" in
1448 ;; 1524 ;;
1449 1525
1450 images) 1526 images)
1527 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "images"
1451 # runtime images 1528 # runtime images
1452 run_runtime_command "$VCONTAINER_RUNTIME_CMD images ${COMMAND_ARGS[*]}" 1529 run_runtime_command "$VCONTAINER_RUNTIME_CMD images ${COMMAND_ARGS[*]}"
1453 ;; 1530 ;;
1454 1531
1455 pull) 1532 pull)
1533 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "pull"
1456 # runtime pull <image> 1534 # runtime pull <image>
1457 # Daemon mode already has networking enabled, so this works via daemon 1535 # Daemon mode already has networking enabled, so this works via daemon
1458 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then 1536 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
@@ -1474,6 +1552,7 @@ case "$COMMAND" in
1474 ;; 1552 ;;
1475 1553
1476 load) 1554 load)
1555 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "load"
1477 # runtime load -i <file> 1556 # runtime load -i <file>
1478 # Parse -i argument 1557 # Parse -i argument
1479 INPUT_FILE="" 1558 INPUT_FILE=""
@@ -1508,6 +1587,7 @@ case "$COMMAND" in
1508 ;; 1587 ;;
1509 1588
1510 import) 1589 import)
1590 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "import"
1511 # runtime import <tarball> [name:tag] - matches Docker/Podman's import exactly 1591 # runtime import <tarball> [name:tag] - matches Docker/Podman's import exactly
1512 # Only accepts tarballs (rootfs archives), not OCI directories 1592 # Only accepts tarballs (rootfs archives), not OCI directories
1513 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then 1593 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
@@ -1536,6 +1616,7 @@ case "$COMMAND" in
1536 ;; 1616 ;;
1537 1617
1538 vimport) 1618 vimport)
1619 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "vimport"
1539 # Extended import: handles OCI directories, tarballs, and plain directories 1620 # Extended import: handles OCI directories, tarballs, and plain directories
1540 # Auto-detects format 1621 # Auto-detects format
1541 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then 1622 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
@@ -1629,6 +1710,7 @@ case "$COMMAND" in
1629 ;; 1710 ;;
1630 1711
1631 save) 1712 save)
1713 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "save"
1632 # runtime save -o <file> <image> 1714 # runtime save -o <file> <image>
1633 OUTPUT_FILE="" 1715 OUTPUT_FILE=""
1634 IMAGE_NAME="" 1716 IMAGE_NAME=""
@@ -1685,12 +1767,27 @@ case "$COMMAND" in
1685 ;; 1767 ;;
1686 1768
1687 tag|rmi) 1769 tag|rmi)
1770 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "$COMMAND"
1688 # Commands that work with existing images 1771 # Commands that work with existing images
1689 run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}" 1772 run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}"
1690 ;; 1773 ;;
1691 1774
1692 # Container lifecycle commands 1775 # Container lifecycle commands
1693 ps) 1776 ps)
1777 # Xen: list per-container DomUs
1778 if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then
1779 printf "%-15s %-25s %-15s %-20s\n" "NAME" "IMAGE" "STATUS" "STARTED"
1780 for cdir in "$HOME/.vxn/containers"/*/; do
1781 [ -d "$cdir" ] || continue
1782 name=$(basename "$cdir")
1783 ctr_image=$(grep '^IMAGE=' "$cdir/container.meta" 2>/dev/null | cut -d= -f2-)
1784 ctr_started=$(grep '^STARTED=' "$cdir/container.meta" 2>/dev/null | cut -d= -f2-)
1785 status=$(vxn_container_status "$name")
1786 printf "%-15s %-25s %-15s %-20s\n" "$name" "${ctr_image:-unknown}" "$status" "${ctr_started:-unknown}"
1787 done
1788 exit 0
1789 fi
1790
1694 # List containers and show port forwards if daemon is running 1791 # List containers and show port forwards if daemon is running
1695 run_runtime_command "$VCONTAINER_RUNTIME_CMD ps ${COMMAND_ARGS[*]}" 1792 run_runtime_command "$VCONTAINER_RUNTIME_CMD ps ${COMMAND_ARGS[*]}"
1696 PS_EXIT=$? 1793 PS_EXIT=$?
@@ -1719,6 +1816,24 @@ case "$COMMAND" in
1719 ;; 1816 ;;
1720 1817
1721 rm) 1818 rm)
1819 # Xen: remove per-container DomU state
1820 if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then
1821 for arg in "${COMMAND_ARGS[@]}"; do
1822 case "$arg" in -*) continue ;; esac
1823 cdir="$(vxn_container_dir "$arg")"
1824 if [ -d "$cdir" ]; then
1825 # Stop if still running
1826 if vxn_container_is_running "$arg"; then
1827 RUNNER_ARGS=$(build_runner_args)
1828 "$RUNNER" $RUNNER_ARGS --daemon-socket-dir "$cdir" --state-dir "$cdir" --daemon-stop 2>/dev/null
1829 fi
1830 rm -rf "$cdir"
1831 echo "$arg"
1832 fi
1833 done
1834 exit 0
1835 fi
1836
1722 # Remove containers and cleanup any registered port forwards 1837 # Remove containers and cleanup any registered port forwards
1723 for arg in "${COMMAND_ARGS[@]}"; do 1838 for arg in "${COMMAND_ARGS[@]}"; do
1724 # Skip flags like -f, --force, etc. 1839 # Skip flags like -f, --force, etc.
@@ -1734,21 +1849,51 @@ case "$COMMAND" in
1734 ;; 1849 ;;
1735 1850
1736 logs) 1851 logs)
1852 # Xen: retrieve entrypoint log from DomU
1853 if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then
1854 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
1855 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} logs requires <container>" >&2
1856 exit 1
1857 fi
1858 cname="${COMMAND_ARGS[0]}"
1859 cdir="$(vxn_container_dir "$cname")"
1860 if [ -d "$cdir" ] && vxn_container_is_running "$cname"; then
1861 RUNNER_ARGS=$(build_runner_args)
1862 "$RUNNER" $RUNNER_ARGS --daemon-socket-dir "$cdir" --state-dir "$cdir" --daemon-send -- "cat /tmp/entrypoint.log 2>/dev/null"
1863 exit $?
1864 fi
1865 echo "Container $cname not running" >&2
1866 exit 1
1867 fi
1868
1737 # View container logs 1869 # View container logs
1738 run_runtime_command "$VCONTAINER_RUNTIME_CMD logs ${COMMAND_ARGS[*]}" 1870 run_runtime_command "$VCONTAINER_RUNTIME_CMD logs ${COMMAND_ARGS[*]}"
1739 ;; 1871 ;;
1740 1872
1741 inspect) 1873 inspect)
1874 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_not_yet "inspect"
1742 # Inspect container or image 1875 # Inspect container or image
1743 run_runtime_command "$VCONTAINER_RUNTIME_CMD inspect ${COMMAND_ARGS[*]}" 1876 run_runtime_command "$VCONTAINER_RUNTIME_CMD inspect ${COMMAND_ARGS[*]}"
1744 ;; 1877 ;;
1745 1878
1746 start|restart|kill|pause|unpause) 1879 start|restart|kill|pause|unpause)
1880 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "$COMMAND"
1747 # Container state commands (no special handling needed) 1881 # Container state commands (no special handling needed)
1748 run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}" 1882 run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}"
1749 ;; 1883 ;;
1750 1884
1751 stop) 1885 stop)
1886 # Xen: stop per-container DomU
1887 if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && [ -n "${COMMAND_ARGS[0]:-}" ]; then
1888 cname="${COMMAND_ARGS[0]}"
1889 cdir="$(vxn_container_dir "$cname")"
1890 if [ -d "$cdir" ]; then
1891 RUNNER_ARGS=$(build_runner_args)
1892 "$RUNNER" $RUNNER_ARGS --daemon-socket-dir "$cdir" --state-dir "$cdir" --daemon-stop
1893 exit $?
1894 fi
1895 fi
1896
1752 # Stop container and cleanup any registered port forwards 1897 # Stop container and cleanup any registered port forwards
1753 if [ ${#COMMAND_ARGS[@]} -ge 1 ]; then 1898 if [ ${#COMMAND_ARGS[@]} -ge 1 ]; then
1754 STOP_CONTAINER_NAME="${COMMAND_ARGS[0]}" 1899 STOP_CONTAINER_NAME="${COMMAND_ARGS[0]}"
@@ -1762,33 +1907,39 @@ case "$COMMAND" in
1762 1907
1763 # Image commands 1908 # Image commands
1764 commit) 1909 commit)
1910 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "commit"
1765 # Commit container to image 1911 # Commit container to image
1766 run_runtime_command "$VCONTAINER_RUNTIME_CMD commit ${COMMAND_ARGS[*]}" 1912 run_runtime_command "$VCONTAINER_RUNTIME_CMD commit ${COMMAND_ARGS[*]}"
1767 ;; 1913 ;;
1768 1914
1769 history) 1915 history)
1916 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "history"
1770 # Show image history 1917 # Show image history
1771 run_runtime_command "$VCONTAINER_RUNTIME_CMD history ${COMMAND_ARGS[*]}" 1918 run_runtime_command "$VCONTAINER_RUNTIME_CMD history ${COMMAND_ARGS[*]}"
1772 ;; 1919 ;;
1773 1920
1774 # Registry commands 1921 # Registry commands
1775 push) 1922 push)
1923 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "push"
1776 # Push image to registry 1924 # Push image to registry
1777 run_runtime_command "$VCONTAINER_RUNTIME_CMD push ${COMMAND_ARGS[*]}" 1925 run_runtime_command "$VCONTAINER_RUNTIME_CMD push ${COMMAND_ARGS[*]}"
1778 ;; 1926 ;;
1779 1927
1780 search) 1928 search)
1929 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "search"
1781 # Search registries 1930 # Search registries
1782 run_runtime_command "$VCONTAINER_RUNTIME_CMD search ${COMMAND_ARGS[*]}" 1931 run_runtime_command "$VCONTAINER_RUNTIME_CMD search ${COMMAND_ARGS[*]}"
1783 ;; 1932 ;;
1784 1933
1785 login) 1934 login)
1935 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "login"
1786 # Login to registry - may need credentials via stdin 1936 # Login to registry - may need credentials via stdin
1787 # For non-interactive: runtime login -u user -p pass registry 1937 # For non-interactive: runtime login -u user -p pass registry
1788 run_runtime_command "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}" 1938 run_runtime_command "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}"
1789 ;; 1939 ;;
1790 1940
1791 logout) 1941 logout)
1942 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "logout"
1792 # Logout from registry 1943 # Logout from registry
1793 run_runtime_command "$VCONTAINER_RUNTIME_CMD logout ${COMMAND_ARGS[*]}" 1944 run_runtime_command "$VCONTAINER_RUNTIME_CMD logout ${COMMAND_ARGS[*]}"
1794 ;; 1945 ;;
@@ -1800,6 +1951,21 @@ case "$COMMAND" in
1800 exit 1 1951 exit 1
1801 fi 1952 fi
1802 1953
1954 # Xen: exec in per-container DomU
1955 if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then
1956 cname="${COMMAND_ARGS[0]}"
1957 cdir="$(vxn_container_dir "$cname")"
1958 if [ -d "$cdir" ] && vxn_container_is_running "$cname"; then
1959 shift_args=("${COMMAND_ARGS[@]:1}")
1960 exec_cmd="${shift_args[*]}"
1961 RUNNER_ARGS=$(build_runner_args)
1962 "$RUNNER" $RUNNER_ARGS --daemon-socket-dir "$cdir" --state-dir "$cdir" --daemon-send -- "$exec_cmd"
1963 exit $?
1964 fi
1965 echo "Container $cname not running" >&2
1966 exit 1
1967 fi
1968
1803 # Check for interactive flags 1969 # Check for interactive flags
1804 EXEC_INTERACTIVE=false 1970 EXEC_INTERACTIVE=false
1805 EXEC_ARGS=() 1971 EXEC_ARGS=()
@@ -1839,6 +2005,7 @@ case "$COMMAND" in
1839 2005
1840 # VM shell - interactive shell into the VM itself (not a container) 2006 # VM shell - interactive shell into the VM itself (not a container)
1841 vshell) 2007 vshell)
2008 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "vshell"
1842 # Opens a shell directly in the vdkr/vpdmn VM for debugging 2009 # Opens a shell directly in the vdkr/vpdmn VM for debugging
1843 # This runs /bin/sh in the VM, not inside a container 2010 # This runs /bin/sh in the VM, not inside a container
1844 # Useful for: 2011 # Useful for:
@@ -1858,6 +2025,7 @@ case "$COMMAND" in
1858 2025
1859 # Runtime cp - copy files to/from container 2026 # Runtime cp - copy files to/from container
1860 cp) 2027 cp)
2028 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "cp"
1861 if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then 2029 if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then
1862 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} cp requires <src> <dest>" >&2 2030 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} cp requires <src> <dest>" >&2
1863 echo "Usage: $VCONTAINER_RUNTIME_NAME cp <container>:<path> <local_path>" >&2 2031 echo "Usage: $VCONTAINER_RUNTIME_NAME cp <container>:<path> <local_path>" >&2
@@ -2025,14 +2193,17 @@ case "$COMMAND" in
2025 ;; 2193 ;;
2026 2194
2027 info) 2195 info)
2196 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "info"
2028 run_runtime_command "$VCONTAINER_RUNTIME_CMD info" 2197 run_runtime_command "$VCONTAINER_RUNTIME_CMD info"
2029 ;; 2198 ;;
2030 2199
2031 version) 2200 version)
2201 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "version"
2032 run_runtime_command "$VCONTAINER_RUNTIME_CMD version" 2202 run_runtime_command "$VCONTAINER_RUNTIME_CMD version"
2033 ;; 2203 ;;
2034 2204
2035 system) 2205 system)
2206 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "system"
2036 # Passthrough to runtime system commands (df, prune, events, etc.) 2207 # Passthrough to runtime system commands (df, prune, events, etc.)
2037 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then 2208 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
2038 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} system requires a subcommand: df, prune, events, info" >&2 2209 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} system requires a subcommand: df, prune, events, info" >&2
@@ -2181,6 +2352,7 @@ case "$COMMAND" in
2181 ;; 2352 ;;
2182 2353
2183 vrun) 2354 vrun)
2355 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "vrun"
2184 # Extended run: run a command in a container (runtime-like syntax) 2356 # Extended run: run a command in a container (runtime-like syntax)
2185 # Usage: <tool> vrun [options] <image> [command] [args...] 2357 # Usage: <tool> vrun [options] <image> [command] [args...]
2186 # Options: 2358 # Options:
@@ -2389,6 +2561,13 @@ case "$COMMAND" in
2389 exit 1 2561 exit 1
2390 fi 2562 fi
2391 2563
2564 # vxn (Xen): ephemeral mode by default.
2565 # Detached mode (-d) uses per-container DomU with daemon loop instead.
2566 if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then
2567 # Detached flag is parsed below; defer NO_DAEMON until after flag parsing
2568 :
2569 fi
2570
2392 # Check if any volume mounts, network, port forwards, or detach are present 2571 # Check if any volume mounts, network, port forwards, or detach are present
2393 RUN_HAS_VOLUMES=false 2572 RUN_HAS_VOLUMES=false
2394 RUN_HAS_NETWORK=false 2573 RUN_HAS_NETWORK=false
@@ -2431,6 +2610,15 @@ case "$COMMAND" in
2431 i=$((i + 1)) 2610 i=$((i + 1))
2432 done 2611 done
2433 2612
2613 # Xen: non-detached runs use ephemeral mode unless memres is running
2614 if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && [ "$RUN_IS_DETACHED" != "true" ]; then
2615 if daemon_is_running; then
2616 NO_DAEMON=false
2617 else
2618 NO_DAEMON=true
2619 fi
2620 fi
2621
2434 # Volume mounts require daemon mode 2622 # Volume mounts require daemon mode
2435 if [ "$RUN_HAS_VOLUMES" = "true" ] && ! daemon_is_running; then 2623 if [ "$RUN_HAS_VOLUMES" = "true" ] && ! daemon_is_running; then
2436 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Volume mounts require daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2 2624 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
2469 if [ "$INTERACTIVE" = "true" ]; then 2657 if [ "$INTERACTIVE" = "true" ]; then
2470 # Interactive mode with volumes still needs to stop daemon (volumes use share dir) 2658 # Interactive mode with volumes still needs to stop daemon (volumes use share dir)
2471 # Interactive mode without volumes can use daemon_interactive (faster) 2659 # Interactive mode without volumes can use daemon_interactive (faster)
2472 if [ "$RUN_HAS_VOLUMES" = "false" ] && daemon_is_running; then 2660 if [ "$NO_DAEMON" != "true" ] && [ "$RUN_HAS_VOLUMES" = "false" ] && daemon_is_running; then
2473 # Use daemon interactive mode - keeps daemon running 2661 # Use daemon interactive mode - keeps daemon running
2474 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon interactive mode" >&2 2662 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon interactive mode" >&2
2475 RUNNER_ARGS=$(build_runner_args) 2663 RUNNER_ARGS=$(build_runner_args)
@@ -2505,6 +2693,66 @@ case "$COMMAND" in
2505 else 2693 else
2506 # Non-interactive - use daemon mode when available 2694 # Non-interactive - use daemon mode when available
2507 2695
2696 # Xen detached mode: per-container DomU with daemon loop
2697 if [ "$RUN_IS_DETACHED" = "true" ] && [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then
2698 # Generate name if not provided
2699 [ -z "$RUN_CONTAINER_NAME" ] && RUN_CONTAINER_NAME="$(cat /proc/sys/kernel/random/uuid | cut -c1-8)"
2700
2701 # Per-container state dir
2702 VXN_CTR_DIR="$HOME/.vxn/containers/$RUN_CONTAINER_NAME"
2703 mkdir -p "$VXN_CTR_DIR"
2704
2705 # Build runner args with per-container state/socket dir
2706 RUNNER_ARGS=$(build_runner_args)
2707 RUNNER_ARGS="$RUNNER_ARGS --daemon-socket-dir $VXN_CTR_DIR --state-dir $VXN_CTR_DIR"
2708 RUNNER_ARGS="$RUNNER_ARGS --container-name $RUN_CONTAINER_NAME"
2709
2710 # Start per-container DomU (daemon mode + initial command)
2711 "$RUNNER" $RUNNER_ARGS --daemon-start -- "$RUNTIME_CMD"
2712
2713 if [ $? -eq 0 ]; then
2714 # Save metadata — extract image name (last positional arg before any cmd)
2715 local_image=""
2716 local_found_image=false
2717 local_skip_next=false
2718 for arg in "${COMMAND_ARGS[@]}"; do
2719 if [ "$local_skip_next" = "true" ]; then
2720 local_skip_next=false
2721 continue
2722 fi
2723 case "$arg" in
2724 --rm|--detach|-d|-i|--interactive|-t|--tty|--privileged|-it) ;;
2725 -p|--publish|-v|--volume|-e|--env|--name|--network|-w|--workdir|--entrypoint|-m|--memory|--cpus)
2726 local_skip_next=true ;;
2727 --publish=*|--volume=*|--env=*|--name=*|--network=*|--workdir=*|--entrypoint=*|--memory=*|--cpus=*) ;;
2728 -*) ;;
2729 *)
2730 if [ "$local_found_image" = "false" ]; then
2731 local_image="$arg"
2732 local_found_image=true
2733 fi
2734 ;;
2735 esac
2736 done
2737 echo "IMAGE=${local_image}" > "$VXN_CTR_DIR/container.meta"
2738 echo "COMMAND=$RUNTIME_CMD" >> "$VXN_CTR_DIR/container.meta"
2739 echo "STARTED=$(date -Iseconds)" >> "$VXN_CTR_DIR/container.meta"
2740 echo "$RUN_CONTAINER_NAME"
2741 else
2742 rm -rf "$VXN_CTR_DIR"
2743 exit 1
2744 fi
2745 exit 0
2746 fi
2747
2748 # Xen memres mode: dispatch container to persistent DomU
2749 if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && [ "$NO_DAEMON" != "true" ] && daemon_is_running; then
2750 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using memres DomU" >&2
2751 RUNNER_ARGS=$(build_runner_args)
2752 "$RUNNER" $RUNNER_ARGS --daemon-run -- "$RUNTIME_CMD"
2753 exit $?
2754 fi
2755
2508 # For detached containers with port forwards, add them dynamically via QMP 2756 # For detached containers with port forwards, add them dynamically via QMP
2509 if [ "$RUN_IS_DETACHED" = "true" ] && [ "$RUN_HAS_PORT_FORWARDS" = "true" ] && daemon_is_running; then 2757 if [ "$RUN_IS_DETACHED" = "true" ] && [ "$RUN_HAS_PORT_FORWARDS" = "true" ] && daemon_is_running; then
2510 # Generate container name if not provided (needed for port tracking) 2758 # Generate container name if not provided (needed for port tracking)
@@ -2676,71 +2924,144 @@ case "$COMMAND" in
2676 echo "" 2924 echo ""
2677 found=0 2925 found=0
2678 tracked_pids="" 2926 tracked_pids=""
2679 for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do 2927
2680 [ -f "$pid_file" ] || continue 2928 # Xen: check for vxn domains via xl list
2681 pid=$(cat "$pid_file" 2>/dev/null) 2929 if [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ]; then
2682 if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then 2930 for domname_file in "$DEFAULT_STATE_DIR"/*/daemon.domname; do
2683 instance_dir=$(dirname "$pid_file") 2931 [ -f "$domname_file" ] || continue
2684 instance_name=$(basename "$instance_dir") 2932 domname=$(cat "$domname_file" 2>/dev/null)
2685 echo " ${CYAN}$instance_name${NC}" 2933 if [ -n "$domname" ] && xl list "$domname" >/dev/null 2>&1; then
2686 echo " PID: $pid" 2934 instance_dir=$(dirname "$domname_file")
2687 echo " State: $instance_dir" 2935 instance_name=$(basename "$instance_dir")
2688 if [ -f "$instance_dir/qemu.log" ]; then 2936 echo " ${CYAN}$instance_name${NC}"
2689 # Try to extract port forwards from qemu command line 2937 echo " Domain: $domname"
2690 ports=$(grep -o 'hostfwd=[^,]*' "$instance_dir/qemu.log" 2>/dev/null | sed 's/hostfwd=tcp:://g; s/-/:/' | tr '\n' ' ') 2938 echo " State: $instance_dir"
2691 [ -n "$ports" ] && echo " Ports: $ports" 2939 if [ -f "$instance_dir/daemon.pty" ]; then
2940 echo " PTY: $(cat "$instance_dir/daemon.pty")"
2941 fi
2942 echo ""
2943 found=$((found + 1))
2692 fi 2944 fi
2693 echo "" 2945 done
2694 found=$((found + 1)) 2946
2695 tracked_pids="$tracked_pids $pid" 2947 # Also check per-container DomUs
2948 if [ -d "$HOME/.vxn/containers" ]; then
2949 for meta_file in "$HOME/.vxn/containers"/*/container.meta; do
2950 [ -f "$meta_file" ] || continue
2951 ctr_dir=$(dirname "$meta_file")
2952 ctr_name=$(basename "$ctr_dir")
2953 if vxn_container_is_running "$ctr_name"; then
2954 image=$(grep '^IMAGE=' "$meta_file" 2>/dev/null | cut -d= -f2)
2955 started=$(grep '^STARTED=' "$meta_file" 2>/dev/null | cut -d= -f2)
2956 echo " ${CYAN}$ctr_name${NC} (per-container DomU)"
2957 echo " Image: ${image:-(unknown)}"
2958 echo " Started: ${started:-(unknown)}"
2959 echo " State: $ctr_dir"
2960 echo ""
2961 found=$((found + 1))
2962 fi
2963 done
2696 fi 2964 fi
2697 done
2698 if [ $found -eq 0 ]; then
2699 echo " (none)"
2700 fi
2701 2965
2702 # Check for zombie/orphan QEMU processes (vdkr or vpdmn) 2966 if [ $found -eq 0 ]; then
2703 echo "" 2967 echo " (none)"
2704 echo "Checking for orphan QEMU processes..."
2705 zombies=""
2706 for qemu_pid in $(pgrep -f "qemu-system.*runtime=(docker|podman)" 2>/dev/null || true); do
2707 # Skip if this PID is already tracked
2708 if echo "$tracked_pids" | grep -qw "$qemu_pid"; then
2709 continue
2710 fi 2968 fi
2711 # Also check other tool's state dirs 2969
2712 other_tracked=false 2970 # Check for orphan vxn domains
2713 for vpid_file in "$OTHER_STATE_DIR"/*/daemon.pid; do 2971 echo ""
2714 [ -f "$vpid_file" ] || continue 2972 echo "Checking for orphan Xen domains..."
2715 vpid=$(cat "$vpid_file" 2>/dev/null) 2973 orphans=""
2716 if [ "$vpid" = "$qemu_pid" ]; then 2974 for domname in $(xl list 2>/dev/null | awk '/^vxn-/{print $1}'); do
2717 other_tracked=true 2975 tracked=false
2718 break 2976 for df in "$DEFAULT_STATE_DIR"/*/daemon.domname "$HOME"/.vxn/containers/*/daemon.domname; do
2977 [ -f "$df" ] || continue
2978 if [ "$(cat "$df" 2>/dev/null)" = "$domname" ]; then
2979 tracked=true
2980 break
2981 fi
2982 done
2983 if [ "$tracked" != "true" ]; then
2984 orphans="$orphans $domname"
2719 fi 2985 fi
2720 done 2986 done
2721 if [ "$other_tracked" = "true" ]; then 2987
2722 continue 2988 if [ -n "$orphans" ]; then
2989 echo -e "${YELLOW}Orphan vxn domains found:${NC}"
2990 for odom in $orphans; do
2991 echo " ${RED}$odom${NC}"
2992 echo " Destroy with: xl destroy $odom"
2993 done
2994 else
2995 echo " (no orphans found)"
2996 fi
2997 else
2998 # QEMU: check PID files
2999 for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do
3000 [ -f "$pid_file" ] || continue
3001 pid=$(cat "$pid_file" 2>/dev/null)
3002 if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then
3003 instance_dir=$(dirname "$pid_file")
3004 instance_name=$(basename "$instance_dir")
3005 echo " ${CYAN}$instance_name${NC}"
3006 echo " PID: $pid"
3007 echo " State: $instance_dir"
3008 if [ -f "$instance_dir/qemu.log" ]; then
3009 # Try to extract port forwards from qemu command line
3010 ports=$(grep -o 'hostfwd=[^,]*' "$instance_dir/qemu.log" 2>/dev/null | sed 's/hostfwd=tcp:://g; s/-/:/' | tr '\n' ' ')
3011 [ -n "$ports" ] && echo " Ports: $ports"
3012 fi
3013 echo ""
3014 found=$((found + 1))
3015 tracked_pids="$tracked_pids $pid"
3016 fi
3017 done
3018 if [ $found -eq 0 ]; then
3019 echo " (none)"
2723 fi 3020 fi
2724 zombies="$zombies $qemu_pid"
2725 done
2726 3021
2727 if [ -n "$zombies" ]; then 3022 # Check for zombie/orphan QEMU processes (vdkr or vpdmn)
2728 echo "" 3023 echo ""
2729 echo -e "${YELLOW}Orphan QEMU processes found:${NC}" 3024 echo "Checking for orphan QEMU processes..."
2730 for zpid in $zombies; do 3025 zombies=""
2731 # Extract runtime from cmdline 3026 for qemu_pid in $(pgrep -f "qemu-system.*runtime=(docker|podman)" 2>/dev/null || true); do
2732 cmdline=$(cat /proc/$zpid/cmdline 2>/dev/null | tr '\0' ' ') 3027 # Skip if this PID is already tracked
2733 runtime=$(echo "$cmdline" | grep -o 'runtime=[a-z]*' | cut -d= -f2) 3028 if echo "$tracked_pids" | grep -qw "$qemu_pid"; then
2734 state_dir=$(echo "$cmdline" | grep -o 'path=[^,]*daemon.sock' | sed 's|path=||; s|/daemon.sock||') 3029 continue
2735 echo "" 3030 fi
2736 echo " ${RED}PID $zpid${NC} (${runtime:-unknown})" 3031 # Also check other tool's state dirs
2737 [ -n "$state_dir" ] && echo " State: $state_dir" 3032 other_tracked=false
2738 echo " Kill with: kill $zpid" 3033 for vpid_file in "$OTHER_STATE_DIR"/*/daemon.pid; do
3034 [ -f "$vpid_file" ] || continue
3035 vpid=$(cat "$vpid_file" 2>/dev/null)
3036 if [ "$vpid" = "$qemu_pid" ]; then
3037 other_tracked=true
3038 break
3039 fi
3040 done
3041 if [ "$other_tracked" = "true" ]; then
3042 continue
3043 fi
3044 zombies="$zombies $qemu_pid"
2739 done 3045 done
2740 echo "" 3046
2741 echo -e "To kill all orphans: ${CYAN}kill$zombies${NC}" 3047 if [ -n "$zombies" ]; then
2742 else 3048 echo ""
2743 echo " (no orphans found)" 3049 echo -e "${YELLOW}Orphan QEMU processes found:${NC}"
3050 for zpid in $zombies; do
3051 # Extract runtime from cmdline
3052 cmdline=$(cat /proc/$zpid/cmdline 2>/dev/null | tr '\0' ' ')
3053 runtime=$(echo "$cmdline" | grep -o 'runtime=[a-z]*' | cut -d= -f2)
3054 state_dir=$(echo "$cmdline" | grep -o 'path=[^,]*daemon.sock' | sed 's|path=||; s|/daemon.sock||')
3055 echo ""
3056 echo " ${RED}PID $zpid${NC} (${runtime:-unknown})"
3057 [ -n "$state_dir" ] && echo " State: $state_dir"
3058 echo " Kill with: kill $zpid"
3059 done
3060 echo ""
3061 echo -e "To kill all orphans: ${CYAN}kill$zombies${NC}"
3062 else
3063 echo " (no orphans found)"
3064 fi
2744 fi 3065 fi
2745 ;; 3066 ;;
2746 clean-ports) 3067 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 @@
13# Key differences from QEMU backend: 13# Key differences from QEMU backend:
14# - Block devices appear as /dev/xvd* instead of /dev/vd* 14# - Block devices appear as /dev/xvd* instead of /dev/vd*
15# - Network uses bridge + iptables NAT instead of QEMU slirp 15# - Network uses bridge + iptables NAT instead of QEMU slirp
16# - Console uses PV console (hvc0/hvc1) instead of virtio-serial 16# - Console uses PV console (hvc0) with serial='pty' for PTY on Dom0
17# - 9p file sharing uses trans=xen instead of trans=virtio 17# - Daemon IPC uses direct PTY I/O (no socat bridge needed)
18# - VM tracking uses domain name instead of PID 18# - VM tracking uses domain name instead of PID
19 19
20# ============================================================================ 20# ============================================================================
@@ -43,8 +43,12 @@ hv_setup_arch() {
43 ;; 43 ;;
44 esac 44 esac
45 45
46 # Xen domain name (unique per instance) 46 # Xen domain name: use container name if set, otherwise PID-based
47 HV_DOMNAME="vxn-$$" 47 if [ -n "${CONTAINER_NAME:-}" ]; then
48 HV_DOMNAME="vxn-${CONTAINER_NAME}"
49 else
50 HV_DOMNAME="vxn-$$"
51 fi
48 HV_VM_PID="" 52 HV_VM_PID=""
49 53
50 # Xen domain config path (generated at runtime) 54 # Xen domain config path (generated at runtime)
@@ -97,6 +101,8 @@ hv_prepare_container() {
97 fi 101 fi
98 102
99 # Parse image name and any trailing command from "docker run [opts] <image> [cmd...]" 103 # Parse image name and any trailing command from "docker run [opts] <image> [cmd...]"
104 # Uses word counting + cut to extract the user command portion from the
105 # original string, preserving internal spaces (e.g., -c "echo hello && sleep 5").
100 local args 106 local args
101 args=$(echo "$DOCKER_CMD" | sed 's/^[a-z]* run //') 107 args=$(echo "$DOCKER_CMD" | sed 's/^[a-z]* run //')
102 108
@@ -104,16 +110,16 @@ hv_prepare_container() {
104 local user_cmd="" 110 local user_cmd=""
105 local skip_next=false 111 local skip_next=false
106 local found_image=false 112 local found_image=false
113 local word_count=0
107 for arg in $args; do 114 for arg in $args; do
115 if [ "$found_image" = "true" ]; then
116 break
117 fi
118 word_count=$((word_count + 1))
108 if [ "$skip_next" = "true" ]; then 119 if [ "$skip_next" = "true" ]; then
109 skip_next=false 120 skip_next=false
110 continue 121 continue
111 fi 122 fi
112 if [ "$found_image" = "true" ]; then
113 # Everything after image name is the user command
114 user_cmd="$user_cmd $arg"
115 continue
116 fi
117 case "$arg" in 123 case "$arg" in
118 --rm|--detach|-d|-i|--interactive|-t|--tty|--privileged|-it) 124 --rm|--detach|-d|-i|--interactive|-t|--tty|--privileged|-it)
119 ;; 125 ;;
@@ -130,7 +136,21 @@ hv_prepare_container() {
130 ;; 136 ;;
131 esac 137 esac
132 done 138 done
133 user_cmd=$(echo "$user_cmd" | sed 's/^ *//') 139
140 # Extract user command from original string using cut (preserves internal spaces)
141 if [ "$found_image" = "true" ]; then
142 user_cmd=$(echo "$args" | cut -d' ' -f$((word_count + 1))-)
143 # If cut returns the whole string (no fields after image), clear it
144 [ "$user_cmd" = "$args" ] && [ "$word_count" -ge "$(echo "$args" | wc -w)" ] && user_cmd=""
145 fi
146
147 # Strip /bin/sh -c wrapper from user command — the guest already wraps
148 # with /bin/sh -c in exec_in_container_background(), so passing it through
149 # would create nested shells with broken quoting.
150 case "$user_cmd" in
151 "/bin/sh -c "*) user_cmd="${user_cmd#/bin/sh -c }" ;;
152 "sh -c "*) user_cmd="${user_cmd#sh -c }" ;;
153 esac
134 154
135 if [ -z "$image" ]; then 155 if [ -z "$image" ]; then
136 log "DEBUG" "hv_prepare_container: no image found in DOCKER_CMD" 156 log "DEBUG" "hv_prepare_container: no image found in DOCKER_CMD"
@@ -245,22 +265,17 @@ hv_build_network_opts() {
245hv_build_9p_opts() { 265hv_build_9p_opts() {
246 local share_dir="$1" 266 local share_dir="$1"
247 local share_tag="$2" 267 local share_tag="$2"
248 # For Xen, 9p is configured in the domain config, not as a command-line option 268 # Xen 9p (xen_9pfsd) is not reliable in all environments (e.g. nested
249 # We accumulate these and include them in the config file 269 # QEMU→Xen). Keep the interface for future use but don't depend on it
250 _XEN_9P+=("{ 'tag': '$share_tag', 'path': '$share_dir', 'security_model': 'none' }") 270 # for daemon IPC — we use serial/PTY + socat instead.
251 # Return empty string since Xen doesn't use command-line 9p options 271 _XEN_9P+=("'tag=$share_tag,path=$share_dir,security_model=none,type=xen_9pfsd'")
252 echo ""
253} 272}
254 273
255hv_build_daemon_opts() { 274hv_build_daemon_opts() {
256 HV_DAEMON_OPTS="" 275 HV_DAEMON_OPTS=""
257 # Xen uses PV console (hvc1) for daemon command channel 276 # Xen daemon mode uses hvc0 with serial='pty' for bidirectional IPC.
258 # The init scripts already have /dev/hvc1 as a fallback for the daemon port 277 # The PTY is created on Dom0 and bridged to a Unix socket via socat.
259 # No extra config needed - hvc1 is automatically available in PV guests 278 # This is the same approach runx used (serial_start).
260 #
261 # For the host-side socket, we'll use xl console with a pipe
262 # The daemon socket is handled differently for Xen:
263 # We create a socat bridge between the Xen console and a unix socket
264} 279}
265 280
266hv_build_vm_cmd() { 281hv_build_vm_cmd() {
@@ -316,6 +331,8 @@ extra = "console=hvc0 quiet loglevel=0 init=/init vcontainer.blk=xvd vcontainer.
316disk = [ $disk_array ] 331disk = [ $disk_array ]
317vif = [ $vif_array ] 332vif = [ $vif_array ]
318 333
334serial = 'pty'
335
319on_poweroff = "destroy" 336on_poweroff = "destroy"
320on_reboot = "destroy" 337on_reboot = "destroy"
321on_crash = "destroy" 338on_crash = "destroy"
@@ -353,16 +370,36 @@ hv_start_vm_background() {
353 # Create the domain 370 # Create the domain
354 xl create "$HV_XEN_CFG" >> "$log_file" 2>&1 371 xl create "$HV_XEN_CFG" >> "$log_file" 2>&1
355 372
356 # For background monitoring, we need a PID-like concept 373 # Xen domains don't have a PID on Dom0 — xl manages them by name.
357 # Use the domain name as VM identifier 374 # For daemon mode, start a lightweight monitor process that stays alive
358 HV_VM_PID="$$" # Use our PID as a placeholder for compatibility 375 # while the domain exists. This gives vcontainer-common.sh a real PID
376 # to check in /proc/$pid for daemon_is_running().
377 HV_VM_PID="$$"
359 378
360 if [ "$DAEMON_MODE" = "start" ]; then 379 if [ "$DAEMON_MODE" = "start" ]; then
361 # Daemon mode: bridge xl console (hvc1) to the daemon unix socket 380 # Daemon mode: get the domain's hvc0 PTY for direct I/O.
362 # xl console -n 1 connects to the second PV console (hvc1) 381 # serial='pty' in the xl config creates a PTY on Dom0.
363 socat "UNIX-LISTEN:$DAEMON_SOCKET,fork" "EXEC:xl console -n 1 $HV_DOMNAME" & 382 # We read/write this PTY directly — no socat bridge needed.
364 _XEN_SOCAT_PID=$! 383 local domid
365 log "DEBUG" "Console-socket bridge started (PID: $_XEN_SOCAT_PID)" 384 domid=$(xl domid "$HV_DOMNAME" 2>/dev/null)
385 if [ -n "$domid" ]; then
386 _XEN_PTY=$(xenstore-read "/local/domain/$domid/console/tty" 2>/dev/null)
387 if [ -n "$_XEN_PTY" ]; then
388 log "DEBUG" "Domain $HV_DOMNAME (domid $domid) console PTY: $_XEN_PTY"
389 echo "$_XEN_PTY" > "$DAEMON_SOCKET_DIR/daemon.pty"
390 else
391 log "ERROR" "Could not read console PTY from xenstore for domid $domid"
392 fi
393 else
394 log "ERROR" "Could not get domid for $HV_DOMNAME"
395 fi
396
397 # Monitor process: stays alive while domain exists.
398 # vcontainer-common.sh checks /proc/$pid → alive means daemon running.
399 # When domain dies (xl destroy, guest reboot), monitor exits.
400 local _domname="$HV_DOMNAME"
401 (while xl list "$_domname" >/dev/null 2>&1; do sleep 10; done) &
402 HV_VM_PID=$!
366 else 403 else
367 # Ephemeral mode: capture guest console (hvc0) to log file 404 # Ephemeral mode: capture guest console (hvc0) to log file
368 # so the monitoring loop in vrunner.sh can see output markers 405 # so the monitoring loop in vrunner.sh can see output markers
@@ -413,11 +450,6 @@ hv_destroy_vm() {
413 if [ -n "${_XEN_CONSOLE_PID:-}" ]; then 450 if [ -n "${_XEN_CONSOLE_PID:-}" ]; then
414 kill $_XEN_CONSOLE_PID 2>/dev/null || true 451 kill $_XEN_CONSOLE_PID 2>/dev/null || true
415 fi 452 fi
416
417 # Clean up console bridge (daemon mode)
418 if [ -n "${_XEN_SOCAT_PID:-}" ]; then
419 kill $_XEN_SOCAT_PID 2>/dev/null || true
420 fi
421} 453}
422 454
423hv_get_vm_id() { 455hv_get_vm_id() {
@@ -524,6 +556,181 @@ hv_daemon_is_running() {
524 [ -n "$HV_DOMNAME" ] && xl list "$HV_DOMNAME" >/dev/null 2>&1 556 [ -n "$HV_DOMNAME" ] && xl list "$HV_DOMNAME" >/dev/null 2>&1
525} 557}
526 558
559# PTY-based daemon readiness check.
560# The guest emits ===PONG=== on hvc0 at daemon startup and in response to PING.
561# We read the PTY (saved by hv_start_vm_background) looking for this marker.
562hv_daemon_ping() {
563 local pty_file="$DAEMON_SOCKET_DIR/daemon.pty"
564 [ -f "$pty_file" ] || return 1
565 local pty
566 pty=$(cat "$pty_file")
567 [ -c "$pty" ] || return 1
568
569 # Open PTY for read/write on fd 3
570 exec 3<>"$pty"
571
572 # Send PING (guest also emits PONG at startup)
573 echo "===PING===" >&3
574
575 # Read lines looking for PONG (skip boot messages, log lines)
576 local line
577 while IFS= read -t 5 -r line <&3; do
578 line=$(echo "$line" | tr -d '\r')
579 case "$line" in
580 *"===PONG==="*) exec 3<&- 3>&-; return 0 ;;
581 esac
582 done
583
584 exec 3<&- 3>&- 2>/dev/null
585 return 1
586}
587
588# PTY-based daemon command send.
589# Writes base64-encoded command to PTY, reads response with markers.
590# Same protocol as socat-based daemon_send in vrunner.sh.
591hv_daemon_send() {
592 local cmd="$1"
593 local pty_file="$DAEMON_SOCKET_DIR/daemon.pty"
594 [ -f "$pty_file" ] || { log "ERROR" "No daemon PTY file"; return 1; }
595 local pty
596 pty=$(cat "$pty_file")
597 [ -c "$pty" ] || { log "ERROR" "PTY $pty not a character device"; return 1; }
598
599 # Update activity timestamp
600 touch "$DAEMON_SOCKET_DIR/activity" 2>/dev/null || true
601
602 # Encode command
603 local cmd_b64
604 cmd_b64=$(echo -n "$cmd" | base64 -w0)
605
606 # Open PTY for read/write on fd 3
607 exec 3<>"$pty"
608
609 # Drain any pending output (boot messages, prior log lines)
610 while IFS= read -t 0.5 -r _discard <&3; do :; done
611
612 # Send command
613 echo "$cmd_b64" >&3
614
615 # Read response with markers
616 local EXIT_CODE=0
617 local in_output=false
618 local line
619 while IFS= read -t 60 -r line <&3; do
620 line=$(echo "$line" | tr -d '\r')
621 case "$line" in
622 *"===OUTPUT_START==="*)
623 in_output=true
624 ;;
625 *"===OUTPUT_END==="*)
626 in_output=false
627 ;;
628 *"===EXIT_CODE="*"==="*)
629 EXIT_CODE=$(echo "$line" | sed 's/.*===EXIT_CODE=\([0-9]*\)===/\1/')
630 ;;
631 *"===END==="*)
632 break
633 ;;
634 *)
635 if [ "$in_output" = "true" ]; then
636 echo "$line"
637 fi
638 ;;
639 esac
640 done
641
642 exec 3<&- 3>&- 2>/dev/null
643 return ${EXIT_CODE:-0}
644}
645
646# Run a container in a memres (persistent) DomU.
647# Hot-plugs an input disk, sends ===RUN_CONTAINER=== command via PTY,
648# reads output, detaches disk.
649# Usage: hv_daemon_run_container <resolved_cmd> <input_disk_path>
650hv_daemon_run_container() {
651 local cmd="$1"
652 local input_disk="$2"
653
654 hv_daemon_load_state
655 if [ -z "$HV_DOMNAME" ]; then
656 log "ERROR" "No memres domain name"
657 return 1
658 fi
659
660 # Hot-plug the input disk as xvdb (read-only)
661 if [ -n "$input_disk" ] && [ -f "$input_disk" ]; then
662 log "INFO" "Hot-plugging container disk to $HV_DOMNAME..."
663 xl block-attach "$HV_DOMNAME" "format=raw,vdev=xvdb,access=ro,target=$input_disk" 2>/dev/null || {
664 log "ERROR" "Failed to attach block device"
665 return 1
666 }
667 sleep 1 # Let the kernel register the device
668 fi
669
670 # Build the command line: ===RUN_CONTAINER===<cmd_b64>
671 local raw_line="===RUN_CONTAINER==="
672 if [ -n "$cmd" ]; then
673 raw_line="${raw_line}$(echo -n "$cmd" | base64 -w0)"
674 fi
675
676 # Send via PTY and read response (same protocol as hv_daemon_send)
677 local pty_file="$DAEMON_SOCKET_DIR/daemon.pty"
678 [ -f "$pty_file" ] || { log "ERROR" "No daemon PTY file"; return 1; }
679 local pty
680 pty=$(cat "$pty_file")
681 [ -c "$pty" ] || { log "ERROR" "PTY $pty not a character device"; return 1; }
682
683 touch "$DAEMON_SOCKET_DIR/activity" 2>/dev/null || true
684
685 exec 3<>"$pty"
686
687 # Drain pending output
688 while IFS= read -t 0.5 -r _discard <&3; do :; done
689
690 # Send command
691 echo "$raw_line" >&3
692
693 # Read response with markers
694 local EXIT_CODE=0
695 local in_output=false
696 local line
697 while IFS= read -t 120 -r line <&3; do
698 line=$(echo "$line" | tr -d '\r')
699 case "$line" in
700 *"===OUTPUT_START==="*)
701 in_output=true
702 ;;
703 *"===OUTPUT_END==="*)
704 in_output=false
705 ;;
706 *"===EXIT_CODE="*"==="*)
707 EXIT_CODE=$(echo "$line" | sed 's/.*===EXIT_CODE=\([0-9]*\)===/\1/')
708 ;;
709 *"===ERROR==="*)
710 in_output=true
711 ;;
712 *"===END==="*)
713 break
714 ;;
715 *)
716 if [ "$in_output" = "true" ]; then
717 echo "$line"
718 fi
719 ;;
720 esac
721 done
722
723 exec 3<&- 3>&- 2>/dev/null
724
725 # Detach the input disk
726 if [ -n "$input_disk" ] && [ -f "$input_disk" ]; then
727 log "DEBUG" "Detaching container disk from $HV_DOMNAME..."
728 xl block-detach "$HV_DOMNAME" xvdb 2>/dev/null || true
729 fi
730
731 return ${EXIT_CODE:-0}
732}
733
527hv_daemon_stop() { 734hv_daemon_stop() {
528 hv_daemon_load_state 735 hv_daemon_load_state
529 if [ -z "$HV_DOMNAME" ]; then 736 if [ -z "$HV_DOMNAME" ]; then
@@ -532,10 +739,15 @@ hv_daemon_stop() {
532 739
533 log "INFO" "Shutting down Xen domain $HV_DOMNAME..." 740 log "INFO" "Shutting down Xen domain $HV_DOMNAME..."
534 741
535 # Send shutdown command via socket first (graceful guest shutdown) 742 # Send shutdown command via PTY (graceful guest shutdown)
536 if [ -S "$DAEMON_SOCKET" ]; then 743 local pty_file="$DAEMON_SOCKET_DIR/daemon.pty"
537 echo "===SHUTDOWN===" | socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true 744 if [ -f "$pty_file" ]; then
538 sleep 2 745 local pty
746 pty=$(cat "$pty_file")
747 if [ -c "$pty" ]; then
748 echo "===SHUTDOWN===" > "$pty" 2>/dev/null || true
749 sleep 2
750 fi
539 fi 751 fi
540 752
541 # Try graceful xl shutdown 753 # Try graceful xl shutdown
@@ -554,11 +766,14 @@ hv_daemon_stop() {
554 xl destroy "$HV_DOMNAME" 2>/dev/null || true 766 xl destroy "$HV_DOMNAME" 2>/dev/null || true
555 fi 767 fi
556 768
557 # Clean up console bridge 769 # Kill monitor process (PID stored in daemon.pid)
558 if [ -n "${_XEN_SOCAT_PID:-}" ]; then 770 local pid_file="$DAEMON_SOCKET_DIR/daemon.pid"
559 kill $_XEN_SOCAT_PID 2>/dev/null || true 771 if [ -f "$pid_file" ]; then
772 local mpid
773 mpid=$(cat "$pid_file" 2>/dev/null)
774 [ -n "$mpid" ] && kill "$mpid" 2>/dev/null || true
560 fi 775 fi
561 776
562 rm -f "$(_xen_domname_file)" 777 rm -f "$(_xen_domname_file)" "$pty_file" "$pid_file"
563 log "INFO" "Xen domain stopped" 778 log "INFO" "Xen domain stopped"
564} 779}
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"
323DAEMON_MODE="" # start, send, stop, status 323DAEMON_MODE="" # start, send, stop, status
324DAEMON_SOCKET_DIR="" # Directory for daemon socket/PID files 324DAEMON_SOCKET_DIR="" # Directory for daemon socket/PID files
325IDLE_TIMEOUT="1800" # Default: 30 minutes 325IDLE_TIMEOUT="1800" # Default: 30 minutes
326EXIT_GRACE_PERIOD="" # Entrypoint exit grace period (vxn)
326 327
327while [ $# -gt 0 ]; do 328while [ $# -gt 0 ]; do
328 case $1 in 329 case $1 in
@@ -443,6 +444,10 @@ while [ $# -gt 0 ]; do
443 DAEMON_MODE="interactive" 444 DAEMON_MODE="interactive"
444 shift 445 shift
445 ;; 446 ;;
447 --daemon-run)
448 DAEMON_MODE="run"
449 shift
450 ;;
446 --daemon-stop) 451 --daemon-stop)
447 DAEMON_MODE="stop" 452 DAEMON_MODE="stop"
448 shift 453 shift
@@ -459,11 +464,20 @@ while [ $# -gt 0 ]; do
459 IDLE_TIMEOUT="$2" 464 IDLE_TIMEOUT="$2"
460 shift 2 465 shift 2
461 ;; 466 ;;
467 --container-name)
468 CONTAINER_NAME="$2"
469 export CONTAINER_NAME
470 shift 2
471 ;;
462 --no-daemon) 472 --no-daemon)
463 # Placeholder for CLI wrapper - vrunner.sh itself doesn't use this 473 # Placeholder for CLI wrapper - vrunner.sh itself doesn't use this
464 # but we accept it so callers can pass it through 474 # but we accept it so callers can pass it through
465 shift 475 shift
466 ;; 476 ;;
477 --exit-grace-period)
478 EXIT_GRACE_PERIOD="$2"
479 shift 2
480 ;;
467 --verbose|-v) 481 --verbose|-v)
468 VERBOSE="true" 482 VERBOSE="true"
469 shift 483 shift
@@ -597,6 +611,12 @@ daemon_send() {
597 exit 1 611 exit 1
598 fi 612 fi
599 613
614 # Use backend-specific send if available (e.g. Xen PTY-based IPC)
615 if type hv_daemon_send >/dev/null 2>&1; then
616 hv_daemon_send "$cmd"
617 return $?
618 fi
619
600 if [ ! -S "$DAEMON_SOCKET" ]; then 620 if [ ! -S "$DAEMON_SOCKET" ]; then
601 log "ERROR" "Daemon socket not found: $DAEMON_SOCKET" 621 log "ERROR" "Daemon socket not found: $DAEMON_SOCKET"
602 exit 1 622 exit 1
@@ -704,6 +724,14 @@ daemon_interactive() {
704 return 1 724 return 1
705 fi 725 fi
706 726
727 # PTY-based backends don't support interactive daemon mode
728 # (file-descriptor polling isn't practical for interactive I/O)
729 if type hv_daemon_send >/dev/null 2>&1; then
730 log "ERROR" "Interactive daemon mode not supported with ${VCONTAINER_HYPERVISOR} backend"
731 log "ERROR" "Use: ${TOOL_NAME} -it --no-daemon run ... for interactive mode"
732 return 1
733 fi
734
707 if [ ! -S "$DAEMON_SOCKET" ]; then 735 if [ ! -S "$DAEMON_SOCKET" ]; then
708 log "ERROR" "Daemon socket not found: $DAEMON_SOCKET" 736 log "ERROR" "Daemon socket not found: $DAEMON_SOCKET"
709 return 1 737 return 1
@@ -1032,6 +1060,25 @@ if [ -n "$INPUT_PATH" ] && [ "$INPUT_TYPE" != "none" ]; then
1032 log "DEBUG" "Input disk: $(ls -lh "$INPUT_IMG" | awk '{print $5}')" 1060 log "DEBUG" "Input disk: $(ls -lh "$INPUT_IMG" | awk '{print $5}')"
1033fi 1061fi
1034 1062
1063# Daemon run mode: try to use memres DomU, fall back to ephemeral
1064# This runs after input disk creation so we have the container disk ready
1065if [ "$DAEMON_MODE" = "run" ]; then
1066 if type hv_daemon_ping >/dev/null 2>&1 && hv_daemon_ping; then
1067 # Memres DomU is responsive — use it
1068 log "INFO" "Memres DomU is idle, dispatching container..."
1069 INPUT_IMG_PATH=""
1070 if [ -n "$DISK_OPTS" ]; then
1071 INPUT_IMG_PATH=$(echo "$DISK_OPTS" | sed -n 's/.*file=\([^,]*\).*/\1/p')
1072 fi
1073 hv_daemon_run_container "$DOCKER_CMD" "$INPUT_IMG_PATH"
1074 exit $?
1075 else
1076 # Memres DomU is occupied or not responding — fall through to ephemeral
1077 log "INFO" "Memres occupied or not responding, using ephemeral mode"
1078 DAEMON_MODE=""
1079 fi
1080fi
1081
1035# Create state disk for persistent storage (--state-dir) 1082# Create state disk for persistent storage (--state-dir)
1036# Xen backend skips this: DomU Docker storage lives in the guest's overlay 1083# Xen backend skips this: DomU Docker storage lives in the guest's overlay
1037# filesystem and persists as long as the domain is running (daemon mode). 1084# filesystem and persists as long as the domain is running (daemon mode).
@@ -1175,6 +1222,11 @@ if [ "$INTERACTIVE" = "true" ]; then
1175 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_interactive=1" 1222 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_interactive=1"
1176fi 1223fi
1177 1224
1225# Exit grace period for entrypoint death detection (vxn)
1226if [ -n "${EXIT_GRACE_PERIOD:-}" ]; then
1227 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_exit_grace=$EXIT_GRACE_PERIOD"
1228fi
1229
1178# Build VM configuration via hypervisor backend 1230# Build VM configuration via hypervisor backend
1179# Drive ordering is important: 1231# Drive ordering is important:
1180# rootfs.img (read-only), input disk (if any), state disk (if any) 1232# rootfs.img (read-only), input disk (if any), state disk (if any)
@@ -1187,15 +1239,15 @@ if [ "$BATCH_IMPORT" = "true" ]; then
1187 BATCH_SHARE_DIR="$TEMP_DIR/share" 1239 BATCH_SHARE_DIR="$TEMP_DIR/share"
1188 mkdir -p "$BATCH_SHARE_DIR" 1240 mkdir -p "$BATCH_SHARE_DIR"
1189 SHARE_TAG="${TOOL_NAME}_share" 1241 SHARE_TAG="${TOOL_NAME}_share"
1190 HV_OPTS="$HV_OPTS $(hv_build_9p_opts "$BATCH_SHARE_DIR" "$SHARE_TAG")" 1242 hv_build_9p_opts "$BATCH_SHARE_DIR" "$SHARE_TAG"
1191 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_9p=1" 1243 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_9p=1"
1192 log "INFO" "Using 9p for fast storage output" 1244 log "INFO" "Using 9p for fast storage output"
1193fi 1245fi
1194 1246
1195# Daemon mode: add serial channel for command I/O 1247# Daemon mode: add serial channel for command I/O
1196if [ "$DAEMON_MODE" = "start" ]; then 1248if [ "$DAEMON_MODE" = "start" ]; then
1197 # Check for required tools 1249 # Check for required tools (socat needed unless backend provides PTY-based IPC)
1198 if ! command -v socat >/dev/null 2>&1; then 1250 if ! type hv_daemon_send >/dev/null 2>&1 && ! command -v socat >/dev/null 2>&1; then
1199 log "ERROR" "Daemon mode requires 'socat' but it is not installed." 1251 log "ERROR" "Daemon mode requires 'socat' but it is not installed."
1200 log "ERROR" "Install with: sudo apt install socat" 1252 log "ERROR" "Install with: sudo apt install socat"
1201 exit 1 1253 exit 1
@@ -1210,15 +1262,6 @@ if [ "$DAEMON_MODE" = "start" ]; then
1210 # Create socket directory 1262 # Create socket directory
1211 mkdir -p "$DAEMON_SOCKET_DIR" 1263 mkdir -p "$DAEMON_SOCKET_DIR"
1212 1264
1213 # Create shared directory for file I/O (9p)
1214 DAEMON_SHARE_DIR="$DAEMON_SOCKET_DIR/share"
1215 mkdir -p "$DAEMON_SHARE_DIR"
1216
1217 # Add 9p for shared directory access
1218 SHARE_TAG="${TOOL_NAME}_share"
1219 HV_OPTS="$HV_OPTS $(hv_build_9p_opts "$DAEMON_SHARE_DIR" "$SHARE_TAG")"
1220 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_9p=1"
1221
1222 # Add daemon command channel (backend-specific: virtio-serial or PV console) 1265 # Add daemon command channel (backend-specific: virtio-serial or PV console)
1223 hv_build_daemon_opts 1266 hv_build_daemon_opts
1224 HV_OPTS="$HV_OPTS $HV_DAEMON_OPTS" 1267 HV_OPTS="$HV_OPTS $HV_DAEMON_OPTS"
@@ -1266,11 +1309,18 @@ if [ "$DAEMON_MODE" = "start" ]; then
1266 1309
1267 log "INFO" "VM started (PID: $HV_VM_PID)" 1310 log "INFO" "VM started (PID: $HV_VM_PID)"
1268 1311
1269 # Wait for socket to appear (container runtime starting) 1312 # Wait for daemon to be ready (backend-specific or socket-based)
1270 log "INFO" "Waiting for daemon to be ready..." 1313 log "INFO" "Waiting for daemon to be ready..."
1271 READY=false 1314 READY=false
1272 for i in $(seq 1 120); do 1315 for i in $(seq 1 120); do
1273 if [ -S "$DAEMON_SOCKET" ]; then 1316 # Backend-specific readiness check (e.g. Xen PTY-based)
1317 if type hv_daemon_ping >/dev/null 2>&1; then
1318 if hv_daemon_ping; then
1319 log "DEBUG" "Got PONG response (backend)"
1320 READY=true
1321 break
1322 fi
1323 elif [ -S "$DAEMON_SOCKET" ]; then
1274 RESPONSE=$( { echo "===PING==="; sleep 3; } | timeout 10 socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true) 1324 RESPONSE=$( { echo "===PING==="; sleep 3; } | timeout 10 socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true)
1275 if echo "$RESPONSE" | grep -q "===PONG==="; then 1325 if echo "$RESPONSE" | grep -q "===PONG==="; then
1276 log "DEBUG" "Got PONG response" 1326 log "DEBUG" "Got PONG response"
@@ -1356,7 +1406,7 @@ if [ -n "$CA_CERT" ] && [ -f "$CA_CERT" ]; then
1356 cp "$CA_CERT" "$CA_SHARE_DIR/ca.crt" 1406 cp "$CA_CERT" "$CA_SHARE_DIR/ca.crt"
1357 1407
1358 SHARE_TAG="${TOOL_NAME}_share" 1408 SHARE_TAG="${TOOL_NAME}_share"
1359 HV_OPTS="$HV_OPTS $(hv_build_9p_opts "$CA_SHARE_DIR" "$SHARE_TAG" "readonly=on")" 1409 hv_build_9p_opts "$CA_SHARE_DIR" "$SHARE_TAG" "readonly=on"
1360 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_9p=1" 1410 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_9p=1"
1361 log "DEBUG" "CA certificate available via 9p" 1411 log "DEBUG" "CA certificate available via 9p"
1362fi 1412fi
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 @@
22# docker_output=<type> Output type: text (default: text) 22# docker_output=<type> Output type: text (default: text)
23# docker_network=1 Enable networking 23# docker_network=1 Enable networking
24# docker_interactive=1 Interactive mode (suppress boot messages) 24# docker_interactive=1 Interactive mode (suppress boot messages)
25# docker_daemon=1 Daemon mode (command loop on hvc1) 25# docker_daemon=1 Daemon mode (command loop on hvc0 via serial PTY)
26# docker_exit_grace=<s> Grace period (seconds) after entrypoint exits [default: 300]
26# 27#
27# Version: 1.0.0 28# Version: 1.2.0
28 29
29# Set runtime-specific parameters before sourcing common code 30# Set runtime-specific parameters before sourcing common code
30VCONTAINER_RUNTIME_NAME="vxn" 31VCONTAINER_RUNTIME_NAME="vxn"
@@ -32,7 +33,7 @@ VCONTAINER_RUNTIME_CMD="chroot"
32VCONTAINER_RUNTIME_PREFIX="docker" 33VCONTAINER_RUNTIME_PREFIX="docker"
33VCONTAINER_STATE_DIR="/var/lib/vxn" 34VCONTAINER_STATE_DIR="/var/lib/vxn"
34VCONTAINER_SHARE_NAME="vxn_share" 35VCONTAINER_SHARE_NAME="vxn_share"
35VCONTAINER_VERSION="1.0.0" 36VCONTAINER_VERSION="1.2.0"
36 37
37# Source common init functions 38# Source common init functions
38. /vcontainer-init-common.sh 39. /vcontainer-init-common.sh
@@ -344,7 +345,7 @@ exec_in_container() {
344 export TERM=linux 345 export TERM=linux
345 printf '\r\033[K' 346 printf '\r\033[K'
346 if [ "$use_sh" = "true" ]; then 347 if [ "$use_sh" = "true" ]; then
347 chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; exec $cmd" 348 chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; $cmd"
348 else 349 else
349 chroot "$rootfs" $cmd 350 chroot "$rootfs" $cmd
350 fi 351 fi
@@ -354,7 +355,7 @@ exec_in_container() {
354 EXEC_OUTPUT="/tmp/container_output.txt" 355 EXEC_OUTPUT="/tmp/container_output.txt"
355 EXEC_EXIT_CODE=0 356 EXEC_EXIT_CODE=0
356 if [ "$use_sh" = "true" ]; then 357 if [ "$use_sh" = "true" ]; then
357 chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; exec $cmd" \ 358 chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; $cmd" \
358 > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$? 359 > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$?
359 else 360 else
360 chroot "$rootfs" $cmd \ 361 chroot "$rootfs" $cmd \
@@ -375,38 +376,158 @@ exec_in_container() {
375 umount "$rootfs/dev" 2>/dev/null || true 376 umount "$rootfs/dev" 2>/dev/null || true
376} 377}
377 378
379# Execute a command inside the container rootfs in the background.
380# Used by detached mode: start entrypoint, then enter daemon loop.
381exec_in_container_background() {
382 local rootfs="$1"
383 local cmd="$2"
384 local workdir="${OCI_WORKDIR:-/}"
385
386 # Mount essential filesystems inside the container rootfs
387 mkdir -p "$rootfs/proc" "$rootfs/sys" "$rootfs/dev" "$rootfs/tmp" 2>/dev/null || true
388 mount -t proc proc "$rootfs/proc" 2>/dev/null || true
389 mount -t sysfs sysfs "$rootfs/sys" 2>/dev/null || true
390 mount --bind /dev "$rootfs/dev" 2>/dev/null || true
391
392 # Copy resolv.conf for DNS
393 if [ -f /etc/resolv.conf ]; then
394 mkdir -p "$rootfs/etc" 2>/dev/null || true
395 cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || true
396 fi
397
398 # Run in background, save PID
399 # Note: no 'exec' — compound commands (&&, ||, ;) need the wrapper shell
400 chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; $cmd" > /tmp/entrypoint.log 2>&1 &
401 ENTRYPOINT_PID=$!
402 echo "$ENTRYPOINT_PID" > /tmp/entrypoint.pid
403 log "Entrypoint PID: $ENTRYPOINT_PID"
404}
405
378# ============================================================================ 406# ============================================================================
379# Daemon Mode (vxn-specific) 407# Memres: Run Container from Hot-Plugged Disk
380# ============================================================================ 408# ============================================================================
381 409
382# In daemon mode, commands come via the hvc1 console channel 410# Run a container from a hot-plugged /dev/xvdb block device.
383# and are executed in the container rootfs via chroot. 411# Called by the daemon loop in response to ===RUN_CONTAINER=== command.
384run_vxn_daemon_mode() { 412# Mounts the device, finds rootfs, executes entrypoint, unmounts.
385 log "=== vxn Daemon Mode ===" 413# Output follows the daemon command protocol (===OUTPUT_START/END/EXIT_CODE/END===).
386 log "Container rootfs: ${CONTAINER_ROOT:-(none)}" 414run_container_from_disk() {
387 log "Idle timeout: ${RUNTIME_IDLE_TIMEOUT}s" 415 local user_cmd="$1"
388 416
389 # Find the command channel (prefer hvc1 for Xen) 417 # Wait for hot-plugged block device
390 DAEMON_PORT="" 418 log "Waiting for input device..."
391 for port in /dev/hvc1 /dev/vport0p1 /dev/vport1p1 /dev/virtio-ports/vxn; do 419 local found=false
392 if [ -c "$port" ]; then 420 for i in $(seq 1 30); do
393 DAEMON_PORT="$port" 421 if [ -b /dev/xvdb ]; then
394 log "Found command channel: $port" 422 found=true
395 break 423 break
396 fi 424 fi
425 sleep 0.5
397 done 426 done
398 427
399 if [ -z "$DAEMON_PORT" ]; then 428 if [ "$found" != "true" ]; then
400 log "ERROR: No command channel for daemon mode" 429 echo "===ERROR==="
401 ls -la /dev/hvc* /dev/vport* /dev/virtio-ports/ 2>/dev/null || true 430 echo "Input device /dev/xvdb not found after 15s"
402 sleep 5 431 echo "===END==="
403 reboot -f 432 return
404 fi 433 fi
405 434
406 # Open bidirectional FD 435 # Mount input disk
407 exec 3<>"$DAEMON_PORT" 436 mkdir -p /mnt/input
437 if ! mount /dev/xvdb /mnt/input 2>/dev/null; then
438 echo "===ERROR==="
439 echo "Failed to mount /dev/xvdb"
440 echo "===END==="
441 return
442 fi
443 log "Input disk mounted"
408 444
409 log "Daemon ready, waiting for commands..." 445 # Find container rootfs
446 if ! find_container_rootfs; then
447 echo "===ERROR==="
448 echo "No container rootfs found on input disk"
449 echo "===END==="
450 umount /mnt/input 2>/dev/null || true
451 return
452 fi
453
454 # Parse OCI config for entrypoint/env/workdir
455 parse_oci_config
456 setup_container_env
457
458 # Determine command: user-supplied takes priority, then OCI config
459 if [ -n "$user_cmd" ]; then
460 RUNTIME_CMD="$user_cmd"
461 else
462 RUNTIME_CMD=""
463 fi
464 local exec_cmd
465 exec_cmd=$(determine_exec_command)
466
467 log "Executing container: $exec_cmd"
468
469 # Execute in container rootfs (blocking)
470 local rootfs="$CONTAINER_ROOT"
471 local workdir="${OCI_WORKDIR:-/}"
472 local exit_code=0
473
474 # Mount essential filesystems inside container
475 mkdir -p "$rootfs/proc" "$rootfs/sys" "$rootfs/dev" "$rootfs/tmp" 2>/dev/null || true
476 mount -t proc proc "$rootfs/proc" 2>/dev/null || true
477 mount -t sysfs sysfs "$rootfs/sys" 2>/dev/null || true
478 mount --bind /dev "$rootfs/dev" 2>/dev/null || true
479 [ -f /etc/resolv.conf ] && {
480 mkdir -p "$rootfs/etc" 2>/dev/null || true
481 cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || true
482 }
483
484 local output_file="/tmp/container_run_output.txt"
485 if [ -x "$rootfs/bin/sh" ]; then
486 chroot "$rootfs" /bin/sh -c "cd '$workdir' 2>/dev/null; $exec_cmd" \
487 > "$output_file" 2>&1 || exit_code=$?
488 else
489 chroot "$rootfs" $exec_cmd \
490 > "$output_file" 2>&1 || exit_code=$?
491 fi
492
493 echo "===OUTPUT_START==="
494 cat "$output_file"
495 echo "===OUTPUT_END==="
496 echo "===EXIT_CODE=$exit_code==="
497
498 # Clean up container mounts
499 umount "$rootfs/proc" 2>/dev/null || true
500 umount "$rootfs/sys" 2>/dev/null || true
501 umount "$rootfs/dev" 2>/dev/null || true
502 umount /mnt/input 2>/dev/null || true
503 rm -f "$output_file"
504
505 # Reset container state for next run
506 CONTAINER_ROOT=""
507 OCI_ENTRYPOINT=""
508 OCI_CMD=""
509 OCI_ENV=""
510 OCI_WORKDIR=""
511 RUNTIME_CMD=""
512 # Clean up extracted OCI rootfs (if any)
513 rm -rf /mnt/container 2>/dev/null || true
514
515 echo "===END==="
516 log "Container finished (exit code: $exit_code)"
517}
518
519# ============================================================================
520# Daemon Mode (vxn-specific)
521# ============================================================================
522
523# Daemon mode: command loop on hvc0 (stdin/stdout).
524# The host bridges the domain's console PTY to a Unix socket via socat.
525# Commands arrive as base64-encoded lines on stdin, responses go to stdout.
526# This is the same model runx used (serial='pty' + serial_start).
527run_vxn_daemon_mode() {
528 log "=== vxn Daemon Mode ==="
529 log "Container rootfs: ${CONTAINER_ROOT:-(none)}"
530 log "Idle timeout: ${RUNTIME_IDLE_TIMEOUT}s"
410 531
411 ACTIVITY_FILE="/tmp/.daemon_activity" 532 ACTIVITY_FILE="/tmp/.daemon_activity"
412 touch "$ACTIVITY_FILE" 533 touch "$ACTIVITY_FILE"
@@ -415,10 +536,17 @@ run_vxn_daemon_mode() {
415 trap 'log "Shutdown signal"; sync; reboot -f' TERM 536 trap 'log "Shutdown signal"; sync; reboot -f' TERM
416 trap 'rm -f "$ACTIVITY_FILE"; exit' INT 537 trap 'rm -f "$ACTIVITY_FILE"; exit' INT
417 538
418 # Command loop 539 log "Using hvc0 console for daemon IPC"
540 log "Daemon ready, waiting for commands..."
541
542 # Emit readiness marker so the host can detect daemon is ready
543 # without needing to send PING first (host reads PTY for this)
544 echo "===PONG==="
545
546 # Command loop: read from stdin (hvc0), write to stdout (hvc0)
419 while true; do 547 while true; do
420 CMD_B64="" 548 CMD_B64=""
421 read -r CMD_B64 <&3 549 read -r CMD_B64
422 READ_EXIT=$? 550 READ_EXIT=$?
423 551
424 if [ $READ_EXIT -eq 0 ] && [ -n "$CMD_B64" ]; then 552 if [ $READ_EXIT -eq 0 ] && [ -n "$CMD_B64" ]; then
@@ -426,20 +554,41 @@ run_vxn_daemon_mode() {
426 554
427 case "$CMD_B64" in 555 case "$CMD_B64" in
428 "===PING===") 556 "===PING===")
429 echo "===PONG===" | cat >&3 557 echo "===PONG==="
558 continue
559 ;;
560 "===STATUS===")
561 if [ -f /tmp/entrypoint.exit_code ]; then
562 echo "===EXITED=$(cat /tmp/entrypoint.exit_code)==="
563 else
564 echo "===RUNNING==="
565 fi
430 continue 566 continue
431 ;; 567 ;;
432 "===SHUTDOWN===") 568 "===SHUTDOWN===")
433 log "Received shutdown command" 569 log "Received shutdown command"
434 echo "===SHUTTING_DOWN===" | cat >&3 570 echo "===SHUTTING_DOWN==="
435 break 571 break
436 ;; 572 ;;
573 "===RUN_CONTAINER==="*)
574 # Memres: run a container from a hot-plugged disk
575 _rc_cmd_b64="${CMD_B64#===RUN_CONTAINER===}"
576 _rc_cmd=""
577 if [ -n "$_rc_cmd_b64" ]; then
578 _rc_cmd=$(echo "$_rc_cmd_b64" | base64 -d 2>/dev/null)
579 fi
580 log "RUN_CONTAINER: cmd='$_rc_cmd'"
581 run_container_from_disk "$_rc_cmd"
582 continue
583 ;;
437 esac 584 esac
438 585
439 # Decode command 586 # Decode command
440 CMD=$(echo "$CMD_B64" | base64 -d 2>/dev/null) 587 CMD=$(echo "$CMD_B64" | base64 -d 2>/dev/null)
441 if [ -z "$CMD" ]; then 588 if [ -z "$CMD" ]; then
442 printf "===ERROR===\nFailed to decode command\n===END===\n" | cat >&3 589 echo "===ERROR==="
590 echo "Failed to decode command"
591 echo "===END==="
443 continue 592 continue
444 fi 593 fi
445 594
@@ -455,13 +604,11 @@ run_vxn_daemon_mode() {
455 eval "$CMD" > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$? 604 eval "$CMD" > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$?
456 fi 605 fi
457 606
458 { 607 echo "===OUTPUT_START==="
459 echo "===OUTPUT_START===" 608 cat "$EXEC_OUTPUT"
460 cat "$EXEC_OUTPUT" 609 echo "===OUTPUT_END==="
461 echo "===OUTPUT_END===" 610 echo "===EXIT_CODE=$EXEC_EXIT_CODE==="
462 echo "===EXIT_CODE=$EXEC_EXIT_CODE===" 611 echo "===END==="
463 echo "===END==="
464 } | cat >&3
465 612
466 log "Command completed (exit code: $EXEC_EXIT_CODE)" 613 log "Command completed (exit code: $EXEC_EXIT_CODE)"
467 else 614 else
@@ -469,7 +616,6 @@ run_vxn_daemon_mode() {
469 fi 616 fi
470 done 617 done
471 618
472 exec 3>&-
473 log "Daemon shutting down..." 619 log "Daemon shutting down..."
474} 620}
475 621
@@ -494,6 +640,15 @@ setup_cgroups
494# Parse kernel command line 640# Parse kernel command line
495parse_cmdline 641parse_cmdline
496 642
643# Parse vxn-specific kernel parameters
644ENTRYPOINT_GRACE_PERIOD="300"
645for param in $(cat /proc/cmdline); do
646 case "$param" in
647 docker_exit_grace=*) ENTRYPOINT_GRACE_PERIOD="${param#docker_exit_grace=}" ;;
648 esac
649done
650log "Entrypoint grace period: ${ENTRYPOINT_GRACE_PERIOD}s"
651
497# Detect and configure disks 652# Detect and configure disks
498detect_disks 653detect_disks
499 654
@@ -525,6 +680,33 @@ parse_oci_config
525setup_container_env 680setup_container_env
526 681
527if [ "$RUNTIME_DAEMON" = "1" ]; then 682if [ "$RUNTIME_DAEMON" = "1" ]; then
683 # If we also have a command, run it in background first (detached container)
684 if [ -n "$RUNTIME_CMD" ] && [ "$RUNTIME_CMD" != "1" ]; then
685 EXEC_CMD=$(determine_exec_command)
686 if [ -n "$EXEC_CMD" ] && [ -n "$CONTAINER_ROOT" ]; then
687 log "Starting entrypoint in background: $EXEC_CMD"
688 exec_in_container_background "$CONTAINER_ROOT" "$EXEC_CMD"
689
690 # Monitor entrypoint: when it exits, record exit code and
691 # schedule DomU shutdown after grace period.
692 # During the grace period, exec and logs still work.
693 if [ -n "$ENTRYPOINT_PID" ]; then
694 (
695 wait $ENTRYPOINT_PID 2>/dev/null
696 EP_EXIT=$?
697 echo "$EP_EXIT" > /tmp/entrypoint.exit_code
698 log "Entrypoint exited (code: $EP_EXIT), grace period: ${ENTRYPOINT_GRACE_PERIOD}s"
699 if [ "$ENTRYPOINT_GRACE_PERIOD" -gt 0 ] 2>/dev/null; then
700 sleep "$ENTRYPOINT_GRACE_PERIOD"
701 fi
702 log "Grace period expired, shutting down"
703 reboot -f
704 ) &
705 ENTRYPOINT_MONITOR_PID=$!
706 log "Entrypoint monitor started (PID: $ENTRYPOINT_MONITOR_PID)"
707 fi
708 fi
709 fi
528 run_vxn_daemon_mode 710 run_vxn_daemon_mode
529else 711else
530 # Determine command to execute 712 # Determine command to execute