Skip to content

Commit 8e7b63f

Browse files
committed
refactor(wgc): guard the WinRT calls that can kill the helper silently
Every projected call on the capture path reports failure by throwing, and not one of them was caught: get_activation_factory, .as<IGraphicsCaptureItemInterop>, item_.Size(), CreateFreeThreaded, CreateCaptureSession, FrameArrived, StartCapture, and everything inside onFrameArrived. So the failure mode was std::terminate -- exit code 0xC0000409, no stderr, nothing. main.cpp has always had an "ERROR: Failed to initialize WGC display session" line ready for this and could never reach it, because initialize() did not return false on failure, it took the process with it. guardWinrt() turns a throw into a logged false, applied one or two calls at a time so its label alone names the call that threw -- no breadcrumb to thread through the way mf_encoder.cpp needs one. It is the counterpart of the existing succeeded(), for the calls that throw instead of returning an HRESULT, and it reuses the catch shape applySessionOptions already had. Both initialize() overloads become their step list and nothing else, which also drops the frame-pool block that was duplicated between them verbatim. onFrameArrived gets the same treatment for the same reason, on the hot path: a throw leaving a WinRT delegate is std::terminate, so mid-recording the process would simply vanish. A bad frame is now dropped instead, logged once rather than at frame rate. No GraphicsCaptureSession::IsSupported() pre-flight, deliberately, though an earlier draft of this had one and it read well. It is the only thing here that could refuse a recording that works today -- a machine where IsSupported() answers false but capture would have succeeded stops recording -- and there is no evidence either way about whether such a machine exists. That is the shape of #336: a new gate in front of a path that was working. A nicer error message does not buy that risk. Everything that remains only adds a branch that did not exist, so at worst it never runs. This is hardening, not a fix for an observed failure. The crash that prompted it turned out to be a MAX_PATH stack-buffer overrun rather than an uncaught throw: a build with these guards dies identically and logs nothing, because __fastfail is not an exception. No shipped install path is anywhere near that limit (measured: 140 chars for the Store build, threshold ~255), so it is a local testing hazard only.
1 parent a6795d2 commit 8e7b63f

2 files changed

Lines changed: 172 additions & 71 deletions

File tree

electron/native/wgc-capture/src/wgc_session.cpp

Lines changed: 163 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <winrt/base.h>
88

99
#include <chrono>
10+
#include <exception>
1011
#include <iostream>
1112
#include <thread>
1213

@@ -31,6 +32,57 @@ bool succeeded(HRESULT hr, const char* label) {
3132
return false;
3233
}
3334

35+
// Turns a C++/WinRT throw into a logged `false`.
36+
//
37+
// The projected calls on the setup path -- get_activation_factory, .as<>,
38+
// item_.Size(), CreateFreeThreaded, CreateCaptureSession, FrameArrived,
39+
// StartCapture -- report failure by throwing, and none of them was caught.
40+
// initialize() therefore could not return false: an exception from any of them
41+
// unwound past it into std::terminate and the process ended with no message at
42+
// all, leaving Electron to report "the helper exited before recording started"
43+
// and nothing else. The HRESULT was in the exception the whole time.
44+
//
45+
// No such failure has actually been observed. This is written from reading the
46+
// calls, not from a reproduction -- see the PR for the crash that prompted it
47+
// and turned out to be an unrelated stack-buffer overrun, which is a fastfail
48+
// rather than an exception and is not catchable here or anywhere.
49+
//
50+
// The label is the diagnostic, so a region never spans two calls a reader would
51+
// want told apart: the frame pool and the capture session get one each. Where a
52+
// region does cover several calls it is because they are one step under one
53+
// name -- "GraphicsCaptureItem for a monitor" is the activation factory, the
54+
// interop cast and Size(), and knowing which of those three threw would not
55+
// change what you do next. `succeeded()` above stays as it is for the calls
56+
// that return an HRESULT rather than throwing; this is its counterpart, not its
57+
// replacement.
58+
template <typename Body>
59+
bool guardWinrt(const char* what, Body&& body) {
60+
try {
61+
return body();
62+
} catch (winrt::hresult_error const& error) {
63+
std::cerr << "ERROR: " << what << " threw (hr=0x" << std::hex
64+
<< static_cast<uint32_t>(error.code()) << std::dec << "): "
65+
<< winrt::to_string(error.message()) << std::endl;
66+
return false;
67+
} catch (std::exception const& error) {
68+
std::cerr << "ERROR: " << what << " threw (" << error.what() << ")" << std::endl;
69+
return false;
70+
} catch (...) {
71+
std::cerr << "ERROR: " << what << " threw a non-standard exception" << std::endl;
72+
return false;
73+
}
74+
}
75+
76+
// Deliberately no GraphicsCaptureSession::IsSupported() pre-flight here, though
77+
// it would give a nicer message than an HRESULT on some later call. It is the
78+
// one thing that could *refuse* a recording that works today: a machine where
79+
// IsSupported() answers false but capture would have succeeded records fine now
80+
// and would stop doing so, and there is no evidence either way about whether
81+
// such a machine exists. That is the exact shape of the #336 regression -- a new
82+
// gate in front of a path that was working -- and a better error message is not
83+
// worth carrying it. Everything below only adds a branch that did not exist, so
84+
// at worst it never runs.
85+
3486
int64_t timeSpanToHns(wf::TimeSpan const& value) {
3587
return value.count();
3688
}
@@ -132,43 +184,77 @@ bool WgcSession::createD3DDevice() {
132184
}
133185

