qtd3dservice: Add triggers for Windows Phone devices and emulators
authorAndrew Knight <andrew.knight@digia.com>
Tue, 4 Mar 2014 11:47:17 +0000 (13:47 +0200)
committerThe Qt Project <gerrit-noreply@qt-project.org>
Tue, 4 Mar 2014 12:56:08 +0000 (13:56 +0100)
This adds event triggers which allow for detecting when a device or
emulator is activated. When a device comes online, it is checked for
applications, and workers are created to monitor each application.

This changes the behavior for device connections. Instead of retrying the
connection repeatedly for 30 seconds, the worker returns after the first
failure. This is acceptable, because the worker will automatically be
reactivated when the device comes online.

Change-Id: Idab622d85de314edd18fad8bf721cbae1c17ca75
Reviewed-by: Oliver Wolff <oliver.wolff@digia.com>
src/qtd3dservice/d3dservice.cpp
src/qtd3dservice/qtd3dservice.pro
src/qtd3dservice/xaphandler.cpp

index b5f85e8..e98e2c9 100644 (file)
@@ -50,6 +50,8 @@
 #include <QtCore/QBitArray>
 
 #include <iostream>
+#include <dbt.h>
+#include <setupapi.h>
 
 QT_USE_NAMESPACE
 
@@ -59,15 +61,24 @@ Q_LOGGING_CATEGORY(lcD3DService, "qt.d3dservice")
 #define WIDEN(x) _WIDEN(x)
 #define LQT_VERSION_STR WIDEN(QT_VERSION_STR)
 
+// The GUID used by the Windows Phone IP over USB service
+static const GUID GUID_DEVICE_WINPHONE8_USB = { 0x26fedc4eL, 0x6ac3, 0x4241, 0x9e, 0x4d, 0xe3, 0xd4, 0xb2, 0xc5, 0xc5, 0x34 };
+
 // Handlers
 typedef int (*HandleDeviceFunction)(int, const QString &, const QString &, HANDLE);
 extern int handleAppxDevice(int deviceIndex, const QString &app, const QString &cacheDir, HANDLE runLock);
 extern int handleXapDevice(int deviceIndex, const QString &app, const QString &cacheDir, HANDLE runLock);
+typedef int (*AppListFunction)(int, QSet<QString> &);
+extern int xapAppNames(int deviceIndex, QSet<QString> &apps);
+
+extern QStringList xapDeviceNames();
 
 // Callbacks
 static void __stdcall run(DWORD argc, LPWSTR argv[]);
 static DWORD __stdcall control(DWORD control, DWORD eventType, void *eventData, void *context);
 static BOOL __stdcall control(DWORD type);
+static LRESULT __stdcall control(HWND window, UINT msg, WPARAM wParam, LPARAM lParam);
+static DWORD __stdcall deviceWorker(LPVOID param);
 static DWORD __stdcall appWorker(LPVOID param);
 
 union ErrorId
@@ -88,7 +99,7 @@ union ErrorId
 
 enum ControlEvent
 {
-    NoCommand = 0, Stop = 1, NewWorker = 2
+    NoCommand = 0, Stop = 1, NewWorker = 2, PhoneConnected = 3
 };
 
 struct D3DServicePrivate
@@ -98,6 +109,8 @@ struct D3DServicePrivate
         , checkPoint(1)
         , isService(false)
         , controlEvent(CreateEvent(NULL, FALSE, FALSE, NULL))
+        , controlWindow(0)
+        , deviceHandle(0)
     {
         GetModuleFileName(NULL, path, MAX_PATH);
         status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
@@ -105,6 +118,10 @@ struct D3DServicePrivate
     }
     ~D3DServicePrivate()
     {
+        if (deviceHandle)
+            UnregisterDeviceNotification(deviceHandle);
+        if (controlWindow)
+            CloseHandle(controlWindow);
         if (controlEvent)
             CloseHandle(controlEvent);
     }
