VB.NETでWMIのWin32_CDROMDriveを列挙しようとしたら、Blu-ray書き込み中だけGet()で固まってしまう——この現象は「WMIは同期で待ち続ける」仕様と、光学ドライブの状態が噛み合ったときに起きます。本記事では、フリーズを回避しつつ安全に扱うための実装パターン(別スレッド化・タイムアウト・非同期WMI・代替取得手段)を、運用を想定した形で具体例つきで解説します。
現象:書き込み中だけ Win32_CDROMDrive の列挙でハングする
以下のようにManagementObjectSearcherでWin32_CDROMDriveを取得するコードは、普段は問題なく動作します。しかし「唯一の光学ドライブが書き込み中(例:Pioneer BDR‑S09XLTでBlu-ray書き込み中)」のタイミングに限り、Get()から返ってこない(=結果としてアプリが固まったように見える)ことがあります。
Using MySearcher As New ManagementObjectSearcher(New SelectQuery("Win32_CDROMDrive"))
For Each MyDisk As ManagementObject In MySearcher.Get()
' ここで処理が止まる(ハングする)
Next
End Using
重要なのは、ここで止まっているのはFor Eachの中身ではなく、列挙結果を得るためのGet()、あるいは列挙開始時点の内部処理だという点です。UIスレッドで実行している場合、メッセージループが止まるため「アプリ全体がフリーズした」ように見えます。
原因:Get()は同期呼び出しで、デバイス応答待ちが無期限になり得る
ManagementObjectSearcher.Get()はWMIクエリの結果が返るまで待つ同期APIです。WMIのクラスWin32_CDROMDriveは、取得時にストレージスタックやデバイスドライバ側へ問い合わせが発生し、状況によっては応答がすぐ戻りません。
- 書き込み中はドライブが高負荷・排他状態になりやすい(書き込み処理優先、メディア状態照会に時間がかかる、など)
- WMIプロバイダ側が応答待ちのままブロックするケースがある
- 結果として
Get()が「無期限待ち」に見える状態になる
つまり「WMIが悪い」というより、デバイス状態に依存する問い合わせを、無期限待ちになり得る同期APIで、UIスレッドから直叩きしていることが主因です。光学ドライブだけでなく、外付けUSBストレージ、カードリーダー、ネットワーク関連のWMI照会でも似た構図が起こり得ます。
結論:フリーズを避ける基本戦略は「UIスレッドから外す」+「待ちに上限を付ける」
「書き込み中でもフリーズさせずに扱う」ための最も現実的な方針は、次の2点です。
- 重い/止まり得る処理はUIスレッドで実行しない(別スレッド、タスク、別プロセス)
- 待ち時間に上限(タイムアウト)を設け、失敗時の動作を仕様として決める
これにより、ドライブが書き込み中で応答が返らないタイミングでも「アプリが固まる」事態を避けられます。
対策一覧:実装コストと強さを比較する
| 対策 | フリーズ回避 | タイムアウトの確実性 | 実装コスト | 向いている用途 |
|---|---|---|---|---|
| タスク/別スレッドで実行 | 強い(UIは止まらない) | 工夫が必要(裏で処理が残る可能性) | 低〜中 | UIアプリ全般、まず最初の安全策 |
WMIのEnumerationOptions.Timeoutを設定 | 強い(例外として戻る) | 環境依存(効くことが多いが万能ではない) | 低 | 「できれば短時間で諦めたい」照会 |
非同期WMI(ManagementOperationObserver)+Cancel | 強い | 比較的強い(Cancelできる設計) | 中 | UIを止めず、結果をイベント駆動で受けたい |
| 別プロセス化(ヘルパーEXE)+プロセスKill | 最強(本体に影響を隔離) | 最強(時間で確実に終了可能) | 中〜高 | 「絶対に固まってはいけない」商用・常駐系 |
WMIを避け、DriveInfoやWin32_LogicalDisk等へ切替 | 強い | 取得情報が限定される | 低 | ドライブレターや存在確認が主目的 |
まず入れるべき安全策:UIスレッドでGet()を呼ばない
最初の改善として効果が大きいのは、WMI照会をワーカースレッドへ逃がすことです。これだけで「アプリが固まったように見える」状況を解消できます。
VB.NET:TaskでWMI照会を別スレッドに逃がす(基本形)
Imports System.Management
Imports System.Threading.Tasks
Public Async Function GetCdRomDrivesOnWorkerAsync() As Task(Of List(Of String))
Return Await Task.Run(Function()
Dim drives As New List(Of String)
Using searcher As New ManagementObjectSearcher(New SelectQuery("SELECT * FROM Win32_CDROMDrive"))
Using results As ManagementObjectCollection = searcher.Get()
For Each mo As ManagementObject In results
Dim name = CStr(mo("Name"))
Dim driveLetter = If(TryCast(mo("Drive"), String), "")
drives.Add($"{driveLetter} {name}".Trim())
Next
End Using
End Using
Return drives
End Function)
End Function
ただし、この段階では「裏側でGet()がいつまでも返らない」可能性は残ります。UIは止まらなくても、ワーカースレッドが塞がり続け、呼び出し頻度によってはリソースを圧迫します。したがって次に「待ち時間の上限」を用意します。
実用度が高い:タイムアウトを設けて「使用中」として扱う
ポイントは2つです。
- 一定時間で諦める(数秒〜十数秒など、製品要件に合わせる)
- 諦めた後の挙動を仕様にする(スキップ/後で再試行/「使用中」表示など)
タイムアウトの実装には大きく2系統あります。
- アプリ側の待機を打ち切る(
Task.WhenAnyなど) - WMI側にタイムアウトを設定する(
EnumerationOptions.Timeout)
方法A:Task.WhenAnyで待機を打ち切る(ただし裏で処理が残り得る)
この方法は「待つのをやめる」だけなので、裏側のWMI呼び出し自体はブロックしたまま残る可能性があります。とはいえ、UIフリーズを確実に防ぎたい・まず動く形にしたい場合には有効です。
Imports System.Management
Imports System.Threading.Tasks
Public Async Function TryGetCdRomDrivesWithWaitTimeoutAsync(timeout As TimeSpan) As Task(Of (Success As Boolean, Drives As List(Of String)))
Dim work = Task.Run(Function()
Dim drives As New List(Of String)
Using searcher As New ManagementObjectSearcher(New SelectQuery("SELECT * FROM Win32_CDROMDrive"))
Using results As ManagementObjectCollection = searcher.Get()
For Each mo As ManagementObject In results
Dim name = CStr(mo("Name"))
Dim driveLetter = If(TryCast(mo("Drive"), String), "")
drives.Add($"{driveLetter} {name}".Trim())
Next
End Using
End Using
Return drives
End Function)
Dim completed = Await Task.WhenAny(work, Task.Delay(timeout))
If completed Is work Then
Return (True, Await work)
Else
' ここでは「待つのをやめる」だけ。裏のWMIがブロックしたままの可能性がある。
Return (False, New List(Of String))
End If
End Function
運用上の注意として、短い間隔で何度も呼ぶと「タイムアウトで待機をやめたタスク」が溜まる構造になり得ます。回避策としては次が現実的です。
- ポーリング間隔を長めにする(例:UI更新は数十秒に1回)
- 同時実行を1本に制限する(前回が終わっていなければスキップ)
- より確実な方法(後述のWMI Timeout設定、非同期WMI、別プロセス化)へ移行する
方法B:WMI側のタイムアウトを設定する(まず試す価値が高い)
System.Managementには、クエリ列挙のオプションとしてタイムアウトを設定できる仕組みがあります。EnumerationOptions.Timeoutに時間を指定し、タイムアウト時は例外として戻す設計にします。
この方法がうまく効く環境では、「裏でいつまでもブロックする」問題も起こしにくく、コードも比較的シンプルです。まずはこの設定を入れて挙動を確認するのがおすすめです。
Imports System.Management
Public Function TryGetCdRomDrivesWithWmiTimeout(timeout As TimeSpan) As (Success As Boolean, Drives As List(Of String), ErrorMessage As String)
Dim drives As New List(Of String)
Dim query As New SelectQuery("SELECT * FROM Win32_CDROMDrive")
Dim options As New EnumerationOptions() With {
.Timeout = timeout,
.ReturnImmediately = False, ' 同期的に集める(タイムアウトを効かせたい意図)
.Rewindable = False
}
Try
Using searcher As New ManagementObjectSearcher(query)
searcher.Options = options
Using results As ManagementObjectCollection = searcher.Get()
For Each mo As ManagementObject In results
Dim name = CStr(mo("Name"))
Dim driveLetter = If(TryCast(mo("Drive"), String), "")
drives.Add($"{driveLetter} {name}".Trim())
Next
End Using
End Using
Return (True, drives, "")
Catch ex As ManagementException
' タイムアウトやWMI側エラーをここで扱う
Return (False, New List(Of String), $"WMI error: {ex.ErrorCode} {ex.Message}")
Catch ex As Exception
Return (False, New List(Of String), $"Unexpected error: {ex.GetType().Name} {ex.Message}")
End Try
End Function
タイムアウトを「使用中」と見なす運用にする場合、例外メッセージをそのままユーザーに見せるより、アプリ側の表現として次のように丸める方が親切です。
- 「光学ドライブが使用中のため情報を取得できませんでした。書き込み完了後に再試行してください。」
- 「ドライブ情報の取得に時間がかかったためスキップしました(再試行します)。」
「タイムアウト=失敗」ではなく、「ドライブ使用中」という状態として扱う
書き込み中は、ドライブが正常であっても照会に応答できないことがあります。ここで重要なのは、タイムアウトを例外的なクラッシュ原因にしないことです。アプリ仕様として、次のような状態遷移を定義しておくと実装が安定します。
| 状態 | 例 | アプリの扱い |
|---|---|---|
| 取得成功 | 通常時に列挙できる | モデル名・ドライブ文字などを表示/利用 |
| 使用中(応答遅延) | 書き込み中でタイムアウト | スキップ、後で再試行、UIは「使用中」表示 |
| 一時的エラー | WMIサービスの一時不調、瞬断 | ログ記録し、一定回数はリトライ |
| 恒久的エラー | 権限不足、WMI破損など | 機能制限を案内、管理者対応を促す |
こうしておくと「たまたま書き込み中だった」だけでアプリの主要機能が止まることがなくなり、ユーザー体験が大きく改善します。
より堅牢にする:非同期WMI(Observer)で取得し、タイムアウト時はCancelする
ManagementObjectSearcherには、結果をイベントで受け取る非同期パターンがあります。ここではManagementOperationObserverを使い、完了イベントを待つ形に組み立てます。タイムアウト時にはCancel()を呼び、可能な範囲でWMI操作を中断します。
UIスレッドを止めず、かつ「待ちを切る」ことを設計に組み込みやすいのが利点です。
Imports System.Management
Imports System.Threading
Imports System.Threading.Tasks
Public Async Function TryGetCdRomDrivesAsyncObserver(timeout As TimeSpan) As Task(Of (Success As Boolean, Drives As List(Of String)))
Dim drives As New List(Of String)
Dim tcs As New TaskCompletionSource(Of Boolean)(TaskCreationOptions.RunContinuationsAsynchronously)
Using searcher As New ManagementObjectSearcher(New SelectQuery("SELECT * FROM Win32_CDROMDrive"))
Dim observer As New ManagementOperationObserver()
AddHandler observer.ObjectReady,
Sub(sender, e)
Try
Dim mo = CType(e.NewObject, ManagementBaseObject)
Dim name = CStr(mo("Name"))
Dim driveLetter = If(TryCast(mo("Drive"), String), "")
SyncLock drives
drives.Add($"{driveLetter} {name}".Trim())
End SyncLock
Catch
' 取得途中の個別エラーは握りつぶす/ログへ(要件に応じて)
End Try
End Sub
AddHandler observer.Completed,
Sub(sender, e)
' e.Statusで成功/失敗が分かる。ここでは単純化して完了通知にする。
tcs.TrySetResult(True)
End Sub
searcher.Get(observer)
Dim completed = Await Task.WhenAny(tcs.Task, Task.Delay(timeout))
If completed Is tcs.Task Then
Return (True, drives)
Else
' タイムアウトしたらキャンセルを試みる
Try
observer.Cancel()
Catch
' Cancelできない/例外の場合もあるので握りつぶすかログへ
End Try
Return (False, New List(Of String))
End If
End Using
End Function
この形にしておくと、「時間内に取れた分だけ返す」「タイムアウトしたら空で返す」など、要件に合わせた振る舞いを作りやすくなります。
それでも“絶対に止めたくない”なら:別プロセスに隔離する
WMIやデバイス問い合わせの怖いところは、「キャンセルが効かない/ブロックが解除されない」ケースがゼロではないことです。たとえば、ある環境やドライバの組み合わせで特定呼び出しがスタックし続けると、アプリ内スレッドに押し込めたままでは回収が難しくなります。
その場合の最終手段として有効なのが別プロセス化です。
- ヘルパーEXE(コンソールなど)にWMI照会だけをさせる
- 本体はヘルパーを起動して結果を受け取る(標準出力/一時ファイル/NamedPipeなど)
- 時間を超えたらヘルパーを終了(Kill)して本体は継続
プロセスはOSが強制終了できるため、「タイムアウトを確実に守る」という点で最も堅牢です。常駐アプリ、業務端末、無人運用など「固まったら困る」要件がある場合に検討価値があります。
代替案:そもそも Win32_CDROMDrive を使わない(目的別)
「光学ドライブがあるか」「ドライブレターは何か」程度が目的なら、WMIでハードウェア情報を掘りに行く必要はないことが多いです。目的に応じて情報源を変えると、ハングのリスク自体を下げられます。
ドライブレターや存在確認なら DriveInfo がシンプル
Imports System.IO
Public Function GetCdromDriveLetters() As List(Of String)
Dim list As New List(Of String)
For Each d In DriveInfo.GetDrives()
If d.DriveType = DriveType.CDRom Then
list.Add(d.Name) ' 例: "D:"
End If
Next
Return list
End Function
DriveInfoは「モデル名」などは取れませんが、WMIのように深くデバイスへ問い合わせない分、軽く済む場面が多いです。UI表示や簡易チェック用途なら十分なことがあります。
WMIでも“軽め”のクラスへ寄せる:Win32_LogicalDisk / Win32_Volume
WMIを使い続けたい場合でも、Win32_CDROMDriveではなく論理ディスク側(ドライブ種別)に寄せると、取得の負荷が下がることがあります。
Imports System.Management
Public Function GetCdromLogicalDisks() As List(Of String)
Dim list As New List(Of String)
Dim q As New SelectQuery("SELECT DeviceID, VolumeName FROM Win32_LogicalDisk WHERE DriveType = 5")
Using searcher As New ManagementObjectSearcher(q)
Using results = searcher.Get()
For Each mo As ManagementObject In results
list.Add($"{mo("DeviceID")} {mo("VolumeName")}".Trim())
Next
End Using
End Using
Return list
End Function
ただし、ここで得られるのは論理ドライブ情報が中心です。「Pioneer BDR‑S09XLT」といったモデル名が必須なら、やはり別手段(SetupAPI等)や、Win32_CDROMDriveを安全に叩く設計(タイムアウト/隔離)が必要になります。
実装でハマりやすいポイント:タイムアウトを入れても“完全に安全”とは限らない
このテーマでつまずきやすい点を、先回りで整理します。
「待機のタイムアウト」と「処理のキャンセル」は別物
Task.WhenAnyでタイムアウトした場合、あなたのコードは「待つのをやめた」だけで、WMI呼び出しが裏で止まったままの可能性があります。短時間に何度も走らせると、スレッドやハンドルなどのリソースが積み上がります。
このリスクを抑えるには、次のどれかに寄せるのが現実的です。
- WMI側タイムアウト(
EnumerationOptions.Timeout)を優先して使う - 非同期WMI+Cancelへ寄せる
- 要件が厳しければ別プロセス化で隔離する
ポーリング設計は「同時実行を1本」に絞る
UIで「現在のドライブ一覧」を定期更新するような設計だと、更新中に次の更新が走り、同時にWMIが複数本走ってしまうことがあります。以下のようなルールを決めておくと安定します。
| ルール | 理由 | 実装イメージ |
|---|---|---|
| 同時実行は最大1件 | タイムアウト時にタスクが残る可能性を抑える | 実行中フラグ、SemaphoreSlim |
| 更新間隔を長めに | 書き込み中はそもそも結果が変わりにくい | 30秒〜数分 |
| 失敗時は指数バックオフ | 書き込み完了まで無駄な問い合わせを減らす | 5秒→15秒→60秒… |
例外は“ログ”と“ユーザー向け表示”を分ける
WMI例外(タイムアウト含む)は、技術者にとっては重要でも、ユーザーにそのまま出すと混乱の元です。おすすめは次の分離です。
- ログ:例外種別、WMIクエリ、タイムアウト値、発生時刻、ドライブ状況(書き込み中か)
- UI:短い文言(「使用中のため取得できません」)と再試行の導線
実運用でおすすめの構成(迷ったらこれ)
要件別に、現実的な落としどころを提案します。
一般的なデスクトップアプリ(UIが止まらなければOK)
- WMI照会は必ずワーカースレッド(Task)
EnumerationOptions.Timeoutを設定(例:3〜5秒)- タイムアウトは「使用中」として扱い、一定時間後に再試行
業務アプリ・常駐アプリ(止まると困る/監視用途)
- 上記に加え、非同期WMI(Observer)+Cancelの採用を検討
- それでも不安が残るなら、WMI照会をヘルパーEXEに隔離
モデル名が不要(ドライブレターが分かればいい)
- まず
DriveInfoやWin32_LogicalDiskで代替できないか検討 - WMIの深いクラスに行くのは最後
よくある質問
書き込み中かどうかを事前に判定してWMI照会を避けられませんか?
「書き込み中」を確実に判定する統一的な方法は、アプリ要件や書き込み方式(自前ライティング、他アプリ、OS機能)によって難易度が変わります。一般論としては、判定のための問い合わせ自体がブロックし得るため、事前判定に頼り切るより、問い合わせは止まり得る前提でタイムアウトと隔離を設計する方が安全です。
タイムアウトは何秒が適切ですか?
ユーザー操作に直結するUIなら3〜5秒程度で「使用中」として諦め、バックグラウンド更新なら10〜20秒など、体感と要件で決めるのが現実的です。重要なのは秒数そのものより、「タイムアウトしたらどう扱うか」を仕様化しておくことです。
この問題はPioneer BDR‑S09XLT固有ですか?
特定のドライブやドライバで起きやすい偏りはあり得ますが、構造としては「同期WMI+デバイス応答待ち」に起因するため、光学ドライブ全般や他の外部デバイスでも起こり得ます。したがって、特定機種に閉じた対処ではなく、止まり得るI/Oを安全に扱う設計として取り込むのが再発防止につながります。
まとめ:WMIでデバイスを触るなら「止まる前提」で設計する
Win32_CDROMDriveの列挙が書き込み中にハングするのは、同期WMIがデバイス応答待ちでブロックし続ける可能性があるためです。解決の要点はシンプルで、UIスレッドから外す、タイムアウトを設ける、タイムアウト時の扱いを「使用中」として仕様化することです。
まずはEnumerationOptions.Timeoutを設定し、取得処理をワーカースレッドへ逃がすところから始めてください。要件が厳しければ非同期WMIや別プロセス化へ段階的に強化することで、「書き込み中でもフリーズしない」堅牢な実装にできます。

コメント