從應用程式看 Windows 關機 ── 正確扛住結束通知、重新啟動與斷電

· · Windows, 關機, Windows 開發, Windows 服務, 設備用 PC, 資料完整性, 長時間執行, UPS

「夜間 Windows Update 重啟時,設備用 PC 上的量測應用程式寫到一半就停了,早上量測檔已經壞了。」「有人從共用 PC 登出,抱怨未儲存的編輯不見了。」── 對長時間執行的 Windows 應用程式來說,這兩種諮詢是經典款。

兩個現場的共通點,是把關機當成「不該發生的異常事件」。實際上,從 Windows Update 自動重啟、使用者登出、UPS 發起的關機,到毫無預告的斷電,從應用程式外面切斷執行的事件遲早會來。你擋不住它來。擋得住的是「來的時候把資料弄丟」。

幸好,無論 GUI 應用程式、主控台應用程式還是服務,Windows 都有在關機前通知應用程式的機制。本文以中小企業資訊人員與 Windows 應用程式開發者(尤其是設備用 PC 與長時間執行應用)為對象,依 2026 年 8 月當下的 Microsoft Learn 一次資訊,整理怎麼接這些通知、怎麼把收尾設計成「幾秒內做完」、重啟後怎麼自動恢復,以及完全沒有通知的斷電該怎麼準備。

1. 先講結論

  • 把關機設計成「遲早會來的正常事件」。收到通知後能用的時間原則上只有大約 5 秒,當場手忙腳亂存一切的設計會崩。前提是平常就頻繁自動儲存,讓「關機時必須存的差額」保持很小。1
  • 從 Windows 8 起的用戶端 OS,在開啟快速啟動時(多數支援休眠的 PC 預設如此),「關機」是混合關機,核心只是在休眠。真正完整重設的只有「重新啟動」。這才是「關機沒好、重啟就好了」的真正原因。2
  • GUI 應用程式應對 WM_QUERYENDSESSION 立刻回 TRUE,收尾放在 WM_ENDSESSION。原則上不可回 FALSE(拒絕)。1
  • 只有真的有不可中斷的作業時,才用 ShutdownBlockReasonCreate 顯示原因。即便如此,使用者和 OS 仍可強制繼續,因此「我們擋得住」的設計不成立。34
  • 主控台應用程式用 SetConsoleCtrlHandler 接收通知。寬限期更短──關閉主控台預設 5 秒。還有一個陷阱:載入了 gdi32.dll 或 user32.dll 的處理程序,有些事件不會送到。56
  • 依賴 .NET AppDomain.ProcessExit 的收尾,從 .NET 10 起,在處理程序「從外面被結束」的路徑上不會跑。從 Main 正常返回時仍會跑,但執行階段不再為關閉主控台與關機這類結束訊號提供預設處理,那些路徑的收尾必須改到符合應用程式模型的通知。7
  • Windows 服務可用 SERVICE_ACCEPT_PRESHUTDOWN,比 SERVICE_ACCEPT_SHUTDOWN(約 20 秒寬限)更早收到、且寬限期可設定。不過 PRESHUTDOWN 預設逾時從 Windows 10 Creators Update 起縮成 10 秒,無論哪條路都不要過度依賴寬限期。89
  • 重啟後的自動恢復,可把 RegisterApplicationRestart 與 ARSO(自動登入)合在一起。當機、沒有回應、更新觸發的重啟都有恢復路徑。1011
  • 斷電完全沒有通知。標準模式是寫完整暫存檔、沖刷、再用 ReplaceFile 交換;但 ReplaceFile 跨斷電也不保證原子性,因此備份(.bak)加上載入時驗證是一套。事後隔離可從事件記錄(1074/41/6008)做。121314

用一句話說,本文的結論是:「隨時保持通知一來就能在幾秒內打烊的狀態,並且用即使沒有通知的斷電也不壞的方式寫檔」

2. 關機時發生什麼 ── 四種結束方式

2.1. 登出、關機、重新啟動與斷電

從應用程式的角度看,重要的是兩軸:「使用者工作階段怎麼結束」以及「核心會怎樣」。

操作 使用者工作階段 核心與驅動程式 給應用程式的通知
登出 結束 繼續跑 WM_QUERYENDSESSION(ENDSESSION_LOGOFF)→ WM_ENDSESSION
關機(開啟快速啟動) 結束 休眠(存進 hiberfil.sys) WM_QUERYENDSESSION → WM_ENDSESSION,服務收到(PRE)SHUTDOWN
重新啟動 結束 完全結束;下次是完整開機 同上
斷電 立刻消失 立刻消失 沒有

登出與關機,從應用程式看幾乎是同一件事。WM_QUERYENDSESSION 的 lParam 若設了 ENDSESSION_LOGOFF 位元就是登出;若是 0 就是關機或重新啟動(兩者分不出來)。1 也就是說,「只是登出,沒關係」這種鬆懈不成立,正確設計是呼叫同一段收尾程式碼

四種結束方式與給應用程式的通知登出、關機與重新啟動都會送到 WM_QUERYENDSESSION 到 WM_ENDSESSION 的通知,收尾在幾秒內做完。只有斷電完全沒有通知,因此用第 8 章的寫入設計與 UPS 來準備登出QUERY → ENDSESSION關機重新啟動斷電無通知:寫入 + UPS幾秒內收尾

圖 1: 登出、關機與重新啟動都會送到 WM_QUERYENDSESSION 到 WM_ENDSESSION 的通知,收尾在幾秒內做完。只有斷電完全沒有通知,因此用第 8 章的寫入設計與 UPS 來準備。

2.2. 「關機沒好」的真正原因 ── 混合關機

表裡容易漏看的是第二列。從 Windows 8 起的用戶端 OS,支援休眠的 PC 預設開啟快速啟動(混合關機),「關機」的行為變了。使用者工作階段仍會照常登出,但核心工作階段不會關閉;連同裝置驅動程式一起存進休眠檔(hiberfil.sys),下次開機原樣還原。這樣開機較快,但核心與驅動程式狀態即使拔電也還在。2 不過這是有條件的行為。休眠本身被關掉(powercfg /hibernate off)、原則或電源選項關掉快速啟動、以及 Windows Server 上,關機仍是傳統的完整關機。某台 PC 走哪條路,可從電源選項的「開啟快速啟動」核取方塊,或 powercfg /a(可用的睡眠狀態)是否列出「Fast Startup」判斷。