@@ -119,6 +136,8 @@ struct D3DServicePrivate
     // Internal use
     bool isService;
     HANDLE controlEvent;
+    HWND controlWindow;
+    HDEVNOTIFY deviceHandle;
     QList<ControlEvent> eventQueue;
     QList<QStringPair> workerQueue;
 };
@@ -338,14 +357,58 @@ void __stdcall run(DWORD argc, LPWSTR argv[])
         D3DService::reportStatus(SERVICE_RUNNING, NO_ERROR, 0);
     } else {
         SetConsoleCtrlHandler(&control, TRUE);
+
+        // Create an invisible window for getting broadcast events
+        WNDCLASS controlWindowClass = { 0, &control, 0, 0, NULL, NULL,
+                                        NULL, NULL, NULL, L"controlWindow" };
+        if (!RegisterClass(&controlWindowClass)) {
+            qCCritical(lcD3DService) << "Unable to register control window class:"
+                                     << qt_error_string(GetLastError());
+            return;
+        }
+        d->controlWindow = CreateWindowEx(0, L"controlWindow", NULL, 0, 0, 0, 0, 0,
+                                          NULL, NULL, NULL, NULL);
     }
 
+    // Register for USB notifications
+    DEV_BROADCAST_DEVICEINTERFACE filter = {
+        sizeof(DEV_BROADCAST_DEVICEINTERFACE), DBT_DEVTYP_DEVICEINTERFACE,
+        0, GUID_DEVICE_WINPHONE8_USB, 0
+    };
+    d->deviceHandle = RegisterDeviceNotification(
+                d->isService ? d->statusHandle : static_cast<HANDLE>(d->controlWindow), &filter,
+                d->isService ? DEVICE_NOTIFY_SERVICE_HANDLE : DEVICE_NOTIFY_WINDOW_HANDLE);
+
     QVector<HANDLE> waitHandles;
     waitHandles.append(d->controlEvent);
 
+    // Dummy handle for phone (gets replaced by a worker when needed)
+    HANDLE dummyHandle;
+    DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
+                    GetCurrentProcess(), &dummyHandle, SYNCHRONIZE, FALSE, 0);
+    waitHandles.append(dummyHandle);
+
+    // Event handles for emulators (XDE)
+    WCHAR username[MAX_PATH];
+    ULONG usernameSize = MAX_PATH;
+    GetUserName(username, &usernameSize);
+    const QStringList emulatorNames = xapDeviceNames().mid(1);
+    foreach (const QString &name, emulatorNames) {
+        const QString eventName = QStringLiteral("Local\\XdeOnServerInitialize")
+                + name + QLatin1Char('.') + QString::fromWCharArray(username, usernameSize).toLower();
+        HANDLE event = CreateEvent(NULL, TRUE, FALSE, reinterpret_cast<LPCWSTR>(eventName.utf16()));
+        if (event)
+            waitHandles.append(event);
+        else
+            qCWarning(lcD3DService) << "Unable to create event:" << qt_error_string(GetLastError());
+    }
+
     // App monitoring threads
     QVector<Worker *> workers;
 
+    // Device monitoring threads - one per device
+    QVector<Worker *> deviceWorkers(emulatorNames.size() + 1, NULL);
+
     // Static list of registrations
     foreach (const QStringPair &registration, D3DService::registrations()) {
         Worker *worker = new Worker(registration, &appWorker);
@@ -353,13 +416,28 @@ void __stdcall run(DWORD argc, LPWSTR argv[])
         waitHandles.append(worker->thread());
     }
 
+    // If a Windows Phone is already connected, queue a device worker
+    HDEVINFO info = SetupDiGetClassDevs(&GUID_DEVICE_WINPHONE8_USB, NULL, NULL,
+                                        DIGCF_DEVICEINTERFACE|DIGCF_PRESENT);
+    if (info != INVALID_HANDLE_VALUE) {
+        SP_DEVINFO_DATA infoData = { sizeof(SP_DEVINFO_DATA) };
+        if (SetupDiEnumDeviceInfo(info, 0, &infoData)) {
+            d->eventQueue.append(PhoneConnected);
+            SetEvent(d->controlEvent);
+        }
+        SetupDiDestroyDeviceInfoList(info);
+    }
+
     // Master loop
     // This loop handles incoming events from the service controller and
     // worker threads. It also creates new worker threads as needed.
