bunite-core 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +4 -4
  2. package/src/host/core/App.ts +17 -1
  3. package/src/host/core/BrowserView.ts +197 -28
  4. package/src/host/core/SurfaceBrowserIPC.ts +44 -3
  5. package/src/host/core/SurfaceManager.ts +260 -28
  6. package/src/host/core/SurfaceRegistry.ts +9 -1
  7. package/src/host/core/inputDispatch.ts +147 -0
  8. package/src/host/events/webviewEvents.ts +8 -1
  9. package/src/host/native.ts +124 -1
  10. package/src/native/linux/bunite_linux_ffi.cpp +223 -6
  11. package/src/native/linux/bunite_linux_internal.h +6 -0
  12. package/src/native/linux/bunite_linux_runtime.cpp +1 -1
  13. package/src/native/linux/bunite_linux_utils.cpp +2 -2
  14. package/src/native/linux/bunite_linux_view.cpp +85 -0
  15. package/src/native/mac/bunite_mac_ffi.mm +356 -8
  16. package/src/native/mac/bunite_mac_internal.h +6 -0
  17. package/src/native/mac/bunite_mac_utils.mm +2 -2
  18. package/src/native/mac/bunite_mac_view.mm +144 -2
  19. package/src/native/shared/ffi_exports.h +135 -0
  20. package/src/native/win/native_host_cef.cpp +86 -3
  21. package/src/native/win/native_host_ffi.cpp +378 -1
  22. package/src/native/win/native_host_internal.h +13 -0
  23. package/src/native/win/native_host_utils.cpp +2 -1
  24. package/src/native/win/process_helper_win.cpp +54 -27
  25. package/src/native/win-webview2/bunite_webview2_ffi.cpp +303 -9
  26. package/src/native/win-webview2/webview2_internal.h +11 -0
  27. package/src/native/win-webview2/webview2_runtime.cpp +128 -12
  28. package/src/native/win-webview2/webview2_utils.cpp +30 -12
  29. package/src/preload/runtime.built.js +1 -1
  30. package/src/preload/runtime.ts +97 -0
  31. package/src/rpc/framework.ts +173 -4
  32. package/src/rpc/index.ts +21 -0
  33. package/src/webview/native.ts +126 -25
  34. package/src/webview/polyfill.ts +196 -12
@@ -2,14 +2,94 @@
2
2
 
3
3
  #include "include/cef_version.h"
4
4
  #include "include/cef_version_info.h"
5
+ #include "include/cef_parser.h"
6
+ #include "include/cef_values.h"
7
+
8
+ // CDP path — input dispatch (mouse wheel) and screenshot use CefBrowserHost::
9
+ // ExecuteDevToolsMethod. Replies arrive on the CefDevToolsMessageObserver.
10
+ // scroll: SendMouseWheelEvent doesn't reach the page in windowed CEF
11
+ // (verified empirically on Win 11 / CEF 119+).
12
+ // screenshot: PrintWindow PW_RENDERFULLCONTENT misses hardware-composited
13
+ // surfaces (returns all-black). Page.captureScreenshot is compositor-aware.
14
+ #include <atomic>
15
+ #include <functional>
16
+ #include <mutex>
17
+ #include <unordered_map>
18
+
5
19
 
6
20
  using bunite_win::runOnUiThreadSync;
7
21
  using bunite_win::runOnCefUiThreadSync;
8
22
 
9
- static constexpr int32_t BUNITE_ABI_VERSION = 5;
23
+ static constexpr int32_t BUNITE_ABI_VERSION = 9;
10
24
 
