-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathWebViewBridgeHost.cpp
More file actions
297 lines (259 loc) · 13.4 KB
/
Copy pathWebViewBridgeHost.cpp
File metadata and controls
297 lines (259 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#include "WebViewBridgeHost.h"
#include <wrl.h>
#include <wil/com.h>
#include <WebView2.h>
#include "HostProtocolBridge.h"
#include "ProtocolTypes.h"
using namespace Microsoft::WRL;
// Stores the host window, startup policy, callback registry, and log sink for one WebView instance.
WebViewBridgeHost::WebViewBridgeHost(
HWND hostHwnd,
WebViewBridgeHostConfig config,
ClientCallbackRegistry callbacks,
LogFn logFn)
: m_hostHwnd(hostHwnd),
m_config(std::move(config)),
m_callbacks(std::move(callbacks)),
m_logFn(std::move(logFn)) {
}
// Creates the WebView2 environment/controller pair and starts the initial navigation loop.
HRESULT WebViewBridgeHost::Initialize() {
// The environment owns the browser process/profile root; the controller binds it to our host window.
const HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(
nullptr,
m_config.userDataFolder.c_str(),
nullptr,
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[this](HRESULT res, ICoreWebView2Environment* env) -> HRESULT {
if (FAILED(res)) {
FailInitialization(res, L"Native Error: Failed to create WebView environment.");
return res;
}
const auto createControllerHandler =
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[this](HRESULT createRes, ICoreWebView2Controller* ctrl) -> HRESULT {
if (FAILED(createRes)) {
FailInitialization(createRes, L"Native Error: Failed to create WebView controller.");
return createRes;
}
m_controller = ctrl;
// A hidden controller still works for script/message bridging, but it is backgrounded.
BOOL isVisible = FALSE;
const HRESULT visibleHr = m_controller->get_IsVisible(&isVisible);
if (SUCCEEDED(visibleHr)) {
Log(WebViewBridgeLogVerbosity::Info,
L"Native: WebView controller IsVisible=" + std::to_wstring(isVisible ? 1 : 0));
}
else {
Log(WebViewBridgeLogVerbosity::Info,
L"Native Error: Failed to query WebView controller IsVisible.");
}
// Without ICoreWebView2, the controller exists but the page/message bridge cannot run.
HRESULT coreWebViewHr = m_controller->get_CoreWebView2(&m_webview);
if (FAILED(coreWebViewHr) || !m_webview) {
FailInitialization(coreWebViewHr, L"Native Error: Failed to get CoreWebView2 instance.");
return FAILED(coreWebViewHr) ? coreWebViewHr : E_POINTER;
}
// Install bridge handlers before navigating so page_ready and early failures are observed.
HRESULT handlersHr = SetupWebViewHandlers();
if (FAILED(handlersHr)) {
FailInitialization(handlersHr, L"Native Error: Failed to install WebView handlers.");
return handlersHr;
}
// The reconnect timer is part of startup policy, not optional background work.
if (SetTimer(m_hostHwnd, m_config.connectTimerId, m_config.connectRetryMs, nullptr) == 0) {
const HRESULT timerHr = HRESULT_FROM_WIN32(GetLastError());
FailInitialization(timerHr, L"Native Error: Failed to start reconnect timer.");
return timerHr;
}
EnsurePageNavigation();
return S_OK;
});
if (m_config.profileName.empty()) {
return env->CreateCoreWebView2Controller(
m_hostHwnd,
createControllerHandler.Get());
}
// Browser-hosted profiles use the browser user-data root plus a WebView2 profile name.
wil::com_ptr<ICoreWebView2Environment10> env10;
if (FAILED(env->QueryInterface(IID_PPV_ARGS(&env10)))) {
FailInitialization(E_NOINTERFACE, L"Native Error: WebView environment does not support named profiles.");
return E_NOINTERFACE;
}
wil::com_ptr<ICoreWebView2ControllerOptions> controllerOptions;
HRESULT optionsHr = env10->CreateCoreWebView2ControllerOptions(&controllerOptions);
if (FAILED(optionsHr)) {
FailInitialization(optionsHr, L"Native Error: Failed to create WebView controller options.");
return optionsHr;
}
optionsHr = controllerOptions->put_ProfileName(m_config.profileName.c_str());
if (FAILED(optionsHr)) {
FailInitialization(optionsHr, L"Native Error: Failed to set WebView profile name.");
return optionsHr;
}
return env10->CreateCoreWebView2ControllerWithOptions(
m_hostHwnd,
controllerOptions.get(),
createControllerHandler.Get());
}).Get());
if (FAILED(hr)) {
// This is the synchronous kickoff failure before WebView2 can invoke async completion callbacks.
Log(WebViewBridgeLogVerbosity::Info, L"Native Error: Failed to begin WebView environment creation.");
}
return hr;
}
// Handles the reconnect and poll timers owned by this bridge host.
bool WebViewBridgeHost::HandleTimer(UINT_PTR timerId) {
if (timerId == m_config.connectTimerId) {
EnsurePageNavigation();
return true;
}
if (timerId == m_config.pollTimerId) {
TriggerPollFromNative();
return true;
}
return false;
}
// Evaluates whether a message should be emitted under the configured bridge verbosity.
bool WebViewBridgeHost::ShouldLog(WebViewBridgeLogVerbosity level) const {
switch (m_config.logVerbosity) {
case WebViewBridgeLogVerbosity::Silent:
return false;
case WebViewBridgeLogVerbosity::Info:
return level == WebViewBridgeLogVerbosity::Info;
case WebViewBridgeLogVerbosity::Debug:
return true;
default:
return false;
}
}
// Forwards one bridge log message to the client-provided sink when enabled.
void WebViewBridgeHost::Log(WebViewBridgeLogVerbosity level, const std::wstring& message) const {
if (m_logFn && ShouldLog(level)) {
m_logFn(message);
}
}
void WebViewBridgeHost::FailInitialization(HRESULT hr, const std::wstring& message) const {
Log(WebViewBridgeLogVerbosity::Info,
message + L" HRESULT=" + std::to_wstring(static_cast<unsigned long>(hr)));
if (m_hostHwnd && IsWindow(m_hostHwnd)) {
// Async bootstrap failures happen after WebLink enters its message loop, so closing the host window is the
// simplest way to surface a fatal bridge startup failure back to the owning app.
PostMessageW(m_hostHwnd, WM_CLOSE, 0, 0);
}
}
// Sends a typed native answer back into the page over the WebView2 message channel.
void WebViewBridgeHost::PostAnswerToPage(const std::wstring& questionId, const std::wstring& answerText) {
if (!m_webview) return;
const std::wstring json = SerializeAnswerMessageJson(AnswerMessage{ questionId, answerText });
const HRESULT hr = m_webview->PostWebMessageAsJson(json.c_str());
if (SUCCEEDED(hr)) {
Log(WebViewBridgeLogVerbosity::Info, L"Native: Posted answer for " + questionId + L": " + answerText);
}
else {
Log(WebViewBridgeLogVerbosity::Info, L"Native Error: Failed to post answer for " + questionId);
}
}
// Executes the configured page polling function inside the currently loaded document.
void WebViewBridgeHost::TriggerPollFromNative() {
if (!m_webview || !m_pageReady || m_pollInFlight) return;
m_pollInFlight = true;
Log(WebViewBridgeLogVerbosity::Debug, L"Native: Triggering poll script.");
// Native owns cadence; the page only supplies the async function we execute inside the document.
m_webview->ExecuteScript(
m_config.pollScript.c_str(),
Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
[this](HRESULT hr, LPCWSTR) -> HRESULT {
m_pollInFlight = false;
if (SUCCEEDED(hr)) {
Log(WebViewBridgeLogVerbosity::Debug, L"Native: Poll script executed successfully.");
}
else {
Log(WebViewBridgeLogVerbosity::Info, L"Native Error: Poll script execution failed.");
}
return S_OK;
}).Get());
}
// Keeps retrying navigation until the page signals that its bridge is ready.
void WebViewBridgeHost::EnsurePageNavigation() {
if (!m_webview || m_pageReady) return;
Log(WebViewBridgeLogVerbosity::Info, L"Native: Navigating WebView to " + m_config.startUrl);
const HRESULT hr = m_webview->Navigate(m_config.startUrl.c_str());
if (FAILED(hr)) {
// If initial navigation cannot start, the page can never report page_ready.
FailInitialization(hr, L"Native Error: Navigate failed.");
}
}
// Wires the navigation and message callbacks that connect WebView2 to the app protocol.
HRESULT WebViewBridgeHost::SetupWebViewHandlers() {
HRESULT hr = m_webview->add_NavigationStarting(
Callback<ICoreWebView2NavigationStartingEventHandler>(
[this](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
wil::unique_cotaskmem_string uri;
if (SUCCEEDED(args->get_Uri(&uri)) && uri) {
Log(WebViewBridgeLogVerbosity::Debug, L"Native: Navigation starting: " + std::wstring(uri.get()));
}
else {
Log(WebViewBridgeLogVerbosity::Debug, L"Native: Navigation starting.");
}
return S_OK;
}).Get(),
nullptr);
if (FAILED(hr)) {
return hr;
}
hr = m_webview->add_NavigationCompleted(
Callback<ICoreWebView2NavigationCompletedEventHandler>(
[this](ICoreWebView2*, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT {
BOOL isSuccess = FALSE;
COREWEBVIEW2_WEB_ERROR_STATUS errorStatus = COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN;
args->get_IsSuccess(&isSuccess);
args->get_WebErrorStatus(&errorStatus);
if (isSuccess) {
Log(WebViewBridgeLogVerbosity::Info, L"Native: Navigation completed successfully.");
}
else {
Log(WebViewBridgeLogVerbosity::Info, L"Native Error: Navigation failed with WebErrorStatus=" + std::to_wstring(static_cast<int>(errorStatus)));
}
return S_OK;
}).Get(),
nullptr);
if (FAILED(hr)) {
return hr;
}
hr = m_webview->add_WebMessageReceived(
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
[this](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT {
wil::unique_cotaskmem_string json;
args->get_WebMessageAsJson(&json);
// Page JS talks back through window.chrome.webview.postMessage(...); this is the native entrypoint.
const auto dispatch = DispatchHostInboundMessageJson(json.get(), m_callbacks);
if (!dispatch.has_value()) {
Log(WebViewBridgeLogVerbosity::Info, L"Native Error: Invalid host message payload.");
return S_OK;
}
if (dispatch->isPageReady) {
Log(WebViewBridgeLogVerbosity::Info, L"App: WebView reported PageReady.");
m_pageReady = true;
// Once the page declares its bridge ready, switch from connect retries to the poll heartbeat.
KillTimer(m_hostHwnd, m_config.connectTimerId);
// The poll timer drives ExecuteScript(window.hostBridge.pollOnce()) for steady-state operation.
if (SetTimer(m_hostHwnd, m_config.pollTimerId, m_config.pollMs, nullptr) == 0) {
const HRESULT timerHr = HRESULT_FROM_WIN32(GetLastError());
FailInitialization(timerHr, L"Native Error: Failed to start poll timer.");
return timerHr;
}
}
if (dispatch->answer.has_value()) {
// Callback dispatch resolves the server question id to native work, then posts an answer back to JS.
Log(WebViewBridgeLogVerbosity::Info, L"Native: Resolved callback answer for " + dispatch->answer->id + L": " + dispatch->answer->text);
PostAnswerToPage(dispatch->answer->id, dispatch->answer->text);
}
return S_OK;
}).Get(),
nullptr);
if (FAILED(hr)) {
return hr;
}
return S_OK;
}