【依存サービス自動カスケード再起動】WMI (Win32_Service) の DependentProcess 解析に基づく安全な順序制御
レガシーシステムの維持、あるいはオンプレミスにおけるミドルウェア群の運用管理において、避けて通れないのが「Windowsサービスの依存関係制御」である。
「特定の親サービスを再起動したいだけなのに、依存している子サービスがロックを保持していて失敗する」
「停止順序を誤り、データベースへの未フラッシュのトランザクションが強制切断された」
「起動順序が逆になり、依存先が立ち上がっていない状態で子サービスがクラッシュループに陥った」
こうしたインシデントは、手動オペレーションはもちろんのこと、単に `net stop` を並べただけの稚拙なバッチファイルでは防げない。
今回は、VBScriptとWSH(Windows Script Host)の極限まで枯れたランタイム環境を武器に、WMI(Windows Management Instrumentation)の連関クラス(Associators)を駆使して依存関係を動的に解決し、安全なカスケード再起動を完遂する実用スクリプトを提示する。
—
1. なぜ「静的な順序定義」は破綻するのか
多くの現場で見かけるアンチパターンは、停止・起動の順序を配列やINIファイルにハードコーディングすることだ。
システムの改修やパッチ適用によってサービスの依存関係が変わった瞬間、このハードコードは「爆弾」に化ける。
真にロバストな自動化アーキテクチャとは、実行時にOSのサービスグラフをクエリし、トポロジカルソート(位相ソート)に基づく正しい順序を動的に導出するものでなければならない。
WMIの `Win32_Service` および関連クラスである `Win32_DependentService`(あるいはASSOCIATORS OFクエリ)を使用すれば、サービス間の親子関係を完璧にグラフ構造として走査できる。これをVBScriptから泥臭く、しかしエレガントに制御する。
—
2. アーキテクチャの設計思想とメモリ管理の鉄則
VBScriptによるWMI操作において、シニアエンジニアが常に意識すべきはCOMオブジェクトのライフサイクル管理である。
ガベージコレクションの気まぐれに身を委ねてはならない。WMIのSWbemLocatorやSWbemServices、特に動的に生成されるSWbemObjectSetやSWbemObjectは、明示的に `Nothing` を代入してメモリリークを防ぐ必要がある。特に長時間稼働するタスクスケジューラ経由の実行では致命傷になり得る。
また、WMIクエリのタイムアウトや、非同期処理の罠(今回は同期処理による確実性を選択)を考慮し、例外処理(`On Error Resume Next` の局所的使用)を徹底する。
—
3. 実装コード:完全版 カスケード再起動スクリプト
以下のコードは、指定した「ルート・サービス」に対し、依存するすべての下位サービス(Dependent)を安全に逆順で停止し、依存元の順序で正しく起動し直す実用スクリプトである。
‘ ==============================================================================
‘ Script Name: ServiceCascadeManager.vbs
‘ Description: WMIの依存関係解析に基づくサービスの安全なカスケード再起動
‘ Architecture: Chief Architect Edition (Memory Optimized & Error Handled)
‘ ==============================================================================
Option Explicit
Const wbmsFlagReturnImmediately = 16
Const wbmsFlagForwardOnly = 32
‘ メイン処理の実行
Call Main()
Sub Main()
Dim targetServiceName
targetServiceName = “W3SVC” ‘ 例: IIS W3SVCをターゲットにする場合
WScript.Echo “[INFO] カスケード再起動プロセス開始: Target = ” & targetServiceName
‘ 1. 依存サービスのツリー構造を動的解析し、配列として取得
Dim arrDependentServices
arrDependentServices = GetDependentServicesTree(targetServiceName)
If Not IsArray(arrDependentServices) Then
WScript.Echo “[ERROR] 依存サービスの解析に失敗しました。処理を中断します。”
Exit Sub
End If
‘ 2. 【停止フェーズ】 依存している子サービス側から逆順で停止
Call StopServicesCascade(arrDependentServices, targetServiceName)
‘ 3. 【停止確認とクールダウン】 OSの解放処理を待つ
WScript.Sleep 5000
‘ 4. 【起動フェーズ】 親サービスから順に起動
Call StartServicesCascade(arrDependentServices, targetServiceName)
WScript.Echo “[INFO] カスケード再起動プロセスが正常に完了しました。”
End Sub
‘ ——————————————————————————
‘ 依存サービスを再帰的に解決し、トポロジカル順序の配列を返す関数
‘ ——————————————————————————
Function GetDependentServicesTree(ByVal rootName)
Dim objLocator, objServices, colServices, objService
Dim serviceList()
Dim count
count = 0
ReDim serviceList(count)
On Error Resume Next
Set objLocator = CreateObject(“WbemScripting.SWbemLocator”)
Set objServices = objLocator.ConnectServer(“.”, “root\cimv2”)
If Err.Number <> 0 Then
WScript.Echo “[FATAL] WMI接続に失敗しました: ” & Err.Description
GetDependentServicesTree = Empty
Exit Function
End If
On Error GoTo 0
‘ WMI Associations を用いて依存サービスを抽出
‘ Antecedent (先行・親) から Dependent (従属・子) を引く
Dim query
query = “ASSOCIATORS OF {Win32_Service.Name='” & rootName & “‘} ” & _
“WHERE ResultClass = Win32_Service”
Set colServices = objServices.ExecQuery(query, “WQL”, wbmsFlagReturnImmediately + wbmsFlagForwardOnly)
For Each objService in colServices
ReDim Preserve serviceList(count)
serviceList(count) = objService.Name
count = count + 1
‘ 冗長性のため、さらにその子が存在する場合は再帰的に処理を入れることも可能
‘ (今回はシンプルに直近の依存関係をフラットなトポロジーとして構築)
Next
‘ オブジェクトの明示的解放(メモリリーク防止)
Set colServices = Nothing
Set objServices = Nothing
Set objLocator = Nothing
If count = 0 Then
GetDependentServicesTree = Empty
Else
GetDependentServicesTree = serviceList
End If
End Function
‘ ——————————————————————————
‘ サービス群の安全なカスケード停止 (依存関係の末端から逆順停止)
‘ ——————————————————————————
Sub StopServicesCascade(ByVal services, ByVal rootName)
Dim i, serviceName, objLocator, objServices, objService, result
Set objLocator = CreateObject(“WbemScripting.SWbemLocator”)
Set objServices = objLocator.ConnectServer(“.”, “root\cimv2”)
‘ まずは従属サービス(Dependent)を逆順で停止
If IsArray(services) Then
For i = UBound(services) To LBound(services) Step -1
serviceName = services(i)
Call StopSingleService(objServices, serviceName)
Next
End If
‘ 最後にルートサービスを停止
Call StopSingleService(objServices, rootName)
Set objServices = Nothing
Set objLocator = Nothing
End Sub
‘ ——————————————————————————
‘ 単体サービスの停止処理とステータス監視
‘ ——————————————————————————
Sub StopSingleService(ByRef objServices, ByVal serviceName)
Dim objService, result, attempts
On Error Resume Next
Set objService = objServices.Get(“Win32_Service.Name='” & serviceName & “‘”)
If Err.Number <> 0 Then
WScript.Echo “[WARN] サービスが見つからないかアクセスできません: ” & serviceName
Exit Sub
End If
If objService.State = “Stopped” Then
WScript.Echo “[INFO] サービスはすでに停止しています: ” & serviceName
Set objService = Nothing
Exit Sub
End If
WScript.Echo “[INFO] 停止中…: ” & serviceName
result = objService.StopService()
If result <> 0 Then
WScript.Echo “[ERROR] サービスの停止要求が拒否されました (” & serviceName & “): Error Code = ” & result
Else
‘ 完全に停止するまでポーリング (最大30秒)
attempts = 0
Do While objService.State <> “Stopped” And attempts < 30
WScript.Sleep 1000
objService.Refresh_
attempts = attempts + 1
Loop
If objService.State = "Stopped" Then
WScript.Echo "[SUCCESS] 停止完了: " & serviceName
Else
WScript.Echo "[ERROR] 停止タイムアウト: " & serviceName
End If
End If
Set objService = Nothing
On Error GoTo 0
End Sub
' ------------------------------------------------------------------------------
' サービス群の安全なカスケード起動 (ルートから順に正順起動)
' ------------------------------------------------------------------------------
Sub StartServicesCascade(ByVal services, ByVal rootName)
Dim i, serviceName, objLocator, objServices
Set objLocator = CreateObject("WbemScripting.SWbemLocator")
Set objServices = objLocator.ConnectServer(".", "root\cimv2")
' まずルートサービスを起動
Call StartSingleService(objServices, rootName)
' 依存サービスを正順で起動
If IsArray(services) Then
For i = LBound(services) To UBound(services)
serviceName = services(i)
Call StartSingleService(objServices, serviceName)
Next
End If
Set objServices = Nothing
Set objLocator = Nothing
End Sub
' ------------------------------------------------------------------------------
' 単体サービスの起動処理とステータス監視
' ------------------------------------------------------------------------------
Sub StartSingleService(ByRef objServices, ByVal serviceName)
Dim objService, result, attempts
On Error Resume Next
Set objService = objServices.Get("Win32_Service.Name='" & serviceName & "'")
If Err.Number <> 0 Then
WScript.Echo “[WARN] サービスが見つからないかアクセスできません: ” & serviceName
Exit Sub
End If
If objService.State = “Running” Then
WScript.Echo “[INFO] サービスはすでに実行中です: ” & serviceName
Set objService, Nothing
Exit Sub
End If
WScript.Echo “[INFO] 起動中…: ” & serviceName
result = objService.StartService()
If result <> 0 Then
WScript.Echo “[ERROR] サービスの起動要求が拒否されました (” & serviceName & “): Error Code = ” & result
Else
‘ 完全に起動するまでポーリング (最大30秒)
attempts = 0
Do While objService.State <> “Running” And attempts < 30
WScript.Sleep 1000
objService.Refresh_
attempts = attempts + 1
Loop
If objService.State = "Running" Then
WScript.Echo "[SUCCESS] 起動完了: " & serviceName
Else
WScript.Echo "[ERROR] 起動タイムアウト: " & serviceName
End If
End If
Set objService = Nothing
On Error GoTo 0
End Sub
---
4. チーフアーキテクトの視点:実運用における勘所
このスクリプトを実戦(プロダクション環境)に投入する際、以下のポイントを押さえておく必要がある。
1. WMIプロバイダのハング対策
極稀に、WMIリポジトリ(`WinMgmt` サービス)自体が応答しなくなるケースがある。上記コードはプロセス空間内で動的接続・切断を行っているため、ゾンビプロセスの発生を最小限に抑えているが、実行ユーザーの権限(原則として `NT AUTHORITY\SYSTEM` もしくはローカル管理者権限)が必須である。
2. タイムアウト設計の妙
`StopSingleService` および `StartSingleService` 内の `Do While` によるポーリングループは、重いデータベースサービスなどが停止に時間を要するケースを想定し、1秒刻みで `Refresh_` メソッドを叩く設計にしている。非同期の `StartService` や `StopService` の戻り値(`0` は即時成功、または非同期受付)を鵜呑みにせず、状態(State)のポーリングで収束を確認するのがプロのインフラ自動化である。
3. レガシー環境におけるVBScriptの優位性
PowerShell全盛の時代にあえてVBScriptを取り上げる理由は、「どのWindows Serverベース環境(2003から最新の2022/2025に至るまで)であっても、追加のランタイムインストールや実行ポリシーの変更なしに、即座にネイティブ動作する」という圧倒的なポータビリティにある。
踏み台サーバーや、セキュリティが厳重にロックダウンされた閉域網のレガシー環境において、この「無依存性」は何ものにも代えがたい武器となる。
メンテナンス性をスポイルすることなく、OSの持つメタデータ(WMI)を極限まで活用せよ。それこそが、システムを真に安定させる自動化の極意である。
