デバイスの追加・削除の通知をアプリケーションで受け取る方法
皆さん、こんにちは。A寿です。
突然ですが、皆さんは、かつお節を使った和食の「本物の味」を味わったことはありますか?・・・このお話にご興味のある方は本文の最後の【閑話休題】までどうぞ。
さて、今回は、デバイスの追加・削除の通知をアプリケーションで受け取る方法をご紹介しようと思います。前回の私の記事で、ToasterサンプルのNotify.exeの使い方をご紹介しました。その記事で、Notify.exeでToasterデバイスを追加・削除した場合に、Notify.exeのウィンドウに、対応するメッセージが表示されていました。今回は、Toasterデバイスが追加・削除されて、Notify.exeのウィンドウに、対応するメッセージが表示されるまでに、Notify.exe(アプリ側)とToaster.sys(ドライバ側)それぞれに必要となる実装について見ていきたいと思います。
まず、Notify.exeがToaster.sysから受け取る通知には以下の2種類があります。
(A) デバイスインターフェイスに関する通知
(B) デバイスハンドルに関する通知
それぞれについて、以下の3つの処理があります。
(1) 通知の受け取りの開始(登録)
(2) 通知の受け取り
(3) 通知の受け取りの終了(登録解除)
Notify.exeの操作と、(A),(B)それぞれの(1)~(3)の処理は、以下の表のような関係になります。
Notify.exe の操作 |
起動 (ウィンドウの作成) |
デバイス 追加 |
デバイス 削除 |
終了 (ウィンドウのクローズ) |
(A) |
(1) |
(2) |
(2) |
(3) |
(B) |
((1)(※)) |
(1) |
(2),(3) |
|
(※:Notify.exe起動前に、すでにToasterデバイスが追加されていた場合。)
それでは、(A)から順番に、(1)~(3)の通知についてNotify.exeとToaster.sysの処理を見ていきましょう。(以下の説明は、可能であれば、ソースコードを見たり、実機やデバッガを動かしたりしながら、お読みいただければ幸いです。)
(A) デバイスインターフェイスの場合
(1) 通知の受け取りの開始(登録)
アプリケーション側で、通知の受け取りを開始するためには、RegisterDeviceNotification() のAPIを使います。後程説明しますが、デバイスインターフェイスだけでなく、デバイスハンドルもこの関数を使います。
Notify.exeでは、ウィンドウが作成されるタイミングで、toaster.sysのデバイスインターフェイスに対して、RegisterDeviceNotification()を呼びます。
RegisterDeviceNotification()周辺の具体的な処理を説明する前に、Notify.exeの最初の処理を簡単に説明しておきます。Notify.exeが実行されると、まずnotify.cの133行目から始まるWinMain()が実行されます。このWinMain()で、CreateWindow()(notify.cの166~176行目)が実行されることで、「Toaster Package Test Application」というタイトルのウィンドウが作成されます。この時、このウィンドウのウィンドウプロシージャ(WNDCLASS構造体のlpfnWndProc)であるWndProc()で、WM_CREATEというメッセージを受け取ります。このWM_CREATEの処理(notify.cの211~240行目)で、デバイスインターフェイスに対して、通知の受け取りの開始の処理を行います。(本当は、上表のように、既存のデバイスに対するデバイスハンドルについての通知の登録も、ここで行いますが、今回はこの部分についての話は割愛します。)
デバイスインターフェイスに関する通知の登録は、以下の抜粋(notify.cの232~235行目)のように行われます。
232 filter.dbcc_size = sizeof(filter); 233 filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; 234 filter.dbcc_classguid = InterfaceGuid; 235 hInterfaceNotification = RegisterDeviceNotification(hWnd, &filter, 0); |
232~234行目は、DEV_BROADCAST_DEVICEINTERFACE構造体の初期化です。
ポイントは、233行目の、dbcc_devicetypeにDBT_DEVTYP_DEVICEINTERFACEをセットしているところです。デバイスインターフェイスもデバイスハンドルも同じRegisterDeviceNotification()を使いますが、DBT_DEVTYP_DEVICEINTERFACEによってデバイスインターフェイスに関する通知であることを指定しています。また、RegisterDeviceNotification()は、以下のドキュメントの通り、2nd parameterはLPVOID型ですが、DBT_DEVTYP_DEVICEINTERFACEを指定していることによって、DEV_BROADCAST_DEVICEINTERFACE構造体を使用することができます。
RegisterDeviceNotification Function
https://msdn.microsoft.com/en-us/library/aa363431(VS.85).aspx
HDEVNOTIFY WINAPI RegisterDeviceNotification( __in HANDLE hRecipient, __in LPVOID NotificationFilter, __in DWORD Flags ); |
【補足】RegisterDeviceNotificationの2nd parameterについて
2nd parameterは、LPVOID型といっても、上記ドキュメントの以下の記述の通り、「DEV_BROADCAST_HDR構造体で始まる構造体」へのポインタを使うことになっています。
NotificationFilter [in] A pointer to a block of data that specifies the type of device for which notifications should be sent. This block always begins with the DEV_BROADCAST_HDR structure. The data following this header is dependent on the value of the dbch_devicetype member, which can be DBT_DEVTYP_DEVICEINTERFACE or DBT_DEVTYP_HANDLE. For more information, see Remarks.
DEV_BROADCAST_HDR構造体は、以下のドキュメントのように定義されています。
DEV_BROADCAST_HDR Structure https://msdn.microsoft.com/en-us/library/aa363246(VS.85).aspx
これに対して、今回使用しているDEV_BROADCAST_DEVICEINTERFACE構造体は、以下のドキュメントのように定義されています。
DEV_BROADCAST_DEVICEINTERFACE Structure https://msdn.microsoft.com/ja-jp/library/aa363244(en-us,VS.85).aspx
つまり、DEV_BROADCAST_HDR構造体は、DEV_BROADCAST_DEVICEINTERFACE構造体や後述のDEV_BROADCAST_HANDLE構造体の先頭(ヘッダ)部分を表す構造体になります。 |
234行目では、DEV_BROADCAST_DEVICEINTERFACE構造体のdbcc_classguidに、デバイスインターフェイスクラスのGUIDを指定します。今回は、Toaster.sysというファンクションドライバのデバイスインターフェイスクラスとして、InterfaceGuidが指定されています。InterfaceGuidは、WinMain()(notify.cの147行目)で、
InterfaceGuid = GUID_DEVINTERFACE_TOASTER;
という値が指定されています。この、GUID_DEVINTERFACE_TOASTERは、toaster\wdm\inc\public.hの43行目あたりに以下のように定義されています。
37 // Define an Interface Guid for toaster device class. 38 // This GUID is used to register (IoRegisterDeviceInterface) 39 // an instance of an interface so that user application 40 // can control the toaster device. 41 // 42 43 DEFINE_GUID (GUID_DEVINTERFACE_TOASTER, 44 0x781EF630, 0x72B2, 0x11d2, 0xB8, 0x52, 0x00, 0xC0, 0x4F, 0xAD, 0x51, 0x71); 45 //{781EF630-72B2-11d2-B852-00C04FAD5171} |
このGUIDをToaster.sysのデバイスインターフェイスクラスとして使えるようにしているのは、Toaster.sysのToasterAddDevice()の以下の処理(toaster.cの290~294行目)です。
290 status = IoRegisterDeviceInterface ( 291 PhysicalDeviceObject, 292 (LPGUID) &GUID_DEVINTERFACE_TOASTER, 293 NULL, 294 &fdoData->InterfaceName); |
【補足】デバイスインターフェイスクラスについて
デバイスインターフェイスクラスについては、英語では、下記ドキュメントをご参照ください。
Device Interface Classes https://msdn.microsoft.com/en-us/library/ff541339(VS.85).aspx
日本語では、まさかたさんの下記の記事が参考になると思います。
SetupDi API と DevCon ~ SetupDi API の使い方編 ~ https://blogs.msdn.com/b/jpwdkblog/archive/2009/07/15/setupdi-api-devcon-setupdi-api.aspx
なお、ご自身でドライバを開発されない場合でも、システム(OS)が提供しているデバイスインターフェイスクラスを使って通知を受け取りたい場合があると思います。そのような場合は、以下のドキュメントから、ご希望のデバイスのカテゴリへのリンクをたどっていただき、該当するGUIDをご利用いただくのがよいかと思います。
System-Defined Device Interface Classes https://msdn.microsoft.com/en-us/library/ff553412(VS.85).aspx
|
以上のように初期化したDEV_BROADCAST_DEVICEINTERFACE構造体を、notify.cの235行目のRegisterDeviceNotification()で登録します。通知の受け取り手は、RegisterDeviceNotification()の3rd parameterで0(=DEVICE_NOTIFY_WINDOW_HANDLE)と指定しているので、ウィンドウのハンドルです。どのウィンドウのハンドルが通知を受け取るかを1st parameterのhWndで指定しています。
RegisterDeviceNotification()の戻り値は、hInterfaceNotificationに代入されています。これは以下のようにnotify.cの64行目に定義されているグローバル変数です。
64 HDEVNOTIFY hInterfaceNotification; |
この値は、後程、(3)で登録解除の際に使われます。
(2) 通知の受け取り
デバイスインターフェイスの場合、前述の図の通り、Toasterデバイスの追加と削除のタイミングで、通知を受け取ります。デバイスインターフェイスの通知は、ドライバ側でIoSetDeviceInterfaceState() を呼ぶことにより有効化・無効化します。IoSetDeviceInterfaceState()によるデバイスインターフェイスの有効化・無効化のタイミングで、RegisterDeviceNotification()で「通知を受け取りたい」という意思表明をしたコンポーネント(ここではnotify.exe)に、通知をします。
IoSetDeviceInterfaceState()の定義は、以下のドキュメントのようになっています。
IoSetDeviceInterfaceState
https://msdn.microsoft.com/en-us/library/ff549700(VS.85).aspx
NTSTATUS IoSetDeviceInterfaceState( __in PUNICODE_STRING SymbolicLinkName, __in BOOLEAN Enable ); |
この2nd parameterをTRUEにすれば有効(enable)、FALSEにすれば無効(disable)になります。
IoSetDeviceInterfaceState()は、同ドキュメントの以下の記述にもあるように、通常IRP_MN_START_DEVICEで有効、IRP_MN_SURPRISE_REMOVEとIRP_MN_REMOVE_DEVICEで無効にします。
A function or a filter driver typically calls this routine with Enable set to TRUE after it successfully starts a device in response to an IRP_MN_START_DEVICE IRP. Such a driver should disable the device interface instance (that is, call IoSetDeviceInterfaceState and set Enable to FALSE) when it removes the device in response to an IRP_MN_REMOVE_DEVICE IRP or an IRP_MN_SURPRISE_REMOVAL IRP.
それでは、デバイスの追加・削除それぞれの処理を説明しながら、IoSetDeviceInterfaceState()が呼ばれるタイミングを確認しましょう。
■デバイスの追加の場合
[ユーザーモード]
Toasterデバイスの追加は、Notify.exeのメニューの[Bus]-[PlugIn]で行います。
これを実施しますと、Notify.exeがWM_COMMANDのウィンドウメッセージを受け取ります。
これにより、WndProc()はHandleCommands()を呼びます(notify.cの208行目)。[PlugIn]に該当する処理は、IDM_PLUGINが該当します。(notify.rcをご参照ください。)
HandleCommands()のIDM_PLUGINの処理で、OpenBusInterface()を呼び出します(notify.cの366行目)。
OpenBusInterface()では、busenum.sysにIOCTL_BUSENUM_PLUGIN_HARDWAREのI/O Controlを送ります(notify.cの1082~1086行目)。
[カーネルモード]
busenum.sysのBus_IoCtl()は、IOCTL_BUSENUM_PLUGIN_HARDWAREを受け取ったら、Bus_PlugInDevice()を呼びます(busenum.cの260~283行目)。
Bus_PlugInDevice()は、IoCreateDeviceSecure()でPDOを作り(pnp.cの1205~1213行目)、Bus_InitializePdo()で初期化し(pnp.cの1234行目)、IoInvalidateDeviceRelations()でPnPのクエリを発生させます(pnp.cの1242行目)。
busenum.sys側でのPnPの処理が終わると、Toaster.sysのToasterAddDevice()が呼ばれます(toaster.cの180行目)。
そこでは、Toaster.sysのデバイスオブジェクトが作られ(toaster.cの205~211行目)、busenum.sysのデバイスオブジェクトにアタッチされます(toaster.cの282行目)。
そしてToaster.sysのPnPの処理が進み、ToasterStartDevice()が呼ばれます(toaster.cの410行目)。
toaster.cの1318行目で、以下のように、IoSetDeviceInterfaceState()が呼ばれます。
1318 status = IoSetDeviceInterfaceState(&FdoData->InterfaceName, TRUE); |
IoSetDeviceInterfaceState()によるユーザーモードへの通知は非同期的に行われます。
[ユーザーモード]
Notify.exeは、WM_DEVICECHANGEのウィンドウメッセージを受け取ります。
この時、WndProc()は、3rd parameterのWPARAM wParamにDBT_DEVICEARRIVAL(0x8000)を受け取ります。
そして、4th parameterのLPARAM lParamは、notify.cの201行目で、DEV_BROADCAST_HDR構造体のポインタにキャストされますが、このdbch_devicetypeには、DBT_DEVTYP_DEVICEINTERFACEが入っています。
これにより、WndProc()は、以下のように、notify.cの277行目で、HandleDeviceInterfaceChange()を呼びます。
277 HandleDeviceInterfaceChange(hWnd, nEventType, (PDEV_BROADCAST_DEVICEINTERFACE) p); |
この中で、前回の記事の「(8) notify.exeでtoasterデバイスを追加」の一番下の図のように、「New device Arrived (Interface Change Notification): ToasterDevice01」と「Opened handled to the device: ToasterDevice01」のメッセージが表示されます。これらは、それぞれ、notify.cの以下の場所で出力されます。
503 Display(TEXT("New device Arrived (Interface Change Notification): %ws"), 504 deviceInfo->DeviceName); |
520 Display(TEXT("Opened handled to the device: %ws"), deviceInfo->DeviceName); |
■デバイスの削除の場合
[ユーザーモード]
Toasterデバイスの削除は、Notify.exeのメニューの[Bus]-[UnPlug(Surprise Removal)]で行います。
これを実施しますと、Notify.exeがWM_COMMANDのウィンドウメッセージを受け取ります。
これにより、WndProc()はHandleCommands()を呼びます(notify.cの208行目)。
[UnPlug(Surprise Removal)]に該当する処理は、IDM_UNPLUGが該当します。(notify.rcをご参照ください。)
HandleCommands()のIDM_UNPLUGの処理で、OpenBusInterface()を呼び出します(notify.cの375行目)。
OpenBusInterface()では、busenum.sysにIOCTL_BUSENUM_UNPLUG_HARDWAREのI/O Controlを送ります(notify.cの1103~1107行目)。
[カーネルモード]
busenum.sysのBus_IoCtl()は、IOCTL_BUSENUM_UNPLUG_HARDWAREを受け取ったら、Bus_UnPlugDevice()を呼びます(busenum.cの285~296行目)。
Bus_UnPlugDevice()は、ユーザーが入力したSerialNoと一致するデバイスを探し(pnp.cの1299~1318行目)、見つかったらIoInvalidateDeviceRelations()でPnPのクエリを発生させます(pnp.cの1322~1325行目)。
PnPの処理が進むと、Toaster.sysのToasterDispatchPnp()でIRP_MN_SURPRISE_REMOVALの処理が行われます(toaster.cの561~597行目)。
その時、toaster.cの568行目で、以下のようにIoSetDeviceInterfaceState()を呼び出します。
568 status = IoSetDeviceInterfaceState(&fdoData->InterfaceName, FALSE); |
[ユーザーモード]
Notify.exeは、WM_DEVICECHANGEのウィンドウメッセージを受け取ります。
この時、WndProc()は、3rd parameterのWPARAM wParamにDBT_DEVICEREMOVECOMPLETE(0x8004)を受け取ります。
そして、4th parameterのLPARAM lParamは、notify.cの201行目で、DEV_BROADCAST_HDR構造体のポインタにキャストされますが、このdbch_devicetypeには、DBT_DEVTYP_DEVICEINTERFACEが入っています。
これにより、WndProc()は、以下のように、notify.cの277行目で、HandleDeviceInterfaceChange()を呼びます。
277 HandleDeviceInterfaceChange(hWnd, nEventType, (PDEV_BROADCAST_DEVICEINTERFACE) p); |
この中で、前回の記事の「(9) notify.exeでtoasterデバイスを削除」の一番下の図の一番下の行のように、「Remove Complete (Interface Change Notification)」のメッセージが表示されます。これは、notify.cの以下の場所で出力されます。
531 Display(TEXT("Remove Complete (Interface Change Notification)")); |
(3) 通知の受け取りの終了(登録解除)
アプリケーション側で、通知の受け取りの終了(登録解除)を行うためには、UnregisterDeviceNotification() のAPIを使います。デバイスインターフェイスだけでなく、デバイスハンドルもこの関数を使います。(RegisterDeviceNotification()と同様です。)
Notify.exeでは、ウィンドウがクローズされるタイミングで、toaster.sysのデバイスインターフェイスに関連したデバイス通知ハンドルに対して、UnregisterDeviceNotification()を呼びます。Notify.exeのウィンドウをクローズすると、WndProc()で、WM_CLOSEというメッセージを受け取ります。このWM_CLOSEの処理(notify.cの289~292行目)で、以下のようにデバイスインターフェイスに対して、通知の受け取りの終了(登録解除)の処理を行います。
291 UnregisterDeviceNotification(hInterfaceNotification); |
UnregisterDeviceNotification()の引数は、以下のドキュメントのように、RegisterDeviceNotification()の戻り値であるデバイス通知ハンドルです。(1)の記述を読み返していただくと、グローバル変数hInterfaceNotificationに、RegisterDeviceNotification()の戻り値をセットしていたことがお分かりいただけると思います。
UnregisterDeviceNotification Function
https://msdn.microsoft.com/en-us/library/aa363475(VS.85).aspx
BOOL WINAPI UnregisterDeviceNotification( __in HDEVNOTIFY Handle ); |
Handle [in]
Device notification handle returned by the RegisterDeviceNotification function.
(B) デバイスハンドルの場合
(1) 通知の受け取りの開始(登録)
デバイスハンドルの通知についての登録は、デバイスインターフェイスの場合と同様、RegisterDeviceNotification()を使います。ただ、呼び出すタイミングやRegisterDeviceNotification()の2nd parameterにセットするデータが、デバイスインターフェイスの場合とは異なります。
デバイスハンドルの通知についての登録の場合、RegisterDeviceNotification()を呼ぶタイミングは、デバイスが追加された後になります。これは、デバイスが追加された後でないと、デバイスをオープンして、デバイスハンドルを取得することができないからです。そのため、前述の表のように、ウィンドウを作成した時にすでにデバイスがある場合と、デバイスが追加された場合のぞれぞれでデバイスハンドルを取得し、RegisterDeviceNotification()を呼んでいます。
具体的なコードの場所として、デバイスが追加された場合を見てみましょう。前述のとおり、デバイスが追加され、デバイスインターフェイスが有効化された通知をnotify.exeが受けると、HandleDeviceInterfaceChange()が呼ばれます。notify.cの482~528行目が、HandleDeviceInterfaceChange()の「case DBT_DEVICEARRIVAL」の処理が行われているところです。この部分のうち、以下の処理が、デバイスハンドルの通知についての登録の処理になります。
521 memset (&filter, 0, sizeof(filter)); //zero the structure 522 filter.dbch_size = sizeof(filter); 523 filter.dbch_devicetype = DBT_DEVTYP_HANDLE; 524 filter.dbch_handle = deviceInfo->hDevice; 525 526 deviceInfo->hHandleNotification = 527 RegisterDeviceNotification(hWnd, &filter, 0); |
521~524行目は、DEV_BROADCAST_HANDLE構造体の初期化です。
ポイントは、523行目の、dbch_devicetypeにDBT_DEVTYP_HANDLEをセットしているところです。これを指定することで、RegisterDeviceNotification()の2nd parameterにDEV_BROADCAST_HANDLE構造体をセットすることができます。
524行目では、DEV_BROADCAST_HANDLE構造体のdbch_handleに、デバイスのハンドルを指定します。今回は、この処理の直前で以下のようにデバイスハンドルを取得しています。
512 deviceInfo->hDevice = CreateFile(dip->dbcc_name, 513 GENERIC_READ |GENERIC_WRITE, 0, NULL, 514 OPEN_EXISTING, 0, NULL); |
dip->dbcc_nameには、例えば、以下のようなデバイス名が入っています。
以上のように初期化したDEV_BROADCAST_HANDLE構造体を、527行目のRegisterDeviceNotification()の2nd parameterにセットして、登録します。RegisterDeviceNotification()のその他の引数については、デバイスインターフェイスのところ((A)の(1))で述べたとおりです。
(2) 通知の受け取り
デバイスハンドルの場合、前述の図の通り、Toasterデバイスの削除のタイミングで、通知を受け取ります。Notify.exeのメニューの[Bus]-[UnPlug(Surprise Removal)]でToasterデバイスを削除すると、結果的に、WndProc()でWM_DEVICECHANGEを受け取ります。この時、WndProc()は、3rd parameterのWPARAM wParamにDBT_DEVICEREMOVECOMPLETE(0x8004)を受け取ります。そして、4th parameterのLPARAM lParamは、notify.cの201行目で、DEV_BROADCAST_HDR構造体のポインタにキャストされますが、このdbch_devicetypeには、DBT_DEVTYP_HANDLEが入っています。これにより、WndProc()は、以下のように、notify.cの280行目で、HandleDeviceChange()を呼びます。
280 HandleDeviceChange(hWnd, nEventType, (PDEV_BROADCAST_HANDLE) p); |
この中で、前回の記事の「(9) notify.exeでtoasterデバイスを削除」の一番下の図の最初の2行のように、「Remove Complete (Handle Notification):ToasterDevice01」と「Closed handle to device ToasterDevice01」のメッセージが表示されます。これは、notify.cの以下の場所で出力されます。
599 Display(TEXT("Remove Complete (Handle Notification):%ws"), 600 deviceInfo->DeviceName); |
614 Display(TEXT("Closed handle to device %ws"), deviceInfo->DeviceName ); |
後者(614行目)の方のメッセージの前に、以下のように、このデバイスハンドルをクローズしています。
612 CloseHandle(deviceInfo->hDevice); |
つまり、デバイスの削除の通知をデバイスハンドルについての通知として受け取ることで、そのデバイスハンドルをクローズするタイミングがわかる、というわけです。
(3) 通知の受け取りの終了(登録解除)
デバイスハンドルの場合も、UnregisterDeviceNotification()で通知の受け取りの終了(登録解除)を行います。Notify.exeでは、デバイスハンドルについてデバイスの削除が通知されたタイミングで、そのデバイスハンドルに関連したデバイス通知ハンドルに対して、UnregisterDeviceNotification()を呼びます。具体的なコードの場所は、以下です。
607 UnregisterDeviceNotification(deviceInfo->hHandleNotification); |
以上が、デバイスの追加・削除の通知をアプリケーションで受け取る方法になります。お役に立てれば幸いです。
それでは、皆さん、よいお年をお迎えください。
――――――――――――――――
【閑話休題】突然ですが、皆さんは、かつお節を使った和食の「本物の味」を味わったことはありますか?
突然ですが、皆さんは、かつお節を使った和食の「本物の味」を味わったことはありますか?
結論から言うと、私はありません。正確には、味わえるはずだったのですが、味わったのか味わってないのかよくわかりませんでした。
数年前、カルチャーセンター主催のかつお節の講座に参加した時の話です。この講座は、大人向けの「食育」を目的に、かつお節が題材に選ばれたものです。講座の内容自体は、某水産加工品メーカーの方が、かつお節ができるまでの工程を開設してくださったり、女性の料理研究家の方が、「本物の味」として、かつお節をメインにだしをとった味噌汁や、かつお節と醤油のかかったご飯などをふるまってくれたり、自分でかつお節を削る体験コーナーもあったりするなど、非常に充実したものでした。
その料理研究家の先生が、講義中に料理を出してくれた時のことです。先生はできたての料理を出すために、受講生の目の前で調理されるのですが、お弟子さんの一人が、どうやら分量を間違えたらしいのです。先生は、激しく怒って、授業中にもかかわらず、ヒステリックに弟子を叱り始めました。そして、やっと叱り終えたかと思うと、時間の都合上、料理をやり直すわけにもいかないので、そのまま料理は受講生に配られました。が、意外にも、私や他の受講生の方たちは、誰も文句を言う必要なく、おいしくいただきました。にもかかわらず、先生の怒りはおさまらず、「こんなはずじゃなかったんです。ごめんなさいね。」という受講生への優しい言葉を皮切りに、また弟子を叱り始めたのです。結局、受講生は、先生のおっしゃる「本物の味」はわからずじまいな上に、先生から弟子への壮絶な説教シーンを延々と見て、帰ったのでした。皆様も、場所を選ばずにブチ切れる先生にはご注意ください。