關機操作時核心會怎樣關機操作依快速啟動是否開啟分成完整關機或核心休眠,重新啟動則一定走完整開機快速啟動開休眠關 / Server關機重新啟動工作階段結束 + 核心休眠完整關機下次:還原核心下次:完整開機

圖 2: 關機操作依快速啟動是否開啟分成完整關機或核心休眠,重新啟動則一定走完整開機。

另一方面,「重新啟動」一定走完整開機週期。例如驅動程式更新後,你需要全新的狀態。2 由此,現場常聽到的幾種現象就對上了。

  • 「關機再加電,裝置麻煩還在」── 核心與驅動程式只是從休眠還原,並沒有重設
  • 「重新啟動之後就好了」── 因為完整開機把它們初始化了
  • 設備用 PC 的事故處理步驟應寫「重新啟動」,不要寫「關電再開」

若要從命令列明確做完整關機,用 shutdown /s(Shutdown.exe 預設就是完整關機);若要預設的混合行為,用 shutdown /s /hybrid2 不建議關掉快速啟動。應用程式側應假設「關機時核心可能只是在休眠」──例如不要用 OS 開機時間去估「累計運轉時間」──並設計成兩種都不會壞(快速啟動開或關依環境而異)。

3. GUI 應用程式該怎麼做 ── WM_QUERYENDSESSION 與 WM_ENDSESSION

3.1. 兩則訊息怎麼分工

有視窗與訊息佇列的應用程式,會分兩階段收到工作階段結束通知。1

  1. WM_QUERYENDSESSION──查詢:「可以結束嗎?」應用程式應立刻回 TRUE;DefWindowProc 的預設回應也是 TRUE。這裡不要開始收尾。
  2. WM_ENDSESSION(wParam=TRUE)──已確定的通知:「工作階段真的要結束了」。收尾在這裡做。

對 WM_QUERYENDSESSION 回 FALSE 可以中止關機,但文件寫得很清楚:「應回 TRUE 並尊重使用者的意圖」;回了 FALSE 的應用程式仍會在全螢幕 UI 被標成「正在阻止關機的應用程式」。主控台應用程式與沒有可見視窗的應用程式本來就不能中止關機,5 秒內沒回應就會被自動結束。14

兩階段工作階段結束通知的流程對 WM_QUERYENDSESSION 查詢回 TRUE 後以 WM_ENDSESSION 確定並做收尾。用 FALSE 拒絕會把應用程式標成正在阻止關機,約 5 秒沒回應可能被強制繼續TRUE(原則)FALSE(拒絕)約 5 秒沒回強制繼續取消WM_QUERYENDSESSIONWM_ENDSESSION(已確定)標成正在阻止關機當成當住在這裡收尾處理程序結束關機中止

圖 3: 對 WM_QUERYENDSESSION 查詢回 TRUE 後以 WM_ENDSESSION 確定並做收尾。用 FALSE 拒絕會把應用程式標成正在阻止關機,約 5 秒沒回應可能被強制繼續。

3.2. 不回應會怎樣 ── 5 秒這道牆

WM_QUERYENDSESSION 與 WM_ENDSESSION 都可以把回應延後大約 5 秒。超過之後,系統會顯示「這個應用程式正在阻止關機」畫面,使用者可選擇強制繼續(=強制結束應用程式)。4 被強制結束的處理程序沒有第二次機會把檔存完。

因此設計重點是這兩點。

  • 把收尾壓在 5 秒內做完的量。 Microsoft 自己也建議平常就頻繁存檔,讓關機時要存的變少,並把未存資料存到暫存位置、下次啟動再還原。1
  • 關機過程中不要跳出確認對話方塊。你坐在那裡等「要儲存嗎?」的時候,5 秒就過了。靜默落到安全側(自動儲存)。

3.3. 在 WinForms 與 WPF 的實作

在 .NET 桌面應用程式裡,這些訊息會被轉成架構事件。WinForms 會引發 FormClosing,CloseReason 告訴你是不是關機造成的。

// WinForms:關機/登出時也會引發 FormClosing
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (e.CloseReason == CloseReason.WindowsShutDown)
    {
        // 只做冪等的快照儲存。不要顯示對話方塊。
        // 也不要設 e.Cancel = true(拒絕)。
        SaveWorkingStateToTempFile();
        return;
    }

    // 使用者按 × 這類一般關閉,這裡可以確認
}

在 WPF,對應的是 Application.SessionEnding 事件(XAML 的 SessionEnding 屬性,或覆寫 OnSessionEnding)。

WinForms/WPF 事件如何對應到訊息WM_QUERYENDSESSION 的查詢階段對應 WinForms FormClosing 與 WPF SessionEnding,那裡最多只做冪等快照儲存。已確定的 WM_ENDSESSION 沒有對應事件,因此用 WndProc 或掛鉤接收,做只有確定後才能做的收尾WM_QUERYENDSESSIONWinForms:FormClosingWPF:SessionEnding只做冪等快照WM_ENDSESSION無事件:WndProc 掛鉤確定後再收尾

圖 4: WM_QUERYENDSESSION 的查詢階段對應 WinForms FormClosing 與 WPF SessionEnding,那裡最多只做冪等快照儲存。已確定的 WM_ENDSESSION 沒有對應事件,因此用 WndProc 或掛鉤接收,做只有確定後才能做的收尾。

// WPF:App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
    base.OnSessionEnding(e);

    // 可以分辨 ReasonSessionEnding.Logoff / Shutdown,
    // 但基準是兩種都跑同一段快照儲存
    SaveWorkingStateToTempFile();

    // 沒有例外理由就不要設 e.Cancel = true
}

這裡有一個注意點。FormClosing(CloseReason.WindowsShutDown)與 WPF 的 SessionEnding 都對應查詢階段(WM_QUERYENDSESSION)。若另一個應用程式拒絕,關機會中止,你的應用程式繼續跑。因此這些事件裡能做的,是即使關機中止也無害、跑幾次結果都一樣的冪等快照儲存。若需要「只有真的要結束才能做的收尾」(斷線、交還資源等),直接在 WndProc 掛已確定的 WM_ENDSESSION(wParam=TRUE)並在那裡做。

無論哪條路徑,把本體收進共用的「快照儲存」函式,讓正常結束、關機、以及(可能的話)當機的還原資料用同一格式寫,下次啟動的還原邏輯就是一條路。即使當機也要留下資訊的設計,見「Windows 應用程式因程式錯誤的例外掉下也要確實留下日誌」。

