bunite-core 0.12.0 → 0.14.0
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 +4 -4
- package/src/host/core/App.ts +17 -1
- package/src/host/core/BrowserView.ts +197 -28
- package/src/host/core/SurfaceBrowserIPC.ts +44 -3
- package/src/host/core/SurfaceManager.ts +260 -28
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +8 -1
- package/src/host/native.ts +124 -1
- package/src/native/linux/bunite_linux_ffi.cpp +223 -6
- package/src/native/linux/bunite_linux_internal.h +6 -0
- package/src/native/linux/bunite_linux_runtime.cpp +1 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +85 -0
- package/src/native/mac/bunite_mac_ffi.mm +356 -8
- package/src/native/mac/bunite_mac_internal.h +6 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +144 -2
- package/src/native/shared/ffi_exports.h +135 -0
- package/src/native/win/native_host_cef.cpp +86 -3
- package/src/native/win/native_host_ffi.cpp +378 -1
- package/src/native/win/native_host_internal.h +13 -0
- package/src/native/win/native_host_utils.cpp +2 -1
- package/src/native/win/process_helper_win.cpp +54 -27
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +303 -9
- package/src/native/win-webview2/webview2_internal.h +11 -0
- package/src/native/win-webview2/webview2_runtime.cpp +128 -12
- package/src/native/win-webview2/webview2_utils.cpp +30 -12
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +97 -0
- package/src/rpc/framework.ts +173 -4
- package/src/rpc/index.ts +21 -0
- package/src/webview/native.ts +126 -25
- package/src/webview/polyfill.ts +196 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "webview2_internal.h"
|
|
2
2
|
|
|
3
3
|
#include <cstring>
|
|
4
|
+
#include <wincrypt.h> // CryptBinaryToStringA — base64 encoding for screenshot payload.
|
|
4
5
|
|
|
5
6
|
using namespace bunite_webview2;
|
|
6
7
|
|
|
@@ -11,7 +12,7 @@ void setViewInputPassthrough(ViewHost* v, bool passthrough);
|
|
|
11
12
|
|
|
12
13
|
extern "C" {
|
|
13
14
|
|
|
14
|
-
BUNITE_EXPORT int32_t bunite_abi_version(void) { return
|
|
15
|
+
BUNITE_EXPORT int32_t bunite_abi_version(void) { return 9; }
|
|
15
16
|
|
|
16
17
|
BUNITE_EXPORT void bunite_set_log_level(int32_t level) {
|
|
17
18
|
if (level < 0) level = 0;
|
|
@@ -164,9 +165,21 @@ BUNITE_EXPORT void bunite_view_evaluate(uint32_t view_id, uint32_t request_id, c
|
|
|
164
165
|
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
165
166
|
return;
|
|
166
167
|
}
|
|
168
|
+
// Wrap user script in a JS try/catch envelope: WebView2 ExecuteScript
|
|
169
|
+
// returns "null" when the script throws (HRESULT success), so without this
|
|
170
|
+
// wrapper a thrown error is indistinguishable from a literal `null`. The
|
|
171
|
+
// wrapper surfaces throws as `{__bunite_err: <message>}` for CEF-parity.
|
|
172
|
+
std::string wrapped =
|
|
173
|
+
"(function(){try{return JSON.stringify({__bunite_ok:true,value:("
|
|
174
|
+
+ std::string(script) +
|
|
175
|
+
")})}catch(e){var c=(e&&e.name===\"SecurityError\")?\"cross_origin\":\"runtime_error\";"
|
|
176
|
+
"return JSON.stringify({__bunite_ok:false,code:c,"
|
|
177
|
+
"message:(e&&e.message)?e.message:String(e),"
|
|
178
|
+
"name:(e&&e.name)||\"\"})}})()";
|
|
179
|
+
|
|
167
180
|
auto lifetime = g_runtime.lifetime;
|
|
168
181
|
v->webview->ExecuteScript(
|
|
169
|
-
utf8ToWide(
|
|
182
|
+
utf8ToWide(wrapped).c_str(),
|
|
170
183
|
Microsoft::WRL::Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
|
|
171
184
|
[lifetime, view_id, request_id](HRESULT hr, LPCWSTR raw) -> HRESULT {
|
|
172
185
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
@@ -178,13 +191,79 @@ BUNITE_EXPORT void bunite_view_evaluate(uint32_t view_id, uint32_t request_id, c
|
|
|
178
191
|
[hr]() { char b[16]; snprintf(b, sizeof(b), "%08x", static_cast<unsigned>(hr)); return std::string(b); }() +
|
|
179
192
|
"\"}";
|
|
180
193
|
} else {
|
|
181
|
-
//
|
|
182
|
-
// the
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
std::string
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
// `raw` is a JSON-encoded string. After WebView2's outer
|
|
195
|
+
// JSON.stringify, the wrapper's return value (itself a JSON
|
|
196
|
+
// string) arrives as a JSON-quoted JSON string — parse the outer
|
|
197
|
+
// quotes via simple unescape into the inner JSON envelope.
|
|
198
|
+
std::string outer = wideToUtf8(raw);
|
|
199
|
+
// outer looks like: "\"{\\\"__bunite_ok\\\":true,...}\""
|
|
200
|
+
// We need the inner JSON. Walk-and-decode minimally.
|
|
201
|
+
std::string inner;
|
|
202
|
+
if (outer.size() >= 2 && outer.front() == '"' && outer.back() == '"') {
|
|
203
|
+
inner.reserve(outer.size());
|
|
204
|
+
for (size_t i = 1; i + 1 < outer.size(); ++i) {
|
|
205
|
+
if (outer[i] == '\\' && i + 2 < outer.size()) {
|
|
206
|
+
char nxt = outer[i + 1];
|
|
207
|
+
switch (nxt) {
|
|
208
|
+
case '"': inner += '"'; ++i; break;
|
|
209
|
+
case '\\': inner += '\\'; ++i; break;
|
|
210
|
+
case 'n': inner += '\n'; ++i; break;
|
|
211
|
+
case 'r': inner += '\r'; ++i; break;
|
|
212
|
+
case 't': inner += '\t'; ++i; break;
|
|
213
|
+
case '/': inner += '/'; ++i; break;
|
|
214
|
+
default: inner += outer[i]; break;
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
inner += outer[i];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (inner.empty()) {
|
|
222
|
+
// Wrapper didn't produce a string — script failure before catch
|
|
223
|
+
// (e.g. parse error). Surface as runtime_error.
|
|
224
|
+
payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
225
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
226
|
+
"\"message\":\"script returned non-string from wrapper\"}";
|
|
227
|
+
} else if (inner.find("\"__bunite_ok\":true") != std::string::npos) {
|
|
228
|
+
// Re-parse to find `value:`. Strip the prefix/suffix manually —
|
|
229
|
+
// the wrapper always emits {"__bunite_ok":true,"value":<JSON>}.
|
|
230
|
+
static const std::string prefix = "{\"__bunite_ok\":true,\"value\":";
|
|
231
|
+
static const std::string suffix = "}";
|
|
232
|
+
std::string value_json = "null";
|
|
233
|
+
if (inner.compare(0, prefix.size(), prefix) == 0 &&
|
|
234
|
+
inner.size() > prefix.size() + suffix.size()) {
|
|
235
|
+
value_json = inner.substr(prefix.size(), inner.size() - prefix.size() - 1);
|
|
236
|
+
}
|
|
237
|
+
payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
238
|
+
",\"ok\":true,\"value\":\"" + escapeJsonString(value_json) + "\"}";
|
|
239
|
+
} else {
|
|
240
|
+
// __bunite_ok:false branch. Anchor extraction at fixed prefix —
|
|
241
|
+
// user-controlled e.message could otherwise inject a fake "code".
|
|
242
|
+
static const std::string codePrefix = "{\"__bunite_ok\":false,\"code\":\"";
|
|
243
|
+
std::string code = "runtime_error";
|
|
244
|
+
std::string msg = "script threw";
|
|
245
|
+
if (inner.compare(0, codePrefix.size(), codePrefix) == 0) {
|
|
246
|
+
size_t start = codePrefix.size();
|
|
247
|
+
size_t end = start;
|
|
248
|
+
while (end < inner.size() && inner[end] != '"') ++end;
|
|
249
|
+
if (end > start) code = inner.substr(start, end - start);
|
|
250
|
+
// message key follows immediately after `","` separator.
|
|
251
|
+
static const std::string msgKey = "\",\"message\":\"";
|
|
252
|
+
if (end + msgKey.size() <= inner.size() &&
|
|
253
|
+
inner.compare(end, msgKey.size(), msgKey) == 0) {
|
|
254
|
+
size_t mstart = end + msgKey.size();
|
|
255
|
+
size_t mend = mstart;
|
|
256
|
+
while (mend < inner.size()) {
|
|
257
|
+
if (inner[mend] == '"' && (mend == mstart || inner[mend - 1] != '\\')) break;
|
|
258
|
+
++mend;
|
|
259
|
+
}
|
|
260
|
+
if (mend > mstart) msg = inner.substr(mstart, mend - mstart);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
264
|
+
",\"ok\":false,\"code\":\"" + escapeJsonString(code) + "\","
|
|
265
|
+
"\"message\":\"" + msg + "\"}";
|
|
266
|
+
}
|
|
188
267
|
}
|
|
189
268
|
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
190
269
|
return S_OK;
|
|
@@ -299,6 +378,221 @@ BUNITE_EXPORT void bunite_view_reload(uint32_t view_id) {
|
|
|
299
378
|
|
|
300
379
|
BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) { destroyView(view_id); }
|
|
301
380
|
|
|
381
|
+
// Input dispatch — CDP via CallDevToolsProtocolMethod (Playwright pattern).
|
|
382
|
+
// MouseEvent.isTrusted is false (CDP-synthesized); capability honest.
|
|
383
|
+
namespace {
|
|
384
|
+
|
|
385
|
+
const char* cdpMouseButton(int32_t b) {
|
|
386
|
+
switch (b) { case 1: return "middle"; case 2: return "right"; default: return "left"; }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
void cdpCall(ViewHost* v, const wchar_t* method, const std::string& json) {
|
|
390
|
+
if (!v || !v->webview) return;
|
|
391
|
+
v->webview->CallDevToolsProtocolMethod(
|
|
392
|
+
method, utf8ToWide(json).c_str(), nullptr);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
} // namespace
|
|
396
|
+
|
|
397
|
+
BUNITE_EXPORT void bunite_view_click(uint32_t view_id, double x, double y,
|
|
398
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
399
|
+
ViewHost* v = getView(view_id);
|
|
400
|
+
if (!v) return;
|
|
401
|
+
if (click_count < 1) click_count = 1;
|
|
402
|
+
// Multi-click → repeated pairs with increasing clickCount so the page sees
|
|
403
|
+
// a dblclick (Playwright convention).
|
|
404
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
405
|
+
std::string base = "\"x\":" + std::to_string(x) + ",\"y\":" + std::to_string(y) +
|
|
406
|
+
",\"button\":\"" + cdpMouseButton(button) + "\","
|
|
407
|
+
"\"clickCount\":" + std::to_string(i) +
|
|
408
|
+
",\"modifiers\":" + std::to_string(modifiers);
|
|
409
|
+
cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mousePressed\"," + base + "}");
|
|
410
|
+
cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mouseReleased\"," + base + "}");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
BUNITE_EXPORT void bunite_view_type(uint32_t view_id, const char* text) {
|
|
415
|
+
ViewHost* v = getView(view_id);
|
|
416
|
+
if (!v || !text) return;
|
|
417
|
+
std::string json = "{\"text\":\"" + escapeJsonString(text) + "\"}";
|
|
418
|
+
cdpCall(v, L"Input.insertText", json);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t windows_vk_code,
|
|
422
|
+
int32_t /*mac_key_code*/, const char* key, const char* code,
|
|
423
|
+
const char* character, uint32_t modifiers,
|
|
424
|
+
int32_t action, bool /*extended*/, int32_t location) {
|
|
425
|
+
ViewHost* v = getView(view_id);
|
|
426
|
+
if (!v) return;
|
|
427
|
+
std::string char_str = character ? character : "";
|
|
428
|
+
std::string key_str = key ? key : "";
|
|
429
|
+
std::string code_str = code ? code : "";
|
|
430
|
+
|
|
431
|
+
auto buildPart = [&](const char* type, bool include_text) {
|
|
432
|
+
std::string out = "{\"type\":\"";
|
|
433
|
+
out += type;
|
|
434
|
+
out += "\",\"modifiers\":" + std::to_string(modifiers);
|
|
435
|
+
if (windows_vk_code != 0) out += ",\"windowsVirtualKeyCode\":" + std::to_string(windows_vk_code);
|
|
436
|
+
if (!key_str.empty()) out += ",\"key\":\"" + escapeJsonString(key_str) + "\"";
|
|
437
|
+
if (!code_str.empty()) out += ",\"code\":\"" + escapeJsonString(code_str) + "\"";
|
|
438
|
+
// CDP `location`: 0 standard, 1 left mod, 2 right mod, 3 numpad.
|
|
439
|
+
if (location > 0) out += ",\"location\":" + std::to_string(location);
|
|
440
|
+
if (include_text && !char_str.empty())
|
|
441
|
+
out += ",\"text\":\"" + escapeJsonString(char_str) + "\"";
|
|
442
|
+
out += "}";
|
|
443
|
+
return out;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Playwright convention: CHAR `text` rides with keyDown for printable keys.
|
|
447
|
+
if (action != 1) cdpCall(v, L"Input.dispatchKeyEvent", buildPart("keyDown", /*include_text=*/true));
|
|
448
|
+
if (action != 0) cdpCall(v, L"Input.dispatchKeyEvent", buildPart("keyUp", /*include_text=*/false));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
} // extern "C" — temporarily exit so C++ helpers below can return std::string.
|
|
452
|
+
|
|
453
|
+
namespace {
|
|
454
|
+
|
|
455
|
+
// Win CryptoAPI base64 — `bytes` → printable string (no line breaks).
|
|
456
|
+
std::string base64Encode(const BYTE* bytes, DWORD len) {
|
|
457
|
+
DWORD out_len = 0;
|
|
458
|
+
if (!CryptBinaryToStringA(bytes, len, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &out_len)) return {};
|
|
459
|
+
std::string out(out_len, '\0');
|
|
460
|
+
if (!CryptBinaryToStringA(bytes, len, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, out.data(), &out_len)) return {};
|
|
461
|
+
out.resize(out_len); // CryptBinaryToString writes including trailing null on some configs; trim.
|
|
462
|
+
while (!out.empty() && out.back() == '\0') out.pop_back();
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
void emitScreenshotError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
467
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
468
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
469
|
+
"\"message\":\"" + escapeJsonString(message) + "\"}";
|
|
470
|
+
emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
} // namespace
|
|
474
|
+
|
|
475
|
+
extern "C" {
|
|
476
|
+
|
|
477
|
+
BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
|
|
478
|
+
// WebView2 — CDP input path. Empirically dispatchMouseEvent /
|
|
479
|
+
// dispatchKeyEvent / insertText produce events with `isTrusted=true` on
|
|
480
|
+
// the page (Edge runtime injects below DevTools surface; differs from
|
|
481
|
+
// browser-process CDP where isTrusted=false).
|
|
482
|
+
ViewHost* v = getView(view_id);
|
|
483
|
+
if (!v) return 0;
|
|
484
|
+
return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
|
|
485
|
+
BUNITE_CAP_NATIVE_INPUT_TRUSTED |
|
|
486
|
+
BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
|
|
487
|
+
BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
|
|
488
|
+
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
|
|
492
|
+
const char* format, int32_t /*quality*/) {
|
|
493
|
+
ViewHost* v = getView(view_id);
|
|
494
|
+
if (!v || !v->webview) {
|
|
495
|
+
emitScreenshotError(view_id, request_id, "not_supported", "view not ready");
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
std::string fmt = format ? format : "png";
|
|
499
|
+
COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT cwv_fmt;
|
|
500
|
+
std::string mime;
|
|
501
|
+
if (fmt == "jpeg" || fmt == "jpg") {
|
|
502
|
+
cwv_fmt = COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_JPEG;
|
|
503
|
+
fmt = "jpeg"; mime = "image/jpeg";
|
|
504
|
+
} else {
|
|
505
|
+
cwv_fmt = COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG;
|
|
506
|
+
fmt = "png"; mime = "image/png";
|
|
507
|
+
}
|
|
508
|
+
ComPtr<IStream> stream;
|
|
509
|
+
HRESULT hr = CreateStreamOnHGlobal(nullptr, TRUE, &stream);
|
|
510
|
+
if (FAILED(hr) || !stream) {
|
|
511
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "CreateStreamOnHGlobal failed");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
auto lifetime = g_runtime.lifetime;
|
|
515
|
+
v->webview->CapturePreview(
|
|
516
|
+
cwv_fmt, stream.Get(),
|
|
517
|
+
Microsoft::WRL::Callback<ICoreWebView2CapturePreviewCompletedHandler>(
|
|
518
|
+
[lifetime, view_id, request_id, fmt, mime, stream](HRESULT hr2) -> HRESULT {
|
|
519
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
520
|
+
if (FAILED(hr2)) {
|
|
521
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "CapturePreview failed");
|
|
522
|
+
return S_OK;
|
|
523
|
+
}
|
|
524
|
+
HGLOBAL hg = nullptr;
|
|
525
|
+
if (FAILED(GetHGlobalFromStream(stream.Get(), &hg)) || !hg) {
|
|
526
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "GetHGlobalFromStream failed");
|
|
527
|
+
return S_OK;
|
|
528
|
+
}
|
|
529
|
+
const SIZE_T size = GlobalSize(hg);
|
|
530
|
+
void* ptr = GlobalLock(hg);
|
|
531
|
+
std::string b64 = ptr ? base64Encode(static_cast<const BYTE*>(ptr), static_cast<DWORD>(size)) : std::string{};
|
|
532
|
+
if (ptr) GlobalUnlock(hg);
|
|
533
|
+
if (b64.empty()) {
|
|
534
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "base64 encode failed");
|
|
535
|
+
return S_OK;
|
|
536
|
+
}
|
|
537
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
538
|
+
",\"ok\":true,\"format\":\"" + fmt +
|
|
539
|
+
"\",\"mime\":\"" + mime +
|
|
540
|
+
"\",\"dataBase64\":\"" + b64 + "\"}";
|
|
541
|
+
emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
542
|
+
return S_OK;
|
|
543
|
+
}).Get());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
BUNITE_EXPORT void bunite_view_scroll(uint32_t view_id, double dx, double dy,
|
|
547
|
+
double x, double y, uint32_t modifiers) {
|
|
548
|
+
ViewHost* v = getView(view_id);
|
|
549
|
+
if (!v) return;
|
|
550
|
+
std::string json = "{\"type\":\"mouseWheel\",\"x\":" + std::to_string(x) +
|
|
551
|
+
",\"y\":" + std::to_string(y) +
|
|
552
|
+
",\"deltaX\":" + std::to_string(dx) +
|
|
553
|
+
",\"deltaY\":" + std::to_string(dy) +
|
|
554
|
+
",\"modifiers\":" + std::to_string(modifiers) + "}";
|
|
555
|
+
cdpCall(v, L"Input.dispatchMouseEvent", json);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
BUNITE_EXPORT void bunite_view_mouse(uint32_t view_id, int32_t action,
|
|
559
|
+
double x, double y, int32_t button,
|
|
560
|
+
uint32_t modifiers) {
|
|
561
|
+
ViewHost* v = getView(view_id);
|
|
562
|
+
if (!v) return;
|
|
563
|
+
// CDP mouseMoved / mousePressed / mouseReleased. WV2 produces isTrusted=true
|
|
564
|
+
// on the page for these (Edge runtime injects below DevTools surface).
|
|
565
|
+
const char* type = (action == 0) ? "mouseMoved"
|
|
566
|
+
: (action == 1) ? "mousePressed" : "mouseReleased";
|
|
567
|
+
std::string json = "{\"type\":\"" + std::string(type) +
|
|
568
|
+
"\",\"x\":" + std::to_string(x) +
|
|
569
|
+
",\"y\":" + std::to_string(y) +
|
|
570
|
+
",\"modifiers\":" + std::to_string(modifiers);
|
|
571
|
+
if (action != 0) {
|
|
572
|
+
const char* btn = (button == 2) ? "right" : (button == 1) ? "middle" : "left";
|
|
573
|
+
json += ",\"button\":\"" + std::string(btn) + "\",\"clickCount\":1";
|
|
574
|
+
}
|
|
575
|
+
json += "}";
|
|
576
|
+
cdpCall(v, L"Input.dispatchMouseEvent", json);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
|
|
580
|
+
bool accept, const char* text) {
|
|
581
|
+
ViewHost* v = getView(view_id);
|
|
582
|
+
if (!v) return;
|
|
583
|
+
auto it = v->pending_dialogs.find(request_id);
|
|
584
|
+
if (it == v->pending_dialogs.end()) return;
|
|
585
|
+
ViewHost::PendingDialog entry = std::move(it->second);
|
|
586
|
+
v->pending_dialogs.erase(it);
|
|
587
|
+
if (entry.args && accept) {
|
|
588
|
+
// prompt: feed user text. WV2 ignores put_ResultText for non-prompt kinds.
|
|
589
|
+
if (text && *text) entry.args->put_ResultText(utf8ToWide(text).c_str());
|
|
590
|
+
entry.args->Accept();
|
|
591
|
+
}
|
|
592
|
+
// accept=false → no Accept() call → WV2 treats as dismiss (default behavior).
|
|
593
|
+
if (entry.deferral) entry.deferral->Complete();
|
|
594
|
+
}
|
|
595
|
+
|
|
302
596
|
BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
|
|
303
597
|
ViewHost* v = getView(view_id);
|
|
304
598
|
if (v && v->webview) v->webview->OpenDevToolsWindow();
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
#include <fstream>
|
|
18
18
|
#include <functional>
|
|
19
19
|
#include <map>
|
|
20
|
+
#include <unordered_map>
|
|
20
21
|
#include <memory>
|
|
21
22
|
#include <mutex>
|
|
22
23
|
#include <optional>
|
|
@@ -86,6 +87,15 @@ struct ViewHost {
|
|
|
86
87
|
|
|
87
88
|
std::atomic<bool> ready{false};
|
|
88
89
|
std::atomic<bool> closing{false};
|
|
90
|
+
|
|
91
|
+
// Pending page-initiated dialogs (alert/confirm/prompt/beforeunload).
|
|
92
|
+
// ScriptDialogOpening hands us a `Deferral` we Complete() on host response.
|
|
93
|
+
struct PendingDialog {
|
|
94
|
+
ComPtr<ICoreWebView2ScriptDialogOpeningEventArgs> args;
|
|
95
|
+
ComPtr<ICoreWebView2Deferral> deferral;
|
|
96
|
+
};
|
|
97
|
+
std::unordered_map<uint32_t, PendingDialog> pending_dialogs;
|
|
98
|
+
uint32_t next_dialog_request_id = 1;
|
|
89
99
|
};
|
|
90
100
|
|
|
91
101
|
struct WindowHost {
|
|
@@ -205,6 +215,7 @@ std::wstring exeDir();
|
|
|
205
215
|
uint32_t permissionKindToBuniteBit(COREWEBVIEW2_PERMISSION_KIND kind);
|
|
206
216
|
COREWEBVIEW2_PERMISSION_STATE buniteStateToWebView2(uint32_t state);
|
|
207
217
|
|
|
218
|
+
bool shouldAlwaysAllowNavigationUrl(const std::string& url);
|
|
208
219
|
bool shouldAllowNavigation(const ViewHost* view, const std::string& url);
|
|
209
220
|
|
|
210
221
|
} // namespace bunite_webview2
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "webview2_internal.h"
|
|
2
2
|
|
|
3
3
|
#include <algorithm>
|
|
4
|
+
#include <unordered_map>
|
|
4
5
|
|
|
5
6
|
using Microsoft::WRL::Callback;
|
|
6
7
|
using Microsoft::WRL::ComPtr;
|
|
@@ -10,6 +11,14 @@ namespace bunite_webview2 {
|
|
|
10
11
|
|
|
11
12
|
RuntimeState g_runtime;
|
|
12
13
|
|
|
14
|
+
// Pending nav URI by (view_id, nav_id) — NavigationCompleted's get_Source()
|
|
15
|
+
// returns the previous committed URL on provisional failure, so we stash the
|
|
16
|
+
// URI at NavigationStarting and look it up on completion.
|
|
17
|
+
static std::unordered_map<uint64_t, std::string> g_nav_uris;
|
|
18
|
+
static uint64_t navKey(uint32_t view_id, uint64_t nav_id) {
|
|
19
|
+
return (static_cast<uint64_t>(view_id) << 56) ^ nav_id;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
static HINSTANCE g_module = nullptr;
|
|
14
23
|
static bool g_co_initialized = false;
|
|
15
24
|
|
|
@@ -121,7 +130,19 @@ bool registerWindowClasses() {
|
|
|
121
130
|
0, 0, 0, 0, 0,
|
|
122
131
|
HWND_MESSAGE, nullptr,
|
|
123
132
|
getCurrentModuleHandle(), nullptr);
|
|
124
|
-
|
|
133
|
+
if (!g_runtime.message_window) return false;
|
|
134
|
+
|
|
135
|
+
// `bun run` passes STARTF_USESHOWWINDOW + SW_HIDE; Win documented behavior
|
|
136
|
+
// is for the first ShowWindow call to use STARTUPINFO.wShowWindow instead
|
|
137
|
+
// of the requested nCmdShow. Consume it here on the message window so the
|
|
138
|
+
// first user-visible window's ShowWindow honors its argument.
|
|
139
|
+
STARTUPINFOW si{};
|
|
140
|
+
si.cb = sizeof(si);
|
|
141
|
+
GetStartupInfoW(&si);
|
|
142
|
+
if ((si.dwFlags & STARTF_USESHOWWINDOW) && si.wShowWindow == SW_HIDE) {
|
|
143
|
+
ShowWindow(g_runtime.message_window, SW_HIDE);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
// ---- environment bootstrap -----------------------------------------------
|
|
@@ -282,6 +303,19 @@ LRESULT CALLBACK messageProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
|
282
303
|
|
|
283
304
|
// ---- init / shutdown ----------------------------------------------------
|
|
284
305
|
|
|
306
|
+
// KILL_ON_JOB_CLOSE — Edge helpers die with bun.exe instead of holding the
|
|
307
|
+
// UDF SingletonLock. Handle leaked intentionally (kernel closes on exit).
|
|
308
|
+
static void reapChildrenOnExit() {
|
|
309
|
+
HANDLE job = CreateJobObjectW(nullptr, nullptr);
|
|
310
|
+
if (!job) return;
|
|
311
|
+
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{};
|
|
312
|
+
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
313
|
+
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info, sizeof(info)) ||
|
|
314
|
+
!AssignProcessToJobObject(job, GetCurrentProcess())) {
|
|
315
|
+
CloseHandle(job); // already in a non-nestable job, etc — give up.
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
285
319
|
bool initRuntime(const char* engine_dir, bool /*hide_console*/,
|
|
286
320
|
bool popup_blocking, const char* engine_config_json) {
|
|
287
321
|
buniteApplyEnvLogLevel();
|
|
@@ -289,6 +323,8 @@ bool initRuntime(const char* engine_dir, bool /*hide_console*/,
|
|
|
289
323
|
(engine_dir && *engine_dir) ? engine_dir : "(null)");
|
|
290
324
|
if (g_runtime.initialized.load()) return true;
|
|
291
325
|
|
|
326
|
+
reapChildrenOnExit();
|
|
327
|
+
|
|
292
328
|
HRESULT co = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
|
293
329
|
if (SUCCEEDED(co)) g_co_initialized = true;
|
|
294
330
|
else if (co != RPC_E_CHANGED_MODE) {
|
|
@@ -766,9 +802,13 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
766
802
|
if (!view->webview) return;
|
|
767
803
|
auto lifetime = g_runtime.lifetime;
|
|
768
804
|
uint32_t view_id = view->id;
|
|
805
|
+
// Token reuse OK — controller->Close() releases all add_* handlers.
|
|
769
806
|
EventRegistrationToken tok;
|
|
770
807
|
|
|
771
|
-
// NavigationStarting —
|
|
808
|
+
// NavigationStarting — emit "will-navigate" (parity with CEF/mac/linux,
|
|
809
|
+
// which fire regardless of allow), then cancel if nav rules say block.
|
|
810
|
+
// Also emit "load-start" for the surfaceEvents stream + stash URI for
|
|
811
|
+
// NavigationCompleted lookup (failure case: get_Source() returns prior URL).
|
|
772
812
|
view->webview->add_NavigationStarting(
|
|
773
813
|
Callback<ICoreWebView2NavigationStartingEventHandler>(
|
|
774
814
|
[lifetime, view_id](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
|
|
@@ -779,34 +819,110 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
779
819
|
args->get_Uri(&uri_raw);
|
|
780
820
|
std::string url = wideToUtf8(uri_raw);
|
|
781
821
|
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
822
|
+
emitWebviewEvent(v->id, "will-navigate", url);
|
|
782
823
|
if (!shouldAllowNavigation(v, url)) {
|
|
783
824
|
args->put_Cancel(TRUE);
|
|
784
825
|
return S_OK;
|
|
785
826
|
}
|
|
786
|
-
|
|
827
|
+
UINT64 nav_id = 0;
|
|
828
|
+
args->get_NavigationId(&nav_id);
|
|
829
|
+
g_nav_uris[navKey(view_id, nav_id)] = url;
|
|
830
|
+
emitWebviewEvent(v->id, "load-start", url);
|
|
787
831
|
return S_OK;
|
|
788
832
|
}).Get(),
|
|
789
833
|
&tok);
|
|
790
834
|
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
835
|
+
// SourceChanged — URL commit point; map to did-navigate (surfaceEvents
|
|
836
|
+
// `navigate` arm). Distinct from NavigationCompleted which fires later.
|
|
837
|
+
view->webview->add_SourceChanged(
|
|
838
|
+
Callback<ICoreWebView2SourceChangedEventHandler>(
|
|
839
|
+
[lifetime, view_id](ICoreWebView2* wv, ICoreWebView2SourceChangedEventArgs*) -> HRESULT {
|
|
795
840
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
796
|
-
BOOL ok = FALSE;
|
|
797
|
-
args->get_IsSuccess(&ok);
|
|
798
|
-
if (!ok) return S_OK;
|
|
799
841
|
LPWSTR src_raw = nullptr;
|
|
800
842
|
if (wv) wv->get_Source(&src_raw);
|
|
801
843
|
std::string url = wideToUtf8(src_raw);
|
|
802
844
|
if (src_raw) CoTaskMemFree(src_raw);
|
|
803
845
|
emitWebviewEvent(view_id, "did-navigate", url);
|
|
804
|
-
emitWebviewEvent(view_id, "dom-ready", url);
|
|
805
846
|
return S_OK;
|
|
806
847
|
}).Get(),
|
|
807
848
|
&tok);
|
|
808
849
|
|
|
809
|
-
//
|
|
850
|
+
// NavigationCompleted — load lifecycle terminator. Success → load-finish
|
|
851
|
+
// + dom-ready; failure → load-fail with WebErrorStatus as reason. Use the
|
|
852
|
+
// URI we stashed at NavigationStarting — get_Source() returns the prior
|
|
853
|
+
// committed URL on provisional-navigation failure.
|
|
854
|
+
view->webview->add_NavigationCompleted(
|
|
855
|
+
Callback<ICoreWebView2NavigationCompletedEventHandler>(
|
|
856
|
+
[lifetime, view_id](ICoreWebView2* wv, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT {
|
|
857
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
858
|
+
BOOL ok = FALSE;
|
|
859
|
+
args->get_IsSuccess(&ok);
|
|
860
|
+
UINT64 nav_id = 0;
|
|
861
|
+
args->get_NavigationId(&nav_id);
|
|
862
|
+
std::string url;
|
|
863
|
+
auto it = g_nav_uris.find(navKey(view_id, nav_id));
|
|
864
|
+
if (it != g_nav_uris.end()) {
|
|
865
|
+
url = std::move(it->second);
|
|
866
|
+
g_nav_uris.erase(it);
|
|
867
|
+
} else {
|
|
868
|
+
LPWSTR src_raw = nullptr;
|
|
869
|
+
if (wv) wv->get_Source(&src_raw);
|
|
870
|
+
url = wideToUtf8(src_raw);
|
|
871
|
+
if (src_raw) CoTaskMemFree(src_raw);
|
|
872
|
+
}
|
|
873
|
+
if (ok) {
|
|
874
|
+
emitWebviewEvent(view_id, "load-finish", url);
|
|
875
|
+
emitWebviewEvent(view_id, "dom-ready", url);
|
|
876
|
+
} else {
|
|
877
|
+
COREWEBVIEW2_WEB_ERROR_STATUS status = COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN;
|
|
878
|
+
args->get_WebErrorStatus(&status);
|
|
879
|
+
std::string payload = "{\"url\":\"" + escapeJsonString(url) +
|
|
880
|
+
"\",\"reason\":\"WebErrorStatus_" + std::to_string(static_cast<int>(status)) + "\"}";
|
|
881
|
+
emitWebviewEvent(view_id, "load-fail", payload);
|
|
882
|
+
}
|
|
883
|
+
return S_OK;
|
|
884
|
+
}).Get(),
|
|
885
|
+
&tok);
|
|
886
|
+
|
|
887
|
+
// ScriptDialogOpening — alert / confirm / prompt / beforeunload. Defer the
|
|
888
|
+
// event so host can decide via `respondToDialog`.
|
|
889
|
+
view->webview->add_ScriptDialogOpening(
|
|
890
|
+
Callback<ICoreWebView2ScriptDialogOpeningEventHandler>(
|
|
891
|
+
[lifetime, view_id](ICoreWebView2*, ICoreWebView2ScriptDialogOpeningEventArgs* args) -> HRESULT {
|
|
892
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
893
|
+
ViewHost* v = getView(view_id);
|
|
894
|
+
if (!v) return S_OK;
|
|
895
|
+
ComPtr<ICoreWebView2Deferral> deferral;
|
|
896
|
+
args->GetDeferral(&deferral);
|
|
897
|
+
COREWEBVIEW2_SCRIPT_DIALOG_KIND kind = COREWEBVIEW2_SCRIPT_DIALOG_KIND_ALERT;
|
|
898
|
+
args->get_Kind(&kind);
|
|
899
|
+
LPWSTR msg_raw = nullptr;
|
|
900
|
+
args->get_Message(&msg_raw);
|
|
901
|
+
std::string message = wideToUtf8(msg_raw);
|
|
902
|
+
if (msg_raw) CoTaskMemFree(msg_raw);
|
|
903
|
+
LPWSTR def_raw = nullptr;
|
|
904
|
+
args->get_DefaultText(&def_raw);
|
|
905
|
+
std::string default_prompt = wideToUtf8(def_raw);
|
|
906
|
+
if (def_raw) CoTaskMemFree(def_raw);
|
|
907
|
+
const char* kind_str = (kind == COREWEBVIEW2_SCRIPT_DIALOG_KIND_CONFIRM) ? "confirm"
|
|
908
|
+
: (kind == COREWEBVIEW2_SCRIPT_DIALOG_KIND_PROMPT) ? "prompt"
|
|
909
|
+
: (kind == COREWEBVIEW2_SCRIPT_DIALOG_KIND_BEFOREUNLOAD) ? "beforeunload"
|
|
910
|
+
: "alert";
|
|
911
|
+
const uint32_t rid = v->next_dialog_request_id++;
|
|
912
|
+
v->pending_dialogs[rid] = ViewHost::PendingDialog{ args, std::move(deferral) };
|
|
913
|
+
std::string payload = "{\"requestId\":" + std::to_string(rid) +
|
|
914
|
+
",\"kind\":\"" + kind_str +
|
|
915
|
+
"\",\"message\":\"" + escapeJsonString(message) + "\"";
|
|
916
|
+
if (kind == COREWEBVIEW2_SCRIPT_DIALOG_KIND_PROMPT) {
|
|
917
|
+
payload += ",\"defaultPrompt\":\"" + escapeJsonString(default_prompt) + "\"";
|
|
918
|
+
}
|
|
919
|
+
payload += "}";
|
|
920
|
+
emitWebviewEvent(view_id, "dialog", payload);
|
|
921
|
+
return S_OK;
|
|
922
|
+
}).Get(),
|
|
923
|
+
&tok);
|
|
924
|
+
|
|
925
|
+
// DocumentTitleChanged — surface for automation surfaceEvents title-change arm.
|
|
810
926
|
view->webview->add_DocumentTitleChanged(
|
|
811
927
|
Callback<ICoreWebView2DocumentTitleChangedEventHandler>(
|
|
812
928
|
[lifetime, view_id](ICoreWebView2* wv, IUnknown*) -> HRESULT {
|
|
@@ -50,14 +50,14 @@ std::string escapeJsonString(const std::string& s) {
|
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Wildcard semantics shared with CEF/mac/linux backends: `*` only (no `?`),
|
|
54
|
+
// locale-respecting std::tolower. Keeping the 4 impls in lockstep is the rule.
|
|
53
55
|
bool globMatchCaseInsensitive(const std::string& pattern, const std::string& value) {
|
|
54
|
-
auto lower = [](char c) -> char {
|
|
55
|
-
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + 32) : c;
|
|
56
|
-
};
|
|
57
56
|
size_t pi = 0, vi = 0, star = std::string::npos, match = 0;
|
|
58
57
|
while (vi < value.size()) {
|
|
59
|
-
if (pi < pattern.size() &&
|
|
60
|
-
|
|
58
|
+
if (pi < pattern.size() &&
|
|
59
|
+
std::tolower(static_cast<unsigned char>(pattern[pi])) ==
|
|
60
|
+
std::tolower(static_cast<unsigned char>(value[vi]))) {
|
|
61
61
|
pi++; vi++;
|
|
62
62
|
} else if (pi < pattern.size() && pattern[pi] == '*') {
|
|
63
63
|
star = pi++; match = vi;
|
|
@@ -201,6 +201,10 @@ std::wstring exeDir() {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
std::string defaultUserDataFolder() {
|
|
204
|
+
// Per-app via BUNITE_USER_DATA_DIR — shared path is a SingletonLock.
|
|
205
|
+
if (const char* env = std::getenv("BUNITE_USER_DATA_DIR"); env && *env) {
|
|
206
|
+
return std::string(env) + "\\WebView2";
|
|
207
|
+
}
|
|
204
208
|
wchar_t* base = nullptr;
|
|
205
209
|
if (SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &base) != S_OK || !base) {
|
|
206
210
|
if (base) CoTaskMemFree(base);
|
|
@@ -232,15 +236,29 @@ COREWEBVIEW2_PERMISSION_STATE buniteStateToWebView2(uint32_t state) {
|
|
|
232
236
|
}
|
|
233
237
|
}
|
|
234
238
|
|
|
239
|
+
// Whitelist must be exact-match — prefix would let `../../evil` style paths
|
|
240
|
+
// bypass nav-rule scrutiny (the appres scheme handler still blocks file
|
|
241
|
+
// access, but the navigation decision itself should be honest).
|
|
242
|
+
bool shouldAlwaysAllowNavigationUrl(const std::string& url) {
|
|
243
|
+
return url == "about:blank" ||
|
|
244
|
+
url == "appres://app.internal/internal/index.html";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Same semantics as CEF / mac / linux: `^` prefix = block, last-match-wins,
|
|
248
|
+
// default-allow. The shared shouldAlwaysAllowNavigationUrl whitelist guards
|
|
249
|
+
// about:blank + appres internal routes from being denied by user rules.
|
|
235
250
|
bool shouldAllowNavigation(const ViewHost* view, const std::string& url) {
|
|
236
|
-
if (!view || view->navigation_rules.empty())
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
if (!view || shouldAlwaysAllowNavigationUrl(url) || view->navigation_rules.empty()) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
bool allowed = true;
|
|
255
|
+
for (const auto& raw : view->navigation_rules) {
|
|
256
|
+
const bool block = !raw.empty() && raw.front() == '^';
|
|
257
|
+
const std::string pattern = block ? raw.substr(1) : raw;
|
|
258
|
+
if (pattern.empty()) continue;
|
|
259
|
+
if (globMatchCaseInsensitive(pattern, url)) allowed = !block;
|
|
242
260
|
}
|
|
243
|
-
return
|
|
261
|
+
return allowed;
|
|
244
262
|
}
|
|
245
263
|
|
|
246
264
|
} // namespace bunite_webview2
|