The no-change path is a single `git fetch`, so polling master every minute costs nothing and lines up with the VPS's own 60s deploy timer instead of lagging it by up to five. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
186 lines
7.7 KiB
PowerShell
186 lines
7.7 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Register AutoFirmer to start at logon and keep itself updated.
|
|
|
|
.DESCRIPTION
|
|
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.
|
|
|
|
Deliberately needs no administrator rights, and changes nothing
|
|
machine-wide:
|
|
|
|
* 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 1 — the no-change path is a
|
|
single `git fetch`, so checking every minute costs nothing and matches the
|
|
VPS's own 60s deploy timer.
|
|
|
|
.PARAMETER SkipClicker
|
|
Register only the dashboard, leaving the clicker to be run by hand.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[int]$IntervalMinutes = 1,
|
|
[switch]$SkipClicker
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$Root = Split-Path -Parent $PSScriptRoot
|
|
|
|
function Info($m) { Write-Host " $m" }
|
|
function Warn($m) { Write-Host " ! $m" -ForegroundColor Yellow }
|
|
|
|
<#
|
|
PM2 ships a PowerShell shim, and anything it writes to stderr becomes a
|
|
NativeCommandError. With $ErrorActionPreference = 'Stop' that is terminating,
|
|
so a routine "process not found" from `pm2 delete` aborts the whole install.
|
|
Redirecting at the call site does not help: the error is raised inside
|
|
pm2.ps1, not here. Drop to Continue for the duration and judge the result by
|
|
$LASTEXITCODE instead.
|
|
#>
|
|
function Invoke-Native {
|
|
param([string]$Exe, [string[]]$Arguments)
|
|
$prev = $ErrorActionPreference
|
|
$ErrorActionPreference = 'Continue'
|
|
try {
|
|
& $Exe @Arguments 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
|
return $LASTEXITCODE
|
|
} finally {
|
|
$ErrorActionPreference = $prev
|
|
}
|
|
}
|
|
|
|
function Invoke-Pm2 {
|
|
param([string[]]$Arguments, [switch]$Quiet)
|
|
$prev = $ErrorActionPreference
|
|
$ErrorActionPreference = 'Continue'
|
|
try {
|
|
$output = & pm2 @Arguments 2>&1
|
|
$code = $LASTEXITCODE
|
|
if (-not $Quiet -and $code -ne 0) {
|
|
$output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
|
}
|
|
return $code
|
|
} finally {
|
|
$ErrorActionPreference = $prev
|
|
}
|
|
}
|
|
|
|
Info "project root: $Root"
|
|
|
|
# -- PM2 ---------------------------------------------------------------------
|
|
if (-not (Get-Command pm2 -ErrorAction SilentlyContinue)) {
|
|
Info 'installing PM2 globally...'
|
|
if ((Invoke-Native 'npm' @('install', '-g', 'pm2')) -ne 0) { throw 'npm install -g pm2 failed' }
|
|
# A fresh global install is not on this session's PATH yet.
|
|
$prevEap = $ErrorActionPreference
|
|
$ErrorActionPreference = 'Continue'
|
|
$npmPrefix = (& npm prefix -g 2>$null | Select-Object -First 1)
|
|
$ErrorActionPreference = $prevEap
|
|
if ($npmPrefix) { $npmPrefix = $npmPrefix.Trim() }
|
|
$env:PATH = "$npmPrefix;$env:PATH"
|
|
if (-not (Get-Command pm2 -ErrorAction SilentlyContinue)) {
|
|
throw 'PM2 installed but not found on PATH. Open a new terminal and re-run.'
|
|
}
|
|
}
|
|
Info "pm2: $((Get-Command pm2).Source)"
|
|
|
|
Push-Location $Root
|
|
try {
|
|
# Run Next directly rather than through `npm start`. On Windows `npm` is a
|
|
# .cmd shim, so PM2 would supervise the shim while the real server ran as
|
|
# its child - restarts and stops then miss the process that matters.
|
|
$nextBin = Join-Path $Root 'node_modules\next\dist\bin\next'
|
|
if (-not (Test-Path $nextBin)) { throw "next not found at $nextBin - run npm install first" }
|
|
|
|
Invoke-Pm2 @('delete', 'autofirmer') -Quiet | Out-Null # absent is fine
|
|
if ((Invoke-Pm2 @('start', $nextBin, '--name', 'autofirmer', '--interpreter', 'node', '--', 'start')) -ne 0) {
|
|
throw 'pm2 start autofirmer failed'
|
|
}
|
|
Info 'pm2: autofirmer registered'
|
|
|
|
if (-not $SkipClicker) {
|
|
# --interpreter wants one executable, so resolve the real path rather
|
|
# than passing something like "py -3".
|
|
$pyCmdFile = Join-Path $Root 'scripts\.python-cmd'
|
|
$pyCmd = if (Test-Path $pyCmdFile) { (Get-Content $pyCmdFile -Raw).Trim() } else { 'python' }
|
|
$pyExe = $null
|
|
$prevEap = $ErrorActionPreference
|
|
$ErrorActionPreference = 'Continue'
|
|
try {
|
|
$pyExe = (& ([scriptblock]::Create("$pyCmd -c `"import sys;print(sys.executable)`"")) 2>$null | Select-Object -First 1)
|
|
} catch { } finally { $ErrorActionPreference = $prevEap }
|
|
|
|
if ($pyExe -and (Test-Path $pyExe)) {
|
|
Invoke-Pm2 @('delete', 'clicker') -Quiet | Out-Null
|
|
if ((Invoke-Pm2 @('start', (Join-Path $Root 'clicker\runner.py'), '--name', 'clicker', '--interpreter', $pyExe)) -eq 0) {
|
|
Info "pm2: clicker registered ($pyExe)"
|
|
} else {
|
|
Warn 'pm2 start clicker failed - the dashboard is unaffected'
|
|
}
|
|
} else {
|
|
Warn 'python not found - skipping the clicker. Re-run setup once Python is installed.'
|
|
}
|
|
}
|
|
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
|
|
# -- 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'
|
|
|
|
@"
|
|
@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
|
|
|
|
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'
|
|
}
|
|
|
|
# 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. 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"
|