アプリから見たWindowsのシャットダウン ── 終了通知・再起動・電源断を正しく生き延びる

· · Windows, シャットダウン, Windows開発, Windowsサービス, 装置PC, データ保全, 長期稼働, UPS

「Windows Updateの夜間再起動のあと、装置PCの計測アプリが書き込み途中のデータごと落ちていて、朝には測定ファイルが壊れていた」「共有PCでサインアウトしたら、編集中の内容が保存されずに消えたとクレームが来た」── 長期稼働するWindowsアプリの相談で、この2つは定番です。

どちらの現場にも共通するのは、シャットダウンを「起きないはずの異常事態」として扱っていることです。しかし実際には、Windows Updateの自動再起動、ユーザーのサインアウト、UPSからのシャットダウン指示、そして予告なしの電源断まで、アプリの外から実行が打ち切られるイベントは、いつか必ず来ます。来ること自体は防げません。防げるのは「来たときにデータを失うこと」です。

幸い、Windowsはシャットダウンの前にアプリへ通知を送る仕組みを、GUIアプリ・コンソールアプリ・サービスのそれぞれに用意しています。この記事では、中小企業の情シス担当者と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_SHUTDOWN(猶予約20秒)より、SERVICE_ACCEPT_PRESHUTDOWNのほうが先に・構成可能な猶予で通知を受けられます。ただしPRESHUTDOWNの既定タイムアウトはWindows 10 Creators Update以降10秒に短縮されており、いずれにせよ猶予に頼りすぎない設計が必要です。89
  • 再起動後の自動復帰は、RegisterApplicationRestartとARSO(自動サインイン)の組み合わせで実現できます。クラッシュ・応答なし・更新による再起動のそれぞれで復帰経路が用意されています。1011
  • 電源断には通知が一切来ません。一時ファイルに書き切ってフラッシュしReplaceFileで差し替えるのが定石ですが、ReplaceFileも電源断をまたいだ原子性までは保証しないため、バックアップ(.bak)+起動時検証の復旧経路までがセットです。事後の切り分けはイベントログ(1074/41/6008)で行えます。121314

一言でまとめるなら、「通知が来たら数秒で店じまいできる状態を常に保ち、通知が来ない電源断でも壊れない書き方をしておく」、これがこの記事の結論です。

2. シャットダウンで何が起きるか ── 4つの「終わり方」

2.1. サインアウト・シャットダウン・再起動・電源断

アプリの視点で重要なのは、「ユーザーセッションがどう終わるか」と「カーネルがどうなるか」の2軸です。

操作 ユーザーセッション カーネル・ドライバー アプリへの通知
サインアウト 終了 動き続ける WM_QUERYENDSESSION(ENDSESSION_LOGOFF)→WM_ENDSESSION
シャットダウン(高速スタートアップ有効時) 終了 休止(hiberfil.sysへ保存) WM_QUERYENDSESSION→WM_ENDSESSION、サービスへ(PRE)SHUTDOWN
再起動 終了 完全に終了し、次回フルブート 同上
電源断 即消滅 即消滅 なし

サインアウトとシャットダウンは、アプリから見るとほぼ同じイベントです。WM_QUERYENDSESSIONのlParamにENDSESSION_LOGOFFビットが立っていればサインアウト、0ならシャットダウンまたは再起動(両者の区別はできません)です。1 つまり「サインアウトぐらいなら大丈夫」という油断は通用せず、同じ後始末コードが呼ばれるように作るのが正解です。

4つの「終わり方」とアプリへの通知サインアウト、シャットダウン、再起動ではWM_QUERYENDSESSIONからWM_ENDSESSIONの通知が届き、数秒で後始末する。電源断だけは通知が一切なく、書き込み設計で備えるしかないサインアウト通知あり: WM_QUERYENDSESSION → WM_ENDSESSION(サービスへは(PRE)SHUTDOWN)シャットダウン再起動電源断通知なし ── 8章の書き込み設計とUPSで備える数秒で後始末(3〜6章)

2.2. 「シャットダウンしたのに直らない」の正体 ── ハイブリッドシャットダウン

