.NET 阻止Windows關(guān)機以及阻止失敗的一些原因
當(dāng)前位置:點晴教程→知識管理交流
→『 技術(shù)文檔交流 』
本文主要介紹Windows在關(guān)閉時,如何正確、可靠的阻止系統(tǒng)關(guān)機以及關(guān)機前執(zhí)行相應(yīng)業(yè)務(wù) Windows關(guān)機,默認會給應(yīng)用幾s的關(guān)閉時間,但有一些場景需要在關(guān)機/重啟前執(zhí)行更長時間的業(yè)務(wù)邏輯,確保下次開機時數(shù)據(jù)的一致性以及可靠性。我司目前業(yè)務(wù)也用到關(guān)機阻止,但這塊之前并未梳理清楚,依賴BUG編程,導(dǎo)致后續(xù)維護項目時又會導(dǎo)致關(guān)機這塊出現(xiàn)新問題。 統(tǒng)一整理,以下是實現(xiàn)這一需求的幾種方法, 1. Windows消息Hook勾子1 public MainWindow() 2 { 3 InitializeComponent(); 4 Loaded += OnLoaded; 5 } 6 7 private void OnLoaded(object sender, RoutedEventArgs e) 8 { 9 Loaded -= OnLoaded; 10 var source = PresentationSource.FromVisual(this) as HwndSource; 11 source?.AddHook(WndProc); 12 } 13 const int WM_QUERYENDSESSION = 0x11; 14 const int WM_ENDSESSION = 0x16; 15 private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) 16 { 17 if (msg == WM_QUERYENDSESSION) 18 { 19 var handle = new WindowInteropHelper(this).Handle; 20 ShutdownBlockReasonCreate(handle, "應(yīng)用保存數(shù)據(jù)中,請等待..."); 21 // 可以在這里執(zhí)行你的業(yè)務(wù)邏輯 22 bool executeSuccess = ExecuteShutdownWork(); 23 // 返回0表示阻止關(guān)機,1表示允許關(guān)機 24 handled = true; 25 return executeSuccess ? (IntPtr)1 : (IntPtr)0; 26 } 27 return (IntPtr)1; 28 } 29 30 private bool ExecuteShutdownWork() 31 { 32 Thread.Sleep(TimeSpan.FromSeconds(20)); 33 //測試,默認返回操作失敗 34 return false; 35 } 36 37 [DllImport("user32.dll")] 38 private static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string reason); 39 [DllImport("user32.dll")] 40 private static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd); 通過Hook循環(huán)windows窗口消息,WndProc接收到WM_QUERYENDSESSION時表示有關(guān)機調(diào)用,詳細的可以查看官網(wǎng)文檔:(WinUser.h) WM_QUERYENDSESSION消息 - Win32 apps | Microsoft Learn WndProc返回1表示業(yè)務(wù)正常,0表示取消、阻止關(guān)機。這里我們默認操作失敗,阻止關(guān)機 拿到每個應(yīng)用的關(guān)機確認結(jié)果,再廣播WM_ENDSESSION、執(zhí)行真正的關(guān)閉 拿到窗口句柄,可以通過ShutdownBlockReasonCreate設(shè)置阻止關(guān)機原因,ShutdownBlockReasonDestroy清理關(guān)機阻止原因,詳見:ShutdownBlockReasonCreate 函數(shù) (winuser.h) - Win32 apps | Microsoft Learn 阻止進行中的效果: 上面demo運行20s之后,系統(tǒng)會退出關(guān)機狀態(tài)、返回登錄界面 2.Win32系統(tǒng)事件SystemEvents1 public partial class App : Application 2 { 3 public App() 4 { 5 SystemEvents.SessionEnding += SystemEvents_SessionEnding; 6 Application.Current.Exit += Current_Exit; 7 } 8 private void Current_Exit(object sender, ExitEventArgs e) 9 { 10 SystemEvents.SessionEnding -= SystemEvents_SessionEnding; 11 } 12 private void SystemEvents_SessionEnding(object sender, SessionEndingEventArgs e) 13 { 14 if (e.Reason == SessionEndReasons.SystemShutdown) 15 { 16 var handle = new WindowInteropHelper(Application.Current.MainWindow).Handle; 17 ShutdownBlockReasonDestroy(handle); 18 ShutdownBlockReasonCreate(handle, "應(yīng)用保存數(shù)據(jù)中,請等待..."); 19 20 var executeSuccess = ExecuteShutdownWork(); 21 e.Cancel = !executeSuccess; 22 } 23 } 24 private bool ExecuteShutdownWork() 25 { 26 //Test 27 Thread.Sleep(TimeSpan.FromSeconds(200)); 28 return false; 29 try 30 { 31 // XXX 32 return true; 33 } 34 catch (Exception e) 35 { 36 return false; 37 } 38 } 39 40 [DllImport("user32.dll")] 41 private static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string reason); 42 [DllImport("user32.dll")] 43 private static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd); 44 } 也可以監(jiān)聽SessionEndReasons.SystemShutdown關(guān)機事件。實際上也是基于消息機制,但封裝了細節(jié)、提供更高級抽象 這里e.Cancel,false表示不取消用戶請求、不關(guān)機,true表示取消用戶請求、阻止關(guān)機 因為需要設(shè)置關(guān)機阻止原因,SystemEvents.SessionEnding也是要依賴窗口的。當(dāng)然,因為依賴窗口會導(dǎo)致勾子失敗,下面我們會聊 阻止關(guān)機失敗的一些原因以上倆種方式,均可以實現(xiàn)阻止系統(tǒng)關(guān)機以及關(guān)機前執(zhí)行相應(yīng)業(yè)務(wù)。但Hook勾子也可能失效,不能正常執(zhí)行完你的業(yè)務(wù)邏輯 1. 關(guān)機勾子只支持UI線程,不支持異步調(diào)用 第一種,SessionEnding事件被修改為了async void 第二種,業(yè)務(wù)內(nèi)部調(diào)用了異步方法,通過.Result、.Wait()期望等待完成。但其實內(nèi)部并沒有完全添加.ConfigureAwait,這也會導(dǎo)致關(guān)機阻止失敗。 我司業(yè)務(wù)就遇到了第二個問題,在消息循環(huán)WndProc之后,添加了上報后臺日志。為了滿足勾子只支持同步調(diào)用,日志模塊就使用了.Result轉(zhuǎn)為同步方法: 對于這類限定UI線程同步執(zhí)行場景,我的解決辦法是,減少邏輯、去除發(fā)送后臺日志。 另外,如果下面業(yè)務(wù)真的需要使用async,需要業(yè)務(wù)上下游所有調(diào)用鏈條均添加.ConfigureAwait,不切換上下文。否則系統(tǒng)不會等待、往下直接關(guān)機了 2. 窗口Hide,導(dǎo)致勾子失效 一些窗口啟動后,需要立即Hide窗口: 1 public MainWindow() 2 { 3 InitializeComponent(); 4 //在構(gòu)造中設(shè)置Hide或者Show之后立即設(shè)置Hide,均會導(dǎo)致關(guān)機阻止失敗 5 Hide(); 6 } 在構(gòu)造中Hide或者Show之后立即Hide,均會導(dǎo)致關(guān)機阻止失敗。錯誤demo,可見 kybs0/ShutdownPreventDemo 我的理解是,ShutdownBlockReasonCreate 函數(shù)需要窗口處于活動狀態(tài),窗口Hide之后肯定是不行了。那如何解決呢? 在Loaded之后去設(shè)置窗口隱藏就行了: 1 public MainWindow() 2 { 3 InitializeComponent(); 4 Loaded += MainWindow_Loaded; 5 } 6 private void MainWindow_Loaded(object sender, RoutedEventArgs e) 7 { 8 Loaded -= MainWindow_Loaded; 9 //如果啟動后需要立即隱藏窗口,請放在Loaded之后 10 Hide(); 11 } 設(shè)置Visibility也沒問題 Visibility=Visibility.Collapsed; 驗證ok 第二,因為根源還是設(shè)置關(guān)機阻止Resion,那是否可以提前去設(shè)置呢?不要等窗口Hide之后再去設(shè)置或者關(guān)機時去設(shè)置... 所以,完全可以在主窗口內(nèi)提前設(shè)置: 1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 Loaded += MainWindow_Loaded; 7 } 8 private void MainWindow_Loaded(object sender, RoutedEventArgs e) 9 { 10 Loaded -= MainWindow_Loaded; 11 var currentMainWindow = Application.Current.MainWindow; 12 var handle = new WindowInteropHelper(currentMainWindow).Handle; 13 ShutdownBlockReasonDestroy(handle); 14 ShutdownBlockReasonCreate(handle, "應(yīng)用保存數(shù)據(jù)中,請等待..."); 15 16 //窗口Hide,并不影響上面的ShutdownBlockReasonDestroy 17 Hide(); 18 } 19 [DllImport("user32.dll")] 20 private static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string reason); 21 [DllImport("user32.dll")] 22 private static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd); 23 } 上面代碼也注釋了,設(shè)置完關(guān)機原因、再去Hide。關(guān)機事件觸發(fā)后,是能正常保障阻止機制的。驗證ok 這里也推薦大家使用SystemEvents.SessionEnding方式,可以不受MainWindow窗口的勾子入口限定 3.360安全衛(wèi)士、QQ電腦管家等優(yōu)化軟件,可能會優(yōu)化此類關(guān)機阻止機制 這些安全軟件關(guān)機時可能直接強殺,用來提升關(guān)機/重啟速度。個人是不建議使用這些安全軟件的,都是流氓。。。 關(guān)機阻止超時的情況及建議關(guān)機重啟是有時間限制的,我試了下,在設(shè)置關(guān)機阻止原因情況下,應(yīng)用最多只能持續(xù)60秒左右。 超過60s后系統(tǒng)取消關(guān)機、回登錄界面,然后當(dāng)前阻止的進程會在執(zhí)行完Hook后自動關(guān)閉(其它進程不會關(guān)閉) 如果Hook勾子內(nèi)我們執(zhí)行的業(yè)務(wù)太過耗時,可能不一定能執(zhí)行完。建議只執(zhí)行更少、必須的業(yè)務(wù) 另外,關(guān)機時應(yīng)用關(guān)閉是有順序的。如果想提高一點應(yīng)用關(guān)機時應(yīng)用能應(yīng)對的時間,略微提升關(guān)機前業(yè)務(wù)執(zhí)行的成功率,可以對進程添加關(guān)閉優(yōu)先級: 1 public MainWindow() 2 { 3 InitializeComponent(); 4 5 // 在應(yīng)用程序啟動時調(diào)用 6 SetProcessShutdownParameters(0x4FF, 0); 7 } 8 [DllImport("kernel32.dll")] 9 static extern bool SetProcessShutdownParameters(uint dwLevel, uint dwFlags); 0x100表示最低優(yōu)先級,確保你的程序最先被關(guān)閉 0x4FF表示最高優(yōu)先級,確保你的程序最后被關(guān)閉 詳細的參考文檔: SetProcessShutdownParameters 函數(shù) (processthreadsapi.h) - Win32 apps | Microsoft Learn
以上demo,可從倉庫獲取 ShutdownPreventDemo: 阻止關(guān)機demo 轉(zhuǎn)自https://www.cnblogs.com/kybs0/p/18822799? 該文章在 2025/4/14 8:32:26 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |