-
Notifications
You must be signed in to change notification settings - Fork 0
fix(airc daemon): sentinel-marker for intentional re-exec on Windows (#203) #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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" <<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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
AI
Apr 28, 2026
There was a problem hiding this comment.
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.
| 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
AI
Apr 28, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.