睡眠・休眠・Modern Standby 與長時間執行的應用程式 ── 用設計預防「半夜停止運轉」

· · 睡眠, Modern Standby, 電源管理, SetThreadExecutionState, 長時間運轉, 計時器, 常駐應用程式, C#, .NET, 缺陷調查, Windows 開發, 技術諮詢

「監控應用程式應該整晚都在運作,早上一看,圖表卻在凌晨一點就停住了」「桌上型電腦上穩定運作了好幾個月的收集工具,一換成筆記型電腦,記錄就開始出現缺漏」── 在長時間持續執行的業務應用程式諮詢案例中,這是最典型不過的症狀。查看日誌,既沒有例外也沒有當機,只是有好幾個小時份的記錄整段消失。而元兇往往不是應用程式的錯誤,而是 Windows 的電源管理。

麻煩的是,「睡眠」這個詞聽起來只有一種,實際上內容並不只一種。傳統的 S3 睡眠、休眠狀態(S4),以及近年筆記型電腦主流的 Modern Standby(S0 low power idle),從應用程式的角度看到的停止方式不同,對策是否有效也不同。尤其 Modern Standby 常因「睡眠中系統仍在運作」的宣傳而被誤解,但實際上桌面應用程式反而會被積極地停止執行。

本文將先掌握睡眠的種類,以及睡眠期間應用程式行為的最低限度知識,接著整理「不讓系統睡眠」「以睡眠為前提」「在指定時刻喚醒」這三種設計該如何區分使用,並附上實作範例與判斷表。

1. 先講結論

由於結論不少,這裡分成前提(1.1)、「不讓系統睡眠」的設計(1.2)、「配合睡眠」的設計(1.3)三部分列出。建議先讀完 1.1,判斷自己的應用程式屬於 1.2 還是 1.3 之後再往下讀正文,這樣最快(判斷表在第 7 章)。

1.1. 前提 ── 睡眠的種類,以及停止期間發生的事

  • Windows 的睡眠有傳統的 S3 睡眠、休眠狀態(S4)、Modern Standby(S0 low power idle)這幾種,支援 Modern Standby 的機種不支援 S1~S3。自己的電腦屬於哪一種,可以用 powercfg /a 確認。123
  • 即使在 Modern Standby 期間,桌面應用程式也不會持續運作。DAM(Desktop Activity Moderator)會暫停桌面行程的執行緒(工作階段 0 的服務則是被節流)。請不要以「因為是 S0 所以應該還在動」這種前提來設計。4
  • 睡眠期間執行緒不會被排程執行,計時器期限的計算方式也因 API 世代而異。Windows 8 以後,相對指定的計時器與等待(SetWaitableTimer 的相對指定、SleepEx 等)在睡眠期間不會累計時間,剩餘時間會延續到復原之後。56 .NET 的計時器在 .NET 10 為止的實作會計入睡眠時間(若睡眠期間已到期,會在復原後立即觸發),預定從 .NET 11 起改為不計入(.NET 11 在本文撰寫時尚未發行,請參見 3.1 的附註)。6 經過時間 API 也同樣分成包含睡眠時間(GetTickCount、QueryPerformanceCounter = Stopwatch)與不包含睡眠時間(QueryUnbiasedInterruptTime)兩種。78
  • 睡眠期間變成閒置狀態的 TCP 連線,有可能被 NAT、防火牆、負載平衡器等中間裝置的閒置逾時機制悄悄丟棄(例如 Azure Load Balancer 的預設值是 4 分鐘後無通知丟棄)。設計上應以「復原後需要重新連線」為前提。94

1.2. 「不讓系統睡眠」的設計(第 4 章)

  • 抑止睡眠的正規做法是 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)。不過它只對因無操作逾時而觸發的自動睡眠有效,無法防止使用者按下電源按鈕或闔上筆電上蓋所指示的睡眠。應只在需要的區段設定,結束後務必清除。1011
  • 抑止是否生效可以用 powercfg /requests 確認。若使用 PowerCreateRequest 系列 API,可以在要求上附加理由字串,並顯示在這份清單中,有利於運維時的調查。121314
  • 如果像是 24 小時持續運作的設備用電腦,需求本來就是「完全不讓系統睡眠」,正確做法應該是用電源設定停用睡眠,而不是靠應用程式的抑止 API(具體設定位置見 7.1)。

1.3. 「配合睡眠」的設計(第 5 章、第 6 章)

  • 若以睡眠為前提來設計,.NET 可用 SystemEvents.PowerModeChanged、Win32 可用 WM_POWERBROADCAST(PBT_APMSUSPEND / PBT_APMRESUMEAUTOMATIC)來偵測。暫停通知的緩衝時間,每個應用程式大約只有 2 秒,電池電量緊迫時甚至可能沒有通知就直接進入睡眠。15161718
  • 沒有視窗的主控台或服務型應用程式,可以透過 PowerRegisterSuspendResumeNotification 在沒有 HWND 的情況下接收相同的通知(最小範例見 5.1)。19
  • 若要在固定時刻確實執行,工作排程器的「執行工作時喚醒電腦」(WakeToRun)是第一選擇。它會讓系統一直保持喚醒直到工作完成為止。不過這項功能依賴電源選項中的允許喚醒計時器設定,因此必須在實機上確認確實會喚醒。2021

2. Windows 的睡眠不只一種

首先,只掌握作為對策思考前提的三種狀態。

狀態 通稱 內容 從應用程式角度來看
S3 傳統睡眠 CPU 停止,只有 RAM 通電以保持狀態 完全不執行任何計算處理1
S4 休眠狀態 將記憶體內容寫入休眠檔後切斷電源 同上。復原時從檔案還原1
S0 low power idle Modern Standby 系統以低電力局部持續運作,並可瞬間復原 桌面應用程式會被 DAM 停止執行24

S3 與 S4 都是「系統完全不執行任何計算工作、看起來像是關機的狀態」。1 相對地,Modern Standby 是接近智慧型手機電源模型的方式,即使螢幕關閉,也會維持網路連線並以低電力待機,從按下電源按鈕到復原不到 1 秒。支援 Modern Standby 的機種不會使用 S1~S3。222

