bunite-core 0.12.1 → 0.16.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 +19 -2
- package/src/host/core/BrowserView.ts +515 -38
- package/src/host/core/SurfaceBrowserIPC.ts +53 -3
- package/src/host/core/SurfaceManager.ts +603 -30
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +25 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +263 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +427 -6
- package/src/native/linux/bunite_linux_internal.h +18 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +296 -5
- package/src/native/mac/bunite_mac_ffi.mm +630 -8
- package/src/native/mac/bunite_mac_internal.h +19 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +371 -9
- package/src/native/shared/ffi_exports.h +200 -2
- package/src/native/win/native_host_cef.cpp +186 -11
- package/src/native/win/native_host_ffi.cpp +1194 -1
- package/src/native/win/native_host_internal.h +35 -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 +1023 -12
- package/src/native/win-webview2/webview2_internal.h +25 -0
- package/src/native/win-webview2/webview2_runtime.cpp +403 -34
- 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 +340 -8
- package/src/rpc/index.ts +32 -0
- package/src/webview/native.ts +253 -51
- package/src/webview/polyfill.ts +283 -22
|
@@ -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,28 @@ struct ViewHost {
|
|
|
86
87
|
|
|
87
88
|
std::atomic<bool> ready{false};
|
|
88
89
|
std::atomic<bool> closing{false};
|
|
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
|
+
|
|
95
|
+
// Pending page-initiated dialogs (alert/confirm/prompt/beforeunload).
|
|
96
|
+
// ScriptDialogOpening hands us a `Deferral` we Complete() on host response.
|
|
97
|
+
struct PendingDialog {
|
|
98
|
+
ComPtr<ICoreWebView2ScriptDialogOpeningEventArgs> args;
|
|
99
|
+
ComPtr<ICoreWebView2Deferral> deferral;
|
|
100
|
+
};
|
|
101
|
+
std::unordered_map<uint32_t, PendingDialog> pending_dialogs;
|
|
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;
|
|
89
112
|
};
|
|
90
113
|
|
|
91
114
|
struct WindowHost {
|
|
@@ -106,6 +129,7 @@ struct RuntimeState {
|
|
|
106
129
|
std::atomic<bool> initialized{false};
|
|
107
130
|
std::atomic<bool> shutting_down{false};
|
|
108
131
|
HWND message_window = nullptr;
|
|
132
|
+
HWND popup_parent = nullptr; // hidden top-level parking parent for popup-minted controllers.
|
|
109
133
|
|
|
110
134
|
std::mutex task_mutex;
|
|
111
135
|
std::deque<std::function<void()>> queued_tasks;
|
|
@@ -205,6 +229,7 @@ std::wstring exeDir();
|
|
|
205
229
|
uint32_t permissionKindToBuniteBit(COREWEBVIEW2_PERMISSION_KIND kind);
|
|
206
230
|
COREWEBVIEW2_PERMISSION_STATE buniteStateToWebView2(uint32_t state);
|
|
207
231
|
|
|
232
|
+
bool shouldAlwaysAllowNavigationUrl(const std::string& url);
|
|
208
233
|
bool shouldAllowNavigation(const ViewHost* view, const std::string& url);
|
|
209
234
|
|
|
210
235
|
} // 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,28 @@ 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
|
+
// 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
|
+
|
|
144
|
+
// `bun run` passes STARTF_USESHOWWINDOW + SW_HIDE; Win documented behavior
|
|
145
|
+
// is for the first ShowWindow call to use STARTUPINFO.wShowWindow instead
|
|
146
|
+
// of the requested nCmdShow. Consume it here on the message window so the
|
|
147
|
+
// first user-visible window's ShowWindow honors its argument.
|
|
148
|
+
STARTUPINFOW si{};
|
|
149
|
+
si.cb = sizeof(si);
|
|
150
|
+
GetStartupInfoW(&si);
|
|
151
|
+
if ((si.dwFlags & STARTF_USESHOWWINDOW) && si.wShowWindow == SW_HIDE) {
|
|
152
|
+
ShowWindow(g_runtime.message_window, SW_HIDE);
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
125
155
|
}
|
|
126
156
|
|
|
127
157
|
// ---- environment bootstrap -----------------------------------------------
|
|
@@ -282,6 +312,19 @@ LRESULT CALLBACK messageProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
|
282
312
|
|
|
283
313
|
// ---- init / shutdown ----------------------------------------------------
|
|
284
314
|
|
|
315
|
+
// KILL_ON_JOB_CLOSE — Edge helpers die with bun.exe instead of holding the
|
|
316
|
+
// UDF SingletonLock. Handle leaked intentionally (kernel closes on exit).
|
|
317
|
+
static void reapChildrenOnExit() {
|
|
318
|
+
HANDLE job = CreateJobObjectW(nullptr, nullptr);
|
|
319
|
+
if (!job) return;
|
|
320
|
+
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{};
|
|
321
|
+
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
322
|
+
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info, sizeof(info)) ||
|
|
323
|
+
!AssignProcessToJobObject(job, GetCurrentProcess())) {
|
|
324
|
+
CloseHandle(job); // already in a non-nestable job, etc — give up.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
285
328
|
bool initRuntime(const char* engine_dir, bool /*hide_console*/,
|
|
286
329
|
bool popup_blocking, const char* engine_config_json) {
|
|
287
330
|
buniteApplyEnvLogLevel();
|
|
@@ -289,6 +332,8 @@ bool initRuntime(const char* engine_dir, bool /*hide_console*/,
|
|
|
289
332
|
(engine_dir && *engine_dir) ? engine_dir : "(null)");
|
|
290
333
|
if (g_runtime.initialized.load()) return true;
|
|
291
334
|
|
|
335
|
+
reapChildrenOnExit();
|
|
336
|
+
|
|
292
337
|
HRESULT co = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
|
293
338
|
if (SUCCEEDED(co)) g_co_initialized = true;
|
|
294
339
|
else if (co != RPC_E_CHANGED_MODE) {
|
|
@@ -598,15 +643,42 @@ static void wireView(ViewHost* view, std::function<void()> on_attached) {
|
|
|
598
643
|
// - main frame only (matching CEF's OnContextCreated main-frame gate)
|
|
599
644
|
// - origin allowlist when `preload_origins` is non-empty
|
|
600
645
|
// Empty allowlist = inject on every main frame (CEF parity default).
|
|
601
|
-
|
|
646
|
+
//
|
|
647
|
+
// AddScriptToExecuteOnDocumentCreated is async — registration must
|
|
648
|
+
// complete BEFORE Navigate() or the first document load runs without
|
|
649
|
+
// the preload. Defer Navigate into the completion handler.
|
|
650
|
+
const bool inject = !v->preload_script.empty() && v->webview;
|
|
651
|
+
auto doInitialNav = [v]() {
|
|
652
|
+
if (!v->url.empty()) {
|
|
653
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu navigate-call url=%.200s%s",
|
|
654
|
+
v->id, static_cast<unsigned long long>(GetTickCount64()),
|
|
655
|
+
v->url.c_str(), v->url.size() > 200 ? "..." : "");
|
|
656
|
+
v->webview->Navigate(utf8ToWide(v->url).c_str());
|
|
657
|
+
} else if (!v->html.empty()) {
|
|
658
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu navigate-tostring-call",
|
|
659
|
+
v->id, static_cast<unsigned long long>(GetTickCount64()));
|
|
660
|
+
v->webview->NavigateToString(utf8ToWide(v->html).c_str());
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
if (inject) {
|
|
602
664
|
std::string allowlist = "[";
|
|
603
665
|
for (size_t i = 0; i < v->preload_origins.size(); ++i) {
|
|
604
666
|
if (i) allowlist += ",";
|
|
605
667
|
allowlist += "\"" + escapeJsonString(v->preload_origins[i]) + "\"";
|
|
606
668
|
}
|
|
607
669
|
allowlist += "]";
|
|
670
|
+
// Diagnostic IIFE-start/end markers, gated by log level. Leading
|
|
671
|
+
// `;` defends against preload_script with no trailing semicolon.
|
|
672
|
+
const bool emitMarkers = buniteShouldLog(BuniteLogLevel::Info);
|
|
673
|
+
const char* markerStart = emitMarkers
|
|
674
|
+
? ";try{(self.chrome&&chrome.webview&&chrome.webview.postMessage)&&chrome.webview.postMessage('__bunite_preload_iife_start__');}catch(_){}"
|
|
675
|
+
: "";
|
|
676
|
+
const char* markerEnd = emitMarkers
|
|
677
|
+
? ";try{(self.chrome&&chrome.webview&&chrome.webview.postMessage)&&chrome.webview.postMessage('__bunite_preload_iife_end__');}catch(_){}"
|
|
678
|
+
: "";
|
|
608
679
|
std::string body =
|
|
609
|
-
"(function(){
|
|
680
|
+
"(function(){"
|
|
681
|
+
"if(window.self!==window.top)return;"
|
|
610
682
|
"var __a=" + allowlist +
|
|
611
683
|
",__o=location.origin;"
|
|
612
684
|
"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;};"
|
|
@@ -616,26 +688,32 @@ static void wireView(ViewHost* view, std::function<void()> on_attached) {
|
|
|
616
688
|
"while(i<p.length&&p[i]===\"*\")i++;return i===p.length;};"
|
|
617
689
|
"var __ok=false;for(var i=0;i<__a.length;i++){if(__m(__a[i],__o)){__ok=true;break;}}"
|
|
618
690
|
"if(!__ok)return;}"
|
|
619
|
-
+
|
|
691
|
+
+ markerStart
|
|
692
|
+
+ v->preload_script
|
|
693
|
+
+ markerEnd +
|
|
620
694
|
"})();";
|
|
621
695
|
std::wstring wpreload = utf8ToWide(body);
|
|
622
696
|
auto lt = lifetime;
|
|
697
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu addscript-request",
|
|
698
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()));
|
|
623
699
|
v->webview->AddScriptToExecuteOnDocumentCreated(
|
|
624
700
|
wpreload.c_str(),
|
|
625
701
|
Callback<ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>(
|
|
626
|
-
[lt, view_id](HRESULT, LPCWSTR id) -> HRESULT {
|
|
702
|
+
[lt, view_id, doInitialNav](HRESULT hr, LPCWSTR id) -> HRESULT {
|
|
627
703
|
if (!lt || !lt->alive.load()) return S_OK;
|
|
704
|
+
if (FAILED(hr)) {
|
|
705
|
+
BUNITE_ERROR("AddScriptToExecuteOnDocumentCreated failed hr=0x%08x", static_cast<unsigned>(hr));
|
|
706
|
+
}
|
|
628
707
|
ViewHost* vv = getView(view_id);
|
|
629
708
|
if (vv && id) vv->add_script_id = id;
|
|
709
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu addscript-completion hr=0x%08x",
|
|
710
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()),
|
|
711
|
+
static_cast<unsigned>(hr));
|
|
712
|
+
doInitialNav();
|
|
630
713
|
return S_OK;
|
|
631
714
|
}).Get());
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Initial navigation.
|
|
635
|
-
if (!v->url.empty()) {
|
|
636
|
-
v->webview->Navigate(utf8ToWide(v->url).c_str());
|
|
637
|
-
} else if (!v->html.empty()) {
|
|
638
|
-
v->webview->NavigateToString(utf8ToWide(v->html).c_str());
|
|
715
|
+
} else {
|
|
716
|
+
doInitialNav();
|
|
639
717
|
}
|
|
640
718
|
|
|
641
719
|
emitWebviewEvent(v->id, "view-ready");
|
|
@@ -719,6 +797,19 @@ void destroyView(uint32_t id) {
|
|
|
719
797
|
vs.erase(std::remove(vs.begin(), vs.end(), v), vs.end());
|
|
720
798
|
}
|
|
721
799
|
|
|
800
|
+
// Remove DevTools event-receiver tokens before tearing down the webview to
|
|
801
|
+
// avoid AVs on event delivery after controller release.
|
|
802
|
+
if (v->oopif_event_tokens_registered && v->webview) {
|
|
803
|
+
ComPtr<ICoreWebView2DevToolsProtocolEventReceiver> attached_r, detached_r;
|
|
804
|
+
if (SUCCEEDED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.attachedToTarget", &attached_r))) {
|
|
805
|
+
attached_r->remove_DevToolsProtocolEventReceived(v->target_attached_token);
|
|
806
|
+
}
|
|
807
|
+
if (SUCCEEDED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.detachedFromTarget", &detached_r))) {
|
|
808
|
+
detached_r->remove_DevToolsProtocolEventReceived(v->target_detached_token);
|
|
809
|
+
}
|
|
810
|
+
v->oopif_event_tokens_registered = false;
|
|
811
|
+
}
|
|
812
|
+
|
|
722
813
|
// Defer Close() → container destroy → delete across three pump ticks. Edge
|
|
723
814
|
// gets at least one tick after Close() to settle before its parent HWND
|
|
724
815
|
// vanishes; see shutdownRuntime's staged drains for the same reason.
|
|
@@ -766,9 +857,13 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
766
857
|
if (!view->webview) return;
|
|
767
858
|
auto lifetime = g_runtime.lifetime;
|
|
768
859
|
uint32_t view_id = view->id;
|
|
860
|
+
// Token reuse OK — controller->Close() releases all add_* handlers.
|
|
769
861
|
EventRegistrationToken tok;
|
|
770
862
|
|
|
771
|
-
// NavigationStarting —
|
|
863
|
+
// NavigationStarting — emit "will-navigate" (parity with CEF/mac/linux,
|
|
864
|
+
// which fire regardless of allow), then cancel if nav rules say block.
|
|
865
|
+
// Also emit "load-start" for the surfaceEvents stream + stash URI for
|
|
866
|
+
// NavigationCompleted lookup (failure case: get_Source() returns prior URL).
|
|
772
867
|
view->webview->add_NavigationStarting(
|
|
773
868
|
Callback<ICoreWebView2NavigationStartingEventHandler>(
|
|
774
869
|
[lifetime, view_id](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
|
|
@@ -779,34 +874,149 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
779
874
|
args->get_Uri(&uri_raw);
|
|
780
875
|
std::string url = wideToUtf8(uri_raw);
|
|
781
876
|
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
877
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu nav-starting url=%.200s%s",
|
|
878
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()),
|
|
879
|
+
url.c_str(), url.size() > 200 ? "..." : "");
|
|
880
|
+
emitWebviewEvent(v->id, "will-navigate", url);
|
|
782
881
|
if (!shouldAllowNavigation(v, url)) {
|
|
783
882
|
args->put_Cancel(TRUE);
|
|
784
883
|
return S_OK;
|
|
785
884
|
}
|
|
786
|
-
|
|
885
|
+
UINT64 nav_id = 0;
|
|
886
|
+
args->get_NavigationId(&nav_id);
|
|
887
|
+
g_nav_uris[navKey(view_id, nav_id)] = url;
|
|
888
|
+
emitWebviewEvent(v->id, "load-start", url);
|
|
787
889
|
return S_OK;
|
|
788
890
|
}).Get(),
|
|
789
891
|
&tok);
|
|
790
892
|
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
893
|
+
// SourceChanged — URL commit point; map to did-navigate (surfaceEvents
|
|
894
|
+
// `navigate` arm). Distinct from NavigationCompleted which fires later.
|
|
895
|
+
view->webview->add_SourceChanged(
|
|
896
|
+
Callback<ICoreWebView2SourceChangedEventHandler>(
|
|
897
|
+
[lifetime, view_id](ICoreWebView2* wv, ICoreWebView2SourceChangedEventArgs*) -> HRESULT {
|
|
795
898
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
796
|
-
BOOL ok = FALSE;
|
|
797
|
-
args->get_IsSuccess(&ok);
|
|
798
|
-
if (!ok) return S_OK;
|
|
799
899
|
LPWSTR src_raw = nullptr;
|
|
800
900
|
if (wv) wv->get_Source(&src_raw);
|
|
801
901
|
std::string url = wideToUtf8(src_raw);
|
|
802
902
|
if (src_raw) CoTaskMemFree(src_raw);
|
|
803
903
|
emitWebviewEvent(view_id, "did-navigate", url);
|
|
804
|
-
emitWebviewEvent(view_id, "dom-ready", url);
|
|
805
904
|
return S_OK;
|
|
806
905
|
}).Get(),
|
|
807
906
|
&tok);
|
|
808
907
|
|
|
809
|
-
//
|
|
908
|
+
// NavigationCompleted — load lifecycle terminator. Success → load-finish
|
|
909
|
+
// + dom-ready; failure → load-fail with WebErrorStatus as reason. Use the
|
|
910
|
+
// URI we stashed at NavigationStarting — get_Source() returns the prior
|
|
911
|
+
// committed URL on provisional-navigation failure.
|
|
912
|
+
view->webview->add_NavigationCompleted(
|
|
913
|
+
Callback<ICoreWebView2NavigationCompletedEventHandler>(
|
|
914
|
+
[lifetime, view_id](ICoreWebView2* wv, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT {
|
|
915
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
916
|
+
BOOL ok = FALSE;
|
|
917
|
+
args->get_IsSuccess(&ok);
|
|
918
|
+
UINT64 nav_id = 0;
|
|
919
|
+
args->get_NavigationId(&nav_id);
|
|
920
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu nav-completed ok=%d nav_id=%llu",
|
|
921
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()),
|
|
922
|
+
ok ? 1 : 0, static_cast<unsigned long long>(nav_id));
|
|
923
|
+
std::string url;
|
|
924
|
+
auto it = g_nav_uris.find(navKey(view_id, nav_id));
|
|
925
|
+
if (it != g_nav_uris.end()) {
|
|
926
|
+
url = std::move(it->second);
|
|
927
|
+
g_nav_uris.erase(it);
|
|
928
|
+
} else {
|
|
929
|
+
LPWSTR src_raw = nullptr;
|
|
930
|
+
if (wv) wv->get_Source(&src_raw);
|
|
931
|
+
url = wideToUtf8(src_raw);
|
|
932
|
+
if (src_raw) CoTaskMemFree(src_raw);
|
|
933
|
+
}
|
|
934
|
+
if (ok) {
|
|
935
|
+
emitWebviewEvent(view_id, "load-finish", url);
|
|
936
|
+
emitWebviewEvent(view_id, "dom-ready", url);
|
|
937
|
+
} else {
|
|
938
|
+
COREWEBVIEW2_WEB_ERROR_STATUS status = COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN;
|
|
939
|
+
args->get_WebErrorStatus(&status);
|
|
940
|
+
std::string payload = "{\"url\":\"" + escapeJsonString(url) +
|
|
941
|
+
"\",\"reason\":\"WebErrorStatus_" + std::to_string(static_cast<int>(status)) + "\"}";
|
|
942
|
+
emitWebviewEvent(view_id, "load-fail", payload);
|
|
943
|
+
}
|
|
944
|
+
return S_OK;
|
|
945
|
+
}).Get(),
|
|
946
|
+
&tok);
|
|
947
|
+
|
|
948
|
+
// Disable default WV2 dialog UI so ScriptDialogOpening drives all dialogs.
|
|
949
|
+
ComPtr<ICoreWebView2Settings> settings;
|
|
950
|
+
if (SUCCEEDED(view->webview->get_Settings(&settings)) && settings) {
|
|
951
|
+
settings->put_AreDefaultScriptDialogsEnabled(FALSE);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ScriptDialogOpening — alert / confirm / prompt / beforeunload. Defer the
|
|
955
|
+
// event so host can decide via `respondToDialog`.
|
|
956
|
+
HRESULT dlg_hr = view->webview->add_ScriptDialogOpening(
|
|
957
|
+
Callback<ICoreWebView2ScriptDialogOpeningEventHandler>(
|
|
958
|
+
[lifetime, view_id](ICoreWebView2*, ICoreWebView2ScriptDialogOpeningEventArgs* args) -> HRESULT {
|
|
959
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
960
|
+
ViewHost* v = getView(view_id);
|
|
961
|
+
if (!v) return S_OK;
|
|
962
|
+
ComPtr<ICoreWebView2Deferral> deferral;
|
|
963
|
+
args->GetDeferral(&deferral);
|
|
964
|
+
COREWEBVIEW2_SCRIPT_DIALOG_KIND kind = COREWEBVIEW2_SCRIPT_DIALOG_KIND_ALERT;
|
|
965
|
+
args->get_Kind(&kind);
|
|
966
|
+
LPWSTR msg_raw = nullptr;
|
|
967
|
+
args->get_Message(&msg_raw);
|
|
968
|
+
std::string message = wideToUtf8(msg_raw);
|
|
969
|
+
if (msg_raw) CoTaskMemFree(msg_raw);
|
|
970
|
+
LPWSTR def_raw = nullptr;
|
|
971
|
+
args->get_DefaultText(&def_raw);
|
|
972
|
+
std::string default_prompt = wideToUtf8(def_raw);
|
|
973
|
+
if (def_raw) CoTaskMemFree(def_raw);
|
|
974
|
+
const char* kind_str = (kind == COREWEBVIEW2_SCRIPT_DIALOG_KIND_CONFIRM) ? "confirm"
|
|
975
|
+
: (kind == COREWEBVIEW2_SCRIPT_DIALOG_KIND_PROMPT) ? "prompt"
|
|
976
|
+
: (kind == COREWEBVIEW2_SCRIPT_DIALOG_KIND_BEFOREUNLOAD) ? "beforeunload"
|
|
977
|
+
: "alert";
|
|
978
|
+
const uint32_t rid = v->next_dialog_request_id++;
|
|
979
|
+
v->pending_dialogs[rid] = ViewHost::PendingDialog{ args, std::move(deferral) };
|
|
980
|
+
BUNITE_INFO("webview2/dialog: ScriptDialogOpening view=%u kind=%s rid=%u",
|
|
981
|
+
view_id, kind_str, rid);
|
|
982
|
+
std::string payload = "{\"requestId\":" + std::to_string(rid) +
|
|
983
|
+
",\"kind\":\"" + kind_str +
|
|
984
|
+
"\",\"message\":\"" + escapeJsonString(message) + "\"";
|
|
985
|
+
if (kind == COREWEBVIEW2_SCRIPT_DIALOG_KIND_PROMPT) {
|
|
986
|
+
payload += ",\"defaultPrompt\":\"" + escapeJsonString(default_prompt) + "\"";
|
|
987
|
+
}
|
|
988
|
+
payload += "}";
|
|
989
|
+
emitWebviewEvent(view_id, "dialog", payload);
|
|
990
|
+
return S_OK;
|
|
991
|
+
}).Get(),
|
|
992
|
+
&tok);
|
|
993
|
+
BUNITE_INFO("webview2/dialog: add_ScriptDialogOpening view=%u hr=0x%08x token=%lld",
|
|
994
|
+
view_id, static_cast<unsigned>(dlg_hr), static_cast<long long>(tok.value));
|
|
995
|
+
|
|
996
|
+
// Capture preload IIFE markers. Production IPC is on the encrypted WS, so
|
|
997
|
+
// skip registering entirely when markers aren't emitted.
|
|
998
|
+
if (buniteShouldLog(BuniteLogLevel::Info)) {
|
|
999
|
+
view->webview->add_WebMessageReceived(
|
|
1000
|
+
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
|
|
1001
|
+
[lifetime, view_id](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT {
|
|
1002
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
1003
|
+
LPWSTR raw = nullptr;
|
|
1004
|
+
if (FAILED(args->TryGetWebMessageAsString(&raw)) || !raw) return S_OK;
|
|
1005
|
+
std::string s = wideToUtf8(raw);
|
|
1006
|
+
CoTaskMemFree(raw);
|
|
1007
|
+
if (s == "__bunite_preload_iife_start__") {
|
|
1008
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu preload-iife-start",
|
|
1009
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()));
|
|
1010
|
+
} else if (s == "__bunite_preload_iife_end__") {
|
|
1011
|
+
BUNITE_INFO("webview2/preload-race: view=%u t=%llu preload-iife-end",
|
|
1012
|
+
view_id, static_cast<unsigned long long>(GetTickCount64()));
|
|
1013
|
+
}
|
|
1014
|
+
return S_OK;
|
|
1015
|
+
}).Get(),
|
|
1016
|
+
&tok);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// DocumentTitleChanged — surface for automation surfaceEvents title-change arm.
|
|
810
1020
|
view->webview->add_DocumentTitleChanged(
|
|
811
1021
|
Callback<ICoreWebView2DocumentTitleChangedEventHandler>(
|
|
812
1022
|
[lifetime, view_id](ICoreWebView2* wv, IUnknown*) -> HRESULT {
|
|
@@ -853,19 +1063,87 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
853
1063
|
}).Get(),
|
|
854
1064
|
&tok);
|
|
855
1065
|
|
|
856
|
-
// NewWindowRequested —
|
|
857
|
-
//
|
|
1066
|
+
// NewWindowRequested — eager-mint a popup ViewHost with the requested
|
|
1067
|
+
// CoreWebView2 (preserves window.opener) and emit `popup-requested`. Host
|
|
1068
|
+
// adopts via `bunite_view_popup_accept` or rejects via `bunite_view_popup_dismiss`;
|
|
1069
|
+
// SurfaceManager arms a 5s timer for the auto-dismiss safety net.
|
|
858
1070
|
view->webview->add_NewWindowRequested(
|
|
859
1071
|
Callback<ICoreWebView2NewWindowRequestedEventHandler>(
|
|
860
|
-
[lifetime, view_id](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs*
|
|
1072
|
+
[lifetime, view_id](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args_raw) -> HRESULT {
|
|
861
1073
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
862
|
-
args
|
|
1074
|
+
ComPtr<ICoreWebView2NewWindowRequestedEventArgs> args(args_raw);
|
|
863
1075
|
LPWSTR uri_raw = nullptr;
|
|
864
1076
|
args->get_Uri(&uri_raw);
|
|
865
1077
|
std::string url = wideToUtf8(uri_raw);
|
|
866
1078
|
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
867
|
-
|
|
868
|
-
|
|
1079
|
+
ComPtr<ICoreWebView2Deferral> deferral;
|
|
1080
|
+
args->GetDeferral(&deferral);
|
|
1081
|
+
args->put_Handled(TRUE);
|
|
1082
|
+
// Popup IDs live in the upper u32 half; TS allocator stays below.
|
|
1083
|
+
static std::atomic<uint32_t> g_popup_seq{0x80000000u};
|
|
1084
|
+
uint32_t new_view_id = g_popup_seq.fetch_add(1);
|
|
1085
|
+
auto* popup_raw = new ViewHost();
|
|
1086
|
+
popup_raw->id = new_view_id;
|
|
1087
|
+
popup_raw->window = nullptr;
|
|
1088
|
+
popup_raw->bounds = RECT{0, 0, 0, 0};
|
|
1089
|
+
popup_raw->auto_resize = false;
|
|
1090
|
+
popup_raw->container_hwnd = CreateWindowExW(
|
|
1091
|
+
0, kViewContainerClass, L"", WS_CHILD | WS_CLIPCHILDREN,
|
|
1092
|
+
0, 0, 0, 0, g_runtime.popup_parent,
|
|
1093
|
+
nullptr, getCurrentModuleHandle(), nullptr);
|
|
1094
|
+
{
|
|
1095
|
+
std::lock_guard<std::mutex> g(g_runtime.object_mutex);
|
|
1096
|
+
g_runtime.views_by_id[new_view_id] = popup_raw;
|
|
1097
|
+
}
|
|
1098
|
+
auto cleanupPopup = [new_view_id]() {
|
|
1099
|
+
auto* p = getView(new_view_id);
|
|
1100
|
+
if (!p) return;
|
|
1101
|
+
if (p->container_hwnd) DestroyWindow(p->container_hwnd);
|
|
1102
|
+
{
|
|
1103
|
+
std::lock_guard<std::mutex> g(g_runtime.object_mutex);
|
|
1104
|
+
g_runtime.views_by_id.erase(new_view_id);
|
|
1105
|
+
}
|
|
1106
|
+
delete p;
|
|
1107
|
+
};
|
|
1108
|
+
HRESULT sync_hr = g_runtime.env->CreateCoreWebView2Controller(
|
|
1109
|
+
popup_raw->container_hwnd,
|
|
1110
|
+
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
|
|
1111
|
+
[lifetime, view_id, new_view_id, url, args, deferral, cleanupPopup](HRESULT hr, ICoreWebView2Controller* controller) -> HRESULT {
|
|
1112
|
+
if (!lifetime || !lifetime->alive.load()) {
|
|
1113
|
+
if (deferral) deferral->Complete();
|
|
1114
|
+
if (controller) controller->Close(); // close the orphan controller too.
|
|
1115
|
+
cleanupPopup();
|
|
1116
|
+
return S_OK;
|
|
1117
|
+
}
|
|
1118
|
+
auto* popup = getView(new_view_id);
|
|
1119
|
+
if (!popup || FAILED(hr) || !controller) {
|
|
1120
|
+
if (deferral) deferral->Complete();
|
|
1121
|
+
if (controller) controller->Close(); // popup was dismissed during creation — close the orphan.
|
|
1122
|
+
cleanupPopup();
|
|
1123
|
+
return S_OK;
|
|
1124
|
+
}
|
|
1125
|
+
popup->controller = controller;
|
|
1126
|
+
controller->get_CoreWebView2(&popup->webview);
|
|
1127
|
+
if (popup->webview) {
|
|
1128
|
+
args->put_NewWindow(popup->webview.Get());
|
|
1129
|
+
}
|
|
1130
|
+
controller->put_IsVisible(FALSE);
|
|
1131
|
+
RECT zero{0,0,0,0};
|
|
1132
|
+
controller->put_Bounds(zero);
|
|
1133
|
+
attachControllerCallbacks(popup);
|
|
1134
|
+
attachAppResFilter(popup);
|
|
1135
|
+
popup->ready.store(true);
|
|
1136
|
+
if (deferral) deferral->Complete();
|
|
1137
|
+
std::string payload = "{\"newSurfaceId\":" + std::to_string(new_view_id) +
|
|
1138
|
+
",\"url\":\"" + escapeJsonString(url) +
|
|
1139
|
+
"\",\"disposition\":\"popup\"}";
|
|
1140
|
+
emitWebviewEvent(view_id, "popup-requested", payload);
|
|
1141
|
+
return S_OK;
|
|
1142
|
+
}).Get());
|
|
1143
|
+
if (FAILED(sync_hr)) {
|
|
1144
|
+
if (deferral) deferral->Complete();
|
|
1145
|
+
cleanupPopup();
|
|
1146
|
+
}
|
|
869
1147
|
return S_OK;
|
|
870
1148
|
}).Get(),
|
|
871
1149
|
&tok);
|
|
@@ -881,13 +1159,15 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
881
1159
|
}).Get(),
|
|
882
1160
|
&tok);
|
|
883
1161
|
|
|
884
|
-
// DownloadStarting (ICoreWebView2_4) —
|
|
1162
|
+
// DownloadStarting (ICoreWebView2_4) — policy-driven: block | auto | ask.
|
|
1163
|
+
// Default block preserves the original behavior.
|
|
885
1164
|
ComPtr<ICoreWebView2_4> wv4;
|
|
886
1165
|
view->webview->QueryInterface(IID_PPV_ARGS(&wv4));
|
|
887
1166
|
if (wv4) {
|
|
1167
|
+
static std::atomic<uint32_t> g_download_seq{1};
|
|
888
1168
|
wv4->add_DownloadStarting(
|
|
889
1169
|
Callback<ICoreWebView2DownloadStartingEventHandler>(
|
|
890
|
-
[lifetime, view_id](ICoreWebView2*, ICoreWebView2DownloadStartingEventArgs* args) -> HRESULT {
|
|
1170
|
+
[lifetime, view_id, view](ICoreWebView2*, ICoreWebView2DownloadStartingEventArgs* args) -> HRESULT {
|
|
891
1171
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
892
1172
|
ComPtr<ICoreWebView2DownloadOperation> op;
|
|
893
1173
|
args->get_DownloadOperation(&op);
|
|
@@ -895,9 +1175,98 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
895
1175
|
if (op) op->get_Uri(&uri_raw);
|
|
896
1176
|
std::string url = wideToUtf8(uri_raw);
|
|
897
1177
|
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
898
|
-
|
|
899
|
-
std::string
|
|
900
|
-
|
|
1178
|
+
int32_t policy = view->download_policy.load();
|
|
1179
|
+
const std::string id = "wv2-" + std::to_string(g_download_seq.fetch_add(1));
|
|
1180
|
+
// Only policy=0 (auto) allows. `ask` (1) falls back to block
|
|
1181
|
+
// until implemented — distinguish via blocked.reason.
|
|
1182
|
+
if (policy != 0) {
|
|
1183
|
+
args->put_Cancel(TRUE);
|
|
1184
|
+
const char* reason = (policy == 1) ? "ask-not-implemented" : "host-policy";
|
|
1185
|
+
std::string payload = "{\"kind\":\"blocked\",\"id\":\"" + id +
|
|
1186
|
+
"\",\"url\":\"" + escapeJsonString(url) +
|
|
1187
|
+
"\",\"reason\":\"" + reason + "\"}";
|
|
1188
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
1189
|
+
return S_OK;
|
|
1190
|
+
}
|
|
1191
|
+
// auto: don't cancel; report started + progress + completed.
|
|
1192
|
+
LPWSTR sugg_raw = nullptr;
|
|
1193
|
+
if (op) op->get_ContentDisposition(&sugg_raw); // fallback
|
|
1194
|
+
LPWSTR result_path_raw = nullptr;
|
|
1195
|
+
args->get_ResultFilePath(&result_path_raw);
|
|
1196
|
+
std::string suggested = result_path_raw ? wideToUtf8(result_path_raw) : "";
|
|
1197
|
+
// strip dir, keep filename only.
|
|
1198
|
+
auto slash = suggested.find_last_of("/\\");
|
|
1199
|
+
if (slash != std::string::npos) suggested = suggested.substr(slash + 1);
|
|
1200
|
+
if (sugg_raw) CoTaskMemFree(sugg_raw);
|
|
1201
|
+
// Optional host downloadDir override.
|
|
1202
|
+
std::string overrideDir = view->download_dir;
|
|
1203
|
+
if (!overrideDir.empty() && result_path_raw) {
|
|
1204
|
+
std::string base = suggested.empty() ? "download" : suggested;
|
|
1205
|
+
std::string custom = overrideDir;
|
|
1206
|
+
if (!custom.empty() && custom.back() != '\\' && custom.back() != '/') custom.push_back('\\');
|
|
1207
|
+
custom += base;
|
|
1208
|
+
args->put_ResultFilePath(utf8ToWide(custom).c_str());
|
|
1209
|
+
}
|
|
1210
|
+
if (result_path_raw) CoTaskMemFree(result_path_raw);
|
|
1211
|
+
int64_t total = 0;
|
|
1212
|
+
if (op) op->get_TotalBytesToReceive(&total);
|
|
1213
|
+
std::string startPayload = "{\"kind\":\"started\",\"id\":\"" + id +
|
|
1214
|
+
"\",\"url\":\"" + escapeJsonString(url) +
|
|
1215
|
+
"\",\"suggestedFilename\":\"" + escapeJsonString(suggested) + "\"";
|
|
1216
|
+
if (total > 0) startPayload += ",\"sizeBytes\":" + std::to_string(total);
|
|
1217
|
+
startPayload += "}";
|
|
1218
|
+
emitWebviewEvent(view_id, "download-event", startPayload);
|
|
1219
|
+
if (op) {
|
|
1220
|
+
// Tokens kept in a shared struct so the StateChanged terminal
|
|
1221
|
+
// branch can self-unregister both handlers — breaks the
|
|
1222
|
+
// op→handler→op ComPtr ref cycle that otherwise leaks per download.
|
|
1223
|
+
struct Tokens { EventRegistrationToken b{}; EventRegistrationToken s{}; };
|
|
1224
|
+
auto toks = std::make_shared<Tokens>();
|
|
1225
|
+
op->add_BytesReceivedChanged(
|
|
1226
|
+
Callback<ICoreWebView2BytesReceivedChangedEventHandler>(
|
|
1227
|
+
[lifetime, view_id, id, op](ICoreWebView2DownloadOperation*, IUnknown*) -> HRESULT {
|
|
1228
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
1229
|
+
INT64 rec = 0; op->get_BytesReceived(&rec);
|
|
1230
|
+
INT64 tot = 0; op->get_TotalBytesToReceive(&tot);
|
|
1231
|
+
std::string payload = "{\"kind\":\"progress\",\"id\":\"" + id +
|
|
1232
|
+
"\",\"receivedBytes\":" + std::to_string(rec);
|
|
1233
|
+
if (tot > 0) payload += ",\"totalBytes\":" + std::to_string(tot);
|
|
1234
|
+
payload += "}";
|
|
1235
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
1236
|
+
return S_OK;
|
|
1237
|
+
}).Get(),
|
|
1238
|
+
&toks->b);
|
|
1239
|
+
op->add_StateChanged(
|
|
1240
|
+
Callback<ICoreWebView2StateChangedEventHandler>(
|
|
1241
|
+
[lifetime, view_id, id, op, toks](ICoreWebView2DownloadOperation*, IUnknown*) -> HRESULT {
|
|
1242
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
1243
|
+
COREWEBVIEW2_DOWNLOAD_STATE state;
|
|
1244
|
+
op->get_State(&state);
|
|
1245
|
+
bool terminal = false;
|
|
1246
|
+
if (state == COREWEBVIEW2_DOWNLOAD_STATE_COMPLETED) {
|
|
1247
|
+
terminal = true;
|
|
1248
|
+
LPWSTR path_raw = nullptr; op->get_ResultFilePath(&path_raw);
|
|
1249
|
+
std::string path = wideToUtf8(path_raw);
|
|
1250
|
+
if (path_raw) CoTaskMemFree(path_raw);
|
|
1251
|
+
std::string payload = "{\"kind\":\"completed\",\"id\":\"" + id +
|
|
1252
|
+
"\",\"localPath\":\"" + escapeJsonString(path) + "\"}";
|
|
1253
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
1254
|
+
} else if (state == COREWEBVIEW2_DOWNLOAD_STATE_INTERRUPTED) {
|
|
1255
|
+
terminal = true;
|
|
1256
|
+
COREWEBVIEW2_DOWNLOAD_INTERRUPT_REASON reason;
|
|
1257
|
+
op->get_InterruptReason(&reason);
|
|
1258
|
+
std::string payload = "{\"kind\":\"failed\",\"id\":\"" + id +
|
|
1259
|
+
"\",\"reason\":\"interrupted-" + std::to_string(reason) + "\"}";
|
|
1260
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
1261
|
+
}
|
|
1262
|
+
if (terminal) {
|
|
1263
|
+
op->remove_BytesReceivedChanged(toks->b);
|
|
1264
|
+
op->remove_StateChanged(toks->s);
|
|
1265
|
+
}
|
|
1266
|
+
return S_OK;
|
|
1267
|
+
}).Get(),
|
|
1268
|
+
&toks->s);
|
|
1269
|
+
}
|
|
901
1270
|
return S_OK;
|
|
902
1271
|
}).Get(),
|
|
903
1272
|
&tok);
|