134186
bool WgcSession::createCaptureItem(HMONITOR monitor) {
135-
auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>();
136-
auto interop = factory.as<IGraphicsCaptureItemInterop>();
137-
138-
wgcap::GraphicsCaptureItem item{nullptr};
139-
HRESULT hr = interop->CreateForMonitor(
140-
monitor,
141-
winrt::guid_of<wgcap::GraphicsCaptureItem>(),
142-
reinterpret_cast<void**>(winrt::put_abi(item)));
143-
if (!succeeded(hr, "CreateForMonitor")) {
144-
return false;
145-
}
187+
return guardWinrt("GraphicsCaptureItem for a monitor", [&] {
188+
auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>();
189+
auto interop = factory.as<IGraphicsCaptureItemInterop>();
190+
191+
wgcap::GraphicsCaptureItem item{nullptr};
192+
HRESULT hr = interop->CreateForMonitor(
193+
monitor,
194+
winrt::guid_of<wgcap::GraphicsCaptureItem>(),
195+
reinterpret_cast<void**>(winrt::put_abi(item)));
196+
if (!succeeded(hr, "CreateForMonitor")) {
197+
return false;
198+
}
146199

147-
item_ = item;
148-
const auto size = item_.Size();
149-
width_ = static_cast<int>(size.Width);
150-
height_ = static_cast<int>(size.Height);
151-
return width_ > 0 && height_ > 0;
200+
item_ = item;
201+
const auto size = item_.Size();
202+
width_ = static_cast<int>(size.Width);
203+
height_ = static_cast<int>(size.Height);
204+
return width_ > 0 && height_ > 0;
205+
});
152206
}
153207

