<< Back to Script Library

Show zombie processes

Find processes which are in sessions which no longer exist or where there are no handle or thread objects open by that process which means that it will not be able to run (unless a UWMP process which are excluded).
If the kill parameter is set to true, all of these processes above will be killed.
Also find processes which are trying to exit but are unable to do so because another process has an open process or thread handle to the exiting process which prevents the process from completely exiting.
Zombie processes may be responsible for end user application usability issues and the cause for high session id numbers.
This script should be run a few times, a few minutes apart, in order to rule out processes which were in the process of closing handles they had open.
There is a known handle leak in ControlUp processes prior to release 8.2. With 8.2 there is still a situation that could result in a leak where the ControlUp agent is running on Windows 10 1809 and later or Server 2019 and the User Input Delay feature is enabled by default. See the 8.2 release notes for further information
Version: 3.14.32
Created: 2018-11-02
Modified: 2021-01-05
Creator: Guy Leech
Downloads: 865
Tags: Zombie processes
The Script Copy Script Copied to clipboard
<#
    Use low level Windows APIs to find processes which are exiting and see which processes have handles open to them

    @guyrleech 2019

    With ideas from:

    https://undocumented.ntinternals.net/
    https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ps/psquery/class.htm

    Modification History:

    18/01/19   GRL  Added thread handle analysis
    22/12/20   GRL  Added code to detect known leaky processes and show details of services hosted in any leaking svchost.exe processes
    23/12/20   GRL  Code optimisation, formatting changes
    24/12/20   GRL  Check for User Input Delay if CUAgent leaks as known MS issue, sort leaky service names
    27/12/20   GRL  Change message for 8.2+ CUAgent non-fault leak
    29/12/20   GRL  Replaced quser.exe parsing with calling WTS APIs via P/Invoke, added handle closing code from "Show Zombie Processes"
    04/01/21   GRL  Updated kill code
    05/01/21   GRL  Kill switch disabled
#>

[CmdletBinding()]

Param
(
    [Parameter(Mandatory=$false,HelpMessage='Whether to kill zombies found or not')]
    [string]$killParameter = 'False'
)

[bool]$kill = [System.Convert]::ToBoolean( $killParameter )
[int]$outputWidth = 250 ## we want some of the columns to wrap as we use newlines as delimiters not commas. This number works nicely on an HD (1920x1080) number with default CU console font sizes
[int]$recentlyCreated = 30 ## seconds within which we ignore processes created

$VerbosePreference = $(if( $PSBoundParameters[ 'verbose' ] ) { $VerbosePreference } else { 'SilentlyContinue' })
$DebugPreference = $(if( $PSBoundParameters[ 'debug' ] ) { $DebugPreference } else { 'SilentlyContinue' })
$ErrorActionPreference = $(if( $PSBoundParameters[ 'erroraction' ] ) { $ErrorActionPreference } else { 'Stop' })
$ProgressPreference = 'SilentlyContinue'

if( ( $PSWindow = (Get-Host).UI.RawUI ) -and ( $WideDimensions = $PSWindow.BufferSize ) )
{
    $WideDimensions.Width = $outputWidth
    $PSWindow.BufferSize = $WideDimensions
}

if( $PSVersionTable.PSVersion.Major -lt 3 )
{
    Write-Warning "This script must be run using PowerShell version 3.0 or higher, this is $($PSVersionTable.PSVersion.ToString())"
    Exit
}

$warnings = New-Object -TypeName System.Collections.Generic.List[string]
$dyingProcesses = New-Object -TypeName System.Collections.Generic.List[object]
$failedToDieProcesses = New-Object -TypeName System.Collections.Generic.List[object]

