diff --git a/setup/installer/windows/eject.c b/setup/installer/windows/eject.c new file mode 100644 index 0000000000..dcff769ad3 --- /dev/null +++ b/setup/installer/windows/eject.c @@ -0,0 +1,423 @@ +/* + * eject.c + * Copyright (C) 2013 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "Windows.h" +#include +#include +#include +#include +#include +#include + +#define BUFSIZE 4096 +#define LOCK_TIMEOUT 10000 // 10 Seconds +#define LOCK_RETRIES 20 + +#define BOOL2STR(x) ((x) ? L"True" : L"False") + +// Error handling {{{ + +static void show_error(LPCWSTR msg) { + MessageBeep(MB_ICONERROR); + MessageBoxW(NULL, msg, L"Error", MB_OK|MB_ICONERROR); +} + +static void show_detailed_error(LPCWSTR preamble, LPCWSTR msg, int code) { + LPWSTR buf; + buf = (LPWSTR)LocalAlloc(LMEM_ZEROINIT, sizeof(WCHAR)* + (wcslen(msg) + wcslen(preamble) + 80)); + + _snwprintf_s(buf, + LocalSize(buf) / sizeof(WCHAR), _TRUNCATE, + L"%s\r\n %s (Error Code: %d)\r\n", + preamble, msg, code); + + show_error(buf); + LocalFree(buf); +} + +static void print_detailed_error(LPCWSTR preamble, LPCWSTR msg, int code) { + fwprintf_s(stderr, L"%s\r\n %s (Error Code: %d)\r\n", preamble, msg, code); + fflush(stderr); +} + +static void show_last_error_crt(LPCWSTR preamble) { + WCHAR buf[BUFSIZE]; + int err = 0; + + _get_errno(&err); + _wcserror_s(buf, BUFSIZE, err); + show_detailed_error(preamble, buf, err); +} + +static void show_last_error(LPCWSTR preamble) { + WCHAR *msg = NULL; + DWORD dw = GetLastError(); + + FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + dw, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)&msg, + 0, NULL ); + + show_detailed_error(preamble, msg, (int)dw); +} + +static void print_last_error(LPCWSTR preamble) { + WCHAR *msg = NULL; + DWORD dw = GetLastError(); + + FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + dw, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)&msg, + 0, NULL ); + + print_detailed_error(preamble, msg, (int)dw); +} + +// }}} + +static void print_help() { + fwprintf_s(stderr, L"Usage: calibre-eject.exe drive-letter1 [drive-letter2 drive-letter3 ...]"); +} + +static LPWSTR root_path = L"X:\\", device_path = L"X:", volume_access_path = L"\\\\.\\X:"; +static wchar_t dos_device_name[MAX_PATH]; +static UINT drive_type = 0; +static long device_number = -1; +static DEVINST dev_inst = 0, dev_inst_parent = 0; + +// Unmount and eject volumes (drives) {{{ +static HANDLE open_volume(wchar_t drive_letter) { + DWORD access_flags; + + switch(drive_type) { + case DRIVE_REMOVABLE: + access_flags = GENERIC_READ | GENERIC_WRITE; + break; + case DRIVE_CDROM: + access_flags = GENERIC_READ; + break; + default: + fwprintf_s(stderr, L"Cannot eject %c: Drive type is incorrect.\r\n", drive_letter); + fflush(stderr); + return INVALID_HANDLE_VALUE; + } + + return CreateFileW(volume_access_path, access_flags, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, 0, NULL); +} + +static BOOL lock_volume(HANDLE volume) { + DWORD bytes_returned; + DWORD sleep_amount = LOCK_TIMEOUT / LOCK_RETRIES; + int try_count; + + // Do this in a loop until a timeout period has expired + for (try_count = 0; try_count < LOCK_RETRIES; try_count++) { + if (DeviceIoControl(volume, + FSCTL_LOCK_VOLUME, + NULL, 0, + NULL, 0, + &bytes_returned, + NULL)) + return TRUE; + + Sleep(sleep_amount); + } + + return FALSE; +} + +static BOOL dismount_volume(HANDLE volume) { + DWORD bytes_returned; + + return DeviceIoControl( volume, + FSCTL_DISMOUNT_VOLUME, + NULL, 0, + NULL, 0, + &bytes_returned, + NULL); +} + +static BOOL disable_prevent_removal_of_volume(HANDLE volume) { + DWORD bytes_returned; + PREVENT_MEDIA_REMOVAL PMRBuffer; + + PMRBuffer.PreventMediaRemoval = FALSE; + + return DeviceIoControl( volume, + IOCTL_STORAGE_MEDIA_REMOVAL, + &PMRBuffer, sizeof(PREVENT_MEDIA_REMOVAL), + NULL, 0, + &bytes_returned, + NULL); +} + +static BOOL auto_eject_volume(HANDLE volume) { + DWORD bytes_returned; + + return DeviceIoControl( volume, + IOCTL_STORAGE_EJECT_MEDIA, + NULL, 0, + NULL, 0, + &bytes_returned, + NULL); +} + +static BOOL unmount_drive(wchar_t drive_letter, BOOL *remove_safely, BOOL *auto_eject) { + // Unmount the drive identified by drive_letter. Code adapted from: + // http://support.microsoft.com/kb/165721 + HANDLE volume; + *remove_safely = FALSE; *auto_eject = FALSE; + + volume = open_volume(drive_letter); + if (volume == INVALID_HANDLE_VALUE) return FALSE; + + // Lock and dismount the volume. + if (lock_volume(volume) && dismount_volume(volume)) { + *remove_safely = TRUE; + + // Set prevent removal to false and eject the volume. + if (disable_prevent_removal_of_volume(volume) && auto_eject_volume(volume)) + *auto_eject = TRUE; + } + CloseHandle(volume); + return TRUE; + +} +// }}} + +// Eject USB device {{{ +static void get_device_number(wchar_t drive_letter) { + HANDLE volume; + DWORD bytes_returned = 0; + STORAGE_DEVICE_NUMBER sdn; + + volume = CreateFileW(volume_access_path, 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, 0, NULL); + if (volume == INVALID_HANDLE_VALUE) { + print_last_error(L"Failed to open volume while getting device number"); + return; + } + + if (DeviceIoControl(volume, + IOCTL_STORAGE_GET_DEVICE_NUMBER, + NULL, 0, &sdn, sizeof(sdn), + &bytes_returned, NULL)) + device_number = sdn.DeviceNumber; + CloseHandle(volume); +} + +static DEVINST get_dev_inst_by_device_number(long device_number, UINT drive_type, LPWSTR dos_device_name) { + GUID *guid; + HDEVINFO dev_info; + DWORD index, bytes_returned; + BOOL bRet, is_floppy; + BYTE Buf[1024]; + PSP_DEVICE_INTERFACE_DETAIL_DATA pspdidd; + long res; + HANDLE drive; + STORAGE_DEVICE_NUMBER sdn; + SP_DEVICE_INTERFACE_DATA spdid; + SP_DEVINFO_DATA spdd; + DWORD size; + + is_floppy = (wcsstr(dos_device_name, L"\\Floppy") != NULL); // is there a better way? + + switch (drive_type) { + case DRIVE_REMOVABLE: + guid = ( (is_floppy) ? (GUID*)&GUID_DEVINTERFACE_FLOPPY : (GUID*)&GUID_DEVINTERFACE_DISK ); + break; + case DRIVE_FIXED: + guid = (GUID*)&GUID_DEVINTERFACE_DISK; + break; + case DRIVE_CDROM: + guid = (GUID*)&GUID_DEVINTERFACE_CDROM; + break; + default: + fwprintf_s(stderr, L"Invalid drive type at line: %d\r\n", __LINE__); + fflush(stderr); + return 0; + } + + // Get device interface info set handle + // for all devices attached to system + dev_info = SetupDiGetClassDevs(guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + + if (dev_info == INVALID_HANDLE_VALUE) { + fwprintf_s(stderr, L"Failed to setup class devs at line: %d\r\n", __LINE__); + fflush(stderr); + return 0; + } + + // Retrieve a context structure for a device interface + // of a device information set. + index = 0; + bRet = FALSE; + + pspdidd = (PSP_DEVICE_INTERFACE_DETAIL_DATA)Buf; + spdid.cbSize = sizeof(spdid); + + while ( TRUE ) { + bRet = SetupDiEnumDeviceInterfaces(dev_info, NULL, + guid, index, &spdid); + if ( !bRet ) break; + + size = 0; + SetupDiGetDeviceInterfaceDetail(dev_info, + &spdid, NULL, 0, &size, NULL); + + if ( size!=0 && size<=sizeof(Buf) ) { + pspdidd->cbSize = sizeof(*pspdidd); // 5 Bytes! + + ZeroMemory((PVOID)&spdd, sizeof(spdd)); + spdd.cbSize = sizeof(spdd); + + res = SetupDiGetDeviceInterfaceDetail(dev_info, &spdid, pspdidd, size, &size, &spdd); + if ( res ) { + drive = CreateFile(pspdidd->DevicePath,0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, 0, NULL); + if ( drive != INVALID_HANDLE_VALUE ) { + bytes_returned = 0; + res = DeviceIoControl(drive, + IOCTL_STORAGE_GET_DEVICE_NUMBER, + NULL, 0, &sdn, sizeof(sdn), + &bytes_returned, NULL); + if ( res ) { + if ( device_number == (long)sdn.DeviceNumber ) { + CloseHandle(drive); + SetupDiDestroyDeviceInfoList(dev_info); + return spdd.DevInst; + } + } + CloseHandle(drive); + } + } + } + index++; + } + + SetupDiDestroyDeviceInfoList(dev_info); + fwprintf_s(stderr, L"Invalid device number at line: %d\r\n", __LINE__); + fflush(stderr); + + return 0; +} + + +static void get_parent_device(wchar_t drive_letter) { + get_device_number(drive_letter); + if (device_number == -1) return; + if (QueryDosDeviceW(device_path, dos_device_name, MAX_PATH) == 0) { + print_last_error(L"Failed to query DOS device name"); + return; + } + + dev_inst = get_dev_inst_by_device_number(device_number, + drive_type, dos_device_name); + if (dev_inst == 0) { + fwprintf_s(stderr, L"Failed to get device by device number"); + fflush(stderr); + return; + } + if (CM_Get_Parent(&dev_inst_parent, dev_inst, 0) != CR_SUCCESS) { + fwprintf_s(stderr, L"Failed to get device parent from CM"); + fflush(stderr); + return; + } +} + +static int eject_device() { + int tries; + CONFIGRET res; + PNP_VETO_TYPE VetoType; + WCHAR VetoNameW[MAX_PATH]; + BOOL success; + + for ( tries = 0; tries < 3; tries++ ) { + VetoNameW[0] = 0; + + res = CM_Request_Device_EjectW(dev_inst_parent, + &VetoType, VetoNameW, MAX_PATH, 0); + + success = (res==CR_SUCCESS && + VetoType==PNP_VetoTypeUnknown); + if ( success ) { + break; + } + + Sleep(500); // required to give the next tries a chance! + } + if (!success) { + fwprintf_s(stderr, L"CM_Request_Device_Eject failed after three tries\r\n"); + fflush(stderr); + } + + return (success) ? 0 : 1; +} + +// }}} + +int wmain(int argc, wchar_t *argv[ ]) { + int i = 0; + wchar_t drive_letter; + BOOL remove_safely, auto_eject; + + // Validate command line arguments + if (argc < 2) { print_help(); return 1; } + for (i = 1; i < argc; i++) { + if (wcsnlen_s(argv[i], 2) != 1) { print_help(); return 1; } + } + + // Unmount all mounted volumes and eject volume media + for (i = 1; i < argc; i++) { + drive_letter = *argv[i]; + root_path[0] = drive_letter; + device_path[0] = drive_letter; + volume_access_path[4] = drive_letter; + drive_type = GetDriveTypeW(root_path); + if (i == 1 && device_number == -1) { + get_parent_device(drive_letter); + } + if (device_number != -1) { + unmount_drive(drive_letter, &remove_safely, &auto_eject); + fwprintf_s(stdout, L"Unmounting: %c: Remove safely: %s Media Ejected: %s\r\n", + drive_letter, BOOL2STR(remove_safely), BOOL2STR(auto_eject)); + fflush(stdout); + } + } + + // Eject the parent USB device + if (device_number == -1) { + fwprintf_s(stderr, L"Cannot eject, failed to get device number\r\n"); + fflush(stderr); + return 1; + } + + if (dev_inst_parent == 0) { + fwprintf_s(stderr, L"Cannot eject, failed to get device parent\r\n"); + fflush(stderr); + return 1; + } + + return eject_device(); +} + + diff --git a/setup/installer/windows/freeze.py b/setup/installer/windows/freeze.py index 194d298279..f25a21cd71 100644 --- a/setup/installer/windows/freeze.py +++ b/setup/installer/windows/freeze.py @@ -42,6 +42,7 @@ DESCRIPTIONS = { 'calibre-parallel': 'calibre worker process', 'calibre-smtp' : 'Command line interface for sending books via email', 'calibre-recycle' : 'Helper program for deleting to recycle bin', + 'calibre-eject' : 'Helper program for ejecting connected reader devices', } def walk(dir): @@ -82,6 +83,7 @@ class Win32Freeze(Command, WixMixIn): self.initbase() self.build_launchers() + self.build_eject() self.build_recycle() self.add_plugins() self.freeze() @@ -388,17 +390,21 @@ class Win32Freeze(Command, WixMixIn): os.remove(y) def run_builder(self, cmd, show_output=False): - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - if p.wait() != 0: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + buf = [] + while p.poll() is None: + x = p.stdout.read() + p.stderr.read() + if x: + buf.append(x) + if p.returncode != 0: self.info('Failed to run builder:') self.info(*cmd) - self.info(p.stdout.read()) - self.info(p.stderr.read()) + self.info(''.join(buf)) + self.info('') + sys.stdout.flush() sys.exit(1) if show_output: - self.info(p.stdout.read()) - self.info(p.stderr.read()) + self.info(''.join(buf) + '\n') def build_portable_installer(self): zf = self.a(self.j('dist', 'calibre-portable-%s.zip.lz'%VERSION)) @@ -554,6 +560,21 @@ class Win32Freeze(Command, WixMixIn): '/OUT:'+exe] + [self.embed_resources(exe), obj, 'Shell32.lib'] self.run_builder(cmd) + def build_eject(self): + self.info('Building calibre-eject.exe') + base = self.j(self.src_root, 'setup', 'installer', 'windows') + src = self.j(base, 'eject.c') + obj = self.j(self.obj_dir, self.b(src)+'.obj') + cflags = '/c /EHsc /MD /W3 /Ox /nologo /D_UNICODE'.split() + if self.newer(obj, src): + cmd = [msvc.cc] + cflags + ['/Fo'+obj, '/Tc'+src] + self.run_builder(cmd, show_output=True) + exe = self.j(self.base, 'calibre-eject.exe') + cmd = [msvc.linker] + ['/MACHINE:'+machine, + '/SUBSYSTEM:CONSOLE', '/RELEASE', + '/OUT:'+exe] + [self.embed_resources(exe), obj, 'setupapi.lib'] + self.run_builder(cmd) + def build_launchers(self, debug=False): if not os.path.exists(self.obj_dir): os.makedirs(self.obj_dir) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index ce04621a56..d349ce46d8 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -908,7 +908,12 @@ class Device(DeviceConfig, DevicePlugin): except: pass - t = Thread(target=do_it, args=[drives]) + def do_it2(drives): + import win32process + EJECT = os.path.join(os.path.dirname(sys.executable), 'calibre-eject.exe') + subprocess.Popen([EJECT] + drives, creationflags=win32process.CREATE_NO_WINDOW).wait() + + t = Thread(target=do_it2, args=[drives]) t.daemon = True t.start() self.__save_win_eject_thread = t