見落とされがちなのが表の2行目です。Windows 8以降のクライアントOSでは、休止をサポートするPCで高速スタートアップ(ハイブリッドシャットダウン)が既定で有効になり、「シャットダウン」の動きが変わりました。ユーザーセッションのサインアウトまでは通常どおり行われますが、カーネルセッションは閉じられず、デバイスドライバーごと休止ファイル(hiberfil.sys)に保存され、次回起動時にそのまま復元されます。これによって起動が速くなる一方、カーネルやドライバーの状態は電源を切っても持ち越されます。2 ただしこれは条件付きの動作です。休止自体が無効な環境(powercfg /hibernate off)、ポリシーや電源オプションで高速スタートアップをオフにした環境、そしてWindows Serverでは、シャットダウンは従来どおりの完全シャットダウンになります。対象PCがどちらで動いているかは、電源オプションの「高速スタートアップを有効にする」のチェック、またはpowercfg /a(利用可能なスリープ状態に「高速スタートアップ」が出るか)で確認できます。

シャットダウン操作でカーネルがどうなるかシャットダウン操作は高速スタートアップの有効無効で完全シャットダウンかカーネル休止かに分かれ、再起動は常にフルブートになる高速スタートアップ有効(クライアント既定)休止無効・ポリシーでオフ・Windows Server「シャットダウン」操作「再起動」操作ユーザーセッション終了 + カーネルはhiberfil.sysへ休止完全シャットダウン次回起動: カーネル・ドライバーの状態を復元次回起動: フルブートで初期化

一方、「再起動」は常に完全なブートサイクルを実行します。ドライバー更新後などに完全に新しい状態が必要だからです。2 ここから、実務でよく聞く現象がきれいに説明できます。

  • 「シャットダウンして電源を入れ直したのに、デバイスの不調が直らない」── カーネルとドライバーは休止から復元されただけで、リセットされていません
  • 「再起動したら直った」── フルブートで初期化されたからです
  • 装置PCの障害対応手順には「電源を切って入れ直す」ではなく「再起動する」と書くべきです

コマンドで完全シャットダウンを明示したい場合はshutdown /s(Shutdown.exeの既定は完全シャットダウン)、既定のハイブリッド動作を再現したい場合はshutdown /s /hybridが使えます。2 なお、高速スタートアップの無効化は推奨されていません。アプリ側は「シャットダウンではカーネルが休止しているだけかもしれない」前提で、たとえば「累積稼働時間」をOS起動時刻から推定しない、といった設計で対応します(高速スタートアップの有効・無効は環境ごとに違うため、どちらでも壊れない設計にします)。

3. GUIアプリの作法 ── WM_QUERYENDSESSIONとWM_ENDSESSION

3.1. 2つのメッセージの役割分担

ウィンドウとメッセージキューを持つアプリには、セッション終了が2段階で通知されます。1

  1. WM_QUERYENDSESSION ── 「終了してよいか」の問い合わせ。アプリは即座にTRUEを返すべきで、DefWindowProcの既定応答もTRUEです。ここで後始末を始めてはいけません。
  2. WM_ENDSESSION(wParam=TRUE) ── 「セッションは本当に終わる」という確定通知。後始末はここで行います。

WM_QUERYENDSESSIONでFALSEを返せばシャットダウンを中止させることもできますが、ドキュメントは「ユーザーの意図を尊重してTRUEを返すべき」と明記しており、FALSEを返したアプリも「シャットダウンを妨げているアプリ」として全画面UIに晒されます。また、コンソールアプリや可視ウィンドウを持たないアプリはそもそもシャットダウンを中止できず、5秒以内に応答しなければ自動的に強制終了されます。14

セッション終了の2段階通知の流れWM_QUERYENDSESSIONの問い合わせにTRUEを返すとWM_ENDSESSIONで確定し後始末を行う。FALSEで拒否すると妨げているアプリとして表示され、約5秒応答しないと強制続行され得るTRUE(原則)FALSE(例外的な拒否)約5秒無応答ユーザーが強制続行ユーザーがキャンセルWM_QUERYENDSESSION(問い合わせ)WM_ENDSESSION wParam=TRUE(確定)「妨げているアプリ」として全画面UIに表示応答なし扱いここで後始末(保存・切断)プロセス終了シャットダウン中止 → 全アプリ続行

3.2. 応答しないとどうなるか ── 5秒の壁

WM_QUERYENDSESSIONにもWM_ENDSESSIONにも、応答を遅らせられるのは約5秒です。それを超えると、システムは「このアプリがシャットダウンを妨げています」画面を表示し、ユーザーは強制続行(=アプリの強制終了)を選べます。4 強制終了されたプロセスに保存処理の続きを走らせる機会はありません。

