Skip to content

feat(airc daemon): Windows support via HKCU Run-key autostart (no admin)#200

Merged
joelteply merged 1 commit intocanaryfrom
fix/daemon-windows-schtasks
Apr 28, 2026
Merged

feat(airc daemon): Windows support via HKCU Run-key autostart (no admin)#200
joelteply merged 1 commit intocanaryfrom
fix/daemon-windows-schtasks

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Summary

  • Adds Windows support to airc daemon install / uninstall / status
  • Uses HKCU Run-key (no admin required) instead of schtasks (which needs admin even for per-user logon tasks)
  • Launcher .bat with crash-restart loop, mirrors launchd KeepAlive / systemd Restart=always

Why

Joel 2026-04-28: 'fix the monitor man / i cant go to bed till this is fixed'. Windows had no daemon path -- airc daemon install died on $(uname -s). The only persistence was leaving a Git Bash window open running airc join; nohup &+disown doesn't survive parent shell exit on MINGW64.

What landed

  • _daemon_os: MINGW*|MSYS*|CYGWIN*) echo "windows"
  • _daemon_install_schtasks (kept name for git-grep continuity, but uses HKCU Run-key now): writes $scope/airc-daemon.bat + registers under HKCU Run, fires-and-forgets cmd /c start /MIN immediately so no logout-login needed
  • uninstall: reg delete + kill launcher + rm bat
  • status: reg query + ps lookup of orphaned airc-connect

Verified locally on continuum-b69f's Windows MINGW64

  • ✓ install: HKCU Run entry registered, daemon process detached + alive
  • ✓ status: RUNNING (PID detected via PPID=1 or airc.pid)
  • ✓ uninstall: clean removal
  • ✓ reinstall: idempotent

Test plan

  • Mac/Linux clean-install jobs still pass (no path changes, daemon code only ran on windows branch)
  • Windows clean-install pass (the new code path)
  • Manual: airc daemon install on a fresh Windows install, verify monitor persists across bash exits + reboots

🤖 Generated with Claude Code

Joel 2026-04-28 ~01:00Z: "fix the monitor man / i cant go to bed till
this is fixed". Windows had no daemon path -- `airc daemon install`
died on $(uname -s) with "not supported on MINGW64_NT-...". Result:
the only way to keep airc alive on Windows was to leave a Git Bash
window open running `airc join`. nohup+disown didn't survive parent
shell exit on MINGW64.

Adds a Windows branch to cmd_daemon_install / uninstall / status
mirroring the launchd (mac) and systemd (linux) patterns.

## Mechanism: HKCU Run-key, not Task Scheduler

