2021/05/30

タフな2.5インチドライブ用ケース

2.5インチドライブ用の外付けケースとして最もタフな部類に入るSilverStoneのSST-MMS01Bを使っていましたが、StarTech.comのS251BRU31C3に買い替えたので、記録として。

この2つ、ベンダーは違いますが、コントローラー基板以外の筐体は見たとおり全く同じなので、ODM元は同じなのでしょう。

インターフェイスとコネクタは以下のとおり。
  • SST-MMS01B : USB 3.0 : Micro-B 3.0
  • S251BRU31C3 : USB 3.1 : USB-C
Micro-B 3.0はとにかく華奢で、SST-MMS01Bでもいかにもタフな筐体に見合っていない感がありましたが、どうもコネクタが歪んでしまったようで、わざと傾けて力をかけないと認識されなくなったので、引退となりました。その点、S251BRU31C3のUSB-Cはコネクタがカチリと固定されて安定感があります。

筐体の中はどんなものかというと、ドライブはコネクタ以外の全方向がゴム素材で支えられる構造になっていて、縁には防水のパッキンがあります。

蓋は六角ネジで固定するようになっています。外側のゴムジャケットも合わせて厚みは30mmとなるので、ドライブが9.5mm厚として上下に10mmの緩衝スペースがある計算になります。見えませんが、ネジ穴部分の内側にワッシャー型のパッキンがあり、ネジ穴からの防水になっています。

コネクタにはパッキン付きの蓋があります。コネクタの貫通部はシールされているようには見えないので、蓋は閉めておかないと防水性は発揮されないかと。

一応性能確認のため、古いものですがSanDiskのExtreme Proを繋いでMBP2019からAmorphousDiskMarkで計測した結果。コントローラーはasmediaのASM235CMでした。

性能的には十分かと思います。

ということで、データを物理的要因から保護するケースとして現状不満点はないです。なお、難点というほどではないですが、アクセスランプはなく、HDDでも動作音はほとんど漏れてこないので、外からは動作状況が分からないのが注意点ではあります。

2021/05/04

Hello Switcherのサービス化

ノートやタブレットPCでWindows Hello対応の内蔵カメラがある場合、Windows Hello対応のWebカメラを接続しても、Windows Helloに使われるカメラを切り換えられない問題は、Microsoftでも認識されているようです。 最近ようやくというか、周辺機器メーカーから普及帯のWindows Hello対応Webカメラが出ましたし(エレコムのUCAM-CF20FBBK)、既にDellやLenovoからWindows Hello対応カメラ付きのモニターも出ています。中でもMicrosoft Teams専用機能を謳ったC2422HEについて、Dellのサポートに問い合わせてみたところ、やはりPC本体にWindows Hello対応カメラがある場合はモニターのカメラは使えないとの回答でした。この問題にぶち当たるケースが増えるにつれ、Microsoftでもいつまでも放置はされないかもと想像しています。
ともあれ、この問題を解決するためのHello Switcherですが、これまでの実績上、切換え機能自体には問題なさそうです。 一方で、ログイン後という起動タイミングに起因する問題は如何ともしがたく、未解決の問題として残っていました。すなわち、電源オフの間やサスペンド中にUSBカメラの着脱があった場合、その後のログイン前にカメラの切換えは当然できません。

試しにタスクスケジューラでの開始タイミングをスタートアップ時に変えてみましたが(この状態だとUIを出せないので、実用的ではない)、ログイン前にカメラの切換えが間に合う場合もあれば合わない場合もあり、解決策にはならず。

よって、Windowsサービスの実行ファイルも作成し、通常のアプリと併用する(アプリの実行中はサービスはPausedにしておく)方針に変えました。
この結果、切換えが間に合わなくなる場面はほぼなくなりました。ということで、ようやくアプリとして所期の要求を満たすようになりました、パチパチ。

実際、サービスとして動かしている限り、表に出ることもなく自動的に切り換えるので、OSの標準機能と変わりません。これで、この問題は実質的には解決かなと。

2021/05/03

Windowsサービスでデバイスイベントを捕捉する

実行ファイルをWindowsサービスとして動かすために.NETではSystem.ServiceProcess名前空間にServiceContollerServiceBaseクラスが用意されていて、これらに則れば比較的容易にサービスを実装できますが、このServiceBaseの派生クラスからデバイスイベント(USBデバイスの着脱)を捕捉するにはどうすればいいか、鉄板的なものが見当たらなかったので、メモしておきます。

1. 基本

Win32的には、Windowsサービスは大体以下のような仕組みになっているようです。
  1. まずサービス名を指定してCreateService関数でサービスを生成する。
  2. このサービス名とコールバック関数を指定してRegisterServiceCtrlHandler関数を実行すると、サービス自体の管理用(開始、停止等)のコントロールコードがこのコールバック関数に流れてくるようになる。また、この関数の実行時にサービスステータスハンドルが返ってくるので、これをサービスステータスの管理等に利用する。
  3. または、このサービス名とコールバック関数を指定してRegisterServiceCtrlHandlerEx関数を実行すると、2のコントロールコードに加えて、デバイスイベントを含めたシステムイベントのコントロールコードも流れてくるようになる(デバイスイベントの場合は、さらにサービスステータスハンドルを指定してイベントへの登録が必要。この部分はWindowを持つアプリでWindowメッセージを処理するのと基本同じ)。具体的なイベントについては、このコールバック関数(LPHANDLER_FUNCTION_EX)を参照。
ServiceBaseはこの2と、3のシステムイベントについては電源イベント(SERVICE_CONTROL_POWEREVENT)とセッションイベント(SERVICE_CONTROL_SESSIONCHANGE)まで実装していますが、デバイスイベント(SERVICE_CONTROL_DEVICEEVENT)は実装していません。というか、ソースを見ると、実装しかけたまま放置されたようで、これも実装してあれば手間が省けたのですが。