したがって設計の要点は次の2つです。

  • 後始末を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_QUERYENDSESSION(問い合わせ)WinForms: FormClosing(WindowsShutDown)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 はよほどの理由がない限り設定しない
}

ここで1つ注意があります。FormClosing(CloseReason.WindowsShutDown)もWPFのSessionEndingも、対応しているのは問い合わせ段階(WM_QUERYENDSESSION)です。別のアプリが拒否すればシャットダウンは中止され、自アプリはそのまま動き続けます。したがってこれらのイベントで行ってよいのは、中止されても害がなく、何度実行しても同じ結果になる(冪等な)スナップショット保存までです。「終わるときにしかやってはいけない後始末」(接続の切断、リソースの明け渡しなど)が必要なら、WndProcで確定通知のWM_ENDSESSION(wParam=TRUE)を直接フックして、そちらで行います。

どちらの経路でも中身は共通の「スナップショット保存関数」に寄せ、通常終了・シャットダウン・(可能なら)クラッシュ時の復元用データを同じ形式で書くと、次回起動時の復元ロジックが1本で済みます。クラッシュ時にも情報を残す設計は「Windowsアプリのクラッシュ時にログとダンプを残す設計」で扱っています。

4. どうしてもブロックするなら ── ShutdownBlockReasonCreate

CDやファームウェアの書き込みのように、途中で切られると物理的に壊れる処理だけは例外です。この場合の正しい作法は、中断できない処理の開始時にShutdownBlockReasonCreateで理由文字列を登録し、完了したら即座にShutdownBlockReasonDestroyで解除することです。シャットダウンが要求されると、この理由が「このアプリがシャットダウンを妨げています」画面に表示され、ユーザーが継続するか中止するかを判断できます。3

ShutdownBlockReasonCreateによる保護の流れ中断できない処理の開始時に理由を登録し、保護中にシャットダウン要求が来たら理由が全画面に表示され、WM_QUERYENDSESSIONへのFALSEで拒否する。ユーザーは中止も強制続行も選べ、処理が完了したら理由を解除するユーザーが中止ユーザーが強制続行中断できない処理を開始ShutdownBlockReasonCreateで理由を登録処理を実行(ワーカースレッド)完了 → ShutdownBlockReasonDestroyで解除この間にシャットダウン要求「妨げています」画面に理由を表示 + 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, "測定データをファイルへ書き込んでいます");
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章 ── は依然として必要です)。

運用上の注意は3つです。

  • 理由文字列は短く、具体的に。ユーザーは急いでいて数秒しか読みません。「CDの書き込み中です」程度が適切とドキュメントも例示しています。3
  • アプリの起動中ずっと登録しっぱなしにしない。「中断できない処理の間だけ」がAPIの想定です。
  • ブロックできる前提で設計しない。ユーザーは強制続行を選べますし、強制シャットダウン(ENDSESSION_CRITICAL)ではそもそも待ってもらえません。「アプリケーションはシャットダウンをブロックできることに依存してはならない」と明記されています。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秒

注意すべき点が2つあります。まず、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 / CTRL_BREAK登録したHandlerRoutineで後始末CTRL_CLOSE_EVENTCTRL_SHUTDOWN_EVENT(サービスプロセス)猶予を超えると強制終了
// コンソールアプリ: 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_EVENT / CTRL_SHUTDOWN_EVENT.NET 9まで: ランタイム既定ハンドラー → ProcessExit発生 → 終了.NET 10以降: 既定ハンドラーなし → OSの既定処理で即終了(ProcessExitなし)対策: SetConsoleCtrlHandler / PosixSignalRegistrationを自分で登録

代わりに、アプリモデルごとの正規の経路へ寄せます。

  • 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(WinForms/WPF)Generic Host / Worker Service素のコンソールどのアプリモデルかFormClosing / SessionEnding(確定処理はWM_ENDSESSIONフック)IHostApplicationLifetime + StopAsync(ShutdownTimeoutを明示)SetConsoleCtrlHandler / PosixSignalRegistrationAppDomain.ProcessExit頼み✕ .NET 10以降、外部シグナル経路では発生しない

猶予は経路ごとに異なります ── GUIやコンソールクローズは約5秒、サービスは6章のSCMの猶予(約20秒、PRESHUTDOWNなら構成した値)、Ctrl+Cには明示のタイムアウトがありません。ただしどの経路でも猶予は限られ、当てにもできないため、「終了イベントで頑張る」のではなく、処理の節目ごとに保存してあるのが正常系、が設計の軸になります。

