Drop Task Scheduler; autostart without administrator rights

Register-ScheduledTask failed with Access denied (0x80070005). Writing to the
root task folder needs elevation, so the claim that this install needed no
admin was simply wrong. Rather than demand UAC, use two mechanisms that need
no privileges at all:

- a Startup-folder entry (AutoFirmer.cmd) runs start-all.bat at logon
- PM2's own --cron-restart with --no-autorestart drives the 5-minute update
  check, so PM2 owns the schedule it was already going to resurrect anyway

Both still run in the logged-in interactive session, which is the requirement
that ruled out a Windows service in the first place: the clicker sends real
input and needs a desktop.

pm2 save now runs after the updater is registered, so `pm2 resurrect` brings
back all three processes rather than two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-30 18:24:47 -05:00
co-authored by Claude Opus 5
parent 7e4714c33d
commit 724ba5581c
2 changed files with 63 additions and 49 deletions
+51 -40
View File
@@ -6,13 +6,22 @@
The only Windows-specific piece of the setup. Everything it schedules is
plain Node, so porting to macOS or Linux means replacing this file alone.
Two scheduled tasks are created, both running as the current user with
LogonType Interactive. That is not incidental: the clicker drives real mouse
and keyboard input and must own a desktop, which a Windows service (session
0) does not have.
Deliberately needs no administrator rights, and changes nothing
machine-wide:
Idempotent - re-running replaces the tasks rather than duplicating them.
No elevation required.
* PM2 supervises the dashboard, the clicker and the update checker.
* A Startup-folder entry runs scripts\start-all.bat at logon.
* PM2's own cron restart drives the update checks.
Task Scheduler is avoided on purpose - Register-ScheduledTask needs
elevation to write to the root task folder.
Everything runs in the logged-in user's interactive session. That is not
incidental: the clicker sends real mouse and keyboard input and must own a
desktop, which a Windows service (session 0) does not have. The trade-off
is that an unattended reboot leaves the instance down until someone logs in.
Idempotent - safe to re-run.
.PARAMETER IntervalMinutes
How often to check master for updates. Default 5.
@@ -28,8 +37,6 @@ param(
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
$StartTask = 'AutoFirmer Start'
$UpdateTask = 'AutoFirmer Update'
function Info($m) { Write-Host " $m" }
function Warn($m) { Write-Host " ! $m" -ForegroundColor Yellow }
@@ -127,46 +134,50 @@ try {
}
}
if ((Invoke-Pm2 @('save')) -ne 0) { throw 'pm2 save failed' }
Info 'pm2: process list saved'
} finally {
Pop-Location
}
# -- Scheduled tasks ---------------------------------------------------------
# LogonType Interactive is what grants the desktop the clicker needs.
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
-LogonType Interactive -RunLevel Limited
# -- Start at logon (Startup folder) -----------------------------------------
# Not Task Scheduler: Register-ScheduledTask needs elevation to write to the
# root task folder, and the whole point is an install that never asks for
# admin. A Startup-folder entry needs no privileges and runs in the user's
# interactive session, which is exactly what the clicker requires - a service
# in session 0 has no desktop and its clicks would go nowhere.
$startupDir = [Environment]::GetFolderPath('Startup')
$startupCmd = Join-Path $startupDir 'AutoFirmer.cmd'
$startAll = Join-Path $Root 'scripts\start-all.bat'
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries -StartWhenAvailable `
-MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 1)
@"
@echo off
REM Written by scripts\install-autostart.ps1 - delete this file to stop
REM AutoFirmer starting at logon.
call "$startAll"
"@ | Set-Content -Path $startupCmd -Encoding ASCII
function Register-Task($Name, $Action, $Triggers, $Description) {
Unregister-ScheduledTask -TaskName $Name -Confirm:$false -ErrorAction SilentlyContinue
Register-ScheduledTask -TaskName $Name -Action $Action -Trigger $Triggers `
-Principal $principal -Settings $settings -Description $Description | Out-Null
Info "task registered: $Name"
Info "logon entry: $startupCmd"
# -- Update checks (PM2 cron) ------------------------------------------------
# Also not Task Scheduler, same reason. PM2 is already running and already
# comes back at logon, so it can own the schedule too: --cron-restart fires the
# script on a schedule and --no-autorestart stops PM2 relaunching it the moment
# it exits.
$updateScript = Join-Path $Root 'scripts\update-check.mjs'
Invoke-Pm2 @('delete', 'autofirmer-update') -Quiet | Out-Null
$cron = "*/$IntervalMinutes * * * *"
if ((Invoke-Pm2 @('start', $updateScript, '--name', 'autofirmer-update',
'--no-autorestart', '--cron-restart', $cron)) -eq 0) {
Info "update checks: every $IntervalMinutes minutes ($cron)"
} else {
Warn 'could not register the update checker - run scripts\update-check.mjs by hand to update'
}
Register-Task $StartTask `
(New-ScheduledTaskAction -Execute 'cmd.exe' `
-Argument "/c `"$(Join-Path $Root 'scripts\start-all.bat')`"" -WorkingDirectory $Root) `
(New-ScheduledTaskTrigger -AtLogOn) `
'Start AutoFirmer and the clicker at logon.'
# AtLogOn covers a reboot; the repeating Once trigger covers the rest of the day.
$repeat = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `
-RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes) `
-RepetitionDuration (New-TimeSpan -Days 3650)
Register-Task $UpdateTask `
(New-ScheduledTaskAction -Execute 'node.exe' `
-Argument 'scripts\update-check.mjs' -WorkingDirectory $Root) `
@((New-ScheduledTaskTrigger -AtLogOn), $repeat) `
"Check master for updates every $IntervalMinutes minutes; rebuild and restart when it moves."
# Save again so `pm2 resurrect` at logon brings the updater back too.
if ((Invoke-Pm2 @('save')) -ne 0) { Warn 'pm2 save failed - processes may not return after a reboot' }
Write-Host ''
Info 'Done. Both processes are registered and will come back at logon.'
Info "Update checks run every $IntervalMinutes minutes; see scripts\update.log"
Info 'Done. No admin was needed and nothing machine-wide was changed.'
Info 'Running now, and again at every logon.'
Info "Updates checked every $IntervalMinutes minutes; see scripts\update.log"
Info 'Useful: pm2 list | pm2 logs autofirmer | pm2 logs clicker'
Info "To disable autostart: delete $startupCmd"