這裡的重點是搭載於 Modern Standby 機種上的 DAM(Desktop Activity Moderator)。DAM 是在進入待機時,把桌面應用程式的執行壓制到與 S3 睡眠相同程度的機制,互動工作階段的行程會所有執行緒都被暫停,工作階段 0 的服務則會被節流(大部分時間被暫停,只斷續執行)。4 也就是說,「因為是 Modern Standby,所以睡眠期間應用程式應該還在動」這種期待其實正好相反,從應用程式的角度看,停止方式和 S3 一樣,而且和 S3 不同的是,「系統本身在動,但只有應用程式停下來」,因此會以時間偏差或計時器行為不一致的形式呈現,建議這樣理解會比較保險。4

自己的電腦(或客戶現場的電腦)屬於哪一種方式,不需要管理員權限,用下面的指令就能確認。3

> powercfg /a
以下のスリープ状態がこのシステムで利用可能です:
    スタンバイ (S0 低電力アイドル) ネットワークに接続されています
    休止状態
    ...

若顯示 スタンバイ (S3) 就是 S3 機種,顯示 S0 低電力アイドル 就是 Modern Standby 機種。遇到「換成筆記型電腦之後就開始出現記錄缺漏」這類諮詢時,第一步一定會先確認這一點。

3. 睡眠期間、以及復原後,應用程式會發生什麼事

3.1. 執行緒與計時器

睡眠期間(S3/S4,以及 DAM 暫停期間)執行緒不會被執行。14 容易被忽略的是,計時器或等待的「期限」是如何計算睡眠時間的,這一點依 API 的層級而有不同的處理方式。以和下一節經過時間 API 表格相同的形式列出,會是這樣:

計時器・等待的種類 睡眠期間的處理方式 復原後的行為
Win32 的相對計時器(SetWaitableTimer / SetWaitableTimerEx 的相對指定) Windows 8 以後不會累計時間(Windows 7 以前會把低電力狀態的時間也算進去)5 等待剩餘時間後才觸發5
指定逾時的等待 API(SleepEx / WaitForMultipleObjectsEx 等) Windows 8 以後不計入非運作時間6 延續剩餘時間繼續等待6
.NET 的受控計時器(System.Threading.Timer 等) .NET 10 為止會計入(以 GetTickCount64 為基礎),預定從 .NET 11 起不計入(以 QueryUnbiasedInterruptTime 為基礎)6 .NET 10 為止若已過期限會在復原後立即觸發,預定從 .NET 11 起會等待剩餘時間後才觸發6
可喚醒計時器(SetWaitableTimerfResume 設為 TRUE) 會將系統本身喚醒23 喚醒後觸發,但若在無人閒置計時器(最短 2 分鐘)期間沒有宣告「使用中」,會再次進入睡眠(第 6 章)23

第三行提到的變更文件,將其說明為 Environment.TickCount64 的底層機制改變所帶來的重大變更(breaking change),文件本身也提醒「有可能出現復原後計時器不再立即觸發的程式碼」。6

關於 .NET 11 的相關敘述

.NET 11 在本文撰寫時(2026 年 7 月)尚未發行。.NET 採用每年 11 月發行主要版本的年度週期,最近一版的 .NET 10 於 2025 年 11 月 11 日發行。24 本文中關於 .NET 11 行為的描述,是根據 Microsoft 公開的重大變更文件(處於預覽階段的資訊)所整理的預定內容,正式發行(GA)後請務必查閱最新文件確認。6

也就是說,假設「用 10 秒間隔的 System.Threading.Timer 進行量測的應用程式睡眠了 8 小時」,8 小時份的觸發無論如何都不會一次全部補上,但復原後究竟是立即觸發一次,還是等待剩餘時間後才觸發,取決於 API 的層級與執行環境(runtime)的版本。不論哪一種,睡眠期間的樣本都會缺失。與其依賴「復原後立即觸發」來設計重建處理,不如在第 5 章的復原事件中明確地重新排程,這樣比較安全。

3.2. 經過時間的量測會產生偏差

經過時間 API 當中,同時混雜著包含睡眠時間與不包含睡眠時間的兩種。

API 睡眠時間
GetTickCount / GetTickCount64 包含7
QueryPerformanceCounter(= .NET Stopwatch 的底層) 包含(standby、hibernate、connected standby)8
QueryUnbiasedInterruptTime 不包含(僅計入 working state 的時間)257
Environment.TickCount / TickCount64 .NET 10 為止包含(以 GetTickCount64 為基礎),從 .NET 11 起改為不包含(以 QueryUnbiasedInterruptTime 為基礎)6

「用 Stopwatch 判斷經過 10 秒就取下一筆樣本」這樣的程式碼,一旦跨過睡眠,就會變成「經過了 8 小時又 10 秒」;反過來說,以 TickCount 為基礎的經過時間判斷,在 .NET 11 之後行為也會改變。把「處理花費的時間」交給 Stopwatch,「下一次應該執行的時鐘時刻」用 DateTime / DateTimeOffset 管理,並在復原事件(第 5 章)時重新取得基準,做好這樣的職責分工,就不會被睡眠或執行環境更新牽著走。短週期計時器本身的設計,在「Windows 上為什麼應先用事件等待而不是計時器等待 - 避免以約 15.6ms 粒度做輪詢」中有討論。

3.3. TCP 連線會「悄悄地」失效

睡眠期間應用程式無法通訊,連線因此變成閒置狀態。問題出在路徑上的裝置。NAT、防火牆、負載平衡器等中間裝置會以逾時機制丟棄閒置的流量,但在許多組態下,丟棄時並不會通知連線兩端,而是悄悄地丟棄。舉例來說,Azure Load Balancer 的預設行為是「達到閒置逾時(預設 4 分鐘)的流量會被靜默丟棄」。9 公司內部的路由器,以及裝置端內建的 TCP 堆疊,也都有類似的逾時機制。

結果就是,復原後應用程式的 socket 表面上看起來仍然正常,卻會在下一次傳送時發生錯誤,或是在等待回應時卡住直到逾時為止。DAM 的文件也明確指出,需要考慮行程暫停對連線存續時間與交握所造成的影響。4 此外,Modern Standby 機種在使用電池供電時,預設會讓睡眠期間的網路活動整個靜止下來(Adaptive Connected Standby)。26 接收到復原事件後,應懷疑連線已經失效,將其捨棄並重新連線,這是標準做法。關於連線「看起來活著、實際上已經失效」這類問題的排查,也可以參考「TCP 重送讓工業相機通訊卡幾秒時 - RFC1323 timestamp 與重送等待的切分」。