4. 真的必須擋住時 ── ShutdownBlockReasonCreate

寫光碟或韌體這類「做到一半被切斷就會實體損壞」的作業是例外。這裡的正確做法是:不可中斷作業開始時用 ShutdownBlockReasonCreate 登錄原因字串,結束時立刻呼叫 ShutdownBlockReasonDestroy。關機被要求時,那個原因會顯示在「這個應用程式正在阻止關機」畫面,使用者可決定繼續或取消。3

用 ShutdownBlockReasonCreate 保護的流程不可中斷作業開始時登錄原因;保護期間若來了關機要求,全螢幕顯示原因並對 WM_QUERYENDSESSION 以 FALSE 拒絕。使用者可取消或強制繼續,作業結束時清除原因取消強制繼續開始不可中斷工作ShutdownBlockReasonCreate在背景執行緒跑結束:Destroy這段期間關機顯示原因 + FALSE處理程序結束

圖 5: 不可中斷作業開始時登錄原因;保護期間若來了關機要求,全螢幕顯示原因並對 WM_QUERYENDSESSION 以 FALSE 拒絕。使用者可取消或強制繼續,作業結束時清除原因。

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, string pwszReason);

[DllImport("user32.dll", SetLastError = true)]
static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd);

// 從建立主視窗的執行緒呼叫(從其他執行緒會失敗)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Writing measurement data to a file");
try
{
    // 不可中斷作業在背景執行緒跑。若在 UI 執行緒同步跑,
    // 訊息泵會停,處理程序會在下面的 WM_QUERYENDSESSION
    // 拒絕程式碼跑到之前就被當成「沒有回應」強制繼續
    await Task.Run(() => WriteMeasurementData());
}
finally
{
    ShutdownBlockReasonDestroy(this.Handle);
    _criticalOperationInProgress = false;
}

// 另外,只在保護期間對 WM_QUERYENDSESSION 以 FALSE 拒絕
protected override void WndProc(ref Message m)
{
    const int WM_QUERYENDSESSION = 0x0011;
    if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
    {
        m.Result = IntPtr.Zero;   // 拒絕。登錄的原因字串會顯示在全螢幕 UI
        return;
    }
    base.WndProc(ref m);
}

這裡容易誤解的是角色分工。ShutdownBlockReasonCreate 只做登錄原因字串;它本身並不擋住關機。真正把關機攔住的,是上面這種在保護旗標立著時對 WM_QUERYENDSESSION 回 FALSE 的自己的處理。兩者當一套用,作業結束立刻兩邊都清掉。另外,被保護的作業本身放在背景執行緒,讓 UI 執行緒能處理訊息──拒絕機制要等訊息送到才會動(即便如此,使用者和 OS 仍可強制繼續,因此「如果沒擋住」也不壞的寫入設計──第 8 章──仍然必要)。

營運上有三個注意點。

  • 原因字串要短而具體。使用者很急,只會讀幾秒。文件自己舉的適當例子是 “Burning a CD”。3
  • 不要讓它登錄整個應用程式生命週期。 API 假定的是「只在不可中斷作業進行中」。
  • 不要用「擋得住」當設計前提。使用者可選擇強制繼續,強制關機(ENDSESSION_CRITICAL)根本不會等。文件寫得很清楚:”Applications should not depend on being able to block shutdown”。4

5. 主控台應用程式與背景處理程序該怎麼做

5.1. SetConsoleCtrlHandler 與較短的寬限期

主控台應用程式收不到視窗訊息,控制訊號會送到用 SetConsoleCtrlHandler 登錄的處理函式。各訊號的預設寬限期如下。5

訊號 何時發生 預設寬限期
CTRL_C_EVENT / CTRL_BREAK_EVENT Ctrl+C / Ctrl+Break 沒有逾時
CTRL_CLOSE_EVENT 關閉主控台、工作管理員「結束工作」(從「詳細資料」頁籤強制殺處理程序是沒有通知的立即結束,不在此表) 約 5 秒
CTRL_SHUTDOWN_EVENT 系統關機(服務處理程序) 約 20 秒

有兩點要注意。第一,實質上只有以服務執行的處理程序能收到 CTRL_LOGOFF_EVENT 與 CTRL_SHUTDOWN_EVENT。互動工作階段裡的應用程式在登出時就被結束,等這些訊號的設計不成立。5 第二,載入了 gdi32.dll 或 user32.dll 的處理程序,即使你當它是主控台應用程式,也會被當成 Windows 應用程式,LOGOFF/SHUTDOWN 處理常式不會被呼叫。官方變通是建立隱藏視窗,接收 WM_QUERYENDSESSION/WM_ENDSESSION。6

各主控台訊號的寬限期Ctrl+C 與 Ctrl+Break 沒有明確逾時;關閉主控台約 5 秒,給服務處理程序的關機訊號約 20 秒;超過就強制結束處理程序無逾時約 5 秒約 20 秒CTRL_C / BREAKHandlerRoutine 收尾CTRL_CLOSECTRL_SHUTDOWN寬限期後強制結束

圖 6: Ctrl+C 與 Ctrl+Break 沒有明確逾時;關閉主控台約 5 秒,給服務處理程序的關機訊號約 20 秒;超過就強制結束處理程序。

// 主控台應用程式:Ctrl+C 與關閉主控台時收尾
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);

delegate bool HandlerRoutine(int ctrlType);   // 2 = CTRL_CLOSE_EVENT

static readonly HandlerRoutine s_handler = OnCtrlEvent;  // 保住參考,避免被 GC 回收

static bool OnCtrlEvent(int ctrlType)
{
    // 只做 5 秒內做完的收尾
    FlushAndCloseDataFile();
    return false;   // 交給預設處理常式;處理程序結束
}

static void Main()
{
    SetConsoleCtrlHandler(s_handler, add: true);
    // ...
}

5.2. .NET 的陷阱 ── 不要依賴 ProcessExit

在 .NET 裡,「在 AppDomain.ProcessExit 收尾就好」長期是現成模式,但從 .NET 10 起執行階段不再提供預設的結束訊號處理常式,CTRL_CLOSE_EVENT/CTRL_SHUTDOWN_EVENT 既不觸發 ProcessExit 也不觸發 AssemblyLoadContext.Unloading。OS 預設處理常式會立刻結束處理程序。7

