bunite-core 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/host/core/App.ts +17 -1
- package/src/host/core/BrowserView.ts +197 -28
- package/src/host/core/SurfaceBrowserIPC.ts +44 -3
- package/src/host/core/SurfaceManager.ts +260 -28
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +8 -1
- package/src/host/native.ts +124 -1
- package/src/native/linux/bunite_linux_ffi.cpp +223 -6
- package/src/native/linux/bunite_linux_internal.h +6 -0
- package/src/native/linux/bunite_linux_runtime.cpp +1 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +85 -0
- package/src/native/mac/bunite_mac_ffi.mm +356 -8
- package/src/native/mac/bunite_mac_internal.h +6 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +144 -2
- package/src/native/shared/ffi_exports.h +135 -0
- package/src/native/win/native_host_cef.cpp +86 -3
- package/src/native/win/native_host_ffi.cpp +378 -1
- package/src/native/win/native_host_internal.h +13 -0
- package/src/native/win/native_host_utils.cpp +2 -1
- package/src/native/win/process_helper_win.cpp +54 -27
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +303 -9
- package/src/native/win-webview2/webview2_internal.h +11 -0
- package/src/native/win-webview2/webview2_runtime.cpp +128 -12
- package/src/native/win-webview2/webview2_utils.cpp +30 -12
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +97 -0
- package/src/rpc/framework.ts +173 -4
- package/src/rpc/index.ts +21 -0
- package/src/webview/native.ts +126 -25
- package/src/webview/polyfill.ts +196 -12
|
@@ -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 =
|
|
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
|
-
|
|
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(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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,
|
|
119
|
+
rl->SetString(2, "runtime_error");
|
|
93
120
|
rl->SetString(3, msg);
|
|
94
121
|
}
|
|
95
122
|
context->Exit();
|