Stop PM2's stderr from aborting the autostart installer
`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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
97ee03d13b
commit
7e4714c33d
@@ -34,56 +34,106 @@ $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 ─────────────────────────────────────────────────────────────────────
|
||||
# -- PM2 ---------------------------------------------------------------------
|
||||
if (-not (Get-Command pm2 -ErrorAction SilentlyContinue)) {
|
||||
Info 'installing PM2 globally...'
|
||||
npm install -g pm2
|
||||
if ($LASTEXITCODE -ne 0) { throw 'npm install -g pm2 failed' }
|
||||
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.
|
||||
$npmPrefix = (npm prefix -g).Trim()
|
||||
$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."
|
||||
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 {
|
||||
# delete + recreate rather than reusing, so a changed path or interpreter
|
||||
# actually takes effect
|
||||
pm2 delete autofirmer 2>$null | Out-Null
|
||||
pm2 start npm --name autofirmer -- start
|
||||
if ($LASTEXITCODE -ne 0) { throw 'pm2 start autofirmer failed' }
|
||||
# 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) {
|
||||
# pm2 --interpreter wants one executable, so resolve the real path
|
||||
# rather than passing something like "py -3".
|
||||
# --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
|
||||
try { $pyExe = (& ([scriptblock]::Create("$pyCmd -c `"import sys;print(sys.executable)`""))).Trim() } catch { }
|
||||
$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)) {
|
||||
pm2 delete clicker 2>$null | Out-Null
|
||||
pm2 start (Join-Path $Root 'clicker\runner.py') --name clicker --interpreter $pyExe
|
||||
if ($LASTEXITCODE -eq 0) { Info "pm2: clicker registered ($pyExe)" }
|
||||
else { Warn 'pm2 start clicker failed - the dashboard is unaffected' }
|
||||
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.'
|
||||
}
|
||||
}
|
||||
|
||||
pm2 save | Out-Null
|
||||
if ((Invoke-Pm2 @('save')) -ne 0) { throw 'pm2 save failed' }
|
||||
Info 'pm2: process list saved'
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# ── Scheduled tasks ─────────────────────────────────────────────────────────
|
||||
# -- Scheduled tasks ---------------------------------------------------------
|
||||
# LogonType Interactive is what grants the desktop the clicker needs.
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
|
||||
-LogonType Interactive -RunLevel Limited
|
||||
|
||||
Reference in New Issue
Block a user