Files
genshin-assistant/scripts/kill-stale-instances.ps1

151 lines
7.4 KiB
PowerShell

# Beendet alle laufenden Prozesse dieses Projekts (Electron, Node/Vite, und
# einen verwaisten lokalen Input-Helper-Kindprozess), bevor eine neue Instanz
# startet.
#
# Warum das noetig ist: Ein normales Schliessen der App (X-Button) raeumt
# sauber auf (main.ts: app.on("will-quit") beendet den gespawnten
# input-helper.ps1-Kindprozess). Wird die App aber gewaltsam beendet - z.B.
# ueber Task-Manager "Task beenden" oder ein Skript, das Stop-Process -Force
# auf den Electron-Prozess anwendet - greift dieser Cleanup-Handler NICHT,
# und der Kindprozess (PowerShell mit dem Input-Helper) bleibt als Waise
# aktiv, obwohl das Hauptfenster laengst weg ist. Dieses Skript faengt genau
# das ab: es wird vor JEDEM Start (npm run dev und npm run dev:admin, ueber
# den "predev"-Hook) ausgefuehrt und raeumt vorherige Instanzen kompromisslos
# weg, egal wie sie beendet wurden.
param(
[switch]$SelfTest
)
$ErrorActionPreference = "Stop"
$project = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$electronPath = Join-Path $project "node_modules\electron\dist\electron.exe"
$devControlPort = 17317
function ConvertTo-ComparablePath([string]$Value) {
if ([string]::IsNullOrWhiteSpace($Value)) { return "" }
try {
return [System.IO.Path]::GetFullPath($Value).TrimEnd("\").ToLowerInvariant()
} catch {
return $Value.Trim().TrimEnd("\").ToLowerInvariant()
}
}
function Test-CommandLineContainsPath([string]$CommandLine, [string]$CandidatePath) {
if ([string]::IsNullOrWhiteSpace($CommandLine) -or [string]::IsNullOrWhiteSpace($CandidatePath)) { return $false }
$normalizedCommand = $CommandLine.Replace("/", "\").ToLowerInvariant()
$normalizedCandidate = (ConvertTo-ComparablePath $CandidatePath).Replace("/", "\")
return $normalizedCommand.Contains($normalizedCandidate)
}
function Test-GaaStaleProcess([object]$Process, [string]$ProjectRoot, [string]$ElectronExecutable) {
$name = [string]$Process.Name
$executable = ConvertTo-ComparablePath ([string]$Process.ExecutablePath)
$commandLine = [string]$Process.CommandLine
if ($name -eq "electron.exe") {
return $executable -eq (ConvertTo-ComparablePath $ElectronExecutable)
}
if ($name -eq "InputHelper.exe") {
$helperPaths = @(
(Join-Path $ProjectRoot "native\input-helper\bin\publish\InputHelper.exe"),
(Join-Path $ProjectRoot "outputs\dist\win-unpacked\resources\input-helper\InputHelper.exe")
)
return @($helperPaths | Where-Object { $executable -eq (ConvertTo-ComparablePath $_) }).Count -gt 0
}
if ($name -eq "powershell.exe" -or $name -eq "pwsh.exe") {
$legacyHelper = Join-Path $env:APPDATA "genshin-artifact-assistant\input-helper.ps1"
return Test-CommandLineContainsPath $commandLine $legacyHelper
}
if ($name -ne "node.exe") { return $false }
# Node itself is shared by Codex and many other tools. Only the known dev
# entrypoints from this repository are safe to classify as stale GAA work.
$knownDevEntrypoints = @(
(Join-Path $ProjectRoot "node_modules\vite\bin\vite.js"),
(Join-Path $ProjectRoot "node_modules\concurrently\dist\bin\concurrently.js"),
(Join-Path $ProjectRoot "node_modules\wait-on\bin\wait-on"),
(Join-Path $ProjectRoot "node_modules\cross-env\src\bin\cross-env.js")
)
return @($knownDevEntrypoints | Where-Object { Test-CommandLineContainsPath $commandLine $_ }).Count -gt 0
}
if ($SelfTest) {
$cases = @(
@{ Name = "project electron"; Expected = $true; Process = [pscustomobject]@{ Name = "electron.exe"; ExecutablePath = $electronPath; CommandLine = "`"$electronPath`" ." } },
@{ Name = "other electron"; Expected = $false; Process = [pscustomobject]@{ Name = "electron.exe"; ExecutablePath = "C:\Other\electron.exe"; CommandLine = "" } },
@{ Name = "project vite"; Expected = $true; Process = [pscustomobject]@{ Name = "node.exe"; ExecutablePath = "C:\Program Files\nodejs\node.exe"; CommandLine = "node `"$(Join-Path $project 'node_modules\vite\bin\vite.js')`" --host 127.0.0.1" } },
@{ Name = "project concurrently"; Expected = $true; Process = [pscustomobject]@{ Name = "node.exe"; ExecutablePath = "C:\Program Files\nodejs\node.exe"; CommandLine = "node `"$(Join-Path $project 'node_modules\concurrently\dist\bin\concurrently.js')`" -k" } },
@{ Name = "Codex CUA working dir"; Expected = $false; Process = [pscustomobject]@{ Name = "node.exe"; ExecutablePath = "C:\Users\vmbao\AppData\Local\OpenAI\Codex\runtimes\cua_node\bin\node.exe"; CommandLine = "node kernel.js --working-dir `"$project`"" } },
@{ Name = "unrelated node in repo"; Expected = $false; Process = [pscustomobject]@{ Name = "node.exe"; ExecutablePath = "C:\Program Files\nodejs\node.exe"; CommandLine = "node custom-task.js --cwd `"$project`"" } },
@{ Name = "legacy helper"; Expected = $true; Process = [pscustomobject]@{ Name = "powershell.exe"; ExecutablePath = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"; CommandLine = "powershell -File `"$(Join-Path $env:APPDATA 'genshin-artifact-assistant\input-helper.ps1')`"" } }
)
$failedCases = @()
foreach ($case in $cases) {
$actual = Test-GaaStaleProcess $case.Process $project $electronPath
if ($actual -ne $case.Expected) { $failedCases += "$($case.Name): expected=$($case.Expected), actual=$actual" }
}
if ($failedCases.Count -gt 0) {
$failedCases | ForEach-Object { Write-Error $_ }
throw "$($failedCases.Count) stale-process classifier self-test(s) failed."
}
Write-Host "Stale-process classifier: $($cases.Count)/$($cases.Count) tests passed."
return
}
$killed = 0
$candidatePids = @{}
try {
$health = Invoke-RestMethod -Method Get -Uri "http://127.0.0.1:$devControlPort/health" -TimeoutSec 2 -ErrorAction Stop
if ($health.appBuild -and $health.appBuild.cwd -eq $project) {
Write-Host "Dev-Control-Port $devControlPort gehoert zu diesem Projekt (PID $($health.appBuild.pid), Signatur $($health.appBuild.signature)). Versuche Self-Shutdown..."
try {
Invoke-RestMethod -Method Get -Uri "http://127.0.0.1:$devControlPort/dev/shutdown?reason=restart" -TimeoutSec 2 -ErrorAction Stop | Out-Null
Start-Sleep -Milliseconds 900
} catch {
Write-Host "Self-Shutdown nicht verfuegbar oder fehlgeschlagen: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
} catch {
# No dev-control server or an older/stuck process; continue with process scan.
}
Get-CimInstance Win32_Process |
Where-Object { Test-GaaStaleProcess $_ $project $electronPath } |
ForEach-Object {
$candidatePids[[int]$_.ProcessId] = $_.Name
}
$failed = 0
foreach ($entry in $candidatePids.GetEnumerator()) {
Write-Host "Beende alte Instanz: $($entry.Value) (PID $($entry.Key))"
try {
Stop-Process -Id $entry.Key -Force -ErrorAction Stop
$killed++
} catch {
Write-Host "WARNUNG: Konnte PID $($entry.Key) nicht beenden: $($_.Exception.Message)" -ForegroundColor Yellow
$failed++
}
}
if ($killed -gt 0) {
# Windows braucht einen Moment, um Ports/Handles wirklich freizugeben.
Start-Sleep -Milliseconds 500
Write-Host "$killed alte Prozess(e) beendet."
}
if ($failed -gt 0) {
Write-Host "$failed alte Prozess(e) konnten nicht beendet werden. Wenn das ein Admin-Prozess ist, starte npm run dev:admin und bestaetige UAC oder schliesse die alte App manuell." -ForegroundColor Yellow
throw "$failed alte Prozess(e) konnten nicht beendet werden."
}
if ($killed -eq 0 -and $failed -eq 0) {
Write-Host "Keine alten Instanzen gefunden."
} else {
Start-Sleep -Milliseconds 300
}