From 6be2aa29dc830bddf22f30a54dcbc12e8a3613f1 Mon Sep 17 00:00:00 2001
From: David Kocik <kocikdav@gmail.com>
Date: Fri, 31 Mar 2023 13:10:56 +0200
Subject: [PATCH] Alternative eject function (IOCTL_STORAGE_EJECT_MEDIA)

Detect if device has CARD READER configuration in Configuration Descriptor


log
---
 src/slic3r/GUI/RemovableDriveManager.cpp | 315 ++++++++++++++++++++++-
 1 file changed, 303 insertions(+), 12 deletions(-)

diff --git a/src/slic3r/GUI/RemovableDriveManager.cpp b/src/slic3r/GUI/RemovableDriveManager.cpp
index b6881f1ae..ecd8e0727 100644
--- a/src/slic3r/GUI/RemovableDriveManager.cpp
+++ b/src/slic3r/GUI/RemovableDriveManager.cpp
@@ -13,6 +13,11 @@
 #include <Dbt.h>
 #include <Setupapi.h>
 #include <cfgmgr32.h>
+
+#include <initguid.h>   // include before devpropdef.h
+#include <devpropdef.h>
+#include <devpkey.h>
+#include <usbioctl.h>
 #else
 // unix, linux & OSX includes
 #include <errno.h>