## Now use low level APIs to find processes which are trying to exit but are unable to do so because other processes have handles still open to them
Add-Type "
using System;
using System.Runtime.InteropServices;

    public static class NtDll
    {
        [DllImport(`"ntdll.dll`")]
        public static extern NT_STATUS NtQuerySystemInformation(
            [In] SYSTEM_INFORMATION_CLASS SystemInformationClass,
            [In] IntPtr SystemInformation,
            [In] int SystemInformationLength,
            [Out] out int ReturnLength);

        [DllImport(`"ntdll.dll`")]
        public static extern NT_STATUS NtQueryInformationProcess(
            [In] IntPtr ProcessHandle,
            [In] PROCESS_INFO_CLASS ProcessInformationClass,
            [In] IntPtr ProcessInformation,
            [In] int ProcessInformationLength,
            [Out] out int ReturnLength);
            
        [DllImport(`"ntdll.dll`")]
        public static extern NT_STATUS NtQueryInformationThread(
            [In] IntPtr ThreadHandle,
            [In] THREAD_INFO_CLASS ThreadInformationClass,
            [In] IntPtr ThreadInformation,
            [In] int ThreadInformationLength,
            [Out] out int ReturnLength);

        [DllImport(`"ntdll.dll`")]
        public static extern NT_STATUS NtGetNextProcess(
          [In] IntPtr ProcessHandle,
          [In] ProcessAccessRights DesiredAccess,
          [In] AttributeFlags HandleAttributes,
          [In] int Flags,
          [Out] out IntPtr NewProcessHandle);

        [DllImport(`"ntdll.dll`")]
        public static extern NT_STATUS NtGetNextThread(
          [In] IntPtr ProcessHandle,
          [In] IntPtr ThreadHandle,
          [In] ThreadAccessRights DesiredAccess,
          [In] AttributeFlags HandleAttributes,
          [In] int Flags,
          [Out] out IntPtr NewThreadHandle);
    }

 public static class Kernel32
 {
        [DllImport(`"kernel32.dll`", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool CloseHandle(
            [In] IntPtr hHandle );
            
  [DllImport(`"kernel32.dll`", SetLastError = true)]
   public static extern uint QueryDosDevice(string lpDeviceName, System.Text.StringBuilder lpTargetPath, int ucchMax);
 }

    public static class Msvcrt
    {
      [DllImport(`"msvcrt.dll`", SetLastError = true)]
   public static extern IntPtr memset(
                [In] IntPtr dest ,
                [In] int c ,
                [In] int count );
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct UNICODE_STRING
    {
        public UInt16 Length;
        public UInt16 MaximumLength;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string buffer;
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_SESSION_INFORMATION
    {
        public ulong SessionId ;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct ProcessBasicInformation
    {
        public int ExitStatus;
        public IntPtr PebBaseAddress;
        public IntPtr AffinityMask;
        public int BasePriority;
        public IntPtr UniqueProcessId;
        public IntPtr InheritedFromUniqueProcessId;
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct CLIENT_ID
    {
        public IntPtr UniqueProcess;
        public IntPtr UniqueThread;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct ThreadBasicInformation
    {
        public int ExitStatus;
        public IntPtr TebBaseAddress;
        public CLIENT_ID ClientId;
        public IntPtr AffinityMask;
        public int Priority;
        public int BasePriority;
    }

    [StructLayout(LayoutKind.Sequential)]
    public class ProcessExtendedBasicInformation
    {
        public IntPtr Size;
        public ProcessBasicInformation BasicInfo;
        public ProcessExtendedBasicInformationFlags Flags;
    }

 [StructLayout(LayoutKind.Sequential)]
    public struct SystemHandleEntry
    {
        public int OwnerProcessId;
        public byte ObjectTypeNumber;
        public byte Flags;
        public ushort Handle;
        public IntPtr Object;
        public int GrantedAccess;
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct SystemHandleTableInfoEntryEx
    {
        public UIntPtr Object;
        public IntPtr UniqueProcessId;
        public IntPtr HandleValue;
        public uint GrantedAccess;
        public ushort CreatorBackTraceIndex;
        public ushort ObjectTypeIndex;
        public uint HandleAttributes;
        public uint Reserved;
    }

 public enum SYSTEM_INFORMATION_CLASS
    {
        SystemBasicInformation = 0,
        SystemPerformanceInformation = 2,
        SystemTimeOfDayInformation = 3,
        SystemProcessInformation = 5,
        SystemProcessorPerformanceInformation = 8,
        SystemHandleInformation = 16,
        SystemInterruptInformation = 23,
        SystemExceptionInformation = 33,
        SystemRegistryQuotaInformation = 37,
        SystemLookasideInformation = 45 ,
        SystemExtendedHandleInformation = 64
    }
    
    public enum THREAD_INFO_CLASS
    {
        ThreadBasicInformation
    }

    public enum PROCESS_INFO_CLASS
    {
        ProcessBasicInformation = 0 ,
        ProcessDebugPort = 7 ,
        ProcessSessionInformation = 24 ,
        ProcessWow64Information = 26 ,
        ProcessImageFileName = 27 ,
        ProcessBreakOnTermination = 29 ,
        ProcessImageFileNameWin32 = 43 ,
        ProcessHandleTable = 58 ,
        ProcessSubsystemInformation = 75 
    }

 public enum OBJECT_INFORMATION_CLASS
    {
        ObjectBasicInformation = 0,
        ObjectNameInformation = 1,
        ObjectTypeInformation = 2,
        ObjectAllTypesInformation = 3,
        ObjectHandleInformation = 4
    }
    
    public enum ProcessExtendedBasicInformationFlags
    {
        None = 0,
        IsProtectedProcess = 0x00000001,
        IsWow64Process = 0x00000002,
        IsProcessDeleting = 0x00000004,
        IsCrossSessionCreate = 0x00000008,
        IsFrozen = 0x00000010,
        IsBackground = 0x00000020,
        IsStronglyNamed = 0x00000040,
        IsSecureProcess = 0x00000080,
        IsSubsystemProcess = 0x00000100,
    }
    
    public enum GenericAccessRights : uint
    {
        None = 0,
        Access0 = 0x00000001,
        Access1 = 0x00000002,
        Access2 = 0x00000004,
        Access3 = 0x00000008,
        Access4 = 0x00000010,
        Access5 = 0x00000020,
        Access6 = 0x00000040,
        Access7 = 0x00000080,
        Access8 = 0x00000100,
        Access9 = 0x00000200,
        Access10 = 0x00000400,
        Access11 = 0x00000800,
        Access12 = 0x00001000,
        Access13 = 0x00002000,
        Access14 = 0x00004000,
        Access15 = 0x00008000,
        Delete = 0x00010000,
        ReadControl = 0x00020000,
        WriteDac = 0x00040000,
        WriteOwner = 0x00080000,
        Synchronize = 0x00100000,
        AccessSystemSecurity = 0x01000000,
        MaximumAllowed = 0x02000000,
        GenericAll = 0x10000000,
        GenericExecute = 0x20000000,
        GenericWrite = 0x40000000,
        GenericRead = 0x80000000,
    }

    public enum ProcessAccessRights : uint
    {
        None = 0,
        Terminate = 0x0001,
        CreateThread = 0x0002,
        SetSessionId = 0x0004,
        VmOperation = 0x0008,
        VmRead = 0x0010,
        VmWrite = 0x0020,
        DupHandle = 0x0040,
        CreateProcess = 0x0080,
        SetQuota = 0x0100,
        SetInformation = 0x0200,
        QueryInformation = 0x0400,
        SuspendResume = 0x0800,
        QueryLimitedInformation = 0x1000,
        SetLimitedInformation = 0x2000,
        AllAccess = 0x1FFFFF,
        GenericRead = GenericAccessRights.GenericRead,
        GenericWrite = GenericAccessRights.GenericWrite,
        GenericExecute = GenericAccessRights.GenericExecute,
        GenericAll = GenericAccessRights.GenericAll,
        Delete = GenericAccessRights.Delete,
        ReadControl = GenericAccessRights.ReadControl,
        WriteDac = GenericAccessRights.WriteDac,
        WriteOwner = GenericAccessRights.WriteOwner,
        Synchronize = GenericAccessRights.Synchronize,
        MaximumAllowed = GenericAccessRights.MaximumAllowed,
        AccessSystemSecurity = GenericAccessRights.AccessSystemSecurity
    }
    
    public enum ThreadAccessRights : uint
    {
        Terminate = 0x0001 ,
        SuspendResume = 0x0002 ,
        GetContext = 0x0008 ,
        SetContext = 0x0010 ,
        QueryInformation = 0x0040 ,
        SetInformation = 0x0020 ,
        SetThreadToken = 0x0080 ,
        Impersonate = 0x0100 ,
        DirectImpersonation = 0x0200 ,
        SetLimitedInformation = 0x0400 ,
        QueryLimitedInformation = 0x0800 ,
        Resume = 0x1000
    }

    public enum AttributeFlags : uint
    {
        None = 0,
        Inherit = 0x00000002,
        Permanent = 0x00000010,
        Exclusive = 0x00000020,
        CaseInsensitive = 0x00000040,
        OpenIf = 0x00000080,
        OpenLink = 0x00000100,
        KernelHandle = 0x00000200,
        ForceAccessCheck = 0x00000400,
        IgnoreImpersonatedDevicemap = 0x00000800,
        DontReparse = 0x00001000,
    }
 public enum NT_STATUS
    {
        STATUS_SUCCESS = 0x00000000,
        STATUS_BUFFER_OVERFLOW = unchecked((int)0x80000005L),
        STATUS_INFO_LENGTH_MISMATCH = unchecked((int)0xC0000004L)
    }
" -ErrorAction Stop

function Get-AllHandles( )
{
    Param
    (
         [int]$onlyPid = -1 ,
         [int[]]$handleType = @()
    )
    $length = 0x10000
    $ptr = [IntPtr]::Zero

    while ($true)
    {
        $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($length)
        $wantedLength = 0
        $result = [NtDll]::NtQuerySystemInformation([SYSTEM_INFORMATION_CLASS]::SystemExtendedHandleInformation, $ptr, $length, [ref] $wantedLength)
        if ($result -eq [NT_STATUS]::STATUS_INFO_LENGTH_MISMATCH)
        {
            $length = [Math]::Max($length, $wantedLength)
            [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)
            $ptr = [IntPtr]::Zero
        }
        elseif ($result -eq [NT_STATUS]::STATUS_SUCCESS)
  {
            break
  }
        else
  {
            throw (New-Object System.ComponentModel.Win32Exception)
  }
    }

 if ([IntPtr]::Size -eq 4)
 {
  $handleCount = [System.Runtime.InteropServices.Marshal]::ReadInt32($ptr)
 }
 else
 {
  $handleCount = [System.Runtime.InteropServices.Marshal]::ReadInt64($ptr)
 }

 $offset = [IntPtr]::Size * 2 ## Reserver value after count
 $She = New-Object -TypeName SystemHandleTableInfoEntryEx
    $size = [System.Runtime.InteropServices.Marshal]::SizeOf($She)

    for ($i = 0; $i -lt $handleCount; $i++)
    {
        $thisHandle = [SystemHandleTableInfoEntryEx][System.Runtime.InteropServices.Marshal]::PtrToStructure([IntPtr]([long]$ptr + $offset),[Type]$She.GetType())
            
        if( ( ! $handleType -or ! $handleType.Count -or $handleType -contains $thisHandle.ObjectTypeIndex ) `
            -and ( $onlyPid -lt 0 -or $thisHandle.UniqueProcessId -eq $onlyPid ) )
        {
            $thisHandle
        }
        $offset += $size
    }

    if ($ptr -ne [IntPtr]::Zero)
 {
        [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)
        $ptr = [IntPtr]::Zero
 }
}

function Get-AllThreadsForProcess( [IntPtr]$processHandle )
{
    [int]$threadCount = 0
    [IntPtr]$threadHandle = [IntPtr]::Zero
    do
    {
        [NT_STATUS]$result = [NtDll]::NtGetNextThread( $processHandle , $threadHandle , [ThreadAccessRights]::QueryLimitedInformation , [AttributeFlags]::None , 0 , [ref]$threadHandle )
        if( $result -eq [NT_STATUS]::STATUS_SUCCESS -and $threadHandle )
  {
            $threadCount++
            ## call NtQueryInformationThread and get THREAD_BASIC_INFORMATION structure
            ## once we have open handles too all threads, we can iterate over the ones for this process later and match object ids to ones in all handle list to find pid like we do for processes
            
            $threadBasicInfo = New-Object 'ThreadBasicInformation'
            [int]$size = [System.Runtime.InteropServices.Marshal]::SizeOf($threadBasicInfo)
            [int]$returnedLength = 0
            [IntPtr]$ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal( $size )
            $threadQueryResult = [NtDll]::NtQueryInformationThread( $threadHandle , [THREAD_INFO_CLASS]::ThreadBasicInformation , $ptr , $size , [ref]$returnedLength )
            if( $threadQueryResult -eq [NT_STATUS]::STATUS_SUCCESS )
            {
                $threadBasicInfo = [System.Runtime.InteropServices.Marshal]::PtrToStructure( $ptr , [Type]$threadBasicInfo.GetType() )
                Add-Member -InputObject $threadBasicInfo -NotePropertyMembers @{
                    'ThreadHandle' = $threadHandle
                }
                $threadBasicInfo
            }
        }
    } while( $result -eq [NT_STATUS]::STATUS_SUCCESS )
}


function Get-AllProcesses( [bool]$withThreads )
{
    [int]$processCount = 0
    [IntPtr]$processHandle = [IntPtr]::Zero
    [int]$nameBufferSize = 1KB
    [IntPtr]$nameBuffer = [Runtime.InteropServices.Marshal]::AllocHGlobal( $nameBufferSize )
    $path = New-Object UNICODE_STRING

    ## Get all device paths so can convert native paths
    [hashtable]$drivePaths = @{}

    ForEach( $logicalDrive in [Environment]::GetLogicalDrives())
    {
        $targetPath = New-Object System.Text.StringBuilder 256
        if([Kernel32]::QueryDosDevice( $logicalDrive.Substring(0, 2) , $targetPath, 256) -gt 0)
        {
            $targetPathString = $targetPath.ToString()
            $drivePaths.Add( $targetPathString , $logicalDrive )
        }
    }

    do
    {
        ## can't be QueryLimitedInformation as that gives access denied when enumerating threads
        [NT_STATUS]$result = [NtDll]::NtGetNextProcess( $processHandle , [ProcessAccessRights]::QueryInformation , [AttributeFlags]::None , 0 , [ref]$processHandle )
        if( $result -eq [NT_STATUS]::STATUS_SUCCESS -and $processHandle )
  {   
            ## Call extended version so we get handle information
         $processInfo = New-Object -TypeName ProcessExtendedBasicInformation
            $size = [System.Runtime.InteropServices.Marshal]::SizeOf($processInfo)
            [IntPtr]$ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal( $size )
            #need to set size field to size of extended structure
            $processInfo.Size = $size
            $marshalResult = [System.Runtime.InteropServices.Marshal]::StructureToPtr( $processInfo , $ptr , $false )
            [int]$returnedLength = 0
            $processQueryResult = [NtDll]::NtQueryInformationProcess( $processHandle , [PROCESS_INFO_CLASS]::ProcessBasicInformation , $ptr , $size , [ref]$returnedLength )
            if( $processQueryResult -eq [NT_STATUS]::STATUS_SUCCESS )
            {
                $processInfo = [System.Runtime.InteropServices.Marshal]::PtrToStructure( $ptr , [Type]$processInfo.GetType() )
                ## Not interested in self
                if( $processInfo -and $processInfo.BasicInfo.UniqueProcessId -ne $pid )
                {
                    ## Now get process name
                    $returnedLength = 0
                    [string]$processName = $null
                    ## Have to get native image file names here as win32 ones are missing for zombies
                    [NT_STATUS]$imageResult = [NtDll]::NtQueryInformationProcess( $processHandle , [PROCESS_INFO_CLASS]::ProcessImageFileName , $nameBuffer , $nameBufferSize , [ref]$returnedLength )
                    if( $imageResult -eq [NT_STATUS]::STATUS_SUCCESS -and $returnedLength )
                    {
                        $unicodeString = [System.Runtime.InteropServices.Marshal]::PtrToStructure( $nameBuffer , [Type]$path.GetType() )
                        if( $unicodeString -and ! [string]::IsNullOrEmpty( $unicodeString.buffer ) )
                        {
                            $processName = $unicodeString.Buffer
                            if( ! [string]::IsNullOrEmpty( $processName ) )
                            {
                                [string]$translated = $null
                                ## iterate ovoer local device paths to find this one
                                $drivePaths.GetEnumerator() | ForEach-Object `
                                {
                                    $regex = "^$([regex]::Escape( $_.Key ))\\"
                                    if( ! $translated -and $processName -match $regex )
                                    {
                                        $translated = $processName -replace $regex, $_.Value
                                    }
                                }
                                if( $translated )
                                {
                                    $processName = $translated
                                }
                            }
                        }
                    }
                    $sessionId = $null
                    $sessionInfo = New-Object -TypeName uint32
                    [int]$sessionSize = [System.Runtime.InteropServices.Marshal]::SizeOf( $sessionInfo )
                    [IntPtr]$sessionPtr = [Runtime.InteropServices.Marshal]::AllocHGlobal( $sessionSize )
                    $returnedLength = 0
                    $processQueryResult = [NtDll]::NtQueryInformationProcess( $processHandle , [PROCESS_INFO_CLASS]::ProcessSessionInformation , $sessionPtr , $sessionSize , [ref]$returnedLength )
                    if( $processQueryResult -eq [NT_STATUS]::STATUS_SUCCESS -and $returnedLength )
                    {
                        $sessionId = [System.Runtime.InteropServices.Marshal]::ReadInt32( $sessionPtr ) ## [System.Runtime.InteropServices.Marshal]::PtrToStructure( $sessionPtr , [Type]$sessionInfo.GetType() )
                    }
                    Add-Member -InputObject $processInfo -NotePropertyMembers @{
                        'ProcessHandle' = $processHandle
                        'ProcessName' = $processName
                        'SessionId' = $sessionId
                    }
                    [Runtime.InteropServices.Marshal]::FreeHGlobal( $sessionSize )
                    $sessionSize = [IntPtr]::Zero
                    $processInfo
                    $processCount++
                }
            }
            [Runtime.InteropServices.Marshal]::FreeHGlobal( $ptr )
            $ptr = [IntPtr]::Zero
  }
    }
    while( $result -eq [NT_STATUS]::STATUS_SUCCESS )
    
    [Runtime.InteropServices.Marshal]::FreeHGlobal( $nameBuffer )
    $nameBuffer = [IntPtr]::Zero
}
[string]$logname = 'Microsoft-Windows-User Profile Service/Operational'
[string[]]$excludedProcesses = @( 'System' , 'Idle' , 'Memory Compression' , 'Secure System' , 'Registry' )

Function Get-ProcessInfo
{
    Param
    (
        $process ,
        $logonTime ## from WTS session info, if null then they have no current session
    )

    Add-Member -InputObject $process -MemberType NoteProperty -Name 'Logged On Now' -Value $(if ($logonTime -ne $null) { 'Yes' } else { 'No' } )

    [string]$loggedOnUser = $null
    $logon = $logons[ $process.SessionId ]
    $logoff = $logoffs[ $process.SessionId ]
    if( ! $logon )
    {
        ## we want a more precise time than WTS Session info gives
        $logonevent = Get-WinEvent -FilterHashtable @{ LogName = $logname ; id = 1 } -ErrorAction SilentlyContinue | Where-Object { $_.Message -match "user logon notification on session $($process.SessionId)\." }  | Select -First 1
        if( ! $logonevent )
        {
            if( ! $logonTime )
            {
                [string]$Warning = "Unable to find logon event for session $($process.SessionId) in event log `"$logname`""
                if( ! $oldestEvent )
                {
                    $oldestEvent = Get-WinEvent -LogName $logname -Oldest -MaxEvents 1 -ErrorAction Continue
                }
                if( $oldestEvent )
                {
                    $Warning += ". Oldest event is @ $(Get-Date -Date $oldestEvent.TimeCreated -Format G)"
                }
                else
                {
                    $warning += ". This event log is empty or access is denied"
                }
                Write-Warning $warning
            }
            else
            {
                $logon = $logonTime
            }
        }
        else
        {
            $logon = $logonevent.TimeCreated
            $loggedOnUser = ([System.Security.Principal.SecurityIdentifier]($logonevent.UserId)).Translate([System.Security.Principal.NTAccount]).Value
            if( $loggedOnUser )
            {
                try
                {
                   $sessionOwners.Add( $process.SessionId , $loggedOnUser )
                }
                catch{} ## already got it
            }
        }

        if( $logon )
        {
            $logons.Add( $process.SessionId , $logon )
        }

        if( ! $logonTime -and  ! $logoff ) ## only look for logoff time if the user is not currently logged on
        {
            $logoffevent = Get-WinEvent -FilterHashtable @{ LogName = $logname ; id = 4 } -ErrorAction SilentlyContinue | Where-Object { $_.Message -match "user logoff notification on session $($process.SessionId)\." }  | Select -First 1
            if( ! $logoffevent )
            {
                [string]$Warning = "Unable to find logoff event for session $($process.SessionId) in event log `"$logname`""
                if( ! $oldestEvent )
                {
                    $oldestEvent = Get-WinEvent -LogName $logname -Oldest -MaxEvents 1 -ErrorAction Continue
                }
                if( $oldestEvent )
                {
                    $Warning += ". Oldest event is @ $(Get-Date -Date $oldestEvent.TimeCreated -Format G)"
                }
                else
                {
                    $warning += ". This event log is empty or access is denied"
                }
                Write-Warning $warning
            }
            else
            {
                $logoff = $logoffevent.TimeCreated
                $logoffs.Add( $process.SessionId , $logoff )
            }
        }
    }
    else
    {
        $loggedOnUser = $sessionOwners[ $process.SessionId ]
    }

    if( $logon )
    {
        Add-Member -InputObject $_ -MemberType NoteProperty -Name 'Logon Time' -Value $logon
    }
    if( $logoff )
    {
        Add-Member -InputObject $_ -MemberType NoteProperty -Name 'Logoff Time' -Value $logoff
    }
    if( $loggedOnUser -and $loggedOnUser -ne $process.UserName )
    {
        Add-Member -InputObject $_ -MemberType NoteProperty -Name 'Logged on User' -Value $loggedOnUser
    }
    $process
}

## Get all current user sessions so we can find processes in non-existent sessions 
[hashtable]$sessions = @{}
[hashtable]$sessionOwners = @{}
$oldestEvent = $null

$compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters
$compilerParameters.CompilerOptions = '-unsafe'

Add-Type -ErrorAction Stop -CompilerParameters $compilerParameters -TypeDefinition @'
    using System;
    using System.Runtime.InteropServices;
    public enum WTS_CONNECTSTATE_CLASS
    {
        WTSActive,
        WTSConnected,
        WTSConnectQuery,
        WTSShadow,
        WTSDisconnected,
        WTSIdle,
        WTSListen,
        WTSReset,
        WTSDown,
        WTSInit
    }
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    public struct WTSINFOEX_LEVEL1_W {
        public Int32                  SessionId;
        public WTS_CONNECTSTATE_CLASS SessionState;
        public Int32                   SessionFlags; // 0 = locked, 1 = unlocked , ffffffff = unknown
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
        public string WinStationName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 21)]
        public string UserName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 18)]
        public string DomainName;
        public UInt64           LogonTime;
        public UInt64           ConnectTime;
        public UInt64           DisconnectTime;
        public UInt64           LastInputTime;
        public UInt64           CurrentTime;
        public Int32            IncomingBytes;
        public Int32            OutgoingBytes;
        public Int32            IncomingFrames;
        public Int32            OutgoingFrames;
        public Int32            IncomingCompressedBytes;
        public Int32            OutgoingCompressedBytes;
    }
    
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    unsafe public struct WTSCLIENTA {
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 21)]
      public string   ClientName;
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 18)]
      public string   Domain;
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 21)]
      public string   UserName;
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 261)]
      public string   WorkDirectory;
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 261)]
      public string   InitialProgram;
      public byte   EncryptionLevel;
      public ushort  ClientAddressFamily;
      [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31 , ArraySubType = UnmanagedType.U2)]
      public ushort[] ClientAddress;
      public ushort HRes;
      public ushort VRes;
      public ushort ColorDepth;
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 261)]
      public string   ClientDirectory;
      public ulong  ClientBuildNumber;
      public ulong  ClientHardwareId;
      public ushort ClientProductId;
      public ushort OutBufCountHost;
      public ushort OutBufCountClient;
      public ushort OutBufLength;
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 261)]
      public string   DeviceId;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct WTS_SESSION_INFO
    {
        public Int32 SessionID;

        [MarshalAs(UnmanagedType.LPStr)]
        public String pWinStationName;

        public WTS_CONNECTSTATE_CLASS State;
    }
    [StructLayout(LayoutKind.Explicit)]
    public struct WTSINFOEX_LEVEL_W
    { //Union
        [FieldOffset(0)]
        public WTSINFOEX_LEVEL1_W WTSInfoExLevel1;
    } 
    [StructLayout(LayoutKind.Sequential)]
    public struct WTSINFOEX
    {
        public Int32 Level ;
        public WTSINFOEX_LEVEL_W Data;
    }
    public enum WTS_INFO_CLASS
    {
        WTSInitialProgram,
        WTSApplicationName,
        WTSWorkingDirectory,
        WTSOEMId,
        WTSSessionId,
        WTSUserName,
        WTSWinStationName,
        WTSDomainName,
        WTSConnectState,
        WTSClientBuildNumber,
        WTSClientName,
        WTSClientDirectory,
        WTSClientProductId,
        WTSClientHardwareId,
        WTSClientAddress,
        WTSClientDisplay,
        WTSClientProtocolType,
        WTSIdleTime,
        WTSLogonTime,
        WTSIncomingBytes,
        WTSOutgoingBytes,
        WTSIncomingFrames,
        WTSOutgoingFrames,
        WTSClientInfo,
        WTSSessionInfo,
        WTSSessionInfoEx,
        WTSConfigInfo,
        WTSValidationInfo,   // Info Class value used to fetch Validation Information through the WTSQuerySessionInformation
        WTSSessionAddressV4,
        WTSIsRemoteSession
    }
    public static class wtsapi
    {
        [DllImport("wtsapi32.dll", SetLastError=true)]
        public static extern int WTSQuerySessionInformationW(
                 System.IntPtr hServer,
                 int SessionId,
                 int WTSInfoClass ,
                 ref System.IntPtr ppSessionInfo,
                 ref int pBytesReturned );

        [DllImport("wtsapi32.dll", SetLastError=true)]
        public static extern int WTSEnumerateSessions(
                 System.IntPtr hServer,
                 int Reserved,
                 int Version,
                 ref System.IntPtr ppSessionInfo,
                 ref int pCount);

        [DllImport("wtsapi32.dll", SetLastError=true)]
        public static extern IntPtr WTSOpenServer(string pServerName);
        
        [DllImport("wtsapi32.dll", SetLastError=true)]
        public static extern void WTSCloseServer(IntPtr hServer);
        
        [DllImport("wtsapi32.dll", SetLastError=true)]
        public static extern void WTSFreeMemory(IntPtr pMemory);
    }
