WindowsでVHDXにドライブ文字を静かに割り当てる方法(通知・This PC表示を回避/DefineDosDevice)

VHDX をバックグラウンドで大量に作成・展開していると、「ドライブ文字を付けた瞬間に通知が出る」「エクスプローラーの『PC(This PC)』に出てしまう」など、ホスト側のユーザー体験を壊しがちです。本記事では、マウント マネージャー経由の割り当てを避け、DefineDosDevice で“静かに”ドライブ文字を割り当てる実装と運用のポイントを整理します。

目次

よくある症状:VHDX にドライブ文字を付けると通知が出る/This PC に表示される

VHDX(仮想ディスク)をアタッチして、ボリュームにドライブ文字(例:S:)を割り当てたいだけなのに、次のような挙動に困るケースがあります。

  • IVdsVolumeMF::AddAccessPath(または SetVolumeMountPoint 相当)で割り当てると、バルーン等のシェル通知が出る
  • さらに、エクスプローラーの 「PC > デバイスとドライブ」 に表示される
  • ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER でアタッチしても、後から文字を付けると結局通知が出る
  • 通知回避を狙ってディスクをオフライン(DISK_ATTRIBUTE_OFFLINE)にしてから AddAccessPath しようとすると失敗する
  • IOCTL_MOUNTMGR_CREATE_POINT を試すが、DeviceIoControl が エラー 123(パス形式が不正) で詰まる

とくに自動化(バックグラウンド展開・検証・差分同期など)では、ホスト側で通知が連発すると運用上の事故になりやすいので、「ユーザーに見せず、静かにドライブ文字だけ欲しい」というニーズが強くなります。

結論:マウント マネージャー経由の“割り当て”を避け、DefineDosDevice でドライブ文字を作る

ポイントはシンプルです。

マウント マネージャー(Mount Manager)経由でドライブ文字を“割り当てる”と、シェルが反応して通知・表示が発生しやすい。

そこで、別ルートで 「ドライブ文字 → 対象パーティション(またはボリューム)」 の対応付けを作ります。その手段として有効なのが DefineDosDeviceW です。

この方式で狙えること

  • 通知(バルーン等)を抑えたい:DDD_NO_BROADCAST_SYSTEM を使う
  • エクスプローラー(非昇格)に見せたくない:実行コンテキストを工夫する(後述)
  • 大量処理に耐える:割り当て・解除を明示的に管理する

手法比較:どの経路が「通知」「This PC 表示」を引き起こしやすいのか

手法主な API / 仕組み通知が出やすいThis PC に出やすい向く用途
ボリュームにドライブ文字を割り当てSetVolumeMountPoint / VDS の AddAccessPath高い高いユーザーに見せる通常運用/永続的な割り当て
マウント マネージャーへ直接登録IOCTL_MOUNTMGR_CREATE_POINT高い(結局マウント マネージャー)高い特殊用途(要:構造体・名前形式の厳密管理)
VDS でドライブレター付与IVdsAdvancedDisk::AssignDriveLetterケースによるケースによる既存環境互換を優先する場合(ただし後始末注意)
DOS デバイスとして割り当てDefineDosDevice低い(DDD_NO_BROADCAST_SYSTEM)実行コンテキスト次第バックグラウンドで静音に使う一時ドライブ

今回の目的(静音・大量展開)なら、最終行の DefineDosDevice が最も筋が良い、という整理になります。

なぜ AddAccessPath / SetVolumeMountPoint だと「静かにできない」のか

SetVolumeMountPoint や VDS の AddAccessPath は、概念としては「ボリュームに正式なマウントポイントを作る」動きです。これは OS のコンポーネント(マウント マネージャー、PnP、シェルなど)から見て “ドライブが増えた/構成が変わった” に近いイベントになります。

その結果、以下が起こりやすくなります。

  • デバイス到着・構成変更系のイベントがシステム全体に通知される
  • エクスプローラーが「新しいドライブ」として認識し、表示を更新する
  • 環境やポリシー次第でバルーン・トーストなどのユーザー通知が出る

また、ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER は「アタッチ直後の自動割り当てを抑える」には効いても、後からマウント マネージャー経由で文字を付ければ、結局その時点で同等のイベントが発生しやすい点が落とし穴です。

さらに、ディスクをオフライン(DISK_ATTRIBUTE_OFFLINE)にしてから割り当てようとして失敗するのは、そもそもオフライン状態ではボリュームの利用が成立しづらく、マウントポイントの作成も拒否されやすいからです。「見えないようにするためにオフライン」は筋が悪く、静音化には別のアプローチが必要になります。

