cj-tauri 0.8.0 → 1.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.
Files changed (124) hide show
  1. package/README.md +3 -3
  2. package/cjpm.toml +52 -25
  3. package/cli/cjpm.toml +1 -1
  4. package/cli/src/doctor.cj +304 -0
  5. package/cli/src/main.cj +33 -7
  6. package/cli/src/project.cj +59 -1
  7. package/cli/src/resolve.cj +348 -304
  8. package/cli/src/scaffold.cj +139 -22
  9. package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
  10. package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
  11. package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
  12. package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
  13. package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
  14. package/cli/templates/ohos-pc/README.md +79 -0
  15. package/cli/templates/ohos-pc/build-hap.sh +190 -0
  16. package/cli/templates/ohos-pc/build-profile.json5 +46 -0
  17. package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
  18. package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
  19. package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
  20. package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
  21. package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
  22. package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
  23. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
  24. package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
  25. package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
  26. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
  27. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
  28. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
  29. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
  30. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
  31. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
  32. package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
  33. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
  34. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
  35. package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
  36. package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
  37. package/cli/templates/ohos-pc/oh-package.json5 +10 -0
  38. package/native/build_linux.sh +30 -2
  39. package/native/build_win.bat +62 -61
  40. package/native/core/bridge_core.c +1626 -0
  41. package/native/{bridge_core.h → core/bridge_core.h} +246 -6
  42. package/native/platforms/harmonyos/README.md +106 -0
  43. package/native/platforms/linux/bridge_linux.c +2485 -0
  44. package/native/platforms/windows/bridge_win.c +3248 -0
  45. package/native/tests/test_bridge_core.c +528 -1
  46. package/package.json +12 -3
  47. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  48. package/src/api_system.cj +10 -2
  49. package/src/app.cj +248 -85
  50. package/src/arkweb_proxy.cj +56 -0
  51. package/src/arkweb_script.cj +154 -0
  52. package/src/capability.cj +140 -0
  53. package/src/clipboard_bridge.cj +55 -0
  54. package/src/dragdrop.cj +26 -0
  55. package/src/embedded_assets.cj +98 -0
  56. package/src/hexutil.cj +53 -0
  57. package/src/host.cj +197 -26
  58. package/src/host_arkweb.cj +740 -0
  59. package/src/host_factory.cj +48 -0
  60. package/src/host_webkit.cj +202 -26
  61. package/src/host_webview2.cj +176 -3
  62. package/src/hotkey.cj +66 -0
  63. package/src/ipc_hub.cj +49 -0
  64. package/src/ipc_message.cj +2 -1
  65. package/src/jsutil.cj +33 -0
  66. package/src/log_bridge.cj +107 -0
  67. package/src/logging.cj +127 -0
  68. package/src/menu.cj +197 -196
  69. package/src/plugin.cj +40 -0
  70. package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
  71. package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
  72. package/src/plugins/fs/plugin_fs.cj +194 -0
  73. package/src/plugins/http/plugin_http.cj +459 -0
  74. package/src/plugins/log/plugin_log.cj +105 -0
  75. package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
  76. package/src/plugins/mqtt/mqtt_codec.cj +661 -0
  77. package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
  78. package/src/plugins/notification/plugin_notification.cj +146 -0
  79. package/src/plugins/plugins.cj +5 -0
  80. package/src/plugins/redis/plugin_redis.cj +2409 -0
  81. package/src/plugins/redis/redis_codec.cj +556 -0
  82. package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
  83. package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
  84. package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
  85. package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
  86. package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
  87. package/src/plugins/store/plugin_store.cj +287 -0
  88. package/src/plugins/ws/plugin_ws.cj +470 -0
  89. package/src/process_hidden.cj +174 -0
  90. package/src/scope.cj +391 -0
  91. package/src/shell_event.cj +69 -68
  92. package/src/tests/capability_test.cj +53 -0
  93. package/src/tests/fake_host.cj +85 -20
  94. package/src/tests/ipc_hub_test.cj +13 -0
  95. package/src/tests/jsutil_test.cj +62 -0
  96. package/src/tests/logging_test.cj +62 -0
  97. package/src/tests/mqtt_codec_test.cj +405 -0
  98. package/src/tests/plugin_clipboard_test.cj +222 -0
  99. package/src/tests/plugin_dialog_test.cj +98 -4
  100. package/src/tests/plugin_http_test.cj +124 -0
  101. package/src/tests/plugin_menu_test.cj +1 -0
  102. package/src/tests/plugin_notification_test.cj +205 -0
  103. package/src/tests/plugin_serial_test.cj +269 -267
  104. package/src/tests/plugin_shell_test.cj +189 -188
  105. package/src/tests/plugin_sqlite_test.cj +230 -0
  106. package/src/tests/plugin_store_test.cj +260 -0
  107. package/src/tests/plugin_test.cj +542 -524
  108. package/src/tests/plugin_ws_test.cj +40 -0
  109. package/src/tests/process_hidden_test.cj +129 -0
  110. package/src/tests/redis_codec_test.cj +396 -0
  111. package/src/tests/scope_test.cj +350 -0
  112. package/src/tests/shell_event_test.cj +0 -1
  113. package/src/tests/system_host_test.cj +81 -79
  114. package/src/tests/urlutil_test.cj +53 -0
  115. package/src/tests/version_test.cj +75 -65
  116. package/src/tray.cj +90 -0
  117. package/src/urlutil.cj +159 -0
  118. package/src/version.cj +23 -2
  119. package/src/window.cj +51 -42
  120. package/native/bridge_core.c +0 -797
  121. package/native/bridge_linux.c +0 -1268
  122. package/native/bridge_win.c +0 -1633
  123. package/src/plugin_fs.cj +0 -118
  124. /package/native/{bridge_js.h → core/bridge_js.h} +0 -0