'@ 

Function Get-WTSSessionInformation
{
    [cmdletbinding()]

    Param
    (
        [string[]]$computers = @( $null )
    )

    [long]$count = 0
    [IntPtr]$ppSessionInfo = 0
    [IntPtr]$ppQueryInfo = 0
    [long]$ppBytesReturned = 0
    $wtsSessionInfo = New-Object -TypeName 'WTS_SESSION_INFO'
    $wtsInfoEx = New-Object -TypeName 'WTSINFOEX'
    $wtsClientInfo = New-Object -TypeName 'WTSCLIENTA'
    [int]$datasize = [system.runtime.interopservices.marshal]::SizeOf( [Type]$wtsSessionInfo.GetType() )

    ForEach( $computer in $computers )
    {
        $wtsinfo = $null
        [string]$machineName = $(if( $computer ) { $computer } else { $env:COMPUTERNAME })
        [IntPtr]$serverHandle = [wtsapi]::WTSOpenServer( $computer )

        ## If the function fails, it returns a handle that is not valid. You can test the validity of the handle by using it in another function call.

        [long]$retval = [wtsapi]::WTSEnumerateSessions( $serverHandle , 0 , 1 , [ref]$ppSessionInfo , [ref]$count );$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()

        if ($retval -ne 0)
        {
             for ([int]$index = 0; $index -lt $count; $index++)
             {
                 $element = [system.runtime.interopservices.marshal]::PtrToStructure( [long]$ppSessionInfo + ($datasize * $index), [type]$wtsSessionInfo.GetType())
                 if( $element -and $element.SessionID -ne 0 ) ## session 0 is non-interactive (session zero isolation)
                 {
                     $retval = [wtsapi]::WTSQuerySessionInformationW( $serverHandle , $element.SessionID , [WTS_INFO_CLASS]::WTSSessionInfoEx , [ref]$ppQueryInfo , [ref]$ppBytesReturned );$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
                     if( $retval -and $ppQueryInfo )
                     {
                        $value = [system.runtime.interopservices.marshal]::PtrToStructure( $ppQueryInfo , [Type]$wtsInfoEx.GetType())
                        if( $value -and $value.Data -and $value.Data.WTSInfoExLevel1.SessionState -ne [WTS_CONNECTSTATE_CLASS]::WTSListen -and $value.Data.WTSInfoExLevel1.SessionState -ne [WTS_CONNECTSTATE_CLASS]::WTSConnected )
                        {
                            $wtsinfo = $value.Data.WTSInfoExLevel1
                            $idleTime = New-TimeSpan -End ([datetime]::FromFileTimeUtc($wtsinfo.CurrentTime)) -Start ([datetime]::FromFileTimeUtc($wtsinfo.LastInputTime))
                            Add-Member -InputObject $wtsinfo -Force -NotePropertyMembers @{
                                'IdleTimeInSeconds' =  $idleTime | Select -ExpandProperty TotalSeconds
                                'IdleTimeInMinutes' =  $idleTime | Select -ExpandProperty TotalMinutes
                                'Computer' = $machineName
                                'LogonTime' = [datetime]::FromFileTime( $wtsinfo.LogonTime )
                                'DisconnectTime' = [datetime]::FromFileTime( $wtsinfo.DisconnectTime )
                                'LastInputTime' = [datetime]::FromFileTime( $wtsinfo.LastInputTime )
                                'ConnectTime' = [datetime]::FromFileTime( $wtsinfo.ConnectTime )
                                'CurrentTime' = [datetime]::FromFileTime( $wtsinfo.CurrentTime )
                            }
                        }
                        [wtsapi]::WTSFreeMemory( $ppQueryInfo )
                        $ppQueryInfo = [IntPtr]::Zero
                     }
                     else
                     {
                        Write-Error "$($machineName): $LastError"
                     }
                     $retval = [wtsapi]::WTSQuerySessionInformationW( $serverHandle , $element.SessionID , [WTS_INFO_CLASS]::WTSClientInfo , [ref]$ppQueryInfo , [ref]$ppBytesReturned );$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
                     if( $retval -and $ppQueryInfo )
                     {
                        $value = [system.runtime.interopservices.marshal]::PtrToStructure( $ppQueryInfo , [Type]$wtsClientInfo.GetType())
                        if( $value -and $wtsinfo )
                        {
                        }
                        [wtsapi]::WTSFreeMemory( $ppQueryInfo )
                        $ppQueryInfo = [IntPtr]::Zero
                     }
                     if( $wtsInfo )
                     {
                        $wtsinfo
                        $wtsinfo = $null
                     }
                 }
             }
        }
        else
        {
            Write-Error "$($machineName): $LastError"
        }

        if( $ppSessionInfo -ne [IntPtr]::Zero )
        {
            [wtsapi]::WTSFreeMemory( $ppSessionInfo )
            $ppSessionInfo = [IntPtr]::Zero
        }
        [wtsapi]::WTSCloseServer( $serverHandle )
        $serverHandle = [IntPtr]::Zero
    }
}

