# Process ownership is determined by checkout paths, never by a port or name alone. function Get-ArtemisUpgradeProcesses { param([string]$ProjectRoot, [object[]]$Processes, [int[]]$ProtectedIds = @()) $root = [IO.Path]::GetFullPath($ProjectRoot).TrimEnd('\') + '\' $venv = $root + '.venv\' $selected = @{} foreach ($process in $Processes) { if ($ProtectedIds -contains [int]$process.ProcessId) { continue } $executable = [string]$process.ExecutablePath $command = [string]$process.CommandLine $python = $process.Name -match '^(python(w)?|artemis(-admin)?)\.exe$' $inVenv = $executable.StartsWith($venv, [StringComparison]::OrdinalIgnoreCase) $explicitRoot = $command.Replace('/', '\').IndexOf($root, [StringComparison]::OrdinalIgnoreCase) -ge 0 $entrypoint = $command -match '(?i)(?:-m\s+(?:artemis(?:\s|\.|$)|apps\.admin_console(?:\.|\s)|mcp_server(?:\.|\s|$))|[\\/]artemis(?:\.exe|[\\/]__main__\.py)|[\\/]mcp_server[\\/])' if (($python -and $inVenv) -or ($explicitRoot -and $entrypoint -and ($python -or $process.Name -eq 'uv.exe'))) { $selected[[int]$process.ProcessId] = $process } } do { $added = $false foreach ($process in $Processes) { $id = [int]$process.ProcessId if ($selected.ContainsKey($id) -or $ProtectedIds -contains $id) { continue } # Shared device services and desktop applications can outlive a task. if ($process.Name -match '^(adb|codex|chrome|msedge|firefox|emulator|MuMu.*|Nemu.*|Nox.*|MEmu.*|LdVBox.*|HD-Player)\.exe$') { continue } if ($selected.ContainsKey([int]$process.ParentProcessId)) { $selected[$id] = $process $added = $true } } } while ($added) return @($selected.Values) } function Stop-ArtemisUpgradeProcesses { param([string]$ProjectRoot) $snapshot = @(Get-CimInstance Win32_Process -ErrorAction Stop) $protected = @($PID) $parentId = $PID do { $parent = $snapshot | Where-Object { [int]$_.ProcessId -eq $parentId } | Select-Object -First 1 $parentId = if ($parent) { [int]$parent.ParentProcessId } else { 0 } if ($parentId -and $protected -notcontains $parentId) { $protected += $parentId } else { break } } while ($true) $deadline = [DateTime]::UtcNow.AddSeconds(10) do { $targets = @(Get-ArtemisUpgradeProcesses -ProjectRoot $ProjectRoot -Processes $snapshot -ProtectedIds $protected) if (-not $targets.Count) { return } foreach ($target in $targets) { $id = [int]$target.ProcessId $current = Get-CimInstance Win32_Process -Filter "ProcessId = $id" -ErrorAction Stop if (-not $current -or $current.CreationDate -ne $target.CreationDate) { continue } Write-Host "[STOP] ARTEMIS PID $id ($($target.Name))" try { Stop-Process -Id $id -Force -ErrorAction Stop } catch { if (Get-CimInstance Win32_Process -Filter "ProcessId = $id" -ErrorAction Stop) { throw } } } Start-Sleep -Milliseconds 200 $snapshot = @(Get-CimInstance Win32_Process -ErrorAction Stop) } while ([DateTime]::UtcNow -lt $deadline) if (@(Get-ArtemisUpgradeProcesses -ProjectRoot $ProjectRoot -Processes $snapshot -ProtectedIds $protected).Count) { throw 'ARTEMIS processes are still running. Source/dependency updates were not started.' } }