.NETのDriveInfoクラスにはCD-ROMドライブの開閉やリムーバブルメディアの取り外しを行うためのメソッドが無いので、APIを使用して実現する。 具体的にはDeviceIoControlを使用してデバイスに直接制御信号を送る。 送信する命令を変えることでドライブのトレイ開閉や、メディアがセットされているかどうかを調べることもできる。

§1 実装

以下のコードでは、次の四つのメソッドを用意して、DriveInfoクラスの拡張メソッドとして呼び出せるようにしている。

Eject
メディアの取り外す、またはドライブのトレイを開く
Load
メディアをセットする、またはドライブのトレイを閉じる
IsTrayOpened
ドライブのトレイが開いているかどうかを返す
HasMedia
ドライブにメディアがセットされているかどうかを返す (DriveInfo.IsReadyと同等)

なお、このコードはNT系のWindowsのみを対象としていて、9x系では動作しない。

using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

class Sample {
  static void Main()
  {
    foreach (var d in DriveInfo.GetDrives()) {
      Console.WriteLine("{0,-10} {1}", d.Name, d.DriveType);
    }

    Console.Write("テストするドライブを指定してください: ");

    var drive = new DriveInfo(Console.ReadLine().TrimEnd());

    if (drive.IsTrayOpened()) {
      Console.WriteLine("トレイを閉じます");

      drive.Load();

      Console.WriteLine("トレイを閉じました");
    }
    else {
      Console.WriteLine("トレイは閉じています");
    }

    Console.WriteLine("メディアをチェックします");

    if (drive.HasMedia()) {
      Console.WriteLine("メディアがセットされています");
    }
    else {
      Console.WriteLine("メディアはセットされていません");
    }

    Console.WriteLine("トレイを開きます");

    drive.Eject();

    Console.WriteLine("トレイを開きました");
  }
}

/// <summary>DriveInfoクラスにトレイの開閉を行う拡張メソッドを追加するクラス</summary>
static class DriveInfoEjectLoadExtensions {
  /// <summary>ドライブにメディアがセットされているかどうかを返す</summary>
  public static bool HasMedia(this DriveInfo drive)
  {
    using (var volume = Open(drive, false)) {
      return IOControl.IOCtl(volume.DangerousGetHandle(), IOControl.IOCTL_STORAGE_CHECK_VERIFY);
    }
  }

  /// <summary>ドライブのトレイが開いているかどうかを返す</summary>
  public unsafe static bool IsTrayOpened(this DriveInfo drive)
  {
    using (var volume = Open(drive, true)) {
      /*
       * http://www.eggheadcafe.com/conversation.aspx?messageid=33820121&threadid=33794406
       * http://forum.sources.ru/index.php?showtopic=225102
       */

      const int dataLength = 8;
      byte* data = stackalloc byte[dataLength];
      SCSI_PASS_THROUGH_DIRECT* sptd = stackalloc SCSI_PASS_THROUGH_DIRECT[1];

      var size = Marshal.SizeOf(typeof(SCSI_PASS_THROUGH_DIRECT));

      sptd[0].Length = (ushort)size;
      sptd[0].PathId = 0;
      sptd[0].TargetId = 0;
      sptd[0].CdbLength = 12;
      sptd[0].DataIn = IOControl.SCSI_IOCTL_DATA_IN;
      sptd[0].DataTransferLength = dataLength;
      sptd[0].TimeOutValue = 5;
      sptd[0].DataBuffer = (void*)data;
      sptd[0].Cdb[0] = 0xbd; // mechanism status
      sptd[0].Cdb[9] = 8; // timeout value

      uint bytesReturned;

      if (!IOControl.DeviceIoControl(volume.DangerousGetHandle(), IOControl.IOCTL_SCSI_PASS_THROUGH_DIRECT, (void*)sptd, (uint)size, (void*)sptd, (uint)size, out bytesReturned, IntPtr.Zero))
        throw new Win32Exception(Marshal.GetLastWin32Error());

      return ((data[1] & 0x10) == 0x10);
    }
  }

  /// <summary>メディアをセットする、またはドライブのトレイを閉じる</summary>
  public static void Load(this DriveInfo drive)
  {
    using (var volume = Open(drive, false)) {
      if (!IOControl.IOCtl(volume.DangerousGetHandle(), IOControl.IOCTL_STORAGE_LOAD_MEDIA))
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }
  }

