アプリから見たWindowsのシャットダウン ── 終了通知・再起動・電源断を正しく生き延びる
· 更新日: · 小村 豪 · Windows, シャットダウン, Windows開発, Windowsサービス, 装置PC, データ保全, 長期稼働, UPS
更新履歴(6件・最終更新 2026年09月06日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- Windowsシャットダウン対応の解説を、アプリ種別ごとの通知経路、短い後始末、再起動後の復帰、電源断に備えた保存と復旧、検証の順に整理した。元の主張・条件・例外、コード例、FAQ、参考資料、知識マップを維持し、問い合わせと終了確定、OSとHostの猶予、保存と起動時検証の役割を見出しと図で明確にした。
- 知識マップの関係を総点検し、説明文と食い違っていた関係(向きの逆転・過剰な一般化・述語の取り違え)を修正しました。本文の説明は変えていません。
- 知識マップの関係を総点検し、説明文と食い違っていた関係(向きの逆転・過剰な一般化・述語の取り違え)を修正しました。本文の説明は変えていません。
- 知識マップの関係を総点検し、説明文と食い違っていた関係(向きの逆転・過剰な一般化・述語の取り違え)を修正しました。本文の説明は変えていません。
- 知識マップの関係を総点検し、説明文と食い違っていた関係(向きの逆転・過剰な一般化・述語の取り違え)を修正しました。本文の説明は変えていません。
- 知識マップの「SERVICE_CONTROL_SHUTDOWNをSCMに用いることは推奨されない」という関係を削除し、WaitToKillServiceTimeoutを延ばさず速く後始末を終えるという指針を既存の関係の説明へ統合しました。本文の説明は変えていません。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.22054271)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「アプリから見たWindowsのシャットダウン ── 終了通知・再起動・電源断を正しく生き延びる」合同会社小村ソフト. https://doi.org/10.5281/zenodo.22054271 https://comcomponent.com/blog/windows-shutdown-handling-for-apps/
- DOI(最新版)
- 10.5281/zenodo.22054271
- DOI(この版)
- 10.5281/zenodo.22525708
「Windows Updateの夜間再起動で、計測中のファイルが壊れた」「共有PCからサインアウトすると、編集中の内容が消えてしまう」。こうした事故を防ぐには、シャットダウンを例外扱いせず、外から終了を要求されることを、アプリの通常の動作として設計する必要があります。
ただし、終了通知を受けるコードだけでは足りません。通知後に使える時間は短く、突然の電源断には通知自体がありません。この記事の軸は、日頃の保存、短い後始末、次回起動時の復旧を一続きにすることです。
中小企業の情シス担当者とWindowsアプリ開発者、特に装置PC・長期稼働アプリを扱う方に向けて、通知経路の選び方から実装、再起動後の復帰、電源断への備え、検証までを整理します。技術的な根拠は、元記事で参照した2026年8月時点のMicrosoft Learnの一次情報です。
1. まず結論 ── 通知を待つ前に、保存すべき量を減らす
「通知が来たら数秒で終了できる状態を保ち、通知が来ない場合も次回起動で復旧できるようにする」。これがシャットダウン対応の基本方針です。
終了通知を受けてから大量のデータを保存するのではなく、処理の節目ごとに保存し、終了時に残る差分を小さくします。GUIアプリやコンソールクローズでは約5秒が重要な目安です。サービスには別の猶予がありますが、いずれも「必ず最後まで待ってもらえる時間」ではありません。123
1.1. 自分のアプリの通知経路を選ぶ
| アプリの形 | 終了要求を受ける場所 | 最初に押さえること | 読む章 |
|---|---|---|---|
| Win32のGUIアプリ | WM_QUERYENDSESSIONとWM_ENDSESSION |
問い合わせには原則即TRUE。終了確定後の後始末はWM_ENDSESSIONで行う |
3章 |
| WinForms / WPF | FormClosing / SessionEnding、必要に応じてメッセージのフック |
この2つのイベントは問い合わせ段階。取り消せない後始末は確定通知へ分ける | 3章 |
| 素のコンソールアプリ | SetConsoleCtrlHandlerなど |
クローズとサインアウト・シャットダウンは、通知される条件が異なる | 5章 |
| Windowsサービス | SCMからのSHUTDOWN / PRESHUTDOWN |
受け入れフラグを宣言し、制御ハンドラーはすぐに返す | 6章 |
| Generic Host / Worker Service | IHostApplicationLifetimeとStopAsync |
Hostの停止経路へ後始末を集約し、ShutdownTimeoutを明示する |
5・6章 |
.NETではAppDomain.ProcessExitだけに頼らないでください。 .NET 10以降、ランタイムはCTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENTへの既定ハンドラーを提供しません。この外部シグナル経路と、Mainからの復帰などの通常終了は別です。詳しくは5.2節で扱います。4
1.2. 通知対応の外側にも、2つの備えが必要
通知を受けたら、利用者に「保存しますか?」と聞くのではなく、短い後始末で終了します。どうしても中断できない処理の一時的なブロックは4章、再起動後の自動復帰は7章で扱います。
もう一つは、通知のない電源断でも読めるデータを残すことです。一時ファイルへの保存、フラッシュ、差し替え、バックアップ、起動時の検証を8章でまとめます。通知経路を実装したら、8章の保存・復旧設計と9章の検証まで確認してください。
flowchart TB
accTitle: 終了通知と電源断に共通する保存設計
accDescr: 日頃の保存で未保存差分を小さくし、通知がある場合は短い後始末、通知がない場合は保存済みデータの検証と復旧を経て業務を再開する
daily["節目ごとに保存"] --> event{"終了時に通知があるか"}
event -->|"ある"| close["残る差分だけ保存・終了"]
event -->|"ない"| lost["その場の後始末はできない"]
close --> boot["次回起動で検証・復旧"]
lost --> boot
boot --> resume["業務を再開"]
図1: 通知時だけ頑張るのではなく、普段の保存から次回起動までを一続きにする。
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全14件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. 最初に区別する ── サインアウト・シャットダウン・再起動・電源断
2.1. ユーザーセッションとカーネルは、別々に見る
「何が終わるか」を2つに分けると、対応が整理できます。ユーザーセッションは、そのユーザーのアプリが動く範囲です。一方、カーネルやドライバーはOSの側にあり、ユーザーのサインアウト後も動き続けます。
| 操作 | ユーザーセッション | カーネル・ドライバー | 通知の扱い |
|---|---|---|---|
| サインアウト | 終了する | 動き続ける | GUIには終了問い合わせ・確定通知。サービスはサインアウトでは停止しない |
| 高速スタートアップ有効時のシャットダウン | 終了する | hiberfil.sysへ休止状態を保存する |
GUIのセッション終了通知と、サービスへのシャットダウン通知 |
| 再起動 | 終了する | 完全に終了し、次回フルブートする | 同上 |
| 突然の電源断 | 即座に失われる | 即座に失われる | 通知はない |
GUIのWM_QUERYENDSESSIONでは、lParamのENDSESSION_LOGOFFビットがサインアウトを表します。lParamが0ならシャットダウンまたは再起動で、両者の区別はできません。lParamはビットマスクとして扱います。1
アプリの未保存データにとっては、サインアウトもシャットダウンも同じ備えが必要です。「サインアウトなら保存しなくてよい」と分けず、共通の保存処理へつなぎます。 ただしコンソールやサービスの通知条件まで同じになるわけではなく、それぞれ5・6章の経路を確認します。
flowchart TB
accTitle: サインアウトとシステム終了を分けて考える
accDescr: サインアウトはユーザーのアプリを終了するがサービスは動き続け、システムのシャットダウンや再起動ではサービスも停止対象になる一方、電源断はどちらにも通知がない
signout["サインアウト"] --> user["ユーザーのアプリが終了"]
signout -.-> alive["サービスは動き続ける"]
system["シャットダウン・再起動"] --> user
system --> svc["サービスも停止対象"]
power["電源断"] --> none["どちらにも通知なし"]
図2: サインアウトでGUIの終了処理は試せても、サービスの停止処理まで試したことにはならない。
2.2. 「シャットダウンでは直らず、再起動で直る」理由
Windows 8以降のクライアントOSでは、休止をサポートする多くのPCで高速スタートアップが既定で有効です。この構成のシャットダウンでは、ユーザーはサインアウトしますが、カーネルとデバイスドライバーの状態は休止ファイルに保存され、次回起動で復元されます。電源を切っても、OSの状態がすべてリセットされたとは限りません。5
これは条件付きの動作です。休止を無効にした環境(powercfg /hibernate off)、ポリシーや電源オプションで高速スタートアップをオフにした環境、Windows Serverでは、従来どおりの完全シャットダウンになります。電源オプションの設定を確認し、powercfg /aでは高速スタートアップを利用可能か確認します。
一方、「再起動」は常に完全なブートサイクルを実行します。ドライバーの不調を切り分ける手順には、「電源を切って入れ直す」ではなく「再起動する」と明記してください。5
flowchart TB
accTitle: 高速スタートアップと完全ブート
accDescr: 高速スタートアップが有効なシャットダウンではカーネルとドライバーの状態が保存・復元され、完全シャットダウンや再起動ではフルブートで初期化される
s["シャットダウン"] --> q{"高速スタートアップを使うか"}
q -->|"はい"| save["カーネル・ドライバーを休止"]
save --> restore["次回起動で状態を復元"]
q -->|"いいえ"| full["完全シャットダウン"]
full --> boot["次回フルブートで初期化"]
r["再起動"] --> boot
図3: 電源を切ったことと、カーネルやドライバーをリセットしたことは同じではない。
コマンドで完全シャットダウンを明示するならshutdown /s、ハイブリッド動作ならshutdown /s /hybridです。Shutdown.exeの既定は完全シャットダウンなので、画面の「シャットダウン」と同じだと思い込まないことが重要です。5
高速スタートアップを無効化して対応することは推奨されていません。アプリは有効・無効のどちらでも動くように作ります。たとえば、装置の「累積稼働時間」をOS起動時刻だけから推定せず、カーネルの状態が持ち越され得ることを設計に織り込みます。
3. GUIアプリ ── 問い合わせと終了確定を分ける
3.1. WM_QUERYENDSESSIONは確認、WM_ENDSESSIONは結果
ウィンドウとメッセージキューを持つアプリには、セッション終了が2段階で通知されます。1
| メッセージ | 意味 | 行うこと |
|---|---|---|
WM_QUERYENDSESSION |
「終了してよいか」の問い合わせ | 原則として即座にTRUEを返す。DefWindowProcの既定もTRUE |
WM_ENDSESSION、wParam=TRUE |
セッションの終了が確定した | 短い保存・切断などの後始末を行う |
WM_ENDSESSION、wParam=FALSE |
セッション終了が中止された | アプリは動作を続ける。確定後にしかできない後始末は行わない |
問い合わせに自分がTRUEを返しても、必ず終了するとは限りません。 別のアプリが拒否して中止になる場合があります。ここで接続を切ったり、必要なリソースを明け渡したりすると、終了しなかったアプリが動けなくなります。後始末を確定通知へ分ける理由はここにあります。1
flowchart TB
accTitle: GUIの問い合わせと終了結果
accDescr: WM_QUERYENDSESSIONにTRUEを返しても他のアプリによって終了が中止され得るため、WM_ENDSESSIONのwParamがTRUEの場合だけ確定後の後始末を行う
query["WM_QUERYENDSESSION"] --> reply["原則は即TRUEを返す"]
reply --> result["WM_ENDSESSION"]
result --> yes{"wParamはTRUEか"}
yes -->|"はい"| cleanup["確定後の後始末"]
yes -->|"いいえ"| running["終了中止・動作を継続"]
図4: 終了を許可する返答と、終了が確定した通知を混同しない。
FALSEを返して終了を拒否できる場合もありますが、原則は利用者の終了意図を尊重することです。拒否したアプリは「シャットダウンを妨げているアプリ」として表示されます。コンソールや可視ウィンドウを持たないアプリにも制約があり、通常の構成では5秒以内に応答しないと自動的に強制終了され得ます。ブロックは4章の例外的な処理として扱い、通常の保存のために使いません。6
3.2. 約5秒は「最後まで保存できる保証」ではない
WM_QUERYENDSESSIONとWM_ENDSESSIONの各段階で応答を約5秒遅らせると、システムは妨げているアプリの画面を表示し、利用者は強制続行を選べます。強制終了後に保存の続きを実行する機会はありません。6
対策は、日頃から保存して終了時の差分を減らすことです。未保存の作業状態は一時的な場所へ退避し、次回起動時に復元します。シャットダウン中に確認ダイアログを出して待つ設計にはしません。 通常の終了確認と、OSからの終了要求は分けて扱います。1
flowchart TB
accTitle: 終了時の仕事を小さくする
accDescr: 日頃から保存する設計は終了時に残る差分が小さく、終了時までメモリに溜める設計は短い猶予に収まらず強制終了で失う危険がある
good["日頃から保存"] --> small["終了時に残る差分は小さい"]
small --> fast["短い後始末で終了"]
bad["終了時までメモリに溜める"] --> large["終了時にまとめて保存"]
large --> risk["猶予不足・強制終了の危険"]
図5: 数秒で終了するための準備は、終了通知より前に行う。
3.3. WinForms・WPFでは、保存と取り消せない後始末を分ける
WinFormsではFormClosingのCloseReason.WindowsShutDown、WPFではApplication.SessionEndingが対応します。WPFはXAMLのSessionEnding属性からも登録でき、OnSessionEndingのオーバーライドでも処理できます。
ただし、どちらも問い合わせ段階のイベントです。ここで許されるのは、中止されても害がなく、何度実行しても同じ結果になる「冪等なスナップショット保存」までです。接続の切断など終了確定後にしかできない処理は、WinFormsのWndProcやWPFのフックでWM_ENDSESSION(wParam=TRUE)を受けて行います。
flowchart TB
accTitle: WinFormsとWPFの終了処理の分担
accDescr: FormClosingとSessionEndingでは中止されても安全なスナップショットを保存し、WM_ENDSESSIONのTRUEをフックして取り消せない後始末を行う
notify["問い合わせ段階"] --> forms["FormClosing"]
notify --> wpf["SessionEnding"]
forms --> snapshot["冪等なスナップショット保存"]
wpf --> snapshot
final["WM_ENDSESSIONのTRUE"] --> hook["WndProcやフックで受信"]
hook --> cleanup["接続切断など確定後の処理"]
図6: フレームワークのイベントに、終了確定後の仕事まで詰め込まない。
次の2例は、問い合わせ段階で作業状態だけを保存する部分です。コード例は実装の抜粋であり、保存関数の中身、失敗時の扱い、イベント登録などはアプリ側で用意します。
// WinForms: シャットダウン/サインアウト時も FormClosing が呼ばれる
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.WindowsShutDown)
{
// 冪等なスナップショット保存だけを行う。ダイアログは出さない。
// e.Cancel = true(拒否)も設定しない。
SaveWorkingStateToTempFile();
return;
}
// ユーザーが×ボタンで閉じた場合など、通常時はここで確認してよい
}
// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
base.OnSessionEnding(e);
// ReasonSessionEnding.Logoff / Shutdown を区別できるが、
// どちらでも同じスナップショット保存を実行するのが基本
SaveWorkingStateToTempFile();
// e.Cancel = true はよほどの理由がない限り設定しない
}
両方とも、通常終了・シャットダウン・可能ならクラッシュ時に使うデータを、共通の保存関数と形式に寄せます。保存経路ごとに別の形式を作らなければ、次回起動時の復元ロジックも1本にできます。クラッシュ時に情報を残す設計は、Windowsアプリのクラッシュ時にログとダンプを残す設計で扱っています。
4. 中断できない処理だけ、一時的にブロックする
4.1. 理由の登録と、終了の拒否は別の仕事
CDやファームウェアの書き込みのように、中断すると物理的に壊れる処理は例外です。処理開始時にShutdownBlockReasonCreateで理由を登録し、完了時にShutdownBlockReasonDestroyで解除します。 登録した理由は、シャットダウンを妨げているアプリの画面に表示されます。7
ただし、理由文字列を登録するだけではシャットダウンは止まりません。保護中のフラグを持ち、その間だけWM_QUERYENDSESSIONへFALSEを返す処理を組み合わせます。処理が終わったら、理由と保護中フラグを両方解除します。
flowchart TB
accTitle: 一時的な終了ブロックの役割分担
accDescr: 中断できない処理中だけ理由登録と問い合わせ拒否を組み合わせ、完了すれば両方を解除するが利用者による強制続行は防げない
start["中断できない処理の開始"] --> reason["理由登録・保護フラグを設定"]
reason --> work["ワーカーで処理"]
work --> done["完了・理由とフラグを解除"]
work -.-> request["この間に終了問い合わせ"]
request --> refuse["FALSEを返し理由を表示"]
refuse --> choice{"利用者の判断"}
choice -->|"中止"| keep["動作を継続"]
choice -->|"強制続行"| terminate["終了され得る"]
図7: 理由の登録だけでは拒否にならず、拒否を実装しても強制続行までは防げない。
4.2. UIスレッドは、終了要求を受けられる状態に保つ
理由の登録・解除は、対象ウィンドウを作ったスレッドから呼びます。他のスレッドからでは失敗します。7 一方、中断できない長い処理そのものはワーカースレッドへ移します。UIスレッドを同期処理で塞ぐと、拒否するためのメッセージが処理される前に「応答なし」になってしまうためです。
[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);
}
この例は理由の登録、ワーカーでの処理、問い合わせの拒否を組み合わせた骨格です。APIの失敗を含むエラー処理は別途必要です。理由文字列は短く具体的にし、アプリ起動中ずっと登録し続けるのではなく、本当に中断できない処理の間だけに限定します。7
それでも利用者は強制続行を選べます。ENDSESSION_CRITICALなどの強制終了経路もあるため、ブロックできることをデータ保全の前提にしてはいけません。止められなかった場合への備えは、8章の保存・復旧設計です。6
5. コンソールと.NET ── 届く通知の条件を確認する
5.1. SetConsoleCtrlHandlerで受ける通知と猶予
コンソールアプリでは、SetConsoleCtrlHandlerで登録したハンドラーに制御シグナルが届きます。GUIのメッセージ処理とは違い、ハンドラーは別スレッドで実行されます。2
| シグナル | 発生する場面 | 既定の猶予 |
|---|---|---|
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を待つ設計にはしません。 対話アプリはサインアウト時点で終了させられるため、これらを受け取れるのは実質サービスとして動くプロセスだけです。さらに、gdi32.dllまたはuser32.dllを読み込むとWindowsアプリとして扱われ、LOGOFF/SHUTDOWN系ハンドラーは呼ばれません。その場合の公式な回避策は、非表示ウィンドウを作り、WM_QUERYENDSESSION / WM_ENDSESSIONを受けることです。28
flowchart TB
accTitle: コンソールの終了通知を受けられる条件
accDescr: 対話コンソールでクローズ通知を受けられてもログオフやシャットダウン通知は期待できず、サービスでもGUI用DLLをロードすると別の通知経路が必要になる
console["コンソールプロセス"] --> close["クローズは制御ハンドラーへ"]
console --> q{"LOGOFF・SHUTDOWNを待つか"}
q -->|"対話セッション"| no["この通知には頼れない"]
q -->|"サービス"| dll{"GUI用DLLをロードしたか"}
dll -->|"いいえ"| signal["対象の制御シグナルを処理"]
dll -->|"はい"| window["非表示ウィンドウで受信"]
図8: コンソールクローズへの対応を、そのままシャットダウン対応と見なさない。
次はCtrl+Cやコンソールクローズに備える抜粋です。デリゲートを保持してGCによる回収を防ぎ、短い後始末の後に既定ハンドラーへ進みます。これを登録しただけで、対話アプリのシャットダウン通知まで保証されるわけではありません。
// コンソールアプリ: 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 10以降、ProcessExitだけに後始末を置かない
.NET 10から、ランタイムはCTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENTへの既定ハンドラーを提供しなくなりました。自前や上位ライブラリのハンドラーがなければOSの既定処理で終了し、この経路ではAppDomain.ProcessExitもAssemblyLoadContext.Unloadingも発生しません。4
これは外部の終了シグナルへの既定動作の変更です。Mainからの復帰など、通常の終了でProcessExitが発生しなくなった、という意味ではありません。
flowchart TB
accTitle: .NETの既定終了ハンドラーへの依存を外す
accDescr: 従来のランタイムは対象シグナルからProcessExitへつないでいたが.NET 10以降は既定ハンドラーを提供しないためアプリモデルの通知経路を使う
signal["CLOSE・SHUTDOWNシグナル"] --> old["従来のランタイム既定処理"]
old --> event["ProcessExitなどを通知"]
signal --> modern[".NET 10以降は既定処理なし"]
modern --> own{"アプリ側の処理はあるか"}
own -->|"ない"| os["OS既定処理で終了"]
own -->|"ある"| handle["モデルに合う後始末へ"]
図9: 通常終了と外部シグナルによる終了を区別し、必要なハンドラーを自分のアプリモデルで用意する。
後始末はアプリモデルに合う場所へ集約します。GUIなら3章のイベントと確定通知、Generic HostならIHostApplicationLifetimeとBackgroundService.StopAsync、素のコンソールならSetConsoleCtrlHandlerやPosixSignalRegistrationです。後者を使う場合もSIGINT・SIGTERM・SIGHUPなど、対象とする終了経路に対応したシグナルを選びます。4
Generic HostではHostOptions.ShutdownTimeoutを明示します。ただしHost側の設定だけで、GUI、コンソール、SCMそれぞれの外側の猶予が延びるわけではありません。Ctrl+Cに明示的なタイムアウトがなくても、電源断や別の終了経路には備えが必要です。どのモデルでも、保存済みの状態を保ち、通知後の仕事を小さくする方針は共通です。
flowchart TB
accTitle: Hostの停止処理と外側の終了猶予
accDescr: Generic Hostの停止はStopAsyncへ集約するがHostOptionsのタイムアウトを設定してもOSやSCMの終了猶予が延長されるわけではないため両方を確認する
outer["OSやSCMから終了要求"] --> host["Hostの停止経路"]
host --> stop["StopAsyncで後始末"]
stop --> inner["Host側の停止時間を設定"]
outer -.-> limit["外側の猶予は別に存在"]
inner --> check["短時間で終わるか実測"]
limit --> check
図10: Host側とOS側の時間制限を別々に確認し、設定を延ばすだけで済ませない。
6. Windowsサービス ── 制御ハンドラーはすぐ返す
6.1. SHUTDOWNとPRESHUTDOWNの違い
サービスはサインアウトでは止まりませんが、シャットダウン・再起動では停止対象になります。通知はSCM(サービス制御マネージャー)から届き、受け取る制御コードに対応するフラグを宣言する必要があります。3
| 宣言するフラグ | 届く制御コード | 使い分け |
|---|---|---|
SERVICE_ACCEPT_SHUTDOWN |
SERVICE_CONTROL_SHUTDOWN |
通常のシャットダウン通知。既定の猶予は約20秒で、WaitToKillServiceTimeoutに依存する |
SERVICE_ACCEPT_PRESHUTDOWN |
SERVICE_CONTROL_PRESHUTDOWN |
通常のSHUTDOWNより先に通知。SCMは停止または構成したタイムアウトまで待つ |
flowchart TB
accTitle: サービスへの終了通知の段階
accDescr: SCMはPRESHUTDOWNを受け入れるサービスへ先に通知し停止またはタイムアウトを待ってから通常のSHUTDOWN通知へ進む
start["システムのシャットダウン開始"] --> pre["PRESHUTDOWN受付サービスへ通知"]
pre --> wait["停止または構成した期限を待つ"]
wait --> shut["SHUTDOWN受付サービスへ通知"]
shut --> proceed["猶予後はシステム終了へ進む"]
図11: 通知の早さと待機時間は別の条件であり、PRESHUTDOWNにも構成した期限がある。
PRESHUTDOWNのタイムアウトはChangeServiceConfig2(SERVICE_CONFIG_PRESHUTDOWN_INFO)で構成します。既定値はWindows 10 Creators Update(ビルド15063)以降は10秒、それ以前は3分です。「PRESHUTDOWNなら必ず3分待ってもらえる」という理解では、期待した時間を確保できません。9
PRESHUTDOWNはシステム全体のシャットダウンを待たせるため、本当に必要な場合だけ使います。通常のWaitToKillServiceTimeoutをサービス側で書き換えて延ばすことも、推奨されていません。3
6.2. 通知を受ける処理と、実際に停止する処理を分ける
制御ハンドラーは30秒以内に戻る必要がありますが、30秒使ってよいと考えるのではなく、停止を指示してすぐ戻す構成にします。長い処理は別スレッドへ任せ、SERVICE_STOP_PENDINGを報告します。3
// 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を報告する
ワーカー側では、後始末が待ちヒントより長引くならdwCheckPointを進めながらSERVICE_STOP_PENDINGを報告します。進捗報告が止まればハングと判断され得ます。完了したらSERVICE_STOPPEDを報告するところまでが停止処理です。39
flowchart TB
accTitle: サービス制御ハンドラーとワーカーの分担
accDescr: 制御ハンドラーは停止中を報告してワーカーに合図しすぐ戻り、ワーカーが後始末と進捗報告を行って最後に停止を報告する
handler["制御ハンドラー"] --> pending["STOP_PENDINGを報告"]
pending --> signal["ワーカーへ停止を合図"]
signal --> back["ハンドラーはすぐ戻る"]
signal --> worker["ワーカーが後始末"]
worker --> report["長引く場合は進捗を報告"]
report --> stopped["完了時にSTOPPEDを報告"]
図12: 通知を受けるスレッドを長い停止処理で塞がない。
6.3. 依存先が先に停止しても、短時間で終了できるようにする
シャットダウン時のSCMは既定でサービスの依存関係を考慮せず通知します。停止処理では、依存先サービスがすでに使えない場合も扱い、ネットワーク相手の返答を待ちすぎないようにします。メモリ解放などへ時間をかけるより、必要なデータを短時間で確定させることを優先します。3
この方針はUPSとの関係でも重要です。サービスが待機を延ばすほど、バッテリーが尽きる前にOS全体のシャットダウンを完了しにくくなります。猶予を増やすより、節目ごとの保存で停止時の仕事を減らすのが基本です。3
.NETのWorker ServiceをUseWindowsServiceで動かすと、STOP / SHUTDOWNはホストの停止へ変換され、BackgroundService.StopAsyncにつながります。元記事の執筆時点の標準実装はPRESHUTDOWNを受け入れないため、必要ならハンドラーの拡張実装が要ります。HostOptions.ShutdownTimeoutを明示し、StopAsync自体を数秒で終える方針は同じです。全般的な実装はWindowsサービスの作り方と運用を参照してください。
7. 再起動後の復帰 ── 登録・復元データ・サインインをそろえる
7.1. RegisterApplicationRestartは、復帰経路の事前登録
装置PCや無人運転では、保存して終了するだけでなく、再起動後に業務を再開するところまでを設計します。RegisterApplicationRestartは、クラッシュ(未処理例外)、応答なし、更新によるアプリ再起動、更新によるOS再起動の各場面で、アプリを再起動対象として登録するAPIです。10
再起動時のコマンドライン引数に、開いていたファイルや復元ポイントを指定できます。ただし、登録するだけであらゆる状況から自動復帰するわけではありません。
| 確認項目 | 条件・制約 |
|---|---|
| 登録する時点 | 問題が起きる前。更新シナリオではWM_QUERYENDSESSION処理中が最後の機会 |
| 再起動ループ防止 | 起動から60秒未満のプロセスは再起動されない |
| クラッシュ・ハング | 利用者の同意を経て再起動する。更新による再起動は自動 |
| OS再起動をまたぐ場合 | 指示側がEWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS付きでシャットダウンAPIを呼ぶ必要がある |
| 昇格プロセス | 自動再起動の対象にならないため、別の明示的な起動経路が必要 |
昇格が必要なアプリでは、UIを標準権限にして特権処理をサービスへ分離するか、タスクスケジューラの「最上位の特権で実行」タスクなどを使って復帰経路を設計します。10
7.2. 回復コールバックとARSOは、それぞれ役割が違う
RegisterApplicationRecoveryCallbackを併用すると、クラッシュ時にWER(Windowsエラー報告)から回復コールバックが呼ばれ、作業中データを保存できます。保存が続く間は、登録したping間隔内にApplicationRecoveryInProgressを呼び、完了時にApplicationRecoveryFinishedを通知します。進捗通知が途切れれば回復処理は打ち切られ得ます。
flowchart TB
accTitle: 回復コールバックでの保存と進捗通知
accDescr: WERに呼ばれた回復コールバックは保存中にApplicationRecoveryInProgressをping間隔内で呼び続け、完了時にApplicationRecoveryFinishedを通知する
reg["回復コールバックを事前登録"] --> crash["クラッシュ・WERが呼び出す"]
crash --> save["作業中データを保存"]
save --> progress["ping間隔内に進捗を通知"]
progress --> completed{"保存を完了したか"}
completed -->|"まだ"| save
completed -->|"完了"| done["回復完了を通知"]
progress -.-> timeout["途切れれば打ち切られ得る"]
図13: 保存するだけでなく、進捗と完了をWERへ通知する。
更新時の使用中ファイルの差し替えと再起動はRestart Managerの役割です。使用中のexe/DLLをどう差し替えるかで扱っています。
一方、OS再起動後にユーザーセッションを戻す仕組みがARSO(Winlogon自動再起動サインオン)です。Windows Updateが自動再起動を開始するとき、最後の対話ユーザーの資格情報を安全に保存してAutologonを構成し、再起動後にそのユーザーをサインインさせて画面をロックします。11
flowchart TB
accTitle: 再起動登録とサインインとデータ復元
accDescr: 事前の再起動登録に対しクラッシュでは利用者の同意が必要であり、OS再起動後のユーザーアプリにはARSOなどによるセッション復帰と保存済み状態の復元が必要になる
reg["問題発生前に再起動登録"] --> crash["クラッシュ・応答なし"]
crash --> consent["利用者の同意を得る"]
consent --> app["アプリを再起動"]
reg --> update["必要なフラグ付きOS再起動"]
update --> session["ARSO等でセッションを戻す"]
session --> app
app --> data["保存した復元ポイントを読む"]
session -.-> policy["ポリシーと起動条件を確認"]
図14: アプリの再起動登録だけでなく、セッションと作業状態が戻る経路も用意する。
shutdown /gは、再起動と登録アプリの再開を指示するコマンドです。ARSOはDisableAutomaticRestartSignOnなどの組織ポリシーで無効になっている場合もあるため、無人復帰の要件と併せて確認します。常時必要なバックグラウンド処理は、ユーザーの自動サインインに頼るより、Windowsサービスとして動かすほうが適しています。11
8. 通知のない電源断 ── 保存と読み込みを対で設計する
8.1. 一時ファイル・差し替え・バックアップ・起動時検証を一組にする
ブレーカー断、電源ユニット故障、プラグ抜けには、WM_ENDSESSIONもPRESHUTDOWNもありません。元のファイルへ直接上書きしていると、途中で切れた際に新旧の内容が混ざったファイルが残り得ます。
基本は、同じボリューム上の一時ファイルへ書き切り、フラッシュしてから差し替えることです。ReplaceFileは、新ファイルへの保存、元ファイルの退避、リネーム、削除に相当する手順をまとめ、作成日時、ACL、代替ストリームなどの属性も引き継ぎます。置換対象・置換ファイル・バックアップは同一ボリュームに置く必要があります。.NETのFile.ReplaceがこのAPIを呼びます。12
flowchart TB
accTitle: ファイル保存と次回起動の復旧
accDescr: 同じボリュームの一時ファイルへ書いてフラッシュしバックアップを残して差し替えた後、起動時に本体を検証し必要ならバックアップへ戻す
tmp["同一ボリュームの一時ファイル"] --> write["最後まで書きフラッシュ"]
write --> replace["差し替え・旧内容をbakへ"]
replace -.-> boot["次回起動"]
boot --> valid{"本体は正常か"}
valid -->|"はい"| main["本体を読む"]
valid -->|"いいえ"| backup["バックアップへ戻す"]
図15: 安全な書き方だけでなく、壊れていた場合の読み込み方も一緒に実装する。
// 設定・データ保存の定石: 一時ファイルに書き切ってから差し替え、旧内容も残す
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;
}
}
この例の名前がSaveAtomicallyでも、電源断をまたぐ原子性まで保証するものではありません。 通常の運用では完全な旧ファイルか新ファイルを読める状態にできますが、ReplaceFileは複数ステップの名前空間操作であり、突然の電源断に対する原子性は仕様上保証されていません。だからこそ.bakを残し、起動時に本体を検証して、壊れていればバックアップへ戻す読み込み処理までを実装します。12
例のcatchは、通常の失敗時に一時ファイルが蓄積してボリュームを埋めないためのものです。電源断時にこの処理が走ることを期待するものではありません。追記型のログやCSVにはファイル全体の差し替え方式をそのまま使わず、「1行を1レコードとして書き、壊れた末尾行を読み込み時に捨てる」など、壊れ方を織り込んだ形式にします。
8.2. WriteFileの成功と、ディスクへの到達を区別する
WriteFileが成功しても、データはまだOSのキャッシュにいる可能性があります。Windowsは通常、システムバッファーへ書き込み、遅延書き込みでディスクへ反映します。重要な節目ではFlushFileBuffersでフラッシュするか、CreateFile時のFILE_FLAG_WRITE_THROUGHで即時の書き込みを指定します。ファイルシステムのメタデータもキャッシュされるため、確定にはフラッシュやwrite-throughが関わります。13
ただし、write-throughと「OSのキャッシュを使わないこと」は同義ではありません。頻繁なFlushFileBuffersは非効率であり、必要に応じてFILE_FLAG_NO_BUFFERINGとの併用を検討します。実務では、トランザクションの節目やファイルを閉じる直前など、保全上重要な場所でフラッシュする設計が現実的です。13
flowchart TB
accTitle: 書き込み成功と永続化の境界
accDescr: 通常の書き込みはOSキャッシュから遅延してストレージへ届き、フラッシュやwrite-throughで確定を促してもデバイス側キャッシュの制約は残る
write["WriteFileの成功"] --> cache["OSキャッシュにある可能性"]
cache --> delayed["遅延書き込み"]
cache --> flush["節目のフラッシュ"]
delayed --> device["ストレージへ反映"]
flush --> device
device -.-> limit["デバイス側キャッシュの限界"]
図16: APIの成功、OSキャッシュの確定、電源断への耐性を同じものとして扱わない。
デバイス側の揮発キャッシュにも限界があり、「フラッシュしたから、どのハードウェアでも電源断に完全に耐える」とは言い切れません。キャッシュマネージャー、遅延書き込み、ハードウェアキャッシュの関係は、キャッシュマネージャー:あなたのWriteFileはいつディスクに届くのかで詳しく説明しています。
8.3. UPSは、電源断を計画シャットダウンへつなぐために使う
UPSの役割は停電をなくすことではなく、通知のない電源断を、通知のある計画シャットダウンへ変えることです。必要な条件は次の関係です。
UPSのバッテリー保持時間 > 切り替わりの検知 + アプリ・サービスの後始末 + OSのシャットダウン完了までの時間
AC電源とバッテリーの切り替わりや残量低下は、PBT_APMPOWERSTATUSCHANGEで通知されます。GUIはWM_POWERBROADCAST、サービスはSERVICE_ACCEPT_POWEREVENTを宣言したうえでHandlerExのSERVICE_CONTROL_POWEREVENTを受けます。WM_POWERBROADCASTがサービスの制御ハンドラーへ届くわけではありません。14
受信後にGetSystemPowerStatusでACLineStatusやBatteryLifePercentを確認し、計測の中断、保存、シャットダウン要求へつなぎます。14
flowchart TB
accTitle: UPSの検知からシャットダウン完了まで
accDescr: UPSのバッテリーへの切り替わりを適切な通知経路で検知し電源状態を確認して保存とシャットダウンへ進み、全体がバッテリー保持時間内に収まるよう設計する
outage["停電・UPSがバッテリーへ"] --> notice["対応する経路で電源通知"]
notice --> check["電源状態と残量を確認"]
check --> save["計測中断・保存"]
save --> req["OSにシャットダウン要求"]
req --> done["後始末・OS終了を完了"]
done -.-> time["保持時間内に全体を収める"]
図17: アプリだけでなく、OSが終了するまでの時間をUPSの保持時間に収める。
USB接続の一般的なUPSなど、Windowsからバッテリーとして見える構成では、標準APIで監視できます。ベンダー管理ソフトが「残量N%でシャットダウンする」機能を持つ場合は、そのしきい値と後始末時間の整合も確認します。スリープ・休止からの復帰は別の軸の話なので、スリープ・休止・Modern Standbyと長時間稼働アプリを参照してください。
9. 検証 ── 通知・時間・復旧結果の3つを確かめる
9.1. 本番ではなく、検証機で終了経路を再現する
本番の装置PCでいきなり試さず、検証機やHyper-Vなどの仮想マシンを使います。仮想マシンでは事前にチェックポイントを取り、失ってよい検証データで繰り返します。
| 試す操作 | 確認すること |
|---|---|
| サインアウト | GUIのWM_QUERYENDSESSION→WM_ENDSESSIONと保存処理。サービス停止の検証の代わりにはならない |
shutdown /s /t 0 |
完全シャットダウン時の動作 |
shutdown /s /hybrid /t 0 |
高速スタートアップを使う構成でのハイブリッド動作 |
shutdown /r /t 0 |
完全ブートを伴う再起動と、その後の復帰 |
| VMの電源オフ | ゲストOSが通知なく停止しても、次回起動時に復旧できるか |
| 本番相当の実機での電源断 | 物理ストレージやコントローラーを含めた耐性 |
サインアウトではENDSESSION_LOGOFFビットが立つ違いがありますが、GUIの通知経路を手軽に確認できます。完全・ハイブリッド・再起動のコマンドは混同せず、別々に試します。15
flowchart TB
accTitle: シャットダウン検証を段階的に広げる
accDescr: 検証環境でGUI通知と各終了操作を試し、突然停止後の復旧をVMで確認してから本番相当の実機でストレージを含む電源断耐性を確認する
prep["検証機・データを準備"] --> notify["通知経路と終了操作を試す"]
notify --> time["後始末の時間を測る"]
time --> vm["VMの突然停止で復旧を確認"]
vm --> real["実機のストレージ込みで検証"]
real --> check["次回起動のデータと復帰を確認"]
図18: 正常終了できるかだけでなく、突然止まった後に何を復旧できるかも試す。
VMの電源オフで再現できるのは、ゲストが予告なく止まることまでです。物理ディスクの揮発キャッシュ消失やコントローラー依存の壊れ方は再現できないため、装置PCとして出荷する場合の最終確認は、本番相当のハードウェアを使います。
後始末関数の先頭・末尾には時刻のログを残し、GUIなどの約5秒、またはサービスに構成した猶予に収まるかを実測します。正常に終了したかだけでなく、次回起動で何が読み込まれ、どこから再開できたかまで確認します。
9.2. 夜間に何が起きたかは、イベントログから切り分ける
WindowsのSystemログでは、イベントID 1074に、シャットダウンを開始したプロセス、利用者、理由が記録されます。予期しないシャットダウンでは、次回起動時に41(Kernel-Power)や6008が記録されます。15
# シャットダウン関連イベントの直近履歴を確認する
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
Select-Object TimeCreated, Id, ProviderName, Message |
Format-List
1074がWindows Updateの再起動を示しているのにデータが壊れていたなら、まず終了通知の処理と保存経路を調べます。一方、41や6008だけで電源断と断定してはいけません。これらが示すのは予期しない終了であり、ブルースクリーンや強制リセットも候補です。15
flowchart TB
accTitle: 終了イベントログから調査対象を選ぶ
accDescr: 1074から開始プロセスと理由を確認し、41や6008は予期しない終了の手掛かりとしてバグチェックコードやダンプなど周辺情報と突き合わせる
log["Systemイベントログ"] --> normal["1074・開始プロセスと理由"]
normal --> cleanup["通知処理と保存経路を調査"]
log --> unexpected["41・6008・予期しない終了"]
unexpected --> evidence["BugcheckCode・ダンプを確認"]
evidence --> classify["クラッシュ・電源断等を切り分け"]
図19: 41や6008は電源断そのものの証明ではなく、追加調査への入口である。
41のBugcheckCodeが0以外ならクラッシュの手掛かりです。0でメモリダンプもなければ電源断が疑われますが、周辺情報と突き合わせて判断します。電源断と分かれば8章の保存設計とUPSを、計画された終了なら3〜6章の通知・後始末を重点的に調べます。
10. まとめ ── 終了処理だけでなく、次回起動までを設計する
シャットダウン対策は、終了時に一度だけ動くイベントハンドラーの話ではありません。日頃の保存 → 短い後始末 → 次回起動時の検証と復旧を、一続きにしておくことが重要です。
| 見直す場所 | 設計の要点 |
|---|---|
| 普段の処理 | こまめに保存し、終了時に残る差分を減らす |
| GUIの終了通知 | 問い合わせには原則即TRUE。冪等な保存と、確定後の後始末を分ける |
| コンソール・サービス | アプリモデルに合う通知を使う。.NETのProcessExitや猶予延長だけに頼らない |
| 中断できない処理 | 必要な間だけ理由登録と拒否を組み合わせる。強制続行にも備える |
| 保存と読み込み | 一時ファイル・フラッシュ・差し替えに加え、バックアップと起動時検証を用意する |
| 再起動後 | 再起動登録、復元データ、サインインやサービスの起動経路を確認する |
高速スタートアップが有効なシャットダウンでは、カーネルやドライバーが休止から戻る場合があります。不調の切り分けでは「再起動」を明記し、アプリ側は完全シャットダウンとハイブリッドのどちらでも動くようにします。5
flowchart TB
accTitle: 終了対応から復旧までの最終確認
accDescr: 通常処理で保存済み状態を保ち、終了通知では小さな後始末を行い、通知なしでも残るデータを検証して次回起動で復旧する
daily["普段から保存済み状態を保つ"] --> endq{"終了通知があるか"}
endq -->|"ある"| short["短い後始末で終了"]
endq -->|"ない"| prior["最後の保存済み状態が頼り"]
short --> nextboot["次回起動で検証・復旧"]
prior --> nextboot
nextboot --> restart["必要な条件で業務へ復帰"]
図20: 終了イベントの実装を、日頃の保存と次回起動の復旧から切り離さない。
最後に、検証機で通知経路と所要時間を試し、突然停止した後の復旧まで確かめます。事後の調査では1074 / 41 / 6008を手掛かりに、計画された終了か、予期しない終了かを切り分けます。
次に機能を追加するときは、「この処理中に終了通知が来たら、あるいは電源が抜かれたら、次回起動時に何が残るか」を確認してください。その答えを設計に含めることが、朝になって初めてデータ破損に気付く事故への備えになります。
関連記事
- 使用中のexe/DLLをどう差し替えるか ── Restart Managerと自動更新の「ファイル使用中」問題
- Windowsサービスの作り方と運用 ── タスクスケジューラとの使い分けからBackgroundServiceのサービス化まで
- スリープ・休止・Modern Standbyと長時間稼働アプリ ── 「夜中に止まっていた」を設計で防ぐ
- Windows I/Oの深層(第4回) ── キャッシュマネージャー:あなたのWriteFileはいつディスクに届くのか
- Windowsアプリのクラッシュ時にログとダンプを残す設計
- Windowsアプリで子プロセスを安全に扱うチェックリスト
関連する相談領域
合同会社小村ソフトでは、装置PC・長期稼働アプリのシャットダウン/電源断対策の設計と実装、Windows Updateの再起動やサインアウトを起点とするデータ破損・「朝止まっていた」障害の原因調査、Windowsサービスの停止処理・自動復帰まわりの設計レビューを扱っています。「シャットダウンのたびに何かが壊れる気がするが、どこから手を付ければいいか分からない」という段階からで構いません。
参考リンク
-
Microsoft Learn, WM_QUERYENDSESSION message. セッション終了時にWM_QUERYENDSESSIONが送られ、アプリはTRUEを返してユーザーの意図を尊重すべきこと(DefWindowProcの既定もTRUE)、後始末はWM_ENDSESSIONまで遅延すべきこと、5秒後にシステムがシャットダウンを妨げているアプリのUIを表示しユーザーが強制終了できること、lParamのENDSESSION_LOGOFF/CLOSEAPP/CRITICALビットの意味、シャットダウンと再起動は区別できないこと、データをこまめに保存して終了時の保存量を減らすべきことについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
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 ↩4
-
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 ↩6 ↩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 ↩3
-
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
-
Microsoft Learn, Shutdown Changes for Windows Vista. WM_QUERYENDSESSION/WM_ENDSESSIONへの応答を遅らせられるのは各5秒でその後はユーザーが続行・中止を選べること、コンソールアプリや可視ウィンドウのないアプリはシャットダウンを中止できず5秒無応答またはFALSE応答で自動終了されること、ブロックが必要ならShutdownBlockReasonCreateで理由を登録すべきこと、アプリはシャットダウンをブロックできることに依存してはならないことについて。 ↩ ↩2 ↩3
-
Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). 中断できない処理の開始時に呼んで理由文字列を登録し、完了時にShutdownBlockReasonDestroyを呼ぶこと、ウィンドウを作成したスレッドからしか呼べないこと、ユーザーは数秒しか理由を読まないため短く明確な文字列にすべきことについて。 ↩ ↩2 ↩3
-
Microsoft Learn, SetConsoleCtrlHandler function. gdi32.dllまたはuser32.dllを読み込んだプロセスはWindowsアプリとして扱われCTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENTのハンドラーが呼ばれないこと、その回避策として非表示ウィンドウを作成しWM_QUERYENDSESSION/WM_ENDSESSIONを処理すべきこと、シグナル処理中はコンソール関数が正常に動作しない場合があることについて。 ↩
-
Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). PRESHUTDOWN通知後にSCMがサービス停止またはタイムアウトまで待つこと、既定タイムアウトがWindows 10 Creators Update(ビルド15063)以降は10秒、それ以前は3分であること、ChangeServiceConfig2で構成すること、SERVICE_STOP_PENDING中は状態更新を続けられることについて。 ↩ ↩2
-
Microsoft Learn, RegisterApplicationRestart function (winbase.h). クラッシュ・応答なし・更新・更新に伴うコンピューター再起動の各シナリオで再起動を登録できること、再起動時のコマンドライン引数を指定できること、登録は問題発生前に行い更新シナリオではWM_QUERYENDSESSION処理中が最後の機会であること、起動60秒未満のプロセスは再起動されないこと、クラッシュ・ハング時はユーザーの同意を経て再起動されること、OS再起動をまたぐにはEWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPSでのシャットダウンが必要なことについて。 ↩ ↩2
-
Microsoft Learn, Winlogon automatic restart sign-on (ARSO). Windows Updateが自動再起動を開始する際に最後の対話ユーザーの資格情報を保存してAutologonを構成すること、再起動後にユーザーを自動サインインさせセッションをロックすること、サインイン成功後に保存された資格情報が削除されること、グループポリシー(DisableAutomaticRestartSignOn等)で構成できることについて。 ↩ ↩2
-
Microsoft Learn, ReplaceFileW function (winbase.h). ReplaceFileが「新ファイルへの保存・元ファイルの一時リネーム・新ファイルのリネーム・元ファイルの削除」に相当する複数の手順を1つの関数にまとめること、作成日時・DACL・暗号化・圧縮・名前付きストリームなど元ファイルの属性を保持すること、バックアップ・置換対象・置換ファイルが同一ボリューム上にある必要があることについて。 ↩ ↩2
-
Microsoft Learn, File Caching. 書き込みが既定でシステムキャッシュに載り遅延書き込みでディスクへ反映されること、FILE_FLAG_WRITE_THROUGHで書き込みを即時にディスクへ書くこと、FlushFileBuffersで明示的にフラッシュできること、ファイルシステムのメタデータは常にキャッシュされるためメタデータの確定にはフラッシュまたはwrite-throughが必要なことについて。 ↩ ↩2
-
Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. バッテリーとAC電源の切り替わりや残量低下時にWM_POWERBROADCASTでこのイベントが通知されること、受信したらGetSystemPowerStatusを呼びSYSTEM_POWER_STATUSのACLineStatus・BatteryFlag・BatteryLifePercent等を確認すべきことについて。 ↩ ↩2
-
Microsoft Learn, Troubleshoot unexpected reboots using system event logs. 正常な再起動ではイベントID 1074(どのプロセスが誰のためにどんな理由でシャットダウンを開始したか)が記録されること、予期しない再起動ではイベントID 41(Kernel-Power)と6008(直前のシャットダウンは予期されていなかった)が記録されること、これらのIDで再起動の種類を切り分けられることについて。 ↩ ↩2
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
高速スタートアップの正体 ── Windowsの「シャットダウン」が再起動と違う理由
Windowsの「シャットダウン」は既定でハイブリッドシャットダウンになり、カーネルとドライバーは休止ファイルへ保存され次回起動で復元されます。再起動でしか直らない理由、稼働時間・更新・Wake on LANへの影響、確認方法と無効化の判断を解説します。
ネットは使えるのに「インターネットなし」?── WindowsのNCSI・DNS・プロキシ・VPNを切り分ける
ネットは使えるのにWindowsが「インターネットなし」と表示する理由を、NCSIの接続判定から解説します。DNS・プロキシ・VPN・認証Wi-Fiを、設定変更前の確認コマンドとイベントログで切り分けます。
Time Travel Debugging ── 長期稼働で再現しない不具合を「録画」して巻き戻す
月に一度しか出ない不具合は、クラッシュダンプでは結果しか写りません。WinDbgのTime Travel Debugging(TTD)で実行を録画して巻き戻す方法を、TTD.exeの録画設計、リングバッファ、TTD.Callsクエリ、ダンプとの使い分けまで解説します。
引数はなぜ壊れるか ── Windowsのコマンドライン引数の規則
Windowsでは引数の配列は存在せず、CreateProcessに渡るのは1本の文字列で、分割は受け取り側が行います。CommandLineToArgvW・CRT・.NETの分割規則と、.NETのArgumentList・C++での正しい組み立て方を解説します。
Windowsのプリンタードライバー終了 ── 業務アプリの帳票・ラベル印刷はどう備えるか
Microsoftはv3/v4プリンタードライバーの提供終了を段階的に進めており、2026年7月からはIPPクラスドライバーが優先されます。Windows protected print modeで何が消えるか、業務アプリの帳票・ラベル印刷の依存箇所の棚卸しと備え方を判断表...
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- 「シャットダウン」では直らなかった不具合が、「再起動」したら直りました。なぜですか?
- 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を併用し、バッテリー駆動への切り替わりを検知して安全にシャットダウンへつなげる構成が定石です。