cj-tauri 2.0.0 → 2.1.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/README.md +113 -99
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +63 -52
- package/cli/cjpm.toml +22 -22
- package/cli/src/doctor.cj +308 -306
- package/cli/src/main.cj +129 -129
- package/cli/src/manifest.cj +632 -558
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -770
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +369 -369
- package/cli/src/sync.cj +526 -526
- package/cli/src/util.cj +168 -168
- package/cli/templates/app/README.md +51 -48
- package/cli/templates/app/assets/capabilities/default.json +7 -7
- package/cli/templates/app/assets/ui/index.html +120 -118
- package/cli/templates/app/cj-tauri.toml +32 -22
- package/cli/templates/app/cjpm.toml +34 -34
- package/cli/templates/app/src/main.cj +101 -90
- package/cli/templates/app-react/README.md +56 -56
- package/cli/templates/app-react/assets/capabilities/default.json +7 -7
- package/cli/templates/app-react/assets/ui/index.html +25 -25
- package/cli/templates/app-react/assets/ui/package.json +19 -19
- package/cli/templates/app-react/assets/ui/src/App.css +67 -67
- package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
- package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
- package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-react/cj-tauri.toml +33 -23
- package/cli/templates/app-react/cjpm.toml +34 -34
- package/cli/templates/app-react/src/main.cj +91 -91
- package/cli/templates/app-vue/README.md +56 -56
- package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
- package/cli/templates/app-vue/assets/ui/index.html +25 -25
- package/cli/templates/app-vue/assets/ui/package.json +18 -18
- package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
- package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
- package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-vue/cj-tauri.toml +33 -23
- package/cli/templates/app-vue/cjpm.toml +34 -34
- package/cli/templates/app-vue/src/main.cj +91 -91
- package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
- package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
- package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/README.md +84 -79
- package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
- package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
- package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
- package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
- package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
- package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
- package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
- package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -53
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -969
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -76
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +142 -142
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -607
- package/src/host_arkweb.cj +924 -778
- package/src/host_webkit.cj +663 -570
- package/src/host_webview2.cj +663 -570
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -62
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -77
- package/src/platform/platform_assets.cj +33 -33
- package/src/platform/platform_discover.cj +176 -176
- package/src/platform/platform_env.cj +29 -29
- package/src/platform/platform_feature.cj +31 -31
- package/src/platform/platform_page_source.cj +87 -87
- package/src/platform/platform_target_info.cj +32 -32
- package/src/platform_host_provider.cj +87 -87
- package/src/plugin.cj +333 -333
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +158 -158
- package/src/plugins/notification/plugin_notification.cj +154 -154
- package/src/plugins/serial/plugin_serial.cj +458 -454
- package/src/plugins/serial/serial_bridge.cj +194 -194
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +258 -258
- package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -127
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -201
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -251
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +605 -585
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/plugins/serial/serial_ohos_stub.cj +0 -61
- package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
|
@@ -1,470 +1,488 @@
|
|
|
1
|
-
package cjTauri.plugins.ws
|
|
2
|
-
|
|
3
|
-
import cjTauri.*
|
|
4
|
-
|
|
5
|
-
import std.collection.*
|
|
6
|
-
import std.sync.*
|
|
7
|
-
import stdx.encoding.json.*
|
|
8
|
-
import stdx.encoding.url.*
|
|
9
|
-
import stdx.net.http.*
|
|
10
|
-
import stdx.net.tls.*
|
|
11
|
-
// 缺它 TLS 握手时拿不到默认实现:连 ws:// 也会报
|
|
12
|
-
// "Global crypto kit is not set. If you want to use the default implementation,
|
|
13
|
-
// make sure that package `stdx.crypto.kit` is imported."(2026-10-20 cjh 接线实测)
|
|
14
|
-
import stdx.crypto.kit.*
|
|
15
|
-
|
|
16
|
-
/** 单帧(含分帧重组累计)上限:8 MiB——一条超长帧直接断连,防内存被撑爆 */
|
|
17
|
-
public let CJ_WS_MAX_FRAME: Int64 = 8 * 1024 * 1024
|
|
18
|
-
|
|
19
|
-
/** 连接状态字面量(ws:state 事件的 state 字段) */
|
|
20
|
-
public let CJ_WS_STATE_CONNECTED: String = "connected"
|
|
21
|
-
public let CJ_WS_STATE_CLOSED: String = "closed"
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 官方插件 ws:WebSocket 客户端(stdx.net.http 自带 RFC6455 实现——升级握手与帧
|
|
25
|
-
* 编解码都是库的事,本插件只做治理与并发,与 http 插件的取舍同一结论:用成熟库)
|
|
26
|
-
*
|
|
27
|
-
* 命令:ws:connect({url})/ ws:send({text})/ ws:close()
|
|
28
|
-
* 事件:ws:frame(对端帧——cjh 这类 JSON 帧原样透传,前端按 type 分发;解不动的包
|
|
29
|
-
* 一层 `{"type":"raw"}`)/ ws:state(connected / closed)
|
|
30
|
-
* 权限:**不自动放行**——"permissions": ["ws:default"] 或在 commands 里明文写。
|
|
31
|
-
*
|
|
32
|
-
* 安全语义与 http 插件同构:
|
|
33
|
-
* - 主机白名单在清单 scopes.ws(判定复用共享层 scopeAllows/globMatch,host 提取用核心
|
|
34
|
-
* urlHostOf——http/https/ws/wss 同一套解析);**没写 scopes.ws = 一律拒绝**;
|
|
35
|
-
* - 只认 host 不认端口(「我信这个主机」);连接目标必须是 ws:// 或 wss://。
|
|
36
|
-
*
|
|
37
|
-
* 并发模型(mqtt 插件同款):读线程独占 read()(stdx WebSocket 不支持多线程读);
|
|
38
|
-
* 写全部在 sess.lock 内串行(不支持多线程写);读 + 写并发是全双工本身的语义
|
|
39
|
-
* (cjh 服务端同款用法:读循环线程 + 持锁写)。
|
|
40
|
-
*/
|
|
41
|
-
public class WsPlugin <: Plugin {
|
|
42
|
-
public init() {}
|
|
43
|
-
|
|
44
|
-
// 单活动连接:cjh 客户端同一时刻只连一个后端;重连须先 close(确定性优先,
|
|
45
|
-
// 不在命令里做隐式替换——旧读线程还没退出时隐式重连会制造两套 gen 竞态)
|
|
46
|
-
private let sess: WsSession = WsSession()
|
|
47
|
-
// 引用一下 DefaultCryptoKit:stdx.crypto.kit 的**包初始化**负责注册全局加密套件,
|
|
48
|
-
// 但零警告门禁要求 import 必须有符号使用——这个字段就是干这个的(不参与逻辑)
|
|
49
|
-
private let kitMarker = DefaultCryptoKit()
|
|
50
|
-
|
|
51
|
-
public func name(): String {
|
|
52
|
-
return "ws"
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
public func commands(): HashMap<String, CommandHandler> {
|
|
56
|
-
let m = HashMap<String, CommandHandler>()
|
|
57
|
-
// 键写**短名**:前缀由框架补(写全名会得到 ws:ws:connect,且所有命令都成未注册)
|
|
58
|
-
m.add("connect", WsCommands(this.sess))
|
|
59
|
-
m.add("send", WsCommands(this.sess))
|
|
60
|
-
m.add("close", WsCommands(this.sess))
|
|
61
|
-
return m
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** 命名权限集(v1.1):default = 连接 + 发送 + 关闭 */
|
|
65
|
-
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
66
|
-
let m = HashMap<String, ArrayList<String>>()
|
|
67
|
-
let all = ArrayList<String>()
|
|
68
|
-
all.add("connect")
|
|
69
|
-
all.add("send")
|
|
70
|
-
all.add("close")
|
|
71
|
-
m.add("default", all)
|
|
72
|
-
return m
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** 推事件:对端帧经 ws:frame、连接状态经 ws:state(应用要在清单 events 里放行短名) */
|
|
76
|
-
public func events(): ArrayList<String> {
|
|
77
|
-
let ev = ArrayList<String>()
|
|
78
|
-
ev.add("frame")
|
|
79
|
-
ev.add("state")
|
|
80
|
-
return ev
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** 吃 scope:主机白名单的判定在清单 scopes.ws(没写 = 全拒,见类注释) */
|
|
84
|
-
public func scopeAware(): Bool {
|
|
85
|
-
return true
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
public func setup(app: TauriApp): Unit {
|
|
89
|
-
this.sess.setApp(app)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* ws 会话:单活动连接。所有字段都在 lock 内访问(除 setApp 的写也持锁);
|
|
95
|
-
* app 是读线程回投事件的通道(mqtt 同款:命令线程装配、读线程只读引用)。
|
|
96
|
-
*/
|
|
97
|
-
public class WsSession {
|
|
98
|
-
public let lock: Mutex = Mutex()
|
|
99
|
-
public var connected: Bool = false
|
|
100
|
-
public var url: String = ""
|
|
101
|
-
public var host: String = ""
|
|
102
|
-
public var lastError: String = ""
|
|
103
|
-
public var rxCount: Int64 = 0
|
|
104
|
-
public var txCount: Int64 = 0
|
|
105
|
-
// 每次连接自增:读线程带出自己的 gen,回来发现不等 = 会话已被 close/替换,静默退出
|
|
106
|
-
public var gen: Int64 = 0
|
|
107
|
-
public var app: Option<TauriApp> = None
|
|
108
|
-
// 连接本体只在 lock 内触碰
|
|
109
|
-
public var ws: Option<WebSocket> = None
|
|
110
|
-
public var client: Option<Client> = None
|
|
111
|
-
|
|
112
|
-
public func setApp(app: TauriApp): Unit {
|
|
113
|
-
this.lock.lock()
|
|
114
|
-
this.app = Some(app)
|
|
115
|
-
this.lock.unlock()
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
case
|
|
145
|
-
case
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private func
|
|
152
|
-
this.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
throw CommandException("ws:
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.sess.
|
|
199
|
-
|
|
200
|
-
let
|
|
201
|
-
this.sess.lock.unlock()
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
this.sess.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
match (
|
|
300
|
-
case
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
sess.lock.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
func
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
let
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1
|
+
package cjTauri.plugins.ws
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import std.sync.*
|
|
7
|
+
import stdx.encoding.json.*
|
|
8
|
+
import stdx.encoding.url.*
|
|
9
|
+
import stdx.net.http.*
|
|
10
|
+
import stdx.net.tls.*
|
|
11
|
+
// 缺它 TLS 握手时拿不到默认实现:连 ws:// 也会报
|
|
12
|
+
// "Global crypto kit is not set. If you want to use the default implementation,
|
|
13
|
+
// make sure that package `stdx.crypto.kit` is imported."(2026-10-20 cjh 接线实测)
|
|
14
|
+
import stdx.crypto.kit.*
|
|
15
|
+
|
|
16
|
+
/** 单帧(含分帧重组累计)上限:8 MiB——一条超长帧直接断连,防内存被撑爆 */
|
|
17
|
+
public let CJ_WS_MAX_FRAME: Int64 = 8 * 1024 * 1024
|
|
18
|
+
|
|
19
|
+
/** 连接状态字面量(ws:state 事件的 state 字段) */
|
|
20
|
+
public let CJ_WS_STATE_CONNECTED: String = "connected"
|
|
21
|
+
public let CJ_WS_STATE_CLOSED: String = "closed"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 官方插件 ws:WebSocket 客户端(stdx.net.http 自带 RFC6455 实现——升级握手与帧
|
|
25
|
+
* 编解码都是库的事,本插件只做治理与并发,与 http 插件的取舍同一结论:用成熟库)
|
|
26
|
+
*
|
|
27
|
+
* 命令:ws:connect({url})/ ws:send({text})/ ws:close()
|
|
28
|
+
* 事件:ws:frame(对端帧——cjh 这类 JSON 帧原样透传,前端按 type 分发;解不动的包
|
|
29
|
+
* 一层 `{"type":"raw"}`)/ ws:state(connected / closed)
|
|
30
|
+
* 权限:**不自动放行**——"permissions": ["ws:default"] 或在 commands 里明文写。
|
|
31
|
+
*
|
|
32
|
+
* 安全语义与 http 插件同构:
|
|
33
|
+
* - 主机白名单在清单 scopes.ws(判定复用共享层 scopeAllows/globMatch,host 提取用核心
|
|
34
|
+
* urlHostOf——http/https/ws/wss 同一套解析);**没写 scopes.ws = 一律拒绝**;
|
|
35
|
+
* - 只认 host 不认端口(「我信这个主机」);连接目标必须是 ws:// 或 wss://。
|
|
36
|
+
*
|
|
37
|
+
* 并发模型(mqtt 插件同款):读线程独占 read()(stdx WebSocket 不支持多线程读);
|
|
38
|
+
* 写全部在 sess.lock 内串行(不支持多线程写);读 + 写并发是全双工本身的语义
|
|
39
|
+
* (cjh 服务端同款用法:读循环线程 + 持锁写)。
|
|
40
|
+
*/
|
|
41
|
+
public class WsPlugin <: Plugin {
|
|
42
|
+
public init() {}
|
|
43
|
+
|
|
44
|
+
// 单活动连接:cjh 客户端同一时刻只连一个后端;重连须先 close(确定性优先,
|
|
45
|
+
// 不在命令里做隐式替换——旧读线程还没退出时隐式重连会制造两套 gen 竞态)
|
|
46
|
+
private let sess: WsSession = WsSession()
|
|
47
|
+
// 引用一下 DefaultCryptoKit:stdx.crypto.kit 的**包初始化**负责注册全局加密套件,
|
|
48
|
+
// 但零警告门禁要求 import 必须有符号使用——这个字段就是干这个的(不参与逻辑)
|
|
49
|
+
private let kitMarker = DefaultCryptoKit()
|
|
50
|
+
|
|
51
|
+
public func name(): String {
|
|
52
|
+
return "ws"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
56
|
+
let m = HashMap<String, CommandHandler>()
|
|
57
|
+
// 键写**短名**:前缀由框架补(写全名会得到 ws:ws:connect,且所有命令都成未注册)
|
|
58
|
+
m.add("connect", WsCommands(this.sess))
|
|
59
|
+
m.add("send", WsCommands(this.sess))
|
|
60
|
+
m.add("close", WsCommands(this.sess))
|
|
61
|
+
return m
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 命名权限集(v1.1):default = 连接 + 发送 + 关闭 */
|
|
65
|
+
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
66
|
+
let m = HashMap<String, ArrayList<String>>()
|
|
67
|
+
let all = ArrayList<String>()
|
|
68
|
+
all.add("connect")
|
|
69
|
+
all.add("send")
|
|
70
|
+
all.add("close")
|
|
71
|
+
m.add("default", all)
|
|
72
|
+
return m
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 推事件:对端帧经 ws:frame、连接状态经 ws:state(应用要在清单 events 里放行短名) */
|
|
76
|
+
public func events(): ArrayList<String> {
|
|
77
|
+
let ev = ArrayList<String>()
|
|
78
|
+
ev.add("frame")
|
|
79
|
+
ev.add("state")
|
|
80
|
+
return ev
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 吃 scope:主机白名单的判定在清单 scopes.ws(没写 = 全拒,见类注释) */
|
|
84
|
+
public func scopeAware(): Bool {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public func setup(app: TauriApp): Unit {
|
|
89
|
+
this.sess.setApp(app)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* ws 会话:单活动连接。所有字段都在 lock 内访问(除 setApp 的写也持锁);
|
|
95
|
+
* app 是读线程回投事件的通道(mqtt 同款:命令线程装配、读线程只读引用)。
|
|
96
|
+
*/
|
|
97
|
+
public class WsSession {
|
|
98
|
+
public let lock: Mutex = Mutex()
|
|
99
|
+
public var connected: Bool = false
|
|
100
|
+
public var url: String = ""
|
|
101
|
+
public var host: String = ""
|
|
102
|
+
public var lastError: String = ""
|
|
103
|
+
public var rxCount: Int64 = 0
|
|
104
|
+
public var txCount: Int64 = 0
|
|
105
|
+
// 每次连接自增:读线程带出自己的 gen,回来发现不等 = 会话已被 close/替换,静默退出
|
|
106
|
+
public var gen: Int64 = 0
|
|
107
|
+
public var app: Option<TauriApp> = None
|
|
108
|
+
// 连接本体只在 lock 内触碰
|
|
109
|
+
public var ws: Option<WebSocket> = None
|
|
110
|
+
public var client: Option<Client> = None
|
|
111
|
+
|
|
112
|
+
public func setApp(app: TauriApp): Unit {
|
|
113
|
+
this.lock.lock()
|
|
114
|
+
this.app = Some(app)
|
|
115
|
+
this.lock.unlock()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 鸿蒙侧 ws 如实降级:stdx 的 WebSocket 升级依赖 OpenSSL(帧掩码随机数走 RAND_bytes),
|
|
121
|
+
* 而仓颉运行时在设备上装不了 OpenSSL 3(真机:`Can not load openssl library or function
|
|
122
|
+
* RAND_bytes`),SDK 也没有 `ohos.net.websocket` 绑定(183 个绑定逐一确认)。
|
|
123
|
+
* connect 当场拒绝并说明原因,不留「openssl 半死」的模糊报错(同 http:postStream 的口径)。
|
|
124
|
+
*/
|
|
125
|
+
@When[env == "ohos"]
|
|
126
|
+
func wsUnavailableOnOhos(): Unit {
|
|
127
|
+
throw CommandException(
|
|
128
|
+
"ws:connect is not available on HarmonyOS: stdx WebSocket requires OpenSSL (RAND_bytes), which the Cangjie runtime cannot load on-device, and there is no ohos.net.websocket binding. Use http:post for request/response traffic.")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@When[env != "ohos"]
|
|
132
|
+
func wsUnavailableOnOhos(): Unit {
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** ws 命令处理器:connect 收 {url},send 收 {text},close 无参 */
|
|
136
|
+
public class WsCommands <: CommandHandler {
|
|
137
|
+
private let sess: WsSession
|
|
138
|
+
public init(sess: WsSession) {
|
|
139
|
+
this.sess = sess
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
143
|
+
match (cmd) {
|
|
144
|
+
case "ws:connect" => return this.connect(args, ipc)
|
|
145
|
+
case "ws:send" => return this.send(args)
|
|
146
|
+
case "ws:close" => return this.close()
|
|
147
|
+
case _ => throw CommandException("unknown ws command: ${cmd}")
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func connect(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
152
|
+
let url = this.requireString(args, "url")
|
|
153
|
+
// 协议收紧:只认 ws/wss(urlHostOf 放行 http(s) 是给 http 插件用的)
|
|
154
|
+
let lower = url.toAsciiLower()
|
|
155
|
+
if (!lower.startsWith("ws://") && !lower.startsWith("wss://")) {
|
|
156
|
+
throw CommandException("ws: url must start with ws:// or wss://")
|
|
157
|
+
}
|
|
158
|
+
let host = match (urlHostOf(url)) {
|
|
159
|
+
case Some(h) => h
|
|
160
|
+
case None => throw CommandException("ws: host is outside the authorized scope")
|
|
161
|
+
}
|
|
162
|
+
this.checkHostScope(url, host, ipc)
|
|
163
|
+
return this.doConnect(url, host)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private func doConnect(url: String, host: String): JsonValue {
|
|
167
|
+
// 鸿蒙侧如实降级(见 wsUnavailableOnOhos):desktop 该函数是空实现。
|
|
168
|
+
// 放在 scope 判定**之后**——安全判定先于可用性判定,越界请求该报 scope 错而不是平台错
|
|
169
|
+
wsUnavailableOnOhos()
|
|
170
|
+
this.sess.lock.lock()
|
|
171
|
+
let busy = this.sess.connected
|
|
172
|
+
this.sess.lock.unlock()
|
|
173
|
+
if (busy) {
|
|
174
|
+
throw CommandException("ws: already connected; call ws:close first")
|
|
175
|
+
}
|
|
176
|
+
// stdx 的升级函数**本来就要求 ws/wss scheme**(报错原文 "the scheme must be
|
|
177
|
+
// ws or wss")——最初把 ws:// 转成 http:// 再传是画蛇添足,反而被库拒(实测踩过)
|
|
178
|
+
let client = ClientBuilder()
|
|
179
|
+
.tlsConfig(tlsConfigForHost(host))
|
|
180
|
+
.build()
|
|
181
|
+
var sockOpt: Option<WebSocket> = None
|
|
182
|
+
try {
|
|
183
|
+
let parsed = URL.parse(url)
|
|
184
|
+
let pair = WebSocket.upgradeFromClient(client, parsed)
|
|
185
|
+
sockOpt = Some(pair[0])
|
|
186
|
+
} catch (e: Exception) {
|
|
187
|
+
client.close()
|
|
188
|
+
eprintln("[cj-tauri] ws: connect failed: ${e}")
|
|
189
|
+
throw CommandException("ws: connect failed: ${e.message}")
|
|
190
|
+
}
|
|
191
|
+
// 登记会话并起读线程
|
|
192
|
+
this.sess.lock.lock()
|
|
193
|
+
this.sess.ws = sockOpt
|
|
194
|
+
this.sess.client = Some(client)
|
|
195
|
+
this.sess.connected = true
|
|
196
|
+
this.sess.url = url
|
|
197
|
+
this.sess.host = host
|
|
198
|
+
this.sess.lastError = ""
|
|
199
|
+
this.sess.gen += 1
|
|
200
|
+
let gen = this.sess.gen
|
|
201
|
+
this.sess.lock.unlock()
|
|
202
|
+
// 走到这里的 sockOpt 必为 Some(catch 分支已经 throw),match 里再写 None 分支会被
|
|
203
|
+
// 编译器判 unreachable pattern——零警告门禁下直接取用
|
|
204
|
+
let sock = sockOpt.getOrThrow()
|
|
205
|
+
spawn { => wsReaderThread(this.sess, sock, client, gen) }
|
|
206
|
+
eprintln("[cj-tauri] ws: connected ${url} (host=${host})")
|
|
207
|
+
wsEmitState(this.sess, CJ_WS_STATE_CONNECTED, "")
|
|
208
|
+
let obj = JsonObject()
|
|
209
|
+
obj.put("connected", JsonBool(true))
|
|
210
|
+
obj.put("url", JsonString(url))
|
|
211
|
+
return obj
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private func send(args: JsonObject): JsonValue {
|
|
215
|
+
let text = this.requireString(args, "text")
|
|
216
|
+
this.sess.lock.lock()
|
|
217
|
+
let ok = this.sendLocked(text)
|
|
218
|
+
let err = this.sess.lastError
|
|
219
|
+
this.sess.lock.unlock()
|
|
220
|
+
if (!ok) {
|
|
221
|
+
throw CommandException("ws: send failed(未连接或对端已断): ${err}")
|
|
222
|
+
}
|
|
223
|
+
let obj = JsonObject()
|
|
224
|
+
obj.put("sent", JsonBool(true))
|
|
225
|
+
return obj
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 仅在持有 sess.lock 时调用(stdx WebSocket 不支持多线程写) */
|
|
229
|
+
private func sendLocked(text: String): Bool {
|
|
230
|
+
if (!this.sess.connected) {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
match (this.sess.ws) {
|
|
234
|
+
case Some(w) => return this.writeText(w, text)
|
|
235
|
+
case None => return false
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func writeText(w: WebSocket, text: String): Bool {
|
|
240
|
+
try {
|
|
241
|
+
w.write(WebSocketFrameType.TextWebFrame, text.toArray())
|
|
242
|
+
this.sess.txCount += 1
|
|
243
|
+
return true
|
|
244
|
+
} catch (e: Exception) {
|
|
245
|
+
this.sess.lastError = "${e}"
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private func close(): JsonValue {
|
|
251
|
+
this.sess.lock.lock()
|
|
252
|
+
let was = this.sess.connected
|
|
253
|
+
let sockOpt = this.sess.ws
|
|
254
|
+
let clientOpt = this.sess.client
|
|
255
|
+
this.sess.connected = false
|
|
256
|
+
this.sess.ws = None
|
|
257
|
+
this.sess.client = None
|
|
258
|
+
this.sess.gen += 1
|
|
259
|
+
this.sess.lock.unlock()
|
|
260
|
+
// 关连接放在锁外:closeConn 可能让阻塞中的 read() 抛错,读线程随后发现 gen
|
|
261
|
+
// 变了会自行退出并兜底收资源——锁内不做任何可能阻塞的 IO
|
|
262
|
+
if (was) {
|
|
263
|
+
match (sockOpt) {
|
|
264
|
+
case Some(w) => this.closeSock(w)
|
|
265
|
+
case None => ()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
match (clientOpt) {
|
|
269
|
+
case Some(c) => c.close()
|
|
270
|
+
case None => ()
|
|
271
|
+
}
|
|
272
|
+
if (was) {
|
|
273
|
+
eprintln("[cj-tauri] ws: closed by command")
|
|
274
|
+
wsEmitState(this.sess, CJ_WS_STATE_CLOSED, "")
|
|
275
|
+
}
|
|
276
|
+
let obj = JsonObject()
|
|
277
|
+
obj.put("closed", JsonBool(was))
|
|
278
|
+
return obj
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private func closeSock(w: WebSocket): Unit {
|
|
282
|
+
try {
|
|
283
|
+
// 先走协议关(发 Close 帧)再硬关底层;客户端场景不必等对端回关
|
|
284
|
+
let empty = Array<UInt8>(0, repeat: 0)
|
|
285
|
+
w.write(WebSocketFrameType.CloseWebFrame, empty)
|
|
286
|
+
} catch (e: Exception) {
|
|
287
|
+
let _ = e
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
w.closeConn()
|
|
291
|
+
} catch (e: Exception) {
|
|
292
|
+
let _ = e
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 主机白名单判定(共享层语义:先 deny 后 allow,allow 必须显式命中) */
|
|
297
|
+
private func checkHostScope(url: String, host: String, ipc: IpcContext): Unit {
|
|
298
|
+
// 分支体保持单语句(块体会被解析器当 lambda——AGENTS §4 实测过)
|
|
299
|
+
match (ipc.scopeRuleFor("ws")) {
|
|
300
|
+
case None => this.rejectOutsideScope(url, "no host whitelist declared")
|
|
301
|
+
case Some(rule) => this.checkRuleAllows(url, host, rule)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private func checkRuleAllows(url: String, host: String, rule: ScopeRule): Unit {
|
|
306
|
+
if (!scopeAllows(rule, host)) {
|
|
307
|
+
this.rejectOutsideScope(url, host)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private func rejectOutsideScope(url: String, why: String): Nothing {
|
|
312
|
+
eprintln("[cj-tauri] scope denied (ws): ${url} -> ${why}")
|
|
313
|
+
throw CommandException("ws: host is outside the authorized scope")
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private func requireString(args: JsonObject, key: String): String {
|
|
317
|
+
match (args.get(key)) {
|
|
318
|
+
case Some(v) => match (v.kind()) {
|
|
319
|
+
case JsonKind.JsString => return v.asString().getValue()
|
|
320
|
+
case _ => throw CommandException("ws: argument \"${key}\" must be a string")
|
|
321
|
+
}
|
|
322
|
+
case None => throw CommandException("ws: missing argument \"${key}\"")
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** 推事件给页面(读线程 / 命令线程都可能调):mqtt 同款,app 引用只读 */
|
|
328
|
+
func wsEmit(sess: WsSession, event: String, obj: JsonObject): Unit {
|
|
329
|
+
var app: Option<TauriApp> = None
|
|
330
|
+
sess.lock.lock()
|
|
331
|
+
app = sess.app
|
|
332
|
+
sess.lock.unlock()
|
|
333
|
+
match (app) {
|
|
334
|
+
case Some(a) => a.emit(event, obj)
|
|
335
|
+
case None => ()
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** 推连接状态;同时落一行 stderr(页面没接住事件时 stderr 仍可对账——AGENTS §1) */
|
|
340
|
+
func wsEmitState(sess: WsSession, state: String, err: String): Unit {
|
|
341
|
+
let obj = JsonObject()
|
|
342
|
+
obj.put("state", JsonString(state))
|
|
343
|
+
sess.lock.lock()
|
|
344
|
+
obj.put("url", JsonString(sess.url))
|
|
345
|
+
obj.put("host", JsonString(sess.host))
|
|
346
|
+
obj.put("rx", JsonInt(sess.rxCount))
|
|
347
|
+
obj.put("tx", JsonInt(sess.txCount))
|
|
348
|
+
sess.lock.unlock()
|
|
349
|
+
obj.put("error", JsonString(err))
|
|
350
|
+
eprintln("[cj-tauri] ws: state=${state} error=${err}")
|
|
351
|
+
wsEmit(sess, "ws:state", obj)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** 把一帧文本投给页面:cjh 的帧是 JSON 对象——能解就原样透传(前端按 type 分发),
|
|
355
|
+
解不动的包一层 raw(原文不丢,前端自己看着办) */
|
|
356
|
+
func wsDeliver(sess: WsSession, bytes: Array<UInt8>): Unit {
|
|
357
|
+
let text = String.fromUtf8(bytes)
|
|
358
|
+
var obj = JsonObject()
|
|
359
|
+
var wrapped = false
|
|
360
|
+
try {
|
|
361
|
+
let v = JsonValue.fromStr(text)
|
|
362
|
+
match (jsonAsObject(v)) {
|
|
363
|
+
case Some(o) => obj = o
|
|
364
|
+
case None => wrapped = true
|
|
365
|
+
}
|
|
366
|
+
} catch (e: Exception) {
|
|
367
|
+
let _ = e
|
|
368
|
+
wrapped = true
|
|
369
|
+
}
|
|
370
|
+
if (wrapped) {
|
|
371
|
+
let raw = JsonObject()
|
|
372
|
+
raw.put("type", JsonString("raw"))
|
|
373
|
+
raw.put("text", JsonString(text))
|
|
374
|
+
obj = raw
|
|
375
|
+
}
|
|
376
|
+
wsEmit(sess, "ws:frame", obj)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
func jsonAsObject(v: JsonValue): Option<JsonObject> {
|
|
380
|
+
match (v.kind()) {
|
|
381
|
+
case JsonKind.JsObject => return Some(v.asObject())
|
|
382
|
+
case _ => return None
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
func wsAppend(buf: ArrayList<UInt8>, payload: Array<UInt8>): Unit {
|
|
387
|
+
for (i in 0..payload.size) {
|
|
388
|
+
buf.add(payload[i])
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 读线程:独占 sock.read()(stdx 不支持多线程读)。
|
|
394
|
+
* gen 不匹配 = 会话已被 close/替换 → 旧连接的残余帧直接丢弃退出,绝不投给页面。
|
|
395
|
+
* 分段帧(fin == false 的首帧 + ContinuationWebFrame 续帧)攒齐再投。
|
|
396
|
+
*/
|
|
397
|
+
func wsReaderThread(sess: WsSession, sock: WebSocket, client: Client, gen: Int64): Unit {
|
|
398
|
+
var buf = ArrayList<UInt8>()
|
|
399
|
+
while (true) {
|
|
400
|
+
try {
|
|
401
|
+
let frame = sock.read()
|
|
402
|
+
sess.lock.lock()
|
|
403
|
+
let stale = (sess.gen != gen) || (!sess.connected)
|
|
404
|
+
if (!stale) {
|
|
405
|
+
sess.rxCount += 1
|
|
406
|
+
}
|
|
407
|
+
sess.lock.unlock()
|
|
408
|
+
if (stale) {
|
|
409
|
+
break
|
|
410
|
+
}
|
|
411
|
+
let ft = frame.frameType
|
|
412
|
+
if (ft == WebSocketFrameType.CloseWebFrame) {
|
|
413
|
+
break
|
|
414
|
+
}
|
|
415
|
+
if (ft == WebSocketFrameType.PingWebFrame || ft == WebSocketFrameType.PongWebFrame) {
|
|
416
|
+
// 控制帧:心跳由库管,这里出现了就忽略(cjh 服务端不主动发——实测再校)
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
let payload = frame.payload // prop 不是函数(stdx 文档确认,写 () 编译不过)
|
|
420
|
+
if (payload.size <= 0) {
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
if (buf.size + payload.size > CJ_WS_MAX_FRAME) {
|
|
424
|
+
eprintln("[cj-tauri] ws: 分帧重组超限(>${CJ_WS_MAX_FRAME} 字节),断开连接")
|
|
425
|
+
break
|
|
426
|
+
}
|
|
427
|
+
if (ft == WebSocketFrameType.ContinuationWebFrame) {
|
|
428
|
+
wsAppend(buf, payload)
|
|
429
|
+
if (frame.fin) {
|
|
430
|
+
wsDeliver(sess, buf.toArray())
|
|
431
|
+
buf = ArrayList<UInt8>()
|
|
432
|
+
}
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
if (frame.fin) {
|
|
436
|
+
wsDeliver(sess, payload)
|
|
437
|
+
continue
|
|
438
|
+
}
|
|
439
|
+
// 分段帧首帧:攒着等续帧
|
|
440
|
+
wsAppend(buf, payload)
|
|
441
|
+
} catch (e: Exception) {
|
|
442
|
+
// 正常退出路径:close / 对端断开都会让阻塞中的 read 抛错
|
|
443
|
+
let _ = e
|
|
444
|
+
break
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
sess.lock.lock()
|
|
448
|
+
let mine = (sess.gen == gen) && sess.connected
|
|
449
|
+
if (mine) {
|
|
450
|
+
sess.connected = false
|
|
451
|
+
sess.ws = None
|
|
452
|
+
sess.client = None
|
|
453
|
+
sess.gen += 1
|
|
454
|
+
sess.lastError = "connection closed by peer"
|
|
455
|
+
}
|
|
456
|
+
sess.lock.unlock()
|
|
457
|
+
if (mine) {
|
|
458
|
+
eprintln("[cj-tauri] ws: connection closed by peer")
|
|
459
|
+
wsEmitState(sess, CJ_WS_STATE_CLOSED, "connection closed by peer")
|
|
460
|
+
}
|
|
461
|
+
// 资源兜底:连接已断或已被替换,double-close 的错吞掉即可
|
|
462
|
+
try {
|
|
463
|
+
sock.closeConn()
|
|
464
|
+
} catch (e: Exception) {
|
|
465
|
+
let _ = e
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
client.close()
|
|
469
|
+
} catch (e: Exception) {
|
|
470
|
+
let _ = e
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* 建 TLS 客户端配置:**域名必须显式写 `serverName`(SNI)**(与 http 插件里那份同源、同口径)。
|
|
476
|
+
*
|
|
477
|
+
* stdx 的 `TlsClientConfig()` 默认不发 SNI,多租户 HTTPS 边缘(实测 Cloudflare 后面的 api 域名)
|
|
478
|
+
* 收不到 SNI 就以 `handshake failure`(40) 拒掉握手——wss 同样中招(错误发生在 TLS 层,与
|
|
479
|
+
* WebSocket 升级无关)。IP 字面量不带 SNI(RFC 6066 不许,且直连 IP 原本可用)。
|
|
480
|
+
* 不放核心共享层的原因见 http 插件里那份的注释(核心一引用 stdx.net.tls 就会给所有应用拉上 OpenSSL)。
|
|
481
|
+
*/
|
|
482
|
+
private func tlsConfigForHost(host: String): TlsClientConfig {
|
|
483
|
+
var cfg = TlsClientConfig()
|
|
484
|
+
if (!isIpLiteralHost(host)) {
|
|
485
|
+
cfg.serverName = host
|
|
486
|
+
}
|
|
487
|
+
return cfg
|
|
488
|
+
}
|