From 0ae446d9703a7295173c9c4362d32b25e51ea257 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Mon, 12 Mar 2012 04:05:31 -0400 Subject: [PATCH] Move Windows CD-ROM detection to another thread that uses device notifications. Fixes blocking when a disc is spinning up, except on initial call. --- docs/TODO.txt | 2 - src/platform_windows.c | 198 +++++++++++++++++++++++++++++++++++------ 2 files changed, 173 insertions(+), 27 deletions(-) diff --git a/docs/TODO.txt b/docs/TODO.txt index 2a759fa..e8d7609 100644 --- a/docs/TODO.txt +++ b/docs/TODO.txt @@ -22,8 +22,6 @@ From http://icculus.org/pipermail/physfs/2009-March/000698.html ... - Deprecate PHYSFS_setSaneConfig(). It really should have been in the extras directory. - Clean up the sources to match my ever-changing coding style. :) -- Get current CD list from windows without blocking? - Read this: http://support.microsoft.com/kb/163503 From http://icculus.org/pipermail/physfs/2010-January/000821.html ... diff --git a/src/platform_windows.c b/src/platform_windows.c index 57622e4..7fcd079 100644 --- a/src/platform_windows.c +++ b/src/platform_windows.c @@ -19,6 +19,7 @@ #define WIN32_LEAN_AND_MEAN 1 #include #include +#include #include #include #include @@ -95,7 +96,10 @@ typedef struct const char *__PHYSFS_platformDirSeparator = "\\"; static char *userDir = NULL; static HANDLE libUserEnv = NULL; - +static HANDLE detectCDThreadHandle = NULL; +static HWND detectCDHwnd = 0; +static volatile int initialDiscDetectionComplete = 0; +static volatile DWORD drivesWithMediaBitmap = 0; /* * Figure out what the last failing Windows API call was, and @@ -192,40 +196,174 @@ static int determineUserDir(void) } /* determineUserDir */ -static BOOL mediaInDrive(const char *drive) +typedef BOOL (WINAPI *fnSTEM)(DWORD, LPDWORD b); + +static DWORD pollDiscDrives(void) { - UINT oldErrorMode; - DWORD tmp; - BOOL retval; + /* Try to use SetThreadErrorMode(), which showed up in Windows 7. */ + HANDLE lib = LoadLibraryA("kernel32.dll"); + fnSTEM stem = NULL; + char drive[4] = { 'x', ':', '\\', '\0' }; + DWORD oldErrorMode = 0; + DWORD drives = 0; + DWORD i; - /* Prevent windows warning message appearing when checking media size */ - /* !!! FIXME: Windows 7 offers SetThreadErrorMode(). */ - oldErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS); + if (lib) + stem = (fnSTEM) GetProcAddress(lib, "SetThreadErrorMode"); + + if (stem) + stem(SEM_FAILCRITICALERRORS, &oldErrorMode); + else + oldErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS); - /* If this function succeeds, there's media in the drive */ - retval = GetVolumeInformationA(drive, NULL, 0, NULL, NULL, &tmp, NULL, 0); + /* Do detection. This may block if a disc is spinning up. */ + for (i = 'A'; i <= 'Z'; i++) + { + DWORD tmp = 0; + drive[0] = (char) i; + if (GetDriveTypeA(drive) != DRIVE_CDROM) + continue; - /* Revert back to old windows error handler */ - SetErrorMode(oldErrorMode); + /* If this function succeeds, there's media in the drive */ + if (GetVolumeInformationA(drive, NULL, 0, NULL, NULL, &tmp, NULL, 0)) + drives |= (1 << (i - 'A')); + } /* for */ + + if (stem) + stem(oldErrorMode, NULL); + else + SetErrorMode(oldErrorMode); + + if (lib) + FreeLibrary(lib); + + return drives; +} /* pollDiscDrives */ + + +static LRESULT CALLBACK detectCDWndProc(HWND hwnd, UINT msg, + WPARAM wp, LPARAM lparam) +{ + PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR) lparam; + PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME) lparam; + const int removed = (wp == DBT_DEVICEREMOVECOMPLETE); + + if (msg == WM_DESTROY) + return 0; + else if ((msg != WM_DEVICECHANGE) || + ((wp != DBT_DEVICEARRIVAL) && (wp != DBT_DEVICEREMOVECOMPLETE)) || + (lpdb->dbch_devicetype != DBT_DEVTYP_VOLUME) || + ((lpdbv->dbcv_flags & DBTF_MEDIA) == 0)) + { + return DefWindowProcW(hwnd, msg, wp, lparam); + } /* else if */ + + if (removed) + drivesWithMediaBitmap &= ~lpdbv->dbcv_unitmask; + else + drivesWithMediaBitmap |= lpdbv->dbcv_unitmask; + + return TRUE; +} /* detectCDWndProc */ + + +static DWORD WINAPI detectCDThread(LPVOID lpParameter) +{ + const char *classname = "PhysicsFSDetectCDCatcher"; + const char *winname = "PhysicsFSDetectCDMsgWindow"; + HINSTANCE hInstance = GetModuleHandleW(NULL); + ATOM class_atom = 0; + WNDCLASSEXA wce; + MSG msg; + + memset(&wce, '\0', sizeof (wce)); + wce.cbSize = sizeof (wce); + wce.lpfnWndProc = detectCDWndProc; + wce.lpszClassName = classname; + wce.hInstance = hInstance; + class_atom = RegisterClassExA(&wce); + if (class_atom == 0) + { + initialDiscDetectionComplete = 1; /* let main thread go on. */ + return 0; + } /* if */ + + detectCDHwnd = CreateWindowExA(0, classname, winname, WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, HWND_DESKTOP, NULL, hInstance, NULL); + + if (detectCDHwnd == NULL) + { + initialDiscDetectionComplete = 1; /* let main thread go on. */ + UnregisterClassA(classname, hInstance); + return 0; + } /* if */ + + /* We'll get events when discs come and go from now on. */ + + /* Do initial detection, possibly blocking awhile... */ + drivesWithMediaBitmap = pollDiscDrives(); + initialDiscDetectionComplete = 1; /* let main thread go on. */ + + do + { + const BOOL rc = GetMessageW(&msg, detectCDHwnd, 0, 0); + if ((rc == 0) || (rc == -1)) + break; /* don't care if WM_QUIT or error break this loop. */ + TranslateMessage(&msg); + DispatchMessageW(&msg); + } while (1); + + /* we've been asked to quit. */ + DestroyWindow(detectCDHwnd); + + do + { + const BOOL rc = GetMessage(&msg, detectCDHwnd, 0, 0); + if ((rc == 0) || (rc == -1)) + break; + TranslateMessage(&msg); + DispatchMessageW(&msg); + } while (1); + + UnregisterClassA(classname, hInstance); + + return 0; +} /* detectCDThread */ - return retval; -} /* mediaInDrive */ -/* - * !!! FIXME: move this to a thread? This function hangs if you call it while - * !!! FIXME: a drive is spinning up right after inserting a disc. - */ void __PHYSFS_platformDetectAvailableCDs(PHYSFS_StringCallback cb, void *data) { - /* !!! FIXME: Can CD drives be non-drive letter paths? */ - /* !!! FIXME: (so can they be Unicode paths?) */ - char drive_str[4] = "x:\\"; - char ch; - for (ch = 'A'; ch <= 'Z'; ch++) + char drive_str[4] = { 'x', ':', '\\', '\0' }; + DWORD drives = 0; + DWORD i; + + /* + * If you poll a drive while a user is inserting a disc, the OS will + * block this thread until the drive has spun up. So we swallow the risk + * once for initial detection, and spin a thread that will get device + * events thereafter, for apps that use this interface to poll for + * disc insertion. + */ + if (!detectCDThreadHandle) { - drive_str[0] = ch; - if (GetDriveTypeA(drive_str) == DRIVE_CDROM && mediaInDrive(drive_str)) + initialDiscDetectionComplete = 0; + detectCDThreadHandle = CreateThread(NULL,0,detectCDThread,NULL,0,NULL); + if (detectCDThreadHandle == NULL) + return; /* oh well. */ + + while (!initialDiscDetectionComplete) + Sleep(50); + } /* if */ + + drives = drivesWithMediaBitmap; /* whatever the thread has seen, we take. */ + for (i = 'A'; i <= 'Z'; i++) + { + if (drives & (1 << (i - 'A'))) + { + drive_str[0] = (char) i; cb(data, drive_str); + } /* if */ } /* for */ } /* __PHYSFS_platformDetectAvailableCDs */ @@ -469,6 +607,16 @@ int __PHYSFS_platformInit(void) int __PHYSFS_platformDeinit(void) { + if (detectCDThreadHandle != NULL) + { + if (detectCDHwnd) + PostMessageW(detectCDHwnd, WM_QUIT, 0, 0); + CloseHandle(detectCDThreadHandle); + detectCDThreadHandle = NULL; + initialDiscDetectionComplete = 0; + drivesWithMediaBitmap = 0; + } /* if */ + if (libUserEnv) FreeLibrary(libUserEnv); libUserEnv = NULL;