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
146 changes: 146 additions & 0 deletions airc
Original file line number Diff line number Diff line change
Expand Up @@ -4856,6 +4856,7 @@ _daemon_os() {
echo "linux"
fi
;;
MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
*) echo "unknown" ;;
esac
}
Expand Down Expand Up @@ -4893,6 +4894,8 @@ _daemon_installed() {
[ -f "$HOME/Library/LaunchAgents/com.cambriantech.airc.plist" ] && return 0 ;;
linux|wsl)
[ -f "$HOME/.config/systemd/user/airc.service" ] && return 0 ;;
windows)
reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" //v airc-monitor >/dev/null 2>&1 && return 0 ;;
esac
return 1
}
Expand All @@ -4906,6 +4909,7 @@ cmd_daemon_install() {
case "$os" in
darwin) _daemon_install_launchd "$airc_bin" "$scope" ;;
linux|wsl) _daemon_install_systemd "$airc_bin" "$scope" "$os" ;;
windows) _daemon_install_schtasks "$airc_bin" "$scope" ;;
*) die "Daemon install not supported on $(uname -s). Manual workaround: run 'airc connect' under your platform's preferred autostart mechanism." ;;
esac
}
Expand Down Expand Up @@ -4970,6 +4974,94 @@ PLIST
echo " will pick up gh credentials on next restart."
}

_daemon_install_schtasks() {
# Windows daemon via HKCU Run key (no admin). Mirrors launchd /
# systemd: per-user autostart at logon, restarts airc connect on
# exit, logs to $scope/daemon.log. Joel 2026-04-28: "fix the monitor
# man / i cant go to bed till this is fixed" — Windows had no daemon
# path, `nohup airc connect &` doesn't survive the launching shell
# on MINGW64 (Git Bash kills the child when the parent bash exits).
#
# Why Run-key instead of Task Scheduler: schtasks //SC ONLOGON
# requires admin even for per-user tasks (UAC prompt + "Access is
# denied" without). HKCU\...\Run writes to user-scope hive, no admin,
# works identically (fires at user logon). Path-of-least-friction
# per Joel: "i just want whatever is least hassle and also robust".
local airc_bin="$1" scope="$2"
local entry_name="airc-monitor"

# Find Git Bash. The launcher .bat bridges from cmd.exe (Run key
# context) into bash (where airc actually runs).
local bash_exe=""
for c in 'C:\Program Files\Git\bin\bash.exe' 'C:\Program Files (x86)\Git\bin\bash.exe' "$HOME/AppData/Local/Programs/Git/bin/bash.exe"; do
local check_path; check_path=$(echo "$c" | sed 's|\\|/|g; s|^C:|/c|')
if [ -f "$c" ] || [ -f "$check_path" ]; then bash_exe="$c"; break; fi
done
Comment on lines +4993 to +4999
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.

bash_exe is written verbatim into the generated .bat. If the Git-for-Windows install is found via the $HOME/.../Git/bin/bash.exe candidate, bash_exe will be a POSIX path like /c/Users/.../bash.exe, which cmd.exe (Run-key context) typically can't execute. Convert the discovered bash path to a Windows path (e.g. via cygpath -w) before embedding it into the batch file so both system-wide and per-user Git installs work.

Copilot uses AI. Check for mistakes.
[ -z "$bash_exe" ] && die "bash.exe not found at any standard Git for Windows path. Install Git for Windows + re-run."

# Convert paths to Windows form; cmd.exe can't read /c/Users/... .
local airc_bin_win scope_win
if command -v cygpath >/dev/null 2>&1; then
airc_bin_win=$(cygpath -w "$airc_bin")
scope_win=$(cygpath -w "$scope")
else
airc_bin_win=$(printf '%s' "$airc_bin" | sed 's|^/\([a-z]\)/|\U\1:\\\\|; s|/|\\\\|g')
scope_win=$(printf '%s' "$scope" | sed 's|^/\([a-z]\)/|\U\1:\\\\|; s|/|\\\\|g')
fi

# Stage a launcher .bat in $scope. Loops with 5s pause for airc-crash
# auto-restart (matches launchd KeepAlive=true / systemd Restart=always).
# Uses `start /B` for the bash invocation so the cmd.exe wrapper
# doesn't pop a visible console window at logon.
Comment on lines +5014 to +5015
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.

This comment says the launcher uses start /B for the bash invocation, but the generated .bat invokes bash directly (no start /B). Update the comment (or the implementation) so they’re consistent.

Suggested change
# Uses `start /B` for the bash invocation so the cmd.exe wrapper
# doesn't pop a visible console window at logon.
# Invokes bash.exe directly from the .bat; no `start /B` wrapper is
# used for the bash launch.

Copilot uses AI. Check for mistakes.
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.
set AIRC_HOME=$scope_win
set AIRC_BACKGROUND_OK=1
Comment on lines +5021 to +5022
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.

In the generated .bat, use the quoted set "VAR=value" form for environment variables (e.g. AIRC_HOME) so paths containing special cmd.exe metacharacters (like &, (, ), ^) or trailing spaces can't break the script or be interpreted as additional commands at logon.

Suggested change
set AIRC_HOME=$scope_win
set AIRC_BACKGROUND_OK=1
set "AIRC_HOME=$scope_win"
set "AIRC_BACKGROUND_OK=1"

Copilot uses AI. Check for mistakes.
:loop
"$bash_exe" -lc "exec '$airc_bin_win' connect"
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 airc-daemon.bat never redirects airc connect stdout/stderr to $scope/daemon.log, but airc daemon log and the install/status output claim $scope/daemon.log is the log location. Either redirect the bash invocation output to daemon.log (and stderr to daemon.err) or adjust the user-facing messages/cmd_daemon_log behavior for Windows so it matches reality.

Suggested change
"$bash_exe" -lc "exec '$airc_bin_win' connect"
"$bash_exe" -lc "exec '$airc_bin_win' connect" >> "$scope_win\\daemon.log" 2>> "$scope_win\\daemon.err"

Copilot uses AI. Check for mistakes.
echo [%date% %time%] airc connect exited. Restarting in 5s. >> "$scope_win\\daemon.err"
timeout /t 5 /nobreak >nul
goto loop
EOF
local launcher_win
if command -v cygpath >/dev/null 2>&1; then
launcher_win=$(cygpath -w "$launcher_bash")
else
launcher_win=$(printf '%s' "$launcher_bash" | sed 's|^/\([a-z]\)/|\U\1:\\\\|; s|/|\\\\|g')
fi

# The Run-key value is what cmd.exe runs at user logon. We wrap with
# `cmd /c start "" /MIN ... ` so the daemon launches detached + with
# a minimized console window (still visible in taskbar but out of
# the way). Without /MIN the user gets a raw cmd window every login.
# The empty "" is the title slot for `start` (otherwise `start "path
# to bat"` interprets the path as the title).
local run_cmd="cmd /c start \"\" /MIN \"$launcher_win\""

# HKCU\Software\Microsoft\Windows\CurrentVersion\Run is the canonical
# per-user autostart hive on Windows. reg add overwrites any prior
# entry with /f (no prompt). Fully idempotent.
reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" //v "$entry_name" //t REG_SZ //d "$run_cmd" //f >/dev/null 2>&1 \
|| die "reg add failed for HKCU Run\\$entry_name"

# Start it now (detached) so the user doesn't have to logout/login.
# cmd /c start fires-and-forgets — returns immediately; the spawned
# bat keeps running independent of this shell.
cmd //c start "" //MIN "$launcher_win" >/dev/null 2>&1 || true

echo " ✓ Registered HKCU Run entry '$entry_name' (runs at every Windows logon)"
echo " ✓ Started monitor in detached cmd window (minimized)"
echo " airc will now auto-start at login + restart on exit."
echo " Logs: $scope/daemon.log (airc's own --background log)"
echo " Errors: $scope/daemon.err (restart events, etc.)"
echo " Launcher: $scope/airc-daemon.bat"
echo " Status: airc daemon status"
echo " Stop: airc daemon uninstall"
}

