diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4f12feb --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/opencode-server.sh b/opencode-server.sh index c61f272..128e58d 100755 --- a/opencode-server.sh +++ b/opencode-server.sh @@ -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"} @@ -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 @@ -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)..." @@ -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 @@ -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 diff --git a/tests/test-devcontainer.sh b/tests/test-devcontainer.sh index 28368ea..3a57f90 100755 --- a/tests/test-devcontainer.sh +++ b/tests/test-devcontainer.sh @@ -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 \ @@ -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 "========================================"