11
25
  namespace {
12
26
 
27
+ // CDP message routing: ExecuteDevToolsMethod returns a message_id; the
28
+ // singleton observer routes OnDevToolsMethodResult back to a stashed callback.
29
+ std::mutex g_cdp_cb_mutex;
30
+ std::unordered_map<int, std::function<void(bool, std::string)>> g_cdp_callbacks;
31
+
32
+ class BuniteDevToolsObserver : public CefDevToolsMessageObserver {
33
+ public:
34
+ void OnDevToolsMethodResult(CefRefPtr<CefBrowser>, int message_id, bool success,
35
+ const void* result, size_t result_size) override {
36
+ std::function<void(bool, std::string)> cb;
37
+ {
38
+ std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
39
+ auto it = g_cdp_callbacks.find(message_id);
40
+ if (it == g_cdp_callbacks.end()) return;
41
+ cb = std::move(it->second);
42
+ g_cdp_callbacks.erase(it);
43
+ }
44
+ std::string r;
45
+ if (result && result_size) r.assign(static_cast<const char*>(result), result_size);
46
+ cb(success, std::move(r));
47
+ }
48
+ void OnDevToolsAgentDetached(CefRefPtr<CefBrowser>) override {
49
+ // Pending method results are dropped by CEF on detach (browser crash,
50
+ // process restart). Fire all callbacks with a failure result to prevent
51
+ // hung promises in the TS layer.
52
+ std::unordered_map<int, std::function<void(bool, std::string)>> orphans;
53
+ {
54
+ std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
55
+ orphans.swap(g_cdp_callbacks);
56
+ }
57
+ for (auto& kv : orphans) kv.second(false, "{\"error\":\"devtools_agent_detached\"}");
58
+ }
59
+ IMPLEMENT_REFCOUNTING(BuniteDevToolsObserver);
60
+ };
61
+
62
+ CefRefPtr<CefDevToolsMessageObserver> getDevToolsObserver() {
63
+ static CefRefPtr<CefDevToolsMessageObserver> obs = new BuniteDevToolsObserver();
64
+ return obs;
65
+ }
66
+
67
+ void cefCdpCall(ViewHost* v, const std::string& method, const std::string& params_json,
68
+ std::function<void(bool, std::string)> cb = nullptr) {
69
+ if (!v || !v->browser) {
70
+ if (cb) cb(false, "{}");
71
+ return;
72
+ }
73
+ CefRefPtr<CefDictionaryValue> params;
74
+ if (!params_json.empty()) {
75
+ CefRefPtr<CefValue> val = CefParseJSON(params_json, JSON_PARSER_RFC);
76
+ if (val && val->GetType() == VTYPE_DICTIONARY) params = val->GetDictionary();
77
+ }
78
+ if (!params) params = CefDictionaryValue::Create();
79
+ // Register the callback under the *assigned* message id (ExecuteDevToolsMethod
80
+ // returns it). We supply 0 to let CEF assign so our counter can't collide
81
+ // with CEF's internal counter.
82
+ const int assigned_id = v->browser->GetHost()->ExecuteDevToolsMethod(0, method, params);
83
+ if (assigned_id == 0) {
84
+ if (cb) cb(false, "{\"error\":\"ExecuteDevToolsMethod failed\"}");
85
+ return;
86
+ }
87
+ if (cb) {
88
+ std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
89
+ g_cdp_callbacks[assigned_id] = std::move(cb);
90
+ }
91
+ }
92
+
13
93
  std::string wideToUtf8(const std::wstring& value) {
14
94
  if (value.empty()) return {};
15
95
  const int bytes = WideCharToMultiByte(
@@ -48,6 +128,24 @@ std::string resolveProcessHelperPath() {
48
128
 
49
129
  } // namespace
50
130
 
131
+ namespace bunite_win {
132
+ void registerCdpObserverForView(ViewHost* view) {
133
+ if (!view || !view->browser) return;
134
+ view->devtools_registration =
135
+ view->browser->GetHost()->AddDevToolsMessageObserver(getDevToolsObserver());
136
+ }
137
+
138
+ void respondToDialogRequest(ViewHost* view, uint32_t request_id,
139
+ bool accept, const std::string& text) {
140
+ if (!view) return;
141
+ auto it = view->pending_dialogs.find(request_id);
142
+ if (it == view->pending_dialogs.end()) return;
143
+ CefRefPtr<CefJSDialogCallback> cb = std::move(it->second);
144
+ view->pending_dialogs.erase(it);
145
+ if (cb) cb->Continue(accept, text);
146
+ }
147
+ } // namespace bunite_win
148
+
51
149
  extern "C" BUNITE_EXPORT int32_t bunite_abi_version(void) {
52
150
  return BUNITE_ABI_VERSION;
53
151
  }
@@ -79,12 +177,25 @@ extern "C" BUNITE_EXPORT void bunite_set_log_level(int32_t level) {
79
177
  buniteSetLogLevel(static_cast<BuniteLogLevel>(level));
80
178
  }
81
179
 
180
+ // See webview2_runtime.cpp `reapChildrenOnExit`. Same rationale for process_helper.
181
+ static void reapChildrenOnExit() {
182
+ HANDLE job = CreateJobObjectW(nullptr, nullptr);
183
+ if (!job) return;
184
+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{};
185
+ info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
186
+ if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info, sizeof(info)) ||
187
+ !AssignProcessToJobObject(job, GetCurrentProcess())) {
188
+ CloseHandle(job);
189
+ }
190
+ }
191
+
82
192
  extern "C" BUNITE_EXPORT bool bunite_init(
83
193
  const char* cef_dir,
84
194
  bool hide_console,
85
195
  bool popup_blocking,
86
196
  const char* engine_config_json
87
197
  ) {
198
+ reapChildrenOnExit();
88
199
  {
89
200
  std::lock_guard<std::mutex> lock(g_runtime.lifecycle_mutex);
90
201
  if (g_runtime.initialized) {
@@ -857,6 +968,272 @@ extern "C" BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) {
857
968
  bunite_win::postCefUiTask([view_id]() { bunite_win::closeViewHost(bunite_win::getViewHostById(view_id)); });
858
969
  }
859
970
 
971
+ // Input dispatch — native CEF API. Real OS-input path → MouseEvent.isTrusted = true.
972
+ namespace {
973
+
974
+ // Local names — `MOD_ALT`/`MOD_SHIFT` are Win32 RegisterHotKey macros.
975
+ constexpr uint32_t kBmodAlt = 1, kBmodCtrl = 2, kBmodMeta = 4, kBmodShift = 8;
976
+
977
+ uint32_t cefModifiers(uint32_t bits) {
978
+ uint32_t flags = 0;
979
+ if (bits & kBmodShift) flags |= EVENTFLAG_SHIFT_DOWN;
980
+ if (bits & kBmodCtrl) flags |= EVENTFLAG_CONTROL_DOWN;
981
+ if (bits & kBmodAlt) flags |= EVENTFLAG_ALT_DOWN;
982
+ if (bits & kBmodMeta) flags |= EVENTFLAG_COMMAND_DOWN;
983
+ return flags;
984
+ }
985
+
986
+ cef_mouse_button_type_t cefButton(int32_t b) {
987
+ switch (b) { case 1: return MBT_MIDDLE; case 2: return MBT_RIGHT; default: return MBT_LEFT; }
988
+ }
989
+
990
+ } // namespace
991
+
992
+ extern "C" BUNITE_EXPORT void bunite_view_click(uint32_t view_id, double x, double y,
993
+ int32_t button, int32_t click_count, uint32_t modifiers) {
994
+ if (click_count < 1) click_count = 1;
995
+ bunite_win::postCefUiTask([view_id, x, y, button, click_count, modifiers]() {
996
+ auto* view = bunite_win::getViewHostById(view_id);
997
+ if (!view || !view->browser) return;
998
+ auto host = view->browser->GetHost();
999
+ if (!host) return;
1000
+ CefMouseEvent ev{};
1001
+ ev.x = static_cast<int>(x);
1002
+ ev.y = static_cast<int>(y);
1003
+ ev.modifiers = cefModifiers(modifiers);
1004
+ // Multi-click → repeated pairs with increasing clickCount so the page sees dblclick.
1005
+ for (int i = 1; i <= click_count; ++i) {
1006
+ host->SendMouseClickEvent(ev, cefButton(button), /*mouseUp=*/false, i);
1007
+ host->SendMouseClickEvent(ev, cefButton(button), /*mouseUp=*/true, i);
1008
+ }
1009
+ });
1010
+ }
1011
+
1012
+ extern "C" BUNITE_EXPORT void bunite_view_type(uint32_t view_id, const char* text) {
1013
+ std::string s = text ? text : "";
1014
+ bunite_win::postCefUiTask([view_id, s]() {
1015
+ auto* view = bunite_win::getViewHostById(view_id);
1016
+ if (!view || !view->browser) return;
1017
+ auto host = view->browser->GetHost();
1018
+ if (!host) return;
1019
+ std::wstring wide(s.size(), 0);
1020
+ int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()),
1021
+ wide.data(), static_cast<int>(wide.size()));
1022
+ wide.resize(n);
1023
+ // Per character: RAWKEYDOWN + CHAR + KEYUP. CHAR-only doesn't trigger the
1024
+ // DOM `input` event on text fields — Chromium expects a paired key cycle.
1025
+ // BMP only (surrogate pairs would mis-fire `keypress` twice).
1026
+ for (size_t i = 0; i < wide.size(); ++i) {
1027
+ wchar_t ch = wide[i];
1028
+ if (ch >= 0xD800 && ch <= 0xDBFF) {
1029
+ static bool warned = false;
1030
+ if (!warned) { warned = true; BUNITE_WARN("cef type: supplementary-plane codepoint skipped"); }
1031
+ if (i + 1 < wide.size()) ++i;
1032
+ continue;
1033
+ }
1034
+ const int vk = (ch >= 'a' && ch <= 'z') ? (ch - ('a' - 'A')) // letters map to upper VK
1035
+ : (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') ? ch
1036
+ : 0;
1037
+ CefKeyEvent down{};
1038
+ down.type = KEYEVENT_RAWKEYDOWN;
1039
+ down.windows_key_code = vk ? vk : ch;
1040
+ down.character = ch;
1041
+ down.unmodified_character = ch;
1042
+ host->SendKeyEvent(down);
1043
+ CefKeyEvent c{};
1044
+ c.type = KEYEVENT_CHAR;
1045
+ c.windows_key_code = ch;
1046
+ c.character = ch;
1047
+ c.unmodified_character = ch;
1048
+ host->SendKeyEvent(c);
1049
+ CefKeyEvent up{};
1050
+ up.type = KEYEVENT_KEYUP;
1051
+ up.windows_key_code = vk ? vk : ch;
1052
+ up.character = ch;
1053
+ up.unmodified_character = ch;
1054
+ host->SendKeyEvent(up);
1055
+ }
1056
+ });
1057
+ }
1058
+
1059
+ extern "C" BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t windows_vk_code,
1060
+ int32_t /*mac_key_code*/,
1061
+ const char* /*key*/, const char* /*code*/,
1062
+ const char* character, uint32_t modifiers,
1063
+ int32_t action, bool extended, int32_t /*location*/) {
1064
+ std::string char_str = character ? character : "";
1065
+ bunite_win::postCefUiTask([view_id, windows_vk_code, char_str, modifiers, action, extended]() {
1066
+ auto* view = bunite_win::getViewHostById(view_id);
1067
+ if (!view || !view->browser) return;
1068
+ auto host = view->browser->GetHost();
1069
+ if (!host) return;
1070
+ uint32_t mod = cefModifiers(modifiers);
1071
+ // Chromium's KeycodeConverter::NativeKeycodeToDomCode expects raw scancode
1072
+ // with 0xE0 prefix when extended (see chromium ui/events dom_code_data.inc).
1073
+ // Not LPARAM — Chromium's Win backend keys off scancode|(extended ? 0xE000 : 0).
1074
+ UINT scancode = windows_vk_code ? MapVirtualKeyW(static_cast<UINT>(windows_vk_code), MAPVK_VK_TO_VSC) : 0;
1075
+ int32_t native = static_cast<int32_t>(extended ? (0xE000u | scancode) : scancode);
1076
+
1077
+ if (action != 1 && windows_vk_code != 0) {
1078
+ CefKeyEvent down{};
1079
+ down.type = KEYEVENT_RAWKEYDOWN;
1080
+ down.windows_key_code = windows_vk_code;
1081
+ down.native_key_code = native;
1082
+ down.modifiers = mod;
1083
+ host->SendKeyEvent(down);
1084
+ }
1085
+ // CHAR rides with the down half (Playwright convention) — emitted only
1086
+ // when we're sending the down (action=both or down).
1087
+ if (action != 1 && !char_str.empty()) {
1088
+ std::wstring wide(char_str.size(), 0);
1089
+ int n = MultiByteToWideChar(CP_UTF8, 0, char_str.c_str(), static_cast<int>(char_str.size()),
1090
+ wide.data(), static_cast<int>(wide.size()));
1091
+ wide.resize(n);
1092
+ for (size_t i = 0; i < wide.size(); ++i) {
1093
+ wchar_t ch = wide[i];
1094
+ if (ch >= 0xD800 && ch <= 0xDBFF) {
1095
+ if (i + 1 < wide.size()) ++i;
1096
+ continue;
1097
+ }
1098
+ CefKeyEvent ce{};
1099
+ ce.type = KEYEVENT_CHAR;
1100
+ ce.character = ch;
1101
+ ce.unmodified_character = ch;
1102
+ ce.windows_key_code = ch;
1103
+ ce.modifiers = mod;
1104
+ host->SendKeyEvent(ce);
1105
+ }
1106
+ }
1107
+ if (action != 0 && windows_vk_code != 0) {
1108
+ CefKeyEvent up{};
1109
+ up.type = KEYEVENT_KEYUP;
1110
+ up.windows_key_code = windows_vk_code;
1111
+ up.native_key_code = native;
1112
+ up.modifiers = mod;
1113
+ host->SendKeyEvent(up);
1114
+ }
1115
+ });
1116
+ }
1117
+
1118
+ extern "C" BUNITE_EXPORT void bunite_view_scroll(uint32_t view_id, double dx, double dy,
1119
+ double x, double y, uint32_t modifiers) {
1120
+ bunite_win::postCefUiTask([view_id, dx, dy, x, y, modifiers]() {
1121
+ auto* view = bunite_win::getViewHostById(view_id);
1122
+ if (!view) return;
1123
+ // CDP path — native SendMouseWheelEvent doesn't reach the page in
1124
+ // windowed CEF. deltaY is CSS pixels, matching WV2.
1125
+ std::string params =
1126
+ "{\"type\":\"mouseWheel\""
1127
+ ",\"x\":" + std::to_string(x) +
1128
+ ",\"y\":" + std::to_string(y) +
1129
+ ",\"deltaX\":" + std::to_string(dx) +
1130
+ ",\"deltaY\":" + std::to_string(dy) +
1131
+ ",\"modifiers\":" + std::to_string(modifiers) + "}";
1132
+ cefCdpCall(view, "Input.dispatchMouseEvent", params);
1133
+ });
1134
+ }
1135
+
1136
+ extern "C" BUNITE_EXPORT void bunite_view_mouse(uint32_t view_id, int32_t action,
1137
+ double x, double y, int32_t button,
1138
+ uint32_t modifiers) {
1139
+ bunite_win::postCefUiTask([view_id, action, x, y, button, modifiers]() {
1140
+ auto* view = bunite_win::getViewHostById(view_id);
1141
+ if (!view || !view->browser) return;
1142
+ auto host = view->browser->GetHost();
1143
+ if (!host) return;
1144
+ CefMouseEvent ev{};
1145
+ ev.x = static_cast<int>(x);
1146
+ ev.y = static_cast<int>(y);
1147
+ ev.modifiers = cefModifiers(modifiers);
1148
+ if (action == 0) {
1149
+ // move
1150
+ host->SendMouseMoveEvent(ev, /*mouseLeave=*/false);
1151
+ } else {
1152
+ // down (1) / up (2)
1153
+ CefBrowserHost::MouseButtonType btn = (button == 2) ? MBT_RIGHT
1154
+ : (button == 1) ? MBT_MIDDLE : MBT_LEFT;
1155
+ host->SendMouseClickEvent(ev, btn, /*mouseUp=*/action == 2, /*clickCount=*/1);
1156
+ }
1157
+ });
1158
+ }
1159
+
1160
+ extern "C" BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
1161
+ bool accept, const char* text) {
1162
+ std::string text_str = text ? text : "";
1163
+ bunite_win::postCefUiTask([view_id, request_id, accept, text_str]() {
1164
+ auto* view = bunite_win::getViewHostById(view_id);
1165
+ if (!view) return;
1166
+ bunite_win::respondToDialogRequest(view, request_id, accept, text_str);
1167
+ });
1168
+ }
1169
+
1170
+ namespace {
1171
+
1172
+ void emitScreenshotError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& msg) {
1173
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1174
+ ",\"ok\":false,\"code\":\"" + code + "\","
1175
+ "\"message\":\"" + bunite_win::escapeJsonString(msg) + "\"}";
1176
+ bunite_win::emitWebviewEvent(view_id, "screenshot-result", payload);
1177
+ }
1178
+
1179
+ } // namespace
1180
+
1181
+ extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
1182
+ // CEF — click/type/press are native (isTrusted=true); scroll and
1183
+ // screenshot go via CDP (windowed SendMouseWheelEvent doesn't reach the
1184
+ // page, and Page.captureScreenshot is compositor-aware vs PrintWindow's
1185
+ // black-frame trap).
1186
+ auto* view = bunite_win::getViewHostById(view_id);
1187
+ if (!view) return 0;
1188
+ return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
1189
+ BUNITE_CAP_NATIVE_INPUT_TRUSTED |
1190
+ BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
1191
+ BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
1192
+ BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG;
1193
+ }
1194
+
1195
+ extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
1196
+ const char* format, int32_t quality) {
1197
+ std::string fmt = format ? format : "png";
1198
+ bunite_win::postCefUiTask([view_id, request_id, fmt, quality]() {
1199
+ auto* view = bunite_win::getViewHostById(view_id);
1200
+ if (!view) {
1201
+ emitScreenshotError(view_id, request_id, "not_supported", "view not ready");
1202
+ return;
1203
+ }
1204
+ const bool jpeg = (fmt == "jpeg" || fmt == "jpg");
1205
+ const std::string outFmt = jpeg ? "jpeg" : "png";
1206
+ const std::string mime = jpeg ? "image/jpeg" : "image/png";
1207
+ std::string params = "{\"format\":\"" + outFmt + "\"";
1208
+ if (jpeg && quality >= 0) params += ",\"quality\":" + std::to_string(quality);
1209
+ params += "}";
1210
+ cefCdpCall(view, "Page.captureScreenshot", params,
1211
+ [view_id, request_id, outFmt, mime](bool ok, std::string result) {
1212
+ if (!ok) {
1213
+ emitScreenshotError(view_id, request_id, "runtime_error",
1214
+ std::string("Page.captureScreenshot failed: ") + result);
1215
+ return;
1216
+ }
1217
+ CefRefPtr<CefValue> val = CefParseJSON(result, JSON_PARSER_RFC);
1218
+ if (!val || val->GetType() != VTYPE_DICTIONARY) {
1219
+ emitScreenshotError(view_id, request_id, "runtime_error", "captureScreenshot malformed result");
1220
+ return;
1221
+ }
1222
+ CefString data = val->GetDictionary()->GetString("data");
1223
+ if (data.empty()) {
1224
+ emitScreenshotError(view_id, request_id, "runtime_error", "captureScreenshot missing data");
1225
+ return;
1226
+ }
1227
+ std::string b64 = data.ToString();
1228
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1229
+ ",\"ok\":true,\"format\":\"" + outFmt +
1230
+ "\",\"mime\":\"" + mime +
1231
+ "\",\"dataBase64\":\"" + b64 + "\"}";
1232
+ bunite_win::emitWebviewEvent(view_id, "screenshot-result", payload);
1233
+ });
1234
+ });
1235
+ }
1236
+
860
1237
  extern "C" BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
