Public/Request-FlightRecorderDump.ps1

# Copyright (c) 2026 Jeffrey Snover. All rights reserved.
# Licensed under the MIT License. See LICENSE file in the project root.

function Request-FlightRecorderDump {
    <#
    .SYNOPSIS
        Requests a flight recorder dump from a running taxonomy process via named pipe.

    .DESCRIPTION
        Connects to the flight recorder named pipe for a given PID, sends a
        dump or summary request. Dump writes events to disk and returns the
        file path; summary returns in-memory stats without I/O.

    .PARAMETER PID
        Process ID of the taxonomy process. Can also be piped from Get-TaxonomyProcess.

    .PARAMETER Summary
        Request a lightweight summary (event count, time range, debate ID)
        instead of a full dump to disk.

    .PARAMETER TimeoutSeconds
        Connection timeout in seconds. Default: 5.

    .EXAMPLE
        Request-FlightRecorderDump -PID 12345

    .EXAMPLE
        Request-FlightRecorderDump -PID 12345 -Summary

    .EXAMPLE
        Get-TaxonomyProcess | Request-FlightRecorderDump

    .EXAMPLE
        Get-TaxonomyProcess -Type electron | Request-FlightRecorderDump | Get-FlightRecorderReport
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [int]$PID,

        [Parameter()]
        [switch]$Summary,

        [Parameter()]
        [int]$TimeoutSeconds = 5
    )

    process {
        $pipeName = "taxonomy-flight-recorder-$PID"

        try {
            $pipe = [System.IO.Pipes.NamedPipeClientStream]::new(
                '.',
                $pipeName,
                [System.IO.Pipes.PipeDirection]::InOut
            )

            $timeoutMs = $TimeoutSeconds * 1000
            try {
                $pipe.Connect($timeoutMs)
            } catch [TimeoutException] {
                throw (New-ActionableError `
                    -Goal "Request flight recorder dump from PID $PID" `
                    -Problem "Named pipe '$pipeName' did not respond within ${TimeoutSeconds}s" `
                    -Location 'Request-FlightRecorderDump' `
                    -NextSteps @(
                        "Verify the process is running: Get-Process -Id $PID"
                        'Ensure the flight recorder pipe listener is enabled in the app'
                    ))
            } catch {
                throw (New-ActionableError `
                    -Goal "Request flight recorder dump from PID $PID" `
                    -Problem "Cannot connect to pipe '$pipeName': $($_.Exception.Message)" `
                    -Location 'Request-FlightRecorderDump' `
                    -NextSteps @(
                        "Verify the process is running: Get-Process -Id $PID"
                        'Check that the flight recorder pipe listener is active'
                        'Run Get-TaxonomyProcess to find valid PIDs'
                    ))
            }

            $writer = [System.IO.StreamWriter]::new($pipe)
            $reader = [System.IO.StreamReader]::new($pipe)

            try {
                $actionName = if ($Summary) { 'summary' } else { 'dump' }
                $request = @{ action = $actionName } | ConvertTo-Json -Compress
                $writer.WriteLine($request)
                $writer.Flush()

                $pipe.WaitForPipeDrain()

                $responseLine = $reader.ReadLine()
                if (-not $responseLine) {
                    throw (New-ActionableError `
                        -Goal "Request flight recorder $actionName from PID $PID" `
                        -Problem 'Received empty response from flight recorder pipe' `
                        -Location 'Request-FlightRecorderDump' `
                        -NextSteps @('The process may have closed the connection unexpectedly'))
                }

                $response = $responseLine | ConvertFrom-Json

                $result = [PSCustomObject]@{
                    PID        = $PID
                    Action     = $actionName
                    EventCount = if ($response.PSObject.Properties['event_count']) {
                                     $response.event_count
                                 } else { $null }
                    DebateId   = if ($response.PSObject.Properties['debate_id']) {
                                     $response.debate_id
                                 } else { $null }
                }

                if ($Summary) {
                    if ($response.PSObject.Properties['buffer_size_bytes']) {
                        $result | Add-Member -NotePropertyName BufferSizeBytes -NotePropertyValue $response.buffer_size_bytes
                    }
                } else {
                    $result | Add-Member -NotePropertyName Path -NotePropertyValue $response.path
                    if ($response.PSObject.Properties['size_bytes']) {
                        $result | Add-Member -NotePropertyName SizeBytes -NotePropertyValue $response.size_bytes
                    }
                }

                if ($response.PSObject.Properties['time_range']) {
                    $result | Add-Member -NotePropertyName TimeRange -NotePropertyValue $response.time_range
                }

                $result
            } finally {
                $reader.Dispose()
                $writer.Dispose()
            }
        } finally {
            if ($pipe) { $pipe.Dispose() }
        }
    }
}