4. 「不讓系統睡眠」── SetThreadExecutionState 與電源要求

只有量測進行中的這幾個小時不能讓系統睡眠時,正規做法就是 SetThreadExecutionState。從 C# 呼叫時要透過 P/Invoke。

using System.Runtime.InteropServices;

internal static class PowerGuard
{
    [Flags]
    private enum EXECUTION_STATE : uint
    {
        ES_CONTINUOUS       = 0x80000000,
        ES_SYSTEM_REQUIRED  = 0x00000001,
        ES_DISPLAY_REQUIRED = 0x00000002,
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);

    /// <summary>在量測開始時呼叫:抑止自動睡眠(必須從與 End 相同的執行緒呼叫)</summary>
    public static void Begin() =>
        SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS |
                                EXECUTION_STATE.ES_SYSTEM_REQUIRED);

    /// <summary>在量測結束時務必呼叫:解除抑止(必須從與 Begin 相同的執行緒呼叫)</summary>
    public static void End() =>
        SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
}

需要掌握的規格如下。

  • 加上 ES_CONTINUOUS 之後,效果會持續到下一次以 ES_CONTINUOUS 重新呼叫為止。若不加上這個旗標,只會重設一次閒置計時器,因此若要讓效果持續,就必須定期不斷呼叫。10
  • ES_SYSTEM_REQUIRED 用來抑止系統睡眠,ES_DISPLAY_REQUIRED 用來抑止顯示器關閉。若是背景量測,只用前者就足夠了。明明螢幕可以關閉,卻連 ES_DISPLAY_REQUIRED 都設定,是在浪費電力。10
  • 無法防止使用者按下電源按鈕、或闔上上蓋所造成的睡眠。這個函式只對因無操作逾時而觸發的自動睡眠有效。官方文件也明確指出,應該尊重使用者的明確操作。10
  • 系統會計算呼叫過 SetThreadExecutionState 的執行緒數量,一旦計數歸零且沒有使用者輸入,就會進入睡眠。11 行程異常結束的話,抑止效果也會一併消失,所以「抑止之後忘記解除、導致電腦永遠不睡眠」這種擔心,只要重新啟動就能解決,但反過來說,這也不能作為對抗當機的保障手段。
  • 正如其名稱所示,這個函式設定的是呼叫端執行緒的執行狀態10 設定與解除請在同一個執行緒中進行。由於 async/await 的接續(continuation)有可能在不同的執行緒集區執行緒上執行,若在夾著 await 的實作中呼叫 Begin()End(),解除動作就會在與設定睡眠抑止時不同的執行緒上落空,導致原本的執行緒存活期間,抑止效果會一直殘留下去。安全的做法是固定呼叫在身分保證一致的執行緒上(例如 UI 執行緒),若設計上需要跨執行緒呼叫,請改用稍後提到的、以控制代碼為基礎的電源要求 API。

自 Windows 7 起,還有另一組目的相同、較新的 API,也就是電源要求(PowerCreateRequest / PowerSetRequest / PowerClearRequest)。這組 API 在建立要求時,可以透過 REASON_CONTEXT 傳入理由字串,在實務上是一大優點;要求種類除了 PowerRequestSystemRequired 之外,還有能夠抑止 Modern Standby 機種上行程暫停的 PowerRequestExecutionRequired1413 官方的最佳實務是「在情境開始前立刻 Set,結束後立刻 Clear,行程結束前清理控制代碼」。13

不過 Modern Standby 機種有一項重要限制。在電池供電的 Modern Standby 系統中,SystemRequired/ExecutionRequired 要求會在超過睡眠逾時後的 5 分鐘被強制終止。此外不論電源方式為何,只要因使用者操作(電源按鈕、闔上上蓋、開始功能表的睡眠選項)而進入睡眠,要求就會結束。13 也就是說,「在筆記型電腦上,即使闔上上蓋、即使用電池供電也要持續運作」是無法只靠應用程式的努力來實現的。這類需求應該由電源設定與運維方式(設定闔蓋不睡眠、以交流電供電)來保障。

抑止是否真的有生效,可以在具有系統管理員權限的命令提示字元中確認。

> powercfg /requests
SYSTEM:
[PROCESS] \Device\HarddiskVolume3\Apps\SensorLogger.exe

powercfg /requests 是列出正在阻止睡眠、關閉螢幕的電源要求的指令,既可用來調查「不知為何不會睡眠的電腦」,也能用來確認自己應用程式的抑止是否生效。12 輸出的解讀方式如下。

  • 輸出依要求種類分成不同標題區塊。基本上是 DISPLAY(抑止關閉螢幕)、SYSTEM(抑止睡眠)、AWAYMODE,此外也會出現對應 PowerRequestExecutionRequiredEXECUTION 分類。若在上面範例中,自己的應用程式出現在 SYSTEM: 底下,就代表已經成功抑止了系統睡眠。1213
  • 標題底下每一行開頭的 [PROCESS] / [SERVICE] / [DRIVER],代表要求來源的種類。這個種類與名稱,可以直接當作傳給 powercfg /requestsoverride 的引數。12
  • 沒有對應要求的種類,會顯示「沒有要求」的說明行(英文環境下為 None.)。確認自己應用程式的抑止時,只需要看 SYSTEM 底下是否出現自己的行程即可,這樣就足夠了。
  • 如果使用 PowerCreateRequest 系列 API 附加理由字串,該字串就會顯示在這份清單中,運維方也能因此知道「這是為了哪項處理而抑止睡眠」。1413

反過來說,也請留意管理員可以透過 powercfg /requestsoverride,設定為忽略特定行程的要求。12 設計上的前提是,抑止 API 終究只是一種「請求」,而非絕對保證。

