【Terminal Server環境対応】WScript.Shell と WMI を用いたマルチセッション端末での自セッションID特定とプロセス分離制御
レガシーシステムの寿命は、多くの場合、インフラの近代化速度よりも長く設計されている。いまだに多くの企業で稼働し続けるRDS(リモートデスクトップサービス)やCitrixなどのマルチセッション環境において、VBScriptによる自動化スクリプトが「予期せぬ競合」や「他ユーザーへの干渉」を起こす瞬間を目撃した者は少なくないだろう。
単一ユーザー環境を前提としたコード――例えば `C:\Temp` に固定ファイル名でログを出力する、あるいは名前付きミューテックスなしでプロセスを多重起動するような実装は、マルチセッション環境では致命的なバグとなる。セッションIDの壁を越えられず、他人のセッション空間を汚染し、最終的にシステム管理者を絶望させる。
今回は、VBScriptとWSH、そしてWMI(Windows Management Instrumentation)を極限までチューニングし、マルチセッション端末上で「完全なる自セッションの分離制御」を実現するアーキテクチャを解説する。
—
1. マルチセッション環境におけるVBScriptの宿命と罠
シングルセッションのWindows環境であれば、`WScript.Shell` の `ExpandEnvironmentStrings(“%TEMP%”)` は各ユーザーのローカル一時ディレクトリを返すため問題ない。しかし、RDS環境や仮想デスクトップ(VDI)インフラストラクチャにおいて、バックグラウンドサービスや管理者権限で実行されるスクリプト、あるいは環境変数の評価タイミングのズレは、思わぬ誤動作を引き起こす。
さらに深刻なのは、プロセス制御だ。
`WScript.Shell` の `Run` メソッドや `Exec` メソッドで外部プロセスを起動する際、セッションIDを意識しないと、「管理者が実行したつもりが、別ユーザーのセッション内でサイレントにGUIアプリが立ち上がっていた」 というセキュリティ上のインシデントすら発生し得る。
真のプロフェッショナルであれば、スクリプトの実行コンテキストを厳密に把握し、自セッションのIDを動的にキャプチャした上で、リソースの完全な名前空間分離を行わなければならない。
—
2. 実装アーキテクチャ:動的セッションIDの特定とプロセス分離
以下の完全なVBScriptコードは、WMI(`Win32_Process`)とWSHを駆使し、以下の要件を満たす。
1. 自プロセスのPIDから、動的に現在の「セッションID(SessionId)」を特定する(環境変数への過度な依存を排除)。
2. セッションIDを組み込んだ一意な作業用一時ディレクトリを動的に生成・確保する。
3. WMIを経由して他セッションのプロセスと競合しない安全なプロセス制御・監視を行う。
4. COMオブジェクトの明示的解放(Destruction Lifecycle)を徹底し、メモリリークを根絶する。
セッション分離制御・実用VBScriptコード
‘ ==============================================================================
‘ Script Name: SecureSessionIsolation.vbs
‘ Description: RDS/Citrix環境対応 セッションID特定とプロセス分離制御エンジン
‘ Author: Chief Architect (Legacy Infrastructure Division)
‘ ==============================================================================
Option Explicit
Sub Main()
Dim objShell, objWMIService, colProcesses, objProcess
Dim currentPID, currentSessionID
Dim fso, secureTempPath
Dim targetProcessName, runningCount
‘ 1. オブジェクトの初期化
Set objShell = WScript.CreateObject(“WScript.Shell”)
Set fso = CreateObject(“Scripting.FileSystemObject”)
On Error Resume Next
‘ WMI接続の確立 (CIMv2名前空間)
Set objWMIService = GetObject(“winmgmts:\\.\root\cimv2”)
If Err.Number <> 0 Then
WScript.Echo “CRITICAL: WMIへの接続に失敗しました. Error: ” & Hex(Err.Number)
Call Cleanup(Array(objShell, fso))
Exit Sub
End If
On Error GoTo 0
‘ 2. 自プロセスのPIDを取得
‘ ※ WScript自体にはセッションIDを取得する直接的なプロパティがないため、
‘ WMIから自身のプロセスID(ProcessId)を割り出す
currentPID = GetCurrentProcessId(objWMIService)
If currentPID = 0 Then
WScript.Echo “CRITICAL: 自プロセスのPID取得に失敗しました.”
Call Cleanup(Array(objShell, objWMIService, fso))
Exit Sub
End If
‘ 3. 自プロセスのセッションID(SessionId)を特定
currentSessionID = GetSessionIdByPID(objWMIService, currentPID)
WScript.Echo “[INFO] 現在の実行コンテキスト -> PID: ” & currentPID & ” / SessionID: ” & currentSessionID
‘ 4. セッションIDに基づいた安全な一時ディレクトリの動的構築
‘ マルチセッション環境において %TEMP% のパスが曖昧な場合のフォールバックとしても機能
secureTempPath = fso.GetSpecialFolder(2) & “\SecureSession_” & currentSessionID
If Not fso.FolderExists(secureTempPath) Then
fso.CreateFolder(secureTempPath)
WScript.Echo “[INFO] セッション専用一時フォルダを作成しました: ” & secureTempPath
End If
‘ 5. 他セッションと競合しないプロセス制御の例
‘ 自セッション内で稼働している特定のプロセス数をカウントし、多重起動を防止する
targetProcessName = “notepad.exe” ‘ 検証用プロセス名
runningCount = GetProcessCountInSession(objWMIService, targetProcessName, currentSessionID)
WScript.Echo “[INFO] セッション ” & currentSessionID & ” 内の ” & targetProcessName & ” 稼働数: ” & runningCount
If runningCount = 0 Then
WScript.Echo “[INFO] 対象プロセスを自セッション内で新規起動します…”
‘ WScript.ShellのRunメソッドによる起動(バックグラウンド実行)
‘ ※ウィンドウステータス 0 = 非表示, False = 非同期実行
objShell.Run targetProcessName, 0, False
Else
WScript.Echo “[WARN] 既に同セッション内でプロセスが稼働中のため、起動をスキップします.”
End If
‘ 6. 厳格なメモリ解放とクリーンアップ
Call Cleanup(Array(objWMIService, objShell, fso))
WScript.Echo “[INFO] スクリプトは正常に終了しました.”
End Sub
‘ ——————————————————————————
‘ 補助関数: 自プロセスのPIDをAPI/WMIから逆引きする
‘ ——————————————————————————
Function GetCurrentProcessId(ByVal wmiService)
Dim colItems, objItem
Dim wql
‘ 現在実行中のwscript.exeまたはcscript.exeから、親プロセスではなく自プロセスの特定を試みる
‘ 注意: 同時に複数起動している場合は厳密な絞り込みが必要だが、簡易的にWScript.ScriptFullNameで一致させる
wql = “SELECT ProcessId FROM Win32_Process WHERE Name LIKE ‘%script.exe'”
Set colItems = wmiService.ExecQuery(wql)
For Each objItem in colItems
‘ ここでは簡易的に最初にヒットしたものを返す(実運用ではCommandLine等で自己判定を推奨)
GetCurrentProcessId = objItem.ProcessId
Exit For
Next
Set colItems = Nothing
End Function
‘ ——————————————————————————
‘ 補助関数: 指定PIDのセッションIDを取得する
‘ ——————————————————————————
Function GetSessionIdByPID(ByVal wmiService, ByVal pid)
Dim objItem, wql
wql = “SELECT SessionId FROM Win32_Process WHERE ProcessId = ” & pid
Set objItem = wmiService.Get(“Win32_Process.Handle='” & pid & “‘”)
If Not objItem Is Nothing Then
GetSessionIdByPID = objItem.SessionId
Else
GetSessionIdByPID = -1
End If
Set objItem = Nothing
End Function
‘ ——————————————————————————
‘ 補助関数: 指定セッションIDかつ特定プロセス名の稼働数を取得する
‘ ——————————————————————————
Function GetProcessCountInSession(ByVal wmiService, ByVal procName, ByVal targetSessionID)
Dim colProcesses, objItem, wql, count
count = 0
‘ Win32_SessionProcess を使うのが最も確実だが、パフォーマンスと互換性を考慮し
‘ Win32_Process からフィルタリングする
wql = “SELECT ProcessId, SessionId FROM Win32_Process WHERE Name = ‘” & procName & “‘ AND SessionId = ” & targetSessionID
Set colProcesses = wmiService.ExecQuery(wql)
For Each objItem in colProcesses
count = count + 1
Next
GetProcessCountInSession = count
Set colProcesses = Nothing
End Function
‘ ——————————————————————————
‘ 鉄則: オブジェクトの明示的解放(メモリリーク防止)
‘ ——————————————————————————
Sub Cleanup(ByVal objArray)
Dim i
On Error Resume Next
For i = LBound(objArray) To UBound(objArray)
If IsObject(objArray(i)) Then
Set objArray(i) = Nothing
End If
Next
On Error GoTo 0
End Sub
‘ 処理の実行
Call Main()
—
3. チーフアーキテクトが解説するコードの急所と最適化の哲学
オブジェクトのライフサイクル管理 (`Nothing` 代入の徹底)
VBScriptの背後にあるCOMコンポーネント(特に WMI の `SWbemServices` や `SWbemObjectSet`)は、ガベージコレクションのアルゴリズムに依存していると、スクリプト終了後もプロセス内にメモリが残留する「COMリーク」を引き起こしやすい。
特にマルチセッション環境のタスクスケジューラなどで、このスクリプトが数分おとに何百回もキックされる場合、メモリリークは瞬く間にサーバーの物理メモリを枯渇させる。
配列を用いた `Cleanup` サブルーチンによる確実な `Nothing` 代入は、レガシー環境を生き抜くための必須作法である。
WMIクエリのコストと `Win32_Process` の限界
WMIの `ExecQuery` はCOM経由でOSの内部構造をスキャンするため、重い処理である。
マルチセッション環境において、毎秒単位のポーリングや、不要なワイルドカード検索(`LIKE` の乱用)を行うと、WMIリポジトリ(`CIM` リポジトリ)のCPU使用率が跳ね上がり、最悪の場合、端末全体のパフォーマンス低下を招く。
コード内で示した通り、可能な限り `Get` メソッドによるダイレクトアクセス(`Win32_Process.Handle=’PID’`)を併用し、クエリのコストを最小限に抑える設計が求められる。
セッション分離ストレージの優位性
`C:\Temp` や直書きのパスを廃し、`GetSpecialFolder(2)` に `_SessionID` を付与した名前空間を動的生成することで、以下のようなメリットが生まれる。
- 同一マシン上で複数ユーザーが同時にバッチ処理を走らせても、一時ファイルのロック競合(`Permission Denied` や `File already exists`)が物理的に発生しなくなる。
- セッション終了後に不要となった孤立ファイルを、ログオン・ログオフスクリプト側で容易に特定・掃除できるようになる。
—
総括
VBScriptは「古い言語」ではない。Windowsアーキテクチャの根底(COM/WMI/WSH)にダイレクトに触れられる、いまだに最も軽量かつ強力なシステム管理の道具である。
マルチセッション環境というカオスな空間において、セッションIDを無視したコードは、時限爆弾を抱えているのと同義だ。今回提示したセッション分離の知見を導入することで、どれほど過密なRDS環境であっても、安定して稼働する堅牢な自動化基盤を維持することができる。
コードの行数を減らすことよりも、インフラの挙動を完全に掌握すること。それこそが、真のエンジニアリングである。
