diff --git a/terrain-intelligence-generator/docker/Dockerfile b/terrain-intelligence-generator/docker/Dockerfile index d762c4c..9972c14 100644 --- a/terrain-intelligence-generator/docker/Dockerfile +++ b/terrain-intelligence-generator/docker/Dockerfile @@ -294,6 +294,9 @@ RUN ARCH_DIR="x86-64-linx" && \ echo "export VICSYS=DEVELOPMENT" >> /usr/local/bin/$cmdname; \ echo "export LD_LIBRARY_PATH=$VICAR_LIB_PATH:\$LD_LIBRARY_PATH" >> /usr/local/bin/$cmdname; \ echo "export PATH=${V2TOP}/gui/lib/$ARCH_DIR:${V2TOP}/mars/lib/$ARCH_DIR:${V2TOP}/p2/lib/$ARCH_DIR:\$PATH" >> /usr/local/bin/$cmdname; \ + echo "# X11 app-defaults (.xres) so Motif widget labels resolve instead of showing placeholder resource names" >> /usr/local/bin/$cmdname; \ + echo "export XFILESEARCHPATH=\"${V2TOP}/gui/lib/$ARCH_DIR/%N%S.xres:${V2TOP}/p2/lib/$ARCH_DIR/%N%S.xres:${V2TOP}/mars/lib/$ARCH_DIR/%N%S.xres:/usr/lib/X11/%L/%T/%N%C%S:/usr/lib/X11/%l/%T/%N%C%S:/usr/lib/X11/%T/%N%C%S:/usr/lib/X11/%L/%T/%N%S:/usr/lib/X11/%l/%T/%N%S:/usr/lib/X11/%T/%N%S\${XFILESEARCHPATH:+:\$XFILESEARCHPATH}\"" >> /usr/local/bin/$cmdname; \ + echo "export XBMLANGPATH=\"%D:${V2TOP}/p2/lib/$ARCH_DIR/%B\"" >> /usr/local/bin/$cmdname; \ echo "$cmd \"\$@\"" >> /usr/local/bin/$cmdname; \ echo "exit_code=\$?" >> /usr/local/bin/$cmdname; \ echo "# VICAR commands often return non-zero on success, treat 0-2 as success" >> /usr/local/bin/$cmdname; \ @@ -319,6 +322,9 @@ RUN ARCH_DIR="x86-64-linx" && \ echo "export VICSYS=DEVELOPMENT" >> /etc/profile.d/vicar.sh && \ echo "export LD_LIBRARY_PATH=$VICAR_LIB_PATH:\$LD_LIBRARY_PATH" >> /etc/profile.d/vicar.sh && \ echo "export PATH=${V2TOP}/tae53/bin:${V2TOP}/bin:\$PATH" >> /etc/profile.d/vicar.sh && \ + echo "# X11 app-defaults (.xres) so Motif GUI (xvd) widget labels resolve instead of showing placeholder resource names" >> /etc/profile.d/vicar.sh && \ + echo "export XFILESEARCHPATH=\"${V2TOP}/gui/lib/$ARCH_DIR/%N%S.xres:${V2TOP}/p2/lib/$ARCH_DIR/%N%S.xres:${V2TOP}/mars/lib/$ARCH_DIR/%N%S.xres:/usr/lib/X11/%L/%T/%N%C%S:/usr/lib/X11/%l/%T/%N%C%S:/usr/lib/X11/%T/%N%C%S:/usr/lib/X11/%L/%T/%N%S:/usr/lib/X11/%l/%T/%N%S:/usr/lib/X11/%T/%N%S\${XFILESEARCHPATH:+:\$XFILESEARCHPATH}\"" >> /etc/profile.d/vicar.sh && \ + echo "export XBMLANGPATH=\"%D:${V2TOP}/p2/lib/$ARCH_DIR/%B\"" >> /etc/profile.d/vicar.sh && \ chmod +x /etc/profile.d/vicar.sh # Verify VICAR installation diff --git a/vicar-native-toolkit/.envrc b/vicar-native-toolkit/.envrc index 688a6aa..2d1be80 100644 --- a/vicar-native-toolkit/.envrc +++ b/vicar-native-toolkit/.envrc @@ -18,8 +18,15 @@ fi # Disable SELinux label enforcement on the container (Linux only). # May be required for images that load certain 32-bit legacy shared # libraries, which can trigger "cannot change memory protections" under -# SELinux. Opt-in; default keeps standard labeling. Set -# DISABLE_SELINUX_LABEL=true in .envrc.local when needed. +# SELinux, and is required for the container to access the host X11 socket +# (/tmp/.X11-unix) when SELinux is Enforcing. Opt-in; default keeps standard +# labeling. Set DISABLE_SELINUX_LABEL=true in .envrc.local to force it. +# When left unset, the toolkit auto-enables it on SELinux-Enforcing Linux hosts. +if [[ -z "${DISABLE_SELINUX_LABEL+x}" ]]; then + DISABLE_SELINUX_LABEL_EXPLICIT=false +else + DISABLE_SELINUX_LABEL_EXPLICIT=true +fi : ${DISABLE_SELINUX_LABEL:=false} # VICAR paths inside container (for terrain-intelligence-generator:opensource) @@ -40,6 +47,73 @@ log_error() { echo "[vicar-toolkit] ERROR: $1" >&2 } +# ===== X11 / Display Readiness ===== +# Ensures the host X server will accept connections from the container, so GUI +# tools (xvd) don't fail with "Authorization required, but no authorization +# protocol specified" / "Can't open display". Runs on every activation because +# xhost ACLs and XQuartz state do not persist across restarts/reboots. +ensure_x11_ready() { + if [[ "${HOST_OS}" == "Darwin" ]]; then + # macOS: container reaches XQuartz over TCP via host.docker.internal:0. + # Requires XQuartz running, TCP listening (nolisten_tcp=false), and the + # host to authorize localhost (xhost +localhost). + if ! command -v xhost >/dev/null 2>&1; then + log_info "XQuartz/xhost not found. Install with: brew install --cask xquartz (GUI tools will not work until then)" + return 0 + fi + + # Ensure XQuartz allows TCP connections (persisted preference). + if command -v defaults >/dev/null 2>&1; then + if [[ "$(defaults read org.xquartz.X11 nolisten_tcp 2>/dev/null)" != "0" ]]; then + defaults write org.xquartz.X11 nolisten_tcp -bool false 2>/dev/null || true + log_info "Set XQuartz to allow TCP connections (restart XQuartz if GUI still fails)" + fi + fi + + # Start XQuartz if not running. + if ! pgrep -q -x Xquartz && ! pgrep -q -x X11.bin 2>/dev/null; then + log_info "Starting XQuartz..." + open -a XQuartz 2>/dev/null || log_info "Could not auto-start XQuartz; run: open -a XQuartz" + # Give XQuartz a moment to bind the TCP socket before xhost. + local _tries=0 + while ! pgrep -q -x Xquartz && [[ ${_tries} -lt 10 ]]; do + sleep 0.5 + ((_tries++)) + done + fi + + # Authorize localhost so the container's TCP connection is accepted. + if DISPLAY=:0 xhost +localhost >/dev/null 2>&1; then + log_info "X11 authorized (xhost +localhost)" + else + log_info "Could not run 'xhost +localhost' yet. If GUI fails, ensure XQuartz is running then run: xhost +localhost && toolkit-restart" + fi + else + # Linux: container shares the X11 unix socket via --network host. + # Authorize local (non-network) connections to the display. Use the + # broad "+local:" form: SELinux label=disable makes the container + # connect as the "LOCAL:" family, which "+local:docker" does NOT cover. + if command -v xhost >/dev/null 2>&1 && [[ -n "${DISPLAY}" ]]; then + xhost +local: >/dev/null 2>&1 || true + fi + + # SELinux Enforcing blocks the container (container_t) from the host + # X11 socket (/tmp/.X11-unix), so xvd fails with a misleading + # "Can't open display". Auto-enable label=disable unless the user has + # explicitly configured DISABLE_SELINUX_LABEL themselves. + if command -v getenforce >/dev/null 2>&1 && [[ "$(getenforce 2>/dev/null)" == "Enforcing" ]]; then + if [[ "${DISABLE_SELINUX_LABEL_EXPLICIT}" != "true" ]]; then + if [[ "${DISABLE_SELINUX_LABEL}" != "true" ]]; then + DISABLE_SELINUX_LABEL=true + log_info "SELinux is Enforcing: enabling container '--security-opt label=disable' so GUI tools can reach the X11 socket (set DISABLE_SELINUX_LABEL=false in .envrc.local to override)" + fi + elif [[ "${DISABLE_SELINUX_LABEL}" != "true" ]]; then + log_info "SELinux is Enforcing and DISABLE_SELINUX_LABEL=false: GUI tools (xvd) may fail with 'Can't open display'. Set DISABLE_SELINUX_LABEL=true in .envrc.local if so." + fi + fi + fi +} + # ===== Container Management ===== ensure_container_running() { if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then @@ -156,8 +230,23 @@ else REL_PATH="." fi +# X11 app-defaults so Motif GUI (xvd) widget labels resolve to real text +# instead of placeholder resource names. Passed at exec time so this works +# even on images built before the in-image fix. %N=class (e.g. XVd), +# %S=suffix -> resolves \${GUILIB}/XVd.xres. Runtime value is a fallback: +# if the container already sets XFILESEARCHPATH, that is preserved (appended). +VICAR_GUI_LIB="${VICAR_INSTALL_PREFIX}/gui/lib/x86-64-linx" +VICAR_P2_LIB="${VICAR_INSTALL_PREFIX}/p2/lib/x86-64-linx" +VICAR_MARS_LIB="${VICAR_INSTALL_PREFIX}/mars/lib/x86-64-linx" +X11_DEFAULTS="/usr/lib/X11/%L/%T/%N%C%S:/usr/lib/X11/%l/%T/%N%C%S:/usr/lib/X11/%T/%N%C%S:/usr/lib/X11/%L/%T/%N%S:/usr/lib/X11/%l/%T/%N%S:/usr/lib/X11/%T/%N%S" +XFSP="\${VICAR_GUI_LIB}/%N%S.xres:\${VICAR_P2_LIB}/%N%S.xres:\${VICAR_MARS_LIB}/%N%S.xres:\${X11_DEFAULTS}" +XBMLP="%D:\${VICAR_P2_LIB}/%B" + # Execute command in container at correct working directory -exec docker exec -w "/workspace/\${REL_PATH}" "\${CONTAINER_NAME}" "\${TOOL_NAME}" "\$@" +exec docker exec \\ + -e "XFILESEARCHPATH=\${XFSP}" \\ + -e "XBMLANGPATH=\${XBMLP}" \\ + -w "/workspace/\${REL_PATH}" "\${CONTAINER_NAME}" "\${TOOL_NAME}" "\$@" VICAR_EXEC_EOF chmod +x "${exec_script}" @@ -232,10 +321,11 @@ create_utility_commands() { cat > "${utils_script}" << UTILS_HEADER_EOF #!/bin/bash # Toolkit utility commands handler -# Handles: toolkit-shell, toolkit-stop, toolkit-restart, toolkit-status +# Handles: toolkit-shell, toolkit-stop, toolkit-restart, toolkit-update, toolkit-status UTIL_NAME="\$(basename "\$0")" CONTAINER_NAME="${CONTAINER_NAME}" +CONTAINER_IMAGE="${CONTAINER_IMAGE}" UTILS_HEADER_EOF cat >> "${utils_script}" << 'TOOLKIT_UTILS_EOF' @@ -253,6 +343,28 @@ case "$UTIL_NAME" in echo "Container stopped. Re-enter directory to restart." ;; + toolkit-update) + echo "Pulling latest image: ${CONTAINER_IMAGE}" + old_id="$(docker image inspect "${CONTAINER_IMAGE}" --format '{{.Id}}' 2>/dev/null || true)" + if ! docker pull --platform linux/amd64 "${CONTAINER_IMAGE}"; then + echo "Error: docker pull failed for ${CONTAINER_IMAGE}" >&2 + exit 1 + fi + new_id="$(docker image inspect "${CONTAINER_IMAGE}" --format '{{.Id}}' 2>/dev/null || true)" + if [[ "${old_id}" == "${new_id}" ]]; then + echo "Image already up to date (${new_id#sha256:})." + else + echo "Image updated: ${old_id#sha256:} -> ${new_id#sha256:}" + fi + if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true + docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true + echo "Old container removed. Re-enter directory to recreate from the new image." + else + echo "No running container. Re-enter directory to create one from the new image." + fi + ;; + toolkit-status) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" echo "VICAR Native Toolkit Status:" @@ -271,7 +383,7 @@ case "$UTIL_NAME" in # Direct invocation handler if [[ $# -eq 0 ]]; then echo "Usage: toolkit-utils " - echo "Commands: shell, stop, restart, status" + echo "Commands: shell, stop, restart, update, status" echo "Or create symlinks: ln -s toolkit-utils toolkit-" exit 1 fi @@ -291,9 +403,10 @@ TOOLKIT_UTILS_EOF ln -sf "../toolkit-utils" "${WRAPPER_DIR}/toolkit-shell" ln -sf "../toolkit-utils" "${WRAPPER_DIR}/toolkit-stop" ln -sf "../toolkit-utils" "${WRAPPER_DIR}/toolkit-restart" + ln -sf "../toolkit-utils" "${WRAPPER_DIR}/toolkit-update" ln -sf "../toolkit-utils" "${WRAPPER_DIR}/toolkit-status" - log_info "Created 4 utility command symlinks" + log_info "Created 5 utility command symlinks" } # ===== Main Activation ===== @@ -304,6 +417,9 @@ main() { # Ensure workspace exists mkdir -p "${WORKSPACE_ROOT}" + # Ensure host X server will accept GUI connections from the container + ensure_x11_ready + # Ensure container is running if ! ensure_container_running; then log_error "Failed to start container" diff --git a/vicar-native-toolkit/README.md b/vicar-native-toolkit/README.md index ab29a68..2d9923f 100644 --- a/vicar-native-toolkit/README.md +++ b/vicar-native-toolkit/README.md @@ -161,6 +161,7 @@ toolkit-shell # Open interactive shell in container toolkit-status # Show container status and wrapper count toolkit-stop # Stop and remove container toolkit-restart # Restart container (useful after config changes) +toolkit-update # Pull the latest CONTAINER_IMAGE and recreate the container toolkit-verify-calib # Verify MARS calibration mounting (if configured) ``` diff --git a/vicar-native-toolkit/docs/CONFIGURATION.md b/vicar-native-toolkit/docs/CONFIGURATION.md index b06aea0..ce0d3f1 100644 --- a/vicar-native-toolkit/docs/CONFIGURATION.md +++ b/vicar-native-toolkit/docs/CONFIGURATION.md @@ -118,6 +118,28 @@ make bootstrap MARS_CALIB=/path/to/mars_calibration toolkit-verify-calib ``` +### SELinux (Linux) + +On Linux hosts with SELinux in **Enforcing** mode, the container (`container_t`) +is denied access to the host X11 socket (`/tmp/.X11-unix`), so GUI tools such as +`xvd` fail with a misleading `Can't open display`. The toolkit handles this +automatically: + +```bash +# Auto-enabled on SELinux-Enforcing hosts when left unset. +# Adds "--security-opt label=disable" at container creation. +DISABLE_SELINUX_LABEL=true +``` + +- **Unset (default):** auto-enabled only when `getenforce` reports `Enforcing`. +- **`DISABLE_SELINUX_LABEL=true`:** always disable labeling. +- **`DISABLE_SELINUX_LABEL=false`:** never disable it (the toolkit logs a hint + if it detects Enforcing mode and GUI tools may fail). + +Changing this only affects a newly created container. After changing it, run +`toolkit-update` (or `toolkit-restart` then re-enter the directory) to recreate +the container with the new setting. + ### Command Auto-Discovery By default, commands are auto-discovered from the container: diff --git a/vicar-native-toolkit/docs/QUICKREF.md b/vicar-native-toolkit/docs/QUICKREF.md index 4addb9c..62b6346 100644 --- a/vicar-native-toolkit/docs/QUICKREF.md +++ b/vicar-native-toolkit/docs/QUICKREF.md @@ -50,6 +50,7 @@ toolkit-status # Show container status toolkit-shell # Interactive shell in container toolkit-stop # Stop and remove container toolkit-restart # Restart container +toolkit-update # Pull latest image and recreate container toolkit-verify-calib # Verify MARS calibration (if configured) # VICAR commands work natively @@ -170,7 +171,7 @@ readlink .direnv/wrappers/gen # Should point to ../vicar-exec # Image issues docker images | grep terrain-intelligence-generator -docker pull ghcr.io/nasa-ammos/tig/terrain-intelligence-generator:opensource +toolkit-update # pull latest CONTAINER_IMAGE and recreate container # Container logs docker logs vicar-sidecar diff --git a/vicar-native-toolkit/docs/VICAR_NATIVE_TOOLKIT_WALKTHROUGH.md b/vicar-native-toolkit/docs/VICAR_NATIVE_TOOLKIT_WALKTHROUGH.md index c8758f0..21c9ef9 100644 --- a/vicar-native-toolkit/docs/VICAR_NATIVE_TOOLKIT_WALKTHROUGH.md +++ b/vicar-native-toolkit/docs/VICAR_NATIVE_TOOLKIT_WALKTHROUGH.md @@ -140,6 +140,7 @@ Available commands: toolkit-shell - Open interactive shell in container toolkit-stop - Stop and remove container toolkit-restart - Restart container with new configuration + toolkit-update - Pull latest image and recreate container VICAR tools: 543 commands available Working directory: /vicar-native-toolkit/workspace