6. Windowsサービスの作法 ── SHUTDOWNとPRESHUTDOWN

6.1. 2種類のシャットダウン通知

サービスはサインアウトの影響を受けませんが、シャットダウン・再起動では止められます。通知はサービスコントロールマネージャー(SCM)から制御コードとして届き、受け取るには受け入れフラグの宣言が必要です。8

宣言 届く通知 タイミングと猶予
SERVICE_ACCEPT_SHUTDOWN SERVICE_CONTROL_SHUTDOWN シャットダウン処理の中で通知。既定で約20秒、上限はWaitToKillServiceTimeout
SERVICE_ACCEPT_PRESHUTDOWN SERVICE_CONTROL_PRESHUTDOWN SHUTDOWNより先に通知。サービスが停止するかタイムアウトまでSCMが待つ
サービスへのシャットダウン通知の順序シャットダウンが始まると、まずPRESHUTDOWNを宣言したサービスに構成した猶予つきで通知され、その後SHUTDOWN通知が既定約20秒の猶予で送られ、猶予切れでプロセスは終了されるシャットダウン開始SERVICE_CONTROL_PRESHUTDOWN(宣言したサービスのみ・構成した猶予)SERVICE_CONTROL_SHUTDOWN(既定 約20秒)猶予切れ → プロセス終了

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より長引くなら、dwCheckPointを増やしながら
// SERVICE_STOP_PENDINGを定期的に報告し続ける。SCMはwaitHintとチェックポイント
// の前進で「まだ生きて進んでいる」と判断する。報告が止まるとハング扱いで
// シャットダウンが先へ進み得る。完了したら必ずSERVICE_STOPPEDを報告する

6.2. 猶予に頼らない設計

猶予の上限であるWaitToKillServiceTimeoutをサービス側で書き換えて延ばすことは、明確に非推奨です。ドキュメントはむしろ逆方向 ── UPS駆動中のマシンが電池切れ前にシャットダウンを完了できるよう、サービスは可能な限り速く後始末を終えるべき ── を要求しています。日頃からこまめに保存して未保存データを最小化し、シャットダウン時はメモリ解放などに時間を使わない、ネットワーク相手の通知は返答を待ちすぎない、という指針です。また、シャットダウン時のSCMは既定で依存関係を考慮せずに通知するため、「依存先サービスが先に落ちていても壊れない」停止処理にしておく必要もあります。8

猶予に頼らない停止処理の設計処理の節目ごとに保存して未保存データを常に最小へ保てば、停止通知が来たときの後始末は数秒で終わる。終了時にまとめて保存する設計は猶予に収まらず、強制終了でデータを失う日頃: 節目ごとに保存(未保存データを常に最小に)停止通知 → 残りわずかを保存 → 数秒で完了日頃: メモリに溜めて終了時にまとめて保存停止通知 → 保存が猶予に収まらない強制終了 → データ喪失

.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エラー報告)がコールバックを呼び、作業中データを保存する猶予をくれます。ただし保存に時間がかかる場合は、登録時に指定したping間隔内にApplicationRecoveryInProgressを呼び続けないと回復処理が途中で打ち切られます。保存が終わったらApplicationRecoveryFinishedで完了を通知します。アプリ更新時の「使用中ファイルの差し替えと再起動」はRestart Managerの領分で、「使用中のexe/DLLをどう差し替えるか」で詳しく扱っています。

7.2. ARSO ── 更新再起動後の自動サインイン

Windows Updateによる再起動後、誰もサインインしなければユーザーセッションのアプリは戻りません。ここを埋めるのがARSO(Winlogon自動再起動サインオン)です。Windows Updateが再起動を開始するとき、最後の対話ユーザーの資格情報を安全に保存してAutologonを構成し、再起動後にそのユーザーを自動サインインさせたうえで画面をロックします11 shutdown /gのように、再起動+登録アプリの再開を指示するコマンドもあります。組織のポリシー(DisableAutomaticRestartSignOn等)で無効化されている環境もあるため、無人復帰を設計するときは、この設定とセットで確認してください。なお、常時必要なバックグラウンド処理をユーザーセッションの自動起動に頼るくらいなら、最初からWindowsサービスにするのが筋です。