_daemon_install_systemd() {
local airc_bin="$1" scope="$2" os="$3"
local unit_dir="$HOME/.config/systemd/user"
Expand Down Expand Up @@ -5065,6 +5157,28 @@ cmd_daemon_uninstall() {
[ -f "$unit_path" ] && rm "$unit_path" && systemctl --user daemon-reload && echo " ✓ Removed $unit_path" \
|| echo " (no unit on disk)"
;;
windows)
local entry_name="airc-monitor"
if reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" //v "$entry_name" >/dev/null 2>&1; then
reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" //v "$entry_name" //f >/dev/null 2>&1 \
&& echo " ✓ Removed HKCU Run entry '$entry_name'" \
|| echo " (reg delete failed — try 'reg delete' manually)"
else
echo " (no Run entry '$entry_name' registered)"
fi
# Kill any currently-running daemon-launched airc-connect tree.
# Match on the launcher .bat path so we don't kill foreground
# `airc join` running in the user's terminal.
local scope; scope=$(_daemon_scope)
if ps -ef 2>/dev/null | grep 'airc-daemon.bat' | grep -v grep >/dev/null; then
ps -ef | grep 'airc-daemon.bat' | grep -v grep | awk '{print $2}' | while read pid; do
kill "$pid" 2>/dev/null || true
done
echo " ✓ Killed running daemon launcher process(es)"
Comment on lines +5170 to +5177
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 Windows uninstall path only matches/kills processes whose cmdline contains airc-daemon.bat, which targets the launcher cmd.exe/batch but not the spawned bash/airc connect processes. Consider reusing the existing process-tree helpers (e.g. proc_children, proc_cmdline, or pidfile-based teardown) to reliably terminate the whole daemon process tree.

Suggested change
# Match on the launcher .bat path so we don't kill foreground
# `airc join` running in the user's terminal.
local scope; scope=$(_daemon_scope)
if ps -ef 2>/dev/null | grep 'airc-daemon.bat' | grep -v grep >/dev/null; then
ps -ef | grep 'airc-daemon.bat' | grep -v grep | awk '{print $2}' | while read pid; do
kill "$pid" 2>/dev/null || true
done
echo " ✓ Killed running daemon launcher process(es)"
# Match the launcher first, but use Windows tree termination so
# spawned `bash` / `airc connect` children are also stopped. Then
# clean up any orphaned daemon children that still reference this
# daemon scope in their command line.
local scope; scope=$(_daemon_scope)
local killed_any=0
local pid
while read -r pid; do
[ -n "$pid" ] || continue
if command -v taskkill >/dev/null 2>&1; then
taskkill //PID "$pid" //T //F >/dev/null 2>&1 || kill "$pid" 2>/dev/null || true
else
kill "$pid" 2>/dev/null || true
fi
killed_any=1
done < <(ps -ef 2>/dev/null | awk '/airc-daemon\.bat/ && !/awk/ {print $2}')
while read -r pid; do
[ -n "$pid" ] || continue
if command -v taskkill >/dev/null 2>&1; then
taskkill //PID "$pid" //T //F >/dev/null 2>&1 || kill "$pid" 2>/dev/null || true
else
kill "$pid" 2>/dev/null || true
fi
killed_any=1
done < <(ps -ef 2>/dev/null | awk -v scope="$scope" '
index($0, scope) && index($0, "airc connect") && $0 !~ /airc-daemon\.bat/ && !/awk/ {print $2}
')
if [ "$killed_any" -eq 1 ]; then
echo " ✓ Killed running daemon process tree"

Copilot uses AI. Check for mistakes.
fi
[ -f "$scope/airc-daemon.bat" ] && rm "$scope/airc-daemon.bat" \
&& echo " ✓ Removed $scope/airc-daemon.bat"
;;
*) echo " Daemon uninstall not supported on $(uname -s)."; return 1 ;;
esac
}
Expand Down Expand Up @@ -5105,6 +5219,38 @@ cmd_daemon_status() {
echo " No daemon installed. Run: airc daemon install"
fi
;;
windows)
local entry_name="airc-monitor"
if reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" //v "$entry_name" >/dev/null 2>&1; then
echo " Type: HKCU Run-key (per-user logon autostart, no admin)"
echo " Entry: $entry_name"
local scope; scope=$(_daemon_scope)
echo " Logs: $scope/daemon.log"
echo " Errors: $scope/daemon.err"
echo " Launcher: $scope/airc-daemon.bat"
# Is the daemon-launched airc actually running right now? The
# launcher .bat spawns bash + airc-connect then exits, so we
# look for the airc-connect process (PPID=1 = orphaned-into-
# init, which is what `start /B` produces on Windows). Falling
# back to airc.pid lookup if that fails.
Comment on lines +5232 to +5235
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 comments mention using start /B (and later rely on behavior attributed to start /B), but the implementation uses start ... /MIN and the .bat directly invokes bash without start /B. Please update the comments to match the actual behavior so future debugging doesn’t chase the wrong process model.

