run.sh shows a window (zenity form, terminal prompts without zenity) for a base URL, token, auth header and optional model list. Left empty, Claude Desktop starts normally with the claude.ai sign-in. Otherwise it runs in its third-party gateway mode: run.sh writes the managed config (inferenceProvider "gateway" and friends) and copies it into the created container as root, because Claude Desktop ignores /etc/claude-desktop/managed-settings.json unless root owns it. The host copy is deleted right away; nothing is kept between starts. Claude Desktop only allows plain http to loopback, so for an http:// gateway launch-apps.sh runs socat on 127.0.0.1:18080 and forwards to it. localhost is rewritten to host.docker.internal for servers on the host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
3.0 KiB
Bash
Executable File
75 lines
3.0 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
|
|
|
|
# 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
|