DefineDosDevice 方式の考え方:ドライブ文字は“マウント”ではなく“名前解決”でも作れる

DefineDosDevice は、ドライブ文字を「ボリュームへ正式に割り当てる」のではなく、DOS デバイス名(例:S:)を、任意のデバイスパスへ結び付けるイメージです。言い換えると、“S: をこのターゲットに向ける” という名前解決の設定を作ります。

そして重要なのが次のフラグです。

  • DDD_NO_BROADCAST_SYSTEM:システム全体へのブロードキャスト通知を抑える(=シェルが反応しにくい)
  • DDD_RAW_TARGET_PATH:ターゲットを「そのままのデバイスパス」として扱う(変換を期待しない)

DefineDosDevice でよく使うフラグ整理

フラグ意味使いどころ
DDD_RAW_TARGET_PATHターゲット文字列を raw(デバイス)パスとして扱うQueryDosDevice で得たデバイスパスをそのまま渡したい
DDD_NO_BROADCAST_SYSTEMシステムへのブロードキャスト通知を行わない通知・UI更新の“起点”を減らしたい(静音化の要)
DDD_REMOVE_DEFINITION定義を削除するデタッチ前のクリーンアップ
DDD_EXACT_MATCH_ON_REMOVE削除時にターゲット完全一致を要求誤削除を避けたい(大量処理で重要)

実装手順(概要):HarddiskXPartitionY → 実体パス → S: を割り当て

大枠の流れは次の通りです。

  1. 対象の VHDX をアタッチする(必要に応じて ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER)
  2. 対象パーティションを表す DOS デバイス名を組み立てる(例:Harddisk2Partition1)
  3. QueryDosDeviceW で、その実体(ターゲット)パスを取得する
  4. DefineDosDeviceW(DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM, "S:", ターゲットパス) で割り当て
  5. 処理が終わったら 必ず DefineDosDeviceW(... DDD_REMOVE_DEFINITION ...) で解除してからデタッチ

この方式の肝は「マウント マネージャーに“ドライブ追加”として登録しない」ことです。結果として、エクスプローラーや通知系が反応するトリガーが大きく減ります。

実装例(C/C++):QueryDosDevice + DefineDosDevice で静音に割り当てる

以下は最小構成の例です。実際にはエラーハンドリング、ログ、競合回避(空きドライブ文字の選定)を追加してください。

#include <windows.h>
#include <string>
#include <vector>

static std::wstring GetFirstMultiSzString(const wchar_t* multiSz)
{
    // QueryDosDevice は MULTI_SZ を返すことがあるが、ここでは先頭要素だけ使う
    if (!multiSz || !*multiSz) return L"";
    return std::wstring(multiSz);
}

static bool QueryTargetPathFromDosDevice(const std::wstring& dosDeviceName, std::wstring& outTarget)
{
    std::vector<wchar_t> buf(32768);
    DWORD len = QueryDosDeviceW(dosDeviceName.c_str(), buf.data(), (DWORD)buf.size());
    if (len == 0) {
        return false;
    }
    outTarget = GetFirstMultiSzString(buf.data());
    return !outTarget.empty();
}

static bool DefineDriveLetterSilently(const std::wstring& drive, const std::wstring& rawTargetPath)
{
    DWORD flags = DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM;
    return DefineDosDeviceW(flags, drive.c_str(), rawTargetPath.c_str()) != 0;
}

static bool RemoveDriveLetterSilently(const std::wstring& drive, const std::wstring& rawTargetPath)
{
    DWORD flags = DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM |
                 DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE;
    return DefineDosDeviceW(flags, drive.c_str(), rawTargetPath.c_str()) != 0;
}

// 使用例
// dosPartitionName = L"Harddisk2Partition1" のように用意しておく
bool AttachWorkAssignLetterAndCleanup(const std::wstring& dosPartitionName, wchar_t letter)
{
    std::wstring target;
    if (!QueryTargetPathFromDosDevice(dosPartitionName, target)) {
        // GetLastError() でログ出し推奨
        return false;
    }

    std::wstring drive;
    drive.push_back(letter);
    drive.append(L":");

    if (!DefineDriveLetterSilently(drive, target)) {
        return false;
    }

    // ここで S:\... として作業する

    // 終了時は必ず解除(重要)
    RemoveDriveLetterSilently(drive, target);
    return true;
}

