Skip to content
Merged
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
52 changes: 52 additions & 0 deletions airc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_write_reexec_marker writes to "$AIRC_WRITE_DIR/airc.reexec-marker" but doesn’t ensure $AIRC_WRITE_DIR exists. At least one call path (stale-host takeover fast-path) can hit _write_reexec_marker before init_identity creates the scope dir, so the marker write can silently fail and the Windows launcher will still treat the exit as a crash. Consider adding a cheap mkdir -p "$AIRC_WRITE_DIR" (best-effort) inside _write_reexec_marker so the sentinel is reliably created whenever needed.

Suggested change
local marker="$AIRC_WRITE_DIR/airc.reexec-marker"
local marker="$AIRC_WRITE_DIR/airc.reexec-marker"
mkdir -p "$AIRC_WRITE_DIR" 2>/dev/null || true

Copilot uses AI. Check for mistakes.
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"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" <<EOF
@echo off
REM AIRC daemon launcher — generated by 'airc daemon install' on Windows.
REM Runs airc connect under bash, restarting on exit. Logs to daemon.log.
REM On intentional re-exec (host-takeover or rejoin-as-joiner), airc
REM writes airc.reexec-marker — we step aside rather than respawn,
REM since the new airc bash from the exec is now the daemon.
cd /d "$cwd_win"
set AIRC_BACKGROUND_OK=1
:loop
"$bash_exe" -c "exec '$airc_bin_unix' connect"
Comment on lines 5092 to 5095
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .bat’s marker path is based on $scope (derived from AIRC_HOME when set), but the launcher deliberately does not set AIRC_HOME and instead relies on cd /d "$cwd_win" + detect_scope(). If a user installs the daemon with AIRC_HOME set, airc connect will write the marker under the cwd-derived scope, while the launcher will look under $scope—so re-exec won’t be detected and the crashloop persists. Fix by exporting AIRC_HOME (as a Unix path) in the bash -c command and/or deriving cwd_win from $scope’s parent so detect_scope and $scope stay aligned.

Suggested change
cd /d "$cwd_win"
set AIRC_BACKGROUND_OK=1
:loop
"$bash_exe" -c "exec '$airc_bin_unix' connect"
REM Export AIRC_HOME explicitly so the child bash resolves the same
REM scope/marker path as this launcher, even when install-time AIRC_HOME
REM differs from cwd-based detect_scope().
cd /d "$cwd_win"
set AIRC_BACKGROUND_OK=1
:loop
"$bash_exe" -c "export AIRC_HOME='$scope'; exec '$airc_bin_unix' connect"

Copilot uses AI. Check for mistakes.
REM Did airc just intentionally re-exec? If marker exists and is recent,
REM the new airc process from the exec is now the running daemon —
REM exit the launcher loop instead of racing-respawn it.
REM forfiles /m airc.reexec-marker /d 0 /c "cmd /c exit 0" succeeds when
REM the file's mtime is today (fine-grained age check below via type +
REM date math is too brittle for .bat; "today" is our 60s proxy).
if exist "$marker_win" (
forfiles /p "$scope_win" /m airc.reexec-marker /d 0 /c "cmd /c exit 0" >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
Comment on lines +5090 to +5106
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “fresh marker” check uses forfiles ... /d 0, which matches any file modified today, not “~60s”. A stale marker left behind (e.g. from an interactive re-exec earlier the same day, or if del fails) could cause the launcher to exit on a real crash and stop auto-restarting. Consider clearing the marker before launching each loop iteration and/or parsing the marker’s embedded UNIX timestamp with PowerShell to enforce a small age window (e.g. <= 60–120s) before treating it as an intentional re-exec; also update the comment to reflect the actual semantics.

Suggested change
REM writes airc.reexec-marker — we step aside rather than respawn,
REM since the new airc bash from the exec is now the daemon.
cd /d "$cwd_win"
set AIRC_BACKGROUND_OK=1
:loop
"$bash_exe" -c "exec '$airc_bin_unix' connect"
REM Did airc just intentionally re-exec? If marker exists and is recent,
REM the new airc process from the exec is now the running daemon —
REM exit the launcher loop instead of racing-respawn it.
REM forfiles /m airc.reexec-marker /d 0 /c "cmd /c exit 0" succeeds when
REM the file's mtime is today (fine-grained age check below via type +
REM date math is too brittle for .bat; "today" is our 60s proxy).
if exist "$marker_win" (
forfiles /p "$scope_win" /m airc.reexec-marker /d 0 /c "cmd /c exit 0" >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
REM writes airc.reexec-marker containing a UNIX timestamp. We step aside
REM rather than respawn only when that marker is freshly written by the
REM exiting process, since the new airc bash from the exec is now daemon.
cd /d "$cwd_win"
set AIRC_BACKGROUND_OK=1
set "AIRC_REEXEC_MARKER=$marker_win"
:loop
REM Clear any stale marker before launching. A leftover file from an
REM earlier run must not suppress restart after a real crash.
if exist "%AIRC_REEXEC_MARKER%" del "%AIRC_REEXEC_MARKER%" >nul 2>&1
"$bash_exe" -c "exec '$airc_bin_unix' connect"
REM Did airc just intentionally re-exec? If the marker exists and its
REM embedded UNIX timestamp is fresh (<=120s old), the new airc process
REM from the exec is now the running daemon — exit this launcher loop
REM instead of racing-respawn it.
if exist "%AIRC_REEXEC_MARKER%" (
powershell -NoProfile -Command "$ts = Get-Content -LiteralPath $env:AIRC_REEXEC_MARKER -TotalCount 1 -ErrorAction Stop; if ($ts -match '^\d+$') { $age = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - [int64]$ts; if ($age -ge 0 -and $age -le 120) { exit 0 } }; exit 1" >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 "%AIRC_REEXEC_MARKER%" >nul 2>&1

Copilot uses AI. Check for mistakes.
exit /b 0
)
)
echo [%date% %time%] airc connect exited. Restarting in 5s. >> daemon.err
timeout /t 5 /nobreak >nul
Comment on lines 5086 to 5111
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated .bat claims to log to daemon.log and cmd_daemon_status points users at $scope/daemon.log + $scope/daemon.err, but the batch file doesn’t redirect airc connect stdout/stderr anywhere, and the >> daemon.err writes are relative to the project cwd (not the .airc scope dir). This will make airc daemon log/status misleading and lose logs. Redirect the bash invocation to an explicit path under the scope (e.g. %scope%\daemon.log/%scope%\daemon.err), and write restart/reexec lines to that same errors file.

Copilot uses AI. Check for mistakes.
goto loop
Expand Down
Loading