[array]$wtssessions = @( Get-WTSSessionInformation )

ForEach( $wtssession in $wtssessions )
{
    $sessions.Add( $wtssession.SessionId , $wtssession.LogonTime )
}

Write-Verbose "Got $($sessions.Count) sessions"

[hashtable]$logons = @{}
[hashtable]$logoffs = @{}

$zombies = New-Object -typename System.Collections.Generic.List[psobject]
[bool]$suspended = $false
[hashtable]$sessionsWithZombies = @{}
[hashtable]$allGetProcessProcesses = @{}
[hashtable]$allCIMProcesses = @{}

## "snapshot" processes 
Get-Process -IncludeUserName | . { Process { $allGetProcessProcesses.Add( [int]$_.Id , $_ ) } }
Get-CimInstance -ClassName win32_process | . { Process { $allCIMProcesses.Add( [int]$_.ProcessId , $_ ) } }

## Grab all processes now so we can get the flags for UWP processes to see if they are frozen which is normal behaviour
[array]$allProcesses = Get-AllProcesses
[hashtable]$frozenUWPProcesses = @{}
$allThreads = New-Object System.Collections.ArrayList

ForEach( $item in $allProcesses )
{
    $allThreads += Get-AllThreadsForProcess -processHandle $item.ProcessHandle -processPid $item.BasicInfo.UniqueProcessId

    if( ( [int]$item.Flags -band [int][ProcessExtendedBasicInformationFlags]::IsFrozen ) -eq [int][ProcessExtendedBasicInformationFlags]::IsFrozen )
    {
        $frozenUWPProcesses.Add( [int]$item.BasicInfo.UniqueProcessId , $item )
    }
}

