@aptre/bldr-saucer 0.2.2 → 0.2.5
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/CMakeLists.txt +0 -1
- package/package.json +7 -7
- package/src/main.cpp +96 -15
- package/src/pipe_client.cpp +32 -10
- package/src/pipe_client.h +2 -1
- package/src/scheme_forwarder.cpp +30 -27
package/CMakeLists.txt
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aptre/bldr-saucer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Native webview bridge for Bldr using Saucer",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"release:publish": "git push && git push --tags"
|
|
34
34
|
},
|
|
35
35
|
"optionalDependencies": {
|
|
36
|
-
"@aptre/bldr-saucer-darwin-arm64": "0.2.
|
|
37
|
-
"@aptre/bldr-saucer-darwin-x64": "0.2.
|
|
38
|
-
"@aptre/bldr-saucer-linux-x64": "0.2.
|
|
39
|
-
"@aptre/bldr-saucer-linux-arm64": "0.2.
|
|
40
|
-
"@aptre/bldr-saucer-win32-x64": "0.2.
|
|
36
|
+
"@aptre/bldr-saucer-darwin-arm64": "0.2.5",
|
|
37
|
+
"@aptre/bldr-saucer-darwin-x64": "0.2.5",
|
|
38
|
+
"@aptre/bldr-saucer-linux-x64": "0.2.5",
|
|
39
|
+
"@aptre/bldr-saucer-linux-arm64": "0.2.5",
|
|
40
|
+
"@aptre/bldr-saucer-win32-x64": "0.2.5"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"index.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"CMakeLists.txt"
|
|
47
47
|
],
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@aptre/common": "^0.30.
|
|
49
|
+
"@aptre/common": "^0.30.3"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@aptre/protobuf-es-lite": "^0.5.2"
|
package/src/main.cpp
CHANGED
|
@@ -4,15 +4,20 @@
|
|
|
4
4
|
#include "pipe_connection.h"
|
|
5
5
|
#include "scheme_forwarder.h"
|
|
6
6
|
|
|
7
|
+
#include <atomic>
|
|
7
8
|
#include <cstdlib>
|
|
9
|
+
#include <cstring>
|
|
8
10
|
#include <iostream>
|
|
11
|
+
#include <memory>
|
|
12
|
+
#include <mutex>
|
|
9
13
|
#include <string>
|
|
10
14
|
#include <thread>
|
|
15
|
+
#include <vector>
|
|
11
16
|
|
|
12
17
|
coco::stray start(saucer::application* app) {
|
|
13
18
|
const char* runtime_id_env = std::getenv("BLDR_RUNTIME_ID");
|
|
14
19
|
if (!runtime_id_env) {
|
|
15
|
-
std::cerr << "BLDR_RUNTIME_ID not set" << std::endl;
|
|
20
|
+
std::cerr << "[bldr-saucer] BLDR_RUNTIME_ID not set" << std::endl;
|
|
16
21
|
co_return;
|
|
17
22
|
}
|
|
18
23
|
std::string runtime_id = runtime_id_env;
|
|
@@ -21,8 +26,8 @@ coco::stray start(saucer::application* app) {
|
|
|
21
26
|
const char* init_b64 = std::getenv("BLDR_SAUCER_INIT");
|
|
22
27
|
if (init_b64) {
|
|
23
28
|
auto data = bldr::proto::Base64Decode(init_b64);
|
|
24
|
-
if (!data.empty()) {
|
|
25
|
-
|
|
29
|
+
if (!data.empty() && !bldr::proto::DecodeSaucerInit(data.data(), data.size(), saucer_init)) {
|
|
30
|
+
std::cerr << "[bldr-saucer] failed to decode BLDR_SAUCER_INIT" << std::endl;
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -30,7 +35,7 @@ coco::stray start(saucer::application* app) {
|
|
|
30
35
|
bldr::PipeClient pipe;
|
|
31
36
|
std::string pipe_path = ".pipe-" + runtime_id;
|
|
32
37
|
if (!pipe.connect(pipe_path)) {
|
|
33
|
-
std::cerr << "
|
|
38
|
+
std::cerr << "[bldr-saucer] failed to connect to pipe: " << pipe_path << std::endl;
|
|
34
39
|
co_return;
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -41,12 +46,12 @@ coco::stray start(saucer::application* app) {
|
|
|
41
46
|
config.enable_keepalive = false;
|
|
42
47
|
auto session = yamux::Session::Client(std::move(conn), config);
|
|
43
48
|
if (!session) {
|
|
44
|
-
std::cerr << "
|
|
49
|
+
std::cerr << "[bldr-saucer] failed to create yamux session" << std::endl;
|
|
45
50
|
co_return;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
// Create the scheme forwarder.
|
|
49
|
-
bldr::SchemeForwarder
|
|
53
|
+
// Create the scheme forwarder (shared_ptr to avoid use-after-free in detached threads).
|
|
54
|
+
auto forwarder = std::make_shared<bldr::SchemeForwarder>(session.get());
|
|
50
55
|
|
|
51
56
|
// Register bldr:// scheme BEFORE creating the webview.
|
|
52
57
|
saucer::webview::register_scheme("bldr");
|
|
@@ -58,15 +63,13 @@ coco::stray start(saucer::application* app) {
|
|
|
58
63
|
window->set_size({1024, 768});
|
|
59
64
|
|
|
60
65
|
// Handle bldr:// scheme: forward all requests to Go over yamux.
|
|
61
|
-
webview->handle_stream_scheme("bldr", [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
forwarder.forward(req, writer);
|
|
66
|
+
webview->handle_stream_scheme("bldr", [forwarder](saucer::scheme::request req, saucer::scheme::stream_writer writer) {
|
|
67
|
+
std::thread([forwarder, req = std::move(req), writer = std::move(writer)]() mutable {
|
|
68
|
+
forwarder->forward(req, writer);
|
|
65
69
|
}).detach();
|
|
66
70
|
});
|
|
67
71
|
|
|
68
|
-
//
|
|
69
|
-
// loadFileURL being called incorrectly for custom schemes.
|
|
72
|
+
// Navigate via HTML redirect (works around WebKit's loadFileURL issue with custom schemes).
|
|
70
73
|
std::string nav_url = "bldr:///index.html";
|
|
71
74
|
const char* doc_id_env = std::getenv("BLDR_WEB_DOCUMENT_ID");
|
|
72
75
|
if (doc_id_env && doc_id_env[0] != '\0') {
|
|
@@ -82,18 +85,96 @@ coco::stray start(saucer::application* app) {
|
|
|
82
85
|
webview->set_dev_tools(true);
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
// Shutdown guard: prevents webview->execute() calls after webview destruction.
|
|
89
|
+
// The mutex ensures no thread is inside execute() when we set the flag.
|
|
90
|
+
auto webview_mtx = std::make_shared<std::mutex>();
|
|
91
|
+
auto webview_alive = std::make_shared<std::atomic<bool>>(true);
|
|
92
|
+
|
|
93
|
+
// Start accept loop for Go-initiated streams (debug eval).
|
|
94
|
+
// webview is a std::expected; use &(*webview) to get a pointer to the contained value.
|
|
95
|
+
auto* webview_ptr = &(*webview);
|
|
96
|
+
std::thread accept_thread([session, webview_ptr, webview_mtx, webview_alive]() {
|
|
97
|
+
while (true) {
|
|
98
|
+
auto [stream, err] = session->Accept();
|
|
99
|
+
if (err != yamux::Error::OK || !stream) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle each stream in a detached thread so accept loop continues.
|
|
104
|
+
std::thread([stream, webview_ptr, webview_mtx, webview_alive]() {
|
|
105
|
+
// Read length-prefixed command frame.
|
|
106
|
+
uint8_t len_buf[4];
|
|
107
|
+
size_t total = 0;
|
|
108
|
+
while (total < 4) {
|
|
109
|
+
auto [n, rerr] = stream->Read(len_buf + total, 4 - total);
|
|
110
|
+
if (rerr != yamux::Error::OK || n == 0) {
|
|
111
|
+
stream->Close();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
total += n;
|
|
115
|
+
}
|
|
116
|
+
uint32_t msg_len;
|
|
117
|
+
std::memcpy(&msg_len, len_buf, 4);
|
|
118
|
+
if (msg_len > 10 * 1024 * 1024) {
|
|
119
|
+
stream->Close();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
std::vector<uint8_t> data(msg_len);
|
|
124
|
+
total = 0;
|
|
125
|
+
while (total < msg_len) {
|
|
126
|
+
auto [n, rerr] = stream->Read(data.data() + total, msg_len - total);
|
|
127
|
+
if (rerr != yamux::Error::OK || n == 0) break;
|
|
128
|
+
total += n;
|
|
129
|
+
}
|
|
130
|
+
if (total < msg_len) {
|
|
131
|
+
stream->Close();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
std::string code(data.begin(), data.end());
|
|
136
|
+
|
|
137
|
+
// Execute the JavaScript code in the webview (guarded against shutdown).
|
|
138
|
+
// Cast to webview* to call webview::execute(cstring_view) instead of
|
|
139
|
+
// smartview::execute(format_string) which has a consteval constructor
|
|
140
|
+
// that breaks std::thread lambdas in C++23.
|
|
141
|
+
{
|
|
142
|
+
std::lock_guard<std::mutex> lock(*webview_mtx);
|
|
143
|
+
if (webview_alive->load()) {
|
|
144
|
+
static_cast<saucer::webview*>(webview_ptr)->execute(code);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Write a simple "ok" response.
|
|
149
|
+
std::string resp = "ok";
|
|
150
|
+
uint8_t resp_len_buf[4];
|
|
151
|
+
uint32_t resp_len = static_cast<uint32_t>(resp.size());
|
|
152
|
+
std::memcpy(resp_len_buf, &resp_len, 4);
|
|
153
|
+
stream->Write(resp_len_buf, 4);
|
|
154
|
+
stream->Write(reinterpret_cast<const uint8_t*>(resp.data()), resp.size());
|
|
155
|
+
stream->Close();
|
|
156
|
+
}).detach();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
accept_thread.detach();
|
|
160
|
+
|
|
85
161
|
window->show();
|
|
86
162
|
co_await app->finish();
|
|
87
163
|
|
|
88
|
-
//
|
|
164
|
+
// Shutdown: close session first (causes Accept/Read/Write to return errors,
|
|
165
|
+
// winding down detached threads), then mark webview as dead.
|
|
89
166
|
session->Close();
|
|
167
|
+
{
|
|
168
|
+
std::lock_guard<std::mutex> lock(*webview_mtx);
|
|
169
|
+
webview_alive->store(false);
|
|
170
|
+
}
|
|
90
171
|
pipe.close();
|
|
91
172
|
}
|
|
92
173
|
|
|
93
174
|
int main() {
|
|
94
175
|
auto app_result = saucer::application::create({.id = "bldr"});
|
|
95
176
|
if (!app_result) {
|
|
96
|
-
std::cerr << "
|
|
177
|
+
std::cerr << "[bldr-saucer] failed to create application" << std::endl;
|
|
97
178
|
return 1;
|
|
98
179
|
}
|
|
99
180
|
return app_result->run(start);
|
package/src/pipe_client.cpp
CHANGED
|
@@ -20,7 +20,8 @@ PipeClient::~PipeClient() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
bool PipeClient::connect(const std::string& pipe_path) {
|
|
23
|
-
std::lock_guard<std::mutex>
|
|
23
|
+
std::lock_guard<std::mutex> rlock(read_mtx_);
|
|
24
|
+
std::lock_guard<std::mutex> wlock(write_mtx_);
|
|
24
25
|
|
|
25
26
|
#ifdef _WIN32
|
|
26
27
|
// Windows named pipe connection
|
|
@@ -40,7 +41,7 @@ bool PipeClient::connect(const std::string& pipe_path) {
|
|
|
40
41
|
return false;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
// Set pipe
|
|
44
|
+
// Set pipe to byte read mode.
|
|
44
45
|
DWORD mode = PIPE_READMODE_BYTE;
|
|
45
46
|
if (!SetNamedPipeHandleState(handle_, &mode, nullptr, nullptr)) {
|
|
46
47
|
std::cerr << "Failed to set pipe mode: " << GetLastError() << std::endl;
|
|
@@ -82,9 +83,22 @@ bool PipeClient::connect(const std::string& pipe_path) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
void PipeClient::close() {
|
|
85
|
-
|
|
86
|
+
// Set disconnected first so blocked reads/writes return.
|
|
86
87
|
connected_ = false;
|
|
87
88
|
|
|
89
|
+
#ifndef _WIN32
|
|
90
|
+
// Shut down the socket to unblock any thread in a blocking read.
|
|
91
|
+
// This is safe to call without holding locks since shutdown on a
|
|
92
|
+
// valid fd is thread-safe and causes blocked read/write to return.
|
|
93
|
+
int fd = fd_;
|
|
94
|
+
if (fd >= 0) {
|
|
95
|
+
::shutdown(fd, SHUT_RDWR);
|
|
96
|
+
}
|
|
97
|
+
#endif
|
|
98
|
+
|
|
99
|
+
std::lock_guard<std::mutex> rlock(read_mtx_);
|
|
100
|
+
std::lock_guard<std::mutex> wlock(write_mtx_);
|
|
101
|
+
|
|
88
102
|
#ifdef _WIN32
|
|
89
103
|
if (handle_ != INVALID_HANDLE_VALUE) {
|
|
90
104
|
CloseHandle(handle_);
|
|
@@ -107,7 +121,7 @@ std::vector<uint8_t> PipeClient::read() {
|
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
std::vector<uint8_t> PipeClient::read_with_timeout(int timeout_ms) {
|
|
110
|
-
std::lock_guard<std::mutex> lock(
|
|
124
|
+
std::lock_guard<std::mutex> lock(read_mtx_);
|
|
111
125
|
std::vector<uint8_t> result;
|
|
112
126
|
|
|
113
127
|
if (!connected_) {
|
|
@@ -186,19 +200,27 @@ std::vector<uint8_t> PipeClient::read_with_timeout(int timeout_ms) {
|
|
|
186
200
|
}
|
|
187
201
|
|
|
188
202
|
bool PipeClient::write(const uint8_t* data, size_t length) {
|
|
189
|
-
std::lock_guard<std::mutex> lock(
|
|
203
|
+
std::lock_guard<std::mutex> lock(write_mtx_);
|
|
190
204
|
|
|
191
205
|
if (!connected_ || data == nullptr || length == 0) {
|
|
192
206
|
return false;
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
#ifdef _WIN32
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
size_t total_written = 0;
|
|
211
|
+
while (total_written < length) {
|
|
212
|
+
DWORD bytes_written = 0;
|
|
213
|
+
if (!WriteFile(handle_, data + total_written, (DWORD)(length - total_written), &bytes_written, nullptr)) {
|
|
214
|
+
connected_ = false;
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (bytes_written == 0) {
|
|
218
|
+
connected_ = false;
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
total_written += bytes_written;
|
|
200
222
|
}
|
|
201
|
-
return
|
|
223
|
+
return true;
|
|
202
224
|
#else
|
|
203
225
|
size_t total_written = 0;
|
|
204
226
|
while (total_written < length) {
|
package/src/pipe_client.h
CHANGED
package/src/scheme_forwarder.cpp
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
#include "scheme_forwarder.h"
|
|
2
2
|
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cctype>
|
|
3
5
|
#include <cstring>
|
|
4
|
-
#include <iostream>
|
|
5
6
|
|
|
6
7
|
namespace bldr {
|
|
7
8
|
|
|
9
|
+
// toLower returns a lowercase copy of the string.
|
|
10
|
+
static std::string toLower(const std::string& s) {
|
|
11
|
+
std::string out = s;
|
|
12
|
+
std::transform(out.begin(), out.end(), out.begin(),
|
|
13
|
+
[](unsigned char c) { return std::tolower(c); });
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// sendError sends an error response to the saucer writer.
|
|
18
|
+
static void sendError(saucer::scheme::stream_writer& writer, int status) {
|
|
19
|
+
writer.start({.mime = "text/plain", .status = status});
|
|
20
|
+
writer.finish();
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
void SchemeForwarder::forward(const saucer::scheme::request& req,
|
|
9
24
|
saucer::scheme::stream_writer& writer) {
|
|
10
25
|
// Open a new yamux stream.
|
|
11
26
|
auto [stream, err] = session_->OpenStream();
|
|
12
27
|
if (err != yamux::Error::OK || !stream) {
|
|
13
|
-
|
|
14
|
-
writer.start({.mime = "text/plain", .status = 502});
|
|
15
|
-
writer.finish();
|
|
28
|
+
sendError(writer, 502);
|
|
16
29
|
return;
|
|
17
30
|
}
|
|
18
31
|
|
|
@@ -33,10 +46,8 @@ void SchemeForwarder::forward(const saucer::scheme::request& req,
|
|
|
33
46
|
// Serialize and send FetchRequestInfo frame.
|
|
34
47
|
auto reqInfoMsg = proto::EncodeFetchRequest_Info(info);
|
|
35
48
|
if (!writeFrame(stream.get(), reqInfoMsg)) {
|
|
36
|
-
std::cerr << "[forwarder] failed to write request info" << std::endl;
|
|
37
49
|
stream->Close();
|
|
38
|
-
writer
|
|
39
|
-
writer.finish();
|
|
50
|
+
sendError(writer, 502);
|
|
40
51
|
return;
|
|
41
52
|
}
|
|
42
53
|
|
|
@@ -51,10 +62,8 @@ void SchemeForwarder::forward(const saucer::scheme::request& req,
|
|
|
51
62
|
|
|
52
63
|
auto reqDataMsg = proto::EncodeFetchRequest_Data(bodyData);
|
|
53
64
|
if (!writeFrame(stream.get(), reqDataMsg)) {
|
|
54
|
-
std::cerr << "[forwarder] failed to write request body" << std::endl;
|
|
55
65
|
stream->Close();
|
|
56
|
-
writer
|
|
57
|
-
writer.finish();
|
|
66
|
+
sendError(writer, 502);
|
|
58
67
|
return;
|
|
59
68
|
}
|
|
60
69
|
}
|
|
@@ -67,16 +76,15 @@ void SchemeForwarder::forward(const saucer::scheme::request& req,
|
|
|
67
76
|
std::vector<uint8_t> frame;
|
|
68
77
|
if (!readFrame(stream.get(), frame)) {
|
|
69
78
|
if (!started) {
|
|
70
|
-
writer
|
|
79
|
+
sendError(writer, 502);
|
|
71
80
|
}
|
|
72
81
|
break;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
proto::FetchResponse resp;
|
|
76
85
|
if (!proto::DecodeFetchResponse(frame.data(), frame.size(), resp)) {
|
|
77
|
-
std::cerr << "[forwarder] failed to decode response" << std::endl;
|
|
78
86
|
if (!started) {
|
|
79
|
-
writer
|
|
87
|
+
sendError(writer, 502);
|
|
80
88
|
}
|
|
81
89
|
break;
|
|
82
90
|
}
|
|
@@ -85,20 +93,13 @@ void SchemeForwarder::forward(const saucer::scheme::request& req,
|
|
|
85
93
|
if (resp.has_info && !started) {
|
|
86
94
|
started = true;
|
|
87
95
|
|
|
88
|
-
//
|
|
96
|
+
// Extract Content-Type header (case-insensitive).
|
|
89
97
|
std::string mime = "application/octet-stream";
|
|
90
|
-
auto it = resp.info.headers.find("Content-Type");
|
|
91
|
-
if (it == resp.info.headers.end()) {
|
|
92
|
-
it = resp.info.headers.find("content-type");
|
|
93
|
-
}
|
|
94
|
-
if (it != resp.info.headers.end()) {
|
|
95
|
-
mime = it->second;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Build response headers for saucer (excluding Content-Type which goes in mime).
|
|
99
98
|
std::map<std::string, std::string> hdrs;
|
|
100
99
|
for (const auto& [key, val] : resp.info.headers) {
|
|
101
|
-
if (key
|
|
100
|
+
if (toLower(key) == "content-type") {
|
|
101
|
+
mime = val;
|
|
102
|
+
} else {
|
|
102
103
|
hdrs[key] = val;
|
|
103
104
|
}
|
|
104
105
|
}
|
|
@@ -127,7 +128,9 @@ void SchemeForwarder::forward(const saucer::scheme::request& req,
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
if (started) {
|
|
132
|
+
writer.finish();
|
|
133
|
+
}
|
|
131
134
|
stream->Close();
|
|
132
135
|
}
|
|
133
136
|
|
|
@@ -135,7 +138,7 @@ bool SchemeForwarder::writeFrame(yamux::Stream* stream, const std::vector<uint8_
|
|
|
135
138
|
// Write LittleEndian uint32 length prefix.
|
|
136
139
|
uint8_t lenBuf[4];
|
|
137
140
|
uint32_t msgLen = static_cast<uint32_t>(data.size());
|
|
138
|
-
std::memcpy(lenBuf, &msgLen, 4); // LE on LE platforms
|
|
141
|
+
std::memcpy(lenBuf, &msgLen, 4); // LE on LE platforms (x86_64, ARM64)
|
|
139
142
|
|
|
140
143
|
auto err = stream->Write(lenBuf, 4);
|
|
141
144
|
if (err != yamux::Error::OK) return false;
|
|
@@ -155,7 +158,7 @@ bool SchemeForwarder::readFrame(yamux::Stream* stream, std::vector<uint8_t>& out
|
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
uint32_t msgLen;
|
|
158
|
-
std::memcpy(&msgLen, lenBuf, 4); // LE on LE platforms
|
|
161
|
+
std::memcpy(&msgLen, lenBuf, 4); // LE on LE platforms (x86_64, ARM64)
|
|
159
162
|
|
|
160
163
|
if (msgLen > kMaxFrameSize) return false;
|
|
161
164
|
|