bunite-core 0.14.0 → 0.17.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 +6 -3
- package/src/host/core/BrowserView.ts +345 -24
- package/src/host/core/BrowserWindow.ts +52 -6
- package/src/host/core/SurfaceBrowserIPC.ts +10 -1
- package/src/host/core/SurfaceManager.ts +357 -16
- package/src/host/core/windowCap.ts +69 -0
- package/src/host/events/webviewEvents.ts +18 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +145 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +225 -1
- package/src/native/linux/bunite_linux_internal.h +12 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_view.cpp +211 -5
- package/src/native/mac/bunite_mac_ffi.mm +293 -4
- package/src/native/mac/bunite_mac_internal.h +13 -0
- package/src/native/mac/bunite_mac_view.mm +227 -7
- package/src/native/shared/ffi_exports.h +97 -30
- package/src/native/win/native_host_cef.cpp +107 -13
- package/src/native/win/native_host_ffi.cpp +831 -2
- package/src/native/win/native_host_internal.h +22 -0
- package/src/native/win/native_host_runtime.cpp +34 -0
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +827 -5
- package/src/native/win-webview2/webview2_internal.h +19 -0
- package/src/native/win-webview2/webview2_runtime.cpp +383 -31
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +39 -0
- package/src/rpc/framework.ts +194 -12
- package/src/rpc/index.ts +12 -0
- package/src/rpc/peer.ts +1 -1
- package/src/webview/native.ts +142 -32
- package/src/webview/polyfill.ts +91 -14
|
@@ -88,6 +88,10 @@ struct ViewHost {
|
|
|
88
88
|
std::atomic<bool> ready{false};
|
|
89
89
|
std::atomic<bool> closing{false};
|
|
90
90
|
|
|
91
|
+
// Download policy: 0=auto, 1=ask, 2=block. Default block (current behavior).
|
|
92
|
+
std::atomic<int32_t> download_policy{2};
|
|
93
|
+
std::string download_dir; // optional; falls back to backend default temp dir.
|
|
94
|
+
|
|
91
95
|
// Pending page-initiated dialogs (alert/confirm/prompt/beforeunload).
|
|
92
96
|
// ScriptDialogOpening hands us a `Deferral` we Complete() on host response.
|
|
93
97
|
struct PendingDialog {
|
|
@@ -96,6 +100,15 @@ struct ViewHost {
|
|
|
96
100
|
};
|
|
97
101
|
std::unordered_map<uint32_t, PendingDialog> pending_dialogs;
|
|
98
102
|
uint32_t next_dialog_request_id = 1;
|
|
103
|
+
|
|
104
|
+
// OOPIF input dispatch — populated by Target.attachedToTarget events after
|
|
105
|
+
// lazy Target.setAutoAttach. frameId → sessionId for flatten:true routing.
|
|
106
|
+
std::atomic<bool> oopif_autoattach_armed{false};
|
|
107
|
+
std::mutex oopif_sessions_mutex;
|
|
108
|
+
std::unordered_map<std::string, std::string> oopif_sessions;
|
|
109
|
+
EventRegistrationToken target_attached_token{};
|
|
110
|
+
EventRegistrationToken target_detached_token{};
|
|
111
|
+
bool oopif_event_tokens_registered = false;
|
|
99
112
|
};
|
|
100
113
|
|
|
101
114
|
struct WindowHost {
|
|
@@ -110,12 +123,18 @@ struct WindowHost {
|
|
|
110
123
|
std::atomic<bool> close_pending{false};
|
|
111
124
|
std::atomic<bool> closing{false};
|
|
112
125
|
std::vector<ViewHost*> views;
|
|
126
|
+
// Capture-based move-drag (no WM_NCLBUTTONDOWN modal loop — see
|
|
127
|
+
// bunite_window_begin_move_drag + windowProc).
|
|
128
|
+
bool drag_active = false;
|
|
129
|
+
POINT drag_anchor_cursor{}; // screen cursor at drag start
|
|
130
|
+
POINT drag_anchor_origin{}; // window top-left at drag start
|
|
113
131
|
};
|
|
114
132
|
|
|
115
133
|
struct RuntimeState {
|
|
116
134
|
std::atomic<bool> initialized{false};
|
|
117
135
|
std::atomic<bool> shutting_down{false};
|
|
118
136
|
HWND message_window = nullptr;
|
|
137
|
+
HWND popup_parent = nullptr; // hidden top-level parking parent for popup-minted controllers.
|
|
119
138
|
|
|
120
139
|
std::mutex task_mutex;
|
|
121
140
|
std::deque<std::function<void()>> queued_tasks;
|
|
@@ -132,6 +132,15 @@ bool registerWindowClasses() {
|
|
|
132
132
|
getCurrentModuleHandle(), nullptr);
|
|
133
133
|
if (!g_runtime.message_window) return false;
|
|
134
134
|
|
|
135
|
+
// Popup parking parent — a hidden top-level window. Children of HWND_MESSAGE
|
|
136
|
+
// can't render (WebView2 child controllers won't initialize), so popup-minted
|
|
137
|
+
// controllers live here until accept reparents to the user-visible host.
|
|
138
|
+
g_runtime.popup_parent = CreateWindowExW(
|
|
139
|
+
WS_EX_TOOLWINDOW, mc.lpszClassName, L"BunitePopupPark",
|
|
140
|
+
WS_POPUP, 0, 0, 0, 0,
|
|
141
|
+
nullptr, nullptr, getCurrentModuleHandle(), nullptr);
|
|
142
|
+
if (!g_runtime.popup_parent) return false;
|
|
143
|
+
|
|
135
144
|
// `bun run` passes STARTF_USESHOWWINDOW + SW_HIDE; Win documented behavior
|
|
136
145
|
// is for the first ShowWindow call to use STARTUPINFO.wShowWindow instead
|
|
137
146
|
// of the requested nCmdShow. Consume it here on the message window so the
|
|
@@ -255,13 +264,103 @@ static void layoutViewsForWindow(WindowHost* w) {
|
|
|
255
264
|
for (ViewHost* v : w->views) applyViewLayout(v);
|
|
256
265
|
}
|
|
257
266
|
|
|
267
|
+
// Set drag_active=false before ReleaseCapture so the WM_CAPTURECHANGED it
|
|
268
|
+
// posts is a no-op (re-entrancy guard).
|
|
269
|
+
static void endMoveDrag(WindowHost* w, HWND hwnd) {
|
|
270
|
+
w->drag_active = false;
|
|
271
|
+
if (GetCapture() == hwnd) ReleaseCapture();
|
|
272
|
+
}
|
|
273
|
+
|
|
258
274
|
LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
259
275
|
switch (msg) {
|
|
276
|
+
case WM_NCCALCSIZE: {
|
|
277
|
+
// Frameless windows keep WS_THICKFRAME (Win11 snap + native resize), so
|
|
278
|
+
// DefWindowProc reserves a sizing frame on every edge. The HTML titlebar
|
|
279
|
+
// fills the client rect, leaving the DWM frame exposed as thin bands.
|
|
280
|
+
if (wp != TRUE) break;
|
|
281
|
+
WindowHost* w = findWindowByHwnd(hwnd);
|
|
282
|
+
if (!w || (w->title_bar_style != L"hidden" && w->title_bar_style != L"hiddenInset"))
|
|
283
|
+
break;
|
|
284
|
+
auto* p = reinterpret_cast<NCCALCSIZE_PARAMS*>(lp);
|
|
285
|
+
// Maximized: the window rect is clamped to the work area (WM_GETMINMAXINFO),
|
|
286
|
+
// so there is no off-screen frame — reclaim every edge to fill it with no
|
|
287
|
+
// frame slivers. Restored: reclaim only the top edge so the titlebar reaches
|
|
288
|
+
// y=0 (kills the top light band); keep L/R/B borders grabbable for native
|
|
289
|
+
// resize. The top is the HTML titlebar (move region), not a resize edge.
|
|
290
|
+
if (IsZoomed(hwnd)) return 0;
|
|
291
|
+
LONG top = p->rgrc[0].top;
|
|
292
|
+
LRESULT r = DefWindowProcW(hwnd, msg, wp, lp);
|
|
293
|
+
p->rgrc[0].top = top;
|
|
294
|
+
return r;
|
|
295
|
+
}
|
|
296
|
+
case WM_MOUSEMOVE: {
|
|
297
|
+
WindowHost* w = findWindowByHwnd(hwnd);
|
|
298
|
+
if (!w || !w->drag_active) break;
|
|
299
|
+
if (!(wp & MK_LBUTTON)) { endMoveDrag(w, hwnd); return 0; }
|
|
300
|
+
POINT cur;
|
|
301
|
+
if (GetCursorPos(&cur)) {
|
|
302
|
+
SetWindowPos(hwnd, nullptr,
|
|
303
|
+
w->drag_anchor_origin.x + (cur.x - w->drag_anchor_cursor.x),
|
|
304
|
+
w->drag_anchor_origin.y + (cur.y - w->drag_anchor_cursor.y),
|
|
305
|
+
0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
306
|
+
}
|
|
307
|
+
return 0; // don't deref w past SetWindowPos (may re-enter windowProc)
|
|
308
|
+
}
|
|
309
|
+
case WM_LBUTTONUP: {
|
|
310
|
+
WindowHost* w = findWindowByHwnd(hwnd);
|
|
311
|
+
if (w && w->drag_active) { endMoveDrag(w, hwnd); return 0; }
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case WM_CAPTURECHANGED:
|
|
315
|
+
case WM_CANCELMODE: {
|
|
316
|
+
WindowHost* w = findWindowByHwnd(hwnd);
|
|
317
|
+
if (w && w->drag_active) endMoveDrag(w, hwnd);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
260
320
|
case WM_SIZE: {
|
|
261
321
|
WindowHost* w = findWindowByHwnd(hwnd);
|
|
262
|
-
if (w)
|
|
322
|
+
if (!w) break;
|
|
323
|
+
layoutViewsForWindow(w);
|
|
324
|
+
RECT r{};
|
|
325
|
+
GetWindowRect(hwnd, &r);
|
|
326
|
+
std::string payload = "{\"x\":" + std::to_string(r.left) + ",\"y\":" + std::to_string(r.top) +
|
|
327
|
+
",\"width\":" + std::to_string(r.right - r.left) + ",\"height\":" + std::to_string(r.bottom - r.top) +
|
|
328
|
+
",\"maximized\":" + (wp == SIZE_MAXIMIZED ? "true" : "false") +
|
|
329
|
+
",\"minimized\":" + (wp == SIZE_MINIMIZED ? "true" : "false") + "}";
|
|
330
|
+
emitWindowEvent(w->id, "resize", payload);
|
|
263
331
|
return 0;
|
|
264
332
|
}
|
|
333
|
+
case WM_MOVE: {
|
|
334
|
+
WindowHost* w = findWindowByHwnd(hwnd);
|
|
335
|
+
if (w) {
|
|
336
|
+
RECT r{};
|
|
337
|
+
GetWindowRect(hwnd, &r);
|
|
338
|
+
emitWindowEvent(w->id, "move",
|
|
339
|
+
"{\"x\":" + std::to_string(r.left) + ",\"y\":" + std::to_string(r.top) +
|
|
340
|
+
",\"maximized\":" + (IsZoomed(hwnd) ? "true" : "false") +
|
|
341
|
+
",\"minimized\":" + (IsIconic(hwnd) ? "true" : "false") + "}");
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case WM_GETMINMAXINFO: {
|
|
346
|
+
// Frameless (WS_POPUP) windows maximize over the taskbar unless clamped
|
|
347
|
+
// to the monitor work area.
|
|
348
|
+
WindowHost* w = findWindowByHwnd(hwnd);
|
|
349
|
+
if (w && (w->title_bar_style == L"hidden" || w->title_bar_style == L"hiddenInset")) {
|
|
350
|
+
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
|
351
|
+
MONITORINFO mi{ sizeof(mi) };
|
|
352
|
+
if (GetMonitorInfoW(mon, &mi)) {
|
|
353
|
+
auto* mmi = reinterpret_cast<MINMAXINFO*>(lp);
|
|
354
|
+
mmi->ptMaxPosition.x = mi.rcWork.left - mi.rcMonitor.left;
|
|
355
|
+
mmi->ptMaxPosition.y = mi.rcWork.top - mi.rcMonitor.top;
|
|
356
|
+
mmi->ptMaxSize.x = mi.rcWork.right - mi.rcWork.left;
|
|
357
|
+
mmi->ptMaxSize.y = mi.rcWork.bottom - mi.rcWork.top;
|
|
358
|
+
mmi->ptMaxTrackSize = mmi->ptMaxSize;
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
265
364
|
case WM_CLOSE: {
|
|
266
365
|
WindowHost* w = findWindowByHwnd(hwnd);
|
|
267
366
|
if (w && !w->close_pending.load()) {
|
|
@@ -272,19 +371,17 @@ LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
|
272
371
|
DestroyWindow(hwnd);
|
|
273
372
|
return 0;
|
|
274
373
|
}
|
|
275
|
-
case WM_SETFOCUS:
|
|
276
374
|
case WM_ACTIVATE: {
|
|
277
375
|
WindowHost* w = findWindowByHwnd(hwnd);
|
|
278
|
-
|
|
376
|
+
// Top-level activation drives focus (not WM_SETFOCUS, which can be child focus).
|
|
377
|
+
if (w) emitWindowEvent(w->id, LOWORD(wp) == WA_INACTIVE ? "blur" : "focus");
|
|
279
378
|
break;
|
|
280
379
|
}
|
|
281
|
-
case
|
|
380
|
+
case WM_DESTROY: {
|
|
282
381
|
WindowHost* w = findWindowByHwnd(hwnd);
|
|
283
|
-
if (w)
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
case WM_DESTROY:
|
|
382
|
+
if (w && w->drag_active) endMoveDrag(w, hwnd);
|
|
287
383
|
return 0;
|
|
384
|
+
}
|
|
288
385
|
}
|
|
289
386
|
return DefWindowProcW(hwnd, msg, wp, lp);
|
|
290
387
|
}
|
|
@@ -506,6 +603,14 @@ bool createWindow(uint32_t window_id, double x, double y, double w, double h,
|
|
|
506
603
|
g_runtime.windows_by_id[window_id] = host;
|
|
507
604
|
}
|
|
508
605
|
|
|
606
|
+
// CreateWindowExW dispatched WM_NCCALCSIZE before the window was registered,
|
|
607
|
+
// so the frameless top-frame reclaim (windowProc) couldn't run yet. Force a
|
|
608
|
+
// frame recalc now that findWindowByHwnd can resolve it.
|
|
609
|
+
if (host->title_bar_style == L"hidden" || host->title_bar_style == L"hiddenInset") {
|
|
610
|
+
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
|
611
|
+
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
612
|
+
}
|
|
613
|
+
|
|
509
614
|
if (maximized) ShowWindow(hwnd, SW_MAXIMIZE);
|
|
510
615
|
else if (minimized) ShowWindow(hwnd, SW_MINIMIZE);
|
|
511
616
|
else if (!hidden) ShowWindow(hwnd, SW_SHOW);
|
|
@@ -634,15 +739,42 @@ static void wireView(ViewHost* view, std::function<void()> on_attached) {
|
|
|
634
739
|
// - main frame only (matching CEF's OnContextCreated main-frame gate)
|
|
635
740
|
// - origin allowlist when `preload_origins` is non-empty
|
|
636
741
|
// Empty allowlist = inject on every main frame (CEF parity default).
|
|
637
|
-
|
|
742
|
+
//
|
|
743
|
+
// AddScriptToExecuteOnDocumentCreated is async — registration must
|
|
744
|
+
// complete BEFORE Navigate() or the first document load runs without
|
|
745
|
+
// the preload. Defer Navigate into the completion handler.
|
|
746
|
+
const bool inject = !v->preload_script.empty() && v->webview;
|
|
747
|
+
auto doInitialNav = [v]() {
|
|
748
|
+
if (!v->url.empty()) {
|
|
749
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu navigate-call url=%.200s%s",
|
|
750
|
+
v->id, static_cast<unsigned long long>(GetTickCount64()),
|
|
751
|
+
v->url.c_str(), v->url.size() > 200 ? "..." : "");
|
|
752
|
+
v->webview->Navigate(utf8ToWide(v->url).c_str());
|
|
753
|
+
} else if (!v->html.empty()) {
|
|
754
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu navigate-tostring-call",
|
|
755
|
+
v->id, static_cast<unsigned long long>(GetTickCount64()));
|
|
756
|
+
v->webview->NavigateToString(utf8ToWide(v->html).c_str());
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
if (inject) {
|
|
638
760
|
std::string allowlist = "[";
|
|
639
761
|
for (size_t i = 0; i < v->preload_origins.size(); ++i) {
|
|
640
762
|
if (i) allowlist += ",";
|
|
641
763
|
allowlist += "\"" + escapeJsonString(v->preload_origins[i]) + "\"";
|
|
642
764
|
}
|
|
643
765
|
allowlist += "]";
|
|
766
|
+
// Diagnostic IIFE-start/end markers, gated by log level. Leading
|
|
767
|
+
// `;` defends against preload_script with no trailing semicolon.
|
|
768
|
+
const bool emitMarkers = buniteShouldLog(BuniteLogLevel::Info);
|
|
769
|
+
const char* markerStart = emitMarkers
|
|
770
|
+
? ";try{(self.chrome&&chrome.webview&&chrome.webview.postMessage)&&chrome.webview.postMessage('__bunite_preload_iife_start__');}catch(_){}"
|
|
771
|
+
: "";
|
|
772
|
+
const char* markerEnd = emitMarkers
|
|
773
|
+
? ";try{(self.chrome&&chrome.webview&&chrome.webview.postMessage)&&chrome.webview.postMessage('__bunite_preload_iife_end__');}catch(_){}"
|
|
774
|
+
: "";
|
|
644
775
|
std::string body =
|
|
645
|
-
"(function(){
|
|
776
|
+
"(function(){"
|
|
777
|
+
"if(window.self!==window.top)return;"
|
|
646
778
|
"var __a=" + allowlist +
|
|
647
779
|
",__o=location.origin;"
|
|
648
780
|
"if(__a.length){var __m=function(p,v){var i=0,j=0,s=-1,k=0,L=function(c){return c.charCodeAt(0)|32;};"
|
|
@@ -652,26 +784,32 @@ static void wireView(ViewHost* view, std::function<void()> on_attached) {
|
|
|
652
784
|
"while(i<p.length&&p[i]===\"*\")i++;return i===p.length;};"
|
|
653
785
|
"var __ok=false;for(var i=0;i<__a.length;i++){if(__m(__a[i],__o)){__ok=true;break;}}"
|
|
654
786
|
"if(!__ok)return;}"
|
|
655
|
-
+
|
|
787
|
+
+ markerStart
|
|
788
|
+
+ v->preload_script
|
|
789
|
+
+ markerEnd +
|
|
656
790
|
"})();";
|
|
657
791
|
std::wstring wpreload = utf8ToWide(body);
|
|
658
792
|
auto lt = lifetime;
|
|
793
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu addscript-request",
|
|
794
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()));
|
|
659
795
|
v->webview->AddScriptToExecuteOnDocumentCreated(
|
|
660
796
|
wpreload.c_str(),
|
|
661
797
|
Callback<ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>(
|
|
662
|
-
[lt, view_id](HRESULT, LPCWSTR id) -> HRESULT {
|
|
798
|
+
[lt, view_id, doInitialNav](HRESULT hr, LPCWSTR id) -> HRESULT {
|
|
663
799
|
if (!lt || !lt->alive.load()) return S_OK;
|
|
800
|
+
if (FAILED(hr)) {
|
|
801
|
+
BUNITE_ERROR("AddScriptToExecuteOnDocumentCreated failed hr=0x%08x", static_cast<unsigned>(hr));
|
|
802
|
+
}
|
|
664
803
|
ViewHost* vv = getView(view_id);
|
|
665
804
|
if (vv && id) vv->add_script_id = id;
|
|
805
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu addscript-completion hr=0x%08x",
|
|
806
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()),
|
|
807
|
+
static_cast<unsigned>(hr));
|
|
808
|
+
doInitialNav();
|
|
666
809
|
return S_OK;
|
|
667
810
|
}).Get());
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Initial navigation.
|
|
671
|
-
if (!v->url.empty()) {
|
|
672
|
-
v->webview->Navigate(utf8ToWide(v->url).c_str());
|
|
673
|
-
} else if (!v->html.empty()) {
|
|
674
|
-
v->webview->NavigateToString(utf8ToWide(v->html).c_str());
|
|
811
|
+
} else {
|
|
812
|
+
doInitialNav();
|
|
675
813
|
}
|
|
676
814
|
|
|
677
815
|
emitWebviewEvent(v->id, "view-ready");
|
|
@@ -755,6 +893,19 @@ void destroyView(uint32_t id) {
|
|
|
755
893
|
vs.erase(std::remove(vs.begin(), vs.end(), v), vs.end());
|
|
756
894
|
}
|
|
757
895
|
|
|
896
|
+
// Remove DevTools event-receiver tokens before tearing down the webview to
|
|
897
|
+
// avoid AVs on event delivery after controller release.
|
|
898
|
+
if (v->oopif_event_tokens_registered && v->webview) {
|
|
899
|
+
ComPtr<ICoreWebView2DevToolsProtocolEventReceiver> attached_r, detached_r;
|
|
900
|
+
if (SUCCEEDED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.attachedToTarget", &attached_r))) {
|
|
901
|
+
attached_r->remove_DevToolsProtocolEventReceived(v->target_attached_token);
|
|
902
|
+
}
|
|
903
|
+
if (SUCCEEDED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.detachedFromTarget", &detached_r))) {
|
|
904
|
+
detached_r->remove_DevToolsProtocolEventReceived(v->target_detached_token);
|
|
905
|
+
}
|
|
906
|
+
v->oopif_event_tokens_registered = false;
|
|
907
|
+
}
|
|
908
|
+
|
|
758
909
|
// Defer Close() → container destroy → delete across three pump ticks. Edge
|
|
759
910
|
// gets at least one tick after Close() to settle before its parent HWND
|
|
760
911
|
// vanishes; see shutdownRuntime's staged drains for the same reason.
|
|
@@ -819,6 +970,9 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
819
970
|
args->get_Uri(&uri_raw);
|
|
820
971
|
std::string url = wideToUtf8(uri_raw);
|
|
821
972
|
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
973
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu nav-starting url=%.200s%s",
|
|
974
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()),
|
|
975
|
+
url.c_str(), url.size() > 200 ? "..." : "");
|
|
822
976
|
emitWebviewEvent(v->id, "will-navigate", url);
|
|
823
977
|
if (!shouldAllowNavigation(v, url)) {
|
|
824
978
|
args->put_Cancel(TRUE);
|
|
@@ -859,6 +1013,9 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
859
1013
|
args->get_IsSuccess(&ok);
|
|
860
1014
|
UINT64 nav_id = 0;
|
|
861
1015
|
args->get_NavigationId(&nav_id);
|
|
1016
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu nav-completed ok=%d nav_id=%llu",
|
|
1017
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()),
|
|
1018
|
+
ok ? 1 : 0, static_cast<unsigned long long>(nav_id));
|
|
862
1019
|
std::string url;
|
|
863
1020
|
auto it = g_nav_uris.find(navKey(view_id, nav_id));
|
|
864
1021
|
if (it != g_nav_uris.end()) {
|
|
@@ -884,9 +1041,18 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
884
1041
|
}).Get(),
|
|
885
1042
|
&tok);
|
|
886
1043
|
|
|
1044
|
+
// Disable default WV2 dialog UI so ScriptDialogOpening drives all dialogs.
|
|
1045
|
+
// NB: NonClientRegionSupport (app-region:drag) is intentionally NOT enabled —
|
|
1046
|
+
// its native window-move runs a modal loop on the shared Bun/UI thread and
|
|
1047
|
+
// freezes JS for the whole drag (spike-confirmed). WV2 drag stays capture-based.
|
|
1048
|
+
ComPtr<ICoreWebView2Settings> settings;
|
|
1049
|
+
if (SUCCEEDED(view->webview->get_Settings(&settings)) && settings) {
|
|
1050
|
+
settings->put_AreDefaultScriptDialogsEnabled(FALSE);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
887
1053
|
// ScriptDialogOpening — alert / confirm / prompt / beforeunload. Defer the
|
|
888
1054
|
// event so host can decide via `respondToDialog`.
|
|
889
|
-
view->webview->add_ScriptDialogOpening(
|
|
1055
|
+
HRESULT dlg_hr = view->webview->add_ScriptDialogOpening(
|
|
890
1056
|
Callback<ICoreWebView2ScriptDialogOpeningEventHandler>(
|
|
891
1057
|
[lifetime, view_id](ICoreWebView2*, ICoreWebView2ScriptDialogOpeningEventArgs* args) -> HRESULT {
|
|
892
1058
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
@@ -910,6 +1076,8 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
910
1076
|
: "alert";
|
|
911
1077
|
const uint32_t rid = v->next_dialog_request_id++;
|
|
912
1078
|
v->pending_dialogs[rid] = ViewHost::PendingDialog{ args, std::move(deferral) };
|
|
1079
|
+
BUNITE_INFO("webview2/dialog: ScriptDialogOpening view=%u kind=%s rid=%u",
|
|
1080
|
+
view_id, kind_str, rid);
|
|
913
1081
|
std::string payload = "{\"requestId\":" + std::to_string(rid) +
|
|
914
1082
|
",\"kind\":\"" + kind_str +
|
|
915
1083
|
"\",\"message\":\"" + escapeJsonString(message) + "\"";
|
|
@@ -921,6 +1089,31 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
921
1089
|
return S_OK;
|
|
922
1090
|
}).Get(),
|
|
923
1091
|
&tok);
|
|
1092
|
+
BUNITE_INFO("webview2/dialog: add_ScriptDialogOpening view=%u hr=0x%08x token=%lld",
|
|
1093
|
+
view_id, static_cast<unsigned>(dlg_hr), static_cast<long long>(tok.value));
|
|
1094
|
+
|
|
1095
|
+
// Capture preload IIFE markers. Production IPC is on the encrypted WS, so
|
|
1096
|
+
// skip registering entirely when markers aren't emitted.
|
|
1097
|
+
if (buniteShouldLog(BuniteLogLevel::Info)) {
|
|
1098
|
+
view->webview->add_WebMessageReceived(
|
|
1099
|
+
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
|
|
1100
|
+
[lifetime, view_id](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT {
|
|
1101
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
1102
|
+
LPWSTR raw = nullptr;
|
|
1103
|
+
if (FAILED(args->TryGetWebMessageAsString(&raw)) || !raw) return S_OK;
|
|
1104
|
+
std::string s = wideToUtf8(raw);
|
|
1105
|
+
CoTaskMemFree(raw);
|
|
1106
|
+
if (s == "__bunite_preload_iife_start__") {
|
|
1107
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu preload-iife-start",
|
|
1108
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()));
|
|
1109
|
+
} else if (s == "__bunite_preload_iife_end__") {
|
|
1110
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu preload-iife-end",
|
|
1111
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()));
|
|
1112
|
+
}
|
|
1113
|
+
return S_OK;
|
|
1114
|
+
}).Get(),
|
|
1115
|
+
&tok);
|
|
1116
|
+
}
|
|
924
1117
|
|
|
925
1118
|
// DocumentTitleChanged — surface for automation surfaceEvents title-change arm.
|
|
926
1119
|
view->webview->add_DocumentTitleChanged(
|
|
@@ -969,19 +1162,87 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
969
1162
|
}).Get(),
|
|
970
1163
|
&tok);
|
|
971
1164
|
|
|
972
|
-
// NewWindowRequested —
|
|
973
|
-
//
|
|
1165
|
+
// NewWindowRequested — eager-mint a popup ViewHost with the requested
|
|
1166
|
+
// CoreWebView2 (preserves window.opener) and emit `popup-requested`. Host
|
|
1167
|
+
// adopts via `bunite_view_popup_accept` or rejects via `bunite_view_popup_dismiss`;
|
|
1168
|
+
// SurfaceManager arms a 5s timer for the auto-dismiss safety net.
|
|
974
1169
|
view->webview->add_NewWindowRequested(
|
|
975
1170
|
Callback<ICoreWebView2NewWindowRequestedEventHandler>(
|
|
976
|
-
[lifetime, view_id](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs*
|
|
1171
|
+
[lifetime, view_id](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args_raw) -> HRESULT {
|
|
977
1172
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
978
|
-
args
|
|
1173
|
+
ComPtr<ICoreWebView2NewWindowRequestedEventArgs> args(args_raw);
|
|
979
1174
|
LPWSTR uri_raw = nullptr;
|
|
980
1175
|
args->get_Uri(&uri_raw);
|
|
981
1176
|
std::string url = wideToUtf8(uri_raw);
|
|
982
1177
|
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
983
|
-
|
|
984
|
-
|
|
1178
|
+
ComPtr<ICoreWebView2Deferral> deferral;
|
|
1179
|
+
args->GetDeferral(&deferral);
|
|
1180
|
+
args->put_Handled(TRUE);
|
|
1181
|
+
// Popup IDs live in the upper u32 half; TS allocator stays below.
|
|
1182
|
+
static std::atomic<uint32_t> g_popup_seq{0x80000000u};
|
|
1183
|
+
uint32_t new_view_id = g_popup_seq.fetch_add(1);
|
|
1184
|
+
auto* popup_raw = new ViewHost();
|
|
1185
|
+
popup_raw->id = new_view_id;
|
|
1186
|
+
popup_raw->window = nullptr;
|
|
1187
|
+
popup_raw->bounds = RECT{0, 0, 0, 0};
|
|
1188
|
+
popup_raw->auto_resize = false;
|
|
1189
|
+
popup_raw->container_hwnd = CreateWindowExW(
|
|
1190
|
+
0, kViewContainerClass, L"", WS_CHILD | WS_CLIPCHILDREN,
|
|
1191
|
+
0, 0, 0, 0, g_runtime.popup_parent,
|
|
1192
|
+
nullptr, getCurrentModuleHandle(), nullptr);
|
|
1193
|
+
{
|
|
1194
|
+
std::lock_guard<std::mutex> g(g_runtime.object_mutex);
|
|
1195
|
+
g_runtime.views_by_id[new_view_id] = popup_raw;
|
|
1196
|
+
}
|
|
1197
|
+
auto cleanupPopup = [new_view_id]() {
|
|
1198
|
+
auto* p = getView(new_view_id);
|
|
1199
|
+
if (!p) return;
|
|
1200
|
+
if (p->container_hwnd) DestroyWindow(p->container_hwnd);
|
|
1201
|
+
{
|
|
1202
|
+
std::lock_guard<std::mutex> g(g_runtime.object_mutex);
|
|
1203
|
+
g_runtime.views_by_id.erase(new_view_id);
|
|
1204
|
+
}
|
|
1205
|
+
delete p;
|
|
1206
|
+
};
|
|
1207
|
+
HRESULT sync_hr = g_runtime.env->CreateCoreWebView2Controller(
|
|
1208
|
+
popup_raw->container_hwnd,
|
|
1209
|
+
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
|
|
1210
|
+
[lifetime, view_id, new_view_id, url, args, deferral, cleanupPopup](HRESULT hr, ICoreWebView2Controller* controller) -> HRESULT {
|
|
1211
|
+
if (!lifetime || !lifetime->alive.load()) {
|
|
1212
|
+
if (deferral) deferral->Complete();
|
|
1213
|
+
if (controller) controller->Close(); // close the orphan controller too.
|
|
1214
|
+
cleanupPopup();
|
|
1215
|
+
return S_OK;
|
|
1216
|
+
}
|
|
1217
|
+
auto* popup = getView(new_view_id);
|
|
1218
|
+
if (!popup || FAILED(hr) || !controller) {
|
|
1219
|
+
if (deferral) deferral->Complete();
|
|
1220
|
+
if (controller) controller->Close(); // popup was dismissed during creation — close the orphan.
|
|
1221
|
+
cleanupPopup();
|
|
1222
|
+
return S_OK;
|
|
1223
|
+
}
|
|
1224
|
+
popup->controller = controller;
|
|
1225
|
+
controller->get_CoreWebView2(&popup->webview);
|
|
1226
|
+
if (popup->webview) {
|
|
1227
|
+
args->put_NewWindow(popup->webview.Get());
|
|
1228
|
+
}
|
|
1229
|
+
controller->put_IsVisible(FALSE);
|
|
1230
|
+
RECT zero{0,0,0,0};
|
|
1231
|
+
controller->put_Bounds(zero);
|
|
1232
|
+
attachControllerCallbacks(popup);
|
|
1233
|
+
attachAppResFilter(popup);
|
|
1234
|
+
popup->ready.store(true);
|
|
1235
|
+
if (deferral) deferral->Complete();
|
|
1236
|
+
std::string payload = "{\"newSurfaceId\":" + std::to_string(new_view_id) +
|
|
1237
|
+
",\"url\":\"" + escapeJsonString(url) +
|
|
1238
|
+
"\",\"disposition\":\"popup\"}";
|
|
1239
|
+
emitWebviewEvent(view_id, "popup-requested", payload);
|
|
1240
|
+
return S_OK;
|
|
1241
|
+
}).Get());
|
|
1242
|
+
if (FAILED(sync_hr)) {
|
|
1243
|
+
if (deferral) deferral->Complete();
|
|
1244
|
+
cleanupPopup();
|
|
1245
|
+
}
|
|
985
1246
|
return S_OK;
|
|
986
1247
|
}).Get(),
|
|
987
1248
|
&tok);
|
|
@@ -997,13 +1258,15 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
997
1258
|
}).Get(),
|
|
998
1259
|
&tok);
|
|
999
1260
|
|
|
1000
|
-
// DownloadStarting (ICoreWebView2_4) —
|
|
1261
|
+
// DownloadStarting (ICoreWebView2_4) — policy-driven: block | auto | ask.
|
|
1262
|
+
// Default block preserves the original behavior.
|
|
1001
1263
|
ComPtr<ICoreWebView2_4> wv4;
|
|
1002
1264
|
view->webview->QueryInterface(IID_PPV_ARGS(&wv4));
|
|
1003
1265
|
if (wv4) {
|
|
1266
|
+
static std::atomic<uint32_t> g_download_seq{1};
|
|
1004
1267
|
wv4->add_DownloadStarting(
|
|
1005
1268
|
Callback<ICoreWebView2DownloadStartingEventHandler>(
|
|
1006
|
-
[lifetime, view_id](ICoreWebView2*, ICoreWebView2DownloadStartingEventArgs* args) -> HRESULT {
|
|
1269
|
+
[lifetime, view_id, view](ICoreWebView2*, ICoreWebView2DownloadStartingEventArgs* args) -> HRESULT {
|
|
1007
1270
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
1008
1271
|
ComPtr<ICoreWebView2DownloadOperation> op;
|
|
1009
1272
|
args->get_DownloadOperation(&op);
|
|
@@ -1011,9 +1274,98 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
1011
1274
|
if (op) op->get_Uri(&uri_raw);
|
|
1012
1275
|
std::string url = wideToUtf8(uri_raw);
|
|
1013
1276
|
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
1014
|
-
|
|
1015
|
-
std::string
|
|
1016
|
-
|
|
1277
|
+
int32_t policy = view->download_policy.load();
|
|
1278
|
+
const std::string id = "wv2-" + std::to_string(g_download_seq.fetch_add(1));
|
|
1279
|
+
// Only policy=0 (auto) allows. `ask` (1) falls back to block
|
|
1280
|
+
// until implemented — distinguish via blocked.reason.
|
|
1281
|
+
if (policy != 0) {
|
|
1282
|
+
args->put_Cancel(TRUE);
|
|
1283
|
+
const char* reason = (policy == 1) ? "ask-not-implemented" : "host-policy";
|
|
1284
|
+
std::string payload = "{\"kind\":\"blocked\",\"id\":\"" + id +
|
|
1285
|
+
"\",\"url\":\"" + escapeJsonString(url) +
|
|
1286
|
+
"\",\"reason\":\"" + reason + "\"}";
|
|
1287
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
1288
|
+
return S_OK;
|
|
1289
|
+
}
|
|
1290
|
+
// auto: don't cancel; report started + progress + completed.
|
|
1291
|
+
LPWSTR sugg_raw = nullptr;
|
|
1292
|
+
if (op) op->get_ContentDisposition(&sugg_raw); // fallback
|
|
1293
|
+
LPWSTR result_path_raw = nullptr;
|
|
1294
|
+
args->get_ResultFilePath(&result_path_raw);
|
|
1295
|
+
std::string suggested = result_path_raw ? wideToUtf8(result_path_raw) : "";
|
|
1296
|
+
// strip dir, keep filename only.
|
|
1297
|
+
auto slash = suggested.find_last_of("/\\");
|
|
1298
|
+
if (slash != std::string::npos) suggested = suggested.substr(slash + 1);
|
|
1299
|
+
if (sugg_raw) CoTaskMemFree(sugg_raw);
|
|
1300
|
+
// Optional host downloadDir override.
|
|
1301
|
+
std::string overrideDir = view->download_dir;
|
|
1302
|
+
if (!overrideDir.empty() && result_path_raw) {
|
|
1303
|
+
std::string base = suggested.empty() ? "download" : suggested;
|
|
1304
|
+
std::string custom = overrideDir;
|
|
1305
|
+
if (!custom.empty() && custom.back() != '\\' && custom.back() != '/') custom.push_back('\\');
|
|
1306
|
+
custom += base;
|
|
1307
|
+
args->put_ResultFilePath(utf8ToWide(custom).c_str());
|
|
1308
|
+
}
|
|
1309
|
+
if (result_path_raw) CoTaskMemFree(result_path_raw);
|
|
1310
|
+
int64_t total = 0;
|
|
1311
|
+
if (op) op->get_TotalBytesToReceive(&total);
|
|
1312
|
+
std::string startPayload = "{\"kind\":\"started\",\"id\":\"" + id +
|
|
1313
|
+
"\",\"url\":\"" + escapeJsonString(url) +
|
|
1314
|
+
"\",\"suggestedFilename\":\"" + escapeJsonString(suggested) + "\"";
|
|
1315
|
+
if (total > 0) startPayload += ",\"sizeBytes\":" + std::to_string(total);
|
|
1316
|
+
startPayload += "}";
|
|
1317
|
+
emitWebviewEvent(view_id, "download-event", startPayload);
|
|
1318
|
+
if (op) {
|
|
1319
|
+
// Tokens kept in a shared struct so the StateChanged terminal
|
|
1320
|
+
// branch can self-unregister both handlers — breaks the
|
|
1321
|
+
// op→handler→op ComPtr ref cycle that otherwise leaks per download.
|
|
1322
|
+
struct Tokens { EventRegistrationToken b{}; EventRegistrationToken s{}; };
|
|
1323
|
+
auto toks = std::make_shared<Tokens>();
|
|
1324
|
+
op->add_BytesReceivedChanged(
|
|
1325
|
+
Callback<ICoreWebView2BytesReceivedChangedEventHandler>(
|
|
1326
|
+
[lifetime, view_id, id, op](ICoreWebView2DownloadOperation*, IUnknown*) -> HRESULT {
|
|
1327
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
1328
|
+
INT64 rec = 0; op->get_BytesReceived(&rec);
|
|
1329
|
+
INT64 tot = 0; op->get_TotalBytesToReceive(&tot);
|
|
1330
|
+
std::string payload = "{\"kind\":\"progress\",\"id\":\"" + id +
|
|
1331
|
+
"\",\"receivedBytes\":" + std::to_string(rec);
|
|
1332
|
+
if (tot > 0) payload += ",\"totalBytes\":" + std::to_string(tot);
|
|
1333
|
+
payload += "}";
|
|
1334
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
1335
|
+
return S_OK;
|
|
1336
|
+
}).Get(),
|
|
1337
|
+
&toks->b);
|
|
1338
|
+
op->add_StateChanged(
|
|
1339
|
+
Callback<ICoreWebView2StateChangedEventHandler>(
|
|
1340
|
+
[lifetime, view_id, id, op, toks](ICoreWebView2DownloadOperation*, IUnknown*) -> HRESULT {
|
|
1341
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
1342
|
+
COREWEBVIEW2_DOWNLOAD_STATE state;
|
|
1343
|
+
op->get_State(&state);
|
|
1344
|
+
bool terminal = false;
|
|
1345
|
+
if (state == COREWEBVIEW2_DOWNLOAD_STATE_COMPLETED) {
|
|
1346
|
+
terminal = true;
|
|
1347
|
+
LPWSTR path_raw = nullptr; op->get_ResultFilePath(&path_raw);
|
|
1348
|
+
std::string path = wideToUtf8(path_raw);
|
|
1349
|
+
if (path_raw) CoTaskMemFree(path_raw);
|
|
1350
|
+
std::string payload = "{\"kind\":\"completed\",\"id\":\"" + id +
|
|
1351
|
+
"\",\"localPath\":\"" + escapeJsonString(path) + "\"}";
|
|
1352
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
1353
|
+
} else if (state == COREWEBVIEW2_DOWNLOAD_STATE_INTERRUPTED) {
|
|
1354
|
+
terminal = true;
|
|
1355
|
+
COREWEBVIEW2_DOWNLOAD_INTERRUPT_REASON reason;
|
|
1356
|
+
op->get_InterruptReason(&reason);
|
|
1357
|
+
std::string payload = "{\"kind\":\"failed\",\"id\":\"" + id +
|
|
1358
|
+
"\",\"reason\":\"interrupted-" + std::to_string(reason) + "\"}";
|
|
1359
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
1360
|
+
}
|
|
1361
|
+
if (terminal) {
|
|
1362
|
+
op->remove_BytesReceivedChanged(toks->b);
|
|
1363
|
+
op->remove_StateChanged(toks->s);
|
|
1364
|
+
}
|
|
1365
|
+
return S_OK;
|
|
1366
|
+
}).Get(),
|
|
1367
|
+
&toks->s);
|
|
1368
|
+
}
|
|
1017
1369
|
return S_OK;
|
|
1018
1370
|
}).Get(),
|
|
1019
1371
|
&tok);
|