Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Tests

on:
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Run tests
run: bash tests/test-devcontainer.sh
92 changes: 88 additions & 4 deletions opencode-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ fi
# Configuration
PASSWORD_FILE="$HOME/.config/opencode-server-local"
PID_FILE="$HOME/.config/opencode-server.pid"
STOP_FLAG="$HOME/.config/opencode-server.stop"
CERT_DIR="$HOME/.config/opencode-certs"
CERT_FILE="$CERT_DIR/cert.pem"
KEY_FILE="$CERT_DIR/key.pem"
CADDYFILE="$HOME/.config/opencode-caddyfile"
LOG_FILE="$HOME/.config/opencode-server.log"
MONITOR_INTERVAL=${OPENCODE_MONITOR_INTERVAL:-5}

OPENCODE_INTERNAL_PORT="4097"
OPENCODE_HTTPS_PORT=${OPENCODE_PORT-"5000"}
Expand Down Expand Up @@ -122,7 +124,7 @@ generate_cert() {
# Check if server is already running
is_running() {
if [ -f "$PID_FILE" ]; then
read OPENCODE_PID CADDY_PID < "$PID_FILE"
read OPENCODE_PID CADDY_PID MONITOR_PID < "$PID_FILE"
if kill -0 "$OPENCODE_PID" 2>/dev/null && kill -0 "$CADDY_PID" 2>/dev/null; then
return 0
fi
Expand All @@ -132,8 +134,14 @@ is_running() {

# Stop the running server
stop_server() {
# Set the stop flag so the monitor knows this is intentional
touch "$STOP_FLAG"
if [ -f "$PID_FILE" ]; then
read OPENCODE_PID CADDY_PID < "$PID_FILE"
read OPENCODE_PID CADDY_PID MONITOR_PID < "$PID_FILE"
if [ -n "$MONITOR_PID" ]; then
echo "Stopping monitor (PID: $MONITOR_PID)..."
kill "$MONITOR_PID" 2>/dev/null
fi
echo "Stopping OpenCode server (PID: $OPENCODE_PID)..."
kill "$OPENCODE_PID" 2>/dev/null
echo "Stopping Caddy proxy (PID: $CADDY_PID)..."
Expand Down Expand Up @@ -162,6 +170,18 @@ if is_running; then
exit 0
fi

# Clean up any orphaned processes from a previous run
if [ -f "$PID_FILE" ]; then
read OLD_OPENCODE_PID OLD_CADDY_PID OLD_MONITOR_PID < "$PID_FILE"
kill "$OLD_OPENCODE_PID" 2>/dev/null || true
kill "$OLD_CADDY_PID" 2>/dev/null || true
[ -n "$OLD_MONITOR_PID" ] && kill "$OLD_MONITOR_PID" 2>/dev/null || true
rm -f "$PID_FILE"
fi

# Remove stop flag for fresh start
rm -f "$STOP_FLAG"

# Check if caddy is installed, if not install it (Linux only)
if ! command -v caddy &> /dev/null; then
if [ "$OS" = "Linux" ]; then
Expand Down Expand Up @@ -207,12 +227,76 @@ sleep 1
caddy run --config "$CADDYFILE" --adapter caddyfile >> "$LOG_FILE" 2>&1 &
CADDY_PID=$!

# Save PIDs to file
echo "$OPENCODE_PID $CADDY_PID" > "$PID_FILE"
# Start the process monitor in the background
# The monitor watches both processes and relaunches them if they die unexpectedly.
# It exits if the stop flag file is present (set by --stop).
(
# Helper: check if a PID is alive and not a zombie
is_process_alive() {
local pid=$1
if ! kill -0 "$pid" 2>/dev/null; then
return 1
fi
# Check for zombie (defunct) state - works on Linux
if [ -f "/proc/$pid/status" ]; then
if grep -q "^State:.*Z" "/proc/$pid/status" 2>/dev/null; then
# Reap the zombie
wait "$pid" 2>/dev/null
return 1
fi
fi
return 0
}

while true; do
sleep "$MONITOR_INTERVAL"

# If the stop flag exists, this is an intentional shutdown - exit monitor
if [ -f "$STOP_FLAG" ]; then
exit 0
fi

# Read current PIDs from the PID file
if [ ! -f "$PID_FILE" ]; then
exit 0
fi
read CUR_OPENCODE_PID CUR_CADDY_PID CUR_MONITOR_PID < "$PID_FILE"

NEED_UPDATE=false

# Check if opencode is alive
if ! is_process_alive "$CUR_OPENCODE_PID"; then
if [ -f "$STOP_FLAG" ]; then exit 0; fi
echo "$(date): OpenCode process died, relaunching..." >> "$LOG_FILE"
opencode serve --hostname 127.0.0.1 --port $OPENCODE_INTERNAL_PORT >> "$LOG_FILE" 2>&1 &
CUR_OPENCODE_PID=$!
NEED_UPDATE=true
fi

# Check if caddy is alive
if ! is_process_alive "$CUR_CADDY_PID"; then
if [ -f "$STOP_FLAG" ]; then exit 0; fi
echo "$(date): Caddy process died, relaunching..." >> "$LOG_FILE"
caddy run --config "$CADDYFILE" --adapter caddyfile >> "$LOG_FILE" 2>&1 &
CUR_CADDY_PID=$!
NEED_UPDATE=true
fi

# Update PID file if any process was relaunched
if [ "$NEED_UPDATE" = true ]; then
echo "$CUR_OPENCODE_PID $CUR_CADDY_PID $CUR_MONITOR_PID" > "$PID_FILE"
fi
done
) >> "$LOG_FILE" 2>&1 &
MONITOR_PID=$!

# Save PIDs to file (opencode, caddy, monitor)
echo "$OPENCODE_PID $CADDY_PID $MONITOR_PID" > "$PID_FILE"

echo "OpenCode server started in background."
echo "OpenCode PID: $OPENCODE_PID"
echo "Caddy PID: $CADDY_PID"
echo "Monitor PID: $MONITOR_PID"
echo "Logs: $LOG_FILE"
echo
print_info
Expand Down
93 changes: 93 additions & 0 deletions tests/test-devcontainer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ else
fi
fi

# ------------------------------------------------------------------------------
echo "=== Test 7c: Stop command kills monitor process ==="
MONITOR_PROCS=$(docker exec "$CONTAINER_NAME" bash -c 'ps aux | grep "opencode-server" | grep -v grep | grep -v test || echo "no monitor"' 2>&1)
if [[ "$MONITOR_PROCS" == *"no monitor"* ]]; then
pass "Monitor process is terminated"
else
fail "Monitor process still running"
echo "$MONITOR_PROCS"
fi

# ------------------------------------------------------------------------------
echo "=== Test 8: Password persists after restart ==="
docker exec "$CONTAINER_NAME" bash -c \
Expand Down Expand Up @@ -170,6 +180,89 @@ else
fail "Connection info not displayed properly"
fi

# ------------------------------------------------------------------------------
# Stop the server from previous tests and start fresh for relaunch tests
docker exec "$CONTAINER_NAME" bash /workspace/opencode-server.sh --stop >/dev/null 2>&1
sleep 2

echo "=== Test 11: OpenCode process is relaunched after external kill ==="
# Start the server with a fast monitor interval for testing
docker exec "$CONTAINER_NAME" bash -c \
"OPENCODE_PORT=$TEST_PORT OPENCODE_MONITOR_INTERVAL=2 bash /workspace/opencode-server.sh --skip-auth" >/dev/null 2>&1
sleep 2

# Get the opencode PID and kill it
OPENCODE_PID_BEFORE=$(docker exec "$CONTAINER_NAME" bash -c \
'read OPID CPID MPID < ~/.config/opencode-server.pid && echo $OPID' 2>/dev/null)
docker exec "$CONTAINER_NAME" bash -c "kill $OPENCODE_PID_BEFORE" 2>/dev/null

# Wait for the monitor to detect and relaunch (monitor interval is 2s)
sleep 5

# Check that a new opencode process exists and has a different PID
OPENCODE_PID_AFTER=$(docker exec "$CONTAINER_NAME" bash -c \
'read OPID CPID MPID < ~/.config/opencode-server.pid && echo $OPID' 2>/dev/null)
if [ -n "$OPENCODE_PID_AFTER" ] && [ "$OPENCODE_PID_AFTER" != "$OPENCODE_PID_BEFORE" ]; then
# Verify the new process is actually running
if docker exec "$CONTAINER_NAME" bash -c "kill -0 $OPENCODE_PID_AFTER" 2>/dev/null; then
pass "OpenCode process was relaunched (PID $OPENCODE_PID_BEFORE -> $OPENCODE_PID_AFTER)"
else
fail "New OpenCode PID exists in file but process is not running"
fi
else
fail "OpenCode process was not relaunched (PID before=$OPENCODE_PID_BEFORE, after=$OPENCODE_PID_AFTER)"
fi

# ------------------------------------------------------------------------------
echo "=== Test 12: Caddy process is relaunched after external kill ==="
# Get the caddy PID and kill it
CADDY_PID_BEFORE=$(docker exec "$CONTAINER_NAME" bash -c \
'read OPID CPID MPID < ~/.config/opencode-server.pid && echo $CPID' 2>/dev/null)
docker exec "$CONTAINER_NAME" bash -c "kill $CADDY_PID_BEFORE" 2>/dev/null

# Wait for the monitor to detect and relaunch
sleep 5

# Check that a new caddy process exists and has a different PID
CADDY_PID_AFTER=$(docker exec "$CONTAINER_NAME" bash -c \
'read OPID CPID MPID < ~/.config/opencode-server.pid && echo $CPID' 2>/dev/null)
if [ -n "$CADDY_PID_AFTER" ] && [ "$CADDY_PID_AFTER" != "$CADDY_PID_BEFORE" ]; then
if docker exec "$CONTAINER_NAME" bash -c "kill -0 $CADDY_PID_AFTER" 2>/dev/null; then
pass "Caddy process was relaunched (PID $CADDY_PID_BEFORE -> $CADDY_PID_AFTER)"
else
fail "New Caddy PID exists in file but process is not running"
fi
else
fail "Caddy process was not relaunched (PID before=$CADDY_PID_BEFORE, after=$CADDY_PID_AFTER)"
fi

# ------------------------------------------------------------------------------
echo "=== Test 13: Server still responds after process relaunch ==="
# Give the relaunched processes time to fully start
sleep 5
HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \
"curl -k -s -o /dev/null -w '%{http_code}' --retry 3 --retry-delay 2 https://127.0.0.1:$TEST_PORT" 2>&1)
if [ "$HTTP_CODE" = "401" ]; then
pass "Server responds with 401 after relaunch on port $TEST_PORT"
else
fail "Expected HTTP 401 after relaunch, got: $HTTP_CODE"
fi

# ------------------------------------------------------------------------------
echo "=== Test 14: Processes are NOT relaunched after --stop ==="
# Stop the server using --stop
docker exec "$CONTAINER_NAME" bash /workspace/opencode-server.sh --stop >/dev/null 2>&1
sleep 5

# Verify no opencode or caddy processes are running (monitor should not have relaunched them)
PROCS=$(docker exec "$CONTAINER_NAME" bash -c 'ps aux | grep -E "(opencode serve|caddy run)" | grep -v grep || echo "no processes"' 2>&1)
if [[ "$PROCS" == *"no processes"* ]]; then
pass "Processes were NOT relaunched after --stop"
else
fail "Processes were relaunched after --stop (they should not have been)"
echo "$PROCS"
fi

# ------------------------------------------------------------------------------
echo ""
echo "========================================"
Expand Down