VB.NETでExitWindowsExのdwReasonを正しく指定して「その他(計画済み)」で再起動する方法

VB.NET から Windows を再起動・シャットダウンするとき、ExitWindowsExdwReason を正しく指定できているか自信が持てない……というケースは意外と多いです。本記事では、特に「その他(計画済み)」として再起動させるための理由コードの指定方法を、VB.NET の型や数値リテラルの落とし穴まで含めて丁寧に解説します。

日程Fit。無料・登録不要。「いつ空いてる?」を、ひとつのリンクで。リンクを送って、○△×でかんたん日程調整。無料で日程を作る。
目次

ExitWindowsEx と dwReason の基本を整理する

まずは、今回の主役である Win32 API ExitWindowsEx の全体像を整理します。VB.NET から P/Invoke で呼び出す前提で、パラメーターの意味と役割を押さえておきましょう。

パラメーター名概要
uFlagsUINT(VB.NET では UInteger再起動・シャットダウン・サインアウトなどの動作を指定するフラグ。
dwReasonDWORD(VB.NET では UIntegerシャットダウン/再起動の「理由」をビットフラグで指定する。

多くのサンプルコードでは dwReason0 を入れがちですが、サーバー運用やログ管理にこだわると、この理由コードをきちんと設定しておくことが重要になります。イベントログ上で「どんな理由で再起動されたのか」を判別できるからです。

「その他(計画済み)」で再起動したいケース

Windows Server やクライアント OS では、GUI からシャットダウン/再起動する際に「理由」を求められることがあります。たとえば次のような選択肢です。

  • ハードウェア:メンテナンス (計画済み)
  • オペレーティング システム:構成の変更 (計画済み)
  • アプリケーション:インストール (計画済み)
  • その他 (計画済み)

本記事で目指すのは、このうちの「その他(計画済み)」をプログラムから指定することです。つまり、イベントログ上で「Planned(計画済み)」扱いになり、かつ「Other(その他)」として記録されるように dwReason を組み立てることがゴールになります。

dwReason のフラグ構造を理解する

dwReason は「1つの数値」ですが、中身は複数のビットフラグを OR 結合したものです。概ね以下の 3 種類のグループから成り立っています。

  • メジャー理由(SHTDN_REASON_MAJOR_*
  • マイナー理由(SHTDN_REASON_MINOR_*
  • フラグ(SHTDN_REASON_FLAG_*
分類ビット位置のイメージ
メジャー理由SHTDN_REASON_MAJOR_OPERATINGSYSTEM など上位 16bit の一部
マイナー理由SHTDN_REASON_MINOR_UPGRADE など下位 16bit の一部
フラグSHTDN_REASON_FLAG_PLANNED など最上位ビット(0x80000000)など

「その他(計画済み)」を指定したい場合に関係する定数は次の 3 つです。

  • SHTDN_REASON_MAJOR_OTHER
  • SHTDN_REASON_MINOR_OTHER
  • SHTDN_REASON_FLAG_PLANNED

そして、実はこのうち SHTDN_REASON_MAJOR_OTHERSHTDN_REASON_MINOR_OTHER は、どちらも値が 0x00000000(ゼロ)です。そのため、実際にビットとして意味を持つのは SHTDN_REASON_FLAG_PLANNED だけとなります。

まとめると、「その他(計画済み)」で再起動したい場合の dwReason は、次の式と同じ結果になります。


SHTDN_REASON_MAJOR_OTHER Or
SHTDN_REASON_MINOR_OTHER Or
SHTDN_REASON_FLAG_PLANNED   ' => 実質 0x80000000

つまり、dwReason = &H80000000UI(UInt32 の最上位ビットが 1)であれば目的は達成できる、ということになります。

VB.NET での型と数値リテラルの落とし穴

ここからが VB.NET 特有のハマりポイントです。C 言語の 0x80000000 をそのまま VB.NET に持ち込むと、符号付き/符号なしの違いにより挙動が変わってしまう可能性があります。

Integer と UInteger の違い

VB.NET の Integer は 32bit の符号付き整数、UInteger は 32bit の符号なし整数です。

ビット数範囲備考
Integer32bit-2,147,483,648 ~ 2,147,483,647符号付き。0x80000000 は負数になる。
UInteger32bit0 ~ 4,294,967,295符号なし。0x80000000 も正の数として扱える。

0x80000000 は十進数にすると 2,147,483,648 です。これは Integer最大値を 1 だけ超えているため、Integer 型でそのまま表現しようとするとオーバーフロー扱いになったり、負数として扱われてしまいます。

そのため、Win32 API の DWORDUINT を扱うときは、VB.NET では基本的に UInteger を使うのが安全です。

&H80000000 と UI サフィックス

VB.NET で 16 進リテラルを書くときは &H を使いますが、型サフィックスを付けないと Integer として評価されてしまうことがあります。

書き方評価される型意味
&H80000000Integer(環境による)オーバーフローまたは負数として扱われる可能性。
&H80000000UIUInteger0x80000000 を正しい 32bit 符号なし整数として扱える。

必ず UI サフィックスを付けて &H80000000UI と書くようにしましょう。こうすることでコンパイラに「これは UInteger だよ」と明示できます。

VB.NET から ExitWindowsEx を呼び出す最小コード

ここまでのポイントを踏まえた上で、「その他(計画済み)」理由で Windows を再起動する VB.NET の最小サンプルコードを示します。


Imports System.Runtime.InteropServices

' --- P/Invoke 宣言 ---
<DllImport("user32.dll", SetLastError:=True)>
Private Shared Function ExitWindowsEx(uFlags As UInteger, dwReason As UInteger) As Boolean
End Function

' --- EWX フラグ ---
Public Const EWX_REBOOT As UInteger = &H2UI           ' 再起動

' --- 理由コード ---
Public Const SHTDN_REASON_MAJOR_OTHER As UInteger = &H0UI
Public Const SHTDN_REASON_MINOR_OTHER As UInteger = &H0UI
Public Const SHTDN_REASON_FLAG_PLANNED As UInteger = &H80000000UI

Public Sub RebootPlannedOther()
    Dim reason As UInteger =
        SHTDN_REASON_MAJOR_OTHER Or
        SHTDN_REASON_MINOR_OTHER Or
        SHTDN_REASON_FLAG_PLANNED   ' => 0x80000000

    Dim ok = ExitWindowsEx(EWX_REBOOT, reason)
    If Not ok Then
        Dim err = Runtime.InteropServices.Marshal.GetLastWin32Error()
        ' 例:1314 = 必要な特権がありません(SeShutdownPrivilege)
        ' ここでログ出力やエラーメッセージを表示するなどの処理を行う
    End If
End Sub

コードのポイント解説

  • uFlagsdwReason はどちらも UInteger で宣言。
  • EWX_REBOOT&H2UI として UInteger に。
  • SHTDN_REASON_FLAG_PLANNED&H80000000UI として最上位ビットを立てる。
  • SetLastError:=True を付けることで Marshal.GetLastWin32Error() でエラーコードを取得可能。

この最小コードをベースに、実環境ではログ出力やユーザーへの確認ダイアログなどを追加すれば、実務でも十分に通用する再起動処理になります。

SeShutdownPrivilege(シャットダウン特権)の罠

ExitWindowsEx を呼ぶときにもう一つ重要なのが、プロセスに SeShutdownPrivilege が付与されているかどうかです。これが無い状態で実行すると、関数は False を返し、GetLastError の結果が 1314(ERROR_PRIVILEGE_NOT_HELD) になります。

一番簡単な対策:アプリを「管理者として実行」する

もっとも手軽な方法は、アプリケーション自体を「管理者として実行」することです。UAC が有効な環境では、通常ユーザー権限と管理者権限とで扱える特権が変わるため、まずはこれを試すのが現実的です。

プログラムから特権を有効化する(応用編)

より踏み込んだ対応として、OpenProcessTokenAdjustTokenPrivileges を使って、実行中プロセスのトークンに対して SeShutdownPrivilege を有効化する方法もあります。少し長くなりますが、VB.NET での例を示します。


Imports System.Runtime.InteropServices
Imports System.Security.Principal

Public Class ShutdownHelper

    <DllImport("advapi32.dll", SetLastError:=True)>
    Private Shared Function OpenProcessToken(
        processHandle As IntPtr,
        desiredAccess As UInteger,
        ByRef tokenHandle As IntPtr
    ) As Boolean
    End Function

    <DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
    Private Shared Function LookupPrivilegeValue(
        lpSystemName As String,
        lpName As String,
        ByRef lpLuid As LUID
    ) As Boolean
    End Function

    <DllImport("advapi32.dll", SetLastError:=True)>
    Private Shared Function AdjustTokenPrivileges(
        tokenHandle As IntPtr,
        disableAllPrivileges As Boolean,
        ByRef newState As TOKEN_PRIVILEGES,
        bufferLength As Integer,
        previousState As IntPtr,
        returnLength As IntPtr
    ) As Boolean
    End Function

    <DllImport("kernel32.dll", SetLastError:=True)>
    Private Shared Function GetCurrentProcess() As IntPtr
    End Function

    <DllImport("kernel32.dll", SetLastError:=True)>
    Private Shared Function CloseHandle(hObject As IntPtr) As Boolean
    End Function

    <StructLayout(LayoutKind.Sequential)>
    Private Structure LUID
        Public LowPart As UInteger
        Public HighPart As Integer
    End Structure

    <StructLayout(LayoutKind.Sequential)>
    Private Structure LUID_AND_ATTRIBUTES
        Public Luid As LUID
        Public Attributes As UInteger
    End Structure

    <StructLayout(LayoutKind.Sequential)>
    Private Structure TOKEN_PRIVILEGES
        Public PrivilegeCount As UInteger
        Public Privileges As LUID_AND_ATTRIBUTES
    End Structure

    Private Const TOKEN_ADJUST_PRIVILEGES As UInteger = &H20UI
    Private Const TOKEN_QUERY As UInteger = &H8UI
    Private Const SE_PRIVILEGE_ENABLED As UInteger = &H2UI
    Private Const SE_SHUTDOWN_NAME As String = "SeShutdownPrivilege"

    Public Shared Function EnableShutdownPrivilege() As Boolean
        Dim hToken As IntPtr = IntPtr.Zero
        Try
            Dim hProcess = GetCurrentProcess()
            If Not OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES Or TOKEN_QUERY, hToken) Then
                Return False
            End If

            Dim luid As LUID
            If Not LookupPrivilegeValue(Nothing, SE_SHUTDOWN_NAME, luid) Then
                Return False
            End If

            Dim tp As New TOKEN_PRIVILEGES()
            tp.PrivilegeCount = 1
            tp.Privileges = New LUID_AND_ATTRIBUTES() With {
                .Luid = luid,
                .Attributes = SE_PRIVILEGE_ENABLED
            }

            If Not AdjustTokenPrivileges(hToken, False, tp, 0, IntPtr.Zero, IntPtr.Zero) Then
                Return False
            End If

            ' AdjustTokenPrivileges は成功しても GetLastError が
            ' ERROR_NOT_ALL_ASSIGNED になるケースがあるので注意。
            Return True
        Finally
            If hToken <> IntPtr.Zero Then
                CloseHandle(hToken)
            End If
        End Try
    End Function

End Class

このヘルパーを使えば、再起動前に次のように呼び出せます。


If Not ShutdownHelper.EnableShutdownPrivilege() Then
    ' 特権を取得できなかった場合の処理
End If

RebootPlannedOther()

実務では「常に管理者実行する」「必要なときだけ特権を上げる」など、運用ポリシーに合わせてどちらの方法を取るか検討しましょう。

再起動以外の EWX フラグの例

ExitWindowsExuFlags には、再起動以外にもさまざまな動作を指定できます。用途に応じて適切に選びましょう。

定数動作注意点
EWX_LOGOFF&H0UIログオフ(サインアウト)システムは継続動作。
EWX_SHUTDOWN&H1UIシャットダウン(電源オフの準備)実際の電源断には ACPI 対応が必要。
EWX_REBOOT&H2UI再起動今回の記事で使用。
EWX_POWEROFF&H8UI電源断ハードウェア側の対応が必要。
EWX_FORCE&H4UI強制終了保存されていないデータが失われる可能性。
EWX_FORCEIFHUNG&H10UI応答なしアプリのみ強制終了なるべくこちらを優先したい。

複数のフラグは OR 結合して使用します。たとえば「ハングしているアプリのみ強制終了しつつ再起動したい」場合は、次のように指定します。


Dim uFlags As UInteger = EWX_REBOOT Or EWX_FORCEIFHUNG
ExitWindowsEx(uFlags, reason)

他のシャットダウン理由コードのパターン例

「その他(計画済み)」以外にも、運用ポリシーに応じて理由を細かく分けたい場合があります。代表的なパターンをいくつか挙げておきます。

用途の例メジャー理由マイナー理由フラグ
OS のアップデート(計画済み)SHTDN_REASON_MAJOR_OPERATINGSYSTEMSHTDN_REASON_MINOR_UPDATESHTDN_REASON_FLAG_PLANNED
アプリケーションのインストール(計画済み)SHTDN_REASON_MAJOR_APPLICATIONSHTDN_REASON_MINOR_INSTALLATIONSHTDN_REASON_FLAG_PLANNED
ハードウェアメンテナンス(計画済み)SHTDN_REASON_MAJOR_HARDWARESHTDN_REASON_MINOR_MAINTENANCESHTDN_REASON_FLAG_PLANNED
システム障害による再起動(計画外)SHTDN_REASON_MAJOR_SYSTEMSHTDN_REASON_MINOR_BLUESCREENフラグなし(計画外)

イベントログ解析を自動化する場合は、これらの理由コードを自社向けの運用ルールとして整理し、どのケースでどの組み合わせを使うかを事前に決めておくと、後からログを分析しやすくなります。

イベントログで dwReason を確認する方法

dwReason が本当に期待どおりに記録されているかは、イベントビューアーで確認できます。

  1. 検索ボックスや「ファイル名を指定して実行」で eventvwr.msc を起動。
  2. 左ペインで 「Windows ログ」→「システム」 を選択。
  3. ソースが 「USER32」 となっているイベントを探す。
  4. 対象のイベントを開き、詳細タブやメッセージ内の「理由」や「Reason Code」を確認。

「その他(計画済み)」であれば、メッセージ内に “Planned” であることが示され、かつ分類が “Other” 相当になっているはずです。もし思った通りになっていなければ、dwReason の組み合わせや型定義を再確認してみてください。

よくあるトラブルとチェックリスト

実務でありがちなトラブルと、そのチェックポイントをまとめておきます。実装時のセルフレビューにも使えるチェックリストです。

症状原因の候補確認すべきポイント
再起動が実行されない特権不足、UAC、グループポリシー管理者として実行しているか、SeShutdownPrivilege が有効か。
イベントログの理由が「不明」になるdwReason が 0、または不正値dwReason の定数定義、型が UInteger になっているか。
コンパイルエラーや警告が出るリテラルの型不一致、オーバーフロー&H80000000UI のように UI サフィックスを付けているか。
一部のアプリが強制終了されるEWX_FORCE を安易に使用EWX_FORCEIFHUNG で代用できないか検討。

実務での設計上のポイント

単に API を叩くだけでなく、運用を意識すると次のような工夫が役立ちます。

ユーザーへの事前通知・猶予時間

たとえ「計画済み」の再起動であっても、ユーザー側から見ると「いきなり再起動された」と感じてしまうことがあります。そこで、次のような流れを組むとユーザー体験が向上します。

  • トレイアイコンやフォームで「xx 分後に再起動します」と告知。
  • カウントダウンを表示し、延期ボタンを用意。
  • 最終的に延期上限を超えたら ExitWindowsEx を呼ぶ。

このときも、内部的には dwReason に「その他(計画済み)」、または適切な理由コードを設定しておくと、後から「なぜ再起動されたのか」を説明しやすくなります。

ログ出力と監査対応

監査対応や障害調査を考えると、「アプリケーション自身のログ」と「Windows のイベントログ」の両方に再起動理由が残っていると理想的です。

  • アプリ側のログ:
    • 誰が(ユーザー名・端末名)
    • いつ再起動を要求したか
    • どの理由コードを使ったか(dwReason の値)
  • Windows 側のログ:
    • USER32 のイベントに理由コードが記録される
    • 他のシステムイベントと突き合わせ可能

このように多層的にログを残しておくと、数ヶ月後に「このとき再起動したのは誰の操作か?」といった質問が来ても説明しやすくなります。

まとめ:VB.NET での dwReason 設定の勘所

最後に、「その他(計画済み)」で再起動するためのポイントを簡単におさらいします。

  • 目的の理由コード
    • SHTDN_REASON_MAJOR_OTHER Or SHTDN_REASON_MINOR_OTHER Or SHTDN_REASON_FLAG_PLANNED
    • 実質的には 0x80000000&H80000000UI)を指定したのと同じ。
  • VB.NET での型
    • uFlagsdwReasonUInteger を使う。
    • 16 進リテラルには UI サフィックスを付ける(例:&H80000000UI)。
  • 特権
    • プロセスに SeShutdownPrivilege が必要。
    • 足りないと Marshal.GetLastWin32Error() が 1314(特権不足)になる。
  • 検証方法
    • イベントビューアーの「システム」ログ(ソース:USER32)を確認。
    • 理由コード・メッセージが想定どおりかをチェック。

これらを押さえておけば、「VB.NET から ExitWindowsEx を呼んだら、意図した理由でイベントログに記録されない」「特権不足で密かに失敗していた」といったトラブルをかなりの確率で防ぐことができます。Windows のシャットダウン/再起動を自動化するツールやメンテナンスバッチを作る際には、本記事のポイントをぜひ取り入れてみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次