param( [string]$ProjectRoot = "" ) $ErrorActionPreference = "SilentlyContinue" if ([string]::IsNullOrWhiteSpace($ProjectRoot)) { $ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path } else { $ProjectRoot = (Resolve-Path $ProjectRoot).Path } $toolDir = Join-Path $ProjectRoot "tools\oem-batch-builder" $activeRunPath = Join-Path $toolDir "active-run.json" $runsDir = Join-Path $toolDir "runs" $currentPid = $PID $killed = New-Object System.Collections.Generic.List[int] function Write-CleanupLog([string]$Message) { $stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Output "[$stamp] $Message" } function Write-JsonNoBom($Path, $Value) { $json = $Value | ConvertTo-Json -Depth 10 $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($Path, $json, $utf8NoBom) } function Get-ProcByPid([int]$ProcId) { Get-CimInstance Win32_Process -Filter "ProcessId=$ProcId" } function Test-IsProjectBuildProcess($Proc) { if (-not $Proc) { return $false } if ([int]$Proc.ProcessId -eq [int]$currentPid) { return $false } $name = [string]$Proc.Name $cmd = [string]$Proc.CommandLine $exe = [string]$Proc.ExecutablePath $text = "$cmd`n$exe" $lower = $text.ToLowerInvariant() $rootLower = $ProjectRoot.ToLowerInvariant() if ($name -ieq "OEMBatchBuilderTool.exe") { return $false } if ($lower.Contains("tools\oem-batch-builder\worker.cjs")) { return $true } if ($lower.Contains("tools\oem-batch-builder\main.cjs")) { return $true } if ($lower.Contains("electron-builder") -and $lower.Contains("--win") -and $lower.Contains("--x64")) { return $true } if ($lower.Contains("npm.cmd run build:win") -or $lower.Contains("npm run build:win")) { return $true } if (-not $lower.Contains($rootLower)) { return $false } if ($lower.Contains("\dist-release-final\")) { return $true } if ($lower.Contains("\build\oem-batch-temp\")) { return $true } if ($lower.Contains("dist-release-final\win-unpacked")) { return $true } if ($lower.Contains("node_modules\.bin\electron-builder")) { return $true } if ($lower.Contains("electron-builder.cmd")) { return $true } if ($lower.Contains("electron-builder --win")) { return $true } if ($lower.Contains("npm.cmd run build:win")) { return $true } if ($lower.Contains("npm run build:win")) { return $true } if ($lower.Contains("npm.cmd run prepare-package")) { return $true } if ($lower.Contains("npm run prepare-package")) { return $true } if ($lower.Contains("npm.cmd run build")) { return $true } if ($lower.Contains("npm run build")) { return $true } if ($lower.Contains("vite build")) { return $true } if ($lower.Contains("apply-oem.cjs")) { return $true } if ($lower.Contains("scripts\prepare-package.cjs")) { return $true } if ($lower.Contains("scripts\build-resource-artifacts.cjs")) { return $true } if ($lower.Contains("scripts\fix-latest-yml.cjs")) { return $true } if ($lower.Contains("scripts\assemble-release-artifacts.cjs")) { return $true } if ($lower.Contains("scripts\clean-build-artifacts.cjs")) { return $true } return $false } function Test-IsWorkerPidProcess($Proc) { if (-not $Proc) { return $false } $cmd = ([string]$Proc.CommandLine).ToLowerInvariant() return $cmd.Contains("tools\oem-batch-builder\worker.cjs") } function Test-ToolInstanceIsAlive() { $instances = @(Get-CimInstance Win32_Process | Where-Object { if (-not $_) { return $false } if ([int]$_.ProcessId -eq [int]$currentPid) { return $false } if ([string]$_.Name -ine "OEMBatchBuilderTool.exe") { return $false } $cmd = ([string]$_.CommandLine).ToLowerInvariant() $cmd.Contains("tools\oem-batch-builder") }) if ($instances.Count -gt 0) { $ids = ($instances | ForEach-Object { $_.ProcessId }) -join "," Write-CleanupLog "OEM batch builder is already running. Skip cleanup and let single-instance focus it. PIDs=$ids" return $true } return $false } function Test-ActiveWorkerIsAlive() { if (-not (Test-Path $activeRunPath)) { return $false } try { $active = Get-Content -Path $activeRunPath -Raw -Encoding UTF8 | ConvertFrom-Json if (-not $active.running) { return $false } $candidatePids = New-Object System.Collections.Generic.List[int] if ($active.pid) { $candidatePids.Add([int]$active.pid) | Out-Null } if ($active.sessionDir) { $workerPidPath = Join-Path ([string]$active.sessionDir) "worker.pid" if (Test-Path $workerPidPath) { $pidText = (Get-Content -Path $workerPidPath -Raw).Trim() if ($pidText -match "^\d+$") { $candidatePids.Add([int]$pidText) | Out-Null } } } foreach ($candidatePid in $candidatePids) { $proc = Get-ProcByPid $candidatePid if ($proc -and (Test-IsWorkerPidProcess $proc)) { Write-CleanupLog "Active OEM build is still running. Skip cleanup. PID=$candidatePid Session=$($active.sessionDir)" return $true } } } catch { Write-CleanupLog "Failed to inspect active-run.json: $($_.Exception.Message)" } return $false } function Stop-Tree([int]$ProcId, [string]$Reason, [bool]$WorkerPidOnly = $false) { if ($ProcId -le 0 -or $ProcId -eq $currentPid) { return } if ($killed.Contains($ProcId)) { return } $proc = Get-ProcByPid $ProcId if (-not $proc) { return } if ($WorkerPidOnly -and -not (Test-IsWorkerPidProcess $proc)) { Write-CleanupLog "Skip PID $ProcId ($($proc.Name)): $Reason points to a non-worker process" return } if (-not (Test-IsProjectBuildProcess $proc)) { return } Write-CleanupLog "Killing PID $ProcId ($($proc.Name)): $Reason" & taskkill.exe /F /T /PID $ProcId | Out-Null $killed.Add($ProcId) | Out-Null } function Stop-ActiveRunState([string]$Message) { if (-not (Test-Path $activeRunPath)) { return } try { $state = Get-Content -Path $activeRunPath -Raw -Encoding UTF8 | ConvertFrom-Json if ($state.running -or $killed.Count -gt 0) { $state | Add-Member -NotePropertyName "running" -NotePropertyValue $false -Force $state | Add-Member -NotePropertyName "stoppedAt" -NotePropertyValue ((Get-Date).ToUniversalTime().ToString("o")) -Force $state | Add-Member -NotePropertyName "message" -NotePropertyValue $Message -Force Write-JsonNoBom $activeRunPath $state Write-CleanupLog "Marked active-run stopped: $Message" } } catch { Write-CleanupLog "Failed to update active-run.json: $($_.Exception.Message)" } } Write-CleanupLog "Cleanup started. ProjectRoot=$ProjectRoot" if (Test-ToolInstanceIsAlive) { Write-CleanupLog "Cleanup skipped because the tool process is alive." exit 0 } if (Test-ActiveWorkerIsAlive) { Write-CleanupLog "Cleanup skipped because an active worker is alive." exit 0 } if (Test-Path $activeRunPath) { try { $active = Get-Content -Path $activeRunPath -Raw -Encoding UTF8 | ConvertFrom-Json if ($active.pid) { Stop-Tree ([int]$active.pid) "active-run pid" $true } if ($active.launcherPid) { Stop-Tree ([int]$active.launcherPid) "active-run launcherPid" } if ($active.sessionDir) { $workerPidPath = Join-Path ([string]$active.sessionDir) "worker.pid" if (Test-Path $workerPidPath) { $pidText = (Get-Content -Path $workerPidPath -Raw).Trim() if ($pidText -match "^\d+$") { Stop-Tree ([int]$pidText) "active session worker.pid" $true } } } } catch { Write-CleanupLog "Failed to read active-run.json: $($_.Exception.Message)" } } Get-CimInstance Win32_Process | Where-Object { Test-IsProjectBuildProcess $_ } | ForEach-Object { Stop-Tree ([int]$_.ProcessId) "matched OEM/build command" } Start-Sleep -Milliseconds 800 $remaining = @(Get-CimInstance Win32_Process | Where-Object { Test-IsProjectBuildProcess $_ }) if ($remaining.Count -gt 0) { foreach ($proc in $remaining) { Write-CleanupLog "Still running PID $($proc.ProcessId) ($($proc.Name))" } } else { Write-CleanupLog "Cleanup finished. Killed=$($killed.Count)" } if ($killed.Count -gt 0) { Stop-ActiveRunState "Stale OEM build processes were cleaned before launching the tool. Killed=$($killed.Count)" }