861
1238
  bunite_win::postCefUiTask([view_id]() { bunite_win::openDevToolsForView(bunite_win::getViewHostById(view_id)); });
862
1239
  }
@@ -34,9 +34,11 @@
34
34
  #include "include/cef_app.h"
35
35
  #include "include/cef_browser.h"
36
36
  #include "include/cef_client.h"
37
+ #include "include/cef_devtools_message_observer.h"
37
38
  #include "include/cef_command_line.h"
38
39
  #include "include/cef_parser.h"
39
40
  #include "include/cef_permission_handler.h"
41
+ #include "include/cef_jsdialog_handler.h"
40
42
  #include "include/cef_resource_handler.h"
41
43
  #include "include/cef_resource_request_handler.h"
42
44
  #include "include/cef_scheme.h"
@@ -97,6 +99,14 @@ struct ViewHost {
97
99
  std::atomic<bool> closing = false;
98
100
  CefRefPtr<CefBrowser> browser;
99
101
  CefRefPtr<CefClient> client;
102
+ // DevTools observer registration — kept alive for the life of the view so
103
+ // CDP method results (screenshot etc.) can route back to bunite handlers.
104
+ CefRefPtr<CefRegistration> devtools_registration;
105
+
106
+ // Page-initiated dialog callbacks held until host responds via
107
+ // `respondToDialogRequest`. CEF holds the page execution while we wait.
108
+ std::unordered_map<uint32_t, CefRefPtr<CefJSDialogCallback>> pending_dialogs;
109
+ uint32_t next_dialog_request_id = 1;
100
110
 
101
111
  // Pending state: applied in OnAfterCreated when browser HWND becomes available.
102
112
  bool pending_visible = true;
@@ -214,6 +224,9 @@ void finalizeWindowHost(WindowHost* window); // [UI thread]
214
224
  void openDevToolsForView(ViewHost* view); // [CEF UI thread]
215
225
  void closeDevToolsForView(ViewHost* view); // [CEF UI thread]
216
226
  void toggleDevToolsForView(ViewHost* view); // [CEF UI thread]
227
+ void registerCdpObserverForView(ViewHost* view); // [CEF UI thread]
228
+ void respondToDialogRequest(ViewHost* view, uint32_t request_id,
229
+ bool accept, const std::string& text); // [CEF UI thread]
217
230
  bool initializeCefOnUiThread(); // [UI thread]
218
231
  void shutdownCefOnUiThread(); // [UI thread]
219
232
  void cancelPendingPermissionRequestsOnUiThread(); // [CEF UI thread]
@@ -211,7 +211,8 @@ std::vector<std::string> parseNavigationRulesJson(const std::string& rules_json)
211
211
  }
