VB.NETでWindowsサービスを安全に停止する方法|ServiceController.Stop()前に確認すべきCanStopとStatusのベストプラクティス

VB(.NET)でWindowsサービスを安全に停止する際に、「ServiceController.Stop() の前に CanStop と Status を両方確認すべきか?」という疑問は実務で必ず出会います。本記事では結論から実装、落とし穴、例外・リトライ・依存関係までを網羅し、コピペで使えるVBコードと共に、運用現場で壊れにくい停止処理のベストプラクティスを詳解します。

目次

結論:Stop() 前は「CanStop かつ Status = Running」を同時に確認する

停止の成否は「停止可能性」と「いま本当に動いているか」の掛け算です。したがって、CanStop = True かつ Status = ServiceControllerStatus.Running のときだけ Stop() を呼ぶのが安全な最小条件です。これにより、無意味な呼び出し(既に停止済み・停止処理中など)や、停止を受け付けないサービスへの誤操作を避けられます。

Using sc As New ServiceController("BlahService")
    sc.Refresh() ' 最新状態を取得
    If sc.CanStop AndAlso sc.Status = ServiceControllerStatus.Running Then
        sc.Stop()
        sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(30))
    End If
End Using

逆条件の罠:「Status <> Running を条件に含める」は誤り

Status <> Running を条件に入れると「動いていないサービスだけ止める」という逆の意味になります。停止呼び出しは「実行中」だけが意味を持つ操作なので、Status の判定は同値(=)で行ってください。

なぜ両方チェックが必要か(設計の背景)

  • 能力の有無:CanStop はサービスが「停止制御を受け付けるか」を表す能力フラグ。ドライバ型や明示的に停止不可にしているサービスは False になります。
  • 時点の状態:Status は「今どうなっているか」を表す実行時ステータス。Running/Stopped/StopPending などの遷移中状態もあります。
  • 冪等性の確保:「動いてもいないのに止める」処理は無意味で、環境により例外や待ち時間の無駄につながります。
  • 誤判定の回避:Status はキャッシュされ得るため、直前に Refresh() を呼び最新化するのが安全です。

ServiceController が扱う状態一覧と意味

状態意味停止呼び出しの扱い備考
Running実行中許可(推奨)この状態のみ明示的に Stop() を呼ぶ
Stopped停止済み不要呼んでも意味がない
StopPending停止処理中不要WaitForStatus(Stopped) で待機
StartPending起動処理中基本は呼ばない競合回避のため完了を待つ
PausePending一時停止へ遷移中呼ばない状態確定まで待つ
ContinuePending再開へ遷移中呼ばない状態確定まで待つ
Paused一時停止中(実装次第で可だが本記事では推奨しない)安全策として「Runningのみ停止」の原則を維持

すぐ使える:堅牢な停止関数(VB.NET)

実務で使いやすいように、タイムアウト・依存関係・例外処理・ログフックを含む停止関数の雛形を提示します。

Option Strict On
Option Infer On
Imports System.ServiceProcess
Imports System.ComponentModel

