Просмотр исходного кода

Move Windows CD-ROM detection to another thread that uses device notifications.

Fixes blocking when a disc is spinning up, except on initial call.
Ryan C. Gordon 14 лет назад
Родитель
Сommit
0ae446d970
2 измененных файлов с 173 добавлено и 27 удалено
  1. 0 2
      docs/TODO.txt
  2. 173 25
      src/platform_windows.c

+ 0 - 2
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 ...

+ 173 - 25
src/platform_windows.c

@@ -19,6 +19,7 @@
 #define WIN32_LEAN_AND_MEAN 1
 #include <windows.h>
 #include <userenv.h>
+#include <dbt.h>
 #include <errno.h>
 #include <ctype.h>
 #include <time.h>
@@ -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)
+    {
+        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++)
     {
-        drive_str[0] = ch;
-        if (GetDriveTypeA(drive_str) == DRIVE_CDROM && mediaInDrive(drive_str))
+        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;