最後談談分寸的問題。若實作成應用程式啟動期間全程持續設定 ES_CONTINUOUS | ES_SYSTEM_REQUIRED,就等於常駐應用程式永久性地扼殺了使用者所設定的電源方案。若是筆記型電腦會耗損電池,若是共用電腦則會影響其他用途。原則上應該把抑止範圍限定在「實際正在執行、不能被睡眠打斷的處理區段」(官方文件的範例也是「開始錄影時設定、錄影完成時清除」10)。另外,使用者端也有一個暫時的迴避方案,就是 PowerToys Awake,它內部同樣是以相同機制(要求執行狀態的執行緒)運作。27 這也可以用來劃分「該由應用程式實作抑止,還是交給運維工具處理」的界線。

5. 「以睡眠為前提」── 偵測、重新連線、記錄缺測

對許多常駐監控或收集類應用程式來說,與其禁止睡眠,不如與睡眠共存,這樣的設計思路更為合理。需要做到的是「在進入睡眠前得知」「得知已經復原」「復原後重建狀態」這三點。

在 .NET 中,入口是 Microsoft.Win32.SystemEvents.PowerModeChanged15

using Microsoft.Win32;

SystemEvents.PowerModeChanged += OnPowerModeChanged;

private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
    switch (e.Mode)
    {
        case PowerModes.Suspend:
            // 緩衝時間很短:只做 flush 緩衝區、記錄量測停止時間這兩件事
            _logger.Info("suspend at {0:O}", DateTimeOffset.Now);
            _collector.Pause();
            break;

        case PowerModes.Resume:
            // 1) 把缺測區間作為資料留存
            _logger.Info("resume at {0:O}", DateTimeOffset.Now);
            // 2) 假設連線已經失效,予以捨棄並重新連線
            _connection.Reset();
            // 3) 以時鐘時刻為基準重新計算排程,重新設定計時器
            _scheduler.Rebase(DateTimeOffset.Now);
            // 4) 等重建完成後才明確恢復收集(不要一直停在 Pause 狀態)
            _collector.Resume();
            break;
    }
}

官方文件明確指出這個事件有兩個注意事項:若訊息幫浦(message pump)沒有在運轉就不會觸發(Windows 服務需要準備隱藏表單之類的東西),以及因為是 static 事件,若疏忽取消訂閱就會造成洩漏15 GUI 應用程式可以直接使用,但主控台/服務型的收集應用程式,則需要自行準備接收 Win32 WM_POWERBROADCAST 的訊息視窗,或是使用不需要 HWND 就能接收回呼的 PowerRegisterSuspendResumeNotification(DAM 環境下的通知也是走這條路徑)。416

5.1. 在沒有視窗的應用程式中接收復原通知

對於像收集工具或 Windows 服務這類沒有視窗的應用程式來說,與其自行製作訊息視窗,使用 PowerRegisterSuspendResumeNotification 所需的工夫更少。這個函式的用法是在 Flags 指定 DEVICE_NOTIFY_CALLBACKRecipient 則傳入指向 DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS(內含回呼函式與內容)的指標,成功時傳回 ERROR_SUCCESS(0)。Windows 8 / Windows Server 2012 以後可以使用。1928

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

// 在沒有視窗的主控台/服務中接收暫停・復原通知的最小範例
// 不需要訊息幫浦。Windows 8 / Windows Server 2012 以降可用
internal sealed class SuspendResumeNotifier : IDisposable
{
    private const uint DEVICE_NOTIFY_CALLBACK  = 2;      // powrprof.h
    private const uint PBT_APMSUSPEND          = 0x0004; // winuser.h
    private const uint PBT_APMRESUMEAUTOMATIC  = 0x0012; // winuser.h
    private const uint ERROR_SUCCESS           = 0;

    // ULONG DeviceNotifyCallbackRoutine(PVOID Context, ULONG Type, PVOID Setting)
    private delegate uint DeviceNotifyCallbackRoutine(IntPtr context, uint type, IntPtr setting);

    [StructLayout(LayoutKind.Sequential)]
    private struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
    {
        public IntPtr Callback;
        public IntPtr Context;
    }

    [DllImport("powrprof.dll")]
    private static extern uint PowerRegisterSuspendResumeNotification(
        uint flags,
        ref DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS recipient,
        out IntPtr registrationHandle);

    [DllImport("powrprof.dll")]
    private static extern uint PowerUnregisterSuspendResumeNotification(IntPtr registrationHandle);

    // 委派(delegate)要保存在欄位中(若只放在區域變數,會被 GC 回收,通知時就會當機)
    private readonly DeviceNotifyCallbackRoutine _callback;
    private IntPtr _registration;

    // 解除失敗的註冊會殘留在 OS 端。只有這個委派需要保留到行程結束為止
    // 的存放處(參見下方的 Dispose)
    private static readonly List<DeviceNotifyCallbackRoutine> AbandonedCallbacks = new();

    public SuspendResumeNotifier()
    {
        _callback = OnPowerNotification;

        var parameters = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
        {
            Callback = Marshal.GetFunctionPointerForDelegate(_callback),
            Context  = IntPtr.Zero,
        };

        uint result = PowerRegisterSuspendResumeNotification(
            DEVICE_NOTIFY_CALLBACK, ref parameters, out _registration);

        if (result != ERROR_SUCCESS)
        {
            throw new InvalidOperationException(
                $"PowerRegisterSuspendResumeNotification failed with {result}.");
        }
    }

    private uint OnPowerNotification(IntPtr context, uint type, IntPtr setting)
    {
        // 這個方法是由 OS(原生端)直接呼叫的。若讓 managed 例外
        // 從這裡拋到外面,因為 managed 端沒有承接的呼叫端,
        // 就會連整個行程一起崩潰。像是日誌寫入目的地已滿、collector
        // 已經被釋放這類「有可能發生」的失敗,會導致應用程式每次睡眠
        // 都當機,所以要在內部完整處理,並且務必回傳代碼
        try
        {
            switch (type)
            {
                case PBT_APMSUSPEND:
                    // 緩衝時間大約 2 秒。只做緩衝區的 flush 與停止時間的記錄
                    _logger.Info("suspend at {0:O}", DateTimeOffset.Now);
                    _collector.Pause();
                    break;

                case PBT_APMRESUMEAUTOMATIC:
                    // 即使是無人值守的復原也一定會收到。較重的重建處理要移到回呼之外執行
                    _logger.Info("resume at {0:O}", DateTimeOffset.Now);
                    _recovery.RequestRebuild();
                    break;
            }
        }
        catch (Exception ex)
        {
            // 有可能連日誌輸出本身都失敗,這裡也要做防護
            try { _logger.Error("power notification failed: {0}", ex); }
            catch { /* 如果在這裡重新拋出,上面的 try 就失去意義了 */ }
        }

        return ERROR_SUCCESS; // 回傳 Windows 的錯誤代碼
    }