.NET 10 之後 ProcessExit 怎麼變到 .NET 9 為止,執行階段的預設訊號處理常式接到結束訊號、引發 ProcessExit 再結束。從 .NET 10 起執行階段不提供預設處理常式,OS 預設處理立刻結束處理程序,要自己登錄處理常式CTRL_CLOSE / SHUTDOWN到 .NET 9:ProcessExit從 .NET 10:立刻結束自己登錄處理常式

圖 7: 到 .NET 9 為止,執行階段的預設訊號處理常式接到結束訊號、引發 ProcessExit 再結束。從 .NET 10 起執行階段不提供預設處理常式,OS 預設處理立刻結束處理程序,要自己登錄處理常式。

改走各應用程式模型的正規路徑。

  • GUI 應用程式:前一章的 FormClosing / SessionEnding
  • Generic Host(含 Worker Service):IHostApplicationLifetime 與 BackgroundService.StopAsync。用 HostOptions.ShutdownTimeout 把停止寬限期寫清楚
  • 裸主控台應用程式:SetConsoleCtrlHandler(或用 PosixSignalRegistration 訂閱 SIGINT/SIGTERM 對等訊號)
各應用程式模型在哪裡接收結束通知GUI 應用程式用 FormClosing 與 SessionEnding,已確定的工作再用 WM_ENDSESSION 掛鉤;Generic Host 用 IHostApplicationLifetime 與 StopAsync;裸主控台應用程式用 SetConsoleCtrlHandler 或 PosixSignalRegistration。依賴 ProcessExit 在外部訊號路徑上不會觸發GUI非 GUIHost主控台哪種應用程式模型?FormClosing / SessionEndingHost 還是主控台?ENDSESSION 掛鉤Lifetime + StopAsyncSetConsoleCtrlHandler設定 ShutdownTimeout不要依賴 ProcessExit

圖 8: GUI 應用程式用 FormClosing 與 SessionEnding,已確定的工作再用 WM_ENDSESSION 掛鉤;Generic Host 用 IHostApplicationLifetime 與 StopAsync;裸主控台應用程式用 SetConsoleCtrlHandler 或 PosixSignalRegistration。依賴 ProcessExit 在外部訊號路徑上不會觸發。

寬限期依路徑而異──GUI 與關閉主控台約 5 秒,服務是第 6 章的 SCM 寬限期(約 20 秒,或 PRESHUTDOWN 的設定值),Ctrl+C 沒有明確逾時。但每條路徑的寬限期都有限、不能指望,因此設計主軸是平常就是「每個處理檢查點都已存好」,而不是「在結束事件裡拼命做」

6. Windows 服務該怎麼做 ── SHUTDOWN 與 PRESHUTDOWN

6.1. 兩種關機通知

服務不受登出影響,但關機與重新啟動時會被停止。通知以控制碼從 Service Control Manager(SCM)送來,接收前要宣告接受旗標。8

宣告 會送到的通知 時機與寬限期
SERVICE_ACCEPT_SHUTDOWN SERVICE_CONTROL_SHUTDOWN 關機處理中通知。預設約 20 秒,上限 WaitToKillServiceTimeout
SERVICE_ACCEPT_PRESHUTDOWN SERVICE_CONTROL_PRESHUTDOWN 在 SHUTDOWN 之前通知。SCM 等到服務停止或逾時
給服務的關機通知順序關機開始時,宣告了 PRESHUTDOWN 的服務先以設定的寬限期收到通知,然後再送預設約 20 秒的 SHUTDOWN 通知,寬限期到期就結束處理程序關機開始PRESHUTDOWN(若有宣告)SHUTDOWN(約 20 秒)寬限期到期 → 結束

圖 9: 關機開始時,宣告了 PRESHUTDOWN 的服務先以設定的寬限期收到通知,然後再送預設約 20 秒的 SHUTDOWN 通知,寬限期到期就結束處理程序。

PRESHUTDOWN 逾時可用 ChangeServiceConfig2(SERVICE_CONFIG_PRESHUTDOWN_INFO)設定;預設是 Windows 10 Creators Update(組建 15063)起 10 秒,之前是 3 分鐘9 若還抱著「PRESHUTDOWN 有 3 分鐘」的舊知識,在現行 OS 上寬限期只有預期的 1/18。另外,PRESHUTDOWN 會讓整個系統的關機停在那個區間,因此文件也說它「只應在特殊情況使用」。8

處理常式側的實務也很重要。控制處理常式必須在 30 秒內返回;花時間的停止工作交給另一條執行緒,回報 SERVICE_STOP_PENDING,立刻返回。8

// Win32 服務:接受 PRESHUTDOWN,把停止工作交給背景執行緒
g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN;

DWORD WINAPI ServiceHandlerEx(DWORD control, DWORD, LPVOID, LPVOID)
{
    switch (control)
    {
    case SERVICE_CONTROL_PRESHUTDOWN:
    case SERVICE_CONTROL_STOP:
        ReportStatus(SERVICE_STOP_PENDING, /*waitHintMs*/ 3000);
        SetEvent(g_stopEvent);   // 告訴背景執行緒停止並立刻返回
        return NO_ERROR;
    }
    return ERROR_CALL_NOT_IMPLEMENTED;
}

// 背景執行緒側:若收尾比 waitHint 還長,持續定期回報
// SERVICE_STOP_PENDING 並遞增 dwCheckPoint。
// SCM 依 waitHint 與檢查點前進判斷「還活著而且有進度」。
// 若回報停了,可能被當成當住並繼續關機。做完一定要回報 SERVICE_STOPPED

6.2. 不依賴寬限期的設計

從服務側改寫寬限期上限 WaitToKillServiceTimeout 來延長,明確不建議。文件要求的是反過來──服務應盡快做完收尾,讓靠 UPS 供電的機器能在電池耗盡前完成關機。指引是平常就頻繁存檔、讓未存資料最少,關機時不要花時間釋放記憶體,通知網路對端時也不要等太久。另外,關機時 SCM 預設不考慮相依關係,因此停止處理必須「即使相依的服務已經倒下也還能動」。8

不依賴寬限期的停止處理設計若在每個處理檢查點都存檔、讓未存資料始終最少,停止通知來時的收尾幾秒就做完。把一切留到結束才存的設計塞不進寬限期,強制結束就丟資料每個檢查點都存停止 → 存一點差額 → 完成結束時才存一切停止 → 存檔趕不上寬限強制結束 → 資料遺失

圖 10: 若在每個處理檢查點都存檔、讓未存資料始終最少,停止通知來時的收尾幾秒就做完。把一切留到結束才存的設計塞不進寬限期,強制結束就丟資料。

