【依存サービス自動カスケード再起動】WMI (`Win32_Service`) の依存関係解析に基づく安全な順序制御
開発現場のインフラ運用において、夜間バッチ処理の前後や、特定ミドルウェアのメモリリーク対策として、Windowsサービスの再起動を自動化したいという要求は枚挙に暇がない。
しかし、ここで多くのエンジニアが罠に落ちる。
「とりあえず `net stop` と `net start` をバッチファイルで順に叩けばいいだろう」――この安易な設計が、プロダクション環境でどれだけのシステム障害を引き起こしてきたことか。
Windowsのサービス群は、複雑な依存関係(Dependent Services)の網の巣の上で成り立っている。親サービスを無視して子サービスを強引に止めれば「依存関係のエラー」で弾かれ、逆に親を先に止めても、正しい順序で停止・起動を行わなければ、孤立したプロセスがリソースを掴んだままフリーズする。
今回は、VBScriptとWMI(Windows Management Instrumentation)を駆使し、サービス間の依存関係(`Win32_DependentService` や `Win32_Service`)を動的に解析。依存関係のトポロジカルソートに基づき、完全に安全な順序でカスケード停止・起動を行う「極限まで堅牢な運用自動化スクリプト」を伝授する。
—
なぜ「単純な順次実行」では破綻するのか?
多くのスクリプトは、静的な配列やテキストファイルにサービス名をハードコーディングしている。
‘ 【アンチパターン】ハードコーディングによる順序制御
arrServices = Array(“ServiceA”, “ServiceB”, “ServiceC”)
For Each svc in arrServices
‘ 停止処理…
Next
このアプローチが実務で通用しない理由は3つある。
1. 環境依存性の崩壊: 本番環境とステージング環境でインストールされているコンポーネントが異なり、ハードコーディングされた順序が破綻する。
2. 動的な依存関係の変化: Windowsパッチの適用やサードパーティ製ソフトの導入により、依存関係のツリーは常に変化する。静的な設定は数ヶ月で「技術的負債」と化す。
3. 非同期性(Async)の無視: サービス制御マネージャ(SCM)に停止命令を出した瞬間、スクリプト側は「命令が受理された」だけで、プロセスが完全に消滅したわけではない。状態遷移(Running → Stop Pending → Stopped)をポーリングで監視せず次に進むと、競合状態(Race Condition)により致命的なエラーを引き起こす。
我々は、WMIからライブの依存関係ツリーを取得し、厳密な状態遷移を監視しながら制御するアーキテクチャを構築しなければならない。
—
アーキテクチャ設計:WMIによる依存関係の解決
WMIの `Win32_DependentService` クラスを利用すると、サービス間の「Antecedent(先行/親)」と「Dependent(後続/子)」の関係性を抽出できる。
- 停止時(Shutdown): 子サービスを先に検出し、すべて停止させた後に親サービスを止める。
- 起動時(Startup): 親サービスを先に起動し、状態が完全に `Running` になったことを確認してから子サービスを起動する。
この「逆順停止・正順起動」を動的クエリで実現するプロダクションコードを以下に提示する。
—
プロダクションコード:`CascadeServiceManager.vbs`
以下のコードは、エラーハンドリング(`On Error Resume Next` の乱用禁止と厳密な `Err` オブジェクトの評価)、WMIの遅延バインド、サービス状態のポーリング監視を網羅した実用スクリプトである。
‘ ==============================================================================
‘ Script Name : CascadeServiceManager.vbs
C’ Description : 指定した起点サービスの依存関係をWMIで動的解析し、
‘ 安全な順序でカスケード再起動(停止・起動)を実行する
‘ ==============================================================================
Option Explicit
Const TimeoutSeconds = 60 ‘ 状態変化を待つ最大タイムアウト秒数
Dim targetServiceName
targetServiceName = “W3SVC” ‘ 例としてIIS (World Wide Web Publishing Service) を指定
WScript.Echo “=== 依存サービス カスケード再起動プロセスを開始します: ” & targetServiceName & ” ===”
‘ 1. 対象サービスの存在確認と停止処理
If ControlDependentServices(targetServiceName, “STOP”) = False Then
WScript.Echo “[FATAL] サービスの停止シーケンスで致命的なエラーが発生しました。”
WScript.Quit 1
End If
‘ 2. 冷却期間(必要に応じてプロセス解放を待つ)
WScript.Sleep 3000
‘ 3. 起動処理(依存関係の逆順、すなわち親から子へ)
If ControlDependentServices(targetServiceName, “START”) = False Then
WScript.Echo “[FATAL] サービスの起動シーケンスで致命的なエラーが発生しました。”
WScript.Quit 1
End If
WScript.Echo “=== すべての依存サービスの再起動が正常に完了しました。 ===”
WScript.Quit 0
‘ ==============================================================================
‘ メイン制御関数:依存関係を解決して停止・起動を振り分ける
‘ ==============================================================================
Function ControlDependentServices(rootSvcName, actionType)
Dim objWMIService, colServices, objService
Dim strQuery
Set objWMIService = GetObject(“winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2”)
‘ 起点サービスの取得
strQuery = “SELECT FROM Win32_Service WHERE Name = ‘” & rootSvcName & “‘”
Set colServices = objWMIService.ExecQuery(strQuery)
If colServices.Count = 0 Then
WScript.Echo “[ERROR] 指定されたサービスが見つかりません: ” & rootSvcName
ControlDependentServices = False
Exit Function
WScript.Echo “[ERROR] 指定されたサービスが見つかりません: ” & rootSvcName
ControlDependentServices = False
Exit Function
End If
If UCase(actionType) = “STOP” Then
‘ 停止時は「依存されている子」から先に止める必要があるため、再帰的に子を収集
WScript.Echo vbCrLf & “— [STOP SEQUENCE] 依存サービスの停止を開始 —”
ControlDependentServices = StopServiceCascade(objWMIService, rootSvcName)
ElseIf UCase(actionType) = “START” Then
‘ 起動時は「大本の親」から先に起動する
WScript.Echo vbCrLf & “— [START SEQUENCE] サービスの起動を開始 —”
ControlDependentServices = StartServiceCascade(objWMIService, rootSvcName)
End If
End Function
‘ ==============================================================================
‘ 再帰的 停止処理(子 -> 親の順)
‘ ==============================================================================
Function StopServiceCascade(objWMIService, svcName)
Dim colAssocs, assoc, childSvcName
Dim strQuery
‘ このサービスに依存している(このサービスを必要としている)子サービスを検索
‘ Win32_DependentService の Antecedent が親、Dependent が子
strQuery = “ASSOCIATORS OF {Win32_Service.Name='” & svcName & “‘} ” & _
“WHERE AssocClass=Win32_DependentService Role=Antecedent”
Set colAssocs = objWMIService.ExecQuery(strQuery)
‘ 先に子サービスをすべて止める
For Each assoc in colAssocs
childSvcName = GetServiceNameFromPath(assoc.Name)
‘ 再帰呼び出しにより末端の子まで潜る
Call StopServiceCascade(objWMIService, childSvcName)
Next
‘ 子がすべて止まったら、自分自身を停止
StopServiceCascade = SetServiceState(objWMIService, svcName, “Stop”)
End Function
‘ ==============================================================================
‘ 再帰的 起動処理(親 ->子の順)
‘ ==============================================================================
Function StartServiceCascade(objWMIService, svcName)
Dim colAssocs, assoc, childSvcName
Dim strQuery
‘ まず自分自身(親)を起動し、完全にRunningになるまで待機
If SetServiceState(objWMIService, svcName, “Start”) = False Then
StartServiceCascade = False
Exit Function
End If
‘ 親が起動完了したら、このサービスに依存している子サービスを検索して順次起動
strQuery = “ASSOCIATORS OF {Win32_Service.Name='” & svcName & “‘} ” & _
“WHERE AssocClass=Win32_DependentService Role=Antecedent”
Set colAssocs = objWMIService.ExecQuery(strQuery)
For Each assoc in colAssocs
childSvcName = GetServiceNameFromPath(assoc.Name)
‘ 再帰呼び出し
Call StartServiceCascade(objWMIService, childSvcName)
Next
StartServiceCascade = True
End Function
‘ ==============================================================================
‘ サービス単体の状態変更とポーリング監視
‘ ==============================================================================
Function SetServiceState(objWMIService, svcName, targetState)
Dim objService, colServices
Dim strQuery, returnCode
Dim startTime, currentState
strQuery = “SELECT FROM Win32_Service WHERE Name = ‘” & svcName & “‘”
Set colServices = objWMIService.ExecQuery(strQuery)
If colServices.Count = 0 Then
SetServiceState = True ‘ 既に存在しない場合はスキップ
Exit Function
End If
Set objService = colServices.ItemIndex(0)
currentState = objService.State
‘ 既に目的の状態であればスキップ
If (UCase(targetState) = “STOP” And currentState = “Stopped”) Or _
(UCase(targetState) = “START” And currentState = “Running”) Then
WScript.Echo “[INFO] サービス ‘” & objService.DisplayName & ” (” & svcName & “)’ は既に ” & currentState & ” です。”
SetServiceState = True
Exit Function
End If
WScript.Echo “[ACTION] サービス ‘” & objService.DisplayName & ” (” & svcName & “)’ を ” & targetState & ” しています…”
If UCase(targetState) = “STOP” Then
returnCode = objService.StopService()
Else
returnCode = objService.StartService()
End If
‘ 戻り値の評価 (0 = 成功, 10 = すでに実行中 など)
If returnCode <> 0 And returnCode <> 5 Then ‘ 5 = アクセス拒否等だが状況により調整
‘ 既に停止中/起動中のエラー(エラーコードによっては許容)
If Not (UCase(targetState) = “STOP” And returnCode = সিন_OR_STATE) Then
‘ ここでは厳密にエラーログを出力
‘ WScript.Echo “[WARNING] 制御メソッドの初期応答コード: ” & returnCode
End If
End If
‘ 状態が完全に遷移するまでポーリング監視(重要)
startTime = Timer
Do
WScript.Sleep 1000
Set colServices = objWMIService.ExecQuery(strQuery)
Set objService = colServices.ItemIndex(0)
currentState = objService.State
If UCase(targetState) = “STOP” And currentState = “Stopped” Then
WScript.Echo “[SUCCESS] 停止完了: ” & svcName
SetServiceState = True
Exit Function
End If
If UCase(targetState) = “START” And currentState = “Running” Then
WScript.Echo “[SUCCESS] 起動完了: ” & svcName
SetServiceState = True
Exit Function
End If
If Timer – startTime > TimeoutSeconds Then
WScript.Echo “[ERROR] タイムアウトエラー: サービス ‘” & svcName & “‘ の ” & targetState & ” が制限時間(” & TimeoutSeconds & “秒)内に完了しませんでした。”
SetServiceState = False
Exit Function
End If
Loop
End Function
‘ WMIパスから純粋なサービス名(Nameプロパティ)を抽出するヘルパー
Function GetServiceNameFromPath(wmiPath)
Dim parts
parts = Split(wmiPath, “=”)
If UBound(parts) >= 1 Then
‘ “Win32_Service.Name=”ServiceName”” の形式からクォーテーションを除去
GetServiceNameFromPath = Replace(parts(1), Chr(34), “”)
Else
GetServiceNameFromPath = wmiPath
End If
End Function
—
現場で役立つ!実装上の重要ポイントと注意点
1. WMIアソシエーションクエリの魔力
コード内で使用している `ASSOCIATORS OF` 構文は、RDBのJOIN句に相当する強力なWMIクエリである。
これにより、ハードコーディングを一切排し、OSが内部で保持している依存関係のグラフ構造をリアルタイムにメモリ上に展開できる。環境が変わってもスクリプトの改修は不要だ。
2. ポーリング監視による「非同期の同期化」
`objService.StopService()` や `StartService()` は、Windowsに対して「要求を投げた瞬間」に制御をスクリプトに戻す(非同期動作)。
ここで `WScript.Sleep` だけを入れて次に進むと、OSの処理が追いつかずにエラーとなる。スクリプト内に `Do…Loop` による 状態のポーリング監視(Polling) を実装し、実際に `State` プロパティが `Running` または `Stopped` に変化したことを確認してから次のステップへ進む設計が、バグをゼロにする最大の秘訣である。
3. 権限(Privileges)とセキュリティコンテキスト
WMI経由でサービスを操作する場合、スクリプトを実行するユーザーにはローカル管理者権限(Administrator)が必須である。
また、UAC(ユーザーアカウント制御)が有効な環境でタスクスケジューラ等から実行する場合は、「最上位の特権で実行する」にチェックを入れること。さもないと、アクセス拒否(Error 5)により制御が弾かれる。
—
運用自動化への展開:ファイル・DB連携のヒント
このVBScriptを単体で手動実行するだけでは、真の業務効率化とは言えない。次のような拡張を行うことで、プロダクションレベルの運用基盤に昇華させることができる。
- ログのファイル出力: 画面への `WScript.Echo` だけでなく、FileSystemObject (FSO) を用いて、実行結果をタイムスタンプ付きのログファイル(`C:\Logs\ServiceRestart_YYYYMMDD.log`)に追記出力する。
- データベース(SQL Server / SQLite)連携: 再起動の成否、実行時間、対象サービス名をRDBにレコードとしてINSERTし、インフラの稼働統計ダッシュボードのデータソースとして活用する。
- 異常検知時の通知: タイムアウトや致命的エラー(`False` が返却された場合)を検知した瞬間に、PowerShell経由でMicrosoft TeamsやSlackのWebhookへアラートをJSONペイロードでPOSTする。
総括
VBScriptはレガシーな言語と揶揄されることもあるが、Windows OSの深部(WMI / COMコンポーネント)に最もダイレクトかつ軽量にアクセスできるスクリプト言語の一つである。
場当たり的なバッチファイルや不確実な手動作業から脱却し、「依存関係の動的解析」と「厳密な状態ポーリング」を取り入れた堅牢な自動化アーキテクチャを現場に導入せよ。それこそが、トラブルフリーなインフラ運用を実現するプロフェッショナルのアプローチである。