    public void Dispose()
    {
        if (_registration == IntPtr.Zero)
        {
            return;
        }

        uint result = PowerUnregisterSuspendResumeNotification(_registration);
        if (result != ERROR_SUCCESS)
        {
            // 若在尚未成功解除的情況下捨棄控制代碼,登記會繼續留在 OS 端,
            // 卻失去了追蹤手段。若此時這個實例被回收,_callback 也會
            // 隨之消失,OS 就會呼叫已經釋放的函式指標。下一次暫停時
            // 就會連整個行程一起崩潰,所以只把委派保留下來
            lock (AbandonedCallbacks)
            {
                AbandonedCallbacks.Add(_callback);
            }

            _logger.Error("PowerUnregisterSuspendResumeNotification failed with {0}.", result);
            return;   // 既然尚未成功解除,就不捨棄控制代碼
        }

        _registration = IntPtr.Zero;
    }
}

實作上要注意的重點有三個。

  • 不要讓例外拋出回呼函式之外。這個方法是由 OS 直接呼叫的,一旦 managed 例外跨越邊界,因為 managed 端沒有承接的呼叫端,會連整個行程一起崩潰。像是日誌輸出失敗、存取已經釋放的物件,這類實際可能發生的失敗,會導致應用程式每次睡眠都當機。請用 try / catch 把整個內容包起來,並且務必回傳代碼。
  • 回呼是由系統端的執行緒呼叫的。PBT_APMSUSPEND 的緩衝時間約 2 秒,這一點和透過視窗接收時相同,因此不能在這裡執行重新連線或跨網路的善後處理。1718 復原處理也一樣,設定旗標交給另一個執行緒處理會比較安全。
  • 不要忘記取消註冊。要把註冊控制代碼傳給 PowerUnregisterSuspendResumeNotification29 也請檢查它的傳回值。若在尚未成功解除的情況下捨棄註冊控制代碼,OS 端會殘留註冊,卻失去追蹤手段。若在這種狀態下實例被回收,回呼的委派也會隨之消失,OS 就會呼叫已經釋放的函式指標。
  • 若想改用視窗控制代碼來接收,也可以把 DEVICE_NOTIFY_WINDOW_HANDLE 傳給 user32.dll 的 RegisterSuspendResumeNotification(這種方式會照常收到 WM_POWERBROADCAST)。30

若要做成 Windows 服務,也可以把 ServiceBaseCanHandlePowerEvent 設為 true,並覆寫 OnPowerEvent(PowerBroadcastStatus)。因為相同的電源事件會透過服務控制管理員傳遞過來,所以可以不用 P/Invoke 就寫出來。31

5.2. Win32 層級事件的意義

不論是透過視窗,還是透過回呼接收,事件的意義都是共通的。這裡要先掌握清楚。

  • PBT_APMSUSPEND:睡眠前一刻的通知。每個應用程式大約只有 2 秒的緩衝時間,一旦超過就可能被系統中斷。這裡只做 flush 與時間記錄這類最小限度的事,不可以撰寫像跨網路善後這種耗時的處理。1718
  • PBT_APMRESUMEAUTOMATIC:每次復原都一定會收到的通知。重新連線、重新排程都要寫在這裡。32
  • PBT_APMRESUMESUSPEND:因使用者操作(或使用者回來)而復原時,會在 PBT_APMRESUMEAUTOMATIC 之後收到。由於遠端喚醒等自動復原不會收到這個通知,若只把「復原處理」寫在這裡,無人值守復原時就不會執行重建33
  • 此外,在電池電量緊迫等臨界情況下進入睡眠時,連事前通知本身都不會送達18 即使沒有收到 Suspend 事件,也要讓復原處理仍然成立,因此 Resume 端要寫成冪等(idempotent)的。

5.3. 把缺測記錄為缺測

另一件和復原對應同樣重要的事,是把缺測記錄為缺測。跨越睡眠期間的收集資料並非「沒有值」,而是「因為系統當時停止運作,所以沒有量測」,只要把該區間連同 suspend/resume 的時間一起留在日誌與資料兩邊,之後看到圖表出現空白的人就不會把它誤認為是故障。關於長時間運轉的應用程式,日誌應該留下哪些內容,「工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計」也有討論。

另外,與睡眠類似、屬於「不知不覺間變慢了」這類問題的,還有 Windows 11 效率模式(EcoQoS)造成的抑制。這部分請參考「Windows 的效率模式是什麼 - Windows 11 的綠色葉子圖示代表什麼,以及如何關閉」。

6. 在固定時刻確實執行 ── 工作排程器的喚醒執行

像「凌晨兩點彙總並傳送」這種以時間點為起點的處理,若用常駐應用程式的計時器加上睡眠抑止來實現,並不是好做法,因為這樣會整晚扼殺系統睡眠。這種用途的正解是工作排程器的「執行工作時喚醒電腦」(WakeToRun)

啟用 WakeToRun 的工作,會在執行時刻把電腦從睡眠或休眠中喚醒,並讓系統一直保持喚醒直到工作完成為止(若電腦原本就已經是喚醒狀態,也會要求維持喚醒直到工作完成)。喚醒時螢幕有可能保持關閉,這是正常現象。20

不過,要成功喚醒是有條件的。正如官方疑難排解文件所列,前提是電源選項中的「允許喚醒計時器」(Allow Wake Timer)必須啟用,且 BIOS 端的喚醒設定也必須啟用,而近年來筆記型電腦基於省電設計,不允許喚醒的組態並不罕見。21powercfg /waketimers 可以列出目前有效的喚醒計時器12,因此務必在導入現場的實機上驗證「是否真的會喚醒」。若想用自己的應用程式來喚醒,也可以使用把 SetWaitableTimerfResume 設為 TRUE 的可喚醒計時器這種手段。這種情況下,系統在自動喚醒後,只會在無人閒置計時器(最短 2 分鐘)期間保持喚醒,若應用程式沒有透過 SetThreadExecutionState 宣告「使用中」,就會很快再次進入睡眠。若喚醒後的處理時間較長,正確的搭配方式是與第 4 章的抑止機制組合使用。23