Suggested change
# launcher .bat spawns bash + airc-connect then exits, so we
# look for the airc-connect process (PPID=1 = orphaned-into-
# init, which is what `start /B` produces on Windows). Falling
# back to airc.pid lookup if that fails.
# launcher .bat starts bash running `airc connect` and then
# exits, so we look for the surviving airc-connect process.
# In the MSYS/Git Bash `ps` view this may appear orphaned
# (often PPID=1). Fall back to airc.pid lookup if that fails.

Copilot uses AI. Check for mistakes.
local live_pid
live_pid=$(ps -ef 2>/dev/null | awk '$3 == 1 && /airc.*connect/ && !/grep/ {print $2; exit}')
if [ -z "$live_pid" ] && [ -f "$scope/airc.pid" ]; then
local pidfile_pid
pidfile_pid=$(head -1 "$scope/airc.pid" 2>/dev/null | tr -d '[:space:]')
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.

$scope/airc.pid contains multiple PIDs separated by spaces (see cmd_connect writes "$$ $PAIR_PID ..."), but this code strips all whitespace and then kill -0 checks a concatenated number (e.g. "123 456" -> "123456"). Parse the first PID field instead (or iterate fields) so Windows status can reliably detect a live daemon via the pidfile fallback.

Suggested change
pidfile_pid=$(head -1 "$scope/airc.pid" 2>/dev/null | tr -d '[:space:]')
pidfile_pid=$(awk 'NR == 1 { print $1; exit }' "$scope/airc.pid" 2>/dev/null)

Copilot uses AI. Check for mistakes.
if [ -n "$pidfile_pid" ] && kill -0 "$pidfile_pid" 2>/dev/null; then
live_pid="$pidfile_pid (from airc.pid)"
fi
fi
if [ -n "$live_pid" ]; then
echo " Status: RUNNING (PID $live_pid)"
else
echo " Status: registered (will start at next logon — or 'airc daemon install' to start now)"
fi
else
echo " No daemon installed. Run: airc daemon install"
fi
;;
*) echo " Daemon status not supported on $(uname -s)." ;;
esac
}
Expand Down
Loading