「HarddiskXPartitionY」をどう作るか(実務メモ)

ここが環境依存になりやすい部分です。一般に、以下いずれかの流れで X と Y を特定します。

  • VHDX をアタッチ → GetVirtualDiskPhysicalPath 等で \\.\PhysicalDriveN を得る → パーティションレイアウト(例:IOCTL_DISK_GET_DRIVE_LAYOUT_EX)から Y を選ぶ
  • アタッチ後にボリューム列挙(ボリューム GUID、デバイス名、ファイルシステム種別など)を行い、目的のボリュームを特定して対応付ける

大量処理では「どの VHDX のどのパーティションか」を確実に識別できるよう、VHDX 内のラベル、GUID、既知のパーティション構成、メタ情報ファイルなどと組み合わせると事故が減ります。

実装例(C#):DefineDosDevice の P/Invoke で同じことをする

Windows サービスやバッチ処理を C# で組む場合の例です(概念は同じ)。

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;

public static class SilentDriveLetter
{
    private const int DDD_RAW_TARGET_PATH = 0x00000001;
    private const int DDD_REMOVE_DEFINITION = 0x00000002;
    private const int DDD_EXACT_MATCH_ON_REMOVE = 0x00000004;
    private const int DDD_NO_BROADCAST_SYSTEM = 0x00000008;

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern bool DefineDosDevice(int dwFlags, string lpDeviceName, string lpTargetPath);

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern uint QueryDosDevice(string lpDeviceName, StringBuilder lpTargetPath, int ucchMax);

    public static string QueryTarget(string dosDeviceName)
    {
        var sb = new StringBuilder(32768);
        uint len = QueryDosDevice(dosDeviceName, sb, sb.Capacity);
        if (len == 0)
            throw new Win32Exception(Marshal.GetLastWin32Error());

        // MULTI_SZ の先頭だけ利用(必要なら分解処理を追加)
        string all = sb.ToString();
        int nullIndex = all.IndexOf('\0');
        return nullIndex >= 0 ? all.Substring(0, nullIndex) : all;
    }

    public static void Assign(string driveLetterWithColon, string rawTargetPath)
    {
        int flags = DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM;
        if (!DefineDosDevice(flags, driveLetterWithColon, rawTargetPath))
            throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    public static void Remove(string driveLetterWithColon, string rawTargetPath)
    {
        int flags = DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM |
                    DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE;
        if (!DefineDosDevice(flags, driveLetterWithColon, rawTargetPath))
            throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

// 使い方例
// var target = SilentDriveLetter.QueryTarget("Harddisk2Partition1");
// SilentDriveLetter.Assign("S:", target);
// ... S:\ に対して作業 ...
// SilentDriveLetter.Remove("S:", target);

重要:This PC に「見える/見えない」は実行権限(トークン)で変わる

DDD_NO_BROADCAST_SYSTEM は「通知の起点」を減らしますが、ドライブ文字そのものが OS に存在する以上、同じ名前空間を見ているプロセスには列挙され得ます。つまり、最終的に This PC に出るかどうかは、どのコンテキスト(どのトークン/セッション/名前空間)にドライブ文字を作ったかが効いてきます。

実務で使いやすい整理は次の通りです。

実行コンテキストドライブ文字が作られる名前空間非昇格 Explorer から見える用途の向き
LocalSystem(サービス等)グローバル寄り(システム全体に影響しやすい)見える可能性がある共有したい/全体に公開したい(静音だけ欲しい用途には注意)
管理者で昇格して実行昇格側のデバイスマップ(分離されやすい)見えにくい(=ユーザーに見せず内部処理したい用途に向く)バックグラウンド処理・検証・展開
非昇格で実行通常ユーザー側見える(=This PC に出やすい)ユーザーに見せるツール、手作業運用

つまり「通知を抑える」だけならフラグで効きますが、「表示まで抑えたい」なら 昇格側(または Explorer と分離されたコンテキスト)で割り当てるのが現実的です。

運用の実感としての注意点

  • “絶対に見えない”の保証ではない:Explorer を再起動したり、別の権限で動く管理ツールが列挙した場合は見える可能性があります
  • 同じユーザーでも「昇格」と「非昇格」で見えるドライブが違う:ネットワークドライブと同じで、UAC の分離が効きます
  • 必要なプロセスがどちら側で動いているか(例:バックグラウンド処理は昇格、UI は非昇格)を設計段階で揃えるとトラブルが減ります

大量の VHDX を捌くときの運用設計:静音よりも「後始末」が重要

DefineDosDevice 方式は静音化に強い一方で、割り当てが永続化されない/マウント マネージャーの管理外という性質があります。大量処理ではここがメリットにもデメリットにもなるので、運用を明確にしておくのがおすすめです。

最低限やっておきたいチェックリスト

項目理由実装のコツ
割り当て前に競合チェック既に使われている文字に上書きすると事故るQueryDosDevice で存在確認、またはドライブ文字プールを管理
解除を必ず実行デタッチ後に“残骸”が残ると次回の衝突原因finally/RAII で DDD_REMOVE_DEFINITION を保証
例外・中断時のクリーンアップバックグラウンド処理は途中停止が起こりやすいプロセス起動時に「自分が作ったドライブ」を掃除するルーチンを用意
ログに「VHDX ⇔ 文字 ⇔ ターゲット」を残す障害時の追跡が楽VHDX パス、PhysicalDrive 番号、Partition 番号、割り当て文字を記録

「ドライブ文字が必要な期間」を短くする

静音の目的が「ユーザー体験の保護」なら、ドライブ文字を付けっぱなしにしない設計が効きます。

  • 作業開始直前に割り当てる
  • 作業が終わったら即解除
  • 失敗しても解除だけは最後まで走らせる

この運用だと、たとえ何かの拍子に表示されても “一瞬だけ” になり、実害が小さくなります。

IOCTL_MOUNTMGR_CREATE_POINT がエラー 123 になりやすい理由(そしておすすめしない理由)

IOCTL_MOUNTMGR_CREATE_POINT はマウント マネージャーへマウントポイントを“直に”登録する系の API です。強力ですが、次の点で今回の目的(静音)と相性が良くありません。

  • 結局マウント マネージャー経由になりやすく、通知・表示を誘発しやすい
  • 入力構造体(オフセット・長さ)と文字列形式が厳密で、少しでも形式がズレるとエラー 123(パス形式が不正)になりやすい
  • ボリューム名/シンボリックリンク名の指定ルールが分かりづらく、実装・保守コストが高い

「マウント マネージャーに登録したい(永続化したい)」という要件なら検討の余地はありますが、「静かに一時ドライブが欲しい」なら、DefineDosDevice の方が目的に直結します。

補足:IVdsAdvancedDisk::AssignDriveLetter を使う場合の注意(“残骸”問題)

環境によっては IVdsAdvancedDisk::AssignDriveLetter が「通知が出ない」ように見えることもあります。ただし運用上の落とし穴として、割り当てを削除しないままデタッチすると、Explorer 側に表示の残骸が残るような現象が起こることがあります。

大量処理では “たまに残る” が一番厄介です。静音・クリーン運用を優先するなら、

  • 自分で割り当てたものは自分で必ず消す
  • 削除操作を失敗させない(例外でも必ず到達させる)

が徹底しやすい DefineDosDevice 方式の方が管理しやすい、という判断になります。

代替案:そもそもドライブ文字を使わずにアクセスする(要件次第で最強)

もし「ドライブ文字が必須ではない(ツールの制約ではなく慣習で使っているだけ)」なら、より静かにできる選択肢があります。

  • ボリューム GUID パス(例:\\?\Volume{GUID}\)でアクセスする
  • アプリ側でパスを受け取れるなら、ドライブレターを作らない

この方式は “ドライブが増えた” という見え方をしにくく、UI への影響も小さくできます。一方で、古いツールや一部のライブラリがドライブ文字前提の場合は適用できません。その場合にこそ、DefineDosDevice で一時的にドライブ文字を提供する戦略が刺さります。

まとめ:VHDX のドライブ文字を静かに付けたいなら「DefineDosDevice + 実行コンテキスト設計」

VHDX を大量にバックグラウンド展開する場合、AddAccessPath や SetVolumeMountPoint といった “正攻法の割り当て” は、ホスト側のシェルが反応して通知・This PC 表示を引き起こしがちです。

そこで、

  • DefineDosDeviceW でドライブ文字を作る
  • DDD_NO_BROADCAST_SYSTEM で通知の起点を抑える
  • 見せたくない相手(非昇格 Explorer)と別の実行コンテキストで割り当てる
  • 解除(クリーンアップ)を必須にする

という組み合わせが、静音・大量処理の現場で最も扱いやすい解になります。ドライブ文字を“マウント”として捉えるのではなく、“名前解決(DOS デバイス)”として提供する発想に切り替えると、ホスト側のノイズを大きく減らせます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次