再起動後にアプリが自動復帰するまでの経路RegisterApplicationRestartで事前登録しておくと、クラッシュや応答なしではユーザーの同意を経て、更新による再起動ではARSOの自動サインインと画面ロックを経てアプリが再起動される。起動60秒未満のプロセスと昇格プロセスは対象外ユーザーの同意RegisterApplicationRestartで登録(問題が起きる前に)クラッシュ・応答なし更新による再起動アプリ再起動ARSO: 自動サインイン + 画面ロック対象外: 起動60秒未満(ループ防止)、昇格プロセス

8. 通知が来ない電源断に耐える ── 書き込み設計とUPS

8.1. 「いつ切れても壊れない」書き込み ── 一時ファイル+ReplaceFile

ブレーカー断・電源ユニット故障・プラグ抜けには、WM_ENDSESSIONもPRESHUTDOWNもありません。設定ファイルや測定結果を「元ファイルへ直接上書き」している限り、書き込み途中の電源断で新旧が混ざった壊れたファイルが残り得ます

定石は、同じボリューム上の一時ファイルに書き切ってから差し替えることです。ReplaceFileは「新ファイルへの保存→元ファイルの退避→リネーム→削除」という一連の手順を1つのAPIにまとめたもので、作成日時・ACL・代替ストリームなど元ファイルの属性も引き継がれます(3つのファイルは同一ボリュームにある必要があります)。12 .NETのFile.Replaceがこれをそのまま呼び出します。