212
212
 
213
213
  bool shouldAlwaysAllowNavigationUrl(const std::string& url) {
214
- return url == "about:blank" || url.rfind("appres://app.internal/internal/", 0) == 0;
214
+ // Exact-match prefix would let `../../evil` style paths bypass scrutiny.
215
+ return url == "about:blank" || url == "appres://app.internal/internal/index.html";
215
216
  }
216
217
 
217
218
  uint32_t cefPermissionsToBuniteKind(uint32_t cef_bits) {
@@ -48,10 +48,10 @@ public:
48
48
  std::string script = args->GetString(1).ToString();
49
49
 
50
50
  auto context = frame->GetV8Context();
51
+ auto reply = CefProcessMessage::Create("bunite.evaluate.result");
52
+ auto rl = reply->GetArgumentList();
53
+ rl->SetInt(0, static_cast<int>(request_id));
51
54
  if (!context) {
52
- auto reply = CefProcessMessage::Create("bunite.evaluate.result");
53
- auto rl = reply->GetArgumentList();
54
- rl->SetInt(0, static_cast<int>(request_id));
55
55
  rl->SetBool(1, false);
56
56
  rl->SetString(2, "runtime_error");
57
57
  rl->SetString(3, "no V8 context");
@@ -59,37 +59,64 @@ public:
59
59
  return true;
60
60
  }
61
61
 
62
+ // Same wrapper as WebView2/mac/linux: try/catch returning a JSON envelope
63
+ // string. SecurityError is detected locale-independently inside the JS.
64
+ std::string wrapped =
65
+ "(function(){try{return JSON.stringify({__bunite_ok:true,value:(" + script +
66
+ ")})}catch(e){var c=(e&&e.name===\"SecurityError\")?\"cross_origin\":\"runtime_error\";"
67
+ "return JSON.stringify({__bunite_ok:false,code:c,"
68
+ "message:(e&&e.message)?e.message:String(e),"
69
+ "name:(e&&e.name)||\"\"})}})()";
70
+
62
71
  context->Enter();
63
72
  CefRefPtr<CefV8Value> retval;
64
73
  CefRefPtr<CefV8Exception> exception;
65
- bool ok = context->Eval(script, "bunite://evaluate", 0, retval, exception);
66
-
67
- auto reply = CefProcessMessage::Create("bunite.evaluate.result");
68
- auto rl = reply->GetArgumentList();
69
- rl->SetInt(0, static_cast<int>(request_id));
70
- if (ok && retval) {
71
- // V8 JSON.stringify for cross-process transport.
72
- auto global = context->GetGlobal();
73
- auto json_obj = global->GetValue("JSON");
74
- auto stringify = json_obj ? json_obj->GetValue("stringify") : nullptr;
75
- std::string json_str;
76
- if (stringify && stringify->IsFunction()) {
77
- CefV8ValueList args2; args2.push_back(retval);
78
- auto json_v = stringify->ExecuteFunction(json_obj, args2);
79
- if (json_v && json_v->IsString()) json_str = json_v->GetStringValue().ToString();
74
+ bool ok = context->Eval(wrapped, "bunite://evaluate", 0, retval, exception);
75
+
76
+ if (ok && retval && retval->IsString()) {
77
+ std::string inner = retval->GetStringValue().ToString();
78
+ if (inner.find("\"__bunite_ok\":true") != std::string::npos) {
79
+ static const std::string prefix = "{\"__bunite_ok\":true,\"value\":";
80
+ std::string value_json = "null";
81
+ if (inner.compare(0, prefix.size(), prefix) == 0 &&
82
+ inner.size() > prefix.size() + 1) {
83
+ value_json = inner.substr(prefix.size(), inner.size() - prefix.size() - 1);
84
+ }
85
+ rl->SetBool(1, true);
86
+ rl->SetString(2, value_json);
87
+ rl->SetString(3, "");
88
+ } else {
89
+ // Anchor at the envelope prefix — user-controlled e.message could
90
+ // otherwise inject a fake "code" via the substring scan above.
91
+ static const std::string codePrefix = "{\"__bunite_ok\":false,\"code\":\"";
92
+ std::string code = "runtime_error";
93
+ std::string msg = "script threw";
94
+ if (inner.compare(0, codePrefix.size(), codePrefix) == 0) {
95
+ size_t start = codePrefix.size();
96
+ size_t end = start;
97
+ while (end < inner.size() && inner[end] != '"') ++end;
98
+ if (end > start) code = inner.substr(start, end - start);
99
+ static const std::string msgKey = "\",\"message\":\"";
100
+ if (end + msgKey.size() <= inner.size() &&
101
+ inner.compare(end, msgKey.size(), msgKey) == 0) {
102
+ size_t mstart = end + msgKey.size();
103
+ size_t mend = mstart;
104
+ while (mend < inner.size()) {
105
+ if (inner[mend] == '"' && (mend == mstart || inner[mend - 1] != '\\')) break;
106
+ ++mend;
107
+ }
108
+ if (mend > mstart) msg = inner.substr(mstart, mend - mstart);
109
+ }
110
+ }
111
+ rl->SetBool(1, false);
112
+ rl->SetString(2, code);
113
+ rl->SetString(3, msg);
80
114
  }
81
- if (json_str.empty()) json_str = "null";
82
- rl->SetBool(1, true);
83
- rl->SetString(2, json_str);
84
- rl->SetString(3, "");
85
115
  } else {
116
+ // Wrapper itself failed (syntax error in user script, or V8 internal).
86
117
  std::string msg = exception ? exception->GetMessage().ToString() : "eval failed";
87
- // SecurityError typically arises from cross-origin reach.
88
- std::string code = (msg.find("SecurityError") != std::string::npos ||
89
- msg.find("cross-origin") != std::string::npos)
90
- ? "cross_origin" : "runtime_error";
91
118
  rl->SetBool(1, false);
92
- rl->SetString(2, code);
119
+ rl->SetString(2, "runtime_error");
93
120
  rl->SetString(3, msg);
94
121
  }
95
122
  context->Exit();