#!/usr/bin/env bash # Container entrypoint: start Claude Desktop and Ghidra, and stop both (so the # container exits) as soon as either one is closed. # # - Ghidra exits when its project window is closed, so its process ending is # the signal. Closing other Ghidra windows (CodeBrowser, Help) doesn't count. # - Closing Claude Desktop's window only hides the app (it keeps running for # the tray icon, which GNOME doesn't show). So its X windows are watched and # it counts as closed once none of them is open or minimized. # # Arguments are passed to Claude Desktop. set -u # Job control puts each app in its own process group, so stopping an app also # stops everything it started (Ghidra's launch script runs java as a child). set -m # Plain-HTTP model gateway chosen in run.sh: Claude Desktop only allows http # to loopback, so it talks to 127.0.0.1:18080 and socat forwards that. if [[ -n "${CLAUDE_RE_GATEWAY_FORWARD:-}" ]]; then socat TCP-LISTEN:18080,bind=127.0.0.1,reuseaddr,fork "TCP:${CLAUDE_RE_GATEWAY_FORWARD}" & fi claude-desktop "$@" & claude=$! "${GHIDRA_INSTALL_DIR:-/opt/ghidra}/support/launch.sh" fg jdk Ghidra "" "" ghidra.GhidraRun & ghidra=$! stop_all() { trap - TERM INT HUP echo "launch-apps: $1, stopping" >&2 # Claude's main process shuts down its own helpers; killing its whole # group would take out the GPU process first and trigger restart loops. # Ghidra's group is needed to reach java behind the launch script. kill -TERM "$claude" 2>/dev/null kill -TERM -- "-$ghidra" 2>/dev/null # Give both a few seconds to shut down cleanly before the container ends. for _ in {1..10}; do kill -0 "$claude" 2>/dev/null || kill -0 -- "-$ghidra" 2>/dev/null || break sleep 0.5 done exit 0 } trap 'stop_all "received stop signal"' TERM INT HUP host="$(hostname)" # True if any of this container's Claude windows is Normal (shown) or Iconic # (minimized). A closed window is Withdrawn or loses WM_STATE altogether. # WM_CLIENT_MACHINE filters out Claude windows from other containers or apps # on the same X server. claude_window_open() { local w props for w in $(xwininfo -root -tree 2>/dev/null \ | awk '/\("com\.anthropic\.claude" "com\.anthropic\.Claude"\)/ {print $1}'); do props="$(xprop -id "$w" WM_CLIENT_MACHINE WM_STATE 2>/dev/null)" || continue [[ "$props" == *"WM_CLIENT_MACHINE(STRING) = \"$host\""* ]] || continue [[ "$props" =~ "window state: "(Normal|Iconic) ]] && return 0 done return 1 } seen=0 # only arm the window check after Claude's window has appeared misses=0 # require a few consecutive misses to ride out brief remaps while :; do sleep 1 & wait $! kill -0 "$ghidra" 2>/dev/null || stop_all "Ghidra exited" kill -0 "$claude" 2>/dev/null || stop_all "Claude Desktop exited" if claude_window_open; then seen=1 misses=0 elif (( seen )) && (( ++misses >= 3 )); then stop_all "Claude Desktop window closed" fi done