  /// <summary>メディアの取り外す、またはドライブのトレイを開く</summary>
  public static void Eject(this DriveInfo drive)
  {
    using (var volume = Open(drive, false)) {
      /*
       * http://support.microsoft.com/kb/165721/
       */

      const int maxRetry = 20; // デバイスロックの最大試行回数
      var locked = false;

      for (var t = 0; t < maxRetry; t++) {
        if (IOControl.IOCtl(volume.DangerousGetHandle(), IOControl.FSCTL_LOCK_VOLUME)) {
          locked = true;
          break;
        }
        else {
          System.Threading.Thread.Sleep(1000); // ロックできなければ1秒後に再試行
        }
      }

      if (!locked)
        throw new IOException("デバイスがビジー状態です");

      if (!IOControl.IOCtl(volume.DangerousGetHandle(), IOControl.FSCTL_DISMOUNT_VOLUME))
        throw new Win32Exception(Marshal.GetLastWin32Error());

      unsafe {
        uint bytesReturned;
        PREVENT_MEDIA_REMOVAL* inBuffer = stackalloc PREVENT_MEDIA_REMOVAL[1];

        inBuffer[0].PreventMediaRemoval = 0; // FALSE

        if (!IOControl.DeviceIoControl(volume.DangerousGetHandle(), IOControl.IOCTL_STORAGE_MEDIA_REMOVAL, (void*)inBuffer, (uint)Marshal.SizeOf(typeof(PREVENT_MEDIA_REMOVAL)), (void*)IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero))
          throw new Win32Exception(Marshal.GetLastWin32Error());
      }

      if (!IOControl.IOCtl(volume.DangerousGetHandle(), IOControl.IOCTL_STORAGE_EJECT_MEDIA))
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }
  }

  /// <summary>ドライブを開いてSafeFileHandleを取得する</summary>
  private static SafeFileHandle Open(DriveInfo drive, bool accessWrite)
  {
    uint accessFlags;

    if (drive.DriveType == DriveType.CDRom)
      accessFlags = GENERIC_READ;
    else if (drive.DriveType == DriveType.Removable)
      accessFlags = GENERIC_READ | GENERIC_WRITE;
    else
      throw new NotSupportedException("リムーバブルドライブではありません");

    if (accessWrite)
      accessFlags |= GENERIC_WRITE;

    var handle = CreateFile(string.Format(@"\\.\{0}:", drive.Name[0]),
                            accessFlags,
                            FILE_SHARE_READ | FILE_SHARE_WRITE,
                            IntPtr.Zero,
                            OPEN_EXISTING,
                            0,
                            IntPtr.Zero);

    if (handle == INVALID_HANDLE_VALUE)
      throw new Win32Exception(Marshal.GetLastWin32Error());

    return new SafeFileHandle(handle, true);
  }

  [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
  private static extern IntPtr CreateFile(string lpFIleName,
                                          uint dwDesiredAccess,
                                          uint dwShareMode,
                                          IntPtr lpSecurityAttributes,
                                          uint dwCreationDisposition,
                                          uint dwFlagsAndAttributes,
                                          IntPtr hTemplateFile);

  private const uint GENERIC_READ = 0x80000000;
  private const uint GENERIC_WRITE = 0x40000000;
  private const uint FILE_SHARE_READ = 0x00000001;
  private const uint FILE_SHARE_WRITE = 0x00000002;
  private const uint OPEN_EXISTING = 3;

  private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);

  // DeviceIoControl呼び出し関連のクラス
  private static class IOControl {
    [DllImport("kernel32", SetLastError = true)]
    public static unsafe extern bool DeviceIoControl(IntPtr hDevice,
                                                     uint dwIoControlCode,
                                                     void* lpInBuffer,
                                                     uint nInBufferSize,
                                                     void* lpOutBuffer,
                                                     uint nOutBufferSize,
                                                     out uint lpBytesReturned,
                                                     IntPtr lpOverlapped);

    public const uint FILE_ANY_ACCESS      = 0x00000000;
    public const uint FILE_SPECIAL_ACCESS  = FILE_ANY_ACCESS;
    public const uint FILE_READ_ACCESS     = 0x00000001;
    public const uint FILE_WRITE_ACCESS    = 0x00000002;

    public const byte SCSI_IOCTL_DATA_OUT         = 0;
    public const byte SCSI_IOCTL_DATA_IN          = 1;
    public const byte SCSI_IOCTL_DATA_UNSPECIFIED = 2;

