bunite-core 0.14.0 → 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 +2 -1
- package/src/host/core/BrowserView.ts +345 -24
- package/src/host/core/SurfaceBrowserIPC.ts +10 -1
- package/src/host/core/SurfaceManager.ts +357 -16
- package/src/host/events/webviewEvents.ts +18 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +140 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +205 -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 +278 -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 +93 -30
- package/src/native/win/native_host_cef.cpp +102 -10
- package/src/native/win/native_host_ffi.cpp +818 -2
- package/src/native/win/native_host_internal.h +22 -0
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +788 -4
- package/src/native/win-webview2/webview2_internal.h +14 -0
- package/src/native/win-webview2/webview2_runtime.cpp +276 -23
- package/src/preload/runtime.built.js +1 -1
- package/src/rpc/framework.ts +174 -11
- package/src/rpc/index.ts +11 -0
- 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 {
|
|
@@ -116,6 +129,7 @@ struct RuntimeState {
|
|
|
116
129
|
std::atomic<bool> initialized{false};
|
|
117
130
|
std::atomic<bool> shutting_down{false};
|
|
118
131
|
HWND message_window = nullptr;
|
|
132
|
+
HWND popup_parent = nullptr; // hidden top-level parking parent for popup-minted controllers.
|
|
119
133
|
|
|
120
134
|
std::mutex task_mutex;
|
|
121
135
|
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
|
|
@@ -634,15 +643,42 @@ static void wireView(ViewHost* view, std::function<void()> on_attached) {
|
|
|
634
643
|
// - main frame only (matching CEF's OnContextCreated main-frame gate)
|
|
635
644
|
// - origin allowlist when `preload_origins` is non-empty
|
|
636
645
|
// Empty allowlist = inject on every main frame (CEF parity default).
|
|
637
|
-
|
|
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) {
|
|
638
664
|
std::string allowlist = "[";
|
|
639
665
|
for (size_t i = 0; i < v->preload_origins.size(); ++i) {
|
|
640
666
|
if (i) allowlist += ",";
|
|
641
667
|
allowlist += "\"" + escapeJsonString(v->preload_origins[i]) + "\"";
|
|
642
668
|
}
|
|
643
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
|
+
: "";
|
|
644
679
|
std::string body =
|
|
645
|
-
"(function(){
|
|
680
|
+
"(function(){"
|
|
681
|
+
"if(window.self!==window.top)return;"
|
|
646
682
|
"var __a=" + allowlist +
|
|
647
683
|
",__o=location.origin;"
|
|
648
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;};"
|
|
@@ -652,26 +688,32 @@ static void wireView(ViewHost* view, std::function<void()> on_attached) {
|
|
|
652
688
|
"while(i<p.length&&p[i]===\"*\")i++;return i===p.length;};"
|
|
653
689
|
"var __ok=false;for(var i=0;i<__a.length;i++){if(__m(__a[i],__o)){__ok=true;break;}}"
|
|
654
690
|
"if(!__ok)return;}"
|
|
655
|
-
+
|
|
691
|
+
+ markerStart
|
|
692
|
+
+ v->preload_script
|
|
693
|
+
+ markerEnd +
|
|
656
694
|
"})();";
|
|
657
695
|
std::wstring wpreload = utf8ToWide(body);
|
|
658
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()));
|
|
659
699
|
v->webview->AddScriptToExecuteOnDocumentCreated(
|
|
660
700
|
wpreload.c_str(),
|
|
661
701
|
Callback<ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>(
|
|
662
|
-
[lt, view_id](HRESULT, LPCWSTR id) -> HRESULT {
|
|
702
|
+
[lt, view_id, doInitialNav](HRESULT hr, LPCWSTR id) -> HRESULT {
|
|
663
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
|
+
}
|
|
664
707
|
ViewHost* vv = getView(view_id);
|
|
665
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();
|
|
666
713
|
return S_OK;
|
|
667
714
|
}).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());
|
|
715
|
+
} else {
|
|
716
|
+
doInitialNav();
|
|
675
717
|
}
|
|
676
718
|
|
|
677
719
|
emitWebviewEvent(v->id, "view-ready");
|
|
@@ -755,6 +797,19 @@ void destroyView(uint32_t id) {
|
|
|
755
797
|
vs.erase(std::remove(vs.begin(), vs.end(), v), vs.end());
|
|
756
798
|
}
|
|
757
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
|
+
|
|
758
813
|
// Defer Close() → container destroy → delete across three pump ticks. Edge
|
|
759
814
|
// gets at least one tick after Close() to settle before its parent HWND
|
|
760
815
|
// vanishes; see shutdownRuntime's staged drains for the same reason.
|
|
@@ -819,6 +874,9 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
819
874
|
args->get_Uri(&uri_raw);
|
|
820
875
|
std::string url = wideToUtf8(uri_raw);
|
|
821
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 ? "..." : "");
|
|
822
880
|
emitWebviewEvent(v->id, "will-navigate", url);
|
|
823
881
|
if (!shouldAllowNavigation(v, url)) {
|
|
824
882
|
args->put_Cancel(TRUE);
|
|
@@ -859,6 +917,9 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
859
917
|
args->get_IsSuccess(&ok);
|
|
860
918
|
UINT64 nav_id = 0;
|
|
861
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));
|
|
862
923
|
std::string url;
|
|
863
924
|
auto it = g_nav_uris.find(navKey(view_id, nav_id));
|
|
864
925
|
if (it != g_nav_uris.end()) {
|
|
@@ -884,9 +945,15 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
884
945
|
}).Get(),
|
|
885
946
|
&tok);
|
|
886
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
|
+
|
|
887
954
|
// ScriptDialogOpening — alert / confirm / prompt / beforeunload. Defer the
|
|
888
955
|
// event so host can decide via `respondToDialog`.
|
|
889
|
-
view->webview->add_ScriptDialogOpening(
|
|
956
|
+
HRESULT dlg_hr = view->webview->add_ScriptDialogOpening(
|
|
890
957
|
Callback<ICoreWebView2ScriptDialogOpeningEventHandler>(
|
|
891
958
|
[lifetime, view_id](ICoreWebView2*, ICoreWebView2ScriptDialogOpeningEventArgs* args) -> HRESULT {
|
|
892
959
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
@@ -910,6 +977,8 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
910
977
|
: "alert";
|
|
911
978
|
const uint32_t rid = v->next_dialog_request_id++;
|
|
912
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);
|
|
913
982
|
std::string payload = "{\"requestId\":" + std::to_string(rid) +
|
|
914
983
|
",\"kind\":\"" + kind_str +
|
|
915
984
|
"\",\"message\":\"" + escapeJsonString(message) + "\"";
|
|
@@ -921,6 +990,31 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
921
990
|
return S_OK;
|
|
922
991
|
}).Get(),
|
|
923
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
|
+
}
|
|
924
1018
|
|
|
925
1019
|
// DocumentTitleChanged — surface for automation surfaceEvents title-change arm.
|
|
926
1020
|
view->webview->add_DocumentTitleChanged(
|
|
@@ -969,19 +1063,87 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
969
1063
|
}).Get(),
|
|
970
1064
|
&tok);
|
|
971
1065
|
|
|
972
|
-
// NewWindowRequested —
|
|
973
|
-
//
|
|
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.
|
|
974
1070
|
view->webview->add_NewWindowRequested(
|
|
975
1071
|
Callback<ICoreWebView2NewWindowRequestedEventHandler>(
|
|
976
|
-
[lifetime, view_id](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs*
|
|
1072
|
+
[lifetime, view_id](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args_raw) -> HRESULT {
|
|
977
1073
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
978
|
-
args
|
|
1074
|
+
ComPtr<ICoreWebView2NewWindowRequestedEventArgs> args(args_raw);
|
|
979
1075
|
LPWSTR uri_raw = nullptr;
|
|
980
1076
|
args->get_Uri(&uri_raw);
|
|
981
1077
|
std::string url = wideToUtf8(uri_raw);
|
|
982
1078
|
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
983
|
-
|
|
984
|
-
|
|
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
|
+
}
|
|
985
1147
|
return S_OK;
|
|
986
1148
|
}).Get(),
|
|
987
1149
|
&tok);
|
|
@@ -997,13 +1159,15 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
997
1159
|
}).Get(),
|
|
998
1160
|
&tok);
|
|
999
1161
|
|
|
1000
|
-
// DownloadStarting (ICoreWebView2_4) —
|
|
1162
|
+
// DownloadStarting (ICoreWebView2_4) — policy-driven: block | auto | ask.
|
|
1163
|
+
// Default block preserves the original behavior.
|
|
1001
1164
|
ComPtr<ICoreWebView2_4> wv4;
|
|
1002
1165
|
view->webview->QueryInterface(IID_PPV_ARGS(&wv4));
|
|
1003
1166
|
if (wv4) {
|
|
1167
|
+
static std::atomic<uint32_t> g_download_seq{1};
|
|
1004
1168
|
wv4->add_DownloadStarting(
|
|
1005
1169
|
Callback<ICoreWebView2DownloadStartingEventHandler>(
|
|
1006
|
-
[lifetime, view_id](ICoreWebView2*, ICoreWebView2DownloadStartingEventArgs* args) -> HRESULT {
|
|
1170
|
+
[lifetime, view_id, view](ICoreWebView2*, ICoreWebView2DownloadStartingEventArgs* args) -> HRESULT {
|
|
1007
1171
|
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
1008
1172
|
ComPtr<ICoreWebView2DownloadOperation> op;
|
|
1009
1173
|
args->get_DownloadOperation(&op);
|
|
@@ -1011,9 +1175,98 @@ static void attachControllerCallbacks(ViewHost* view) {
|
|
|
1011
1175
|
if (op) op->get_Uri(&uri_raw);
|
|
1012
1176
|
std::string url = wideToUtf8(uri_raw);
|
|
1013
1177
|
if (uri_raw) CoTaskMemFree(uri_raw);
|
|
1014
|
-
|
|
1015
|
-
std::string
|
|
1016
|
-
|
|
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
|
+
}
|
|
1017
1270
|
return S_OK;
|
|
1018
1271
|
}).Get(),
|
|
1019
1272
|
&tok);
|