154208
bool WgcSession::createCaptureItem(HWND window) {
155-
auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>();
156-
auto interop = factory.as<IGraphicsCaptureItemInterop>();
157-
158-
wgcap::GraphicsCaptureItem item{nullptr};
159-
HRESULT hr = interop->CreateForWindow(
160-
window,
161-
winrt::guid_of<wgcap::GraphicsCaptureItem>(),
162-
reinterpret_cast<void**>(winrt::put_abi(item)));
163-
if (!succeeded(hr, "CreateForWindow")) {
209+
return guardWinrt("GraphicsCaptureItem for a window", [&] {
210+
auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>();
211+
auto interop = factory.as<IGraphicsCaptureItemInterop>();
212+
213+
wgcap::GraphicsCaptureItem item{nullptr};
214+
HRESULT hr = interop->CreateForWindow(
215+
window,
216+
winrt::guid_of<wgcap::GraphicsCaptureItem>(),
217+
reinterpret_cast<void**>(winrt::put_abi(item)));
218+
if (!succeeded(hr, "CreateForWindow")) {
219+
return false;
220+
}
221+
222+
item_ = item;
223+
const auto size = item_.Size();
224+
width_ = roundUpToEven(static_cast<int>(size.Width));
225+
height_ = roundUpToEven(static_cast<int>(size.Height));
226+
return width_ > 0 && height_ > 0;
227+
});
228+
}
229+
230+
// Two guards, not one around both: they are separate projected calls, and a
231+
// single region would have reported a CreateCaptureSession throw under the
232+
// CreateFreeThreaded label -- naming the wrong call, which is worse than naming
233+
// none.
234+
bool WgcSession::createFramePoolAndSession() {
235+
const bool pooled = guardWinrt("Direct3D11CaptureFramePool::CreateFreeThreaded", [&] {
236+
framePool_ = wgcap::Direct3D11CaptureFramePool::CreateFreeThreaded(
237+
winrtDevice_,
238+
wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized,
239+
2,
240+
winrt::Windows::Graphics::SizeInt32{width_, height_});
241+
return true;
242+
});
243+
if (!pooled) {
164244
return false;
165245
}
166246

167-
item_ = item;
168-
const auto size = item_.Size();
169-
width_ = roundUpToEven(static_cast<int>(size.Width));
170-
height_ = roundUpToEven(static_cast<int>(size.Height));
171-
return width_ > 0 && height_ > 0;
247+
return guardWinrt("Direct3D11CaptureFramePool::CreateCaptureSession", [&] {
248+
session_ = framePool_.CreateCaptureSession(item_);
249+
return true;
250+
});
251+
}
252+
253+
bool WgcSession::registerFrameArrived() {
254+
return guardWinrt("Direct3D11CaptureFramePool::FrameArrived", [&] {
255+
frameArrivedToken_ = framePool_.FrameArrived({this, &WgcSession::onFrameArrived});
256+
return true;
257+
});
172258
}
173259

174260
bool WgcSession::applySessionOptions(bool captureCursor) {
@@ -217,52 +303,26 @@ bool WgcSession::applySessionOptions(bool captureCursor) {
217303
return true;
218304
}
219305

306+
// Every step reports its own failure, so the two overloads are the step list and
307+
// nothing else. Each returns false rather than throwing past its caller, which
308+
// is what main.cpp's "Failed to initialize WGC display session" has always
309+
// assumed and, until now, was not true of any of them.
220310
bool WgcSession::initialize(HMONITOR monitor, int fps, bool captureCursor) {
221311
fps_ = fps > 0 ? fps : 60;
222-
if (!createD3DDevice()) {
223-
return false;
224-
}
225-
if (!createCaptureItem(monitor)) {
226-
return false;
227-
}
228-
229-
framePool_ = wgcap::Direct3D11CaptureFramePool::CreateFreeThreaded(
230-
winrtDevice_,
231-
wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized,
232-
2,
233-
winrt::Windows::Graphics::SizeInt32{width_, height_});
234-
session_ = framePool_.CreateCaptureSession(item_);
235-
236-
if (!applySessionOptions(captureCursor)) {
237-
return false;
238-
}
239-
240-
frameArrivedToken_ = framePool_.FrameArrived({this, &WgcSession::onFrameArrived});
241-
return true;
312+
return createD3DDevice() &&
313+
createCaptureItem(monitor) &&
314+
createFramePoolAndSession() &&
315+
applySessionOptions(captureCursor) &&
316+
registerFrameArrived();
242317
}
243318

244319
bool WgcSession::initialize(HWND window, int fps, bool captureCursor) {
245320
fps_ = fps > 0 ? fps : 60;
246-
if (!createD3DDevice()) {
247-
return false;
248-
}
249-
if (!createCaptureItem(window)) {
250-
return false;
251-
}
252-
253-
framePool_ = wgcap::Direct3D11CaptureFramePool::CreateFreeThreaded(
254-
winrtDevice_,
255-
wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized,
256-
2,
257-
winrt::Windows::Graphics::SizeInt32{width_, height_});
258-
session_ = framePool_.CreateCaptureSession(item_);
259-
260-
if (!applySessionOptions(captureCursor)) {
261-
return false;
262-
}
263-
264-
frameArrivedToken_ = framePool_.FrameArrived({this, &WgcSession::onFrameArrived});
265-
return true;
321+
return createD3DDevice() &&
322+
createCaptureItem(window) &&
323+
createFramePoolAndSession() &&
324+
applySessionOptions(captureCursor) &&
325+
registerFrameArrived();
266326
}
267327

268328
void WgcSession::setFrameCallback(FrameCallback callback) {
@@ -277,7 +337,12 @@ bool WgcSession::start() {
277337
if (!applySessionOptions(captureCursor_)) {
278338
return false;
279339
}
280-
session_.StartCapture();
340+
if (!guardWinrt("GraphicsCaptureSession::StartCapture", [&] {
341+
session_.StartCapture();
342+
return true;
343+
})) {
344+
return false;
345+
}
281346
started_ = true;
282347
return true;
283348
}
@@ -359,9 +424,36 @@ void WgcSession::stop() {
359424
d3dDevice_.Reset();
360425
}
361426

427+
// The same defect as the setup path, on the hot path. TryGetNextFrame,
428+
// Surface(), the interop cast and SystemRelativeTime() are all projections that
429+
// throw, and a throw leaving a WinRT delegate goes straight to std::terminate --
430+
// a recording that ends with the process disappearing mid-capture, no stderr,
431+
// and the partial file as the only evidence.
432+
//
433+
// Dropping the frame is the only useful response: one bad frame is not a reason
434+
// to end a recording, and a surface that has gone bad usually stays bad. So it
435+
// is logged once and not at frame rate, which at 60 fps is the difference
436+
// between a diagnostic and a denial of service on the log.
362437
void WgcSession::onFrameArrived(
363438
wgcap::Direct3D11CaptureFramePool const& sender,
364439
wf::IInspectable const&) {
440+
try {
441+
deliverFrame(sender);
442+
} catch (winrt::hresult_error const& error) {
443+
if (!frameErrorLogged_.exchange(true)) {
444+
std::cerr << "WARNING: Dropped a WGC frame (hr=0x" << std::hex
445+
<< static_cast<uint32_t>(error.code()) << std::dec
446+
<< "). Further frame errors are not repeated." << std::endl;
447+
}
448+
} catch (...) {
449+
if (!frameErrorLogged_.exchange(true)) {
450+
std::cerr << "WARNING: Dropped a WGC frame. "
451+
<< "Further frame errors are not repeated." << std::endl;
452+
}
453+
}
454+
}
455+
456+
void WgcSession::deliverFrame(wgcap::Direct3D11CaptureFramePool const& sender) {
365457
auto frame = sender.TryGetNextFrame();
366458
if (!frame) {
367459
return;

electron/native/wgc-capture/src/wgc_session.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,17 @@ class WgcSession {
4646
bool createD3DDevice();
4747
bool createCaptureItem(HMONITOR monitor);
4848
bool createCaptureItem(HWND window);
49+
bool createFramePoolAndSession();
50+
bool registerFrameArrived();
4951
bool applySessionOptions(bool captureCursor);
5052
void onFrameArrived(
5153
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender,
5254
winrt::Windows::Foundation::IInspectable const&);
55+
// The body of onFrameArrived, split out so the handler itself is nothing but
56+
// the try/catch that keeps a throwing projection from reaching the WinRT
57+
// delegate and taking the process down with it.
58+
void deliverFrame(
59+
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender);
5360

5461
Microsoft::WRL::ComPtr<ID3D11Device> d3dDevice_;
5562
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3dContext_;
@@ -61,6 +68,8 @@ class WgcSession {
6168
FrameCallback frameCallback_;
6269
std::mutex callbackMutex_;
6370
std::atomic<int> callbacksInFlight_ = 0;
71+
// One line per recording, not one per bad frame.
72+
std::atomic<bool> frameErrorLogged_ = false;
6473
bool quiesced_ = false;
6574
int width_ = 0;
6675
int height_ = 0;

0 commit comments

Comments
 (0)