    public const uint METHOD_BUFFERED    = 0;
    public const uint METHOD_IN_DIRECT   = 1;
    public const uint METHOD_OUT_DIRECT  = 2;
    public const uint METHOD_NEITHER     = 3;

    public const uint FILE_DEVICE_CONTROLLER    = 4;
    public const uint FILE_DEVICE_FILE_SYSTEM   = 9;
    public const uint FILE_DEVICE_MASS_STORAGE  = 45;

    public static readonly uint IOCTL_SCSI_BASE                   = FILE_DEVICE_CONTROLLER;
    public static readonly uint IOCTL_SCSI_PASS_THROUGH_DIRECT    = CTL_CODE(IOCTL_SCSI_BASE, 0x0405, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS);

    public static readonly uint FSCTL_BASE                        = FILE_DEVICE_FILE_SYSTEM;
    public static readonly uint FSCTL_LOCK_VOLUME                 = CTL_CODE(FSCTL_BASE, 6, METHOD_BUFFERED, FILE_ANY_ACCESS);
    public static readonly uint FSCTL_DISMOUNT_VOLUME             = CTL_CODE(FSCTL_BASE, 8, METHOD_BUFFERED, FILE_ANY_ACCESS);

    public static readonly uint IOCTL_STORAGE_BASE                = FILE_DEVICE_MASS_STORAGE;
    public static readonly uint IOCTL_STORAGE_CHECK_VERIFY        = CTL_CODE(IOCTL_STORAGE_BASE, 0x0200, METHOD_BUFFERED, FILE_READ_ACCESS);
    public static readonly uint IOCTL_STORAGE_MEDIA_REMOVAL       = CTL_CODE(IOCTL_STORAGE_BASE, 0x0201, METHOD_BUFFERED, FILE_READ_ACCESS);
    public static readonly uint IOCTL_STORAGE_EJECT_MEDIA         = CTL_CODE(IOCTL_STORAGE_BASE, 0x0202, METHOD_BUFFERED, FILE_READ_ACCESS);
    public static readonly uint IOCTL_STORAGE_LOAD_MEDIA          = CTL_CODE(IOCTL_STORAGE_BASE, 0x0203, METHOD_BUFFERED, FILE_READ_ACCESS);

    private static uint CTL_CODE(uint t, uint f, uint m, uint a)
    {
      return (uint)((t << 16) | (a << 14) | (f << 2) | m);
    }

    public unsafe static bool IOCtl(IntPtr hDevice, uint dwIoControlCode)
    {
      uint bytesReturned;

      return DeviceIoControl(hDevice, dwIoControlCode, (void*)IntPtr.Zero, 0, (void*)IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);
    }
  }

  [StructLayout(LayoutKind.Sequential)]
  private unsafe struct SCSI_PASS_THROUGH_DIRECT {
    public ushort Length;
    public byte   ScsiStatus;
    public byte   PathId;
    public byte   TargetId;
    public byte   Lun;
    public byte   CdbLength;
    public byte   SenseInfoLength;
    public byte   DataIn;
    public uint   DataTransferLength;
    public uint   TimeOutValue;
    public void*  DataBuffer;
    public uint   SenseInfoOffset;
    public fixed byte Cdb[16];
  }

  [StructLayout(LayoutKind.Sequential)]
  private struct PREVENT_MEDIA_REMOVAL {
    public byte PreventMediaRemoval;
  }
}

§2 動作例

Q:\に接続されている光学ディスクドライブでの例。 テストに使ったのはIODATA製ブルーレイドライブ、BRD-SH10B。

実行結果例
E:\temp>eject.exe
A:\        Removable
C:\        Fixed
D:\        Removable
E:\        Fixed
F:\        Fixed
Q:\        CDRom
R:\        CDRom
テストするドライブを指定してください: q:\
トレイを閉じます
トレイを閉じました
メディアをチェックします
メディアがセットされています
トレイを開きます
トレイを開きました

D:\に接続されているUSBメモリでの例。 テストに使ったのはSandisk製のもの。 Ejectメソッドを実行すると、「ハードウェアの安全な取り外し」を行った場合と同等の動作となる。

実行結果例
E:\temp>eject.exe
A:\        Removable
C:\        Fixed
D:\        Removable
E:\        Fixed
F:\        Fixed
Q:\        CDRom
R:\        CDRom
テストするドライブを指定してください: d:\
トレイは閉じています
メディアをチェックします
メディアがセットされています
トレイを開きます
トレイを開きました