diff --git a/airc b/airc index 2339e6d..189dad2 100755 --- a/airc +++ b/airc @@ -291,6 +291,22 @@ fi unset _gh_resolved AIRC_WRITE_DIR="$(detect_scope)" + +# Write a sentinel marker before any intentional `exec env ... "$0" ...` +# call, so the Windows daemon launcher .bat can distinguish "intentional +# re-exec into different mode" from "actual crash" (#203). On Linux/Mac +# `exec` is a true execve — the parent bash's PID becomes the new +# program, so the launcher script never observes an exit and the marker +# is harmless. On Windows MSYS-bash, exec is emulated as spawn-and-exit: +# the original bash exits + a new airc bash takes over. The launcher +# .bat sees the original bash exit, would normally treat it as a crash, +# and respawn — racing the new airc that just took over (Joel/continuum- +# b69f's #203 crashloop). Marker contents: "PID:UNIX_TIMESTAMP". Caller +# is responsible for invoking this immediately before exec. +_write_reexec_marker() { + local marker="$AIRC_WRITE_DIR/airc.reexec-marker" + printf '%d:%d\n' "$$" "$(date +%s)" > "$marker" 2>/dev/null || true +} CONFIG="$AIRC_WRITE_DIR/config.json" IDENTITY_DIR="$AIRC_WRITE_DIR/identity" PEERS_DIR="$AIRC_WRITE_DIR/peers" @@ -2182,11 +2198,13 @@ cmd_connect() { if [ -n "$_new_picked" ]; then echo " ✓ Another tab beat us to it — joining their fresh gist ($_new_picked)" echo "" + _write_reexec_marker exec env ${_preserved_name:+AIRC_NAME="$_preserved_name"} "$0" connect "$_new_picked" fi echo " Re-execing into host mode for #${resolved_room_name}..." echo "" + _write_reexec_marker exec env AIRC_NO_DISCOVERY=1 ${_preserved_name:+AIRC_NAME="$_preserved_name"} "$0" connect --room "$resolved_room_name" fi @@ -2386,6 +2404,7 @@ except Exception: echo " ✓ Another tab beat us to it — joining their fresh gist ($_new_picked)" echo "" # Re-exec as joiner pointing at the winner's gist. + _write_reexec_marker exec env ${_preserved_name:+AIRC_NAME="$_preserved_name"} "$0" connect "$_new_picked" fi @@ -2394,6 +2413,7 @@ except Exception: # exec replaces the current bash process. AIRC_NO_DISCOVERY=1 # prevents the new instance from re-finding the just-deleted gist # (gh's gist-list cache might still show it for a few seconds). + _write_reexec_marker exec env AIRC_NO_DISCOVERY=1 ${_preserved_name:+AIRC_NAME="$_preserved_name"} "$0" connect --room "$resolved_room_name" fi # Either not a room flow, or no gh, or no resolved_room_name → original die. @@ -2834,6 +2854,7 @@ JSON "$AIRC_WRITE_DIR/room_gist_id" \ "$AIRC_WRITE_DIR/room_name" local _preserved_name; _preserved_name=$(get_config_val name "") + _write_reexec_marker exec env ${_preserved_name:+AIRC_NAME="$_preserved_name"} "$0" connect "$_winner_id" fi fi @@ -5046,15 +5067,46 @@ _daemon_install_schtasks() { cwd_win=$(printf '%s' "$(pwd -P)" | sed 's|^/\([a-z]\)/|\U\1:\\\\|; s|/|\\\\|g') airc_bin_unix="$airc_bin" fi + # Marker path the daemon-launcher polls between iterations to + # distinguish "intentional re-exec into different mode" from "actual + # crash" (#203). airc itself writes this file via _write_reexec_marker + # right before any `exec env ... "$0" connect ...` call. On Windows + # MSYS-bash, exec is emulated as spawn-and-exit (not a true execve), + # so the launcher .bat sees the original bash exit while the new + # airc takes over — the marker tells the .bat to step aside instead + # of racing-respawn the new airc with another instance. + local marker_win + if command -v cygpath >/dev/null 2>&1; then + marker_win=$(cygpath -w "$scope/airc.reexec-marker") + else + marker_win=$(printf '%s' "$scope/airc.reexec-marker" | sed 's|^/\([a-z]\)/|\U\1:\\\\|; s|/|\\\\|g') + fi local launcher_bash="$scope/airc-daemon.bat" cat > "$launcher_bash" <nul 2>&1 + if not errorlevel 1 ( + echo [%date% %time%] airc re-exec'd into different mode ^(host-takeover or rejoin^); new process is now daemon, launcher exiting. >> daemon.err + del "$marker_win" >nul 2>&1 + exit /b 0 + ) +) echo [%date% %time%] airc connect exited. Restarting in 5s. >> daemon.err timeout /t 5 /nobreak >nul goto loop