-    const uint minWorker = WAIT_OBJECT_0 + 1;
+    const uint phoneEvent = WAIT_OBJECT_0 + 1;
+    const uint minEmulatorEvent = phoneEvent + 1;
+    const uint maxEmulatorEvent = minEmulatorEvent + emulatorNames.size() - 1;
+    const uint minWorker = maxEmulatorEvent + 1;
     uint maxWorker = minWorker + workers.size() - 1;
     forever {
-        DWORD event = WaitForMultipleObjects(waitHandles.size(), waitHandles.data(), FALSE, INFINITE);
+        DWORD event = MsgWaitForMultipleObjects(waitHandles.size(), waitHandles.data(), FALSE, INFINITE, QS_ALLINPUT);
         if (event >= WAIT_OBJECT_0 && event < WAIT_OBJECT_0 + waitHandles.size()) {
             // A control event occurred
             if (event == WAIT_OBJECT_0) {
@@ -383,6 +461,21 @@ void __stdcall run(DWORD argc, LPWSTR argv[])
                         }
                         continue;
                     }
+
+                    // A Windows Phone device was connected
+                    if (controlEvent == PhoneConnected) {
+                        qCDebug(lcD3DService) << "A Windows Phone has connected.";
+                        // The worker is already active
+                        if (deviceWorkers.first())
+                            continue;
+
+                        // Start the phone monitoring thread
+                        Worker *worker = new Worker(qMakePair(QString::number(0), QString()), &deviceWorker);
+                        deviceWorkers[0] = worker;
+                        CloseHandle(waitHandles[phoneEvent - WAIT_OBJECT_0]);
+                        waitHandles[phoneEvent - WAIT_OBJECT_0] = worker->thread();
+                        continue;
+                    }
                 }
 
                 if (shutdown)
@@ -391,6 +484,60 @@ void __stdcall run(DWORD argc, LPWSTR argv[])
                 continue;
             }
 
+            // Device events
+            if (event >= phoneEvent && event <= maxEmulatorEvent) {
+                const int deviceIndex = event - phoneEvent;
+                // Determine if the handle in the slot is an event or thread
+                if (GetThreadId(waitHandles[event - WAIT_OBJECT_0])) {
+                    qCDebug(lcD3DService) << "Device worker exited:" << deviceIndex;
+                    // The thread has exited, close the handle and replace the event
+                    delete deviceWorkers.at(deviceIndex);
+                    deviceWorkers[deviceIndex] = 0;
+
+                    // The phone case is handled elsewhere; set a dummy handle
+                    if (event == phoneEvent) {
+                        HANDLE dummyHandle;
+                        DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
+                                        GetCurrentProcess(), &dummyHandle, SYNCHRONIZE, FALSE, 0);
+                        waitHandles[event - WAIT_OBJECT_0] = dummyHandle;
+                        continue;
+                    }
+
+                    // Re-create the event handle
+                    const QString eventName = QStringLiteral("Local\\XdeOnServerInitialize")
+                            + emulatorNames.at(deviceIndex - 1) + QLatin1Char('.')
+                            + QString::fromWCharArray(username, usernameSize).toLower();
+                    HANDLE emulatorEvent = CreateEvent(
+                                NULL, TRUE, FALSE, reinterpret_cast<LPCWSTR>(eventName.utf16()));
+                    if (emulatorEvent) {
+                        waitHandles[event - WAIT_OBJECT_0] = emulatorEvent;
+                    } else {
+                        // If the above fails, replace it with a dummy to keep things going
+                        qCCritical(lcD3DService).nospace() << "Unable to create event for emulator "
+                                                           << emulatorNames.at(deviceIndex - 1)
+                                                           << ": " << qt_error_string(GetLastError());
+                        HANDLE dummyHandle;
+                        DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
+                                        GetCurrentProcess(), &dummyHandle, SYNCHRONIZE, FALSE, 0);
+                        waitHandles[event - WAIT_OBJECT_0] = dummyHandle;
+                    }
+                } else {
+                    qCDebug(lcD3DService) << "An emulator was activated:" << deviceIndex;
+                    // The event was set; close the handle and replace with a thread
+                    CloseHandle(waitHandles[event - WAIT_OBJECT_0]);
+
+                    // This shouldn't happen
+                    if (event == phoneEvent)
+                        continue;
+
+                    const QStringPair config = qMakePair(QString::number(deviceIndex), QString());
+                    Worker *worker = new Worker(config, &deviceWorker);
+                    deviceWorkers[deviceIndex] = worker;
+                    waitHandles[event - WAIT_OBJECT_0] = worker->thread();
+                }
+                continue;
+            }
+
             // A worker exited
             if (event >= minWorker && event <= maxWorker) {
                 // Delete the worker and clear the handle (TODO: remove it?)
@@ -400,6 +547,13 @@ void __stdcall run(DWORD argc, LPWSTR argv[])
                 continue;
             }
         }
