diff --git a/scripts/install-autostart.ps1 b/scripts/install-autostart.ps1 index fcda8f6..28d9263 100644 --- a/scripts/install-autostart.ps1 +++ b/scripts/install-autostart.ps1 @@ -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