Install Ghidra 12.2 from the official GitHub release into /opt/ghidra, verified against the SHA-256 in the release notes, with Ubuntu's openjdk-25-jdk (Ghidra 12.2 requires Java 25). Version, build date and hash are build args. The entrypoint, renamed from launch-claude.sh to launch-apps.sh, now starts both apps and stops both, ending the container, when either is closed: Ghidra when its process exits (closing the project window), Claude Desktop when none of its windows is open or minimized. Claude is stopped via its main process so it can shut down its own helpers. CLAUDE.md now records that Claude Desktop and Ghidra share one container, at the user's request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
2.7 KiB
Bash
Executable File
69 lines
2.7 KiB
Bash
Executable File
#!/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
|
|
|
|
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
|