關於工作本身的執行帳戶、以 0x1 結束的問題、防止多重啟動等運維設計,整理在「工作排程器的工作不執行、以 0x1 結束 ── 原因排查與安全的維運設計」。

7. 判斷表 ── 抑止、復原對應,還是喚醒

這三種設計並非互斥,而是要依應用程式的種類決定主從關係並加以組合。

應用程式種類 第一候選 組合・補充說明
量測・資料收集(數小時~數天的連續量測) 只在量測區段用 ES_SYSTEM_REQUIRED 抑止 也務必實作復原對應(無法防止使用者操作造成的睡眠10)。電池供電的 Modern Standby 機種有 5 分鐘強制終止的限制,因此要把交流電供電列為前提條件13
夜間批次・定時傳輸 工作排程器 + WakeToRun20 比常駐 + 抑止更省電、更可靠。前提是要確認允許喚醒計時器已啟用21
常駐監控・通知代理程式 以睡眠為前提(用 PowerModeChanged 偵測 → 重新連線・重新排程・記錄缺測)15 若是以監控為主要目的的專用機,應該在電源方案端停用睡眠,而不是靠應用程式
桌面操作工具(簡報、儀表板顯示等) 只在顯示期間把 ES_DISPLAY_REQUIREDES_SYSTEM_REQUIRED 併用10 顯示結束務必清除。若是常時顯示的終端機,改用電源設定處理
24/7 的設備控制・產線電腦 用電源設定停用睡眠本身(以運維方式保障) 不要把應用程式的抑止 API 當成保險。可以用 powercfg /requests 做定期檢查12

判斷的軸線有兩個:「是否有可以停止運作的時段」(有的話用工作排程器,沒有的話用電源設定),以及「在誰的電腦上執行」(越是在使用者私人裝置、共用筆電上執行的應用程式,越應該傾向復原對應,而非抑止)。

7.1. 「用電源設定停用睡眠本身」的具體內容

表格中多次出現的「用電源設定停用」,具體來說指的是以下操作。像設備控制電腦這種不靠應用程式、而靠運維方式來阻止睡眠的情況,必須做到這個程度才算真正得到保障。

要做的事 位置・指令
把進入睡眠的時間設為「無」 設定 > 系統 > 電源(筆記型電腦則是「電源與電池」) > 螢幕與睡眠。或是 控制台 > 硬體和音效 > 電源選項 > 變更計畫設定。用指令則是 powercfg /change standby-timeout-ac 0(0 代表不進入睡眠)1234
也一併檢視關閉螢幕的時間 同一畫面中的「關閉螢幕」。用指令則是 powercfg /change monitor-timeout-ac 012
變更闔蓋・電源按鈕的動作 電源選項 > 選擇按下電源按鈕時的行為 > 選擇「不執行任何動作」。這條路徑是 SetThreadExecutionState 無法防止的10
確認機種的方式 powercfg /a。是 S3 機種還是 Modern Standby 機種,後續要注意的重點會不同3

Modern Standby 機種還需要多一層注意。支援 Modern Standby 的機種,會在螢幕關閉時就進入待命。官方文件列出的觸發時機,除了電源按鈕、闔上上蓋、開始功能表選擇睡眠之外,還包括「系統閒置逾時」。35 也就是說,若還停留在 S3 機種的感覺,以為「只要關掉睡眠的逾時就沒問題」,就會留下一條從螢幕關閉進入待命的路徑。請重新檢視睡眠與螢幕關閉兩邊的逾時設定,用 powercfg /a 確認機種方式,並且務必把「在實機上放置一整晚,確認不會停止運作」納入操作流程之中

電池供電時的處理方式也很容易被忽略。standby-timeout-ac 是交流電供電時的設定,電池供電時使用的是 standby-timeout-dc。若設備用電腦是筆記型電腦,安全的做法是明確把交流電供電列為前提條件(如第 4 章所述,電池供電的 Modern Standby 機種,電源要求本身會在 5 分鐘後被強制終止)。13

8. 總結

  • 睡眠有 S3、休眠(S4)、Modern Standby(S0 low power idle)這幾種,可以用 powercfg /a 判別。即使是 Modern Standby 機種,桌面應用程式也會被 DAM 暫停,因此「睡眠期間仍在運作」的前提並不成立。
  • 睡眠期間執行緒與計時器都不會前進。Windows 8 以後,相對計時器與等待不會計入睡眠時間,而是延續剩餘時間;.NET 的計時器也將從 .NET 11 起變成相同行為(.NET 10 為止有可能在復原後立即觸發)。經過時間 API 混雜著計入睡眠時間與不計入睡眠時間兩種。經過時間請用 Stopwatch 管理,時鐘時刻請用 DateTime 管理,並在復原時重新取得基準。
  • TCP 連線會因中間裝置的閒置逾時而悄悄失效。標準做法是在復原事件中將其捨棄並重新連線。
  • 抑止睡眠請只在需要的區段使用 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)。它無法防止使用者操作造成的睡眠,且在 Modern Standby 機種以電池供電時會在 5 分鐘後被強制終止。可用 powercfg /requests 確認。
  • 復原對應請用 SystemEvents.PowerModeChanged / WM_POWERBROADCAST。暫停通知的緩衝時間約 2 秒,也存在沒有通知就直接睡眠的情況,因此 Resume 端要寫成冪等,並記錄缺測區間。
  • 定時執行的正解是工作排程器的 WakeToRun。設計時請連同喚醒計時器的電源設定、在實機上確認確實喚醒都一併納入考量。

相關文章

相關諮詢領域

合同會社小村軟體承接量測、監控、資料收集等長時間執行的 Windows 應用程式的設計與實作,也承接「半夜停止運轉」「記錄出現缺漏」等長期執行特有問題的調查。若遇到與睡眠、電源管理相關、難以重現的症狀排查,也歡迎諮詢。

