# 如何透過 PowerShell 優雅的關閉 Chrome、Edge 或任何視窗應用程式

我們最近有個專案使用 Blazor WebAssembly 技術打造一個以 Web 介面為主的 POS 系統，過程中有個功能需要重啟 [Microsoft Edge](https://www.microsoft.com/zh-tw/edge) 瀏覽器，我發現直接停用 Process (處理序) 的作法，可能會導致 Microsoft Edge 重啟時發生異常通知，這才發現原來還有更安全的關閉方法。這篇文章我打算來分享幾種不同的方法與適用的情境。

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/b437c437432e0d65ed4c-202392104-7413c31b-3677-4c87-bc03-aa137b9f3624.png)

### 使用 PowerShell 取得正在運行的處理序

基本上有兩種方法，各有優缺點：

1.  使用 [Get-Process](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-process?WT.mc_id=DT-MVP-4015686) cmdlet
    
    取得所有處理序 (不含處理序擁有者資訊，你看不出啟動該處理序的使用者是誰)
    
    ```powershell
    Get-Process
    ```
    
    取得名為 `msedge` 的處理序 (若用 `msedge.exe` 是搜尋不到的喔)
    
    ```powershell
    Get-Process -Name 'msedge'
    ```
    
    取得名為 `msedge` 或 `chrome` 的處理序
    
    ```powershell
    Get-Process -Name 'msedge', 'chrome'
    ```
    
    篩選出**有視窗標題**的處理序清單 (**這些都是你目前可以肉眼看見的視窗應用程式清單**)
    
    ```powershell
    Get-Process | Where-Object {$_.MainWindowTitle} | Format-Table Id, Name, MainWindowTitle -AutoSize
    ```
    
    取得名為 `notepad2` 的處理序物件，並取得該物件所有屬性
    
    ```powershell
    Get-Process -Name 'notepad2' | Format-List *
    ```
    
    > 請注意: 使用 `Format-List` 只會顯示 `Id`, `Handles`, `CPU`, `SI`, `Name` 這 5 個摘要欄位，你必須使用 `Format-List *` 才能取得完整 [Process](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process?view=net-7.0&WT.mc_id=DT-MVP-4015686) 物件屬性清單！
    
    ```
    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` 的處理序，並取得執行該處理序的使用者名稱 (**需以系統管理員身份執行**)
    
    ```powershell
    Get-Process -Name 'notepad2' -IncludeUserName | Format-List *
    ```
    
