diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b17d0a3c..b1e75819 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -898,6 +898,39 @@ "command": ".\\4-example_with_glfw_c.exe", "problemMatcher": "$msCompile" } + }, + { + "label": "Run Device Hot-plug Test (Debug)", + "type": "shell", + "command": "bash", + "args": [ + "-l", + "-c", + "( if [[ $(pwd) =~ ^/mnt ]]; then ./ccap_device_hotplug_test.exe; else ./ccap_device_hotplug_test; fi )" + ], + "options": { + "cwd": "${workspaceFolder}/build/tests/Debug" + }, + "group": "test", + "problemMatcher": "$gcc", + "dependsOn": [ + "Build Project (Debug)" + ], + "windows": { + "command": ".\\ccap_device_hotplug_test.exe", + "options": { + "cwd": "${workspaceFolder}/build/tests/Debug" + }, + "problemMatcher": "$msCompile" + }, + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "dedicated", + "showReuseMessage": false, + "clear": true + } } ] } \ No newline at end of file diff --git a/src/ccap_imp_windows.cpp b/src/ccap_imp_windows.cpp index d9eeebe8..255b853d 100644 --- a/src/ccap_imp_windows.cpp +++ b/src/ccap_imp_windows.cpp @@ -91,6 +91,58 @@ using namespace ccap; namespace { constexpr FrameOrientation kDefaultFrameOrientation = FrameOrientation::BottomToTop; +#if defined(_MSC_VER) || (defined(__MINGW64__) && defined(__SEH__)) +#define CCAP_SEH_SUPPORTED 1 +#else +#define CCAP_SEH_SUPPORTED 0 +#endif + +enum class DeviceBindResult { + Success = 0, + Failed = 1, + Exception = 2 +}; + +/// Bind moniker to filter with SEH protection for device validation +DeviceBindResult tryBindMonikerToFilter(IMoniker* moniker, std::string_view name) { +#if CCAP_SEH_SUPPORTED + __try { +#endif + IBaseFilter* filter = nullptr; + HRESULT hr = moniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&filter); + if (SUCCEEDED(hr) && filter) { + filter->Release(); + return DeviceBindResult::Success; + } else { + CCAP_LOG_I("ccap: \"%s\" is not a valid video capture device, removed\n", name.data()); + return DeviceBindResult::Failed; + } +#if CCAP_SEH_SUPPORTED + } __except (EXCEPTION_EXECUTE_HANDLER) { + CCAP_LOG_W("ccap: \"%s\" caused an exception during device binding, skipping\n", name.data()); + return DeviceBindResult::Exception; + } +#endif +} + +/// Bind moniker to device filter with SEH protection for device opening +DeviceBindResult tryBindMonikerForOpen(IMoniker* moniker, IBaseFilter** deviceFilter) { +#if CCAP_SEH_SUPPORTED + __try { +#endif + HRESULT hr = moniker->BindToObject(0, 0, IID_IBaseFilter, (void**)deviceFilter); + if (SUCCEEDED(hr)) { + return DeviceBindResult::Success; + } else { + return DeviceBindResult::Failed; + } +#if CCAP_SEH_SUPPORTED + } __except (EXCEPTION_EXECUTE_HANDLER) { + return DeviceBindResult::Exception; + } +#endif +} + // Release the format block for a media type. void freeMediaType(AM_MEDIA_TYPE& mt) { if (mt.cbFormat != 0) { @@ -400,15 +452,12 @@ std::vector ProviderDirectShow::findDeviceNames() { enumerateDevices([&](IMoniker* moniker, std::string_view name) { // Try to bind device, check if available - IBaseFilter* filter = nullptr; - HRESULT hr = moniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&filter); - if (SUCCEEDED(hr) && filter) { + // Use helper function with SEH to catch crashes from exception-throwing devices + DeviceBindResult result = tryBindMonikerToFilter(moniker, name); + if (result == DeviceBindResult::Success) { m_allDeviceNames.emplace_back(name.data(), name.size()); - filter->Release(); - } else { - CCAP_LOG_I("ccap: \"%s\" is not a valid video capture device, removed\n", name.data()); } - // Unavailable devices are not added to the list + // Unavailable devices (Failed or Exception) are not added to the list return false; // Continue enumeration }); @@ -660,19 +709,30 @@ bool ProviderDirectShow::open(std::string_view deviceName) { enumerateDevices([&](IMoniker* moniker, std::string_view name) { if (deviceName.empty() || deviceName == name) { - auto hr = moniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&m_deviceFilter); - if (SUCCEEDED(hr)) { + // Use helper function with SEH to catch crashes from exception-throwing devices + DeviceBindResult result = tryBindMonikerForOpen(moniker, &m_deviceFilter); + + if (result == DeviceBindResult::Success) { + // Success - save device name and stop enumeration CCAP_LOG_V("ccap: Using video capture device: %s\n", name.data()); m_deviceName = name; found = true; - return true; // stop enumeration when returning true - } else { - if (!deviceName.empty()) { + return true; + } else if (!deviceName.empty()) { + // Specific device was requested but failed + if (result == DeviceBindResult::Exception) { + reportError(ErrorCode::InvalidDevice, "\"" + std::string(deviceName) + "\" caused an exception during binding"); + } else { reportError(ErrorCode::InvalidDevice, "\"" + std::string(deviceName) + "\" is not a valid video capture device, bind failed"); - return true; // stop enumeration when returning true } - - CCAP_LOG_I("ccap: bind \"%s\" failed(result=%x), try next device...\n", name.data(), hr); + return true; // stop enumeration + } else { + // No specific device requested and this one failed - log and continue trying next device + if (result == DeviceBindResult::Exception) { + CCAP_LOG_W("ccap: \"%s\" caused an exception during device binding, skipping\n", name.data()); + } else { + CCAP_LOG_I("ccap: bind \"%s\" failed, try next device...\n", name.data()); + } } } // continue enumerating when returning false diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6a95bb22..c4908ac1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -205,3 +205,67 @@ else() -O3 ) endif() + +# ============================================================ +# Interactive Device Hot-plug Test +# Tests crash prevention when enumerating unplugged devices +# with buggy drivers (Windows DirectShow) +# This test requires manual interaction and is NOT part of +# the automated test suite +# ============================================================ + +add_executable( + ccap_device_hotplug_test + test_device_hotplug.cpp +) + +target_link_libraries( + ccap_device_hotplug_test + ccap +) + +set_target_properties(ccap_device_hotplug_test PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON +) + +# Platform-specific compile options +if(MSVC) + target_compile_options(ccap_device_hotplug_test PRIVATE + /MP + /std:c++17 + /Zc:__cplusplus + /Zc:preprocessor + /source-charset:utf-8 + /bigobj + /wd4996 + /D_CRT_SECURE_NO_WARNINGS + ) +else() + target_compile_options(ccap_device_hotplug_test PRIVATE + -std=c++17 + ) +endif() + +# Add a custom target to run this interactive test manually +# Usage: cmake --build . --target run_device_hotplug_test +if(WIN32) + add_custom_target(run_device_hotplug_test + COMMAND ${CMAKE_COMMAND} -E echo "==========================================" + COMMAND ${CMAKE_COMMAND} -E echo "Running Device Hot-plug Interactive Test" + COMMAND ${CMAKE_COMMAND} -E echo "==========================================" + COMMAND $ + DEPENDS ccap_device_hotplug_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Running interactive device hot-plug test (buggy driver crash prevention)" + ) +endif() + +message(STATUS "Device hot-plug interactive test configured: ccap_device_hotplug_test") +if(WIN32) + message(STATUS " Run manually with: cmake --build . --target run_device_hotplug_test") + message(STATUS " Or execute: ${CMAKE_BINARY_DIR}/tests/Debug/ccap_device_hotplug_test.exe") +else() + message(STATUS " Run manually with: ./ccap_device_hotplug_test") +endif() + diff --git a/tests/test_device_hotplug.cpp b/tests/test_device_hotplug.cpp new file mode 100644 index 00000000..88a01c35 --- /dev/null +++ b/tests/test_device_hotplug.cpp @@ -0,0 +1,342 @@ +/** + * @file test_device_hotplug.cpp + * @brief Interactive test for device hot-plug handling with crash prevention + * + * This test verifies crash prevention when enumerating devices that have been + * physically unplugged while their drivers remain active. Some camera drivers + * (especially VR headsets like Oculus Quest 3) can cause crashes when their + * BindToObject() calls are made after the device is disconnected. + * + * The fix uses Windows SEH (Structured Exception Handling) to catch and handle + * these crashes gracefully, allowing enumeration to continue. + * + * Usage: + * 1. Run this test with all devices connected + * 2. When prompted, physically unplug a device + * 3. Press Enter to continue enumeration + * 4. The test verifies that enumeration completes without crashing + * + * This is NOT a regular unit test. It requires manual interaction and should be run + * separately from automated test suites. + * + * @see https://github.com/wysaid/CameraCapture/issues/26 + */ + +#include "ccap.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +// ANSI color codes for better output readability +constexpr const char* COLOR_RESET = "\033[0m"; +constexpr const char* COLOR_GREEN = "\033[32m"; +constexpr const char* COLOR_YELLOW = "\033[33m"; +constexpr const char* COLOR_RED = "\033[31m"; +constexpr const char* COLOR_BLUE = "\033[34m"; +constexpr const char* COLOR_CYAN = "\033[36m"; + +void printHeader(const char* message) { + std::cout << "\n" << COLOR_CYAN << "========================================" << COLOR_RESET << "\n"; + std::cout << COLOR_CYAN << message << COLOR_RESET << "\n"; + std::cout << COLOR_CYAN << "========================================" << COLOR_RESET << "\n\n"; +} + +void printSuccess(const char* message) { + std::cout << COLOR_GREEN << "[SUCCESS] " << message << COLOR_RESET << "\n"; +} + +void printWarning(const char* message) { + std::cout << COLOR_YELLOW << "[WARNING] " << message << COLOR_RESET << "\n"; +} + +void printError(const char* message) { + std::cout << COLOR_RED << "[ERROR] " << message << COLOR_RESET << "\n"; +} + +void printInfo(const char* message) { + std::cout << COLOR_BLUE << "[INFO] " << message << COLOR_RESET << "\n"; +} + +void waitForUserInput() { + std::cout << COLOR_YELLOW << "\n>>> Press Enter to continue..." << COLOR_RESET << std::flush; + std::string dummy; + std::getline(std::cin, dummy); +} + +void printDeviceList(const std::vector& devices, const char* title) { + std::cout << "\n" << COLOR_BLUE << title << COLOR_RESET << "\n"; + std::cout << "----------------------------------------\n"; + + if (devices.empty()) { + printWarning("No devices found!"); + } else { + for (size_t i = 0; i < devices.size(); ++i) { + std::cout << " [" << i << "] " << devices[i] << "\n"; + } + } + std::cout << "----------------------------------------\n"; +} + +void countdownTimer(int seconds) { + std::cout << "\n" << COLOR_YELLOW; + for (int i = seconds; i > 0; --i) { + std::cout << "\rStarting test in " << i << " second(s)... " << std::flush; + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + std::cout << "\r" << COLOR_RESET; +} + +} // anonymous namespace + +/** + * @brief Test scenario 1: Enumerate devices before and after unplugging + * + * This test verifies that device enumeration: + * 1. Completes successfully with all devices connected + * 2. Does not crash when a device with buggy driver is unplugged + * 3. Properly skips problematic devices and continues enumeration + */ +void testDeviceEnumerationWithHotplug() { + printHeader("TEST: Device Enumeration with Hot-plug/Unplug"); + + std::cout << COLOR_CYAN << "This test verifies crash prevention during device hot-plug:\n" + << " - Enumerating devices with buggy drivers after physical disconnect\n" + << " - Example: Oculus Quest 3 VR headset, some USB capture cards\n" + << COLOR_RESET << "\n"; + + // Step 1: Initial enumeration with all devices connected + printInfo("Step 1: Enumerating devices with all cameras connected..."); + + ccap::Provider provider; + std::vector devicesBeforeUnplug; + + try { + devicesBeforeUnplug = provider.findDeviceNames(); + printSuccess("Initial enumeration completed successfully!"); + printDeviceList(devicesBeforeUnplug, "Devices Found (Initial):"); + } catch (const std::exception& e) { + printError("Exception during initial enumeration!"); + std::cerr << " Exception: " << e.what() << "\n"; + return; + } catch (...) { + printError("Unknown exception during initial enumeration!"); + return; + } + + if (devicesBeforeUnplug.empty()) { + printWarning("No devices found. Cannot proceed with hot-plug test."); + printInfo("Please connect at least one camera device and try again."); + return; + } + + // Step 2: Wait for user to unplug a problematic device + std::cout << "\n" << COLOR_YELLOW << "========================================\n" + << "ACTION REQUIRED:\n" + << "========================================\n" << COLOR_RESET; + + std::cout << COLOR_YELLOW << "Please perform the following:\n\n" + << " 1. Identify a device with potentially buggy drivers\n" + << " (e.g., VR headset, virtual camera, or any device\n" + << " known to cause issues when unplugged)\n\n" + << " 2. Physically disconnect/unplug that device\n\n" + << " 3. Wait a few seconds for the OS to detect the change\n\n" + << " 4. Press Enter to continue the test\n" << COLOR_RESET; + + std::cout << "\n" << COLOR_RED << "NOTE: If you don't have a problematic device, you can:\n" + << " - Still proceed to test normal device enumeration\n" + << " - Or unplug any camera to test hot-unplug handling\n" << COLOR_RESET; + + waitForUserInput(); + + // Step 3: Enumerate again after device unplugging + printInfo("Step 2: Re-enumerating devices after unplugging..."); + printInfo("This should NOT crash, even with buggy drivers!"); + + std::vector devicesAfterUnplug; + bool enumerationSucceeded = false; + + try { + // Create a new provider instance to force fresh enumeration + ccap::Provider newProvider; + devicesAfterUnplug = newProvider.findDeviceNames(); + enumerationSucceeded = true; + + printSuccess("Enumeration after unplug completed successfully!"); + printSuccess("Crash prevention is working correctly!"); + + printDeviceList(devicesAfterUnplug, "Devices Found (After Unplug):"); + + } catch (const std::exception& e) { + printError("Exception during post-unplug enumeration!"); + std::cerr << " Exception: " << e.what() << "\n"; + printError("This indicates the crash prevention may not be working!"); + } catch (...) { + printError("Unknown exception during post-unplug enumeration!"); + printError("This indicates the crash prevention may not be working!"); + } + + // Step 4: Analyze results + std::cout << "\n" << COLOR_CYAN << "========================================\n" + << "TEST RESULTS ANALYSIS\n" + << "========================================\n" << COLOR_RESET; + + if (!enumerationSucceeded) { + printError("FAILED: Enumeration crashed or threw an exception!"); + printError("Crash prevention may not be working properly."); + return; + } + + printSuccess("PASSED: No crashes during enumeration!"); + + // Compare device counts + std::cout << "\n" << COLOR_BLUE << "Device Count Comparison:" << COLOR_RESET << "\n"; + std::cout << " Before unplug: " << devicesBeforeUnplug.size() << " device(s)\n"; + std::cout << " After unplug: " << devicesAfterUnplug.size() << " device(s)\n"; + + if (devicesAfterUnplug.size() < devicesBeforeUnplug.size()) { + printSuccess("Device count decreased as expected."); + } else if (devicesAfterUnplug.size() == devicesBeforeUnplug.size()) { + printWarning("Device count unchanged. You may not have unplugged a device,"); + printWarning("or the unplugged device was already being filtered out."); + } else { + printWarning("Device count increased (unexpected). A new device may have been connected."); + } + + // Identify missing devices + if (devicesAfterUnplug.size() < devicesBeforeUnplug.size()) { + std::cout << "\n" << COLOR_BLUE << "Missing Device(s):" << COLOR_RESET << "\n"; + for (const auto& device : devicesBeforeUnplug) { + if (std::find(devicesAfterUnplug.begin(), devicesAfterUnplug.end(), device) == devicesAfterUnplug.end()) { + std::cout << " - " << device << "\n"; + } + } + } + + std::cout << "\n" << COLOR_GREEN << "========================================\n" + << "TEST COMPLETED SUCCESSFULLY\n" + << "========================================\n" << COLOR_RESET; +} + +/** + * @brief Test scenario 2: Try to open a device after unplugging + * + * This test verifies that attempting to open an unplugged device: + * 1. Does not crash the application + * 2. Returns a proper error status + * 3. Allows the application to continue normally + */ +void testOpenUnpluggedDevice() { + printHeader("TEST: Open Device After Unplug"); + + std::cout << COLOR_CYAN << "This test verifies that opening a device that was\n" + << "unplugged after enumeration handles the error gracefully.\n" + << COLOR_RESET << "\n"; + + ccap::Provider provider; + auto devices = provider.findDeviceNames(); + + if (devices.empty()) { + printWarning("No devices found. Skipping this test."); + return; + } + + printDeviceList(devices, "Available Devices:"); + + std::cout << "\n" << COLOR_YELLOW << "ACTION REQUIRED:\n" + << " 1. Note one of the devices listed above\n" + << " 2. Physically unplug that device\n" + << " 3. Press Enter when ready\n" << COLOR_RESET; + + waitForUserInput(); + + printInfo("Attempting to open the first device from the previous enumeration..."); + + ccap::Provider testProvider; + bool openResult = false; + + try { + // Try to open the first device from our enumerated list + // If it was unplugged, this should fail gracefully, not crash + openResult = testProvider.open(devices[0], false); + + if (openResult) { + printSuccess("Device opened successfully (it may not have been unplugged)."); + testProvider.close(); + } else { + printSuccess("Device open failed gracefully (expected if unplugged)."); + printSuccess("No crash occurred - this is the correct behavior!"); + } + } catch (const std::exception& e) { + printError("Exception when trying to open device!"); + std::cerr << " Exception: " << e.what() << "\n"; + } catch (...) { + printError("Unknown exception when trying to open device!"); + } +} + +/** + * @brief Main interactive test program + */ +int main(int argc, char** argv) { + std::cout << COLOR_CYAN; + std::cout << R"( +╭────────────────────────────────────────────────────────────────╮ +│ │ +│ Device Hot-plug Interactive Test │ +│ Testing: Crash prevention for buggy camera drivers │ +│ │ +╰────────────────────────────────────────────────────────────────╯ + )" << COLOR_RESET << "\n"; + + std::cout << COLOR_YELLOW << "IMPORTANT: This is an INTERACTIVE test that requires:\n" + << " - Physical access to camera devices\n" + << " - Ability to connect/disconnect devices during test\n" + << " - Manual verification of results\n" << COLOR_RESET << "\n"; + +#ifndef _WIN32 + printWarning("This test is primarily designed for Windows platform."); + printWarning("The tested issue specifically affects Windows DirectShow implementation."); + printInfo("You can still run this test on other platforms for basic functionality."); + std::cout << "\n"; +#endif + + std::cout << COLOR_BLUE << "The test will proceed in 5 seconds...\n" << COLOR_RESET; + countdownTimer(5); + + // Run test scenarios + try { + testDeviceEnumerationWithHotplug(); + + std::cout << "\n\n"; + std::cout << COLOR_CYAN << "Ready for second test scenario?\n" << COLOR_RESET; + waitForUserInput(); + + testOpenUnpluggedDevice(); + + } catch (const std::exception& e) { + printError("Unhandled exception in test!"); + std::cerr << "Exception: " << e.what() << "\n"; + return 1; + } catch (...) { + printError("Unknown unhandled exception in test!"); + return 1; + } + + std::cout << "\n" << COLOR_GREEN; + std::cout << R"( +╭────────────────────────────────────────────────────────────────╮ +│ │ +│ All tests completed! │ +│ Thank you for testing the hot-plug crash prevention. │ +│ │ +╰────────────────────────────────────────────────────────────────╯ + )" << COLOR_RESET << "\n"; + + return 0; +}