2. 実装

デバイスイベントの捕捉については、ざっと探して以下の先例がありました。
このコードをダウンロードして見ると、ServiceBaseの派生クラス上で上記の3を行い、イベントを独自のコールバック関数で捕捉するようにしています。つまり、ServiceBase内と二重にRegisterServiceCtrlHandlerEx関数を実行しているわけですが、こうするとServiceBase内のコールバック関数へのポインターが上書きされるようで、ServiceBase内のサービスステータス管理用の処理が実行されなくなります。このため、独自のコールバック関数の方でSERVICE_CONTROL_STOPを捕捉してServiceBase.Stopメソッドを呼んでいます。

とりあえず確認したところ、デバイスイベントの捕捉は問題なくできましたが、致命的な問題が。サスペンド時(Windows 10では、標準では「シャットダウン」がサスペンドになる)にAccessViolationExceptionが発生します。これは少し確認したところ、RegisterServiceCtrlHandlerEx関数を実行しただけで起こり、たぶんアンマネージドなハンドル絡みだと思うので、ほぼ対処不能。

ではどうするかというと、既にServiceBase内にコールバックの仕組みはあるので、これを利用できないかとソースを改めて見ると、コールバック関数に来たコントロールコードは途中で捕捉されなければ最終的にServiceBase.OnCustomCommandメソッドに流れてくるので、これを捕捉すればよいと発見(LPHANDLER_FUNCTION_EXにおけるdwControl = ServiceBase.OnCustomCommandにおけるcommand)。カスタムコマンドは128から255までという決まりがあり、SERVICE_CONTROL_DEVICEEVENTは11なので、この範囲に入りませんが、わざわざフィルターしているわけでもないので。

ということで、これなら簡単に実装できます。デバイスイベントへの登録はWindowメッセージの場合とDEVICE_NOTIFY_SERVICE_HANDLE以外は同じ。
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.ServiceProcess;
public partial class DeviceDetectService : ServiceBase
{
public DeviceDetectService()
{
InitializeComponent();
}
private static readonly Guid GUID_DEVINTERFACE_USB_DEVICE = new Guid("A5DCBF10-6530-11D2-901F-00C04FB951ED");
private IntPtr _notificationHandle;
protected override void OnStart(string[] args)
{
DeviceNotification.Unregister(_notificationHandle);
_notificationHandle = DeviceNotification.Register(this.ServiceHandle, GUID_DEVINTERFACE_USB_DEVICE);
}
protected override void OnStop()
{
DeviceNotification.Unregister(_notificationHandle);
_notificationHandle = IntPtr.Zero;
}
protected override void OnCustomCommand(int command)
{
switch (command)
{
case DeviceNotification.SERVICE_CONTROL_DEVICEEVENT:
Debug.WriteLine("USB device event received!");
this.EventLog.WriteEntry("USB device event received!");
break;
}
}
}
public static class DeviceNotification
{
[DllImport("User32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr RegisterDeviceNotification(
IntPtr hRecipient,
IntPtr NotificationFilter,
uint Flags);
[DllImport("User32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnregisterDeviceNotification(IntPtr Handle);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct DEV_BROADCAST_DEVICEINTERFACE
{
public uint dbcc_size;
public uint dbcc_devicetype;
public uint dbcc_reserved;
public Guid dbcc_classguid;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
public string dbcc_name;
}
private const int DEVICE_NOTIFY_SERVICE_HANDLE = 0x00000001;
private const int DBT_DEVTYP_DEVICEINTERFACE = 0x00000005;
public const int SERVICE_CONTROL_DEVICEEVENT = 0x0000000B;
/// <summary>
/// Registers to device events.
/// </summary>
/// <param name="serviceHandle">Service status handle</param>
/// <param name="classGuid">Device interface class Guid</param>
/// <returns>Device notification handle</returns>
public static IntPtr Register(IntPtr serviceHandle, Guid classGuid)
{
var dbcc = new DEV_BROADCAST_DEVICEINTERFACE
{
dbcc_size = (uint)Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>(),
dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
dbcc_classguid = classGuid
};
var buffer = IntPtr.Zero;
try
{
buffer = Marshal.AllocHGlobal((int)dbcc.dbcc_size);
Marshal.StructureToPtr(dbcc, buffer, true);
return RegisterDeviceNotification(
serviceHandle,
buffer,
DEVICE_NOTIFY_SERVICE_HANDLE);
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to register.\r\n{ex}");
return IntPtr.Zero;
}
finally
{
if (buffer != IntPtr.Zero)
Marshal.FreeHGlobal(buffer);
}
}
/// <summary>
/// Unregisters from device events.
/// </summary>
/// <param name="notificationHandle">Device notification handle</param>
public static void Unregister(IntPtr notificationHandle)
{
if (notificationHandle != IntPtr.Zero)
UnregisterDeviceNotification(notificationHandle);
}
}
なお、OnStartでRegisterの前にUnregisterしているのは、OnStartの後に必ずOnStopが来るとは限らないため。

惜しむらくは、一緒に来るdwEventType(WM_DEVICECHANGEにおけるwParamと同じ)とlpEventData(同じくlParamと同じ)は流れてこないので、デバイスイベントが起きたことしか分からないことですが、イベントがあったことが分かればやりようはあるので。

このサンプルのレポジトリは以下。 以上で用には足りますが、ServiceBaseクラスは.NETでサービスを使うには必須の基底クラスなので、もう少し拡張にオープンにしてほしかったというのが率直な印象でした。