@@ -1,797 +0,0 @@
1
- /*
2
- * cj-tauri C 桥公共核心(批次 1):句柄化的宿主状态 + 平台无关逻辑
3
- *
4
- * 契约见 native/bridge_core.h。本文件只放「两平台逐字相同」的东西:
5
- * 回投队列与批处理(含单条快路径)、对话框单槽状态机、窗口配置与预执行脚本存储、
6
- * 生命周期标志、以及**全部 cj_bridge_* 导出**(单一来源,两平台导出一致由此保证)。
7
- * 任何平台能力都经 cj_plat_*,本文件不 include 任何平台头。
8
- */
9
- #include "bridge_core.h"
10
-
11
- /* ===== 内部小工具 ===== */
12
-
13
- static char *cj_strdup(const char *s) {
14
- size_t n;
15
- char *p;
16
- if (!s) return NULL;
17
- n = strlen(s) + 1;
18
- p = (char *)malloc(n);
19
- if (!p) return NULL;
20
- memcpy(p, s, n);
21
- return p;
22
- }
23
-
24
- /* 回投队列整体释放(宿主销毁时兜底) */
25
- static void cj_js_queue_free(cj_host *h) {
26
- js_node *n = h->js_head;
27
- while (n) {
28
- js_node *next = n->next;
29
- free(n->js);
30
- free(n);
31
- n = next;
32
- }
33
- h->js_head = NULL;
34
- h->js_tail = NULL;
35
- }
36
-
37
- /* ===== 建 / 释放宿主 ===== */
38
-
39
- cj_host *cj_bridge_create(void) {
40
- cj_host *h = (cj_host *)calloc(1, sizeof(cj_host));
41
- if (!h) {
42
- fprintf(stderr, "[cj-bridge] create host failed: out of memory\n");
43
- return NULL;
44
- }
45
- /* 默认与两平台原先的静态初值一致 */
46
- h->win_w = 900;
47
- h->win_h = 640;
48
- h->devtools = 1;
49
- h->js_lock = cj_plat_sync_create();
50
- h->dlg_lock = cj_plat_sync_create();
51
- if (!h->js_lock || !h->dlg_lock) {
52
- if (h->js_lock) cj_plat_sync_destroy(h->js_lock);
53
- if (h->dlg_lock) cj_plat_sync_destroy(h->dlg_lock);
54
- free(h);
55
- fprintf(stderr, "[cj-bridge] create host failed: sync init failed\n");
56
- return NULL;
57
- }
58
- fprintf(stderr, "[cj-bridge] host created: %p\n", (void *)h);
59
- return h;
60
- }
61
-
62
- void cj_bridge_destroy(cj_host *h) {
63
- int i;
64
- if (!h) return;
65
- /* 平台资源先走:COM 环境引用 / 模块句柄等(幂等)。返回 0=宿主线程还在收尾,
66
- 它可能仍在用 h(GTK/WebView2 回调的 user_data 就是它)——这时连句柄一起留着不释放。 */
67
- if (!cj_plat_fini(h)) {
68
- fprintf(stderr, "[cj-bridge] destroy: host thread still running, handle kept\n");
69
- return;
70
- }
71
- if (h->js_lock) cj_plat_sync_destroy(h->js_lock);
72
- if (h->dlg_lock) cj_plat_sync_destroy(h->dlg_lock);
73
- cj_js_queue_free(h);
74
- /* dlg_cur 此时应为空:窗口销毁路径已作废并交了槽位;故意不碰,免得与等待方抢 free */
75
- free(h->win_title);
76
- free(h->win_icon);
77
- free(h->pending_menu_text);
78
- for (i = 0; i < h->init_script_count; i++) {
79
- free(h->init_scripts[i]);
80
- }
81
- free(h->init_scripts);
82
- free(h->pending_html);
83
- free(h->pending_url);
84
- fprintf(stderr, "[cj-bridge] host destroyed: %p\n", (void *)h);
85
- free(h);
86
- }
87
-
88
- void cj_bridge_init(cj_host *h, cj_on_message_fn m, cj_on_destroy_fn d) {
89
- if (!h) return;
90
- h->on_message = m;
91
- h->on_destroy = d;
92
- cj_plat_init(h); /* 平台一次性初始化(Windows 在这里装 COM vtbl 并打 init 日志) */
93
- /* 能力位:平台给静态事实(有没有菜单 / 托盘 / 拖放),core 只缓存——仓颉侧 system:host 如实上报 */
94
- h->capabilities = cj_plat_host_capabilities(h);
95
- fprintf(stderr, "[cj-bridge] host capabilities: 0x%x (menu=%d tray=%d dragDrop=%d)\n",
96
- (unsigned)h->capabilities,
97
- (h->capabilities & CJ_CAP_MENU) ? 1 : 0,
98
- (h->capabilities & CJ_CAP_TRAY) ? 1 : 0,
99
- (h->capabilities & CJ_CAP_DRAG_DROP) ? 1 : 0);
100
- }
101
-
102
- /* ===== 窗口配置 / 预执行脚本(必须在 cj_bridge_start 之前注入)===== */
103
-
104
- void cj_bridge_set_window(cj_host *h, const char *title, int width, int height) {
105
- if (!h) return;
106
- fprintf(stderr, "[cj-bridge] set window: title=%s size=%dx%d\n",
107
- title ? title : "(default)", width, height);
108
- free(h->win_title);
109
- h->win_title = NULL;
110
- if (title) {
111
- h->win_title = cj_strdup(title);
112
- }
113
- if (width > 0) h->win_w = width;
114
- if (height > 0) h->win_h = height;
115
- }
116
-
117
- void cj_bridge_set_icon(cj_host *h, const char *path) {
118
- if (!h) return;
119
- fprintf(stderr, "[cj-bridge] set icon: path=%s\n", (path && path[0]) ? path : "(default)");
120
- free(h->win_icon);
121
- h->win_icon = NULL;
122
- if (path && path[0]) {
123
- h->win_icon = cj_strdup(path);
124
- }
125
- }
126
-
127
- void cj_bridge_set_devtools(cj_host *h, int enabled) {
128
- if (!h) return;
129
- h->devtools = enabled ? 1 : 0;
130
- fprintf(stderr, "[cj-bridge] devtools %s\n", h->devtools ? "enabled" : "disabled");
131
- }
132
-
133
- void cj_bridge_add_init_script(cj_host *h, const char *js) {
134
- if (!h || !js || !js[0]) {
135
- return;
136
- }
137
- if (h->init_script_count == h->init_script_cap) {
138
- int cap = h->init_script_cap ? h->init_script_cap * 2 : 4;
139
- char **grown = (char **)realloc(h->init_scripts, (size_t)cap * sizeof(char *));
140
- if (!grown) {
141
- fprintf(stderr, "[cj-bridge] add init script failed: out of memory\n");
142
- return;
143
- }
144
- h->init_scripts = grown;
145
- h->init_script_cap = cap;
146
- }
147
- h->init_scripts[h->init_script_count] = cj_strdup(js);
148
- if (!h->init_scripts[h->init_script_count]) {
149
- fprintf(stderr, "[cj-bridge] add init script failed: out of memory\n");
150
- return;
151
- }
152
- h->init_script_count++;
153
- fprintf(stderr, "[cj-bridge] init script queued: %d bytes (total %d)\n",
154
- (int)strlen(js), h->init_script_count);
155
- }
156
-
157
- /* ===== 菜单栏(RFC-002;v1 只做菜单栏,托盘 / 拖放在后续档期)===== */
158
-
159
- void cj_bridge_set_menu(cj_host *h, const char *wire) {
160
- int rows = 0;
161
- const char *p;
162
- if (!h || !wire) return;
163
- for (p = wire; *p; p++) {
164
- if (*p == '\n') rows++;
165
- }
166
- fprintf(stderr, "[cj-bridge] set menu: %d bytes, %d row(s)\n", (int)strlen(wire), rows);
167
- free(h->pending_menu_text);
168
- h->pending_menu_text = cj_strdup(wire);
169
- /* 两头都要能用:未就绪时平台只记下(建窗时读 h->pending_menu_text),就绪后平台投到 UI 线程重建 */
170
- cj_plat_set_menu(h, h->pending_menu_text ? h->pending_menu_text : "");
171
- }
172
-
173
- void cj_bridge_set_menu_item_state(cj_host *h, const char *id, int enabled, int checked) {
174
- if (!h || !id || !id[0]) return;
175
- fprintf(stderr, "[cj-bridge] set menu item: id=%s enabled=%d checked=%d\n",
176
- id, enabled ? 1 : 0, checked ? 1 : 0);
177
- cj_plat_menu_item_state(h, id, enabled ? 1 : 0, checked ? 1 : 0);
178
- }
179
-
180
- /* ===== 运行期操作(投递细节留给平台:各平台的原生时机与日志口径不同)===== */
181
-
182
- void cj_bridge_start(cj_host *h, const char *html) {
183
- if (!h) return;
184
- if (html && html[0]) { /* 空串表示「页面由 cj_bridge_load_url 指定」 */
185
- free(h->pending_html);
186
- h->pending_html = cj_strdup(html);
187
- }
188
- cj_plat_start(h);
189
- }
190
-
191
- void cj_bridge_load_url(cj_host *h, const char *url) {
192
- if (!h || !url || !url[0]) return;
193
- free(h->pending_url);
194
- h->pending_url = cj_strdup(url);
195
- cj_plat_load_url(h, url);
196
- }
197
-
198
- void cj_bridge_reload(cj_host *h) {
199
- if (!h) return;
200
- cj_plat_reload(h);
201
- }
202
-
203
- void cj_bridge_open_devtools(cj_host *h) {
204
- if (!h) return;
205
- cj_plat_open_devtools(h);
206
- }
207
-
208
- void cj_bridge_quit(cj_host *h) {
209
- if (!h) return;
210
- h->should_quit = 1;
211
- /* 平台走与「用户关窗」同一条销毁路径,onDestroy 因此恰好触发一次(批次 1 验收项) */
212
- cj_plat_quit(h);
213
- }
214
-
215
- /* ===== 原生 → JS:入队(执行在 UI 线程的 cj_core_flush_js 里)===== */
216
-
217
- void cj_bridge_run_js(cj_host *h, const char *js) {
218
- js_node *node;
219
- int was_empty;
220
- if (!h || !js) return;
221
- node = (js_node *)calloc(1, sizeof(js_node));
222
- if (!node) return;
223
- node->js = cj_strdup(js);
224
- if (!node->js) {
225
- free(node);
226
- return;
227
- }
228
- if (!h->js_lock) {
229
- /* 未 init 时直接丢弃,避免未初始化锁 */
230
- free(node->js);
231
- free(node);
232
- return;
233
- }
234
- cj_plat_sync_lock(h->js_lock);
235
- if (h->js_tail) {
236
- h->js_tail->next = node;
237
- } else {
238
- h->js_head = node;
239
- }
240
- h->js_tail = node;
241
- was_empty = (h->js_head == node); /* 队列由空转非空:需要唤醒 UI 线程 */
242
- cj_plat_sync_unlock(h->js_lock);
243
- if (was_empty) {
244
- cj_plat_wake_ui(h); /* 平台自己判断窗口是否已就绪(未就绪就攒着) */
245
- }
246
- }
247
-
248
- int cj_bridge_is_ready(cj_host *h) {
249
- return h ? (int)h->ready : 0;
250
- }
251
-
252
- int cj_bridge_should_quit(cj_host *h) {
253
- return h ? (int)h->should_quit : 0;
254
- }
255
-
256
- /* ===== core 提供给平台的状态迁移 ===== */
257
-
258
- void cj_core_set_ready(cj_host *h) {
259
- if (h) h->ready = 1;
260
- }
261
-
262
- void cj_core_window_destroyed(cj_host *h) {
263
- if (!h) return;
264
- h->window_gone = 1;
265
- h->should_quit = 1;
266
- /* 每个宿主恰好通知一次:无论是用户关窗还是 cj_bridge_quit,都汇到这条路径 */
267
- if (!h->destroy_notified) {
268
- h->destroy_notified = 1;
269
- if (h->on_destroy) {
270
- h->on_destroy(h); /* 带上句柄:仓颉侧据此认领「是哪扇窗没了」(多窗口不许再走静态槽) */
271
- }
272
- }
273
- /* 有对话框开着时先作废并唤醒等待方:事件循环退出后挂起的回调不再执行,等待方会永远挂住 */
274
- cj_core_dialog_abort(h);
275
- }
276
-
277
- /* 线路格式的共享解析(契约见 bridge_core.h 的 cj_menu_row_fn 注释)。
278
- 就地切分一份副本:字段指针只在回调期间有效,拷贝与释放都在这里管,平台不用操心。 */
279
- void cj_core_menu_walk(const char *wire, cj_menu_row_fn fn, void *ctx) {
280
- char *copy;
281
- char *line;
282
-
283
- if (!wire || !fn) return;
284
- copy = cj_strdup(wire);
285
- if (!copy) return;
286
-
287
- line = copy;
288
- while (*line) {
289
- char *nl = strchr(line, '\n');
290
- char *cur;
291
- int depth = 0;
292
- char *t1, *t2, *t3, *t4;
293
- char *kind_s, *id_s, *label_s, *flags_s, *accel_s;
294
-
295
- if (nl) {
296
- *nl = '\0';
297
- }
298
- while (line[depth] == '\t') {
299
- depth++;
300
- }
301
- cur = line + depth;
302
- kind_s = cur;
303
- id_s = NULL;
304
- label_s = NULL;
305
- flags_s = NULL;
306
- accel_s = NULL;
307
- t1 = strchr(kind_s, '\t');
308
- if (t1) {
309
- *t1 = '\0';
310
- id_s = t1 + 1;
311
- t2 = strchr(id_s, '\t');
312
- if (t2) {
313
- *t2 = '\0';
314
- label_s = t2 + 1;
315
- t3 = strchr(label_s, '\t');
316
- if (t3) {
317
- *t3 = '\0';
318
- flags_s = t3 + 1;
319
- t4 = strchr(flags_s, '\t');
320
- if (t4) {
321
- *t4 = '\0';
322
- accel_s = t4 + 1;
323
- }
324
- }
325
- }
326
- }
327
- if (kind_s[0]) { /* kind 为空的行整行跳过;缺字段按空串 / 0——容错优于崩溃 */
328
- fn(ctx, depth, kind_s[0], id_s ? id_s : "", label_s ? label_s : "",
329
- flags_s ? atoi(flags_s) : 0, accel_s ? accel_s : "");
330
- }
331
-
332
- if (!nl) {
333
- break;
334
- }
335
- line = nl + 1;
336
- }
337
- free(copy);
338
- }
339
-
340
- /* shell 事件编成 JSON(RFC-002 §5.5):id 做最小转义——应用给的标识里出现 \ 或 " 时
341
- 不至于把载荷弄坏。(拖放的 paths 到 7.C 才落地,届时照此扩一份路径转义。) */
342
- static void cj_json_escape_into(char *dst, size_t cap, const char *src) {
343
- size_t o = 0;
344
- const char *p;
345
- if (!dst || cap == 0) return;
346
- for (p = src ? src : ""; *p; p++) {
347
- if (o + 2 >= cap) break;
348
- if (*p == '\\' || *p == '"') {
349
- dst[o++] = '\\';
350
- }
351
- dst[o++] = *p;
352
- }
353
- dst[o] = '\0';
354
- }
355
-
356
- void cj_core_menu_clicked(cj_host *h, const char *id, int enabled, int checked) {
357
- char idbuf[256];
358
- char payload[384];
359
- if (!h) return;
360
- if (!id || !id[0]) {
361
- return; /* 没有 id 就没有「被点的项」(分隔线与子菜单不会走到这里)*/
362
- }
363
- fprintf(stderr, "[cj-bridge] menu clicked: id=%s enabled=%d checked=%d\n",
364
- id, enabled ? 1 : 0, checked ? 1 : 0);
365
- if (!h->on_shell) {
366
- return; /* 未注册回调:上面那行日志已经落盘,够诊断了 */
367
- }
368
- cj_json_escape_into(idbuf, sizeof(idbuf), id);
369
- snprintf(payload, sizeof(payload),
370
- "{\"kind\":\"menu\",\"id\":\"%s\",\"enabled\":%s,\"checked\":%s}",
371
- idbuf, enabled ? "true" : "false", checked ? "true" : "false");
372
- /* 带上宿主句柄:仓颉侧按句柄分发到对应窗口,不再依赖进程级静态槽 */
373
- h->on_shell(h, payload);
374
- }
375
-
376
- void cj_core_dialog_abort(cj_host *h) {
377
- if (!h || !h->dlg_lock) return;
378
- cj_plat_sync_lock(h->dlg_lock);
379
- if (h->dlg_cur && !h->dlg_ready) {
380
- h->dlg_cur->ok = 0;
381
- h->dlg_ready = 1;
382
- h->dlg_cur = NULL;
383
- cj_plat_sync_signal(h->dlg_lock);
384
- }
385
- cj_plat_sync_unlock(h->dlg_lock);
386
- }
387
-
388
- /* ===== 回投队列:一次空闲窗口拼批回投(UI 线程调用)===== */
389
-
390
- void cj_core_flush_js(cj_host *h) {
391
- js_node *batch[CJ_JS_BATCH_MAX];
392
- char *single = NULL; /* 快路径:接管字符串所有权,省掉一次拷贝 */
393
- char *script = NULL;
394
- size_t len = 0;
395
- size_t cap = 0;
396
- int n = 0;
397
- int more = 0;
398
- int i, j;
399
-
400
- if (!h || !h->js_lock) return;
401
-
402
- cj_plat_sync_lock(h->js_lock);
403
- if (h->js_head) {
404
- js_node *node = h->js_head;
405
- h->js_head = node->next;
406
- if (!h->js_head) {
407
- h->js_tail = NULL;
408
- single = node->js; /* 队里只有这一条 */
409
- free(node);
410
- } else {
411
- batch[n++] = node;
412
- while (h->js_head && n < CJ_JS_BATCH_MAX) {
413
- node = h->js_head;
414
- h->js_head = node->next;
415
- if (!h->js_head) {
416
- h->js_tail = NULL;
417
- }
418
- batch[n++] = node;
419
- }
420
- }
421
- }
422
- more = (h->js_head != NULL);
423
- cj_plat_sync_unlock(h->js_lock);
424
-
425
- if (single) {
426
- cj_plat_post_js(h, single);
427
- free(single);
428
- } else if (n > 0) {
429
- /* 拼批:一段脚本只跨一次语言边界,省掉 n-1 次固定开销。
430
- 为什么不用 GString 之类的缓冲:core 不依赖平台库(glib / Win32)。 */
431
- for (i = 0; i < n; i++) {
432
- size_t nl = strlen(batch[i]->js);
433
- if (len + nl + 2 > cap) {
434
- size_t ncap = (len + nl + 2) * 2;
435
- char *grown = (char *)realloc(script, ncap);
436
- if (!grown) {
437
- /* 内存不够:退回逐条执行——宁可多花开销,也不丢回投(回投丢了前端会一直 pending)*/
438
- for (j = i; j < n; j++) {
439
- cj_plat_post_js(h, batch[j]->js);
440
- }
441
- for (j = 0; j < n; j++) {
442
- free(batch[j]->js);
443
- free(batch[j]);
444
- }
445
- free(script);
446
- if (more) cj_plat_wake_ui(h);
447
- return;
448
- }
449
- script = grown;
450
- cap = ncap;
451
- }
452
- memcpy(script + len, batch[i]->js, nl);
453
- len += nl;
454
- script[len++] = '\n';
455
- script[len] = '\0';
456
- free(batch[i]->js);
457
- free(batch[i]);
458
- }
459
- cj_plat_post_js(h, script);
460
- free(script);
461
- }
462
-
463
- /* 队列没取空:自续一次,剩下的下一批走 */
464
- if (more) {
465
- cj_plat_wake_ui(h);
466
- }
467
- }
468
-
469
- /* ===== 原生对话框:单槽状态机(模态,同时只允许一个)===== */
470
-
471
- static dlg_req *cj_dlg_req_new(int kind, const char *title, const char *message, const char *filter) {
472
- dlg_req *r = (dlg_req *)calloc(1, sizeof(dlg_req));
473
- if (!r) return NULL;
474
- r->kind = kind;
475
- r->title = cj_strdup(title ? title : "");
476
- r->message = cj_strdup(message ? message : "");
477
- r->filter = cj_strdup(filter ? filter : "");
478
- return r;
479
- }
480
-
481
- static void cj_dlg_req_free(dlg_req *r) {
482
- if (!r) return;
483
- free(r->title);
484
- free(r->message);
485
- free(r->filter);
486
- free(r->path);
487
- free(r);
488
- }
489
-
490
- static void cj_dlg_log_closed(const dlg_req *r) {
491
- if (r->kind == 0 || r->kind == 1) { /* 文件类才有路径 */
492
- fprintf(stderr, "[cj-bridge] dialog closed: kind=%d ok=%d path=%s\n",
493
- r->kind, r->ok, r->path ? r->path : "(none)");
494
- } else {
495
- fprintf(stderr, "[cj-bridge] dialog closed: kind=%d ok=%d\n", r->kind, r->ok);
496
- }
497
- }
498
-
499
- void cj_bridge_set_dialog_callback(cj_host *h, cj_on_dialog_fn cb) {
500
- if (!h) return;
501
- h->on_dialog = cb;
502
- }
503
-
504
- void cj_bridge_set_shell_callback(cj_host *h, cj_on_shell_fn cb) {
505
- if (!h) return;
506
- h->on_shell = cb;
507
- }
508
-
509
- int cj_bridge_host_capabilities(cj_host *h) {
510
- return h ? h->capabilities : 0;
511
- }
512
-
513
- int cj_bridge_host_eq(cj_host *a, cj_host *b) {
514
- /* 只比指针本身:句柄由 core 分配、生命周期也归 core,仓颉侧只当不透明凭据使 */
515
- return (a && b && a == b) ? 1 : 0;
516
- }
517
-
518
- int cj_bridge_show_dialog(cj_host *h, int kind, const char *title,
519
- const char *message, const char *filter) {
520
- dlg_req *r;
521
- int ok;
522
-
523
- if (!h) return 0;
524
- if (!h->ready) { /* 没有窗口就没有父窗口,模态对话框无处安放 */
525
- fprintf(stderr, "[cj-bridge] dialog ignored: host not ready\n");
526
- return 0;
527
- }
528
-
529
- cj_plat_sync_lock(h->dlg_lock);
530
- if (h->dlg_busy) {
531
- cj_plat_sync_unlock(h->dlg_lock);
532
- fprintf(stderr, "[cj-bridge] dialog rejected: another dialog is open\n");
533
- return 0;
534
- }
535
- h->dlg_busy = 1;
536
- h->dlg_ready = 0;
537
- cj_plat_sync_unlock(h->dlg_lock);
538
-
539
- /* 调用方已经站在 UI 线程上(WebKit 的 script-message / WebView2 的 WebMessageReceived 就跑在那里):
540
- 直接弹。这里**不能**走「投递 + 阻塞等待」——等待的事件得由 UI 线程处理,而 UI 线程正卡在这次调用里,
541
- 等于自己把自己锁死。 */
542
- if (cj_plat_is_ui_thread(h)) {
543
- fprintf(stderr, "[cj-bridge] dialog: kind=%d title=%s (caller on ui thread)\n",
544
- kind, title ? title : "");
545
- r = cj_dlg_req_new(kind, title, message, filter);
546
- if (!r) {
547
- cj_plat_sync_lock(h->dlg_lock);
548
- h->dlg_busy = 0;
549
- cj_plat_sync_unlock(h->dlg_lock);
550
- return 0;
551
- }
552
- r->ok = cj_plat_run_dialog(h, r);
553
- ok = r->ok;
554
- cj_dlg_log_closed(r);
555
- if (h->on_dialog) {
556
- h->on_dialog(h, r->path ? r->path : ""); /* 带上句柄:结果只落发起这次对话框的窗口 */
557
- }
558
- cj_dlg_req_free(r);
559
- cj_plat_sync_lock(h->dlg_lock);
560
- h->dlg_busy = 0;
561
- cj_plat_sync_unlock(h->dlg_lock);
562
- return ok;
563
- }
564
-
565
- r = cj_dlg_req_new(kind, title, message, filter);
566
- if (!r) {
567
- cj_plat_sync_lock(h->dlg_lock);
568
- h->dlg_busy = 0;
569
- cj_plat_sync_unlock(h->dlg_lock);
570
- return 0;
571
- }
572
- fprintf(stderr, "[cj-bridge] dialog: kind=%d title=%s\n", kind, r->title);
573
- cj_plat_sync_lock(h->dlg_lock);
574
- h->dlg_cur = r;
575
- cj_plat_sync_unlock(h->dlg_lock);
576
-
577
- cj_plat_post_dialog(h, r);
578
-
579
- /* 等结果:窗口先关的话,cj_core_window_destroyed 会作废并唤醒(ok=0) */
580
- cj_plat_sync_lock(h->dlg_lock);
581
- cj_plat_sync_wait(h->dlg_lock, &h->dlg_ready);
582
- ok = r->ok;
583
- h->dlg_busy = 0;
584
- cj_plat_sync_unlock(h->dlg_lock);
585
- cj_dlg_req_free(r);
586
- return ok;
587
- }
588
-
589
- void cj_core_dialog_tick(cj_host *h, dlg_req *req) {
590
- int alive;
591
- if (!h || !req) return;
592
-
593
- cj_plat_sync_lock(h->dlg_lock);
594
- alive = (h->dlg_cur == req);
595
- cj_plat_sync_unlock(h->dlg_lock);
596
- if (!alive) {
597
- /* 窗口先关了:槽位已交回调用方(它会 free 这个请求)——这里连碰都不能碰 */
598
- return;
599
- }
600
-
601
- req->ok = cj_plat_run_dialog(h, req);
602
- cj_dlg_log_closed(req);
603
- if (h->on_dialog) {
604
- h->on_dialog(h, req->path ? req->path : "");
605
- }
606
-
607
- cj_plat_sync_lock(h->dlg_lock);
608
- h->dlg_cur = NULL;
609
- h->dlg_ready = 1;
610
- cj_plat_sync_signal(h->dlg_lock);
611
- cj_plat_sync_unlock(h->dlg_lock);
612
- }
613
-
614
- /* =========================================================================
615
- * 串口(plugin_serial):路径白名单 + 参数校验 + 导出转发
616
- *
617
- * 句柄表与 fd 全在平台层(那边本来就有 pthread / CS),本文件**不持有任何串口状态**——
618
- * 因此也不需要「一次性初始化」这种钩子,导出函数可以在没有宿主的进程里直接用。
619
- * ========================================================================= */
620
-
621
- #if defined(_MSC_VER)
622
- #define CJ_THREAD_LOCAL __declspec(thread)
623
- #else
624
- #define CJ_THREAD_LOCAL __thread
625
- #endif
626
-
627
- /* 线程局部:命令各自跑在 worker 线程上,静态单槽会被并发调用互相覆盖(AGENTS §4 的静态单槽坑) */
628
- static CJ_THREAD_LOCAL char g_serial_err[256];
629
-
630
- static void cj_serial_err_set(const char *msg) {
631
- if (!msg) {
632
- g_serial_err[0] = '\0';
633
- return;
634
- }
635
- snprintf(g_serial_err, sizeof(g_serial_err), "%s", msg);
636
- }
637
-
638
- /* 白名单前缀表。**只放行设备节点类路径**:页面递进来的是页面可控字符串,放行面越小越好。
639
- 注意 "\\.\COM" 与 "COM" 两条要额外约束「后面全是数字」,否则 COMfoo 这种也会被放行。 */
640
- static const char *const CJ_SERIAL_PREFIXES[] = {
641
- "/dev/ttyUSB", "/dev/ttyACM", "/dev/ttyS", "/dev/ttyAMA", "/dev/ttyPS",
642
- "/dev/ttyTHS", "/dev/ttyXRUSB", "/dev/ttyV", /* Linux 串口(含虚拟 ttyV*)*/
643
- "/dev/tty.", "/dev/cu.", /* macOS:调制解调器 / 呼出设备 */
644
- "/dev/serial/by-id/", "/dev/serial/by-path/", /* udev 稳定符号链接(推荐写法)*/
645
- "/dev/pts/", /* 伪终端:socat / openpty 造的虚拟串口 */
646
- "COM", "\\\\.\\COM", /* Windows:COM3 / 长名形式 */
647
- };
648
-
649
- #define CJ_SERIAL_MAX_PATH 255
650
-
651
- int cj_serial_path_allowed(const char *path) {
652
- size_t i, n;
653
-
654
- if (!path || !path[0]) return 0;
655
- n = strlen(path);
656
- if (n > CJ_SERIAL_MAX_PATH) return 0;
657
- /* 含 ".." 一律拒绝:否则 "/dev/ttyUSB0/../../etc/shadow" 这种前缀命中就能穿越出去 */
658
- if (strstr(path, "..")) return 0;
659
-
660
- for (i = 0; i < sizeof(CJ_SERIAL_PREFIXES) / sizeof(CJ_SERIAL_PREFIXES[0]); i++) {
661
- const char *pfx = CJ_SERIAL_PREFIXES[i];
662
- size_t pl = strlen(pfx);
663
- const char *rest;
664
- size_t k;
665
-
666
- if (strncmp(path, pfx, pl) != 0) continue;
667
- rest = path + pl;
668
- if (!rest[0]) return 0; /* 前缀本身不是设备名("/dev/ttyS" 这种不收)*/
669
-
670
- if (strcmp(pfx, "COM") != 0 && strcmp(pfx, "\\\\.\\COM") != 0) {
671
- return 1;
672
- }
673
- for (k = 0; rest[k]; k++) {
674
- if (rest[k] < '0' || rest[k] > '9') return 0;
675
- if (k >= 3) return 0; /* COM1..COM999(Windows 实际到 COM256)*/
676
- }
677
- return 1;
678
- }
679
- return 0;
680
- }
681
-
682
- static int cj_serial_cfg_ok(const cj_serial_cfg *cfg) {
683
- if (!cfg) return 0;
684
- if (cfg->baud <= 0) return 0;
685
- if (cfg->data_bits < 5 || cfg->data_bits > 8) return 0;
686
- if (cfg->parity < 0 || cfg->parity > 2) return 0;
687
- if (cfg->stop_bits != 1 && cfg->stop_bits != 2) return 0;
688
- return 1;
689
- }
690
-
691
- long long cj_bridge_serial_open(const char *path, int baud, int data_bits, int parity, int stop_bits) {
692
- cj_serial_cfg cfg;
693
- char err[256];
694
- long long h;
695
-
696
- g_serial_err[0] = '\0';
697
- if (!path || !path[0]) {
698
- cj_serial_err_set("serial: 设备路径为空");
699
- return -1;
700
- }
701
- if (!cj_serial_path_allowed(path)) {
702
- snprintf(g_serial_err, sizeof(g_serial_err),
703
- "serial: 路径不在白名单内(只放行串口设备节点 / 伪终端,且不得含 ..): %s", path);
704
- return -1;
705
- }
706
- cfg.baud = baud;
707
- cfg.data_bits = data_bits;
708
- cfg.parity = parity;
709
- cfg.stop_bits = stop_bits;
710
- if (!cj_serial_cfg_ok(&cfg)) {
711
- cj_serial_err_set("serial: 串口参数非法(baud>0 / dataBits 5..8 / parity 0|1|2 / stopBits 1|2)");
712
- return -1;
713
- }
714
-
715
- err[0] = '\0';
716
- h = cj_plat_serial_open(path, &cfg, err, (int)sizeof(err));
717
- if (h <= 0) {
718
- cj_serial_err_set(err[0] ? err : "serial: 平台打开失败");
719
- /* 失败也留一行:串口问题多半要拿 stderr 对账(AGENTS §1 的取证口径)*/
720
- fprintf(stderr, "[cj-bridge] serial open failed: path=%s baud=%d code=%lld (%s)\n",
721
- path, baud, h, g_serial_err);
722
- return h < 0 ? h : -3;
723
- }
724
- fprintf(stderr, "[cj-bridge] serial open: handle=%lld path=%s baud=%d data=%d parity=%d stop=%d\n",
725
- h, path, baud, data_bits, parity, stop_bits);
726
- return h;
727
- }
728
-
729
- int cj_bridge_serial_read(long long h, unsigned char *buf, int len, int timeout_ms) {
730
- char err[256];
731
- int n;
732
-
733
- g_serial_err[0] = '\0';
734
- if (h <= 0 || !buf || len <= 0 || len > (1 << 20)) {
735
- cj_serial_err_set("serial: read 参数非法(handle>0 / len 1..1048576)");
736
- return -1;
737
- }
738
- if (timeout_ms < 0) timeout_ms = 0;
739
- err[0] = '\0';
740
- n = cj_plat_serial_read(h, buf, len, timeout_ms, err, (int)sizeof(err));
741
- if (n < 0) {
742
- cj_serial_err_set(err[0] ? err : "serial: 读失败");
743
- fprintf(stderr, "[cj-bridge] serial read failed: handle=%lld (%s)\n", h, g_serial_err);
744
- }
745
- return n;
746
- }
747
-
748
- int cj_bridge_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms) {
749
- char err[256];
750
- int n;
751
-
752
- g_serial_err[0] = '\0';
753
- if (h <= 0 || !buf || len <= 0 || len > (1 << 20)) {
754
- cj_serial_err_set("serial: write 参数非法(handle>0 / len 1..1048576)");
755
- return -1;
756
- }
757
- if (timeout_ms < 0) timeout_ms = 0;
758
- err[0] = '\0';
759
- n = cj_plat_serial_write(h, buf, len, timeout_ms, err, (int)sizeof(err));
760
- if (n < 0) {
761
- cj_serial_err_set(err[0] ? err : "serial: 写失败");
762
- fprintf(stderr, "[cj-bridge] serial write failed: handle=%lld (%s)\n", h, g_serial_err);
763
- }
764
- return n;
765
- }
766
-
767
- int cj_bridge_serial_close(long long h) {
768
- char err[256];
769
- int rc;
770
-
771
- g_serial_err[0] = '\0';
772
- if (h <= 0) {
773
- cj_serial_err_set("serial: close 参数非法(handle 必须 >0)");
774
- return -1;
775
- }
776
- err[0] = '\0';
777
- rc = cj_plat_serial_close(h, err, (int)sizeof(err));
778
- if (rc != 0) {
779
- cj_serial_err_set(err[0] ? err : "serial: 关句柄失败");
780
- return rc < 0 ? rc : -4;
781
- }
782
- fprintf(stderr, "[cj-bridge] serial close: handle=%lld\n", h);
783
- return 0;
784
- }
785
-
786
- const char *cj_bridge_serial_last_error(void) {
787
- return g_serial_err;
788
- }
789
-
790
- int cj_bridge_serial_list(char *out, int out_len) {
791
- if (!out || out_len <= 0) {
792
- cj_serial_err_set("serial: list 参数非法(out 缓冲为空)");
793
- return -1;
794
- }
795
- out[0] = '\0';
796
- return cj_plat_serial_list(out, out_len); /* 负码原样交回(-4 = 本平台未实现) */
797
- }