+
+        // TODO: check return val for this
+        if (!d->isService) {
+            MSG msg;
+            if (PeekMessage(&msg, d->controlWindow, 0, 0, PM_REMOVE))
+                DispatchMessage(&msg);
+        }
     }
 
     qDeleteAll(workers);
@@ -412,6 +566,59 @@ void __stdcall run(DWORD argc, LPWSTR argv[])
     D3DService::reportStatus(SERVICE_STOPPED, NO_ERROR, 0);
 }
 
+DWORD __stdcall deviceWorker(LPVOID param)
+{
+    WorkerParam *args = reinterpret_cast<WorkerParam *>(param);
+
+    // The list of applications on this device will be polled until
+    // an error code is returned (e.g. the device is disconnected) or
+    // the thread is told to exit.
+    QSet<QString> appNames;
+    AppListFunction appList;
+
+    int deviceIndex = 0;
+    if (args->deviceName.isEmpty() || args->deviceName == QStringLiteral("local")) {
+        // Not implemented
+        return 0;
+    } else {
+        // CoreCon (Windows Phone)
+        bool ok;
+        deviceIndex = args->deviceName.toInt(&ok);
+        if (!ok)
+            return WorkerParam::BadDeviceIndex;
+
+        appList = xapAppNames;
+    }
+
+    forever {
+        if (WaitForSingleObject(args->runLock, 0) == WAIT_OBJECT_0)
+            return 0;
+
+        QSet<QString> latestAppNames;
+        int exitCode = appList(deviceIndex, latestAppNames);
+        if (exitCode != WorkerParam::NoError)
+            return exitCode;
+
+        QSet<QString> newAppNames = latestAppNames - appNames;
+        if (!newAppNames.isEmpty()) {
+            // Create a new app watcher for each new app
+            foreach (const QString &app, newAppNames) {
+                qCWarning(lcD3DService).nospace() << "Found app " << app << " on device "
+                                                  << args->deviceName << '.';
+                d->workerQueue.append(qMakePair(args->deviceName, app));
+            }
+
+            d->eventQueue.append(NewWorker);
+            SetEvent(d->controlEvent);
+        }
+
+        appNames = latestAppNames;
+        Sleep(1000);
+    }
+
+    return 0;
+}
+
 DWORD __stdcall appWorker(LPVOID param)
 {
     WorkerParam *args = reinterpret_cast<WorkerParam *>(param);
@@ -458,12 +665,38 @@ DWORD __stdcall control(DWORD code, DWORD eventType, void *eventData, void *cont
         SetEvent(d->controlEvent);
         return NO_ERROR;
     }
+    case SERVICE_CONTROL_DEVICEEVENT: {
+        if (eventType == DBT_DEVICEARRIVAL) {
+            DEV_BROADCAST_DEVICEINTERFACE *header =
+                reinterpret_cast<DEV_BROADCAST_DEVICEINTERFACE *>(eventData);
+            if (header->dbcc_classguid != GUID_DEVICE_WINPHONE8_USB)
+                break;
+            d->eventQueue.append(PhoneConnected);
+            SetEvent(d->controlEvent);
+            return NO_ERROR;
+        }
+        break;
+    }
     default:
         break;
     }
     return ERROR_CALL_NOT_IMPLEMENTED;
 }
 
