|
| 1 | +/** |
| 2 | + * @file test_apple_permission.cpp |
| 3 | + * @brief Regression test for the macOS camera-permission request deadlock. |
| 4 | + * |
| 5 | + * ccap::runBlockingAsyncRequest() (used by ProviderApple::open) must run the |
| 6 | + * permission request on the calling thread and must NOT bounce it onto the main |
| 7 | + * dispatch queue. Otherwise Provider::open() hangs forever when called from a worker |
| 8 | + * thread in a process whose main thread is not running a run loop -- exactly the |
| 9 | + * situation a Node.js / Electron addon or any head-less multi-threaded embedder |
| 10 | + * creates. |
| 11 | + * |
| 12 | + * We exercise the real helper with a *simulated* asynchronous request: a short |
| 13 | + * countdown that fires the completion from a background thread, just like |
| 14 | + * AVCaptureDevice requestAccessForMediaType: delivers its completion off the caller's |
| 15 | + * run loop. No camera is required, so this runs deterministically in CI. |
| 16 | + * |
| 17 | + * On non-Apple platforms this file compiles to an empty translation unit. |
| 18 | + */ |
| 19 | + |
| 20 | +#if defined(__APPLE__) |
| 21 | + |
| 22 | +#include <gtest/gtest.h> |
| 23 | + |
| 24 | +#include <chrono> |
| 25 | +#include <functional> |
| 26 | +#include <future> |
| 27 | +#include <thread> |
| 28 | + |
| 29 | +#include "ccap_apple_async.h" |
| 30 | + |
| 31 | +namespace |
| 32 | +{ |
| 33 | + |
| 34 | +// Stand-in for AVCaptureDevice requestAccessForMediaType:completionHandler:: it fires |
| 35 | +// the completion asynchronously from a *background* thread after a short countdown, |
| 36 | +// never touching the caller's main run loop. |
| 37 | +void simulateAsyncPermissionRequest(const std::function<void()>& done) |
| 38 | +{ |
| 39 | + std::function<void()> completion = done; // must outlive this call |
| 40 | + std::thread([completion]() { |
| 41 | + std::this_thread::sleep_for(std::chrono::milliseconds(50)); // countdown |
| 42 | + completion(); |
| 43 | + }).detach(); |
| 44 | +} |
| 45 | + |
| 46 | +// Runs runBlockingAsyncRequest (optionally on a worker thread) and reports whether it |
| 47 | +// returned within the timeout. A timeout means it deadlocked. |
| 48 | +bool completesWithoutDeadlock(bool onWorkerThread, std::chrono::milliseconds timeout) |
| 49 | +{ |
| 50 | + std::promise<void> donePromise; |
| 51 | + std::future<void> doneFuture = donePromise.get_future(); |
| 52 | + |
| 53 | + auto body = [&donePromise]() { |
| 54 | + ccap::runBlockingAsyncRequest(&simulateAsyncPermissionRequest); |
| 55 | + donePromise.set_value(); |
| 56 | + }; |
| 57 | + |
| 58 | + std::thread worker; |
| 59 | + if (onWorkerThread) { |
| 60 | + worker = std::thread(body); |
| 61 | + } else { |
| 62 | + body(); |
| 63 | + } |
| 64 | + |
| 65 | + const bool completed = doneFuture.wait_for(timeout) == std::future_status::ready; |
| 66 | + if (worker.joinable()) { |
| 67 | + if (completed) { |
| 68 | + worker.join(); |
| 69 | + } else { |
| 70 | + worker.detach(); // leave the hung thread; the process exits regardless |
| 71 | + } |
| 72 | + } |
| 73 | + return completed; |
| 74 | +} |
| 75 | + |
| 76 | +} // namespace |
| 77 | + |
| 78 | +// The regression: open() called off the main thread with no run loop servicing the |
| 79 | +// main queue. This deadlocked with the old dispatch-to-main-queue implementation. |
| 80 | +TEST(AppleCameraPermission, OffMainThreadWithoutRunLoopDoesNotDeadlock) |
| 81 | +{ |
| 82 | + EXPECT_TRUE(completesWithoutDeadlock(/*onWorkerThread=*/true, std::chrono::seconds(5))) |
| 83 | + << "runBlockingAsyncRequest() deadlocked off the main thread -- the request was " |
| 84 | + "likely bounced onto an unserviced main dispatch queue."; |
| 85 | +} |
| 86 | + |
| 87 | +// Sanity: the common main-thread path must also complete promptly. |
| 88 | +TEST(AppleCameraPermission, MainThreadDoesNotDeadlock) |
| 89 | +{ |
| 90 | + EXPECT_TRUE(completesWithoutDeadlock(/*onWorkerThread=*/false, std::chrono::seconds(5))) |
| 91 | + << "runBlockingAsyncRequest() deadlocked on the main thread."; |
| 92 | +} |
| 93 | + |
| 94 | +#endif // __APPLE__ |
0 commit comments