一時ファイルとReplaceFileによる保存と復旧の流れ保存時は一時ファイルへ書き切ってフラッシュしReplaceFileで差し替えて旧内容を.bakに残す。起動時は本体を検証し、壊れていれば.bakへフォールバックする次回起動時保存時正常壊れているどの瞬間に電源断が起きても本体ファイルを検証そのまま使用.bakへフォールバックフラッシュ(FlushFileBuffers相当)一時ファイルへ書き切るReplaceFileで差し替え(旧内容は.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には使えないため、そちらは「1行=1レコードで書き、読み込み時に壊れた末尾行を捨てる」といった、壊れ方を織り込んだフォーマットにします。

8.2. WriteFileの成功はディスク到達ではない

もう1つの前提が、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の役割は「停電を止める」ことではなく、「通知のない電源断」を「通知のある計画シャットダウン」に変換することだと捉えてください。設計は次の2段構えです。

  1. 猶予の設計: UPSのバッテリー保持時間 >「バッテリー切り替わりの検知〜アプリ・サービスの後始末〜OSシャットダウン完了」の合計時間、を満たすこと。サービスの停止処理が遅いと、この式が成立しなくなります(6.2節)
  2. 検知: AC電源からバッテリーへの切り替わりや残量低下は、PBT_APMPOWERSTATUSCHANGEイベントで通知されます。ウィンドウを持つアプリはWM_POWERBROADCASTで受け取り、ウィンドウを持たないサービスはSERVICE_ACCEPT_POWEREVENTを宣言したうえでHandlerExのSERVICE_CONTROL_POWEREVENTとして受け取ります(WM_POWERBROADCASTはサービスの制御ハンドラーには届きません)。受け取ったらGetSystemPowerStatusを呼び、ACLineStatus(AC給電か)やBatteryLifePercentを確認して、計測の中断・保存・シャットダウン要求へつなげます15
UPSで電源断を計画シャットダウンに変換する流れ停電でUPSがバッテリー給電へ切り替わるとPBT_APMPOWERSTATUSCHANGEが通知され、電源状態を確認して保存とシャットダウン要求につなげることで、通知のない電源断が通知のある計画シャットダウンに変わる停電・電源断UPSがバッテリー給電へ切替PBT_APMPOWERSTATUSCHANGE通知GetSystemPowerStatusで状態確認計測の中断・保存OSへシャットダウン要求通常のシャットダウン通知の流れ(3〜6章)

USB接続の一般的なUPSはWindowsからバッテリーとして見えるため、この標準APIで検知できます。ベンダー製の管理ソフトが「残量N%でOSをシャットダウンする」機能を持つ場合は、そのしきい値と自アプリの後始末時間の整合も確認してください。なお、スリープや休止からの復帰と長期稼働の問題は別軸の話として「スリープ・休止・Modern Standbyと長時間稼働アプリ」で扱っています。

9. 検証方法 ── シャットダウンを安全に試す

シャットダウン処理は「書いたが一度も本番相当で試していない」コードになりがちです。安全に検証する手順を用意しておきます。

  • 検証機・仮想マシンで試す: 本番の装置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秒(サービスなら構成した猶予)に収まっているかを実測します
検証する操作と確認できる範囲サインアウトで通知経路を手軽に確認し、shutdownコマンドの完全・ハイブリッド・再起動で本番の通知経路と猶予を確認し、VMの電源オフで突然停止への耐性を、実機の電源断テストで物理ストレージ込みの耐性を最終確認するサインアウトWM_QUERYENDSESSION → WM_ENDSESSIONの経路(手軽な確認)shutdown /s・/s /hybrid・/r本番の通知経路と猶予VMの電源オフゲストが予告なく止まる耐性実機の電源断テスト物理ストレージ込みの耐性(最終確認)

事後の切り分けにはイベントログ(System)が使えます。正常なシャットダウン・再起動では、イベントID 1074(どのプロセスが誰のために、どんな理由でシャットダウンを開始したか)が記録されます。突然の電源断やクラッシュでは1074がなく、次回起動時にイベントID 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以外ならクラッシュ、0でメモリダンプも残っていなければ電源断が濃厚、というように周辺情報で原因を切り分け、電源断と分かったら8章の書き込み設計とUPSの出番です。

10. まとめ

  • シャットダウンは「いつか必ず来る正常なイベント」です。通知後の猶予は原則5秒程度しかないため、こまめな自動保存で「終了時にやること」を最小化しておくのが大前提です。
  • Windows 8以降のクライアントOSでは、高速スタートアップが有効ならば「シャットダウン」はハイブリッドシャットダウンで、カーネルは休止しているだけです。完全リセットは「再起動」だけ ── 障害対応手順には「再起動」と書いてください。
  • GUIアプリはWM_QUERYENDSESSIONに即TRUEを返し、確定後の後始末はWM_ENDSESSIONで行います。WinForms/WPFのFormClosing・SessionEndingは問い合わせ段階に対応するため、そこで行うのは冪等なスナップショット保存までです。シャットダウン中にダイアログを出してはいけません。
  • どうしても中断できない処理は、ShutdownBlockReasonCreateで理由を掲示して守ります。ただしブロックできる保証はどこにもありません。
  • コンソールアプリはSetConsoleCtrlHandler、サービスはSERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWNで通知を受けます。PRESHUTDOWNの既定猶予は現行OSで10秒です。.NETではProcessExit頼みをやめ、アプリモデルの正規経路へ寄せます。
  • 再起動後の復帰は、RegisterApplicationRestart(+回復コールバック)とARSOで無人化できます。
  • 電源断には通知が来ません。一時ファイル+ReplaceFileによる差し替え(バックアップ+起動時検証とセット)、節目のフラッシュ、UPSによる「電源断の計画シャットダウン化」で備えます。
シャットダウン対応の全体像通知のある終わり方には数秒で店じまいできる後始末で応じて再起動後の自動復帰につなげ、通知のない電源断にはいつ切れても壊れない書き込みとUPSで備えて実機まで含めて検証する。この2本柱が記事の結論通知あり(サインアウト・シャットダウン・再起動)通知なし(電源断)終わり方数秒で店じまいできる後始末(3〜6章)いつ切れても壊れない書き込み + UPS(8章)再起動後の自動復帰(7章)実機まで含めた検証(9章)
  • 検証は仮想マシンとサインアウトで安全に行い、事後はイベントID 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が「新ファイルへの保存・元ファイルの一時リネーム・新ファイルのリネーム・元ファイルの削除」に相当する複数の手順を1つの関数にまとめること、作成日時・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. 正常な再起動ではイベントID 1074(どのプロセスが誰のためにどんな理由でシャットダウンを開始したか)が記録されること、予期しない再起動ではイベントID 41(Kernel-Power)と6008(直前のシャットダウンは予期されていなかった)が記録されること、これらのIDで再起動の種類を切り分けられることについて。  2

  15. Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. バッテリーとAC電源の切り替わりや残量低下時に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の電源断をまたいだ原子性は仕様上保証されているわけではないため、バックアップ(第3引数)を残し、起動時に本体を検証して壊れていればバックアップへ戻す読み込み処理までをセットで実装します。加えて、WriteFileの成功はディスク到達を意味しないため、重要な節目ではFlushFileBuffersやFILE_FLAG_WRITE_THROUGHで書き込みを確定させます。装置PCではUPSを併用し、バッテリー駆動への切り替わりを検知して安全にシャットダウンへつなげる構成が定石です。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る