[array]$allProcessAndThreadHandles = Get-AllHandles -handleType 7,8 ## Only Process and Thread handles
[int]$uwpFrozenApps = 0

## ignore some session 0 processes as they're special. Also ignore anything newly created in case it is in the process of being created or was deliberately created suspended.
$zombies += @( Get-Process -IncludeUserName | Where-Object { $_.StartTime -lt (Get-Date).AddSeconds( -$recentlyCreated ) `
    -and ( $suspended = ( $_.Threads.Where( { $_.ThreadState -eq 'Wait' -and $_.WaitReason -eq 'Suspended' } )).Count -eq $_.Threads.Count )`
        -or ! $_.HandleCount -or ( $_.SessionId -gt 1 -and ! $sessions[ $_.SessionId ] ) } | . { Process `
{
    if( $_.SessionId -or ( $_.SessionId -eq 0 -and $excludedProcesses -notcontains $_.Name ))
    {
        [bool]$probablyExpected = $false
        if( $suspended )
        {
            if( $_.Path )
            {
                if( $probablyExpected = ( $frozenUWPProcesses[ [int]$_.Id ] -ne $null ) )
                {
                    $uwpFrozenApps++
                }
            }
            else
            {
                Write-Verbose "No path for $($_.Name)"
            }
        }
        [string]$vendor = '-'
        if( $_.Path )
        {
            $vendor = Get-ItemProperty -Path $_.Path -ErrorAction SilentlyContinue | Select -ExpandProperty VersionInfo -ErrorAction SilentlyContinue | Select -ExpandProperty CompanyName -ErrorAction SilentlyContinue
        }
        if( ! $probablyExpected )
        {
            [string]$processName = $_.Name + '.exe'
            [string]$loadedModules = if( $_.Modules ) { $_.Modules | Where-Object { $_.ModuleName -ne $processName } | Measure-Object | Select -ExpandProperty Count } else {'-'}
            Add-Member -InputObject $_ -NotePropertyMembers @{
                'Suspended' = $( if( $suspended ) { 'Yes' } else { 'No' } )
                'Vendor' = $vendor
                'Loaded Modules' = $loadedModules }

            if( $_.SessionId )
            {
                Get-ProcessInfo -process $_ -logonTime $sessions[ $_.SessionId ]
            }
            else  ## don't try and get session logon/logoff details for session 0 since cannot be logged on to
            {
                Add-Member -InputObject $_ -MemberType NoteProperty -Name 'Logged On Now' -Value '-'
                $_
            }
            try
            {
                $sessionsWithZombies.Add( $_.SessionId , $(if( $sessions[ $_.SessionId ] ) { 'Yes' } else { 'No' } ) )
            }
            catch{}
        }
        else
        {
            Write-Verbose "Excluding $($_.Name) ($($_.Id)) as frozen flag set so probably UWP"
        }
    }}
})

[datetime]$startedKilling = [datetime]::Now

if( $zombies -and $zombies.Count )
{
    $outputFields = New-Object -TypeName System.Collections.ArrayList
    $outputFields += @{n='Process';e={$_.Name}},@{n='Pid';e={$_.Id}},@{n='Session Id';e={$_.SessionId}},'Vendor','Username','Suspended'
    if( ( $zombies | Where-Object { $_.PsObject.properties[ 'Logged on User' ] -and $_.'Logged on User' } | Measure-Object | Select -ExpandProperty Count ) -gt 0 )
    {
        $outputFields += 'Logged on User'
    }
    $outputFields += 'Logged On Now',@{n='Process Start Time';e={$_.StartTime}},'Loaded Modules','Handles',@{n='Threads';e={$_.Threads.Count}},@{n='Working Set (KB)';e={[math]::Round( $_.WS/1KB)}},@{n='Total CPU (s)';e={[math]::Round( $_.TotalProcessorTime.TotalSeconds , 2 )}},'Logon Time','Logoff Time'
    "Found $($zombies.Count) user mode zombie processes in $($sessionsWithZombies.Count) sessions of which $($sessionsWithZombies.GetEnumerator() | Where-Object Value -eq 'no' | Measure-Object | Select-Object -ExpandProperty Count) no longer exist"
    $zombies | Sort-Object -Property 'SessionId' | Format-Table -Property $outputFields -AutoSize

    if( $kill )
    {
        ForEach( $zombie in $zombies )
        {
            if( $zombie.hasExited )
            {
                Add-Member -InputObject $zombie -MemberType NoteProperty -Name 'WasDying' -Value ([datetime]::Now)
            }
            ## ignore errors as we explicitly check for process exit
            Stop-Process -Id $zombie.Id -Force
        }
    }
}
else
{
    Write-Output "No processes found with all threads suspended (except $uwpFrozenApps UWP apps which is normal), no open handles or in a non-existent session"
}

## We have a handle open to all processes as a result of calling Get-AllProcesses so we get all handles for this PowerShell process and for each process from Get-AllProcesses we look for Flags of
## IsProcessDeleting and for each of those we find the handle we saved for it in the handle list for this process and then the Object property is looked up against all process handles which gives us
## the pid (UniqueProcessId) of the pid that has the open handle

Write-Verbose -Message "$(Get-Date -Format G): getting all process and thread handles"

[hashtable]$handles = @{}
$allProcessAndThreadHandles.Where( { $_.UniqueProcessId -eq $pid } ) | . { Process `
{
    $handles.Add( $_.HandleValue , $_ )
}}

[hashtable]$leakers = @{}

[int]$exitingProcesses = 0
[int]$zero = 0
[uint16]$seven = 7
[uint16]$eight = 8
[int]$frozenFlag = [ProcessExtendedBasicInformationFlags]::IsFrozen

Write-Verbose -Message "$(Get-Date -Format G): getting results"
[array]$results = @( $allProcesses.Where( { ( ( [int]$_.Flags -band [int][ProcessExtendedBasicInformationFlags]::IsProcessDeleting ) -eq [int][ProcessExtendedBasicInformationFlags]::IsProcessDeleting ) } ) | . { Process `
{
    $deadProcess = $_
    $exitingProcesses++
    Write-Verbose ( "{0} ({1}) flags {2:x}" -f $deadProcess.ProcessName , $deadProcess.BasicInfo.UniqueProcessId , $deadProcess.Flags )
    if( ( [int]$_.Flags -band $frozenFlag ) -eq $zero )
    {
        if( $ourHandleToDeadProcess = $handles[ $deadProcess.ProcessHandle ] )
        {
            $object = $ourHandleToDeadProcess.Object
            [System.Collections.Generic.List[object]]$openHandles = @( $allProcessAndThreadHandles.Where( { $_.ObjectTypeIndex -eq $seven -and $_.Object -eq $object -and $_.UniqueProcessId -ne $pid } ) | Group-Object -Property UniqueProcessId )
  
            [int]$processHandleCount = $openHandles.Count
            [int]$threadHandleCount = 0
            
            [array]$openThreadHandles = @( $allThreads.Where( { $_.ClientId.UniqueProcess -eq $deadProcess.BasicInfo.UniqueProcessId } ) | . { Process `
            {
                $deadThread = $_
                ## Group by UniqueProcessId since nay existing elements in $openHandles will be so we must add the same type objects here
                [array]$leakedThreadHandles = @( $allProcessAndThreadHandles.Where( { $_.ObjectTypeIndex -eq $eight -and $_.Object -eq $handles[ $deadThread.ThreadHandle ].Object -and $_.UniqueProcessId -ne $pid } ) )
                if( $leakedThreadHandles.Count )
                {
                    $threadHandleCount += $leakedThreadHandles | Group-Object -Property Object | Measure-Object -Property Count -Sum | Select-Object -ExpandProperty Sum
                    $leakedThreadHandles
                }
            }} | Group-Object -Property UniqueProcessId)
            
            if( $openThreadHandles -and $openThreadHandles.Count )
            {
                $openHandles += $openThreadHandles
            }

            [string]$separator = $null
            [hashtable]$thisProcessesLeakers = @{}
        
            $openHandles | . { Process `
            {
                if( $_.Name )
                {
                    if( $thisLeaker = $thisProcessesLeakers[ $_.Name ] )
                    {
                        $thisProcessesLeakers.Set_Item( $_.Name , $thisLeaker + [int]$_.Count )
                    }
                    else
                    {
                        $thisProcessesLeakers.Add( $_.Name , $_.Count )
                    }
                    if( $leaker = $leakers[ $_.Name ] )
                    {
                        $leakers.Set_Item( $_.Name , $leaker + [int]$_.Count )
                    }
                    else
                    {
                        $leakers.Add( $_.Name , $_.Count )
                    }
                }
                else
                {
                    Write-Verbose "Name (pid) is null for $($deadProcess.Name)"
                }
            }}
            if( $thisProcessesLeakers -and $thisProcessesLeakers.Count )
            {
                Add-Member -InputObject $deadProcess -NotePropertyMembers @{
                    'Leakers' = $thisProcessesLeakers
                    'ProcessHandleCount' = $processHandleCount
                    'ThreadHandleCount' = $threadHandleCount }
                $deadProcess
            }
            else ## found no handles so may not be zombie per se
            {
                Write-Verbose "`tFound no handles for zombie $($deadProcess.ProcessName) ($($deadProcess.BasicInfo.UniqueProcessId))"
            }
        }
        else
        {
            Write-Warning "No open handle in pid $pid for handle $($deadProcess.ProcessHandle) in dead process $($deadProcess.ProcessName)"
        }
    }
    else
    {
        Write-Verbose "Excluding $($deadProcess.ProcessName) ($($deadProcess.BasicInfo.UniqueProcessId)) as frozen flag set so probably UWP"
    }
}})

