我們最近有個專案使用 Blazor WebAssembly 技術打造一個以 Web 介面為主的 POS 系統,過程中有個功能需要重啟 Microsoft Edge 瀏覽器,我發現直接停用 Process (處理序) 的作法,可能會導致 Microsoft Edge 重啟時發生異常通知,這才發現原來還有更安全的關閉方法。這篇文章我打算來分享幾種不同的方法與適用的情境。
使用 PowerShell 取得正在運行的處理序
基本上有兩種方法,各有優缺點:
-
使用 Get-Process cmdlet
取得所有處理序 (不含處理序擁有者資訊,你看不出啟動該處理序的使用者是誰)
Get-Process
取得名為 msedge
的處理序 (若用 msedge.exe
是搜尋不到的喔)
Get-Process -Name 'msedge'
取得名為 msedge
或 chrome
的處理序
Get-Process -Name 'msedge', 'chrome'
篩選出有視窗標題的處理序清單 (這些都是你目前可以肉眼看見的視窗應用程式清單)
Get-Process | Where-Object {$_.MainWindowTitle} | Format-Table Id, Name, MainWindowTitle -AutoSize
取得名為 notepad2
的處理序物件,並取得該物件所有屬性
Get-Process -Name 'notepad2' | Format-List *
請注意: 使用 Format-List
只會顯示 Id
, Handles
, CPU
, SI
, Name
這 5 個摘要欄位,你必須使用 Format-List *
才能取得完整 Process 物件屬性清單!
Name : Notepad2
Id : 19680
PriorityClass : Normal
FileVersion : 4.2.25
HandleCount : 219
WorkingSet : 16220160
PagedMemorySize : 2654208
PrivateMemorySize : 2654208
VirtualMemorySize : 171982848
TotalProcessorTime : 00:00:00.3593750
SI : 1
Handles : 219
VM : 4466950144
WS : 16220160
PM : 2654208
NPM : 13048
Path : C:\Program Files\Notepad2\Notepad2.exe
CommandLine : "C:\Program Files\Notepad2\Notepad2.exe" /z "C:\WINDOWS\system32\notepad.exe"
Parent : System.Diagnostics.Process (explorer)
Company :
CPU : 0.359375
ProductVersion :
Description : Notepad2 x64
Product :
__NounName : Process
SafeHandle : Microsoft.Win32.SafeHandles.SafeProcessHandle
Handle : 596
BasePriority : 8
ExitCode :
HasExited : False
StartTime : 2022/11/17 下午 12:11:47
ExitTime :
MachineName : .
MaxWorkingSet : 1413120
MinWorkingSet : 204800
Modules : {System.Diagnostics.ProcessModule (Notepad2.exe), System.Diagnostics.ProcessModule (ntdll.
dll), System.Diagnostics.ProcessModule (KERNEL32.DLL), System.Diagnostics.ProcessModule (K
ERNELBASE.dll)…}
NonpagedSystemMemorySize64 : 13048
NonpagedSystemMemorySize : 13048
PagedMemorySize64 : 2654208
PagedSystemMemorySize64 : 247008
PagedSystemMemorySize : 247008
PeakPagedMemorySize64 : 3317760
PeakPagedMemorySize : 3317760
PeakWorkingSet64 : 16269312
PeakWorkingSet : 16269312
PeakVirtualMemorySize64 : 4502978560
PeakVirtualMemorySize : 208011264
PriorityBoostEnabled : True
PrivateMemorySize64 : 2654208
ProcessorAffinity : 65535
SessionId : 1
StartInfo :
Threads : {33320}
VirtualMemorySize64 : 4466950144
EnableRaisingEvents : False
StandardInput :
StandardOutput :
StandardError :
WorkingSet64 : 16220160
SynchronizingObject :
MainModule : System.Diagnostics.ProcessModule (Notepad2.exe)
PrivilegedProcessorTime : 00:00:00.2968750
UserProcessorTime : 00:00:00.0625000
ProcessName : Notepad2
MainWindowHandle : 4653976
MainWindowTitle : Untitled - Notepad2
Responding : True
Site :
Container :
取得名為 notepad2
的處理序,並取得執行該處理序的使用者名稱 (需以系統管理員身份執行)
Get-Process -Name 'notepad2' -IncludeUserName | Format-List *
-
使用 Get-CimInstance 取得執行該處理序的使用者名稱
由於透過 Get-Process 取得使用者名稱需要以系統管理員身份執行,我個人比較不喜歡這樣,所以我找到一個方法可以繞過這個限制,那就是透過 WMI 來取得這個資訊!
$process = (Get-CimInstance Win32_Process)
$process | foreach {
$owner = Invoke-CimMethod -InputObject $_ -MethodName GetOwner | select -ExpandProperty user
Get-Process -Id $_.ProcessId | select-object Id,ProcessName,SI,StartTime,Name,@{n='UserName';e={$owner}}
}
取得 msedge.exe
處理序並顯示所有屬性
$process = (Get-CimInstance Win32_Process -Filter "name = 'msedge.exe'")
$process | foreach {
$owner = Invoke-CimMethod -InputObject $_ -MethodName GetOwner | select -ExpandProperty user
Get-Process -Id $_.ProcessId | select-object *,@{n='UserName';e={$owner}}
}
使用 PowerShell 關閉處理序
透過 PowerShell 關閉處理序有好幾種方法:
-
強制關閉處理序 (Kill the process)
我從 PowerShell 的 Stop-Process cmdlet 的原始碼之中可以發現一個文件沒寫的秘密,那就是 Stop-Process
骨子裡就是用 Process 的 Kill 方法立即停止該處理序,所以是比較暴力的解法,可能會導致應用程式的資料遺失!
例如:關閉 Notepad2 處理序,你會這樣執行:
Get-Process -Name 'notepad2' | Stop-Process
如果你正好有正在編輯但尚未儲存的資料,當處理序被砍掉後,資料也會遺失!
除了用 Stop-Process cmdlet 砍掉處理序外,你也可以用以下命令做到一樣的事:
(Get-Process -Name 'notepad2').Kill()
透過上述 Kill
方法 (原始碼) 是透過 Win32 API 對 Process 送出 SIGKILL
訊號,讓作業系統強制關閉該處理序。
如果你的應用程式有開很多子處理序(Child process),而你希望連同子處理序一起砍掉的話,可以這樣執行:
(Get-Process -Name 'notepad2').Kill($true)
這個方法比 Stop-Process
cmdlet 更暴力,但也非常有效!
如下圖是 Microsoft Edge 強制關閉之後重開的顯示訊息:
-
優雅的關閉處理序 (Close the process)
你透過 Get-Process
取得到的 Process 物件,都會有個 MainWindowHandle 屬性,這是處理序主視窗的視窗控制代碼,只要該值大於 0
就代表這是一個有「視窗」的應用程式,也意味著這是一個含有 UI 介面的應用程式。
注意: 你的 Windows 作業系統中,所有處理序的所有視窗,都會有個唯一的 MainWindowHandle
值,在同一台電腦上是唯一識別值(Unique identifier),不會有重複的 Handle ID 出現。
我們如果要關閉這類視窗應用程式,就可以改用 CloseMainWindow
方法來關閉。我以 Notepad2
為例,在編輯到一半的時候,收到關閉的通知,該應用程式會先問你要不要儲存檔案,因此這是一種比較溫和的關閉方法:
透過上述 CloseMainWindow
方法 (原始碼),其技術核心並非使用 Signal 的方式關閉程式,而是透過 Microsoft.Win32
命名空間下的 NativeMethods 類別 (這是一個 internal static class
所以不是一個公開的 API) 的 PostMessage
方法,對視窗應用程式送出 NativeMethods.WM_CLOSE
訊息(Message),讓該視窗應用程式自行決定該如何關閉應用程式。
如何優雅的關閉 Chrome/Edge 瀏覽器
最後回到我們的專案需求,我們需要重開 Chrome 或 Edge 瀏覽器,但是又怕使用者在瀏覽器上操作到一半,所以最完美的作法,應該要符合以下條件:
-
找出 Chrome 或 Edge 瀏覽器的主要處理序(Main Process)
由於 Chrome 或 Edge 瀏覽器採用 Multi-process 策略,當你開啟瀏覽器時,事實上會從工作管理員看到數十個不同的 Process 同時執行,基本上一個頁籤就會有一個 Process 在跑。這種策略可以大幅提昇瀏覽器的穩定度,這才不會當你在一個頁籤掛掉的時候,連同其他頁籤也一起掛掉。
在這麼多 Process 之間,其實只有一個是主要處理序(Main Process),我們只要找到該處理序,並優雅的關閉他即可!
Get-Process -Name 'chrome' | Where-Object { $_.MainWindowHandle -gt 0 }
-
透過 CloseMainWindow
方法關閉這些主要處理序
關閉 Google Chrome 瀏覽器的主要處理序(Main Process)
Get-Process -Name 'chrome' | Where-Object { $_.MainWindowHandle -gt 0 } | Foreach-Item {
$_.CloseMainWindow()
}
關閉 Microsoft Edge 瀏覽器的主要處理序(Main Process)
Get-Process -Name 'msedge' | Where-Object { $_.MainWindowHandle -gt 0 } | Foreach-Item {
$_.CloseMainWindow()
}
此時如果有任意頁籤的網頁有實作 beforeunload 事件,你的瀏覽器就會出現「是否要離開網站?」的提示訊息,如下圖示:
-
等待 10 秒鐘,如果使用者不回應,就強制關閉處理序
你可以透過 .HasExited
屬性來判斷該程序是否已經退出
# 嘗試關閉 Edge 瀏覽器
$edge = Get-Process -Name 'msedge' | Where-Object { $_.MainWindowHandle -gt 0 }
$edge.CloseMainWindow()
# 等候 10 秒鐘
Start-Sleep -Seconds 10
# 更新 Edge 瀏覽器處理序狀態
$edge.Refresh()
# 判斷該處理序是否已經結束
if (!$edge.HasExited) {
# 若處理序未結束就強制結束它
$edge | Stop-Process -Force
}
總結
給大家一些懶人包,複製貼上就能跑!👍
-
強制關閉 Google Chrome 或 Microsoft Edge 瀏覽器
Get-Process -Name 'chrome' | Where-Object { $_.MainWindowHandle -gt 0 } | Stop-Process
Get-Process -Name 'msedge' | Where-Object { $_.MainWindowHandle -gt 0 } | Stop-Process
-
優雅關閉 Google Chrome 或 Microsoft Edge 瀏覽器
(Get-Process -Name 'chrome' | Where-Object { $_.MainWindowHandle -gt 0 }).CloseMainWindow()
(Get-Process -Name 'msedge' | Where-Object { $_.MainWindowHandle -gt 0 }).CloseMainWindow()
-
軟硬兼施從容優雅的關閉方法
我自己寫了一個 Close-Process
cmdlet 並搭配遞迴(Recursion)呼叫,可以很方便的關閉任意視窗應用程式。它會自動倒數計時 9 秒,只要應用程式已結束就會立刻結束執行,但如果時間到還沒結束,程式就會強制關閉應用程式!
Function Close-Process {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
$Process,
[int]$WaitSeconds = 9
)
if ($Process -eq $null) { return }
"Closing $($edge.ProcessName) process. Counting down: $WaitSeconds" | Out-Default
if ($Process.HasExited) {
$Process.Close()
return
}
if ($Process.HasExited -eq $false) {
$Process.Refresh()
[void]$Process.CloseMainWindow()
}
if ($WaitSeconds -eq 0) {
$Process | Stop-Process
return
}
Start-Sleep -Seconds 1
$Process | Close-Process -WaitSeconds ($WaitSeconds - 1)
}
基本使用方式:
Get-Process -Name 'chrome' -ErrorAction Ignore | Where-Object { $_.MainWindowHandle -gt 0 } | Close-Process
指定等待秒數 (倒數計時 30 秒,若視窗應用程式依然未結束就強制關閉應用程式)
Get-Process -Name 'msedge' -ErrorAction Ignore | Where-Object { $_.MainWindowHandle -gt 0 } | Close-Process -WaitSeconds 30
-
透過 C# 來撰寫相同功能,程式碼更好看,也比 PowerShell 還容易閱讀!
以下用 .NET 7 + Top-level statement 環境下執行:
using System.Diagnostics;
var data = from p in Process.GetProcesses()
where p.MainWindowHandle > 0 && p.ProcessName == "msedge"
select p;
foreach (Process edge in data)
{
GracefullyShutdownProcess(edge);
Console.WriteLine(edge.ProcessName + " closed.");
}
void GracefullyShutdownProcess(Process p, int waitSeconds = 9)
{
if (!p.HasExited)
{
p.CloseMainWindow();
if (waitSeconds == 0)
{
p.Kill();
return;
}
Console.WriteLine($"Closing {p.ProcessName} process. Counting down: {waitSeconds}");
Thread.Sleep(1_000);
p.Refresh();
GracefullyShutdownProcess(p, waitSeconds - 1);
}
}
相關連結