First attempt was schtasks //SC ONLOGON, but Windows requires admin
to create per-user logon-triggered scheduled tasks (Access Denied for
non-elevated users, even with //RL LIMITED). Per Joel: "i just want
whatever is least hassle and also robust" -- forcing a UAC prompt at
'airc daemon install' time is exactly the kind of friction we kill.

HKCU\Software\Microsoft\Windows\CurrentVersion\Run is the per-user
autostart hive. Writing to it with `reg add` requires no admin (HKCU
is user-scope), fires at every interactive logon for the user, and
matches launchd-Agent / systemd-user semantics exactly.

## Implementation

1. `_daemon_os` returns "windows" on MINGW*/MSYS*/CYGWIN*.
2. `_daemon_install_schtasks` (kept the function name for grep
   continuity even though it's now reg-based) writes a launcher .bat
   to $scope/airc-daemon.bat that:
     - sets AIRC_HOME + AIRC_BACKGROUND_OK
     - exec's `bash -lc 'airc connect'`
     - on exit, logs to daemon.err and `goto loop` after 5s
     (matches launchd KeepAlive / systemd Restart=always)
3. `reg add` registers `cmd /c start "" /MIN "<launcher.bat>"` under
   HKCU Run, key name `airc-monitor`.
4. Fires-and-forgets `cmd /c start /MIN <launcher>` immediately so
   user doesn't need to logout/login to start the monitor.
5. uninstall: reg delete + kill + rm launcher .bat.
6. status: reg query for the entry + ps for the running airc-connect
   (matches PPID=1 orphan or falls back to airc.pid lookup).

## Verified locally on continuum-b69f

  $ airc daemon install
    ✓ Registered HKCU Run entry 'airc-monitor' (runs at every Windows logon)
    ✓ Started monitor in detached cmd window (minimized)
  $ airc daemon status
    Status:  RUNNING (PID 341089)
  $ airc daemon uninstall
    ✓ Removed HKCU Run entry 'airc-monitor'
    ✓ Killed running daemon launcher process(es)
    ✓ Removed /c/Users/green/.airc/airc-daemon.bat
  $ airc daemon install   # idempotent reinstall
    ✓ Registered ... ✓ Started monitor ...

Detached process survives the launching bash exit (which `nohup &
disown` could not on MINGW64).

## Note on AIRC_BACKGROUND_OK

The launcher sets this env var because `airc connect` may otherwise
refuse to run when not on a TTY. Same hint as the launchd plist's
EnvironmentVariables block.
Copilot AI review requested due to automatic review settings April 28, 2026 05:50
@joelteply joelteply merged commit 132b67c into canary Apr 28, 2026
8 checks passed
@joelteply joelteply deleted the fix/daemon-windows-schtasks branch April 28, 2026 05:53
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Windows support for airc daemon by introducing a Windows OS branch that installs a per-user autostart entry via the HKCU Run registry key (no admin), backed by a generated .bat launcher that restarts airc connect on exit.

Changes:

  • Extend daemon OS detection to classify MINGW/MSYS/CYGWIN as windows.
  • Implement Windows daemon install/uninstall/status using HKCU Run-key + a scoped airc-daemon.bat launcher.
  • Update _daemon_installed to detect Windows installation via reg query.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread airc
set AIRC_HOME=$scope_win
set AIRC_BACKGROUND_OK=1
: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.
Comment thread airc
Comment on lines +5021 to +5022
set AIRC_HOME=$scope_win
set AIRC_BACKGROUND_OK=1
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.
Comment thread airc
Comment on lines +5232 to +5235
# 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.
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.
Comment thread airc
Comment on lines +5014 to +5015
# Uses `start /B` for the bash invocation so the cmd.exe wrapper
# doesn't pop a visible console window at logon.
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.
Comment thread airc
Comment on lines +5170 to +5177
# 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)"
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.
Comment thread airc
Comment on lines +4993 to +4999
# 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
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.
Comment thread airc
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.
joelteply added a commit that referenced this pull request Apr 28, 2026
fix(airc daemon): scope tracks cwd at install time, not always $HOME/.airc

PR #200 follow-up. _daemon_scope was returning ${AIRC_HOME:-$HOME/.airc}
unconditionally, but actual user state lives in $cwd/.airc per
detect_scope(). So 'airc daemon install' from ~/continuum/ captured
the wrong scope (~/.airc, empty), spawned a monitor that connected to
nothing, user appeared offline despite 'RUNNING (PID xxx)' in status.

Mirror detect_scope's logic exactly: AIRC_HOME if set, else cwd/.airc.
Now 'airc daemon install' from a project dir captures THAT dir's
.airc as the daemon's scope, launcher .bat sets AIRC_HOME=that, the
spawned airc connect uses the right room state.

Joel 2026-04-28 ~01:05Z caught this: 'lol obv if it worked you would
have a monitor and be online. FAIL'.
joelteply added a commit that referenced this pull request Apr 28, 2026
…op) (#202)

* fix(airc daemon): scope tracks cwd at install time, not always $HOME/.airc

PR #200 follow-up. _daemon_scope was returning ${AIRC_HOME:-$HOME/.airc}
unconditionally, but actual user state lives in $cwd/.airc per
detect_scope(). So 'airc daemon install' from ~/continuum/ captured
the wrong scope (~/.airc, empty), spawned a monitor that connected to
nothing, user appeared offline despite 'RUNNING (PID xxx)' in status.

Mirror detect_scope's logic exactly: AIRC_HOME if set, else cwd/.airc.
Now 'airc daemon install' from a project dir captures THAT dir's
.airc as the daemon's scope, launcher .bat sets AIRC_HOME=that, the
spawned airc connect uses the right room state.

Joel 2026-04-28 ~01:05Z caught this: 'lol obv if it worked you would
have a monitor and be online. FAIL'.

* fix(airc daemon): launcher cd's to cwd, skip AIRC_HOME (Windows fs view fix)

Daemon installed via PR #200/#201 was still crashlooping (every 4s)
because the launcher .bat set AIRC_HOME to a Windows-form path
(C:\Users\green\continuum\.airc) which Git Bash's airc binary
couldn't traverse cleanly downstream. Plus 'bash -lc' was reading
login profile and re-exporting PATH which churned env.

Restructured launcher .bat:
1. 'cd /d <cwd_win>' from cmd.exe so the bash subprocess inherits
   the project dir as pwd. detect_scope() then returns <cwd>/.airc
   the same way it does in the user's interactive shell.
2. Drop AIRC_HOME entirely — let detect_scope work normally.
3. 'bash -c' not 'bash -lc' — non-login skips profile, keeps the
   env we set in cmd uncorrupted.
4. Absolute Unix-form path to airc (cygpath -u) — bash -c doesn't
   read ~/.bashrc, so PATH may not include ~/.local/bin.
5. Errors log to daemon.err relative to cwd (already cd'd into it).

Joel 2026-04-28 caught both the wrong-scope (PR #201) and now the
crashloop. Verified locally: with this launcher shape, airc connect
runs to completion + maintains the SSH tail to the host.
joelteply added a commit that referenced this pull request Apr 28, 2026
 target 2, net -40) (#209)

refactor(airc): _daemon_install_done helper + trim duplicated comments (#205 target 2)

Three platform daemon installers (launchd/systemd/schtasks) duplicated
the same 5-line "Loaded into X / airc will auto-start / Logs / Status"
print block. Plus the schtasks function had ~30 lines of comment
paragraphs duplicating commit-history context (#200/#202 explanations).

Now: one `_daemon_install_done` helper for the print footer, called
by all three installers. Schtasks comment block trimmed to a 4-line
summary that points at PR #202 for the bug-history detail.

Behavior unchanged on every platform — same plist/unit/.bat content,
same registration calls, same status output (just printed via the helper).

#205 target 2 of 6.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants