bunite-core 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/host/core/App.ts +18 -4
- package/src/host/index.ts +1 -1
- package/src/host/native.ts +37 -9
- package/src/host/paths.ts +39 -20
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +294 -0
- package/src/native/win-webview2/spike/main.cpp +375 -0
- package/src/native/win-webview2/webview2_appres.cpp +210 -0
- package/src/native/win-webview2/webview2_internal.h +210 -0
- package/src/native/win-webview2/webview2_runtime.cpp +874 -0
- package/src/native/win-webview2/webview2_utils.cpp +246 -0
- package/src/rpc/renderer.ts +20 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// WebView2 cooperative-pump spike (Stage 0).
|
|
2
|
+
//
|
|
3
|
+
// Validates the hypothesis that a non-blocking message-pump loop driven from
|
|
4
|
+
// the main thread (mirroring mac/linux's `bunite_pump_once` model) can dispatch
|
|
5
|
+
// every WebView2 callback class bunite relies on.
|
|
6
|
+
//
|
|
7
|
+
// Measurement targets (printed to stderr as `SPIKE: …`):
|
|
8
|
+
// 1. Environment / Controller create-completion latency
|
|
9
|
+
// 2. NavigationCompleted latency
|
|
10
|
+
// 3. WebResourceRequested concurrent stress (N=100) — p95
|
|
11
|
+
// 4. WebMessageReceived hot-path throughput — drop rate
|
|
12
|
+
// 5. PermissionRequested Deferral completion latency
|
|
13
|
+
// 6. Sleep(1) vs MsgWaitForMultipleObjects(QS_ALLEVENTS) — wake cycle cost
|
|
14
|
+
//
|
|
15
|
+
// Exit code 0 = all green. Non-zero = at least one measurement failed a budget.
|
|
16
|
+
|
|
17
|
+
#include <windows.h>
|
|
18
|
+
#include <wrl.h>
|
|
19
|
+
#include "WebView2.h"
|
|
20
|
+
#include "WebView2EnvironmentOptions.h"
|
|
21
|
+
|
|
22
|
+
#include <cstdio>
|
|
23
|
+
#include <cstdint>
|
|
24
|
+
#include <chrono>
|
|
25
|
+
#include <string>
|
|
26
|
+
#include <vector>
|
|
27
|
+
#include <atomic>
|
|
28
|
+
#include <algorithm>
|
|
29
|
+
|
|
30
|
+
using Microsoft::WRL::Callback;
|
|
31
|
+
using Microsoft::WRL::ComPtr;
|
|
32
|
+
using Microsoft::WRL::Make;
|
|
33
|
+
|
|
34
|
+
namespace {
|
|
35
|
+
|
|
36
|
+
using SteadyClock = std::chrono::steady_clock;
|
|
37
|
+
|
|
38
|
+
constexpr wchar_t kClassName[] = L"BuniteWebView2Spike";
|
|
39
|
+
|
|
40
|
+
double ElapsedMs(SteadyClock::time_point from) {
|
|
41
|
+
return std::chrono::duration<double, std::milli>(SteadyClock::now() - from).count();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- HTML payload --------------------------------------------------------
|
|
45
|
+
// The page generates synthetic load for every callback class we measure.
|
|
46
|
+
constexpr const char* kSpikeHtml = R"(<!doctype html>
|
|
47
|
+
<html><head><meta charset="utf-8"><title>spike</title></head>
|
|
48
|
+
<body>
|
|
49
|
+
<script>
|
|
50
|
+
// 3. WebResourceRequested stress: 100 concurrent fetches of /probe/N.
|
|
51
|
+
window.__startResourceStress = (n) => {
|
|
52
|
+
const t0 = performance.now();
|
|
53
|
+
const promises = [];
|
|
54
|
+
for (let i = 0; i < n; i++) {
|
|
55
|
+
promises.push(fetch('/probe/' + i).then(r => r.text()));
|
|
56
|
+
}
|
|
57
|
+
return Promise.all(promises).then(() => performance.now() - t0);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 4. WebMessageReceived hot-path.
|
|
61
|
+
window.__startMessageStress = (n) => {
|
|
62
|
+
const t0 = performance.now();
|
|
63
|
+
for (let i = 0; i < n; i++) window.chrome.webview.postMessage('p:' + i);
|
|
64
|
+
return performance.now() - t0;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
window.chrome.webview.addEventListener('message', (ev) => {
|
|
68
|
+
if (ev.data === 'ping') window.chrome.webview.postMessage('pong');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 5. PermissionRequested: ask for clipboard read (any permission works).
|
|
72
|
+
window.__requestPermission = async () => {
|
|
73
|
+
try { await navigator.clipboard.readText(); } catch (e) {}
|
|
74
|
+
};
|
|
75
|
+
</script>
|
|
76
|
+
<p>WebView2 spike loaded</p>
|
|
77
|
+
</body></html>)";
|
|
78
|
+
|
|
79
|
+
// --- Spike state ---------------------------------------------------------
|
|
80
|
+
struct Spike {
|
|
81
|
+
HWND hwnd = nullptr;
|
|
82
|
+
ComPtr<ICoreWebView2Environment> env;
|
|
83
|
+
ComPtr<ICoreWebView2Controller> controller;
|
|
84
|
+
ComPtr<ICoreWebView2> webview;
|
|
85
|
+
|
|
86
|
+
std::atomic<bool> envReady{false};
|
|
87
|
+
std::atomic<bool> controllerReady{false};
|
|
88
|
+
std::atomic<bool> navigationCompleted{false};
|
|
89
|
+
|
|
90
|
+
SteadyClock::time_point t_env_request;
|
|
91
|
+
SteadyClock::time_point t_controller_request;
|
|
92
|
+
SteadyClock::time_point t_navigate_request;
|
|
93
|
+
|
|
94
|
+
double env_ms = 0;
|
|
95
|
+
double controller_ms = 0;
|
|
96
|
+
double navigation_ms = 0;
|
|
97
|
+
|
|
98
|
+
// Resource stress.
|
|
99
|
+
std::vector<double> resource_latencies;
|
|
100
|
+
std::atomic<int> messageCount{0};
|
|
101
|
+
std::atomic<int> permissionEvents{0};
|
|
102
|
+
|
|
103
|
+
bool stress_started = false;
|
|
104
|
+
bool resource_done = false;
|
|
105
|
+
bool message_done = false;
|
|
106
|
+
bool permission_done = false;
|
|
107
|
+
|
|
108
|
+
HRESULT lastError = S_OK;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
void Fail(Spike& s, const char* where, HRESULT hr) {
|
|
112
|
+
s.lastError = hr;
|
|
113
|
+
std::fprintf(stderr, "SPIKE: FAIL %s hr=0x%08x\n", where, static_cast<unsigned>(hr));
|
|
114
|
+
PostQuitMessage(static_cast<int>(hr));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Pump --------------------------------------------------------------
|
|
118
|
+
// Drains the message queue using the cooperative model the plan proposes.
|
|
119
|
+
// Returns false when WM_QUIT is dequeued.
|
|
120
|
+
bool PumpOnce() {
|
|
121
|
+
MSG msg;
|
|
122
|
+
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
|
123
|
+
if (msg.message == WM_QUIT) return false;
|
|
124
|
+
TranslateMessage(&msg);
|
|
125
|
+
DispatchMessageW(&msg);
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Async chain --------------------------------------------------------
|
|
131
|
+
void StartNavigate(Spike& s);
|
|
132
|
+
void StartStress(Spike& s);
|
|
133
|
+
|
|
134
|
+
HRESULT OnEnvironmentCreated(Spike& s, HRESULT hr, ICoreWebView2Environment* env) {
|
|
135
|
+
if (FAILED(hr) || !env) { Fail(s, "environment-create", hr); return hr; }
|
|
136
|
+
s.env_ms = ElapsedMs(s.t_env_request);
|
|
137
|
+
s.envReady = true;
|
|
138
|
+
s.env = env;
|
|
139
|
+
std::fprintf(stderr, "SPIKE: env_ready ms=%.2f\n", s.env_ms);
|
|
140
|
+
|
|
141
|
+
s.t_controller_request = SteadyClock::now();
|
|
142
|
+
hr = env->CreateCoreWebView2Controller(
|
|
143
|
+
s.hwnd,
|
|
144
|
+
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
|
|
145
|
+
[&s](HRESULT cr, ICoreWebView2Controller* ctl) -> HRESULT {
|
|
146
|
+
if (FAILED(cr) || !ctl) { Fail(s, "controller-create", cr); return cr; }
|
|
147
|
+
s.controller_ms = ElapsedMs(s.t_controller_request);
|
|
148
|
+
s.controller = ctl;
|
|
149
|
+
s.controllerReady = true;
|
|
150
|
+
std::fprintf(stderr, "SPIKE: controller_ready ms=%.2f\n", s.controller_ms);
|
|
151
|
+
|
|
152
|
+
RECT rc; GetClientRect(s.hwnd, &rc);
|
|
153
|
+
ctl->put_Bounds(rc);
|
|
154
|
+
|
|
155
|
+
HRESULT gh = ctl->get_CoreWebView2(&s.webview);
|
|
156
|
+
if (FAILED(gh) || !s.webview) { Fail(s, "get_CoreWebView2", gh); return gh; }
|
|
157
|
+
|
|
158
|
+
// 3. WebResourceRequested — add filter + handler for /probe/*.
|
|
159
|
+
s.webview->AddWebResourceRequestedFilter(
|
|
160
|
+
L"https://spike.invalid/*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL);
|
|
161
|
+
EventRegistrationToken tok;
|
|
162
|
+
s.webview->add_WebResourceRequested(
|
|
163
|
+
Callback<ICoreWebView2WebResourceRequestedEventHandler>(
|
|
164
|
+
[&s](ICoreWebView2*, ICoreWebView2WebResourceRequestedEventArgs* args) -> HRESULT {
|
|
165
|
+
ComPtr<ICoreWebView2WebResourceRequest> req;
|
|
166
|
+
args->get_Request(&req);
|
|
167
|
+
// Build a 1-byte response.
|
|
168
|
+
ComPtr<ICoreWebView2WebResourceResponse> resp;
|
|
169
|
+
s.env->CreateWebResourceResponse(
|
|
170
|
+
nullptr, 200, L"OK",
|
|
171
|
+
L"Content-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n",
|
|
172
|
+
&resp);
|
|
173
|
+
args->put_Response(resp.Get());
|
|
174
|
+
s.resource_latencies.push_back(ElapsedMs(s.t_env_request));
|
|
175
|
+
return S_OK;
|
|
176
|
+
}).Get(),
|
|
177
|
+
&tok);
|
|
178
|
+
|
|
179
|
+
// 4. WebMessageReceived counter.
|
|
180
|
+
s.webview->add_WebMessageReceived(
|
|
181
|
+
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
|
|
182
|
+
[&s](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs*) -> HRESULT {
|
|
183
|
+
s.messageCount.fetch_add(1, std::memory_order_relaxed);
|
|
184
|
+
return S_OK;
|
|
185
|
+
}).Get(),
|
|
186
|
+
&tok);
|
|
187
|
+
|
|
188
|
+
// 5. PermissionRequested counter — silent grant.
|
|
189
|
+
s.webview->add_PermissionRequested(
|
|
190
|
+
Callback<ICoreWebView2PermissionRequestedEventHandler>(
|
|
191
|
+
[&s](ICoreWebView2*, ICoreWebView2PermissionRequestedEventArgs* args) -> HRESULT {
|
|
192
|
+
args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW);
|
|
193
|
+
s.permissionEvents.fetch_add(1, std::memory_order_relaxed);
|
|
194
|
+
return S_OK;
|
|
195
|
+
}).Get(),
|
|
196
|
+
&tok);
|
|
197
|
+
|
|
198
|
+
// NavigationCompleted handler.
|
|
199
|
+
s.webview->add_NavigationCompleted(
|
|
200
|
+
Callback<ICoreWebView2NavigationCompletedEventHandler>(
|
|
201
|
+
[&s](ICoreWebView2*, ICoreWebView2NavigationCompletedEventArgs*) -> HRESULT {
|
|
202
|
+
s.navigation_ms = ElapsedMs(s.t_navigate_request);
|
|
203
|
+
s.navigationCompleted = true;
|
|
204
|
+
std::fprintf(stderr, "SPIKE: navigation_done ms=%.2f\n", s.navigation_ms);
|
|
205
|
+
StartStress(s);
|
|
206
|
+
return S_OK;
|
|
207
|
+
}).Get(),
|
|
208
|
+
&tok);
|
|
209
|
+
|
|
210
|
+
StartNavigate(s);
|
|
211
|
+
return S_OK;
|
|
212
|
+
}).Get());
|
|
213
|
+
return hr;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
void StartNavigate(Spike& s) {
|
|
217
|
+
// Embed HTML via NavigateToString. We mount fetch base via document.baseURI =
|
|
218
|
+
// any synthetic origin so WebResourceRequested fires for the stress URLs.
|
|
219
|
+
std::wstring page = L"<base href=\"https://spike.invalid/\">";
|
|
220
|
+
std::string utf8 = kSpikeHtml;
|
|
221
|
+
std::wstring html(utf8.begin(), utf8.end());
|
|
222
|
+
s.t_navigate_request = SteadyClock::now();
|
|
223
|
+
HRESULT hr = s.webview->NavigateToString((page + html).c_str());
|
|
224
|
+
if (FAILED(hr)) Fail(s, "NavigateToString", hr);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
void StartStress(Spike& s) {
|
|
228
|
+
if (s.stress_started) return;
|
|
229
|
+
s.stress_started = true;
|
|
230
|
+
|
|
231
|
+
// 3. Resource stress.
|
|
232
|
+
s.webview->ExecuteScript(
|
|
233
|
+
L"window.__startResourceStress(100).then(ms => window.chrome.webview.postMessage('rs:' + ms))",
|
|
234
|
+
Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
|
|
235
|
+
[](HRESULT, LPCWSTR) -> HRESULT { return S_OK; }).Get());
|
|
236
|
+
|
|
237
|
+
// 4. Message stress.
|
|
238
|
+
s.webview->ExecuteScript(
|
|
239
|
+
L"window.__startMessageStress(500)",
|
|
240
|
+
Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
|
|
241
|
+
[](HRESULT, LPCWSTR) -> HRESULT { return S_OK; }).Get());
|
|
242
|
+
|
|
243
|
+
// 5. Permission request.
|
|
244
|
+
s.webview->ExecuteScript(
|
|
245
|
+
L"window.__requestPermission()",
|
|
246
|
+
Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
|
|
247
|
+
[](HRESULT, LPCWSTR) -> HRESULT { return S_OK; }).Get());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
251
|
+
if (msg == WM_DESTROY) { PostQuitMessage(0); return 0; }
|
|
252
|
+
return DefWindowProcW(hwnd, msg, wp, lp);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
double Percentile(std::vector<double> v, double p) {
|
|
256
|
+
if (v.empty()) return 0;
|
|
257
|
+
std::sort(v.begin(), v.end());
|
|
258
|
+
size_t idx = std::min<size_t>(v.size() - 1, static_cast<size_t>(v.size() * p));
|
|
259
|
+
return v[idx];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
} // namespace
|
|
263
|
+
|
|
264
|
+
int main() {
|
|
265
|
+
HRESULT co_hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
|
266
|
+
if (FAILED(co_hr)) {
|
|
267
|
+
std::fprintf(stderr, "SPIKE: CoInitializeEx failed hr=0x%08x\n", static_cast<unsigned>(co_hr));
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
Spike spike;
|
|
272
|
+
|
|
273
|
+
WNDCLASSEXW wc{};
|
|
274
|
+
wc.cbSize = sizeof(wc);
|
|
275
|
+
wc.lpfnWndProc = WndProc;
|
|
276
|
+
wc.hInstance = GetModuleHandleW(nullptr);
|
|
277
|
+
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
|
278
|
+
wc.lpszClassName = kClassName;
|
|
279
|
+
RegisterClassExW(&wc);
|
|
280
|
+
|
|
281
|
+
spike.hwnd = CreateWindowExW(
|
|
282
|
+
0, kClassName, L"webview2-spike", WS_OVERLAPPEDWINDOW,
|
|
283
|
+
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, nullptr, nullptr, wc.hInstance, nullptr);
|
|
284
|
+
ShowWindow(spike.hwnd, SW_HIDE); // headless
|
|
285
|
+
|
|
286
|
+
// Environment.
|
|
287
|
+
spike.t_env_request = SteadyClock::now();
|
|
288
|
+
HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(
|
|
289
|
+
nullptr, nullptr, nullptr,
|
|
290
|
+
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
|
|
291
|
+
[&spike](HRESULT cr, ICoreWebView2Environment* env) -> HRESULT {
|
|
292
|
+
return OnEnvironmentCreated(spike, cr, env);
|
|
293
|
+
}).Get());
|
|
294
|
+
if (FAILED(hr)) {
|
|
295
|
+
std::fprintf(stderr, "SPIKE: FAIL bootstrap hr=0x%08x\n", static_cast<unsigned>(hr));
|
|
296
|
+
CoUninitialize();
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Cooperative pump loop. Budget: 10 s total.
|
|
301
|
+
// Arm A: Sleep(1) — busy-ish wake at ~1kHz.
|
|
302
|
+
// Arm B (after Arm A drains): MsgWaitForMultipleObjects(QS_ALLEVENTS, INFINITE) — event-driven wake.
|
|
303
|
+
auto t_pump_start = SteadyClock::now();
|
|
304
|
+
size_t loop_iters = 0;
|
|
305
|
+
size_t arm_b_iters = 0;
|
|
306
|
+
bool arm_b = false;
|
|
307
|
+
auto t_arm_b_start = SteadyClock::now();
|
|
308
|
+
while (true) {
|
|
309
|
+
if (!PumpOnce()) break;
|
|
310
|
+
loop_iters++;
|
|
311
|
+
if (arm_b) arm_b_iters++;
|
|
312
|
+
|
|
313
|
+
bool done = spike.navigationCompleted && spike.messageCount >= 500 &&
|
|
314
|
+
spike.resource_latencies.size() >= 100;
|
|
315
|
+
|
|
316
|
+
if (done && !arm_b) {
|
|
317
|
+
// Switch to Arm B for an additional 2 s idle measurement, posting a heartbeat
|
|
318
|
+
// every 250 ms via SetTimer so we can count wake-ups without busy looping.
|
|
319
|
+
arm_b = true;
|
|
320
|
+
arm_b_iters = 0;
|
|
321
|
+
t_arm_b_start = SteadyClock::now();
|
|
322
|
+
SetTimer(spike.hwnd, /*id*/1, 250, nullptr);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (arm_b && ElapsedMs(t_arm_b_start) > 2000.0) {
|
|
326
|
+
KillTimer(spike.hwnd, 1);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
if (!arm_b && ElapsedMs(t_pump_start) > 10000.0) {
|
|
330
|
+
std::fprintf(stderr, "SPIKE: TIMEOUT after %.0f ms\n", ElapsedMs(t_pump_start));
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (arm_b) {
|
|
335
|
+
MsgWaitForMultipleObjects(0, nullptr, FALSE, INFINITE, QS_ALLEVENTS);
|
|
336
|
+
} else {
|
|
337
|
+
Sleep(1);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Report.
|
|
342
|
+
std::fprintf(stderr, "SPIKE: ---- results ----\n");
|
|
343
|
+
std::fprintf(stderr, "SPIKE: env_ms=%.2f controller_ms=%.2f navigation_ms=%.2f\n",
|
|
344
|
+
spike.env_ms, spike.controller_ms, spike.navigation_ms);
|
|
345
|
+
std::fprintf(stderr, "SPIKE: armA(Sleep1) pump_iters=%zu elapsed=%.0fms\n",
|
|
346
|
+
loop_iters - arm_b_iters, ElapsedMs(t_pump_start) - 2000.0);
|
|
347
|
+
std::fprintf(stderr, "SPIKE: armB(MsgWait) idle_iters=%zu / 2000ms (lower=better)\n",
|
|
348
|
+
arm_b_iters);
|
|
349
|
+
std::fprintf(stderr, "SPIKE: resource_count=%zu p50=%.2f p95=%.2f p99=%.2f\n",
|
|
350
|
+
spike.resource_latencies.size(),
|
|
351
|
+
Percentile(spike.resource_latencies, 0.5),
|
|
352
|
+
Percentile(spike.resource_latencies, 0.95),
|
|
353
|
+
Percentile(spike.resource_latencies, 0.99));
|
|
354
|
+
std::fprintf(stderr, "SPIKE: messages=%d / 500 expected\n", spike.messageCount.load());
|
|
355
|
+
std::fprintf(stderr, "SPIKE: permission_events=%d\n", spike.permissionEvents.load());
|
|
356
|
+
|
|
357
|
+
bool pass = spike.navigationCompleted &&
|
|
358
|
+
spike.resource_latencies.size() == 100 &&
|
|
359
|
+
spike.messageCount >= 500 &&
|
|
360
|
+
spike.lastError == S_OK;
|
|
361
|
+
|
|
362
|
+
std::fprintf(stderr, "SPIKE: verdict=%s\n", pass ? "PASS" : "FAIL");
|
|
363
|
+
|
|
364
|
+
if (spike.controller) spike.controller->Close();
|
|
365
|
+
DestroyWindow(spike.hwnd);
|
|
366
|
+
// Drain so Close() completion handlers run.
|
|
367
|
+
auto t_drain = SteadyClock::now();
|
|
368
|
+
while (ElapsedMs(t_drain) < 500.0) { if (!PumpOnce()) break; Sleep(1); }
|
|
369
|
+
|
|
370
|
+
spike.controller.Reset();
|
|
371
|
+
spike.webview.Reset();
|
|
372
|
+
spike.env.Reset();
|
|
373
|
+
CoUninitialize();
|
|
374
|
+
return pass ? 0 : 2;
|
|
375
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#include "webview2_internal.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <shlwapi.h>
|
|
5
|
+
#pragma comment(lib, "shlwapi.lib")
|
|
6
|
+
|
|
7
|
+
using Microsoft::WRL::Callback;
|
|
8
|
+
using Microsoft::WRL::ComPtr;
|
|
9
|
+
using Microsoft::WRL::Make;
|
|
10
|
+
|
|
11
|
+
namespace bunite_webview2 {
|
|
12
|
+
|
|
13
|
+
void configureSchemes(ICoreWebView2EnvironmentOptions* base_opts) {
|
|
14
|
+
ComPtr<ICoreWebView2EnvironmentOptions4> opts4;
|
|
15
|
+
if (!base_opts) return;
|
|
16
|
+
if (FAILED(base_opts->QueryInterface(IID_PPV_ARGS(&opts4))) || !opts4) {
|
|
17
|
+
BUNITE_WARN("webview2: ICoreWebView2EnvironmentOptions4 unavailable — appres scheme will fall back to WebResourceRequested-only");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
auto reg = Make<CoreWebView2CustomSchemeRegistration>(L"appres");
|
|
22
|
+
reg->put_TreatAsSecure(TRUE);
|
|
23
|
+
reg->put_HasAuthorityComponent(TRUE);
|
|
24
|
+
// Allow injected preload + appres origin to issue cross-origin fetches.
|
|
25
|
+
const WCHAR* allowed[] = { L"*" };
|
|
26
|
+
reg->SetAllowedOrigins(1, allowed);
|
|
27
|
+
|
|
28
|
+
ICoreWebView2CustomSchemeRegistration* regs[] = { reg.Get() };
|
|
29
|
+
if (FAILED(opts4->SetCustomSchemeRegistrations(1, regs))) {
|
|
30
|
+
BUNITE_WARN("webview2: SetCustomSchemeRegistrations failed");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Builds a WebResourceResponse from in-memory bytes.
|
|
35
|
+
static ComPtr<ICoreWebView2WebResourceResponse> makeResponse(
|
|
36
|
+
const std::string& body, const std::string& mime, int status, const std::wstring& reason) {
|
|
37
|
+
ComPtr<ICoreWebView2WebResourceResponse> resp;
|
|
38
|
+
if (!g_runtime.env) return resp;
|
|
39
|
+
|
|
40
|
+
ComPtr<IStream> stream;
|
|
41
|
+
if (!body.empty()) {
|
|
42
|
+
stream.Attach(SHCreateMemStream(reinterpret_cast<const BYTE*>(body.data()),
|
|
43
|
+
static_cast<UINT>(body.size())));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Restrict to the bunite scheme; the preload-injected runtime expects same-origin
|
|
47
|
+
// semantics here, and CEF's scheme handler is same-origin by default. Wider
|
|
48
|
+
// CORS surface is opt-in via the (future) scheme registration policy.
|
|
49
|
+
std::wstring headers = L"Content-Type: " + utf8ToWide(mime) + L"\r\n";
|
|
50
|
+
headers += L"Access-Control-Allow-Origin: appres://app.internal\r\n";
|
|
51
|
+
headers += L"Cache-Control: no-store\r\n";
|
|
52
|
+
|
|
53
|
+
g_runtime.env->CreateWebResourceResponse(stream.Get(), status, reason.c_str(),
|
|
54
|
+
headers.c_str(), &resp);
|
|
55
|
+
return resp;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static std::string fileSlurp(const std::filesystem::path& p) {
|
|
59
|
+
std::ifstream f(p, std::ios::binary);
|
|
60
|
+
if (!f) return {};
|
|
61
|
+
std::string out((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Returns true if a static file under the view's appres_root could be served.
|
|
66
|
+
static bool tryServeStatic(ViewHost* view, const std::string& path,
|
|
67
|
+
ICoreWebView2WebResourceRequestedEventArgs* args) {
|
|
68
|
+
if (!view || view->appres_root.empty()) return false;
|
|
69
|
+
std::error_code ec;
|
|
70
|
+
std::filesystem::path root = std::filesystem::weakly_canonical(
|
|
71
|
+
std::filesystem::path(utf8ToWide(view->appres_root)), ec);
|
|
72
|
+
if (ec) return false;
|
|
73
|
+
|
|
74
|
+
std::string rel = path;
|
|
75
|
+
if (!rel.empty() && rel[0] == '/') rel.erase(0, 1);
|
|
76
|
+
std::filesystem::path full = root;
|
|
77
|
+
if (!rel.empty()) full /= utf8ToWide(rel);
|
|
78
|
+
if (!full.has_extension()) {
|
|
79
|
+
auto with_html = full;
|
|
80
|
+
with_html.replace_extension(".html");
|
|
81
|
+
if (std::filesystem::exists(with_html, ec)) full = with_html;
|
|
82
|
+
}
|
|
83
|
+
full = std::filesystem::weakly_canonical(full, ec);
|
|
84
|
+
if (ec) return false;
|
|
85
|
+
|
|
86
|
+
// Containment check — resolved path must live under appres_root.
|
|
87
|
+
auto rel_check = std::filesystem::relative(full, root, ec);
|
|
88
|
+
if (ec || rel_check.empty() || rel_check.native()[0] == L'.') return false;
|
|
89
|
+
|
|
90
|
+
if (!std::filesystem::is_regular_file(full, ec)) return false;
|
|
91
|
+
std::string body = fileSlurp(full);
|
|
92
|
+
std::string mime = getMimeType(full);
|
|
93
|
+
auto resp = makeResponse(body, mime, 200, L"OK");
|
|
94
|
+
if (!resp) return false;
|
|
95
|
+
args->put_Response(resp.Get());
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
void attachAppResFilter(ViewHost* view) {
|
|
100
|
+
if (!view || !view->webview) return;
|
|
101
|
+
auto lifetime = g_runtime.lifetime;
|
|
102
|
+
|
|
103
|
+
view->webview->AddWebResourceRequestedFilter(L"appres://*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL);
|
|
104
|
+
|
|
105
|
+
EventRegistrationToken tok;
|
|
106
|
+
view->webview->add_WebResourceRequested(
|
|
107
|
+
Callback<ICoreWebView2WebResourceRequestedEventHandler>(
|
|
108
|
+
[lifetime, view_id = view->id](ICoreWebView2*, ICoreWebView2WebResourceRequestedEventArgs* args) -> HRESULT {
|
|
109
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
110
|
+
ViewHost* v = getView(view_id);
|
|
111
|
+
if (!v) return S_OK;
|
|
112
|
+
|
|
113
|
+
ComPtr<ICoreWebView2WebResourceRequest> req;
|
|
114
|
+
args->get_Request(&req);
|
|
115
|
+
|
|
116
|
+
LPWSTR uri_raw = nullptr;
|
|
117
|
+
req->get_Uri(&uri_raw);
|
|
118
|
+
std::string url = wideToUtf8(uri_raw);
|
|
119
|
+
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
120
|
+
|
|
121
|
+
std::string path = normalizeAppResPath(url);
|
|
122
|
+
if (path.empty()) return S_OK;
|
|
123
|
+
|
|
124
|
+
if (tryServeStatic(v, path, args)) return S_OK;
|
|
125
|
+
|
|
126
|
+
bool route_match = false;
|
|
127
|
+
{
|
|
128
|
+
std::lock_guard<std::mutex> g(g_runtime.route_mutex);
|
|
129
|
+
for (auto& p : g_runtime.registered_routes) {
|
|
130
|
+
if (globMatchCaseInsensitive(p, path)) { route_match = true; break; }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!route_match) {
|
|
134
|
+
auto resp = makeResponse("", "text/plain", 404, L"Not Found");
|
|
135
|
+
if (resp) args->put_Response(resp.Get());
|
|
136
|
+
return S_OK;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ComPtr<ICoreWebView2Deferral> deferral;
|
|
140
|
+
args->GetDeferral(&deferral);
|
|
141
|
+
|
|
142
|
+
uint32_t req_id;
|
|
143
|
+
{
|
|
144
|
+
std::lock_guard<std::mutex> g(g_runtime.route_mutex);
|
|
145
|
+
req_id = g_runtime.next_route_request_id++;
|
|
146
|
+
PendingRouteRequest p;
|
|
147
|
+
p.view_id = v->id;
|
|
148
|
+
p.uri = utf8ToWide(url);
|
|
149
|
+
p.path = path;
|
|
150
|
+
p.args = args;
|
|
151
|
+
p.deferral = deferral;
|
|
152
|
+
g_runtime.pending_routes[req_id] = std::move(p);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
std::string payload = "{\"requestId\":" + std::to_string(req_id) +
|
|
156
|
+
",\"path\":\"" + escapeJsonString(path) +
|
|
157
|
+
"\",\"url\":\"" + escapeJsonString(url) + "\"}";
|
|
158
|
+
emitWebviewEvent(v->id, "route-request", payload);
|
|
159
|
+
return S_OK;
|
|
160
|
+
}).Get(),
|
|
161
|
+
&tok);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
void registerAppResRoute(const char* path) {
|
|
165
|
+
if (!path) return;
|
|
166
|
+
std::lock_guard<std::mutex> g(g_runtime.route_mutex);
|
|
167
|
+
g_runtime.registered_routes.emplace_back(path);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
void unregisterAppResRoute(const char* path) {
|
|
171
|
+
if (!path) return;
|
|
172
|
+
std::lock_guard<std::mutex> g(g_runtime.route_mutex);
|
|
173
|
+
auto& v = g_runtime.registered_routes;
|
|
174
|
+
v.erase(std::remove(v.begin(), v.end(), std::string(path)), v.end());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
void completeRouteRequest(uint32_t request_id, const char* html) {
|
|
178
|
+
PendingRouteRequest p;
|
|
179
|
+
{
|
|
180
|
+
std::lock_guard<std::mutex> g(g_runtime.route_mutex);
|
|
181
|
+
auto it = g_runtime.pending_routes.find(request_id);
|
|
182
|
+
if (it == g_runtime.pending_routes.end()) return;
|
|
183
|
+
p = std::move(it->second);
|
|
184
|
+
g_runtime.pending_routes.erase(it);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
postUiTask([p = std::move(p), body = std::string(html ? html : "")]() mutable {
|
|
188
|
+
if (!p.args || !p.deferral) return;
|
|
189
|
+
auto resp = makeResponse(body, "text/html; charset=utf-8", 200, L"OK");
|
|
190
|
+
if (resp) p.args->put_Response(resp.Get());
|
|
191
|
+
p.deferral->Complete();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void cancelAllRouteRequests() {
|
|
196
|
+
std::map<uint32_t, PendingRouteRequest> drained;
|
|
197
|
+
{
|
|
198
|
+
std::lock_guard<std::mutex> g(g_runtime.route_mutex);
|
|
199
|
+
drained.swap(g_runtime.pending_routes);
|
|
200
|
+
}
|
|
201
|
+
for (auto& [_, p] : drained) {
|
|
202
|
+
if (p.args) {
|
|
203
|
+
auto resp = makeResponse("", "text/plain", 503, L"Shutting Down");
|
|
204
|
+
if (resp) p.args->put_Response(resp.Get());
|
|
205
|
+
}
|
|
206
|
+
if (p.deferral) p.deferral->Complete();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
} // namespace bunite_webview2
|