diff --git a/airc b/airc index 16f4b3d..98e5d5c 100755 --- a/airc +++ b/airc @@ -4856,6 +4856,7 @@ _daemon_os() { echo "linux" fi ;; + MINGW*|MSYS*|CYGWIN*) echo "windows" ;; *) echo "unknown" ;; esac } @@ -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 } @@ -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 } @@ -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 + [ -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. + local launcher_bash="$scope/airc-daemon.bat" + cat > "$launcher_bash" <> "$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" @@ -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)" + 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 } @@ -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. + 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:]') + 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 }