<# .SYNOPSIS Captures one launcher child PID by its unique isolated session-config path. .DESCRIPTION Writes a sanitized gate-only sidecar. It never reads the session-config contents and records no command line, account, character, or credential. #> [CmdletBinding(DefaultParameterSetName = 'Path')] param( [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [string]$SessionConfigPath, [Parameter(Mandatory = $true, ParameterSetName = 'Directory')] [string]$SessionsDirectory, [Parameter(ParameterSetName = 'Directory')] [DateTimeOffset]$CreatedAfterUtc = [DateTimeOffset]::MinValue, [Parameter(Mandatory = $true)][string]$ReportPath, [ValidateRange(1, 60)][int]$WaitSeconds = 10 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ($PSVersionTable.PSVersion.Major -lt 7) { throw 'Campaign LA PID capture requires PowerShell 7 or newer.' } . (Join-Path $PSScriptRoot 'CampaignLaProcessCorrelation.ps1') if ($PSCmdlet.ParameterSetName -eq 'Path') { if (-not [IO.Path]::IsPathFullyQualified($SessionConfigPath)) { throw '-SessionConfigPath must be absolute.' } $SessionConfigPath = [IO.Path]::GetFullPath($SessionConfigPath) if (-not (Test-Path -LiteralPath $SessionConfigPath -PathType Leaf)) { throw "Session config does not exist: $SessionConfigPath" } } else { if (-not [IO.Path]::IsPathFullyQualified($SessionsDirectory)) { throw '-SessionsDirectory must be absolute.' } $SessionsDirectory = [IO.Path]::TrimEndingDirectorySeparator( [IO.Path]::GetFullPath($SessionsDirectory)) if (-not (Test-Path -LiteralPath $SessionsDirectory -PathType Container)) { throw "Sessions directory does not exist: $SessionsDirectory" } } if (-not [IO.Path]::IsPathFullyQualified($ReportPath)) { throw '-ReportPath must be absolute.' } $ReportPath = [IO.Path]::GetFullPath($ReportPath) if (Test-Path -LiteralPath $ReportPath) { throw '-ReportPath must be fresh.' } $deadline = [DateTime]::UtcNow.AddSeconds($WaitSeconds) do { if ($PSCmdlet.ParameterSetName -eq 'Path') { $correlations = @(Get-CampaignLaSessionProcessCorrelations | Where-Object { $comparison = if ($IsWindows) { [StringComparison]::OrdinalIgnoreCase } else { [StringComparison]::Ordinal } [string]::Equals( $_.SessionConfigPath, $SessionConfigPath, $comparison) }) } else { $comparison = if ($IsWindows) { [StringComparison]::OrdinalIgnoreCase } else { [StringComparison]::Ordinal } $prefix = $SessionsDirectory + [IO.Path]::DirectorySeparatorChar $correlations = @(Get-CampaignLaSessionProcessCorrelations | Where-Object { $_.SessionConfigPath.StartsWith($prefix, $comparison) -and [IO.Path]::GetFileName($_.SessionConfigPath) -ceq 'session.json' -and (Test-Path -LiteralPath $_.SessionConfigPath -PathType Leaf) -and (Get-Item -LiteralPath $_.SessionConfigPath).LastWriteTimeUtc -ge $CreatedAfterUtc.UtcDateTime }) } if ($correlations.Count -eq 1) { break } if ($correlations.Count -gt 1) { throw "More than one process uses the isolated session config." } Start-Sleep -Milliseconds 100 } while ([DateTime]::UtcNow -lt $deadline) if ($correlations.Count -ne 1) { throw 'No live process uses the isolated session config.' } $SessionConfigPath = [IO.Path]::GetFullPath($correlations[0].SessionConfigPath) $directory = Split-Path -Parent $ReportPath if (-not [string]::IsNullOrEmpty($directory)) { $null = New-Item -ItemType Directory -Force -Path $directory } $report = [ordered]@{ schemaVersion = 1 kind = 'campaign-la-session-process-capture' processId = [int]$correlations[0].ProcessId sessionId = [IO.Path]::GetFileName( [IO.Path]::GetDirectoryName($SessionConfigPath)) sessionConfigFile = [IO.Path]::GetFileName($SessionConfigPath) capturedUtc = [DateTime]::UtcNow.ToString('O') } $report | ConvertTo-Json -Depth 3 | Set-Content -LiteralPath $ReportPath -Encoding utf8NoBOM Write-Host "Campaign LA process capture: $ReportPath"