Skip to content
25 changes: 24 additions & 1 deletion scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ verify_config_integrity() {
fi
}

# Write an auth profile JSON for the NVIDIA API key so the gateway can authenticate.
write_auth_profile() {
if [ -z "${NVIDIA_API_KEY:-}" ]; then
return
Expand All @@ -106,6 +107,7 @@ os.chmod(path, 0o600)
PYAUTH
}

# Print the local and remote dashboard URLs, appending the auth token if available.
print_dashboard_urls() {
local token chat_ui_base local_url remote_url

Expand Down Expand Up @@ -202,7 +204,25 @@ while time.time() < DEADLINE:
else:
print(f'[auto-pair] watcher timed out approvals={APPROVED}')
PYAUTOPAIR
echo "[gateway] auto-pair watcher launched (pid $!)"
AUTO_PAIR_PID=$!
echo "[gateway] auto-pair watcher launched (pid $AUTO_PAIR_PID)"
}

# Forward SIGTERM/SIGINT to child processes for graceful shutdown.
# This script is PID 1 — without a trap, signals interrupt wait and
# children are orphaned until Docker sends SIGKILL after the grace period.
cleanup() {
echo "[gateway] received signal, forwarding to children..."
local gateway_status=0
kill -TERM "$GATEWAY_PID" 2>/dev/null || true
if [ -n "${AUTO_PAIR_PID:-}" ]; then
kill -TERM "$AUTO_PAIR_PID" 2>/dev/null || true
fi
wait "$GATEWAY_PID" 2>/dev/null || gateway_status=$?
if [ -n "${AUTO_PAIR_PID:-}" ]; then
wait "$AUTO_PAIR_PID" 2>/dev/null || true
fi
exit "$gateway_status"
}

# ── Proxy environment ────────────────────────────────────────────
Expand Down Expand Up @@ -316,8 +336,10 @@ if [ "$(id -u)" -ne 0 ]; then
nohup "$OPENCLAW" gateway run >/tmp/gateway.log 2>&1 &
GATEWAY_PID=$!
echo "[gateway] openclaw gateway launched (pid $GATEWAY_PID)"
trap cleanup SIGTERM SIGINT
start_auto_pair
print_dashboard_urls

wait "$GATEWAY_PID"
exit $?
fi
Expand Down Expand Up @@ -365,6 +387,7 @@ done
nohup gosu gateway "$OPENCLAW" gateway run >/tmp/gateway.log 2>&1 &
GATEWAY_PID=$!
echo "[gateway] openclaw gateway launched as 'gateway' user (pid $GATEWAY_PID)"
trap cleanup SIGTERM SIGINT

start_auto_pair
print_dashboard_urls
Expand Down
52 changes: 52 additions & 0 deletions test/nemoclaw-start.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,55 @@ describe("nemoclaw-start non-root fallback", () => {
expect(src).toMatch(/nohup "\$OPENCLAW" gateway run >\/tmp\/gateway\.log 2>&1 &/);
});
});

describe("nemoclaw-start signal handling", () => {
const src = fs.readFileSync(START_SCRIPT, "utf-8");

it("defines cleanup() as a single top-level function", () => {
const matches = src.match(/^cleanup\(\)/gm);
expect(matches).toHaveLength(1);
});

it("cleanup() forwards SIGTERM to both GATEWAY_PID and AUTO_PAIR_PID", () => {
const cleanup = src.match(/cleanup\(\) \{[\s\S]*?^}/m)?.[0];
expect(cleanup).toBeDefined();
expect(cleanup).toMatch(/kill -TERM "\$GATEWAY_PID"/);
expect(cleanup).toMatch(/kill -TERM "\$AUTO_PAIR_PID"/);
});

it("cleanup() waits for both child processes", () => {
const cleanup = src.match(/cleanup\(\) \{[\s\S]*?^}/m)?.[0];
expect(cleanup).toMatch(/wait "\$GATEWAY_PID"/);
expect(cleanup).toMatch(/wait "\$AUTO_PAIR_PID"/);
});

it("cleanup() exits with the gateway exit status", () => {
const cleanup = src.match(/cleanup\(\) \{[\s\S]*?^}/m)?.[0];
expect(cleanup).toMatch(/exit "\$gateway_status"/);
});

it("registers trap before start_auto_pair in non-root path", () => {
// trap must appear before start_auto_pair within the non-root block
const nonRootBlock = src.match(/if \[ "\$\(id -u\)" -ne 0 \]; then[\s\S]*?^fi$/m)?.[0];
expect(nonRootBlock).toBeDefined();
const trapIdx = nonRootBlock.indexOf("trap cleanup SIGTERM SIGINT");
const autoIdx = nonRootBlock.indexOf("start_auto_pair");
expect(trapIdx).toBeGreaterThan(-1);
expect(autoIdx).toBeGreaterThan(-1);
expect(trapIdx).toBeLessThan(autoIdx);
});

it("registers trap before start_auto_pair in root path", () => {
// In the root path (after the non-root fi), trap must precede start_auto_pair
const rootBlock = src.split(/^fi$/m).slice(-1)[0];
const trapIdx = rootBlock.indexOf("trap cleanup SIGTERM SIGINT");
const autoIdx = rootBlock.indexOf("start_auto_pair");
expect(trapIdx).toBeGreaterThan(-1);
expect(autoIdx).toBeGreaterThan(-1);
expect(trapIdx).toBeLessThan(autoIdx);
});

it("captures AUTO_PAIR_PID from background process", () => {
expect(src).toMatch(/AUTO_PAIR_PID=\$!/);
});
});