2.  使用 [Get-CimInstance](https://learn.microsoft.com/en-us/powershell/module/cimcmdlets/get-ciminstance?view=powershell-7.3&WT.mc_id=DT-MVP-4015686) 取得執行該處理序的**使用者名稱**
    
    由於透過 [Get-Process](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-process?WT.mc_id=DT-MVP-4015686) 取得**使用者名稱**需要**以系統管理員身份執行**，我個人比較不喜歡這樣，所以我找到一個方法可以繞過這個限制，那就是透過 WMI 來取得這個資訊！
    
    ```powershell
    $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` 處理序並**顯示所有屬性**
    
    ```powershell
    $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 的原始碼](https://github.com/PowerShell/PowerShell/blob/3c7f4b573e96684781b8e1935eeb0b981bcd93bc/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs#L1399-L1408)之中可以發現一個文件沒寫的秘密，那就是 `Stop-Process` 骨子裡就是用 [Process](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process?view=net-7.0&WT.mc_id=DT-MVP-4015686) 的 [Kill 方法](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process.kill?view=net-7.0&WT.mc_id=DT-MVP-4015686#system-diagnostics-process-kill)立即停止該處理序，所以是比較暴力的解法，可能會導致應用程式的資料遺失！
    
    例如：關閉 [Notepad2](https://www.flos-freeware.ch/notepad2.html) 處理序，你會這樣執行：
    
    ```powershell
    Get-Process -Name 'notepad2' | Stop-Process
    ```
    
    如果你正好有正在編輯但尚未儲存的資料，當處理序被砍掉後，資料也會遺失！
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f6f8e727394a2c0497cb-202362820-0c8e3591-1289-4532-a7b6-cb260d285350.png)
    
    除了用 [Stop-Process](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/stop-process?WT.mc_id=DT-MVP-4015686) cmdlet 砍掉處理序外，你也可以用以下命令做到一樣的事：
    
    ```powershell
    (Get-Process -Name 'notepad2').Kill()
    ```
    
    > 透過上述 `Kill` 方法 ([原始碼](https://source.dot.net/#System.Diagnostics.Process/System/Diagnostics/Process.Unix.cs,4bfdd089d4e52d08)) 是透過 Win32 API 對 Process 送出 `SIGKILL` 訊號，讓作業系統強制關閉該處理序。
    
    如果你的應用程式有開很多**子處理序**(Child process)，而你希望連同子處理序一起砍掉的話，可以這樣執行：
    
    ```powershell
    (Get-Process -Name 'notepad2').Kill($true)
    ```
    
    > 這個方法比 `Stop-Process` cmdlet 更暴力，但也非常有效！
    
    如下圖是 Microsoft Edge 強制關閉之後重開的顯示訊息：
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/a80de786d51a48c773da-202381005-e73594c7-5dfc-4258-ae78-8340646a12c7.png)
    
-   優雅的關閉處理序 (Close the process)
    
    你透過 `Get-Process` 取得到的 Process 物件，都會有個 [MainWindowHandle](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process.mainwindowhandle?view=net-7.0&WT.mc_id=DT-MVP-4015686) 屬性，這是處理序**主視窗**的**視窗控制代碼**，只要該值大於 `0` 就代表這是一個有「視窗」的應用程式，也意味著這是一個含有 UI 介面的應用程式。
    
    > 注意: 你的 Windows 作業系統中，所有**處理序**的**所有視窗**，都會有個唯一的 `MainWindowHandle` 值，在同一台電腦上是**唯一識別值**(Unique identifier)，不會有重複的 Handle ID 出現。
    
    我們如果要關閉這類視窗應用程式，就可以改用 `CloseMainWindow` 方法來關閉。我以 `Notepad2` 為例，在編輯到一半的時候，收到關閉的通知，該應用程式會先問你要不要儲存檔案，因此這是一種比較溫和的關閉方法：
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/ef193796170da3eae4cb-202367461-1ab6b72d-fdf9-4569-811f-84844193cc04.png)
    
    > 透過上述 `CloseMainWindow` 方法 ([原始碼](https://referencesource.microsoft.com/#System/services/monitoring/system/diagnosticts/Process.cs,3abe28bbc909290d))，其技術核心並非使用 Signal 的方式關閉程式，而是透過 `Microsoft.Win32` 命名空間下的 [NativeMethods](https://referencesource.microsoft.com/#system/compmod/microsoft/win32/NativeMethods.cs) 類別 (這是一個 `internal static class` 所以不是一個公開的 API) 的 `PostMessage` 方法，對視窗應用程式送出 `NativeMethods.WM_CLOSE` 訊息(Message)，讓該視窗應用程式自行決定該如何關閉應用程式。
    

### 如何優雅的關閉 Chrome/Edge 瀏覽器

最後回到我們的專案需求，我們需要重開 Chrome 或 Edge 瀏覽器，但是又怕使用者在瀏覽器上操作到一半，所以最完美的作法，應該要符合以下條件：

1.  找出 Chrome 或 Edge 瀏覽器的**主要處理序**(Main Process)
    
    由於 Chrome 或 Edge 瀏覽器採用 Multi-process 策略，當你開啟瀏覽器時，事實上會從工作管理員看到數十個不同的 Process 同時執行，基本上一個頁籤就會有一個 Process 在跑。這種策略可以大幅提昇瀏覽器的穩定度，這才不會當你在一個頁籤掛掉的時候，連同其他頁籤也一起掛掉。
    
    在這麼多 Process 之間，其實只有一個是**主要處理序**(Main Process)，我們只要找到該處理序，並優雅的關閉他即可！
    
    ```powershell
    Get-Process -Name 'chrome' | Where-Object { $_.MainWindowHandle -gt 0 }
    ```
    
2.  透過 `CloseMainWindow` 方法關閉這些主要處理序
    
    關閉 Google Chrome 瀏覽器的**主要處理序**(Main Process)
    
    ```powershell
    Get-Process -Name 'chrome' | Where-Object { $_.MainWindowHandle -gt 0 } | Foreach-Item {
      $_.CloseMainWindow()
    }
    ```
    
    關閉 Microsoft Edge 瀏覽器的**主要處理序**(Main Process)
    
    ```powershell
    Get-Process -Name 'msedge' | Where-Object { $_.MainWindowHandle -gt 0 } | Foreach-Item {
      $_.CloseMainWindow()
    }
    ```
    
    此時如果有任意頁籤的網頁有實作 [beforeunload](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) 事件，你的瀏覽器就會出現「是否要離開網站？」的提示訊息，如下圖示：
    
    ![是否要離開網站？](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f6b5a64bde16cffc1a36-202368649-f4457ca6-8e74-494e-a6c4-9ed4f1e3f131.png)
    
3.  等待 10 秒鐘，如果使用者不回應，就強制關閉處理序
    
    你可以透過 `.HasExited` 屬性來判斷該程序是否已經退出
    
    ```powershell
    # 嘗試關閉 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 瀏覽器
    
    ```powershell
    Get-Process -Name 'chrome' | Where-Object { $_.MainWindowHandle -gt 0 } | Stop-Process
    ```
    
    ```powershell
    Get-Process -Name 'msedge' | Where-Object { $_.MainWindowHandle -gt 0 } | Stop-Process
    ```
    
-   優雅關閉 Google Chrome 或 Microsoft Edge 瀏覽器
    
    ```powershell
    (Get-Process -Name 'chrome' | Where-Object { $_.MainWindowHandle -gt 0 }).CloseMainWindow()
    ```
    
    ```powershell
    (Get-Process -Name 'msedge' | Where-Object { $_.MainWindowHandle -gt 0 }).CloseMainWindow()
    ```
    
-   軟硬兼施從容優雅的關閉方法
    
    我自己寫了一個 `Close-Process` cmdlet 並搭配遞迴(Recursion)呼叫，可以很方便的關閉任意視窗應用程式。它會自動倒數計時 9 秒，只要應用程式已結束就會立刻結束執行，但如果時間到還沒結束，程式就會強制關閉應用程式！
    
    ```powershell
    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)
    }
    ```
    
    基本使用方式：
    
    ```powershell
    Get-Process -Name 'chrome' -ErrorAction Ignore | Where-Object { $_.MainWindowHandle -gt 0 } | Close-Process
    ```
    
    指定等待秒數 (倒數計時 30 秒，若視窗應用程式依然未結束就強制關閉應用程式)
    
    ```powershell
    Get-Process -Name 'msedge' -ErrorAction Ignore | Where-Object { $_.MainWindowHandle -gt 0 } | Close-Process -WaitSeconds 30
    ```
    
-   透過 C# 來撰寫相同功能，程式碼更好看，也比 PowerShell 還容易閱讀！
    
    以下用 .NET 7 + Top-level statement 環境下執行：
    
    ```csharp
    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);
        }
    }
    ```
    

### 相關連結

-   [當 .NET Core 執行在 Linux 或 Docker 容器中如何優雅的結束 | The Will Will Web](/post/2020/07/22/How-to-handle-graceful-shutdown-in-NET-Core)
-   [Get-Process (Microsoft.PowerShell.Management) - PowerShell | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-process?WT.mc_id=DT-MVP-4015686)
-   [Stop-Process (Microsoft.PowerShell.Management) - PowerShell | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/stop-process?WT.mc_id=DT-MVP-4015686)
-   [Get-CimInstance (CimCmdlets) - PowerShell | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/module/cimcmdlets/get-ciminstance?view=powershell-7.3&WT.mc_id=DT-MVP-4015686)
-   [Window: beforeunload event - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event)
-   [Process 類別 (System.Diagnostics) | Microsoft Learn](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process?view=net-7.0&WT.mc_id=DT-MVP-4015686)
    -   [Process.MainWindowHandle 屬性](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process.mainwindowhandle?view=net-7.0&WT.mc_id=DT-MVP-4015686)
    -   [Process.HasExited 屬性](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process.hasexited?source=recommendations&view=net-7.0&WT.mc_id=DT-MVP-4015686)
    -   [Process.SessionId Property](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.sessionid?view=net-7.0&WT.mc_id=DT-MVP-4015686#system-diagnostics-process-sessionid)
    -   [Process.Refresh 方法](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics.process.refresh?view=net-7.0&WT.mc_id=DT-MVP-4015686#system-diagnostics-process-refresh)
-   [powershell - Get-Process and Process Owner - Stack Overflow](https://stackoverflow.com/a/41960963/910074)
-   [當 .NET Core 執行在 Linux 或 Docker 容器中如何優雅的結束](/post/2020/07/22/How-to-handle-graceful-shutdown-in-NET-Core)
