private/Debug-LongRunningProcess.ps1

function Debug-LongRunningProcess {
    [CmdletBinding()]
    Param (
        [Parameter( Mandatory = $true )]
        [System.Diagnostics.Process]$Process
    )

    # Maybe try-catch this in case the Assemblys aren't available?
    Add-Type -AssemblyName UIAutomationClient
    Add-Type -AssemblyName UIAutomationTypes

    function Get-WindowInfo {
        [CmdletBinding()]
        Param (
            [IntPtr]$WindowHandle,
            [switch]$IncludeUIAInfo
        )

        [int]$GWL_STYLE = -16
        [Uint32]$WS_DISABLED = 0x08000000

        $IsVisible = [LSUClient.User32]::IsWindowVisible($WindowHandle)

        $style = [LSUClient.User32]::GetWindowLong($WindowHandle, $GWL_STYLE)
        [LSUClient.User32+RECT]$RECT = New-Object 'LSUClient.User32+RECT'
        $null = [LSUClient.User32]::GetWindowRect($WindowHandle, [ref]$RECT)

        $InfoHashtable = @{
            'Width'      = $RECT.Right - $RECT.Left
            'Height'     = $RECT.Bottom - $RECT.Top
            'IsVisible'  = $IsVisible
            'IsDisabled' = ($style -band $WS_DISABLED) -eq $WS_DISABLED
            'StyleHex'   = '0x{0:X8}' -f $style
            'UIAWindowTitle' = ''
            'UIAElements'    = @()
        }

        if ($IncludeUIAInfo) {
            $WindowUIA = $null
            try {
                # If a window(handle) doesn't exist anymore this throws an ElementNotAvailable exception
                $WindowUIA = [System.Windows.Automation.AutomationElement]::FromHandle($WindowHandle)
            }
            catch {}
            if ($WindowUIA) {
                # Get element text by implementing https://stackoverflow.com/a/23851560
                $patternObj = $null
                if ($WindowUIA.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref] $patternObj)) {
                    $ElementText = $patternObj.Current.Value
                } elseif ($WindowUIA.TryGetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern, [ref] $patternObj)) {
                    $ElementText = $patternObj.DocumentRange.GetText(-1).TrimEnd("`r") # often there is an extra CR hanging off the end
                } else {
                    $ElementText = $WindowUIA.Current.Name
                }

                if ([string]::IsNullOrWhiteSpace($ElementText)) {
                    # If the ElementText is entirely blank (e.g. empty terminal window)
                    # then discard the whitespace and just set it to an empty string
                    $ElementText = ''
                } else {
                    # If there is non-whitespace content in the ElementText,
                    # only trim whitespace from the end of every line
                    [string[]]$ElementText = $ElementText.Split(
                        [string[]]("`r`n", "`r", "`n"),
                        [StringSplitOptions]::None
                    ) | ForEach-Object -MemberName TrimEnd
                }

                $InfoHashtable['UIAWindowTitle'] = $ElementText -join "`r`n"

                $UIADescendants = $WindowUIA.FindAll([Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
                $UIAElements = foreach ($UIAE in @($UIADescendants)) {
                    # Get element text by implementing https://stackoverflow.com/a/23851560
                    $patternObj = $null
                    if ($UIAE.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref] $patternObj)) {
                        $ElementText = $patternObj.Current.Value
                    } elseif ($UIAE.TryGetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern, [ref] $patternObj)) {
                        $ElementText = $patternObj.DocumentRange.GetText(-1).TrimEnd("`r") # often there is an extra CR hanging off the end
                    } else {
                        $ElementText = $UIAE.Current.Name
                    }

                    if ([string]::IsNullOrWhiteSpace($ElementText)) {
                        # If the ElementText is entirely blank (e.g. empty terminal window)
                        # then discard the whitespace and just set it to an empty string
                        $ElementText = ''
                    } else {
                        # If there is non-whitespace content in the ElementText,
                        # only trim whitespace from the end of every line
                        [string[]]$ElementText = $ElementText.Split(
                            [string[]]("`r`n", "`r", "`n"),
                            [StringSplitOptions]::None
                        ) | ForEach-Object -MemberName TrimEnd
                    }

                    [PSCustomObject]@{
                        'ControlType' = $UIAE.Current.ControlType.ProgrammaticName
                        'Text' = $ElementText -join "`r`n"
                        'XPosition' = $UIAE.Current.BoundingRectangle.X
                        'YPosition' = $UIAE.Current.BoundingRectangle.Y
                    }
                }

                if ($UIAElements) {
                    $InfoHashtable['UIAElements'] = @($UIAElements | Sort-Object -Property YPosition, XPosition)
                }
            }
        }

        return [PSCustomObject]$InfoHashtable
    }

    # Look into the process
    [bool]$AllThreadsWaiting = $true
    [UInt32]$WindowCount     = 0
    $InteractableWindows     = [System.Collections.Generic.List[PSObject]]::new()
    $CimProcessInformation   = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId = $($Process.ID)" -Property ParentProcessId, CommandLine -Verbose:$false

    Write-Debug "Process $($Process.ID) ('$($Process.ProcessName)'):"
    Write-Debug " Session: $($Process.SessionId)"
    if ($CimProcessInformation.CommandLine) {
        Write-Debug " CommandLine: $($CimProcessInformation.CommandLine)"
    } else {
        Write-Debug " CommandLine: <BLANK / INACCESSIBLE>"
    }
    Write-Debug " StartTime: $($Process.StartTime.TimeOfDay)"
    if ($CimProcessInformation.ParentProcessId) {
        Write-Debug " Parent Id: $($CimProcessInformation.ParentProcessId)"
    } else {
        Write-Debug " Parent Id: <BLANK / INACCESSIBLE>"
    }

    if ($Process.Threads.ThreadState -ne 'Wait') {
        $AllThreadsWaiting = $false
    }

    foreach ($Thread in $Process.Threads) {
        $ThreadWindows = [System.Collections.Generic.List[IntPtr]]::new()
        $null = [LSUClient.User32]::EnumThreadWindows($thread.id, { Param($hwnd, $lParam) $ThreadWindows.Add($hwnd); return $true }, [System.IntPtr]::Zero)

        if ($ThreadWindows) {
            Write-Debug " Thread $($thread.id) in state $($thread.ThreadState) ($($thread.WaitReason)) has $($ThreadWindows.Count) windows:"
        } else {
            Write-Debug " Thread $($thread.id) in state $($thread.ThreadState) ($($thread.WaitReason)) has no windows"
        }
        foreach ($window in $ThreadWindows) {
            $WindowCount++

            $WindowInfo = Get-WindowInfo -WindowHandle $window -IncludeUIAInfo
            if ($WindowInfo.IsVisible -and -not $WindowInfo.IsDisabled -and $WindowInfo.Width -gt 0 -and $WindowInfo.Height -gt 0) {
                $InteractableWindows.Add([PSCustomObject]@{
                    'WindowTitle'    = $WindowInfo.UIAWindowTitle
                    'WindowElements' = $WindowInfo.UIAElements
                    'WindowText'     = if ($WindowInfo.UIAElements) { $WindowInfo.UIAElements.Text }
                })
            }

            # Even if a window is not interactable (not visible and/or disabled) it is worth logging in debug mode,
            # because for example windows in session 0 will never be visible or interactable but can still open and cause a hang
            Write-Debug " ThreadWindow ${window}, Title: $($WindowInfo.UIAWindowTitle)"
            Write-Debug " IsVisible: $($WindowInfo.IsVisible), IsDisabled: $($WindowInfo.IsDisabled), Style: $($WindowInfo.StyleHex), Size: $($WindowInfo.Width) x $($WindowInfo.Height):"
            Write-Debug " UIA Info: Got $($WindowInfo.UIAElements.Count) UIAElements from this window handle:"
            foreach ($UIAElement in $WindowInfo.UIAElements) {
                if ($UIAElement.Text) {
                    Write-Debug " Type: $($UIAElement.ControlType), Text: $($UIAElement.Text -replace '(?s)^(.{60})(.*)', '$1...')"
                } else {
                    Write-Debug " Type: $($UIAElement.ControlType), no Text"
                }
            }
        }
    }

    return [PSCustomObject]@{
        'ProcessName'         = $Process.ProcessName
        'AllThreadsWaiting'   = $AllThreadsWaiting
        'WindowCount'         = $WindowCount
        'InteractableWindows' = $InteractableWindows
    }
}