【マルチタスク依存関係制御】WScript.Shell を用いた複数バッチ・スクリプトの実行順序制御タスクランナー
レガシーシステムの深部、あるいは厳格にセキュアに隔離されたオンプレミス環境において、未だにVBScript(Visual Basic Scripting Edition)とWSH(Windows Script Host)は、インフラ自動化の最後の砦として静かに、しかし強靭に稼働し続けている。
「`.NET Framework`や`PowerShell`の実行ポリシーに阻まれる」「サードパーティ製のジョブスケジューラを導入する予算がない」。そんな極限の現場において、Windows標準機能だけで複雑な依存関係を持つ複数プロセスのオーケストレーションをどう実現するか。
今回は、`WScript.Shell`の`Run`メソッドの同期・非同期制御を極限までハックし、メモリリークの罠を回避しながら、DAG(有向非巡回グラフ)的な依存関係とリカバリー機構を持つ「VBScript製ワークフロー・エンジン(タスクランナー)」の構築手法を解説する。
—
1. VBScriptプロセス制御の暗部:`Run` vs `Exec` の選択と寿命管理
複数スクリプトやバッチファイルを連鎖させる際、多くのエンジニアは直感的に `WScript.Shell` の `Run` メソッドを使う。しかし、その内部挙動とメモリのライフサイクルを理解している者は少ない。
`Run` メソッドの同期フラグ (`bWaitOnReturn`) の罠
`WScript.Shell.Run(strCommand, [intWindowStyle], [bWaitOnReturn])` の第3引数 `bWaitOnReturn` は、プロセス完了までスクリプトの実行をブロックするかどうかを決める。
- `True`(同期): 子プロセスの終了コードを取得できるが、ブロッキング中は親VBScriptのスレッドが完全に停止する。
- `False`(非同期): 即座に制御が戻るが、子プロセスの終了コード(Exit Code)が取得できない。
依存関係制御(「タスクAが正常終了 `Exit Code 0` の場合のみタスクBを実行する」)において、非同期実行では正確なステータスハンドリングが破綻する。したがって、厳密な順序制御には `True` を用いた逐次同期的実行、あるいは `Exec` メソッドによるI/Oストリームの監視が必須となる。今回は、環境依存性が低く最も安定する `Run` の同期モードをベースに、タイムアウトとエラーハンドリングを極限まで洗練させたアーキテクチャを採用する。
COMオブジェクトの明示的解放(メモリ最適化)
VBScriptの弱点として、COMオブジェクトの参照カウント管理の曖昧さがある。特に動的にループ内で `WScript.Shell` や `Scripting.FileSystemObject` を生成・破棄する場合、明示的に `Nothing` を代入してメモリプレッシャーを軽減しなければ、長時間のバッチ処理でメモリリークを引き起こす。
—
2. 実装:依存関係制御タスクランナー(`WorkflowRunner.vbs`)
以下のコードは、定義されたタスクリスト(JSONライクな構造、または配列による依存関係)を読み込み、エラー時のリカバリー(スキップ、リトライ、即座のロールバック的処理)を制御するプロダクション品質のタスクランナーである。
‘ ==============================================================================
‘ Script Name : WorkflowRunner.vbs
‘ Description : WScript.Shell を用いた依存関係制御型タスクランナー
‘ Architecture: シニアアーキテクト向け堅牢性重視モデル
‘ ==============================================================================
Option Explicit
‘ — 定数定義 —
Const CONST_LOG_PATH = “C:\Logs\WorkflowRunner.log”
Const CONST_TIMEOUT_SEC = 3600 ‘ 最大許容実行時間(秒)
‘ エラーハンドリングのポリシー
Const POLICY_STOP_ON_ERROR = 1 ; エラー発生時に即座にワークフロー全体を中断
Const POLICY_IGNORE_AND_NEXT = 2; エラーを無視して次のタスクへ継続
Const POLICY_RETRY = 3 ; リトライを実行
Call Main()
Sub Main()
Dim fso, logger
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set logger = New Logger
logger.Info “=== Workflow Runner Started ===”
‘ タスク定義のロード(実際には外部設定ファイルやDBから読み込むことも想定)
‘ 構造: TaskID, Command, DependsOn, OnErrorPolicy, MaxRetries
Dim tasks
tasks = GetTaskDefinitions()
Dim i, result
Dim taskStatus : Set taskStatus = CreateObject(“Scripting.Dictionary”)
‘ 初期ステータス設定
For i = 0 UBound(tasks)
taskStatus.Add tasks(i)(0), “PENDING”
Next
‘ 実行エンジン本体
For i = 0 UBound(tasks)
Dim taskId, command, dependsOn, errorPolicy, maxRetries
taskId = tasks(i)(0)
command = tasks(i)(1)
dependsOn = tasks(i)(2)
errorPolicy = tasks(i)(3)
maxRetries = tasks(i)(4)
logger.Info “Evaluating Task: [” & taskId & “]”
‘ 1. 依存関係の評価
If dependsOn <> “” Then
If taskStatus.Exists(dependsOn) Then
If taskStatus(dependsOn) <> “SUCCESS” Then
logger.Warn “Task [” & taskId & “] skipped because dependency [” & dependsOn & “] did not succeed (Status: ” & taskStatus(dependsOn) & “).”
taskStatus(taskId) = “SKIPPED”
Continue For
End If
End If
End If
‘ 2. タスクの実行とリトライ制御
Dim exitCode, attempt
attempt = 0
Do
attempt = attempt + 1
logger.Info “Executing: ” & command & ” (Attempt ” & attempt & “/” & maxRetries + 1 & “)”
exitCode = ExecuteCommand(command)
If exitCode = 0 Then
Exit Do
End If
logger.Error “Task [” & taskId & “] failed with Exit Code: ” & exitCode
If attempt > maxRetries Then
Exit Do
End If
WScript.Sleep(2000) ‘ リトライ前のインターバル(2秒)
Loop
‘ 3. 結果の判定とポリシー処理
If exitCode = 0 Then
taskStatus(taskId) = “SUCCESS”
logger.Info “Task [” & taskId & “] COMPLETED successfully.”
Else
taskStatus(taskId) = “FAILED”
Select Case errorPolicy
Case POLICY_STOP_ON_ERROR
logger.Error “Critical error in Task [” & taskId & “]. Aborting workflow.”
Exit For
Case POLICY_IGNORE_AND_NEXT
logger.Warn “Error ignored by policy for Task [” & taskId & “]. Continuing…”
Case Else
logger.Error “Unknown error policy. Aborting workflow.”
Exit For
End Select
End If
Next
logger.Info “=== Workflow Runner Finished ===”
‘ オブジェクトの明示的解放(メモリ最適化)
Set taskStatus = Nothing
Set logger = Nothing
Set fso = Nothing
End Sub
‘ ——————————————————————————
‘ Function: ExecuteCommand
‘ Purpose : WScript.Shell を利用したプロセス同期実行
‘ ——————————————————————————
Function ExecuteCommand(ByVal cmd)
Dim shell, execResult
Set shell = CreateObject(“WScript.Shell”)
‘ 第3引数を True にすることで、プロセスの終了を完全に同期待機し終了コードを取得
‘ cmd.exe を経由させることで環境変数やリダイレクトを安全に処理
execResult = shell.Run(“%COMSPEC% /c ” & cmd, 0, True)
ExecuteCommand = execResult
‘ 明示的解放
Set shell = Nothing
End Function
‘ ——————————————————————————
‘ Function: GetTaskDefinitions
‘ Purpose : ワークフロー定義の配列を返す(拡張時は外部CSV/XML読込に変更可能)
‘ フォーマット: Array(TaskID, Command, DependsOn, OnErrorPolicy, MaxRetries)
‘ ——————————————————————————
Function GetTaskDefinitions()
Dim tasks(2)
‘ Task 1: 前処理・データクレンジング
tasks(0) = Array(“Task_Prep”, “cscript.exe //Nologo C:\Scripts\clean.vbs”, “”, POLICY_STOP_ON_ERROR, 1)
‘ Task 2: メインバッチ処理(Task_Prep に依存)
tasks(1) = Array(“Task_Main”, “C:\Batches\process_data.bat”, “Task_Prep”, POLICY_STOP_ON_ERROR, 2)
‘ Task 3: 後処理・レポート生成(Task_Main に依存、エラーでも継続可能)
tasks(2) = Array(“Task_Report”, “cscript.exe //Nologo C:\Scripts\report.vbs”, “Task_Main”, POLICY_IGNORE_AND_NEXT, 0)
GetTaskDefinitions = tasks
End Function
‘ ——————————————————————————
‘ Class: Logger
‘ Purpose : 堅牢なファイルおよび標準出力ロギング
‘ ——————————————————————————
Class Logger
Private fso, ts
Private Sub Class_Initialize()
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ ログファイルのオープン(追記モード、存在しない場合は作成)
On Error Resume Next
Set ts = fso.OpenTextFile(CONST_LOG_PATH, 8, True)
If Err.Number <> 0 Then
‘ フォールバック:ローカルカレントに作成
Set ts = fso.OpenTextFile(“WorkflowRunner_fallback.log”, 8, True)
End If
On Error GoTo 0
End Sub
Private Sub Class_Terminate()
If Not ts Is Nothing Then
ts.Close
Set ts = Nothing
End If
Set fso = Nothing
End Sub
Public Sub Info(ByVal msg)
WriteLog “INFO”, msg
End Sub
Public Sub Warn(ByVal msg)
WriteLog “WARN”, msg
End Sub
Public Sub Error(ByVal msg)
WriteLog “ERROR”, msg
End Sub
Private Sub WriteLog(ByVal level, ByVal msg)
Dim logLine
logLine = “[” & Now & “] [” & level & “] ” & msg
WScript.Echo logLine ‘ 標準出力へのエコー(タスクスケジューラ対策)
If Not ts Is Nothing Then
ts.WriteLine logLine
End If
End Sub
End Class
—
3. チーフアーキテクトが解説する設計の急所
A. `%COMSPEC% /c` ラッピングの効用
単に実行ファイルを直接 `Run` するのではなく、環境変数やバッチファイルのコンテキストを正しく引き継ぐために `%COMSPEC% /c`(すなわち `cmd.exe /c`)を経由させている。これにより、パスの通っていないカスタムユーティリティや、環境変数(`%PATH%` やカスタム変数)に依存するレガシーバッチの実行失敗率を劇的に下げることができる。
B. ステータス伝播と「カスケード・スキップ」
依存関係 (`DependsOn`) の概念において、親タスクが `FAILED` になった場合、それに依存する子タスクは自動的に `SKIPPED` ステータスに遷移する。これにより、無駄な子プロセスの起動を防ぎ、システムリソースを保護するとともに、ログ解析時に「どこで根本的な障害が発生したのか」を瞬時に特定できる。
C. クラス化されたLoggerによるリソースのデッドロック防止
VBScriptにおいてファイルI/Oはしばしば競合やハンドル解放漏れを引き起こす。上記の `Logger` クラスでは、`Class_Initialize` でファイルを開き、`Class_Terminate`(スクリプト終了時、またはオブジェクト破棄時)に確実に `Close` を実行するRAII(Resource Acquisition Is Initialization)に近いイディオムを実装している。これにより、ログファイルのロック起因によるプロセスハングを完全に排除している。
—
4. 現場運用における極意とまとめ
レガシーシステムの自動化において最も恐ろしいのは、「スクリプトが途中でゾンビプロセス化し、タスクマネージャーに無数の `cscript.exe` や `cmd.exe` が残留する」という事態である。
今回のアーキテクチャでは、すべてのプロセスが `Run` の同期的アプローチ(`bWaitOnReturn = True`)によって厳格にスコープ制御されており、親が子を看取る(Waitする)構造になっているため、ゾンビプロセスの発生余地がない。
最新のモダンなクラウド環境やコンテナ技術から見れば、VBScriptによるオーケストレーションは前時代的に映るかもしれない。しかし、OSの初期インストール状態だけで完全に動作し、外部ランタイムのアップデートによる破壊的変更(Breaking Changes)に怯える必要のないこの「究極の自己完結性」は、インフラストラクチャの信頼性を担保する上で今なお圧倒的な価値を持つ。
枯れた技術の地べたを這うような堅牢性。それこそが、現場を支えるエンジニアの最強の武器である。
