# 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 }