acdream/tools/capture-campaign-la-session-process.ps1

109 lines
4.2 KiB
PowerShell

<#
.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"