+// Console message controller
+LRESULT __stdcall control(HWND window, UINT msg, WPARAM wParam, LPARAM lParam)
+{
+    if (msg == WM_DEVICECHANGE && wParam == DBT_DEVICEARRIVAL) {
+        DEV_BROADCAST_DEVICEINTERFACE *header =
+                reinterpret_cast<DEV_BROADCAST_DEVICEINTERFACE *>(lParam);
+        if (header->dbcc_classguid == GUID_DEVICE_WINPHONE8_USB) {
+            d->eventQueue.append(PhoneConnected);
+            SetEvent(d->controlEvent);
+        }
+    }
+    return DefWindowProc(window, msg, wParam, lParam);
+}
+
 // Console CTRL controller
 BOOL __stdcall control(DWORD type)
 {
index 9283538..bbc936a 100644 (file)
@@ -7,7 +7,7 @@ DEFINES += QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII WINRT_LIBRARY WINAPI_FAMILY
 
 QMAKE_CXXFLAGS += -EHsc
 
-LIBS += -lruntimeobject
+LIBS += -lruntimeobject -lsetupapi
 
 SOURCES = \
     appxhandler.cpp \
index 193bc12..135c211 100644 (file)
@@ -56,6 +56,96 @@ Q_GLOBAL_STATIC(CoreConServer, coreConServer)
 
 #define bstr(s) _bstr_t((const wchar_t *)s.utf16())
 
+// This is used by the service to simplify gathering of device data
+extern QStringList xapDeviceNames()
+{
+    if (!coreConServer->initialize()) {
+        while (!coreConServer.exists())
+            Sleep(1);
+    }
+
+    QStringList deviceNames;
+    foreach (const CoreConDevice *device, coreConServer->devices())
+        deviceNames.append(device->name());
+
+    return deviceNames;
+}
+
+// Allows looking up of application names
+extern int xapAppNames(int deviceIndex, QSet<QString> &apps)
+{
+    if (!coreConServer->initialize()) {
+        while (!coreConServer.exists())
+            Sleep(1);
+    }
+
+    CoreConDevice *device = coreConServer->devices().value(deviceIndex, 0);
+    if (!device) {
+        qCWarning(lcD3DService) << "Device at index" << deviceIndex << "not found.";
+        return 1;
+    }
+
+    HRESULT hr;
+    _bstr_t connectionName;
+    ComPtr<ICcConnection> connection;
+    hr = coreConServer->handle()->GetConnection(
+        device->handle(), 5000, NULL, connectionName.GetAddress(), &connection);
+    if (FAILED(hr)) {
+        qCWarning(lcD3DService) << "Unable to initialize connection:"
+                                << coreConServer->formatError(hr);
+        return 1;
+    }
+
+    hr = connection->ConnectDevice();
+    // For phones, we wait around for a pin unlock (or a different error)
+    if (!device->isEmulator()) {
+        while (hr == 0x89740006) { // Device is pinlocked
+            qCDebug(lcD3DService) << coreConServer->formatError(hr);
+            Sleep(1000);
+            hr = connection->ConnectDevice();
+        }
+    }
+    if (FAILED(hr)) {
+        qCWarning(lcD3DService) << "Unable to connect to device:"
+                                << coreConServer->formatError(hr);
+        return 1;
+    }
+
+    ComPtr<ICcConnection3> connection3;
+    hr = connection.As(&connection3);
+    if (FAILED(hr)) {
+        qCWarning(lcD3DService) << "Unable to obtain connection3 interface:"
+                                << coreConServer->formatError(hr);
+        return 1;
+    }
+
+    SAFEARRAY *productIds, *instanceIds;
+    hr = connection3->GetInstalledApplicationIDs(&productIds, &instanceIds);
+    if (FAILED(hr)) {
+        qCWarning(lcD3DService) << "Unable to get installed applications:"
+                                 << coreConServer->formatError(hr);
+        return 1;
+    }
+    if (productIds && instanceIds) {
+        Q_ASSERT(productIds->rgsabound[0].cElements == instanceIds->rgsabound[0].cElements);
+        for (ulong i = 0; i < productIds->rgsabound[0].cElements; ++i) {
+            LONG indices[] = { i };
+            _bstr_t productId;
+            _bstr_t instanceId;
+            if (SUCCEEDED(SafeArrayGetElement(productIds, indices, productId.GetAddress()))
+                    && SUCCEEDED(SafeArrayGetElement(instanceIds, indices, instanceId.GetAddress()))) {
+                apps.insert(QString::fromWCharArray(productId));
+            }
+        }
+        SafeArrayDestroy(productIds);
+        SafeArrayDestroy(instanceIds);
+        return 0;
+    }
+
+    // No installed applications
+    return 0;
+}
+
 /* This method runs in its own thread for each CoreCon device/application combo
  * the service is currently handling. */
 extern int handleXapDevice(int deviceIndex, const QString &app, const QString &localBase, HANDLE runLock)
@@ -125,43 +215,29 @@ extern int handleXapDevice(int deviceIndex, const QString &app, const QString &l
             continue;
         }
 
-        int retryCount = 0;
-        while (!connected) {
+        if (!connected) {
             hr = connection->ConnectDevice();
             connected = SUCCEEDED(hr);
             if (connected) {
                 qCWarning(lcD3DService).nospace() << "Connected to " << device->name() << ".";
                 wasDisconnected = true;
-            }
-            if (FAILED(hr)) {
-                qCDebug(lcD3DService) << "Unable to connect to device. Retrying..."
-                                      << coreConServer->formatError(hr);
-                if (++retryCount > 30) {
-                    qCCritical(lcD3DService) << "Unable to connect to device. Exiting.";
-                    return 1;
-                }
-                Sleep(1000);
-            }
-        }
-
-        VARIANT_BOOL isInstalled = false;
-        retryCount = 0;
-        while (!isInstalled) {
-            hr = connection3->IsApplicationInstalled(bstr(app), &isInstalled);
-            if (FAILED(hr)) {
-                qCCritical(lcD3DService) << "Unable to determine if package is installed:"
-                                         << coreConServer->formatError(hr);
+            } else {
+                qCDebug(lcD3DService).nospace() << "Unable to connect to " << device->name()
+                                                << ": " << coreConServer->formatError(hr);
                 return 1;
             }
+        }
 
-            if (!isInstalled) {
-                qCDebug(lcD3DService) << "Package is not installed. Checking again...";
-                if (++retryCount > 30) {
-                    qCCritical(lcD3DService) << "Package did not materialize within 30 seconds. Exiting.";
-                    return 1;
-                }
-                Sleep(1000);
-            }
+        VARIANT_BOOL isInstalled;
+        hr = connection3->IsApplicationInstalled(bstr(app), &isInstalled);
+        if (FAILED(hr)) {
+            qCCritical(lcD3DService) << "Unable to determine if package is installed:"
+                                     << coreConServer->formatError(hr);
+            return 1;
+        }
+        if (!isInstalled) {
+            qCWarning(lcD3DService) << "Package" << app << "is not installed. Exiting worker.";
+            return 1;
         }
 
         // Run certain setup steps once per connection