Write-Verbose -Message "$(Get-Date -Format G): got results from $exitingProcesses terminating processes"

[int]$killed = 0
$sinceKilling = New-TimeSpan -Start $startedKilling -End ([datetime]::Now)

if( $kill -and $zombies -and $zombies.Count )
{
    ## check on state of previously killed processes

    ForEach( $zombie in $zombies )
    {
        if( ! ( Get-Process -InputObject $zombie -ErrorAction SilentlyContinue | Where-Object StartTime -lt $startedKilling ) )
        {
            $killed++
        }
        elseif( $zombie.PSObject.Properties[ 'WasDying' ] )
        {
            $dyingProcesses.Add( $zombie )
        }
        else
        {
            $failedToDieProcesses.Add( $zombie )
        }                
    }

    "Successfully killed $killed processes"
}

if( $results -and $results.Count )
{
    "`nSummary of the $($results.Count) dead processes which other processes still have open process or thread handles to:"
    [array]$grouped = @( $results | Group-Object -Property ProcessName | . { Process `
    {
        ## grouping has flattened the leakers so we need to summarise per grouped item which is a pid
        $deadProcess = $_
        [string]$separator = $null
        [string]$handleTypes = if( $deadProcess.Group.ProcessHandleCount )
        {
            'Process'
        }
        $handleTypes += if( $deadProcess.Group.ThreadHandleCount )
        {
            if( $deadProcess.Group.ProcessHandleCount )
            {
                ' &'
            }
            'Thread'
        }
        ## Need to amalgamate the pid, session and leakers data for each of these dead processes so we can display on a single output line
        [string]$offenders = ($deadProcess.group.Leakers | . { Process `
        { 
            $_.GetEnumerator()|select name,value 
        }} `
            | Group-Object -Property Name -AsHashTable).GetEnumerator() | Select -Property @{n='Pid';e={$_.Name}},@{n='TotalHandles';e={$_.value|Measure-Object -Property value -Sum|select -ExpandProperty Sum}} | . { Process `
        {
            "{0}{1} ({2}) {3} {4} handles" -f $separator , ( $allCIMProcesses[ [int]$_.Pid ] | Select -ExpandProperty Name ) , $_.Pid , $_.TotalHandles , $handleTypes
            $separator = "`n"
        }}
        
        ## See how many different sessions these are in
        [hashtable]$exisingSessions = @{}
        [hashtable]$nonexistentSessions = @{}
        $deadProcess.group | . { Process `
        {
            try
            {
                if( ! $_.SessionId -or $sessions[ $_.SessionId ] )
                {
                    $exisingSessions.Add( $_.SessionId , $_ )
                }
                else
                {
                    $nonexistentSessions.Add( $_.SessionId , $_ )
                }
            }
            catch {}
        }}
        Add-Member -InputObject $deadProcess -NotePropertyMembers @{
            'Live Sessions' = $exisingSessions.Count
            'Dead Sessions' = $nonexistentSessions.Count
            'Instances' = $deadProcess.Group.Count
            'Offenders' = $offenders
        }
        $deadProcess
    }})
    
    $grouped | Sort -Property Instances -Descending | Format-Table -Wrap -AutoSize -Property 'Name' , 'Instances' , 'Live Sessions','Dead Sessions',@{n='Processes with handles to this process';e={$_.Offenders -join "`n"}}

    if( $leakers -and $leakers.Count )
    {
        "The $($leakers.Count) processes causing these zombies are:"

        $leakers.GetEnumerator() | Select @{n='Executable';e={$allCIMProcesses[ [int]$_.Key ]|select -ExpandProperty ExecutablePath}},
            @{n='Company';e={$allGetProcessProcesses[ [int]$_.Key ]|Select -ExpandProperty Company}},
            @{n='Version';e={$allGetProcessProcesses[ [int]$_.Key ]|Select -ExpandProperty ProductVersion}},
            @{n='Pid';e={$_.Key}},
            @{n='Session Id';e={$allGetProcessProcesses[ [int]$_.Key ]|Select -ExpandProperty SessionId}},
            @{n='Username';e={$allGetProcessProcesses[ [int]$_.Key ]|Select -ExpandProperty Username}},
            @{n='Start Time';e={$allGetProcessProcesses[ [int]$_.Key ]|Select -ExpandProperty StartTime}},
            @{n='Handles to Zombies';e={$_.Value}} | Sort -Property 'Handles to Zombies' -Descending | Format-Table -AutoSize

        ## See if any are ControlUp processes and check versions as there was an issue, fixed in 8.2 RTM
        [string[]]$knownLeakyProcesses = @( 'cuagent' , 'AppLoadTimeTracer' )
        [hashtable]$svchostProcesses = @{}
        [hashtable]$services = @{}
        $leakyServices = New-Object -TypeName System.Collections.Generic.List[object]

        ## build hashtable where svchost pid is key and has array of service names(s) so can be sorted for display
        Get-WmiObject -Class win32_service -Filter "State = 'running'" | Where-Object { $_.PathName -match '\\svchost\.exe\b' } | ForEach-Object `
        {
            $service = $_
            if( $existingEntry = $services[ $service.ProcessId ] )
            {
                $existingEntry.Add( $service.DisplayName )
            }
            else
            {
                $services.Add( $service.ProcessId , [System.Collections.Generic.List[string]]$service.DisplayName )
            }
        }

        ForEach( $leaker in $leakers.GetEnumerator() )
        {
            if( ( $processDetails = $allGetProcessProcesses[ [int]$leaker.Key ] ) -and $null -ne $processDetails.PSObject.Properties[ 'Name' ] )
            {
                if( $processDetails.PSObject.Properties[ 'Company' ] -and $processDetails.Company -cmatch '^ControlUp' -and  $processDetails.Name -in $knownLeakyProcesses -and $processDetails.PSObject.Properties[ 'FileVersion' ] )
                {
                    if( [version]$processDetails.FileVersion -lt [version]'8.2' )
                    {
                        $warnings.Add( "ControlUp process $($processDetails.Name) version $($processDetails.FileVersion) has a handle leak, fixed in 8.2" )
                    }
                    elseif( Get-Counter -Counter "\User Input Delay per Session($(Get-Process -Id $pid|Select-Object -ExpandProperty SessionId))\Max Input Delay"  -ErrorAction SilentlyContinue )
                    {
                        $warnings.Add( "cuAgent.exe appears due to a known Microsoft leak with the User Input Delay performance metrics, until Microsoft fixes this issue, the workaround is to disable the User Input Delay feature via the registry. For more details please contact ControlUp Support" )
                    }
                }
                ## if services then we will list the service names for that pid
                if( $processDetails.Name -eq 'svchost' -and ($serviceNames = $services[ [uint32]$processDetails.Id ] ))
                {
                    $leakyServices.Add( (New-Object -TypeName PSCustomObject -Property (@{ 'Pid' = $processDetails.Id ; 'Services' = $serviceNames }) ) )
                }
            }
        }

        if( $leakyServices -and $leakyServices.Count )
        {
            'Leaking svchost process services are:'
            $leakyServices | Sort-Object -Property Pid | Select-Object -Property Pid,@{n='Services';e={( $_.Services | Sort-Object ) -join "`n"}} | Format-Table -Wrap -AutoSize
        }
    }
}
else
{
    Write-Output "`nFound no processes with handles open to dead processes"
}

## Close all handles otherwise parent PowerShell process will leak handles until exit which makes this script run slower if nothing else
$allProcesses | . { Process `
{
    [void][Kernel32]::CloseHandle( $_.ProcessHandle )
}}

$allThreads | . { Process `
{
    [void][Kernel32]::CloseHandle( $_.ThreadHandle )
}}

if( $warnings -and $warnings.Count )
{
    ''
    $warnings | Write-Warning
}

if( $dyingProcesses -and $dyingProcesses.Count )
{
    ''
    Write-Warning -Message "Got $($dyingProcesses.Count) processes which were already exiting but are still alive after being terminated (pids $(($dyingProcesses | Select-Object -ExpandProperty Id | Sort-Object) -join ','))"
}

if( $failedToDieProcesses -and $failedToDieProcesses.Count )
{
    ''
    Write-Warning -Message "Got $($failedToDieProcesses.Count) processes which didn't exit within $([math]::Round( $sinceKilling.TotalSeconds , 1 )) seconds of being terminated (pids $(($failedToDieProcesses | Select-Object -ExpandProperty Id | Sort-Object) -join ','))"
}