Public Module ServiceOps

    ''' &lt;summary&gt;
    ''' Windowsサービスを安全に停止する。
    ''' ・CanStop かつ Status = Running のときのみ Stop を呼ぶ
    ''' ・StopPending は完了まで Wait
    ''' ・依存サービスを先に停止(任意)
    ''' ・例外は呼び出し側に伝播(必要に応じてCatchで再スロー)
    ''' &lt;/summary&gt;
    Public Function StopServiceSafely(
        serviceName As String,
        Optional machineName As String = ".",
        Optional timeout As TimeSpan = Nothing,
        Optional stopDependencies As Boolean = True,
        Optional onLog As Action(Of String) = Nothing
    ) As Boolean

        If timeout = TimeSpan.Zero Then
            timeout = TimeSpan.FromSeconds(30)
        End If

        Using sc As New ServiceController(serviceName, machineName)

            ' 最新状態へ
            sc.Refresh()
            onLog?.Invoke($"[{serviceName}] 初期状態: {sc.Status}")

            ' 依存サービス(このサービスに依存する子)を先に停止
            If stopDependencies Then
                For Each child In sc.DependentServices
                    Using c As New ServiceController(child.ServiceName, machineName)
                        c.Refresh()
                        If c.Status = ServiceControllerStatus.Running AndAlso c.CanStop Then
                            onLog?.Invoke($"依存停止: {c.ServiceName}")
                            c.Stop()
                            c.WaitForStatus(ServiceControllerStatus.Stopped, timeout)
                        ElseIf c.Status = ServiceControllerStatus.StopPending Then
                            onLog?.Invoke($"依存停止待ち: {c.ServiceName}")
                            c.WaitForStatus(ServiceControllerStatus.Stopped, timeout)
                        End If
                    End Using
                Next
            End If

            ' 自サービスの状態別にハンドリング
            sc.Refresh()
            Select Case sc.Status
                Case ServiceControllerStatus.Stopped
                    onLog?.Invoke("既に停止済み。何もしません。")
                    Return True

                Case ServiceControllerStatus.StopPending
                    onLog?.Invoke("停止進行中のため完了まで待機。")
                    sc.WaitForStatus(ServiceControllerStatus.Stopped, timeout)
                    Return True

                Case ServiceControllerStatus.Running
                    If Not sc.CanStop Then
                        Throw New InvalidOperationException("このサービスは停止を受け付けません。")
                    End If
                    onLog?.Invoke("Stop を送信します。")
                    sc.Stop()
                    sc.WaitForStatus(ServiceControllerStatus.Stopped, timeout)
                    Return True

                Case ServiceControllerStatus.StartPending,
                     ServiceControllerStatus.ContinuePending,
                     ServiceControllerStatus.PausePending
                    onLog?.Invoke($"状態遷移中({sc.Status})のため、安全のため状態確定まで待機。")
                    ' 待ち合わせポーリング
                    Dim endAt = DateTime.UtcNow + timeout
                    Do
                        sc.Refresh()
                        If sc.Status = ServiceControllerStatus.Running OrElse
                           sc.Status = ServiceControllerStatus.Stopped OrElse
                           sc.Status = ServiceControllerStatus.Paused Then
                            Exit Do
                        End If
                        Threading.Thread.Sleep(500)
                    Loop While DateTime.UtcNow &lt; endAt

                    ' 改めて判定
                    Return StopServiceSafely(serviceName, machineName, timeout, stopDependencies, onLog)

                Case ServiceControllerStatus.Paused
                    ' 方針:Running のみ停止を許可(本記事の原則)
                    onLog?.Invoke("Paused のため Stop は呼びません(運用方針)。")
                    Return False
            End Select
        End Using

        Return False
    End Function
End Module

この関数は「原則:Running のときだけ Stop()」を守りつつ、遷移中や依存関係・再入にも強い実装になっています。ログ出力は onLog デリゲートに渡すことで、任意のロガー(EventLog、Console、ファイル等)へ差し替え可能です。

WaitForStatus の使い方とタイムアウト戦略

  • 必ずタイムアウトを与える:サービス側の停止が長引く可能性は常にあります。WaitForStatus(Stopped, TimeSpan) を使い、運用で許容できる上限時間(例:30〜120秒)を設定しましょう。
  • 例外の扱い:タイムアウト時は TimeoutException、無効な状態遷移等は InvalidOperationException、OSエラーは Win32Exception が発生し得ます。上位で捕捉してログ・リトライ・フォールバックに繋げます。
  • StopPending を待つ:Stop() 直後は StopPending に遷移することが多いので、Stopped まで待ち切るのが安全です。

Refresh() を忘れない:キャッシュ更新の重要性

ServiceController.Status は内部キャッシュを読むことがあり、状態判定前に Refresh() を呼ぶことで誤検知(例:直前に別プロセスが停止させた)を防げます。特にループ・再試行・分岐の直前には Refresh() を入れて最新化するのが定石です。

依存サービスの正しい止め方

あるサービス A を停止するには、A に依存しているサービス群(DependentServices)を先に止める必要があります。順序を誤ると Win32Exception(依存関係があるため停止不可)となります。前掲の関数では子サービスを先行停止し、各子に対しても CanStop と Status=Running を確認しています。

依存関係停止のポイント

  • 子サービスから先に停止、親は最後。
  • 各子の停止も StopPending を待ち切る。
  • 停止不可(CanStop=False)な子がある場合は、運用上の手順(タスクスケジューラ停止・キューのドレイン等)を事前に検討。

例外・エラーの整理(発生しやすいものに集中)

例外代表ケース対処
System.ComponentModel.Win32Exceptionアクセス拒否(権限不足)/依存関係あり/サービス応答なし管理者権限で実行/依存停止を先行/ログ収集と手動手順へフォールバック
System.TimeoutExceptionWaitForStatus が許容時間を超過タイムアウト延長/停止前の前処理(接続切断・リクエスト停止)/再試行
System.InvalidOperationExceptionCanStop=False なのに Stop()/無効な状態遷移事前チェックの徹底/状態確定まで待ってから再判定

アンチパターンとベストプラクティス

アンチパターンなぜ問題か正しい代替
If sc.Status <> Running Then sc.Stop()「動いていないときに止める」という逆の条件If sc.CanStop AndAlso sc.Status = Running Then sc.Stop()
Stop() 直後に Status を即参照通常は StopPending を経由、即時反映されないWaitForStatus(Stopped, timeout) で完了同期
Refresh() を呼ばない古いキャッシュで誤判定判定直前に Refresh() を必ず実行
依存サービスを考慮しない停止拒否・障害の連鎖DependentServices を先に停止し待機
無制限待ちや無限再試行バッチやジョブがハングタイムアウトとリトライ回数を設計で固定

リトライ・バックオフ・サーキットブレーカーの設計

停止は外部要因(接続・I/O・キュー滞留)に左右されやすい操作です。業務上の再現性を高めるために、以下のポリシーを推奨します。

  • 有限回リトライ:タイムアウト後に 1〜2 回まで再試行(各回で Refresh())。
  • 指数バックオフ:再試行間隔を 1秒 → 2秒 → 4秒と延伸。
  • サーキットブレーカー:一定回数失敗で以後の停止要求を遮断し、オペレータ対応に切り替える。

権限・運用上の前提

  • 実行権限:サービス制御には管理者権限が必要です。UAC昇格や実行アカウントの権限を確認してください。
  • 実運用前の手順化:停止前にロードバランサから切り離し、受信キューを塞ぐ・新規セッションを拒否するといった「前処理」をルール化すると停止が速く安定します。
  • 監査ログ:いつ・誰が・どのホストで停止したかを必ず記録。障害解析時の第一資料になります。

リモート停止とサービス名の取り扱い

ServiceController は MachineName を指定すればリモートのサービスも制御できます(資格情報・ファイアウォール設定は事前に整備)。また、サービス名(ServiceName)と表示名(DisplayName)は別物なので、正しい ServiceName を用いてください。

Using sc As New ServiceController(serviceName:="BlahService", machineName:="SRV-APP-01")
    sc.Refresh()
    If sc.CanStop AndAlso sc.Status = ServiceControllerStatus.Running Then
        sc.Stop()
        sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(60))
    End If
End Using

運用で効く監視・ログの具体例

  • 停止所要時間:Stop() から Stopped までの経過秒数を計測・記録。
  • 最終エラー:例外の種類・メッセージ・HResult(Win32エラーコード)を保存。
  • 依存連鎖:どの依存サービスの停止に時間を要したかを可視化。

現場でありがちな疑問と回答

Q. Paused のときに Stop() してもよい?

サービスの実装次第で可能な場合がありますが、運用設計としては「Running のときだけ止める」原則に寄せる方が安全です。Paused → Continue で Running に戻し、その後停止という手順を採用する組織もあります。

Q. Close() は必要?

Using ... End Using を使えば Dispose() が自動で呼ばれ、その内部で Close() も実行されます。明示的な Close() 呼び出しは不要です。

Q. Status の変化が読めない/反映が遅い

呼び出し直前の Refresh() と、Stop() 後の WaitForStatus(Stopped) を徹底しましょう。状態遷移中(*Pending)はポーリングで待つのが安定です。

チェックリスト(導入・保守で確認すべきこと)

  • 停止前に Refresh() を呼んで最新状態で判定しているか。
  • 条件は CanStop AndAlso Status = Running になっているか。
  • 停止後は WaitForStatus(Stopped, timeout) で完了同期しているか。
  • 依存サービスを先に止めるロジックを持っているか。
  • タイムアウト・リトライ・ログのポリシーがあるか。
  • 権限(昇格・サービス制御権限)が満たされているか。

サンプル:CLIユーティリティ化(簡易版)

上記関数を利用した簡易コマンドライン例です。運用の現場ツールに組み込むことで、手作業のばらつきを抑えられます。

Imports System
Imports System.ServiceProcess

Module Program
    Sub Main(args As String())
        If args.Length = 0 Then
            Console.Error.WriteLine("Usage: stopsvc &lt;ServiceName&gt; [timeoutSec]")
            Environment.Exit(1)
        End If

        Dim name = args(0)
        Dim timeout = TimeSpan.FromSeconds(If(args.Length &gt;= 2, Integer.Parse(args(1)), 30))

        Try
            Dim ok = ServiceOps.StopServiceSafely(
                serviceName:=name,
                machineName:=".",
                timeout:=timeout,
                stopDependencies:=True,
                onLog:=Sub(msg) Console.WriteLine($"{DateTime.Now:HH:mm:ss} {msg}")
            )
            Console.WriteLine(If(ok, "停止完了", "停止不要/条件不成立"))
        Catch ex As Exception
            Console.Error.WriteLine($"停止失敗: {ex.GetType().Name}: {ex.Message}")
            Environment.Exit(2)
        End Try
    End Sub
End Module

テスト観点:壊れにくさを検証する

  • 並行操作:別ツール(サービスマネージャ、SC.exe)が同時に操作するケースでも、例外なく安全に振る舞うか。
  • 遷移中連打:StartPending/StopPending に連続で遭遇した場合の安定性。
  • 長時間停止:アプリケーション側のシャットダウンが長引くケースでタイムアウトが適切に機能するか。
  • 依存関係の深さ:ネストした依存関係が複数段あるときに正しい順序で停止できるか。

まとめ:可観測性とガードを積み上げる

VB(.NET) で Windows サービスを止める処理は、一見シンプルに見えて実際は多くの落とし穴があります。CanStop と Status = Running の同時確認という基本を守り、Refresh() の徹底、WaitForStatus による同期、依存関係の先行停止、タイムアウトとログ設計を積み上げれば、運用現場で壊れにくい停止処理を実現できます。今日からあなたの停止ロジックを点検し、上記の雛形コードを基盤として自社の要件に合わせて強化していきましょう。


参考実装(短縮版:質問に対する最小回答)

最後に、冒頭の質問にそのまま答える最小コードを再掲します。
「停止可能かつ実行中」のときだけ Stop() を呼び、待ち合わせまで行います。

Using sc As New ServiceController("BlahService")
    sc.Refresh()
    If sc.CanStop AndAlso sc.Status = ServiceControllerStatus.Running Then
        sc.Stop()
        sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(30))
    End If
End Using

これに例外処理・依存関係・ログ・リトライを足すと、実務で長く使える「止める力の強い」実装になります。

この記事を書いた人

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

コメント

コメントする

目次