@@ -73,6 +78,287 @@ std::vector<DriveData> RemovableDriveManager::search_for_removable_drives() cons
 }
 
 namespace {
+int eject_alt(const std::wstring& volume_access_path)
+{
+	HANDLE handle = CreateFileW(volume_access_path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
+	if (handle == INVALID_HANDLE_VALUE) {
+		BOOST_LOG_TRIVIAL(error) << "Alt Ejecting " << volume_access_path << " failed (handle == INVALID_HANDLE_VALUE): " << GetLastError();
+		return 1;
+	}
+	DWORD deviceControlRetVal(0);
+	//these 3 commands should eject device safely but they dont, the device does disappear from file explorer but the "device was safely remove" notification doesnt trigger.
+	//sd cards does  trigger WM_DEVICECHANGE messege, usb drives dont
+	BOOL e1 = DeviceIoControl(handle, FSCTL_LOCK_VOLUME, nullptr, 0, nullptr, 0, &deviceControlRetVal, nullptr);
+	BOOST_LOG_TRIVIAL(debug) << "FSCTL_LOCK_VOLUME " << e1 << " ; " << deviceControlRetVal << " ; " << GetLastError();
+	BOOL e2 = DeviceIoControl(handle, FSCTL_DISMOUNT_VOLUME, nullptr, 0, nullptr, 0, &deviceControlRetVal, nullptr);
+	BOOST_LOG_TRIVIAL(debug) << "FSCTL_DISMOUNT_VOLUME " << e2 << " ; " << deviceControlRetVal << " ; " << GetLastError();
+	
+	// some implemenatations also calls IOCTL_STORAGE_MEDIA_REMOVAL here with FALSE as third parameter, which should set PreventMediaRemoval 
+	BOOL error = DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, nullptr, 0, nullptr, 0, &deviceControlRetVal, nullptr);
+	if (error == 0) {
+		CloseHandle(handle);
+		BOOST_LOG_TRIVIAL(error) << "Alt Ejecting " << volume_access_path << " failed (IOCTL_STORAGE_EJECT_MEDIA)" << deviceControlRetVal << " " << GetLastError();
+		return 1;
+	}
+	CloseHandle(handle);
+	BOOST_LOG_TRIVIAL(info) << "Alt Ejecting finished";
+	return 0;
+}
+
+
+// From https://github.com/microsoft/Windows-driver-samples/tree/main/usb/usbview
+typedef struct _STRING_DESCRIPTOR_NODE
+{
+	struct _STRING_DESCRIPTOR_NODE* Next;
+	UCHAR                           DescriptorIndex;
+	USHORT                          LanguageID;
+	USB_STRING_DESCRIPTOR           StringDescriptor[1];
+} STRING_DESCRIPTOR_NODE, * PSTRING_DESCRIPTOR_NODE;
+
+// Based at https://github.com/microsoft/Windows-driver-samples/tree/main/usb/usbview
+PSTRING_DESCRIPTOR_NODE GetStringDescriptor(
+	HANDLE  handle_hub_device,
+	ULONG   connection_index,
+	UCHAR   descriptor_index,
+	USHORT  language_ID
+)
+{
+	BOOL					success = 0;
+	ULONG					nbytes = 0;
+	ULONG					nbytes_returned = 0;
+	UCHAR					string_desc_req_buf[sizeof(USB_DESCRIPTOR_REQUEST) + MAXIMUM_USB_STRING_LENGTH];
+	PUSB_DESCRIPTOR_REQUEST string_desc_req = NULL;
+	PUSB_STRING_DESCRIPTOR  string_desc = NULL;
+	PSTRING_DESCRIPTOR_NODE string_desc_node = NULL;
+
+	nbytes			= sizeof(string_desc_req_buf);
+	string_desc_req = (PUSB_DESCRIPTOR_REQUEST)string_desc_req_buf;
+	string_desc		= (PUSB_STRING_DESCRIPTOR)(string_desc_req + 1);
+
+	// Zero fill the entire request structure
+	memset(string_desc_req, 0, nbytes);
+
+	// Indicate the port from which the descriptor will be requested
+	string_desc_req->ConnectionIndex = connection_index;
+
+	// USBHUB uses URB_FUNCTION_GET_DESCRIPTOR_FROM_DEVICE to process this
+	// IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION request.
+	//
+	// USBD will automatically initialize these fields:
+	//     bmRequest = 0x80
+	//     bRequest  = 0x06
+	//
+	// We must inititialize these fields:
+	//     wValue    = Descriptor Type (high) and Descriptor Index (low byte)
+	//     wIndex    = Zero (or Language ID for String Descriptors)
+	//     wLength   = Length of descriptor buffer
+	string_desc_req->SetupPacket.wValue = (USB_STRING_DESCRIPTOR_TYPE << 8)
+		| descriptor_index;
+	string_desc_req->SetupPacket.wIndex = language_ID;
+	string_desc_req->SetupPacket.wLength = (USHORT)(nbytes - sizeof(USB_DESCRIPTOR_REQUEST));
+
+	// Now issue the get descriptor request.
+	success = DeviceIoControl(handle_hub_device,
+		IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION,
+		string_desc_req,
+		nbytes,
+		string_desc_req,
+		nbytes,
+		&nbytes_returned,
+		NULL);
+
+	// Do some sanity checks on the return from the get descriptor request.
+	if (!success) {
+		return NULL;
+	}
+	if (nbytes_returned < 2) {
+		return NULL;
+	}
+	if (string_desc->bDescriptorType != USB_STRING_DESCRIPTOR_TYPE) {
+		return NULL;
+	}
+	if (string_desc->bLength != nbytes_returned - sizeof(USB_DESCRIPTOR_REQUEST)) {
+		return NULL;
+	}
+	if (string_desc->bLength % 2 != 0) {
+		return NULL;
+	}
+
+	// Looks good, allocate some (zero filled) space for the string descriptor
+	// node and copy the string descriptor to it.
+	string_desc_node = (PSTRING_DESCRIPTOR_NODE)malloc(sizeof(STRING_DESCRIPTOR_NODE) + string_desc->bLength * sizeof(DWORD));
+	if (string_desc_node == NULL) {
+		return NULL;
+	}
+	string_desc_node->Next = NULL;
+	string_desc_node->DescriptorIndex = descriptor_index;
+	string_desc_node->LanguageID = language_ID;
+
+	memcpy(string_desc_node->StringDescriptor,
+		string_desc,
+		string_desc->bLength);
+
+	return string_desc_node;
+}
+
+// Based at https://github.com/microsoft/Windows-driver-samples/tree/main/usb/usbview
+HRESULT GetStringDescriptors(
+	_In_ HANDLE								handle_hub_device,
+	_In_ ULONG								connection_index,
+	_In_ UCHAR								descriptor_index,
+	_In_ ULONG								num_language_IDs,
+	_In_reads_(num_language_IDs) USHORT*	language_IDs,
+	_In_ PSTRING_DESCRIPTOR_NODE			string_desc_node_head,
+	std::wstring&							result
+)
+{
+	PSTRING_DESCRIPTOR_NODE tail = NULL;
+	PSTRING_DESCRIPTOR_NODE trailing = NULL;
+	ULONG					i = 0;
+
+	// Go to the end of the linked list, searching for the requested index to
+	// see if we've already retrieved it
+	for (tail = string_desc_node_head; tail != NULL; tail = tail->Next) {
+		if (tail->DescriptorIndex == descriptor_index) {
+			// copy string descriptor to result
+			for(int i = 0; i < tail->StringDescriptor->bLength / 2 - 1; i++) {
+				result += tail->StringDescriptor->bString[i];
+			}
+			return S_OK;
+		}
+		trailing = tail;
+	}
+	tail = trailing;
+
+	// Get the next String Descriptor. If this is NULL, then we're done (return)
+	// Otherwise, loop through all Language IDs
+	for (i = 0; (tail != NULL) && (i < num_language_IDs); i++) {
+		tail->Next = GetStringDescriptor(handle_hub_device,
+			connection_index,
+			descriptor_index,
+			language_IDs[i]);
+		tail = tail->Next;
+	}
+
+	if (tail == NULL) {
+		return E_FAIL;
+	} else {
+		// copy string descriptor to result
+		for (int i = 0; i < tail->StringDescriptor->bLength / 2 - 1; i++) {
+			result += tail->StringDescriptor->bString[i];
+		}
+		return S_OK;
+	}
+}
+
+bool get_handle_from_devinst(DEVINST devinst, HANDLE& handle)
+{
+	// create path consisting of device id and guid
+	wchar_t			device_id[MAX_PATH];
+	CM_Get_Device_ID(devinst, device_id, MAX_PATH, 0);
+
+	//convert device id string to device path - https://stackoverflow.com/a/32641140/981766
+	std::wstring	dev_id_wstr(device_id);
+	dev_id_wstr = std::regex_replace(dev_id_wstr, std::wregex(LR"(\\)"), L"#"); // '\' is special for regex
+	dev_id_wstr = std::regex_replace(dev_id_wstr, std::wregex(L"^"), LR"(\\?\)", std::regex_constants::format_first_only);
+	dev_id_wstr = std::regex_replace(dev_id_wstr, std::wregex(L"$"), L"#", std::regex_constants::format_first_only);
+	
+	// guid
+	wchar_t			guid_wchar[64];//guid is 32 chars+4 hyphens+2 paranthesis+null => 64 should be more than enough
+	StringFromGUID2(GUID_DEVINTERFACE_USB_HUB, guid_wchar, 64);
+	dev_id_wstr.append(guid_wchar);
+
+	// get handle
+	std::wstring&	usb_hub_path = dev_id_wstr; 
+	handle = CreateFileW(usb_hub_path.c_str(), GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
+	if (handle == INVALID_HANDLE_VALUE) {
+		// Sometimes device is not GUID_DEVINTERFACE_USB_HUB, than we need to check parent recursively
+		DEVINST parent_devinst = 0;
+		if (CM_Get_Parent(&parent_devinst, devinst, 0) != CR_SUCCESS)
+			return false;
+		return get_handle_from_devinst(parent_devinst, handle);
+	}
+	return true;
+}
+
+// Read Configuration Descriptor - configuration string indexed by iConfiguration and decide if card reader
+bool is_card_reader(HDEVINFO h_dev_info, SP_DEVINFO_DATA& spdd)
+{
+	// First get port number of device.
+
+	DEVINST		parent_devinst = 0;
+	HANDLE		handle; // usb hub handle
+	DWORD		usb_port_number = 0;
+	DWORD		required_size = 0;
+	// First we need handle for GUID_DEVINTERFACE_USB_HUB device.
+	if (CM_Get_Parent(&parent_devinst, spdd.DevInst, 0) != CR_SUCCESS) {
+		BOOST_LOG_TRIVIAL(warning) << "is_card_reader failed: Couldn't get parent DEVINST.";
+		return false;
+	}
+	if(!get_handle_from_devinst(parent_devinst, handle) || handle == INVALID_HANDLE_VALUE) {
+		BOOST_LOG_TRIVIAL(warning) << "is_card_reader failed: Couldn't get HANDLE for parent DEVINST.";
+		return false;
+	}
+	// Get port number to which the usb device is attached on the hub.
+	if (SetupDiGetDeviceRegistryProperty(h_dev_info, &spdd, SPDRP_ADDRESS, nullptr, (PBYTE)&usb_port_number, sizeof(usb_port_number), &required_size) == 0) {
+		BOOST_LOG_TRIVIAL(warning) << "is_card_reader failed: Couldn't get port number.";
+		return false;
+	}
+
+	// Fill USB request packet to get iConfiguration value.
+
+	int								buffer_size = sizeof(USB_DESCRIPTOR_REQUEST) + sizeof(USB_CONFIGURATION_DESCRIPTOR);
+	BYTE*							buffer = new BYTE[buffer_size];
+	USB_DESCRIPTOR_REQUEST*			request_packet = (USB_DESCRIPTOR_REQUEST*)buffer;
+	USB_CONFIGURATION_DESCRIPTOR*	configuration_descriptor = (USB_CONFIGURATION_DESCRIPTOR*)((BYTE*)buffer + sizeof(USB_DESCRIPTOR_REQUEST));
+	DWORD							bytes_returned = 0;
+	// Fill information in packet.
+	request_packet->SetupPacket.bmRequest = 0x80;
+	request_packet->SetupPacket.bRequest = USB_REQUEST_GET_CONFIGURATION;
+	request_packet->ConnectionIndex = usb_port_number;
+	request_packet->SetupPacket.wValue = (USB_CONFIGURATION_DESCRIPTOR_TYPE << 8 | 0 /*Since only 1 device descriptor => index : 0*/);
+	request_packet->SetupPacket.wLength = sizeof(USB_CONFIGURATION_DESCRIPTOR);
+	// Issue ioctl.
+	if (DeviceIoControl(handle, IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION, buffer, buffer_size, buffer, buffer_size, &bytes_returned, nullptr) == 0) {
+		BOOST_LOG_TRIVIAL(warning) << "is_card_reader failed: Couldn't get Configuration Descriptor.";
+		return false;
+	}
+	// Nothing to read.
+	if (configuration_descriptor->iConfiguration == 0) {
+		BOOST_LOG_TRIVIAL(warning) << "is_card_reader failed: iConfiguration value is 0.";
+		return false;
+	}
+
+	// Get string descriptor and read string on address given by iConfiguration index .
+	// Based at https://github.com/microsoft/Windows-driver-samples/tree/main/usb/usbview
+
+	PSTRING_DESCRIPTOR_NODE		supported_languages_string = NULL;
+	ULONG						num_language_IDs = 0;
+	USHORT*						language_IDs = NULL;
+	std::wstring				configuration_string;
+	// Get languages.
+	supported_languages_string	= GetStringDescriptor(handle, usb_port_number, 0, 0);
+	if (supported_languages_string == NULL) {
+		BOOST_LOG_TRIVIAL(warning) << "is_card_reader failed: Couldn't get language string descriptor.";
+		return false;
+	}
+	num_language_IDs			= (supported_languages_string->StringDescriptor->bLength - 2) / 2;
+	language_IDs				= (USHORT*)&supported_languages_string->StringDescriptor->bString[0];
+	// Get configration string.
+	if (GetStringDescriptors(handle, usb_port_number, configuration_descriptor->iConfiguration, num_language_IDs, language_IDs, supported_languages_string, configuration_string) == E_FAIL) {
+		BOOST_LOG_TRIVIAL(warning) << "is_card_reader failed: Couldn't get configuration string descriptor.";
+		return false;
+	}
+	
+	// Final compare.
+	BOOST_LOG_TRIVIAL(error) << "Ejecting information: Retrieved configuration string: " << configuration_string;
+	if (configuration_string.find(L"CARD READER") != std::wstring::npos) {
+		BOOST_LOG_TRIVIAL(info) << "Detected external reader.";
+		return true;
+	}
+	return false;
+}
+
 // returns the device instance handle of a storage volume or 0 on error
 // called from eject_inner, based on https://stackoverflow.com/a/58848961
 DEVINST get_dev_inst_by_device_number(long device_number, UINT drive_type, WCHAR* dos_device_name)
@@ -80,7 +366,7 @@ DEVINST get_dev_inst_by_device_number(long device_number, UINT drive_type, WCHAR
 	bool is_floppy = (wcsstr(dos_device_name, L"\\Floppy") != NULL); // TODO: could be tested better?
 	
 	if (drive_type != DRIVE_REMOVABLE || is_floppy) {
-		BOOST_LOG_TRIVIAL(debug) << "get_dev_inst_by_device_number failed: Drive is not removable.";
+		BOOST_LOG_TRIVIAL(warning) << "get_dev_inst_by_device_number failed: Drive is not removable.";
 		return 0;
 	}
 
@@ -89,7 +375,7 @@ DEVINST get_dev_inst_by_device_number(long device_number, UINT drive_type, WCHAR
 	HDEVINFO h_dev_info = SetupDiGetClassDevs(guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
 
 	if (h_dev_info == INVALID_HANDLE_VALUE) {
-		BOOST_LOG_TRIVIAL(debug) << "get_dev_inst_by_device_number failed: Invalid dev info handle.";
+		BOOST_LOG_TRIVIAL(warning) << "get_dev_inst_by_device_number failed: Invalid dev info handle.";
 		return 0;
 	}
 
@@ -135,13 +421,16 @@ DEVINST get_dev_inst_by_device_number(long device_number, UINT drive_type, WCHAR
 		if (device_number != (long)sdn.DeviceNumber) {
 			continue;
 		}
-		// this is the drive, return the device instance
+
+		// check if is sd card reader - if yes, indicate by returning invalid value.
+		bool reader = is_card_reader(h_dev_info, spdd);
+
 		SetupDiDestroyDeviceInfoList(h_dev_info);
-		return spdd.DevInst;
+		return !reader ? spdd.DevInst : 0;
 	}
 
 	SetupDiDestroyDeviceInfoList(h_dev_info);
-	BOOST_LOG_TRIVIAL(debug) << "get_dev_inst_by_device_number failed: Enmurating couldn't find the drive.";
+	BOOST_LOG_TRIVIAL(warning) << "get_dev_inst_by_device_number failed: Enmurating couldn't find the drive.";
 	return 0;
 }
 
@@ -194,10 +483,9 @@ int eject_inner(const std::string& path)
 
 	// get the device instance handle of the storage volume by means of a SetupDi enum and matching the device number
 	DEVINST dev_inst = get_dev_inst_by_device_number(device_number, drive_type, dos_device_name);
-
 	if (dev_inst == 0) {
-		BOOST_LOG_TRIVIAL(error) << GUI::format("Ejecting of %1% has failed: Invalid device instance handle.", path);
-		return 1;
+		BOOST_LOG_TRIVIAL(error) << GUI::format("Ejecting of %1%: Invalid device instance handle. Going to try alternative ejecting method.", path);
+		return eject_alt(volume_access_path);
 	}
 
 	PNP_VETO_TYPE veto_type = PNP_VetoTypeUnknown;
@@ -249,7 +537,7 @@ int eject_inner(const std::string& path)
 	return 1;
 }
 
-}
+} // namespace
 // Called from UI therefore it blocks the UI thread.
 // It also blocks updates at the worker thread.
 // Win32 implementation.
@@ -268,18 +556,21 @@ void RemovableDriveManager::eject_drive()
 	if (it_drive_data != m_current_drives.end()) {
 		if (!eject_inner(m_last_save_path)) {
 		// success
-		assert(m_callback_evt_handler);
-		if (m_callback_evt_handler)
-			wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair< DriveData, bool >(std::move(*it_drive_data), true)));
+			BOOST_LOG_TRIVIAL(info) << "Ejecting has succeeded.";
+			assert(m_callback_evt_handler);
+			if (m_callback_evt_handler)
+				wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair< DriveData, bool >(std::move(*it_drive_data), true)));
 		} else {
 			// failed to eject
 			// this should not happen, throwing exception might be the way here
+			BOOST_LOG_TRIVIAL(error) << "Ejecting has failed.";
 			assert(m_callback_evt_handler);
 			if (m_callback_evt_handler)
 				wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair<DriveData, bool>(*it_drive_data, false)));
 		}
 	} else {
 		// drive not found in m_current_drives
+		BOOST_LOG_TRIVIAL(error) << "Ejecting has failed. Drive not found in m_current_drives.";
 		assert(m_callback_evt_handler);
 		if (m_callback_evt_handler)
 			wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair<DriveData, bool>({"",""}, false)));