在 .NET Worker Service(UseWindowsService)裡,SERVICE_CONTROL_STOP 與 SHUTDOWN 會被轉成主機停止,並呼叫 BackgroundService.StopAsync。本文撰寫時的現成實作接受 STOP/SHUTDOWN 一族;若也需要 PRESHUTDOWN,就要擴充處理常式。無論哪種,把 HostOptions.ShutdownTimeout 寫清楚,並在幾秒內做完 StopAsync。一般如何做服務,見「Windows 服務的建立與維運」。

7. 重新啟動後自動恢復

對設備用 PC 或無人值守 PC,設計範圍不只是「扛過關機」,還有「重啟後自己回來」。

7.1. RegisterApplicationRestart 與恢復回呼

若已呼叫 RegisterApplicationRestart,應用程式會被登錄為當機(未處理例外)、沒有回應、更新觸發的應用程式重啟、以及更新觸發的 OS 重啟的重啟候補。可以登錄重啟用的命令列引數,因此若包含「當時開著哪個檔」與「哪個還原點」,重啟後就能從停下的地方繼續。10

要掌握的規格如下。10

  • 登錄必須在問題發生前完成(更新情境裡,處理 WM_QUERYENDSESSION 是最後機會)
  • 為避免重啟迴圈,執行未滿 60 秒的處理程序不會被重啟
  • 以提升權限執行的處理程序不是自動重啟候補(沒有提升同意就無法重建處理程序)。需要提升的應用程式的自動恢復,做法是 UI 維持標準權限、把特權工作隔離到服務,或用工作排程器「以最高權限執行」這類明確啟動路徑
  • 當機或當住後的重啟要經使用者同意;更新後的重啟是自動的
  • 要跨過 OS 重啟恢復,要求重啟的那一方(安裝程式等)必須以 EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS 旗標呼叫關機 API

若也登錄 RegisterApplicationRecoveryCallback,當機時 WER(Windows Error Reporting)會呼叫回呼,給你一段寬限期去存進行中的資料。不過若儲存花時間,必須在登錄時指定的 ping 間隔內持續呼叫 ApplicationRecoveryInProgress,否則恢復工作會做到一半被切斷。存完後用 ApplicationRecoveryFinished 通知完成。應用程式更新時「替換使用中的檔並重啟」是 Restart Manager 的領域,細節見「如何替換使用中的 exe/DLL」。

7.2. ARSO ── 更新重啟後的自動登入

Windows Update 重啟後,若沒有人登入,使用者工作階段的應用程式不會回來。補這段空隙的是 ARSO(Winlogon Automatic Restart Sign-On)。Windows Update 開始重啟時,會安全地保存最後一位互動使用者的認證資料、設定 Autologon,重啟後自動讓該使用者登入,然後鎖定畫面11 也有 shutdown /g 這類要求重啟並恢復已登錄應用程式的命令。有些環境會用組織原則(DisableAutomaticRestartSignOn 等)關掉它,因此設計無人值守恢復時,把這個設定當一套檢查。若背景工作一直需要依賴使用者工作階段的自動啟動,一開始就做成 Windows 服務才對。

應用程式在重啟後自動恢復的路徑若在問題發生前用 RegisterApplicationRestart 登錄,當機或沒有回應時經使用者同意後重啟應用程式,更新觸發的重啟則在 ARSO 自動登入並鎖定畫面後重啟。執行未滿 60 秒與提升權限的處理程序不在範圍內同意RegisterApplicationRestart當機或當住更新重啟應用程式重啟ARSO 登入 + 鎖定不包含:未滿 60 秒/提升權限

圖 11: 若在問題發生前用 RegisterApplicationRestart 登錄,當機或沒有回應時經使用者同意後重啟應用程式,更新觸發的重啟則在 ARSO 自動登入並鎖定畫面後重啟。執行未滿 60 秒與提升權限的處理程序不在範圍內。

8. 扛住完全沒有通知的斷電 ── 寫入設計與 UPS

8.1. 「無論何時被切斷都不壞」的寫法 ── 暫存檔 + ReplaceFile

跳電、電源供應器故障、插頭被拔,既沒有 WM_ENDSESSION 也沒有 PRESHUTDOWN。只要對設定或量測結果「就地覆寫原檔」,寫到一半斷電就可能留下新舊混在一起的壞檔

標準模式是在同一磁碟區寫完整的暫存檔再交換。ReplaceFile 把「存到新檔 → 把原檔挪開 → 重新命名 → 刪除」包成單一 API,也會帶過原檔的建立時間、ACL、替代資料流等屬性(三個檔必須在同一磁碟區)。12 .NET 的 File.Replace 就是直接呼叫它。

用暫存檔與 ReplaceFile 儲存並恢復的流程儲存時寫完整暫存檔、沖刷、再用 ReplaceFile 交換,舊內容留在 .bak。下次啟動驗證主檔,壞了就回退 .bak下次啟動儲存時完好損壞任一步斷電驗證主檔直接使用回退到 .bak沖刷到磁碟寫完整暫存檔ReplaceFile → .bak

圖 12: 儲存時寫完整暫存檔、沖刷、再用 ReplaceFile 交換,舊內容留在 .bak。下次啟動驗證主檔,壞了就回退 .bak。

// 設定與資料的標準模式:寫完整暫存檔再交換,並留下舊內容
public static void SaveAtomically(string path, string content)
{
    string dir = Path.GetDirectoryName(path)!;
    string tmp = Path.Combine(dir, Path.GetRandomFileName());  // 在同一磁碟區建立

    try
    {
        using (var fs = new FileStream(tmp, FileMode.CreateNew, FileAccess.Write))
        using (var writer = new StreamWriter(fs))
        {
            writer.Write(content);
            writer.Flush();
            fs.Flush(flushToDisk: true);   // 等同 FlushFileBuffers。把 OS 緩衝區
                                           // 寫出到磁碟(裝置側快取的限制見 8.2)
        }

        if (File.Exists(path))
            File.Replace(tmp, path, path + ".bak");  // 呼叫 ReplaceFile。舊內容留成 .bak
        else
            File.Move(tmp, path);
    }
    catch
    {
        // 做到一半失敗時不要留下暫存檔。定期儲存反覆失敗
        // 會讓磁碟區堆滿完整複本
        try { File.Delete(tmp); } catch { /* 刪除失敗時優先拋出原本的例外 */ }
        throw;
    }
}

