<# .SYNOPSIS Closes your open apps to free up memory and CPU before a game or a big job, without touching Windows itself. .DESCRIPTION Finds processes running in your own session and asks each windowed app to close, the same as clicking its X, so it can save or prompt you. Anything still open after -GracePeriodSeconds is left alone unless you add -Force, which ends it. Never touched: processes in other sessions (services run in session 0), anything that runs from the Windows folder, a built-in list of shell and security processes, this PowerShell window and the terminal it runs in, and anything matching -Exclude. Background (windowless) apps such as tray icons are only included with -IncludeBackground. Services are only stopped if you name them with -StopService. Every close asks for confirmation (answer A for Yes to All), and -WhatIf lists what would close without closing anything. Returns one object per process or service. .PARAMETER Exclude Process names to keep running, wildcards allowed (for example steam*, discord, obs64). .PARAMETER IncludeBackground Also end windowless processes from outside the Windows folder, such as tray apps and updaters. These have no window to close politely, so they're ended directly. .PARAMETER GracePeriodSeconds How long to wait for apps to close on their own. Default: 10. .PARAMETER Force End any app that's still running after the grace period (usually one showing a "save changes?" prompt). .PARAMETER StopService Service names to stop as well. Nothing is stopped unless you list it here. .EXAMPLE .\Stop-NonEssentialProcess.ps1 -WhatIf .EXAMPLE .\Stop-NonEssentialProcess.ps1 -Exclude steam*, discord, obs64 -IncludeBackground -Confirm:$false #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [string[]]$Exclude = @(), [switch]$IncludeBackground, [ValidateRange(0, 300)] [int]$GracePeriodSeconds = 10, [switch]$Force, [string[]]$StopService = @() ) # Shell, input, audio, security and session plumbing. Most live in the Windows folder anyway; this catches the ones that don't. $protected = @( 'System', 'Idle', 'Registry', 'smss', 'csrss', 'wininit', 'winlogon', 'services', 'lsass', 'LsaIso', 'svchost', 'fontdrvhost', 'dwm', 'Memory Compression', 'explorer', 'sihost', 'ctfmon', 'taskhostw', 'RuntimeBroker', 'dllhost', 'conhost', 'audiodg', 'ShellExperienceHost', 'StartMenuExperienceHost', 'SearchHost', 'SearchApp', 'TextInputHost', 'ApplicationFrameHost', 'LockApp', 'SystemSettings', 'Widgets', 'WidgetService', 'PhoneExperienceHost', 'smartscreen', 'SecurityHealthSystray', 'SecurityHealthService', 'MsMpEng', 'NisSrv', 'MpDefenderCoreService', 'Taskmgr', 'WindowsTerminal', 'OpenConsole', 'powershell', 'pwsh', 'powershell_ise', 'cmd' ) $mySession = (Get-Process -Id $PID).SessionId $windowsDir = if ($env:SystemRoot) { $env:SystemRoot.TrimEnd('\') + '\' } else { $null } # Keep this script's own process and everything above it (the console, the terminal, the editor that launched it). $keepIds = [System.Collections.Generic.HashSet[int]]::new() $parentOf = @{} try { Get-CimInstance -ClassName Win32_Process -Property ProcessId, ParentProcessId -ErrorAction Stop | ForEach-Object { $parentOf[[int]$_.ProcessId] = [int]$_.ParentProcessId } } catch { Write-Verbose "Couldn't read parent processes from CIM: $($_.Exception.Message)" } $walk = $PID while ($walk -and $keepIds.Add($walk)) { $walk = if ($parentOf.ContainsKey($walk)) { $parentOf[$walk] } else { (Get-Process -Id $walk -ErrorAction SilentlyContinue).Parent.Id } } function New-Result($Process, [string]$Action, [string]$Detail) { [pscustomobject]@{ Type = 'Process' Name = $Process.ProcessName Id = $Process.Id Window = $Process.MainWindowTitle MemoryMB = [math]::Round($Process.WorkingSet64 / 1MB, 1) Action = $Action Detail = $Detail Path = $Process.Path } } $candidates = foreach ($p in Get-Process) { if ($p.SessionId -ne $mySession -or $keepIds.Contains($p.Id)) { continue } if ($protected -contains $p.ProcessName) { continue } if ($Exclude | Where-Object { $p.ProcessName -like $_ }) { Write-Verbose "Excluded: $($p.ProcessName) ($($p.Id))"; continue } $path = $null try { $path = $p.Path } catch { } if (-not $path) { Write-Verbose "Skipped $($p.ProcessName) ($($p.Id)): can't read its path, so it's probably elevated or protected."; continue } if ($windowsDir -and $path.StartsWith($windowsDir, [StringComparison]::OrdinalIgnoreCase)) { continue } $hasWindow = $p.MainWindowHandle -ne [IntPtr]::Zero if (-not $hasWindow -and -not $IncludeBackground) { continue } [pscustomobject]@{ Process = $p; HasWindow = $hasWindow } } $waiting = [System.Collections.Generic.List[object]]::new() foreach ($c in $candidates | Sort-Object { $_.Process.ProcessName }) { $p = $c.Process $label = if ($p.MainWindowTitle) { "$($p.ProcessName) ($($p.Id)) '$($p.MainWindowTitle)'" } else { "$($p.ProcessName) ($($p.Id))" } $verb = if ($c.HasWindow) { 'Close app window' } else { 'End background process' } if (-not $PSCmdlet.ShouldProcess($label, $verb)) { continue } try { if ($c.HasWindow) { if ($p.CloseMainWindow()) { $waiting.Add($p) } else { New-Result $p 'Failed' 'The window refused the close request.' } } else { $result = New-Result $p 'Ended' 'Background process' Stop-Process -Id $p.Id -Force -ErrorAction Stop -Confirm:$false $result } } catch { New-Result $p 'Failed' $_.Exception.Message } } # Give the polite closes time to finish, then deal with whatever's left. $deadline = (Get-Date).AddSeconds($GracePeriodSeconds) while ($waiting.Count -and (Get-Date) -lt $deadline) { foreach ($p in @($waiting)) { $p.Refresh() if ($p.HasExited) { New-Result $p 'Closed' $null; [void]$waiting.Remove($p) } } if ($waiting.Count) { Start-Sleep -Milliseconds 500 } } foreach ($p in $waiting) { if ($Force) { try { Stop-Process -Id $p.Id -Force -ErrorAction Stop -Confirm:$false; New-Result $p 'Ended' "Still open after $GracePeriodSeconds s" } catch { New-Result $p 'Failed' $_.Exception.Message } } else { New-Result $p 'StillRunning' 'Probably waiting on a save prompt. Use -Force to end it.' } } foreach ($name in $StopService) { $svc = Get-Service -Name $name -ErrorAction SilentlyContinue if (-not $svc) { [pscustomobject]@{ Type = 'Service'; Name = $name; Id = $null; Window = $null; MemoryMB = $null; Action = 'NotFound'; Detail = $null; Path = $null }; continue } if ($svc.Status -ne 'Running') { [pscustomobject]@{ Type = 'Service'; Name = $svc.Name; Id = $null; Window = $svc.DisplayName; MemoryMB = $null; Action = 'AlreadyStopped'; Detail = [string]$svc.Status; Path = $null }; continue } if (-not $PSCmdlet.ShouldProcess("$($svc.DisplayName) ($($svc.Name))", 'Stop service')) { continue } try { Stop-Service -InputObject $svc -ErrorAction Stop -Confirm:$false [pscustomobject]@{ Type = 'Service'; Name = $svc.Name; Id = $null; Window = $svc.DisplayName; MemoryMB = $null; Action = 'Stopped'; Detail = 'Starts again on the next reboot if set to Automatic'; Path = $null } } catch { [pscustomobject]@{ Type = 'Service'; Name = $svc.Name; Id = $null; Window = $svc.DisplayName; MemoryMB = $null; Action = 'Failed'; Detail = $_.Exception.Message; Path = $null } } }