參考連結

  1. Microsoft Learn, System Sleeping States。關於 S1~S4 屬於睡眠狀態且不執行任何計算工作、S3 只保留記憶體、S4 則寫入休眠檔,以及 powercfg /a 可以列出可用睡眠狀態的說明。  2 3 4 5

  2. Microsoft Learn, System power states。關於 S0 low power idle(Modern Standby)會讓系統以低電力局部持續運作、支援 Modern Standby 的 SoC 系統不使用 S1~S3,以及臨界狀態轉換不會發出通知的說明。  2 3

  3. Microsoft Learn, Overview of Modern Standby Testing and Diagnostics。關於可用 powercfg /a(顯示 Standby (S0 Low Power Idle))判別是否支援 Modern Standby 的說明。  2 3

  4. Microsoft Learn, Desktop Activity Moderator。關於 DAM 會把桌面應用程式的執行壓制到與 S3 相同程度、互動工作階段的行程會所有執行緒都被暫停・工作階段 0 則被節流、暫停前會有 WM_POWERBROADCAST 通知,以及需要考慮計時器行為、運作時間與時鐘時刻的不一致、連線存續時間的說明。  2 3 4 5 6 7 8

  5. Microsoft Learn, SetWaitableTimerEx function。關於相對時間指定的計時器,在 Windows 7 以前會把低電力狀態的時間也算進去(睡眠期間倒數也會前進),而 Windows 8 以後則不包含(睡眠期間倒數不會前進)的說明。  2 3

  6. Microsoft Learn, Environment.TickCount made consistent with Windows timeout behavior。關於 Environment.TickCount / TickCount64 在 .NET 10 為止以 GetTickCount64 為基礎(包含睡眠時間),從 .NET 11 起改為以 QueryUnbiasedInterruptTime 為基礎(不包含)這項重大變更、Windows 8 以後指定逾時的等待 API(SleepEx / WaitForMultipleObjectsEx)已經不再計入非運作時間,以及這項變更可能導致復原後計時器不再立即觸發的程式碼的說明。  2 3 4 5 6 7 8 9

  7. Microsoft Learn, Windows Time。關於 GetTickCount / GetTickCount64 的經過時間包含睡眠、休眠時間,QueryUnbiasedInterruptTime 只包含 working state 時間的說明。  2 3

  8. Microsoft Learn, Acquiring high-resolution time stamps。關於受控程式碼中的 System.Diagnostics.Stopwatch 以 QPC 為時間基礎、QueryPerformanceCounter 傳回的刻度數會包含 standby、hibernate、connected standby 期間的說明。  2

  9. Microsoft Learn, Load Balancer TCP Reset and Idle Timeout。關於負載平衡器的預設行為是達到閒置逾時(預設 4 分鐘)時靜默丟棄流量、傳送 TCP 重設需要另外明確啟用,以及對策上會使用 TCP keep-alive 的說明。  2

  10. Microsoft Learn, SetThreadExecutionState function。關於 ES_CONTINUOUS / ES_SYSTEM_REQUIRED / ES_DISPLAY_REQUIRED 的意義、不加 ES_CONTINUOUS 只會重設閒置計時器、無法防止使用者的睡眠操作,以及只在必要處理期間設定、完成後清除的使用範例說明。  2 3 4 5 6 7 8 9

  11. Microsoft Learn, System Sleep Criteria。關於系統會計算呼叫過 SetThreadExecutionState 的應用程式/執行緒數量,計數歸零且沒有使用者輸入時就會進入睡眠的說明。  2

  12. Microsoft Learn, Powercfg command-line options。關於 /requests 會列出阻礙睡眠或關閉螢幕的電源要求、/requestsoverride 可以忽略特定行程的要求、/waketimers 可以列出有效喚醒計時器的說明。  2 3 4 5 6 7 8 9

  13. Microsoft Learn, PowerSetRequest function。關於 PowerRequestSystemRequired / PowerRequestExecutionRequired 等要求種類、電池供電的 Modern Standby 系統中要求會在超過睡眠逾時後的 5 分鐘被強制終止、因使用者操作進入睡眠時要求會結束,以及附上理由字串、在情境開始前 Set・結束後立刻 Clear 的最佳實務說明。  2 3 4 5 6 7 8

  14. Microsoft Learn, PowerCreateRequest function。關於指定 REASON_CONTEXT 建立電源要求物件、不再需要時以 CloseHandle 釋放的說明。  2 3

  15. Microsoft Learn, SystemEvents.PowerModeChanged Event。關於這是在暫停/恢復時觸發的事件、若訊息幫浦沒有運轉就不會觸發(服務需要準備隱藏表單等)、因為是 static 事件所以不取消訂閱就會造成洩漏的說明。  2 3 4

  16. Microsoft Learn, WM_POWERBROADCAST message。關於電源管理事件會以 WM_POWERBROADCAST 訊息(PBT_APMSUSPEND / PBT_APMRESUMEAUTOMATIC / PBT_APMRESUMESUSPEND 等)通知視窗的說明。  2

  17. Microsoft Learn, PBT_APMSUSPEND event。關於這是在暫停前一刻收到的通知、處理的緩衝時間約為 2 秒、超過時可能被系統中斷的說明。  2 3

  18. Microsoft Learn, System Power Management Events。關於因電池電量臨界等原因觸發的緊急暫停不會有事前通知、暫停通知的處理每個應用程式最長逾時 2 秒的說明。  2 3 4

  19. Microsoft Learn, PowerRegisterSuspendResumeNotification function。關於在 Flags 指定 DEVICE_NOTIFY_CALLBACK、Recipient 傳入指向 DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS 的指標、回呼的 Type 會帶入 PBT_APMSUSPEND / PBT_APMRESUMESUSPEND / PBT_APMRESUMEAUTOMATIC、成功時傳回 ERROR_SUCCESS,以及 Windows 8 / Windows Server 2012 以後包含在 Powrprof.dll 中的說明。  2

  20. Microsoft Learn, ITaskSettings::get_WakeToRun method。關於 WakeToRun 會在工作執行時喚醒電腦、並讓系統保持喚醒直到工作完成,以及喚醒時螢幕可能保持關閉的說明。  2 3

  21. Microsoft Learn, Automatic maintenance。關於定時喚醒無法運作時的確認項目,包括 BIOS 的喚醒設定、電源選項的「Allow Wake Timer」、工作的 WakeToRun 設定,以及近年筆記型電腦普遍不允許 S3 喚醒的說明。  2 3

  22. Microsoft Learn, What is Modern Standby。關於 Modern Standby 是 S0 低電力閒置模型、在維持網路連線的情況下實現瞬時開關,以及復原(從按下電源按鈕到螢幕點亮)不到 1 秒的說明。 

  23. Microsoft Learn, System Wake-up Events。關於將 SetWaitableTimer 的 fResume 設為 TRUE 可用計時器喚醒系統、自動喚醒後會設定最短 2 分鐘的無人閒置計時器,若沒有透過 SetThreadExecutionState 表示使用中就會再次進入睡眠的說明。  2 3

  24. Microsoft, .NET and .NET Core Support Policy。關於 .NET 主要版本每年 11 月發行、.NET 10 是於 2025 年 11 月 11 日發行的 LTS 版本的說明。 

  25. Microsoft Learn, QueryUnbiasedInterruptTime function。關於 unbiased interrupt time 只計入 working state 的時間、不包含睡眠、休眠時間的說明。 

  26. Microsoft Learn, Modern standby network connectivity。關於透過 Adaptive Connected Standby,在電池供電時,除非有需要的情境,否則睡眠期間的網路活動會被靜止的說明。 

  27. Microsoft Learn, PowerToys Awake utility。關於這是不需要變更電源方案就能讓電腦保持喚醒的公用程式,其運作方式是產生要求機器狀態的背景執行緒,結束後就會恢復一般電源方案行為的說明。 

  28. Microsoft Learn, DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS structure。關於此結構具有 Callback 與 Context 兩個成員、回呼函式 DeviceNotifyCallbackRoutine 的形式為 ULONG(PVOID Context, ULONG Type, PVOID Setting) 並回傳 Windows 錯誤代碼的說明。 

  29. Microsoft Learn, PowerUnregisterSuspendResumeNotification function。關於傳入註冊控制代碼以解除通知註冊、成功時傳回 ERROR_SUCCESS 的說明。 

  30. Microsoft Learn, RegisterSuspendResumeNotification function。關於在 Flags 指定 DEVICE_NOTIFY_WINDOW_HANDLE 會將事件傳遞給視窗、指定 DEVICE_NOTIFY_CALLBACK 則可透過回呼接收的說明。 

  31. Microsoft Learn, ServiceBase.OnPowerEvent(PowerBroadcastStatus) Method。關於 Windows 服務用來接收電腦電源狀態變化的方法,以及必須在 CanHandlePowerEvent 屬性為 true 時才需要覆寫的前提說明。 

  32. Microsoft Learn, PBT_APMRESUMEAUTOMATIC event。關於這是每次復原都一定會送達的事件、並不代表使用者存在,若偵測到使用者活動,之後會再送達 PBT_APMRESUMESUSPEND 的說明。 

  33. Microsoft Learn, PBT_APMRESUMESUSPEND event。關於因使用者操作、偵測到使用者而復原時,會在 PBT_APMRESUMEAUTOMATIC 之後送達,而遠端喚醒時只會送達 PBT_APMRESUMEAUTOMATIC 的說明。 

  34. Microsoft Learn, Sleep idle timeout。關於這是指定自動進入睡眠前的無操作時間的設定、最小值 0 代表「不進入睡眠」的說明。 

  35. Microsoft Learn, Prepare software for modern standby。關於進入 Modern Standby 的時機是螢幕關閉時,觸發原因包括電源按鈕、闔上上蓋、從設定選擇睡眠、系統閒置逾時,DAM 階段之後桌面應用程式的執行會被停止、工作階段 0 的服務會被節流,以及電源要求在交流電供電下可無限期、在電池供電下最長可延長此階段 5 分鐘的說明。 

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

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

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