這樣一來,平常運作下永遠能讀到「完整舊檔」或「完整新檔」。不過 ReplaceFile 是多步驟的命名空間操作,跨斷電的原子性規格並不保證。所以上面的例子才留備份(.bak)──讀取側在啟動時驗證主檔、壞了就回退備份,這是一套。附加寫入的記錄或 CSV 不能用這招,那些用「一行=一筆紀錄,讀取時丟掉壞掉的最後一行」這種把損壞算進去的格式。

8.2. WriteFile 成功不代表已到磁碟

另一個前提是:即使 WriteFile 回成功,資料可能還只在 OS 快取裡。Windows 把檔案讀寫放進系統緩衝區,再用延遲寫入定期反映到磁碟。要確定資料到磁碟,要嘛用 FlushFileBuffers 明確沖刷,要嘛在 CreateFile 指定 FILE_FLAG_WRITE_THROUGH 讓每次寫入穿過快取。檔案系統中繼資料一律會被快取,因此確認中繼資料也需要沖刷或 write-through。13

不過每次都呼叫 FlushFileBuffers 沒效率,文件也鼓勵考慮 FILE_FLAG_NO_BUFFERING+WRITE_THROUGH,而不是頻繁呼叫。13 實務上,「只在交易檢查點或即將關閉檔案時沖刷」是務實的折衷。這一層的機制──快取管理員、延遲寫入,以及「沖刷了仍可能沒到磁碟」的硬體快取──深入見「快取管理員:你的 WriteFile 究竟何時送達磁碟」。

8.3. UPS 與電池監視 ── 把斷電變成關機

設備用 PC 對抗斷電的真正手段是 UPS。把 UPS 的角色想成不是「阻止停電」,而是把「沒有通知的斷電」變成「有通知的計劃關機」。設計是兩段。

  1. 設計寬限期:UPS 電池撐住時間 >「偵測切到電池 → 應用程式與服務收尾 → OS 關機完成」的總和。若服務停止處理太慢,這個不等式就不成立(第 6.2 節)
  2. 偵測:從交流電切到電池、以及剩餘容量下降,會以 PBT_APMPOWERSTATUSCHANGE 事件通知。有視窗的應用程式以 WM_POWERBROADCAST 接收;沒有視窗的服務宣告 SERVICE_ACCEPT_POWEREVENT,在 HandlerEx 以 SERVICE_CONTROL_POWEREVENT 接收(WM_POWERBROADCAST 不會送到服務控制處理常式)。收到後呼叫 GetSystemPowerStatus,檢查 ACLineStatus(是否在交流電上)與 BatteryLifePercent,導入中斷量測、儲存、要求關機15
用 UPS 把斷電變成計劃關機的流程停電讓 UPS 切到電池時會通知 PBT_APMPOWERSTATUSCHANGE,檢查電源狀態後,儲存加上關機要求,把沒有通知的斷電變成有通知的計劃關機停電UPS 切到電池PBT_APMPOWERSTATUSCHANGEGetSystemPowerStatus中斷並儲存要求關機平常的通知流程(3–6)

圖 13: 停電讓 UPS 切到電池時會通知 PBT_APMPOWERSTATUSCHANGE,檢查電源狀態後,儲存加上關機要求,把沒有通知的斷電變成有通知的計劃關機。

典型的 USB 連接 UPS 在 Windows 上看起來像電池,因此可用這套標準 API 偵測。若廠商管理軟體有「剩餘 N% 時關 OS」功能,也要確認門檻與應用程式的收尾時間對得上。從睡眠或休眠恢復,以及長時間執行的問題,是另一條軸,見「睡眠、休眠、Modern Standby 與長時間執行的應用程式」。

9. 怎麼驗證 ── 安全地試關機

關機處理很容易變成「寫了,卻從沒在接近正式的條件下試過」。準備一套安全驗證的步驟。

  • 在測試機或 VM 上試:不要先在正式設備用 PC 上試。在有 Hyper-V 檢查點(快照)的測試環境,反覆關機、重新啟動、強制斷電(關掉 VM)。不過 VM「關電」只重現「客體 OS 毫無預告地停」;它不重現實體磁碟揮發性快取消失、或控制器相依的損壞。若要以設備用 PC 出貨,最終檢查是在等同正式的硬體上做真正的切斷電源測試
  • 用登出做快速檢查:WM_QUERYENDSESSION → WM_ENDSESSION 路徑在登出時也會跑(唯一差別是 lParam 設了 ENDSESSION_LOGOFF 位元),因此可在開發機方便確認收尾程式碼的行為1
  • 完整關機與混合分別試:分別試 shutdown /s /t 0(完整)、shutdown /s /hybrid /t 0(預設行為)、shutdown /r /t 0(重新啟動)2
  • 量收尾花多久:在收尾函式開頭與結尾把時間戳寫進記錄,量是否塞進 5 秒(或服務的設定寬限期)
要驗證的操作與各能確認什麼登出方便檢查通知路徑;關機命令的完整、混合與重新啟動確認正式通知路徑與寬限期;VM 關電測試突然停止的耐受力;實體切斷電源測試是含實體儲存的最終檢查登出QUERY → ENDSESSION 路徑shutdown /s /hybrid /r正式路徑 + 寬限VM 關電客體突然停止實體切斷電源含儲存(最終)

圖 14: 登出方便檢查通知路徑;關機命令的完整、混合與重新啟動確認正式通知路徑與寬限期;VM 關電測試突然停止的耐受力;實體切斷電源測試是含實體儲存的最終檢查。

事後隔離時,事件記錄(系統)有用。正常關機或重新啟動會記錄 事件識別碼 1074(哪個處理程序為誰、因何開始關機)。突然斷電或當機沒有 1074,下次開機會記錄 事件識別碼 41(Kernel-Power)與 6008(前一次系統關機非預期)14「夜裡發生了什麼」從這裡開始。

# 檢查近期關機相關事件的歷史
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
    Select-Object TimeCreated, Id, ProviderName, Message |
    Format-List

若 1074 顯示「Windows Update 的重新啟動」且應用程式資料壞了,問題在收尾程式碼。6008/41 只顯示「非預期關機」;藍白當機(當機)或強制重設也會記,不只斷電。若 41 的 BugcheckCode 非零就是當機;若是 0 且也沒有記憶體傾印,斷電的可能性較高──從周圍資訊隔離原因,一旦知道是斷電,下一步就是第 8 章的寫入設計與 UPS。

