From 4248065b5498fc1dcaee4ddcf5e152ec09067eb7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:32:26 +0000 Subject: [PATCH 1/6] Initial plan From 2df4cf035d5f93116ccd3cec0bf288506c3e08dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:36:09 +0000 Subject: [PATCH 2/6] Add watchdog to auto-restart crashed processes and tests for restart behavior Co-authored-by: PabloZaiden <2192882+PabloZaiden@users.noreply.github.com> --- opencode-server.sh | 63 +++++++++++++++++++++++++++++++++++--- tests/test-devcontainer.sh | 48 +++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/opencode-server.sh b/opencode-server.sh index c61f272..09962dd 100755 --- a/opencode-server.sh +++ b/opencode-server.sh @@ -51,6 +51,7 @@ 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" @@ -122,10 +123,14 @@ 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 WATCHDOG_PID < "$PID_FILE" if kill -0 "$OPENCODE_PID" 2>/dev/null && kill -0 "$CADDY_PID" 2>/dev/null; then return 0 fi + # Also consider the server running if the watchdog is alive (it will restart processes) + if [ -n "$WATCHDOG_PID" ] && kill -0 "$WATCHDOG_PID" 2>/dev/null; then + return 0 + fi fi return 1 } @@ -133,7 +138,13 @@ is_running() { # Stop the running server stop_server() { if [ -f "$PID_FILE" ]; then - read OPENCODE_PID CADDY_PID < "$PID_FILE" + read OPENCODE_PID CADDY_PID WATCHDOG_PID < "$PID_FILE" + # Signal the watchdog to stop before killing processes + touch "$STOP_FLAG" + if [ -n "$WATCHDOG_PID" ]; then + echo "Stopping watchdog (PID: $WATCHDOG_PID)..." + kill "$WATCHDOG_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)..." @@ -146,6 +157,43 @@ stop_server() { fi } +# Watchdog: monitors and restarts processes if they die +start_watchdog() { + ( + while true; do + sleep 5 + # Check if stop was requested + if [ -f "$STOP_FLAG" ]; then + exit 0 + fi + if [ ! -f "$PID_FILE" ]; then + exit 0 + fi + read OPENCODE_PID CADDY_PID WATCHDOG_PID < "$PID_FILE" + NEED_UPDATE=false + # Restart opencode if it died + if ! kill -0 "$OPENCODE_PID" 2>/dev/null; then + echo "$(date): OpenCode process died. Restarting..." >> "$LOG_FILE" + opencode serve --hostname 127.0.0.1 --port $OPENCODE_INTERNAL_PORT >> "$LOG_FILE" 2>&1 & + OPENCODE_PID=$! + NEED_UPDATE=true + sleep 1 + fi + # Restart caddy if it died + if ! kill -0 "$CADDY_PID" 2>/dev/null; then + echo "$(date): Caddy process died. Restarting..." >> "$LOG_FILE" + caddy run --config "$CADDYFILE" --adapter caddyfile >> "$LOG_FILE" 2>&1 & + CADDY_PID=$! + NEED_UPDATE=true + fi + if [ "$NEED_UPDATE" = true ]; then + echo "$OPENCODE_PID $CADDY_PID $WATCHDOG_PID" > "$PID_FILE" + fi + done + ) & + echo $! +} + # Handle --stop flag if [ "$1" = "--stop" ]; then stop_server @@ -196,6 +244,9 @@ cat > "$CADDYFILE" <> "$LOG_FILE" 2>&1 & OPENCODE_PID=$! @@ -207,12 +258,16 @@ 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 watchdog to monitor and restart processes +WATCHDOG_PID=$(start_watchdog) + +# Save PIDs to file (including watchdog) +echo "$OPENCODE_PID $CADDY_PID $WATCHDOG_PID" > "$PID_FILE" echo "OpenCode server started in background." echo "OpenCode PID: $OPENCODE_PID" echo "Caddy PID: $CADDY_PID" +echo "Watchdog PID: $WATCHDOG_PID" echo "Logs: $LOG_FILE" echo print_info diff --git a/tests/test-devcontainer.sh b/tests/test-devcontainer.sh index 28368ea..1d02dae 100755 --- a/tests/test-devcontainer.sh +++ b/tests/test-devcontainer.sh @@ -133,6 +133,54 @@ else fi fi +# ------------------------------------------------------------------------------ +echo "=== Test 7c: Watchdog restarts killed OpenCode process ===" +# Start server fresh +docker exec "$CONTAINER_NAME" bash -c \ + "OPENCODE_PORT=$TEST_PORT bash /workspace/opencode-server.sh --skip-auth" >/dev/null 2>&1 +# Kill only the opencode process (not via --stop) +docker exec "$CONTAINER_NAME" bash -c \ + 'OPENCODE_PID=$(cut -d" " -f1 ~/.config/opencode-server.pid); kill "$OPENCODE_PID" 2>/dev/null' +# Wait for watchdog to detect and restart (watchdog checks every 5 seconds) +sleep 8 +# Verify server is responding again +HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \ + "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1) +if [ "$HTTP_CODE" = "401" ]; then + pass "Server auto-restarted after OpenCode process was killed" +else + fail "Server did not auto-restart after kill (got: $HTTP_CODE)" +fi + +# ------------------------------------------------------------------------------ +echo "=== Test 7d: Watchdog restarts killed Caddy process ===" +# Kill only the caddy process (not via --stop) +docker exec "$CONTAINER_NAME" bash -c \ + 'CADDY_PID=$(cut -d" " -f2 ~/.config/opencode-server.pid); kill "$CADDY_PID" 2>/dev/null' +# Wait for watchdog to detect and restart +sleep 8 +# Verify server is responding again +HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \ + "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1) +if [ "$HTTP_CODE" = "401" ]; then + pass "Server auto-restarted after Caddy process was killed" +else + fail "Server did not auto-restart after Caddy kill (got: $HTTP_CODE)" +fi + +# ------------------------------------------------------------------------------ +echo "=== Test 7e: Stop command permanently stops server (no restart) ===" +docker exec "$CONTAINER_NAME" bash /workspace/opencode-server.sh --stop >/dev/null 2>&1 +# Wait longer than the watchdog interval to confirm it stays down +sleep 8 +LISTENING=$(docker exec "$CONTAINER_NAME" bash -c "ss -tlnp | grep $TEST_PORT || echo 'not listening'" 2>&1) +if [[ "$LISTENING" == *"not listening"* ]]; then + pass "Server stays stopped after --stop (watchdog does not restart)" +else + fail "Server restarted after --stop (watchdog should not restart)" + echo "$LISTENING" +fi + # ------------------------------------------------------------------------------ echo "=== Test 8: Password persists after restart ===" docker exec "$CONTAINER_NAME" bash -c \ From a1ca0ca917d604a75a4a8cb64f927f832122fed6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:37:31 +0000 Subject: [PATCH 3/6] Address code review: use $BASHPID for watchdog self-reference in PID file updates Co-authored-by: PabloZaiden <2192882+PabloZaiden@users.noreply.github.com> --- opencode-server.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opencode-server.sh b/opencode-server.sh index 09962dd..122789d 100755 --- a/opencode-server.sh +++ b/opencode-server.sh @@ -169,7 +169,7 @@ start_watchdog() { if [ ! -f "$PID_FILE" ]; then exit 0 fi - read OPENCODE_PID CADDY_PID WATCHDOG_PID < "$PID_FILE" + read OPENCODE_PID CADDY_PID _WATCHDOG_PID < "$PID_FILE" NEED_UPDATE=false # Restart opencode if it died if ! kill -0 "$OPENCODE_PID" 2>/dev/null; then @@ -187,7 +187,7 @@ start_watchdog() { NEED_UPDATE=true fi if [ "$NEED_UPDATE" = true ]; then - echo "$OPENCODE_PID $CADDY_PID $WATCHDOG_PID" > "$PID_FILE" + echo "$OPENCODE_PID $CADDY_PID $BASHPID" > "$PID_FILE" fi done ) & From 8e794c4806f145be501fbdc4f680ffa5d948bdcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:42:05 +0000 Subject: [PATCH 4/6] Add CI workflow to run shellcheck and tests on PRs Co-authored-by: PabloZaiden <2192882+PabloZaiden@users.noreply.github.com> --- .github/workflows/ci.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3176510 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Shellcheck + run: shellcheck --severity=warning opencode-server.sh + + test: + needs: validate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run tests + run: ./tests/test-devcontainer.sh From 2818c5e2c08bef6537eb7a25ccbff83ddda41aea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:46:41 +0000 Subject: [PATCH 5/6] Fix test failures: use retry loops for watchdog restart checks and increase startup time Co-authored-by: PabloZaiden <2192882+PabloZaiden@users.noreply.github.com> --- opencode-server.sh | 2 +- tests/test-devcontainer.sh | 38 +++++++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/opencode-server.sh b/opencode-server.sh index 122789d..906de3b 100755 --- a/opencode-server.sh +++ b/opencode-server.sh @@ -177,7 +177,7 @@ start_watchdog() { opencode serve --hostname 127.0.0.1 --port $OPENCODE_INTERNAL_PORT >> "$LOG_FILE" 2>&1 & OPENCODE_PID=$! NEED_UPDATE=true - sleep 1 + sleep 3 fi # Restart caddy if it died if ! kill -0 "$CADDY_PID" 2>/dev/null; then diff --git a/tests/test-devcontainer.sh b/tests/test-devcontainer.sh index 1d02dae..cd2be04 100755 --- a/tests/test-devcontainer.sh +++ b/tests/test-devcontainer.sh @@ -141,12 +141,18 @@ docker exec "$CONTAINER_NAME" bash -c \ # Kill only the opencode process (not via --stop) docker exec "$CONTAINER_NAME" bash -c \ 'OPENCODE_PID=$(cut -d" " -f1 ~/.config/opencode-server.pid); kill "$OPENCODE_PID" 2>/dev/null' -# Wait for watchdog to detect and restart (watchdog checks every 5 seconds) -sleep 8 -# Verify server is responding again -HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \ - "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1) -if [ "$HTTP_CODE" = "401" ]; then +# Wait for watchdog to detect and restart, then retry checking the server +RESTART_OK=false +for i in $(seq 1 6); do + sleep 5 + HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \ + "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1) + if [ "$HTTP_CODE" = "401" ]; then + RESTART_OK=true + break + fi +done +if [ "$RESTART_OK" = true ]; then pass "Server auto-restarted after OpenCode process was killed" else fail "Server did not auto-restart after kill (got: $HTTP_CODE)" @@ -157,12 +163,18 @@ echo "=== Test 7d: Watchdog restarts killed Caddy process ===" # Kill only the caddy process (not via --stop) docker exec "$CONTAINER_NAME" bash -c \ 'CADDY_PID=$(cut -d" " -f2 ~/.config/opencode-server.pid); kill "$CADDY_PID" 2>/dev/null' -# Wait for watchdog to detect and restart -sleep 8 -# Verify server is responding again -HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \ - "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1) -if [ "$HTTP_CODE" = "401" ]; then +# Wait for watchdog to detect and restart, then retry checking the server +RESTART_OK=false +for i in $(seq 1 6); do + sleep 5 + HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \ + "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1) + if [ "$HTTP_CODE" = "401" ]; then + RESTART_OK=true + break + fi +done +if [ "$RESTART_OK" = true ]; then pass "Server auto-restarted after Caddy process was killed" else fail "Server did not auto-restart after Caddy kill (got: $HTTP_CODE)" @@ -172,7 +184,7 @@ fi echo "=== Test 7e: Stop command permanently stops server (no restart) ===" docker exec "$CONTAINER_NAME" bash /workspace/opencode-server.sh --stop >/dev/null 2>&1 # Wait longer than the watchdog interval to confirm it stays down -sleep 8 +sleep 10 LISTENING=$(docker exec "$CONTAINER_NAME" bash -c "ss -tlnp | grep $TEST_PORT || echo 'not listening'" 2>&1) if [[ "$LISTENING" == *"not listening"* ]]; then pass "Server stays stopped after --stop (watchdog does not restart)" From 3409de0080c564c7cd22204651a5a03a4f68533f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:54:59 +0000 Subject: [PATCH 6/6] Rewrite watchdog as detached script file to survive parent shell exit in docker exec Co-authored-by: PabloZaiden <2192882+PabloZaiden@users.noreply.github.com> --- opencode-server.sh | 82 +++++++++++++++++++++++--------------- tests/test-devcontainer.sh | 13 +++--- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/opencode-server.sh b/opencode-server.sh index 906de3b..1d0d93d 100755 --- a/opencode-server.sh +++ b/opencode-server.sh @@ -151,6 +151,7 @@ stop_server() { kill "$CADDY_PID" 2>/dev/null rm -f "$PID_FILE" rm -f "$CADDYFILE" + rm -f "$HOME/.config/opencode-watchdog.sh" echo "Server stopped." else echo "No running server found." @@ -158,40 +159,53 @@ stop_server() { } # Watchdog: monitors and restarts processes if they die +# Written to a script file and run detached so it survives parent shell exit start_watchdog() { - ( - while true; do - sleep 5 - # Check if stop was requested - if [ -f "$STOP_FLAG" ]; then - exit 0 - fi - if [ ! -f "$PID_FILE" ]; then - exit 0 - fi - read OPENCODE_PID CADDY_PID _WATCHDOG_PID < "$PID_FILE" - NEED_UPDATE=false - # Restart opencode if it died - if ! kill -0 "$OPENCODE_PID" 2>/dev/null; then - echo "$(date): OpenCode process died. Restarting..." >> "$LOG_FILE" - opencode serve --hostname 127.0.0.1 --port $OPENCODE_INTERNAL_PORT >> "$LOG_FILE" 2>&1 & - OPENCODE_PID=$! - NEED_UPDATE=true - sleep 3 - fi - # Restart caddy if it died - if ! kill -0 "$CADDY_PID" 2>/dev/null; then - echo "$(date): Caddy process died. Restarting..." >> "$LOG_FILE" - caddy run --config "$CADDYFILE" --adapter caddyfile >> "$LOG_FILE" 2>&1 & - CADDY_PID=$! - NEED_UPDATE=true - fi - if [ "$NEED_UPDATE" = true ]; then - echo "$OPENCODE_PID $CADDY_PID $BASHPID" > "$PID_FILE" - fi - done - ) & - echo $! + local watchdog_script="$HOME/.config/opencode-watchdog.sh" + cat > "$watchdog_script" </dev/null; then + echo "\$(date): OpenCode process died. Restarting..." >> "\$LOG_FILE" + opencode serve --hostname 127.0.0.1 --port \$OPENCODE_INTERNAL_PORT >> "\$LOG_FILE" 2>&1 & + OPENCODE_PID=\$! + NEED_UPDATE=true + sleep 3 + fi + # Restart caddy if it died + if ! kill -0 "\$CADDY_PID" 2>/dev/null; then + echo "\$(date): Caddy process died. Restarting..." >> "\$LOG_FILE" + caddy run --config "\$CADDYFILE" --adapter caddyfile >> "\$LOG_FILE" 2>&1 & + CADDY_PID=\$! + NEED_UPDATE=true + fi + if [ "\$NEED_UPDATE" = true ]; then + echo "\$OPENCODE_PID \$CADDY_PID \$\$" > "\$PID_FILE" + fi +done +WATCHDOG_EOF + chmod +x "$watchdog_script" + nohup bash "$watchdog_script" >> "$LOG_FILE" 2>&1 & + local pid=$! + disown $pid 2>/dev/null + echo $pid } # Handle --stop flag @@ -250,6 +264,7 @@ rm -f "$STOP_FLAG" # Start opencode on localhost only (not exposed to network) opencode serve --hostname 127.0.0.1 --port $OPENCODE_INTERNAL_PORT >> "$LOG_FILE" 2>&1 & OPENCODE_PID=$! +disown $OPENCODE_PID 2>/dev/null # Wait a moment for opencode to start sleep 1 @@ -257,6 +272,7 @@ sleep 1 # Start Caddy natively caddy run --config "$CADDYFILE" --adapter caddyfile >> "$LOG_FILE" 2>&1 & CADDY_PID=$! +disown $CADDY_PID 2>/dev/null # Start watchdog to monitor and restart processes WATCHDOG_PID=$(start_watchdog) diff --git a/tests/test-devcontainer.sh b/tests/test-devcontainer.sh index cd2be04..40e9375 100755 --- a/tests/test-devcontainer.sh +++ b/tests/test-devcontainer.sh @@ -138,15 +138,16 @@ echo "=== Test 7c: Watchdog restarts killed OpenCode process ===" # Start server fresh docker exec "$CONTAINER_NAME" bash -c \ "OPENCODE_PORT=$TEST_PORT bash /workspace/opencode-server.sh --skip-auth" >/dev/null 2>&1 +sleep 2 # Kill only the opencode process (not via --stop) docker exec "$CONTAINER_NAME" bash -c \ - 'OPENCODE_PID=$(cut -d" " -f1 ~/.config/opencode-server.pid); kill "$OPENCODE_PID" 2>/dev/null' + 'OPENCODE_PID=$(cut -d" " -f1 ~/.config/opencode-server.pid); kill "$OPENCODE_PID" 2>/dev/null || true' # Wait for watchdog to detect and restart, then retry checking the server RESTART_OK=false -for i in $(seq 1 6); do +for _i in $(seq 1 8); do sleep 5 HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \ - "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1) + "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1 || true) if [ "$HTTP_CODE" = "401" ]; then RESTART_OK=true break @@ -162,13 +163,13 @@ fi echo "=== Test 7d: Watchdog restarts killed Caddy process ===" # Kill only the caddy process (not via --stop) docker exec "$CONTAINER_NAME" bash -c \ - 'CADDY_PID=$(cut -d" " -f2 ~/.config/opencode-server.pid); kill "$CADDY_PID" 2>/dev/null' + 'CADDY_PID=$(cut -d" " -f2 ~/.config/opencode-server.pid); kill "$CADDY_PID" 2>/dev/null || true' # Wait for watchdog to detect and restart, then retry checking the server RESTART_OK=false -for i in $(seq 1 6); do +for _i in $(seq 1 8); do sleep 5 HTTP_CODE=$(docker exec "$CONTAINER_NAME" bash -c \ - "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1) + "curl -k -s -o /dev/null -w '%{http_code}' https://127.0.0.1:$TEST_PORT" 2>&1 || true) if [ "$HTTP_CODE" = "401" ]; then RESTART_OK=true break