常見問題

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

只想在應用程式執行期間讓電腦不進入睡眠,該怎麼做?
基本做法是在處理開始時呼叫 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED),結束時再用 SetThreadExecutionState(ES_CONTINUOUS) 清除。只有在也想讓螢幕保持點亮時,才追加 ES_DISPLAY_REQUIRED。它只對因無操作逾時而觸發的自動睡眠有效,無法防止使用者按下電源按鈕或闔上上蓋所指示的睡眠。可以用 powercfg /requests 確認抑止是否生效。
什麼是 Modern Standby?和傳統的睡眠有什麼不同?
這是又稱為 S0 low power idle 的新式睡眠方式,特徵是系統雖然以低電力運作,卻仍會局部持續動作,並可瞬間復原。支援 Modern Standby 的機種不支援傳統的 S3 睡眠。不過「持續動作」的僅限於 OS 允許的活動,桌面應用程式會被 DAM(Desktop Activity Moderator)連同執行緒一起暫停,因此從應用程式的角度來看,停止方式和 S3 相同。自己的電腦屬於哪一種方式,可以用 powercfg /a 確認。
為什麼睡眠復原後 TCP 連線就不能用了?
因為睡眠期間應用程式無法通訊,連線會變成閒置狀態,路徑上的 NAT、防火牆、負載平衡器等中間裝置會因閒置逾時而丟棄該流量。許多裝置在丟棄時並不會發出通知,而是悄悄地丟掉,所以應用程式端的 socket 表面上看起來仍然正常,要到復原後第一次收發時才會出現錯誤,或是卡住直到逾時為止。標準做法是收到復原事件後,就懷疑連線已經失效並重新建立。
要確實執行夜間批次,該用睡眠抑止還是工作排程器比較好?
工作排程器的「執行工作時喚醒電腦」(WakeToRun)是第一選擇。它會在執行時刻喚醒電腦,並保持喚醒直到工作完成,因此不需要整晚扼殺睡眠功能。不過如果電源選項中的喚醒計時器被停用,就不會喚醒,因此「允許喚醒計時器」的設定與在實機上確認確實喚醒是必要的步驟。若全程持續抑止睡眠,不僅會浪費這段期間的電力,也會變成覆寫使用者電源設定的失禮設計。

作者檔案

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

Go Komura

小村軟體有限公司 代表

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

回到部落格一覽