10. 總結

  • 關機是「遲早會來的正常事件」。通知後的寬限期原則上只有大約 5 秒,因此前提是頻繁自動儲存,讓「結束時要做的事」最少。
  • 從 Windows 8 起的用戶端 OS,若開啟快速啟動,「關機」是混合關機,核心只是在休眠。真正完整重設的只有「重新啟動」──事故處理步驟請寫「重新啟動」。
  • GUI 應用程式對 WM_QUERYENDSESSION 立刻回 TRUE,已確定的收尾放在 WM_ENDSESSION。WinForms/WPF 的 FormClosing 與 SessionEnding 對應查詢階段,那裡最多只做冪等快照儲存。關機過程中不要跳出對話方塊。
  • 真的不可中斷的作業,用 ShutdownBlockReasonCreate 顯示原因來保護。但任何地方都不保證你擋得住。
  • 主控台應用程式用 SetConsoleCtrlHandler 接收通知;服務用 SERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWN。現行 OS 上 PRESHUTDOWN 預設寬限期是 10 秒。在 .NET,停止依賴 ProcessExit,改走符合應用程式模型的正規路徑。
  • 重啟後的恢復可用 RegisterApplicationRestart(加上恢復回呼)與 ARSO 做成無人值守。
  • 斷電沒有通知。用暫存檔 + ReplaceFile 交換(搭配備份 + 載入時驗證)、檢查點沖刷,以及「把斷電變成計劃關機」的 UPS 來準備。
關機處理的整體圖像有通知的結束,用幾秒內能打烊的收尾回應,並導入重啟後自動恢復;沒有通知的斷電,用無論何時被切斷都不壞的寫法與 UPS 準備,並含實體硬體驗證。這兩根支柱就是本文的結論有通知無通知怎麼結束幾秒收尾(3–6)安全寫入 + UPS(8)重啟後自動恢復(7)在硬體上驗證(9)

圖 15: 有通知的結束,用幾秒內能打烊的收尾回應,並導入重啟後自動恢復;沒有通知的斷電,用無論何時被切斷都不壞的寫法與 UPS 準備,並含實體硬體驗證。這兩根支柱就是本文的結論。

  • 在 VM 與登出上安全驗證,事後用事件識別碼 1074/41/6008 隔離。

下次替應用程式加功能時,問自己一次:如果這段工作做到一半 WM_ENDSESSION 來了,或電源被拔掉,下次啟動還剩下什麼?把那個答案寫進設計,就是以後不用在設備用 PC 前面抱頭站一早上的最短路徑。

相關文章

相關諮詢領域

小村軟體有限公司承接設備用 PC 與長時間執行應用的關機與斷電對策設計與實作、從 Windows Update 重啟或登出開始的資料損壞與「早上已經停了」事故的根本原因調查,以及 Windows 服務停止處理與自動恢復的設計審查。從「每次關機好像都會壞、不知道從哪下手」這個階段開始就可以。

參考連結

  1. Microsoft Learn, WM_QUERYENDSESSION message. 關於工作階段結束時會送出 WM_QUERYENDSESSION,應用程式應回 TRUE 並尊重使用者意圖(DefWindowProc 預設也是 TRUE);關於收尾應延到 WM_ENDSESSION;關於 5 秒後系統會顯示正在阻止關機的應用程式 UI、使用者可強制結束;關於 lParam 裡 ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL 位元的意義;關於關機與重新啟動分不出來;以及應頻繁存檔、讓結束時要存的變少。  2 3 4 5 6 7

  2. Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. 關於快速啟動時核心工作階段不會關閉、被當成休眠,核心與裝置驅動程式狀態存進 hiberfil.sys;關於「重新啟動」一定走完整開機,因為需要全新的 Windows 狀態;關於快速啟動預設開啟、不建議關掉;以及 Shutdown.exe 預設是完整關機,/hybrid 選項才是混合行為。  2 3 4 5

  3. Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). 關於在不可中斷作業開始時呼叫以登錄原因字串、結束時呼叫 ShutdownBlockReasonDestroy;關於只能從建立視窗的執行緒呼叫;以及使用者只會讀原因幾秒,因此字串應短而清楚。  2 3

  4. Microsoft Learn, Shutdown Changes for Windows Vista. 關於對 WM_QUERYENDSESSION/WM_ENDSESSION 的回應各可延後 5 秒、之後使用者可選擇繼續或取消;關於主控台應用程式或沒有可見視窗的應用程式不能中止關機,5 秒沒回應或回 FALSE 會被自動結束;關於若需要擋住應以 ShutdownBlockReasonCreate 登錄原因;以及應用程式不可依賴擋得住關機。  2 3 4

  5. Microsoft Learn, HandlerRoutine callback function. 關於用 SetConsoleCtrlHandler 登錄的處理常式會收到的 CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN 事件;關於 CTRL_CLOSE_EVENT 預設逾時約 5000 毫秒、服務處理程序的 CTRL_SHUTDOWN_EVENT 約 20000 毫秒;關於 CTRL_LOGOFF/SHUTDOWN_EVENT 實質上只有服務會收到,因為互動應用程式在登出時就被結束;以及處理常式在另一條執行緒上跑。  2 3

  6. Microsoft Learn, SetConsoleCtrlHandler function. 關於載入了 gdi32.dll 或 user32.dll 的處理程序會被當成 Windows 應用程式,CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT 處理常式不會被呼叫;關於變通是建立隱藏視窗並處理 WM_QUERYENDSESSION/WM_ENDSESSION;以及訊號處理期間主控台函式可能無法正確運作。  2

  7. Microsoft Learn, .NET runtime no longer provides default termination signal handlers. 關於從 .NET 10 起執行階段不再為 Windows 的 CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT(Unix 的 SIGTERM/SIGHUP 對等)提供預設處理常式;關於 OS 預設處理會立刻結束應用程式,AppDomain.ProcessExit 與 AssemblyLoadContext.Unloading 不再觸發;以及應在較高層函式庫或應用程式程式碼登錄符合應用程式模型的訊號處理。  2

  8. Microsoft Learn, Service Control Handler Function. 關於宣告 SERVICE_ACCEPT_PRESHUTDOWN 的服務先收到 SERVICE_CONTROL_PRESHUTDOWN,然後宣告 SERVICE_ACCEPT_SHUTDOWN 的服務收到 SERVICE_CONTROL_SHUTDOWN;關於關機時預設寬限期約 20 秒、OS 重啟時上限是 WaitToKillServiceTimeout;關於不應延長這個值;關於控制處理常式應在 30 秒內返回、回報 STOP_PENDING 與等待提示、把長工作交給另一條執行緒;關於考量 UPS 運作應盡快做完收尾;以及關機時 SCM 預設不考慮相依關係。  2 3 4 5

  9. Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). 關於 PRESHUTDOWN 通知後 SCM 等到服務停止或逾時;關於預設逾時從 Windows 10 Creators Update(組建 15063)起是 10 秒、之前是 3 分鐘;關於用 ChangeServiceConfig2 設定;以及 SERVICE_STOP_PENDING 期間可繼續更新狀態。  2

  10. Microsoft Learn, RegisterApplicationRestart function (winbase.h). 關於可為當機、沒有回應、更新、以及伴隨更新的電腦重啟登錄重啟;關於可指定重啟用的命令列引數;關於必須在問題發生前登錄,更新情境裡處理 WM_QUERYENDSESSION 是最後機會;關於執行未滿 60 秒的處理程序不會被重啟;關於當機或當住後的重啟要經使用者同意;以及跨過 OS 重啟需要以 EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS 關機。  2 3

  11. Microsoft Learn, Winlogon automatic restart sign-on (ARSO). 關於 Windows Update 開始自動重啟時會保存最後一位互動使用者的認證資料並設定 Autologon;關於重啟後自動讓使用者登入並鎖定工作階段;關於成功登入後刪除已保存的認證資料;以及可用群組原則(DisableAutomaticRestartSignOn 等)設定。  2

  12. Microsoft Learn, ReplaceFileW function (winbase.h). 關於 ReplaceFile 把相當於「存到新檔、暫時重新命名原檔、重新命名新檔、刪除原檔」的多個步驟包成單一函式;關於它保留原檔的建立時間、DACL、加密、壓縮、具名資料流等屬性;以及備份、被替換的檔與替換檔必須在同一磁碟區。  2

  13. Microsoft Learn, File Caching. 關於寫入預設進系統快取、再以延遲寫入反映到磁碟;關於 FILE_FLAG_WRITE_THROUGH 立刻寫到磁碟;關於 FlushFileBuffers 可明確沖刷;以及檔案系統中繼資料一律被快取,因此確認中繼資料需要沖刷或 write-through。  2 3

  14. Microsoft Learn, Troubleshoot unexpected reboots using system event logs. 關於正常重啟會記錄事件識別碼 1074(哪個處理程序為誰、因何開始關機);關於非預期重啟會記錄事件識別碼 41(Kernel-Power)與 6008(前一次關機非預期);以及這些識別碼可隔離重啟種類。  2

  15. Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. 關於電池與交流電切換或剩餘容量下降時,此事件經 WM_POWERBROADCAST 通知;以及收到後應呼叫 GetSystemPowerStatus,檢查 SYSTEM_POWER_STATUS 的 ACLineStatus、BatteryFlag、BatteryLifePercent 等欄位。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

