diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-17 13:27:23 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-26 01:05:01 +0000 |
| commit | fa4b171a436559787cfcebd4046a1354a1f5cacf (patch) | |
| tree | b974e470795e2715f45a6d666206118194f5711a | |
| parent | dec4b4dfc719e095c0b3dcbff638896282c389af (diff) | |
| download | meta-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-x | recipes-containers/vcontainer/files/vcontainer-common.sh | 433 | ||||
| -rw-r--r-- | recipes-containers/vcontainer/files/vrunner-backend-xen.sh | 301 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vrunner.sh | 80 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vxn-init.sh | 262 |
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 | ||
| 1397 | vxn_container_dir() { echo "$HOME/.vxn/containers/$1"; } | ||
| 1398 | |||
| 1399 | vxn_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" | ||
| 1408 | vxn_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 | ||
| 1455 | vxn_unsupported() { | ||
| 1456 | echo "${VCONTAINER_RUNTIME_NAME}: '$1' is not supported (VM is the container, no runtime inside)" >&2 | ||
| 1457 | exit 1 | ||
| 1458 | } | ||
| 1459 | vxn_not_yet() { | ||
| 1460 | echo "${VCONTAINER_RUNTIME_NAME}: '$1' is not yet supported" >&2 | ||
| 1461 | exit 1 | ||
| 1462 | } | ||
| 1463 | |||
| 1389 | # Handle commands | 1464 | # Handle commands |
| 1390 | case "$COMMAND" in | 1465 | case "$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() { | |||
| 245 | hv_build_9p_opts() { | 265 | hv_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 | ||
| 255 | hv_build_daemon_opts() { | 274 | hv_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 | ||
| 266 | hv_build_vm_cmd() { | 281 | hv_build_vm_cmd() { |
| @@ -316,6 +331,8 @@ extra = "console=hvc0 quiet loglevel=0 init=/init vcontainer.blk=xvd vcontainer. | |||
| 316 | disk = [ $disk_array ] | 331 | disk = [ $disk_array ] |
| 317 | vif = [ $vif_array ] | 332 | vif = [ $vif_array ] |
| 318 | 333 | ||
| 334 | serial = 'pty' | ||
| 335 | |||
| 319 | on_poweroff = "destroy" | 336 | on_poweroff = "destroy" |
| 320 | on_reboot = "destroy" | 337 | on_reboot = "destroy" |
| 321 | on_crash = "destroy" | 338 | on_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 | ||
| 423 | hv_get_vm_id() { | 455 | hv_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. | ||
| 562 | hv_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. | ||
| 591 | hv_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> | ||
| 650 | hv_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 | |||
| 527 | hv_daemon_stop() { | 734 | hv_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" | |||
| 323 | DAEMON_MODE="" # start, send, stop, status | 323 | DAEMON_MODE="" # start, send, stop, status |
| 324 | DAEMON_SOCKET_DIR="" # Directory for daemon socket/PID files | 324 | DAEMON_SOCKET_DIR="" # Directory for daemon socket/PID files |
| 325 | IDLE_TIMEOUT="1800" # Default: 30 minutes | 325 | IDLE_TIMEOUT="1800" # Default: 30 minutes |
| 326 | EXIT_GRACE_PERIOD="" # Entrypoint exit grace period (vxn) | ||
| 326 | 327 | ||
| 327 | while [ $# -gt 0 ]; do | 328 | while [ $# -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}')" |
| 1033 | fi | 1061 | fi |
| 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 | ||
| 1065 | if [ "$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 | ||
| 1080 | fi | ||
| 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" |
| 1176 | fi | 1223 | fi |
| 1177 | 1224 | ||
| 1225 | # Exit grace period for entrypoint death detection (vxn) | ||
| 1226 | if [ -n "${EXIT_GRACE_PERIOD:-}" ]; then | ||
| 1227 | KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_exit_grace=$EXIT_GRACE_PERIOD" | ||
| 1228 | fi | ||
| 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" |
| 1193 | fi | 1245 | fi |
| 1194 | 1246 | ||
| 1195 | # Daemon mode: add serial channel for command I/O | 1247 | # Daemon mode: add serial channel for command I/O |
| 1196 | if [ "$DAEMON_MODE" = "start" ]; then | 1248 | if [ "$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" |
| 1362 | fi | 1412 | 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 @@ | |||
| 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 |
| 30 | VCONTAINER_RUNTIME_NAME="vxn" | 31 | VCONTAINER_RUNTIME_NAME="vxn" |
| @@ -32,7 +33,7 @@ VCONTAINER_RUNTIME_CMD="chroot" | |||
| 32 | VCONTAINER_RUNTIME_PREFIX="docker" | 33 | VCONTAINER_RUNTIME_PREFIX="docker" |
| 33 | VCONTAINER_STATE_DIR="/var/lib/vxn" | 34 | VCONTAINER_STATE_DIR="/var/lib/vxn" |
| 34 | VCONTAINER_SHARE_NAME="vxn_share" | 35 | VCONTAINER_SHARE_NAME="vxn_share" |
| 35 | VCONTAINER_VERSION="1.0.0" | 36 | VCONTAINER_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. | ||
| 381 | exec_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. |
| 384 | run_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)}" | 414 | run_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). | ||
| 527 | run_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 |
| 495 | parse_cmdline | 641 | parse_cmdline |
| 496 | 642 | ||
| 643 | # Parse vxn-specific kernel parameters | ||
| 644 | ENTRYPOINT_GRACE_PERIOD="300" | ||
| 645 | for param in $(cat /proc/cmdline); do | ||
| 646 | case "$param" in | ||
| 647 | docker_exit_grace=*) ENTRYPOINT_GRACE_PERIOD="${param#docker_exit_grace=}" ;; | ||
| 648 | esac | ||
| 649 | done | ||
| 650 | log "Entrypoint grace period: ${ENTRYPOINT_GRACE_PERIOD}s" | ||
| 651 | |||
| 497 | # Detect and configure disks | 652 | # Detect and configure disks |
| 498 | detect_disks | 653 | detect_disks |
| 499 | 654 | ||
| @@ -525,6 +680,33 @@ parse_oci_config | |||
| 525 | setup_container_env | 680 | setup_container_env |
| 526 | 681 | ||
| 527 | if [ "$RUNTIME_DAEMON" = "1" ]; then | 682 | if [ "$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 |
| 529 | else | 711 | else |
| 530 | # Determine command to execute | 712 | # Determine command to execute |
