`pm2 delete autofirmer` on a machine that has never registered it writes "[PM2][ERROR] Process or Namespace autofirmer not found" to stderr. PM2 ships a PowerShell shim, and under $ErrorActionPreference = 'Stop' any native stderr becomes a terminating NativeCommandError - so the routine "remove it if it is there" line killed the install before a single task was registered. The 2>$null at the call site was useless: the error is raised inside pm2.ps1. Route every pm2 and npm call through helpers that drop to 'Continue' and judge by $LASTEXITCODE. npm had the same latent problem, since its warnings also go to stderr. Also start Next directly instead of via `npm start`. On Windows npm is a .cmd shim, so PM2 was supervising the shim while the real server ran as its child - restarts and stops would have missed the process that actually matters. The file is now pure ASCII. PowerShell 5.1 reads a non-BOM UTF-8 script as ANSI, so the box-drawing characters in the comments were a latent hazard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
173 lines
7.1 KiB
PowerShell
173 lines
7.1 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.
|
|
|
|
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.
|
|
|
|
Idempotent - re-running replaces the tasks rather than duplicating them.
|
|
No elevation required.
|
|
|
|
.PARAMETER IntervalMinutes
|
|
How often to check master for updates. Default 5.
|
|
|
|
.PARAMETER SkipClicker
|
|
Register only the dashboard, leaving the clicker to be run by hand.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[int]$IntervalMinutes = 5,
|
|
[switch]$SkipClicker
|
|
)
|
|
|
|
$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 }
|
|
|
|
<#
|
|
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.'
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
|
|
-DontStopIfGoingOnBatteries -StartWhenAvailable `
|
|
-MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 1)
|
|
|
|
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"
|
|
}
|
|
|
|
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."
|
|
|
|
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 'Useful: pm2 list | pm2 logs autofirmer | pm2 logs clicker'
|