「關機」之後沒好的問題,「重新啟動」之後卻好了。為什麼?
從 Windows 8 起的用戶端 OS,在開啟快速啟動時(多數支援休眠的 PC 預設如此),「關機」走的是混合關機。使用者會登出,但核心與驅動程式狀態會存進休眠檔,下次開機原樣還原。也就是說,OS 核心並沒有被重設。「重新啟動」則一定走完整開機,驅動程式與服務的麻煩才會被清掉。隔離步驟請寫「重新啟動」,不要寫「關機再加電」。若要從命令列做完整關機,可用 shutdown /s。
能不能擋住關機,等到應用程式存完檔?
可以暫時要求等待,但不能可靠地擋住。只在不可中斷的作業進行中,用 ShutdownBlockReasonCreate 登錄原因字串,那個原因會出現在「這個應用程式正在阻止關機」畫面,使用者可決定繼續或取消。但使用者仍可選擇強制繼續,強制關機或更新觸發的重啟也可能完全不等。因此正解不是「擋住」,而是頻繁自動儲存以降低風險資料量,再加上從結束通知起幾秒內做完的收尾設計。
Windows 服務停很久。能不能延長關機寬限期?
預設接收 SERVICE_CONTROL_SHUTDOWN 時,寬限期大約 20 秒,取決於 WaitToKillServiceTimeout 登錄值。不建議應用程式自己改這個值來延長。若需要更長寬限期,可宣告 SERVICE_ACCEPT_PRESHUTDOWN 並接收 SERVICE_CONTROL_PRESHUTDOWN;會比其他人更早收到通知,逾時可用 ChangeServiceConfig2 設定(Windows 10 Creators Update 起預設 10 秒,之前是 3 分鐘)。不過 PRESHUTDOWN 會讓整個關機停在那個區間,因此只限真正需要的情況,根本做法仍是把停止工作本身設計成幾秒內做完。
在 .NET 的 AppDomain.ProcessExit 做關機收尾安全嗎?
建議不要依賴它。過去執行階段會登錄預設訊號處理常式,CTRL_CLOSE_EVENT 與 CTRL_SHUTDOWN_EVENT 會觸發 ProcessExit;從 .NET 10 起執行階段不再提供預設的結束訊號處理,那些路徑也不再觸發 ProcessExit。請依應用程式模型走對應的通知路徑:GUI 應用程式用 FormClosing 或 SessionEnding(那是查詢階段通知,只做冪等儲存;必須在工作階段確定結束後才能做的收尾,放進 WM_ENDSESSION 掛鉤);Generic Host / Worker Service 用 IHostApplicationLifetime 與 StopAsync;主控台應用程式用 SetConsoleCtrlHandler 或 PosixSignalRegistration。
要怎麼避免突然斷電把檔案弄壞?
斷電完全沒有通知,所以只能用「無論何時被切斷都不壞」的寫法。基準是不要就地覆寫原檔:在同一磁碟區寫完整的暫存檔、沖刷,再用 ReplaceFile(.NET 的 File.Replace)交換。平常運作下,讀到的不是完整舊檔就是完整新檔;但 ReplaceFile 跨斷電的原子性規格並不保證,因此要留備份(第三個引數),並在載入時驗證主檔、壞了就回退備份。另外,WriteFile 成功不代表資料已到磁碟,重要檢查點要用 FlushFileBuffers 或 FILE_FLAG_WRITE_THROUGH 確認寫入。設備用 PC 的標準做法是搭配 UPS,偵測切到電池後導入安全關機。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