Public/iis/Get-IISWorkerProcess.ps1
|
#Requires -Version 5.1 function Get-IISWorkerProcess { <# .SYNOPSIS Inventories IIS worker processes (w3wp.exe) enriched with app pool, sites, identity, and resource metrics. .DESCRIPTION Enumerates every w3wp.exe process on one or more target servers and joins it with IIS configuration so each row carries the owning application pool, the sites and applications it serves, its identity, PID, uptime, CPU time, memory footprint (working set / private / virtual), thread count and handle count. Provides the operational overview that the native IISAdministration module does not expose in a single cmdlet. Falls back gracefully from WebAdministration to IISAdministration to appcmd/CIM when modules are missing, and from Get-Process to CIM Win32_Process when needed. .PARAMETER ComputerName One or more computer names to query. Defaults to the local machine. Accepts pipeline input by value and by property name. .PARAMETER Credential Optional PSCredential for authenticating to remote computers. Not used for local queries. .PARAMETER AppPoolName Restrict the inventory to w3wp processes belonging to one or more named application pools. Wildcards accepted via -like. .PARAMETER ProcessId Filter to specific worker process PIDs (useful when correlating with Get-Process / event logs). .EXAMPLE Get-IISWorkerProcess Returns all running w3wp.exe processes on the local machine enriched with app pool, site, identity and resource data. .EXAMPLE Get-IISWorkerProcess -ComputerName 'WEB01' Returns IIS worker process inventory from a single remote server. .EXAMPLE 'WEB01','WEB02' | Get-IISWorkerProcess -Credential (Get-Credential) Queries multiple remote servers via pipeline with alternate credentials. .EXAMPLE Get-IISWorkerProcess -AppPoolName 'DefaultAppPool','API*' Returns only worker processes belonging to DefaultAppPool or any pool whose name matches API*. .EXAMPLE Get-IISWorkerProcess | Sort-Object WorkingSetMB -Descending | Select-Object -First 5 Returns the top 5 worker processes by working set memory. .OUTPUTS PSCustomObject (PSTypeName='PSWinOps.IISWorkerProcess') .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-05-15 Requires: PowerShell 5.1+ / Windows only Requires: Web-Server (IIS) role Optional: Module WebAdministration or IISAdministration (falls back to appcmd) .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/iis/get-started/planning-your-iis-architecture/introduction-to-iis-architecture#worker-processes #> [CmdletBinding()] [OutputType('PSWinOps.IISWorkerProcess')] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [ValidateNotNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, [Parameter(Mandatory = $false)] [string[]]$AppPoolName, [Parameter(Mandatory = $false)] [int[]]$ProcessId ) begin { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting" $scriptBlock = { param( [string[]]$FilterAppPool, [int[]]$FilterPid ) $results = [System.Collections.Generic.List[hashtable]]::new() # ── 1. Verify IIS (W3SVC) presence ─────────────────────────────── try { $null = Get-Service -Name 'W3SVC' -ErrorAction Stop } catch { $results.Add(@{ ProcessId = $null AppPoolName = $null Sites = @() Applications = @() Identity = $null IdentityType = $null StartTime = $null UptimeSeconds = $null CPUSeconds = $null WorkingSetMB = $null PrivateMemoryMB = $null VirtualMemoryMB = $null ThreadCount = $null HandleCount = $null CommandLine = $null Status = 'IISNotInstalled' ErrorMessage = "W3SVC service not found: $($_.Exception.Message)" }) return $results } # ── 2. Collect w3wp.exe process data ───────────────────────────── # Primary path: Get-Process (gives CPU/memory/threads/handles/starttime) $procMap = @{} # int PID -> System.Diagnostics.Process $cimProcMap = @{} # int PID -> CIM Win32_Process (for CommandLine + fallback) try { foreach ($p in @(Get-Process -Name 'w3wp' -ErrorAction Stop)) { $procMap[[int]$p.Id] = $p } } catch { Write-Verbose -Message '[Get-IISWorkerProcess] Get-Process w3wp returned no results; using CIM fallback.' } # CIM is always queried for CommandLine (not exposed by Get-Process) try { $cimInstances = @(Get-CimInstance -ClassName 'Win32_Process' ` -Filter "Name='w3wp.exe'" -ErrorAction Stop) foreach ($cp in $cimInstances) { $cpid = [int]$cp.ProcessId $cimProcMap[$cpid] = $cp if (-not $procMap.ContainsKey($cpid)) { $procMap[$cpid] = $null } } } catch { Write-Verbose -Message '[Get-IISWorkerProcess] CIM Win32_Process query failed.' } if ($procMap.Count -eq 0) { $results.Add(@{ ProcessId = $null AppPoolName = $null Sites = @() Applications = @() Identity = $null IdentityType = $null StartTime = $null UptimeSeconds = $null CPUSeconds = $null WorkingSetMB = $null PrivateMemoryMB = $null VirtualMemoryMB = $null ThreadCount = $null HandleCount = $null CommandLine = $null Status = 'NoWorkerProcess' ErrorMessage = $null }) return $results } # ── 3. PID -> AppPool mapping via appcmd ───────────────────────── $pidToPool = @{} $appcmdExe = Join-Path -Path $env:windir -ChildPath 'system32\inetsrv\appcmd.exe' if (Test-Path -LiteralPath $appcmdExe -PathType Leaf) { try { $wpRaw = & $appcmdExe list wp /xml 2>$null if (-not [string]::IsNullOrWhiteSpace($wpRaw)) { [xml]$wpXml = $wpRaw foreach ($wpNode in @($wpXml.appcmd.WP)) { if ($null -ne $wpNode) { $pidToPool[[int]$wpNode.PID] = $wpNode.'APPPOOL.NAME' } } } } catch { Write-Verbose -Message "[Get-IISWorkerProcess] appcmd list wp failed: $($_.Exception.Message)" } } # ── 4. AppPool identity + Sites/Apps via IIS module or appcmd ──── $poolIdentityMap = @{} $poolToSites = @{} $poolToApps = @{} $iisLoaded = $false # -- WebAdministration path -- if (-not $iisLoaded -and (Get-Module -Name 'WebAdministration' -ListAvailable -ErrorAction SilentlyContinue)) { try { Import-Module -Name 'WebAdministration' -ErrorAction Stop foreach ($pool in @(Get-ChildItem -Path 'IIS:\AppPools' ` -ErrorAction SilentlyContinue)) { $pm = $pool.processModel $rawIdType = [string]$pm.identityType $identityType = switch ($rawIdType) { 'ApplicationPoolIdentity' { 'ApplicationPoolIdentity' } 'LocalSystem' { 'LocalSystem' } 'LocalService' { 'LocalService' } 'NetworkService' { 'NetworkService' } 'SpecificUser' { 'SpecificUser' } default { 'Unknown' } } $identity = if ($identityType -eq 'SpecificUser') { [string]$pm.userName } else { $identityType } $poolIdentityMap[$pool.Name] = @{ Identity = $identity IdentityType = $identityType } } foreach ($site in @(Get-Website -ErrorAction SilentlyContinue)) { $pn = [string]$site.applicationPool if (-not $poolToSites.ContainsKey($pn)) { $poolToSites[$pn] = [System.Collections.Generic.List[string]]::new() } if (-not $poolToSites[$pn].Contains($site.Name)) { $poolToSites[$pn].Add($site.Name) } } foreach ($app in @(Get-WebApplication -ErrorAction SilentlyContinue)) { $pn = [string]$app.applicationPool $siteName = $app.PSParentPath -replace '^.*\\Sites\\', '' if (-not $poolToApps.ContainsKey($pn)) { $poolToApps[$pn] = [System.Collections.Generic.List[string]]::new() } $poolToApps[$pn].Add("$siteName$($app.Path)") } $iisLoaded = $true } catch { Write-Verbose -Message "[Get-IISWorkerProcess] WebAdministration failed: $($_.Exception.Message)" } } # -- IISAdministration path -- if (-not $iisLoaded -and (Get-Module -Name 'IISAdministration' -ListAvailable -ErrorAction SilentlyContinue)) { try { Import-Module -Name 'IISAdministration' -ErrorAction Stop foreach ($pool in @(Get-IISAppPool -ErrorAction Stop)) { $rawIdType = $pool.ProcessModel.IdentityType.ToString() $identityType = switch ($rawIdType) { 'ApplicationPoolIdentity' { 'ApplicationPoolIdentity' } 'LocalSystem' { 'LocalSystem' } 'LocalService' { 'LocalService' } 'NetworkService' { 'NetworkService' } 'SpecificUser' { 'SpecificUser' } default { 'Unknown' } } $identity = if ($identityType -eq 'SpecificUser') { [string]$pool.ProcessModel.UserName } else { $identityType } $poolIdentityMap[$pool.Name] = @{ Identity = $identity IdentityType = $identityType } } foreach ($site in @(Get-IISSite -ErrorAction Stop)) { foreach ($app in $site.Applications) { $pn = [string]$app.ApplicationPoolName if ($app.Path -eq '/') { if (-not $poolToSites.ContainsKey($pn)) { $poolToSites[$pn] = [System.Collections.Generic.List[string]]::new() } if (-not $poolToSites[$pn].Contains($site.Name)) { $poolToSites[$pn].Add($site.Name) } } else { if (-not $poolToApps.ContainsKey($pn)) { $poolToApps[$pn] = [System.Collections.Generic.List[string]]::new() } $poolToApps[$pn].Add("$($site.Name)$($app.Path)") } } } $iisLoaded = $true } catch { Write-Verbose -Message "[Get-IISWorkerProcess] IISAdministration failed: $($_.Exception.Message)" } } # -- appcmd fallback for identity + site/app mapping -- if (-not $iisLoaded -and (Test-Path -LiteralPath $appcmdExe -PathType Leaf)) { try { $rawPools = & $appcmdExe list apppool /xml /config:* 2>$null if (-not [string]::IsNullOrWhiteSpace($rawPools)) { [xml]$poolXml = $rawPools foreach ($poolNode in @($poolXml.appcmd.APPPOOL)) { if ($null -eq $poolNode) { continue } $addNode = $poolNode.add $rawIdType = [string]$addNode.processModel.identityType $identityType = switch ($rawIdType) { 'ApplicationPoolIdentity' { 'ApplicationPoolIdentity' } 'LocalSystem' { 'LocalSystem' } 'LocalService' { 'LocalService' } 'NetworkService' { 'NetworkService' } 'SpecificUser' { 'SpecificUser' } default { 'Unknown' } } $identity = if ($identityType -eq 'SpecificUser') { [string]$addNode.processModel.userName } else { $identityType } $poolIdentityMap[$poolNode.'APPPOOL.NAME'] = @{ Identity = $identity IdentityType = $identityType } } } } catch { Write-Verbose -Message "[Get-IISWorkerProcess] appcmd list apppool failed: $($_.Exception.Message)" } try { $rawApps = & $appcmdExe list app /xml 2>$null if (-not [string]::IsNullOrWhiteSpace($rawApps)) { [xml]$appXml = $rawApps foreach ($appNode in @($appXml.appcmd.APP)) { if ($null -eq $appNode) { continue } $pn = [string]$appNode.'APPPOOL.NAME' $appName = [string]$appNode.'APP.NAME' $parts = $appName -split '/', 2 $sn = $parts[0] $vPath = if ($parts.Count -gt 1) { '/' + $parts[1] } else { '/' } if ($vPath -eq '/') { if (-not $poolToSites.ContainsKey($pn)) { $poolToSites[$pn] = [System.Collections.Generic.List[string]]::new() } if (-not $poolToSites[$pn].Contains($sn)) { $poolToSites[$pn].Add($sn) } } else { if (-not $poolToApps.ContainsKey($pn)) { $poolToApps[$pn] = [System.Collections.Generic.List[string]]::new() } $poolToApps[$pn].Add("$sn$vPath") } } } } catch { Write-Verbose -Message "[Get-IISWorkerProcess] appcmd list app failed: $($_.Exception.Message)" } } # ── 5. Build result objects ─────────────────────────────────────── foreach ($workerPid in ($procMap.Keys | Sort-Object)) { $pool = if ($pidToPool.ContainsKey($workerPid)) { $pidToPool[$workerPid] } else { '' } # Apply business filters if ($FilterPid.Count -gt 0 -and ($FilterPid -notcontains $workerPid)) { continue } if ($FilterAppPool.Count -gt 0) { $poolMatched = $false foreach ($fp in $FilterAppPool) { if ($pool -like $fp) { $poolMatched = $true; break } } if (-not $poolMatched) { continue } } # Identity $identity = if ($poolIdentityMap.ContainsKey($pool)) { $poolIdentityMap[$pool].Identity } else { '' } $identityType = if ($poolIdentityMap.ContainsKey($pool)) { $poolIdentityMap[$pool].IdentityType } else { 'Unknown' } # Sites / Applications $sites = if ($poolToSites.ContainsKey($pool)) { @($poolToSites[$pool]) } else { @() } $apps = if ($poolToApps.ContainsKey($pool)) { @($poolToApps[$pool]) } else { @() } # Status $status = if ([string]::IsNullOrEmpty($pool)) { 'Orphaned' } else { 'Running' } # Process metrics $procObj = $procMap[$workerPid] $cimObj = if ($cimProcMap.ContainsKey($workerPid)) { $cimProcMap[$workerPid] } else { $null } $startTime = $null $uptimeSecs = [long]0 $cpuSecs = [double]0 $workingSetMB = [long]0 $privateMemMB = [long]0 $virtualMemMB = [long]0 $threadCount = 0 $handleCount = 0 $commandLine = '' if ($null -ne $procObj -and $procObj -is [System.Diagnostics.Process]) { try { $startTime = $procObj.StartTime } catch { Write-Verbose -Message "[Get-IISWorkerProcess] Cannot read StartTime for PID $workerPid." } if ($null -ne $startTime) { $uptimeSecs = [long]([datetime]::Now - $startTime).TotalSeconds } try { $cpuSecs = [Math]::Round($procObj.TotalProcessorTime.TotalSeconds, 2) } catch { Write-Verbose -Message "[Get-IISWorkerProcess] Cannot read CPU time for PID $workerPid." } $workingSetMB = [long]($procObj.WorkingSet64 / 1MB) $privateMemMB = [long]($procObj.PrivateMemorySize64 / 1MB) $virtualMemMB = [long]($procObj.VirtualMemorySize64 / 1MB) try { $threadCount = $procObj.Threads.Count } catch { Write-Verbose -Message "[Get-IISWorkerProcess] Cannot read thread count for PID $workerPid." } try { $handleCount = $procObj.HandleCount } catch { Write-Verbose -Message "[Get-IISWorkerProcess] Cannot read handle count for PID $workerPid." } } elseif ($null -ne $cimObj) { $startTime = $cimObj.CreationDate if ($null -ne $startTime) { $uptimeSecs = [long]([datetime]::Now - $startTime).TotalSeconds } $workingSetMB = [long]($cimObj.WorkingSetSize / 1MB) $threadCount = [int]$cimObj.ThreadCount $handleCount = [int]$cimObj.HandleCount } if ($null -ne $cimObj) { $commandLine = [string]$cimObj.CommandLine } $results.Add(@{ ProcessId = $workerPid AppPoolName = $pool Sites = $sites Applications = $apps Identity = $identity IdentityType = $identityType StartTime = $startTime UptimeSeconds = $uptimeSecs CPUSeconds = $cpuSecs WorkingSetMB = $workingSetMB PrivateMemoryMB = $privateMemMB VirtualMemoryMB = $virtualMemMB ThreadCount = $threadCount HandleCount = $handleCount CommandLine = $commandLine Status = $status ErrorMessage = $null }) } if ($results.Count -eq 0) { $results.Add(@{ ProcessId = $null AppPoolName = $null Sites = @() Applications = @() Identity = $null IdentityType = $null StartTime = $null UptimeSeconds = $null CPUSeconds = $null WorkingSetMB = $null PrivateMemoryMB = $null VirtualMemoryMB = $null ThreadCount = $null HandleCount = $null CommandLine = $null Status = 'NoWorkerProcess' ErrorMessage = $null }) } return $results } } process { $filterPoolArg = if ($PSBoundParameters.ContainsKey('AppPoolName')) { $AppPoolName } else { @() } $filterPidArg = if ($PSBoundParameters.ContainsKey('ProcessId')) { $ProcessId } else { @() } foreach ($machine in $ComputerName) { $displayName = $machine.ToUpper() Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying '$machine'" try { $rawResults = Invoke-RemoteOrLocal ` -ComputerName $machine ` -Credential $Credential ` -ScriptBlock $scriptBlock ` -ArgumentList @($filterPoolArg, $filterPidArg) foreach ($entry in $rawResults) { [PSCustomObject]@{ PSTypeName = 'PSWinOps.IISWorkerProcess' ComputerName = $displayName ProcessId = $entry.ProcessId AppPoolName = $entry.AppPoolName Sites = $entry.Sites Applications = $entry.Applications Identity = $entry.Identity IdentityType = $entry.IdentityType StartTime = $entry.StartTime UptimeSeconds = $entry.UptimeSeconds CPUSeconds = $entry.CPUSeconds WorkingSetMB = $entry.WorkingSetMB PrivateMemoryMB = $entry.PrivateMemoryMB VirtualMemoryMB = $entry.VirtualMemoryMB ThreadCount = $entry.ThreadCount HandleCount = $entry.HandleCount CommandLine = $entry.CommandLine Status = $entry.Status ErrorMessage = $entry.ErrorMessage Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' } } } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '$machine': $_" continue } } } end { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed" } } |