cj-tauri 2.0.0 → 2.1.1

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 (157) hide show
  1. package/README.md +206 -139
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +643 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +638 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3745 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/src/api_system.cj +67 -67
  77. package/src/app.cj +335 -969
  78. package/src/app_events.cj +166 -0
  79. package/src/app_host_ops.cj +287 -0
  80. package/src/app_run.cj +283 -0
  81. package/src/app_spec.cj +76 -76
  82. package/src/arkweb_menu.cj +216 -0
  83. package/src/arkweb_script.cj +142 -142
  84. package/src/capability.cj +409 -409
  85. package/src/clipboard_bridge.cj +178 -0
  86. package/src/dialog_spec.cj +79 -0
  87. package/src/dragdrop.cj +26 -26
  88. package/src/embedded_assets.cj +98 -98
  89. package/src/host.cj +752 -607
  90. package/src/host_arkweb.cj +924 -778
  91. package/src/host_webkit.cj +663 -570
  92. package/src/host_webview2.cj +663 -570
  93. package/src/hotkey.cj +66 -66
  94. package/src/ipc_hub.cj +401 -337
  95. package/src/ipc_message.cj +286 -286
  96. package/src/launch.cj +62 -62
  97. package/src/logging.cj +127 -127
  98. package/src/menu.cj +197 -197
  99. package/src/platform/platform_app_context.cj +77 -77
  100. package/src/platform/platform_assets.cj +33 -33
  101. package/src/platform/platform_discover.cj +176 -176
  102. package/src/platform/platform_env.cj +29 -29
  103. package/src/platform/platform_feature.cj +31 -31
  104. package/src/platform/platform_page_source.cj +87 -87
  105. package/src/platform/platform_target_info.cj +32 -32
  106. package/src/platform_host_provider.cj +87 -87
  107. package/src/plugin.cj +333 -333
  108. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  109. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  110. package/src/plugins/fs/plugin_fs.cj +105 -1
  111. package/src/plugins/http/http_backend.cj +131 -0
  112. package/src/plugins/http/http_desktop.cj +195 -0
  113. package/src/plugins/http/http_ohos.cj +204 -0
  114. package/src/plugins/http/plugin_http.cj +310 -459
  115. package/src/plugins/log/plugin_log.cj +105 -105
  116. package/src/plugins/menu/plugin_menu.cj +158 -158
  117. package/src/plugins/notification/plugin_notification.cj +154 -154
  118. package/src/plugins/serial/plugin_serial.cj +458 -454
  119. package/src/plugins/serial/serial_bridge.cj +194 -194
  120. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  121. package/src/plugins/serial/serial_ohos.cj +529 -0
  122. package/src/plugins/shell/plugin_shell.cj +258 -258
  123. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  124. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  125. package/src/plugins/ws/plugin_ws.cj +488 -470
  126. package/src/process_hidden.cj +174 -174
  127. package/src/scope.cj +391 -391
  128. package/src/shell_event.cj +69 -69
  129. package/src/tests/app_spec_test.cj +130 -127
  130. package/src/tests/capability_test.cj +243 -243
  131. package/src/tests/dialog_spec_test.cj +73 -0
  132. package/src/tests/fake_host.cj +201 -201
  133. package/src/tests/ipc_hub_test.cj +563 -487
  134. package/src/tests/logging_test.cj +62 -62
  135. package/src/tests/platform_contract_test.cj +264 -251
  136. package/src/tests/plugin_clipboard_test.cj +222 -222
  137. package/src/tests/plugin_dialog_test.cj +237 -237
  138. package/src/tests/plugin_fs_test.cj +207 -0
  139. package/src/tests/plugin_http_test.cj +124 -124
  140. package/src/tests/plugin_notification_test.cj +205 -205
  141. package/src/tests/plugin_serial_test.cj +48 -0
  142. package/src/tests/plugin_test.cj +605 -585
  143. package/src/tests/plugin_ws_test.cj +40 -40
  144. package/src/tests/process_hidden_test.cj +129 -129
  145. package/src/tests/scope_test.cj +350 -350
  146. package/src/tests/system_host_test.cj +99 -81
  147. package/src/tests/urlutil_test.cj +53 -53
  148. package/src/tests/version_test.cj +75 -75
  149. package/src/tests/window_registry_test.cj +123 -123
  150. package/src/tray.cj +90 -90
  151. package/src/urlutil.cj +159 -159
  152. package/src/version.cj +44 -44
  153. package/src/window.cj +51 -51
  154. package/src/window_registry.cj +116 -116
  155. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  156. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  157. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -1,1626 +1,1681 @@
1
- /*
2
- * cj-tauri C 桥公共核心(批次 1):句柄化的宿主状态 + 平台无关逻辑
3
- *
4
- * 契约见 native/bridge_core.h。本文件只放「两平台逐字相同」的东西:
5
- * 回投队列与批处理(含单条快路径)、对话框单槽状态机、窗口配置与预执行脚本存储、
6
- * 生命周期标志、以及**全部 cj_bridge_* 导出**(单一来源,两平台导出一致由此保证)。
7
- * 任何平台能力都经 cj_plat_*,本文件不 include 任何平台头。
8
- */
9
- /* 日志落盘要用 ftruncate / fileno 这些 POSIX 名,而桩自检是按 `-std=c99` 编译的,
10
- 该标准档位下它们默认被隐藏(表象是 implicit declaration 警告)。特性宏必须写在
11
- 任何 include 之前,否则不生效。 */
12
- #define _POSIX_C_SOURCE 200809L
13
-
14
- #include "bridge_core.h"
15
-
16
- /* 日志落盘要用到的 CRT 原语(RFC-003 第 5 期)。这是**编译器运行库**的名字差异,
17
- 不是平台实现差异——core 依旧不碰 pthread / windows.h / gtk 这些宿主头。 */
18
- #include <stdarg.h>
19
- #include <errno.h>
20
- #include <time.h>
21
- #include <sys/stat.h>
22
- #include <fcntl.h>
23
- #if defined(_WIN32)
24
- #include <io.h>
25
- #include <direct.h>
26
- #define cj_log_open _open
27
- #define cj_log_dup2 _dup2
28
- #define cj_log_close _close
29
- #define cj_log_fileno _fileno
30
- #define cj_log_mkdir(path) _mkdir(path)
31
- #define cj_log_truncate(fd) _chsize((fd), 0)
32
- #define CJ_LOG_OFLAGS (_O_WRONLY | _O_CREAT | _O_APPEND)
33
- #define CJ_LOG_OMODE (_S_IREAD | _S_IWRITE)
34
- #else
35
- #include <unistd.h>
36
- #define cj_log_open open
37
- #define cj_log_dup2 dup2
38
- #define cj_log_close close
39
- #define cj_log_fileno fileno
40
- #define cj_log_mkdir(path) mkdir((path), 0755)
41
- #define cj_log_truncate(fd) ftruncate((fd), 0)
42
- #define CJ_LOG_OFLAGS (O_WRONLY | O_CREAT | O_APPEND)
43
- #define CJ_LOG_OMODE 0644
44
- #endif
45
-
46
- /* ===== 内部小工具 ===== */
47
-
48
- static char *cj_strdup(const char *s) {
49
- size_t n;
50
- char *p;
51
- if (!s) return NULL;
52
- n = strlen(s) + 1;
53
- p = (char *)malloc(n);
54
- if (!p) return NULL;
55
- memcpy(p, s, n);
56
- return p;
57
- }
58
-
59
- /* 回投队列整体释放(宿主销毁时兜底) */
60
- static void cj_js_queue_free(cj_host *h) {
61
- js_node *n = h->js_head;
62
- while (n) {
63
- js_node *next = n->next;
64
- free(n->js);
65
- free(n);
66
- n = next;
67
- }
68
- h->js_head = NULL;
69
- h->js_tail = NULL;
70
- }
71
-
72
- /* ===== 建 / 释放宿主 ===== */
73
-
74
- cj_host *cj_bridge_create(void) {
75
- cj_host *h = (cj_host *)calloc(1, sizeof(cj_host));
76
- if (!h) {
77
- cj_logf("[cj-bridge] create host failed: out of memory\n");
78
- return NULL;
79
- }
80
- /* 默认与两平台原先的静态初值一致 */
81
- h->win_w = 900;
82
- h->win_h = 640;
83
- h->devtools = 1;
84
- h->win_visible = 1; /* 默认显示:没碰窗口可见性的应用行为不变 */
85
- h->js_lock = cj_plat_sync_create();
86
- h->dlg_lock = cj_plat_sync_create();
87
- h->clip_lock = cj_plat_sync_create();
88
- if (!h->js_lock || !h->dlg_lock || !h->clip_lock) {
89
- if (h->js_lock) cj_plat_sync_destroy(h->js_lock);
90
- if (h->dlg_lock) cj_plat_sync_destroy(h->dlg_lock);
91
- if (h->clip_lock) cj_plat_sync_destroy(h->clip_lock);
92
- free(h);
93
- cj_logf("[cj-bridge] create host failed: sync init failed\n");
94
- return NULL;
95
- }
96
- cj_logf("[cj-bridge] host created: %p\n", (void *)h);
97
- return h;
98
- }
99
-
100
- void cj_bridge_destroy(cj_host *h) {
101
- int i;
102
- if (!h) return;
103
- /* 平台资源先走:COM 环境引用 / 模块句柄等(幂等)。返回 0=宿主线程还在收尾,
104
- 它可能仍在用 h(GTK/WebView2 回调的 user_data 就是它)——这时连句柄一起留着不释放。 */
105
- if (!cj_plat_fini(h)) {
106
- cj_logf("[cj-bridge] destroy: host thread still running, handle kept\n");
107
- return;
108
- }
109
- if (h->js_lock) cj_plat_sync_destroy(h->js_lock);
110
- if (h->dlg_lock) cj_plat_sync_destroy(h->dlg_lock);
111
- if (h->clip_lock) cj_plat_sync_destroy(h->clip_lock);
112
- cj_js_queue_free(h);
113
- /* dlg_cur 此时应为空:窗口销毁路径已作废并交了槽位;故意不碰,免得与等待方抢 free */
114
- free(h->win_title);
115
- free(h->win_icon);
116
- free(h->pending_menu_text);
117
- free(h->pending_hotkeys);
118
- free(h->pending_tray);
119
- for (i = 0; i < h->init_script_count; i++) {
120
- free(h->init_scripts[i]);
121
- }
122
- free(h->init_scripts);
123
- free(h->pending_html);
124
- free(h->pending_url);
125
- cj_logf("[cj-bridge] host destroyed: %p\n", (void *)h);
126
- free(h);
127
- }
128
-
129
- void cj_bridge_init(cj_host *h, cj_on_message_fn m, cj_on_destroy_fn d) {
130
- if (!h) return;
131
- h->on_message = m;
132
- h->on_destroy = d;
133
- cj_plat_init(h); /* 平台一次性初始化(Windows 在这里装 COM vtbl 并打 init 日志) */
134
- /* 能力位:平台给静态事实(有没有菜单 / 托盘 / 拖放),core 只缓存——仓颉侧 system:host 如实上报 */
135
- h->capabilities = cj_plat_host_capabilities(h);
136
- cj_logf("[cj-bridge] host capabilities: 0x%x (menu=%d tray=%d dragDrop=%d notify=%d hotkey=%d)\n",
137
- (unsigned)h->capabilities,
138
- (h->capabilities & CJ_CAP_MENU) ? 1 : 0,
139
- (h->capabilities & CJ_CAP_TRAY) ? 1 : 0,
140
- (h->capabilities & CJ_CAP_DRAG_DROP) ? 1 : 0,
141
- (h->capabilities & CJ_CAP_NOTIFICATION) ? 1 : 0,
142
- (h->capabilities & CJ_CAP_HOTKEY) ? 1 : 0);
143
- }
144
-
145
- /* ===== 窗口配置 / 预执行脚本(必须在 cj_bridge_start 之前注入)===== */
146
-
147
- void cj_bridge_set_window(cj_host *h, const char *title, int width, int height) {
148
- if (!h) return;
149
- cj_logf("[cj-bridge] set window: title=%s size=%dx%d\n",
150
- title ? title : "(default)", width, height);
151
- free(h->win_title);
152
- h->win_title = NULL;
153
- if (title) {
154
- h->win_title = cj_strdup(title);
155
- }
156
- if (width > 0) h->win_w = width;
157
- if (height > 0) h->win_h = height;
158
- }
159
-
160
- void cj_bridge_set_icon(cj_host *h, const char *path) {
161
- if (!h) return;
162
- cj_logf("[cj-bridge] set icon: path=%s\n", (path && path[0]) ? path : "(default)");
163
- free(h->win_icon);
164
- h->win_icon = NULL;
165
- if (path && path[0]) {
166
- h->win_icon = cj_strdup(path);
167
- }
168
- }
169
-
170
- void cj_bridge_set_devtools(cj_host *h, int enabled) {
171
- if (!h) return;
172
- h->devtools = enabled ? 1 : 0;
173
- cj_logf("[cj-bridge] devtools %s\n", h->devtools ? "enabled" : "disabled");
174
- }
175
-
176
- void cj_bridge_add_init_script(cj_host *h, const char *js) {
177
- if (!h || !js || !js[0]) {
178
- return;
179
- }
180
- if (h->init_script_count == h->init_script_cap) {
181
- int cap = h->init_script_cap ? h->init_script_cap * 2 : 4;
182
- char **grown = (char **)realloc(h->init_scripts, (size_t)cap * sizeof(char *));
183
- if (!grown) {
184
- cj_logf("[cj-bridge] add init script failed: out of memory\n");
185
- return;
186
- }
187
- h->init_scripts = grown;
188
- h->init_script_cap = cap;
189
- }
190
- h->init_scripts[h->init_script_count] = cj_strdup(js);
191
- if (!h->init_scripts[h->init_script_count]) {
192
- cj_logf("[cj-bridge] add init script failed: out of memory\n");
193
- return;
194
- }
195
- h->init_script_count++;
196
- cj_logf("[cj-bridge] init script queued: %d bytes (total %d)\n",
197
- (int)strlen(js), h->init_script_count);
198
- }
199
-
200
- /* ===== 菜单栏(RFC-002;v1 只做菜单栏,托盘 / 拖放在后续档期)===== */
201
-
202
- void cj_bridge_set_menu(cj_host *h, const char *wire) {
203
- int rows = 0;
204
- const char *p;
205
- if (!h || !wire) return;
206
- for (p = wire; *p; p++) {
207
- if (*p == '\n') rows++;
208
- }
209
- cj_logf("[cj-bridge] set menu: %d bytes, %d row(s)\n", (int)strlen(wire), rows);
210
- free(h->pending_menu_text);
211
- h->pending_menu_text = cj_strdup(wire);
212
- /* 两头都要能用:未就绪时平台只记下(建窗时读 h->pending_menu_text),就绪后平台投到 UI 线程重建 */
213
- cj_plat_set_menu(h, h->pending_menu_text ? h->pending_menu_text : "");
214
- }
215
-
216
- int cj_bridge_set_titlebar_subtitle(cj_host *h, const char *text) {
217
- int rc;
218
-
219
- if (!h || !text) return -1;
220
- if (strlen(text) > 1024) return -1;
221
- rc = cj_plat_set_titlebar_subtitle(h, text);
222
- /* 失败必须留痕:页面上的状态行不出现时,这一行是唯一能区分
223
- 「应用没调」与「平台没这块」的凭证 */
224
- if (rc != 0) {
225
- cj_logf("[cj-bridge] titlebar subtitle rejected: rc=%d text=\"%s\"\n", rc, text);
226
- }
227
- return rc;
228
- }
229
-
230
- void cj_bridge_set_menu_item_state(cj_host *h, const char *id, int enabled, int checked) {
231
- if (!h || !id || !id[0]) return;
232
- cj_logf("[cj-bridge] set menu item: id=%s enabled=%d checked=%d\n",
233
- id, enabled ? 1 : 0, checked ? 1 : 0);
234
- cj_plat_menu_item_state(h, id, enabled ? 1 : 0, checked ? 1 : 0);
235
- }
236
-
237
- /* ===== 拖放(RFC-002 §5;v1 载荷只有文件路径)===== */
238
-
239
- void cj_bridge_set_drag_drop(cj_host *h, int enabled, int files) {
240
- if (!h) return;
241
- cj_logf("[cj-bridge] set drag drop: enabled=%d files=%d\n",
242
- enabled ? 1 : 0, files ? 1 : 0);
243
- h->pending_drop_enabled = enabled ? 1 : 0;
244
- h->pending_drop_files = files ? 1 : 0;
245
- /* 两头都能用:未就绪时平台只记(本函数已写进 pending 字段,建 view 时读);
246
- 就绪后平台投 UI 线程重挂 drag dest */
247
- cj_plat_set_drag_drop(h, enabled ? 1 : 0, files ? 1 : 0);
248
- }
249
-
250
- /* ===== 全局快捷键(RFC-003 第 5 期首个落地项)===== */
251
-
252
- void cj_bridge_set_global_hotkeys(cj_host *h, const char *wire) {
253
- if (!h) return;
254
- cj_logf("[cj-bridge] set global hotkeys: %s\n",
255
- (wire && wire[0]) ? wire : "(none)");
256
- free(h->pending_hotkeys);
257
- h->pending_hotkeys = cj_strdup(wire ? wire : "");
258
- /* 两头都能用:未就绪时平台只记(建宿主线程后读 h->pending_hotkeys);
259
- 就绪后整表替换(ungrab 旧表再 grab 新表)。平台不支持时在 cj_plat_* 里打一行如实说明。 */
260
- cj_plat_set_global_hotkeys(h, h->pending_hotkeys ? h->pending_hotkeys : "");
261
- }
262
-
263
- /* ===== 托盘(RFC-002 §7.C)===== */
264
-
265
- void cj_bridge_set_tray(cj_host *h, const char *wire) {
266
- if (!h) return;
267
- cj_logf("[cj-bridge] set tray: %d bytes\n",
268
- (int)(wire ? strlen(wire) : 0));
269
- free(h->pending_tray);
270
- h->pending_tray = cj_strdup(wire ? wire : "");
271
- /* 两头都能用:未就绪时平台只记(宿主线程装配时读 h->pending_tray);
272
- 就绪后投 UI 线程重建。平台没编托盘支持时在 cj_plat_* 里打一行如实说明。 */
273
- cj_plat_set_tray(h, h->pending_tray ? h->pending_tray : "");
274
- }
275
-
276
- /* ===== 窗口可见性 / 最小化到托盘(RFC-003 第 5 期余项)===== */
277
-
278
- void cj_bridge_set_window_visible(cj_host *h, int visible) {
279
- if (!h) return;
280
- cj_logf("[cj-bridge] set window visible: %d\n", visible ? 1 : 0);
281
- h->win_visible = visible ? 1 : 0;
282
- /* 两头都能用:**未就绪时到此为止**——平台建窗时读 h->win_visible 定稿(建窗那一刻就定下来,
283
- 比「先显示再藏起来」少一次闪烁);就绪后平台投 UI 线程执行。 */
284
- if (h->ready) {
285
- cj_plat_set_window_visible(h, h->win_visible);
286
- }
287
- }
288
-
289
- void cj_bridge_set_minimize_to_tray(cj_host *h, int enabled) {
290
- if (!h) return;
291
- h->minimize_to_tray = enabled ? 1 : 0;
292
- cj_logf("[cj-bridge] set minimize to tray: %d\n", h->minimize_to_tray);
293
- /* 这里只记 + 转给平台,**不**在此刻查能力位:能力位(cj_plat_init)与「应用有没有真的 setTray」
294
- 的先后顺序不稳定,提前拒绝会误伤。策略在**真要最小化那一刻**由平台复核托盘是否装上了。 */
295
- cj_plat_set_minimize_to_tray(h, h->minimize_to_tray);
296
- }
297
-
298
- /* ===== 运行期操作(投递细节留给平台:各平台的原生时机与日志口径不同)===== */
299
-
300
- void cj_bridge_start(cj_host *h, const char *html) {
301
- if (!h) return;
302
- if (html && html[0]) { /* 空串表示「页面由 cj_bridge_load_url 指定」 */
303
- free(h->pending_html);
304
- h->pending_html = cj_strdup(html);
305
- }
306
- cj_plat_start(h);
307
- }
308
-
309
- void cj_bridge_load_url(cj_host *h, const char *url) {
310
- if (!h || !url || !url[0]) return;
311
- free(h->pending_url);
312
- h->pending_url = cj_strdup(url);
313
- cj_plat_load_url(h, url);
314
- }
315
-
316
- void cj_bridge_reload(cj_host *h) {
317
- if (!h) return;
318
- cj_plat_reload(h);
319
- }
320
-
321
- void cj_bridge_open_devtools(cj_host *h) {
322
- if (!h) return;
323
- cj_plat_open_devtools(h);
324
- }
325
-
326
- void cj_bridge_quit(cj_host *h) {
327
- if (!h) return;
328
- h->should_quit = 1;
329
- /* 平台走与「用户关窗」同一条销毁路径,onDestroy 因此恰好触发一次(批次 1 验收项) */
330
- cj_plat_quit(h);
331
- }
332
-
333
- /* ===== 原生 → JS:入队(执行在 UI 线程的 cj_core_flush_js 里)===== */
334
-
335
- void cj_bridge_run_js(cj_host *h, const char *js) {
336
- js_node *node;
337
- int was_empty;
338
- if (!h || !js) return;
339
- node = (js_node *)calloc(1, sizeof(js_node));
340
- if (!node) return;
341
- node->js = cj_strdup(js);
342
- if (!node->js) {
343
- free(node);
344
- return;
345
- }
346
- if (!h->js_lock) {
347
- /* 未 init 时直接丢弃,避免未初始化锁 */
348
- free(node->js);
349
- free(node);
350
- return;
351
- }
352
- cj_plat_sync_lock(h->js_lock);
353
- if (h->js_tail) {
354
- h->js_tail->next = node;
355
- } else {
356
- h->js_head = node;
357
- }
358
- h->js_tail = node;
359
- was_empty = (h->js_head == node); /* 队列由空转非空:需要唤醒 UI 线程 */
360
- cj_plat_sync_unlock(h->js_lock);
361
- if (was_empty) {
362
- cj_plat_wake_ui(h); /* 平台自己判断窗口是否已就绪(未就绪就攒着) */
363
- }
364
- }
365
-
366
- int cj_bridge_is_ready(cj_host *h) {
367
- return h ? (int)h->ready : 0;
368
- }
369
-
370
- int cj_bridge_should_quit(cj_host *h) {
371
- return h ? (int)h->should_quit : 0;
372
- }
373
-
374
- /* ===== core 提供给平台的状态迁移 ===== */
375
-
376
- void cj_core_set_ready(cj_host *h) {
377
- if (h) h->ready = 1;
378
- }
379
-
380
- void cj_core_window_destroyed(cj_host *h) {
381
- if (!h) return;
382
- h->window_gone = 1;
383
- h->should_quit = 1;
384
- /* 每个宿主恰好通知一次:无论是用户关窗还是 cj_bridge_quit,都汇到这条路径 */
385
- if (!h->destroy_notified) {
386
- h->destroy_notified = 1;
387
- if (h->on_destroy) {
388
- h->on_destroy(h); /* 带上句柄:仓颉侧据此认领「是哪扇窗没了」(多窗口不许再走静态槽) */
389
- }
390
- }
391
- /* 有对话框开着时先作废并唤醒等待方:事件循环退出后挂起的回调不再执行,等待方会永远挂住 */
392
- cj_core_dialog_abort(h);
393
- /* 剪贴板同理:投递进 UI 线程的读写不会再被执行 */
394
- cj_core_clipboard_abort(h);
395
- }
396
-
397
- /* ===== 剪贴板(plugin_clipboard):单槽 + UI 线程投递 ===== */
398
-
399
- /* UI 线程在 GTK 线程上执行真正的剪贴板调用(经平台原语),结果放回槽位并唤醒等待方 */
400
- /* 返回 1 = 成功(含读出空),0 = 平台失败。UI 线程直行路径靠它取结果。 */
401
- int cj_core_clipboard_tick(cj_host *h, int is_write, char *text) {
402
- int ok = 0;
403
- char *readback = NULL;
404
-
405
- if (!h) {
406
- free(text);
407
- return 0;
408
- }
409
- if (is_write) {
410
- ok = (cj_plat_clipboard_write_text(h, text ? text : "", NULL, 0) == 0);
411
- if (ok) {
412
- cj_logf("[cj-bridge] clipboard write: %d bytes\n",
413
- text ? (int)strlen(text) : 0);
414
- } else {
415
- cj_logf("[cj-bridge] clipboard write failed\n");
416
- }
417
- free(text);
418
- } else {
419
- readback = cj_plat_clipboard_read_text(h, NULL, 0);
420
- ok = 1; /* 读失败按「空结果」处理:NULL 是合法回值(剪贴板为空/非文本)*/
421
- cj_logf("[cj-bridge] clipboard read: %s\n",
422
- readback ? "got text" : "empty");
423
- }
424
-
425
- cj_plat_sync_lock(h->clip_lock);
426
- h->clip_ok = ok;
427
- h->clip_text = readback;
428
- h->clip_ready = 1;
429
- h->clip_busy = 0;
430
- cj_plat_sync_signal(h->clip_lock);
431
- cj_plat_sync_unlock(h->clip_lock);
432
- return ok;
433
- }
434
-
435
- /* 作废进行中的剪贴板请求并唤醒等待方(ok=0;等待方知道宿主已不在,不再碰槽位)*/
436
- void cj_core_clipboard_abort(cj_host *h) {
437
- if (!h || !h->clip_lock) return;
438
- cj_plat_sync_lock(h->clip_lock);
439
- if (h->clip_busy && !h->clip_ready) {
440
- h->clip_ok = 0;
441
- h->clip_text = NULL;
442
- h->clip_ready = 1;
443
- h->clip_busy = 0;
444
- cj_plat_sync_signal(h->clip_lock);
445
- }
446
- cj_plat_sync_unlock(h->clip_lock);
447
- }
448
-
449
- /* UI 线程投递的载荷:一个指针 + 一个标志 */
450
- typedef struct clip_call {
451
- cj_host *h;
452
- int is_write;
453
- char *text;
454
- } clip_call;
455
-
456
- static void clip_idle(void *d) {
457
- clip_call *c = (clip_call *)d;
458
- if (c) {
459
- cj_core_clipboard_tick(c->h, c->is_write, c->text);
460
- free(c);
461
- }
462
- }
463
-
464
- static int clipboard_execute(cj_host *h, int is_write, char *text, char **out_text) {
465
- if (!h || !h->clip_lock) {
466
- free(text);
467
- return -1;
468
- }
469
- if (h->window_gone) {
470
- free(text);
471
- return -3; /* 宿主已销毁:不再往已退出的 GTK 循环投东西 */
472
- }
473
- cj_plat_sync_lock(h->clip_lock);
474
- if (h->clip_busy) {
475
- cj_plat_sync_unlock(h->clip_lock);
476
- free(text);
477
- return -3; /* 剪贴板是单槽:同时只允许一次(并发写抢剪贴板本身就没有意义)*/
478
- }
479
- h->clip_busy = 1;
480
- h->clip_ready = 0;
481
- h->clip_ok = 0;
482
- h->clip_text = NULL;
483
- cj_plat_sync_unlock(h->clip_lock);
484
-
485
- if (cj_plat_is_ui_thread(h)) {
486
- /* 已在 UI 线程(对话框同类):直接做,不投递——投递 + 等待会自锁(AGENTS §4)*/
487
- int ok = cj_core_clipboard_tick(h, is_write, text);
488
- if (!ok) {
489
- return -3;
490
- }
491
- if (out_text) {
492
- *out_text = h->clip_text; /* 所有权交调用方(tick 已把槽位清待取)*/
493
- h->clip_text = NULL;
494
- }
495
- } else {
496
- /* 投递的载荷必须堆分配:UI 线程处理完由 clip_idle free(通知路径同款)。
497
- 早前这里是**栈上**结构体 + clip_idle 里 free,实机当场 munmap_chunk 崩掉
498
- (AGENTS §4:跨线程投递的载荷谁 post 谁分配,UI 线程释放)。 */
499
- clip_call *call = (clip_call *)malloc(sizeof(clip_call));
500
- if (!call) {
501
- free(text);
502
- cj_core_clipboard_abort(h);
503
- return -3;
504
- }
505
- call->h = h;
506
- call->is_write = is_write;
507
- call->text = text;
508
- if (!cj_plat_post_ui(h, clip_idle, call)) {
509
- /* 没投出去 = 回调永不执行:载荷与 text 仍归本函数,就地回收 */
510
- free(call);
511
- free(text);
512
- cj_core_clipboard_abort(h);
513
- return -3;
514
- }
515
- /* 等结果:窗口先关的话,cj_core_window_destroyed 会作废并唤醒(ok=0)*/
516
- cj_plat_sync_lock(h->clip_lock);
517
- cj_plat_sync_wait(h->clip_lock, &h->clip_ready);
518
- if (out_text && h->clip_ok) {
519
- *out_text = h->clip_text; /* 所有权交调用方 */
520
- h->clip_text = NULL;
521
- }
522
- cj_plat_sync_unlock(h->clip_lock);
523
- if (!h->clip_ok) {
524
- return -3;
525
- }
526
- }
527
- return 0;
528
- }
529
-
530
- int cj_bridge_clipboard_write_text(cj_host *h, const char *text) {
531
- char *copy = NULL;
532
- size_t len;
533
- int rc;
534
-
535
- if (!text) {
536
- return -1;
537
- }
538
- len = strlen(text);
539
- if (len >= (8u << 20)) {
540
- cj_logf("[cj-bridge] clipboard write rejected: %zu bytes exceeds 8 MiB limit\n", len);
541
- return -1;
542
- }
543
- copy = (char *)malloc(len + 1);
544
- if (!copy) {
545
- return -3;
546
- }
547
- memcpy(copy, text, len + 1);
548
- rc = clipboard_execute(h, 1, copy, NULL);
549
- if (rc == 0) {
550
- /* 成功行在 tick 里打(字节口径以投递载荷为准);这里只补参数被拒的取证 */
551
- }
552
- return rc;
553
- }
554
-
555
- char *cj_bridge_clipboard_read_text(cj_host *h) {
556
- char *out = NULL;
557
- int rc;
558
-
559
- rc = clipboard_execute(h, 0, NULL, &out);
560
- if (rc == 0) {
561
- return out; /* 可能为 NULL = 剪贴板为空 / 非文本 */
562
- }
563
- return NULL;
564
- }
565
-
566
- void cj_bridge_clipboard_free(char *s) {
567
- free(s);
568
- }
569
-
570
- /* ===== 通知(plugin_notification):校验 + 发后即忘 ===== */
571
-
572
- /* UI 线程投递的载荷(谁 post 谁分配,UI 线程处理完释放)*/
573
- typedef struct note_call {
574
- cj_host *h;
575
- char *title;
576
- char *body;
577
- } note_call;
578
-
579
- static void note_idle(void *d) {
580
- note_call *c = (note_call *)d;
581
- if (c) {
582
- int ok = cj_plat_notification_notify(c->h, c->title, c->body, NULL, 0) == 0;
583
- cj_logf("[cj-bridge] notification: title=%d bytes -> %s\n",
584
- c->title ? (int)strlen(c->title) : 0, ok ? "sent" : "failed");
585
- free(c->title);
586
- free(c->body);
587
- free(c);
588
- }
589
- }
590
-
591
- int cj_bridge_notification_notify(cj_host *h, const char *title, const char *body) {
592
- char *t_copy = NULL;
593
- char *b_copy = NULL;
594
- size_t t_len, b_len;
595
-
596
- if (!h || !title || !title[0]) {
597
- return -1;
598
- }
599
- t_len = strlen(title);
600
- b_len = body ? strlen(body) : 0;
601
- if (t_len > 256 || b_len > (8u << 10)) {
602
- cj_logf("[cj-bridge] notification rejected: title=%zu (<=256) body=%zu (<=8192)\n",
603
- t_len, b_len);
604
- return -1;
605
- }
606
- if (h->window_gone) {
607
- return -3;
608
- }
609
- /* 平台没声明通知能力(如 Windows 桩阶段)就当场拒绝,别走「发后即忘」:
610
- 那条路径投递成功就返回 0,UI 线程实际的失败没人带回命令层——桩会被读成成功。 */
611
- if (!(h->capabilities & CJ_CAP_NOTIFICATION)) {
612
- cj_logf("[cj-bridge] notification rejected: platform does not declare CJ_CAP_NOTIFICATION\n");
613
- return -4;
614
- }
615
- t_copy = (char *)malloc(t_len + 1);
616
- if (t_copy) {
617
- memcpy(t_copy, title, t_len + 1);
618
- }
619
- if (body && b_len) {
620
- b_copy = (char *)malloc(b_len + 1);
621
- if (b_copy) {
622
- memcpy(b_copy, body, b_len + 1);
623
- }
624
- }
625
- if (!t_copy || (body && b_len && !b_copy)) {
626
- free(t_copy);
627
- free(b_copy);
628
- return -3;
629
- }
630
- if (cj_plat_is_ui_thread(h)) {
631
- int ok = cj_plat_notification_notify(h, t_copy, b_copy, NULL, 0) == 0;
632
- cj_logf("[cj-bridge] notification: title=%d bytes -> %s\n",
633
- (int)t_len, ok ? "sent" : "failed");
634
- free(t_copy);
635
- free(b_copy);
636
- return ok ? 0 : -3;
637
- }
638
- /* 发后即忘:投递后立即返回。载荷归 UI 线程释放(不等待、不占剪贴板那套同步槽)*/
639
- {
640
- note_call *call = (note_call *)malloc(sizeof(note_call));
641
- if (!call) {
642
- free(t_copy);
643
- free(b_copy);
644
- return -3;
645
- }
646
- call->h = h;
647
- call->title = t_copy;
648
- call->body = b_copy;
649
- if (!cj_plat_post_ui(h, note_idle, call)) {
650
- free(t_copy);
651
- free(b_copy);
652
- free(call);
653
- return -3;
654
- }
655
- }
656
- return 0;
657
- }
658
-
659
- /* 线路格式的共享解析(契约见 bridge_core.h 的 cj_menu_row_fn 注释)。
660
- 就地切分一份副本:字段指针只在回调期间有效,拷贝与释放都在这里管,平台不用操心。 */
661
- void cj_core_menu_walk(const char *wire, cj_menu_row_fn fn, void *ctx) {
662
- char *copy;
663
- char *line;
664
-
665
- if (!wire || !fn) return;
666
- copy = cj_strdup(wire);
667
- if (!copy) return;
668
-
669
- line = copy;
670
- while (*line) {
671
- char *nl = strchr(line, '\n');
672
- char *cur;
673
- int depth = 0;
674
- char *t1, *t2, *t3, *t4;
675
- char *kind_s, *id_s, *label_s, *flags_s, *accel_s;
676
-
677
- if (nl) {
678
- *nl = '\0';
679
- }
680
- while (line[depth] == '\t') {
681
- depth++;
682
- }
683
- cur = line + depth;
684
- kind_s = cur;
685
- id_s = NULL;
686
- label_s = NULL;
687
- flags_s = NULL;
688
- accel_s = NULL;
689
- t1 = strchr(kind_s, '\t');
690
- if (t1) {
691
- *t1 = '\0';
692
- id_s = t1 + 1;
693
- t2 = strchr(id_s, '\t');
694
- if (t2) {
695
- *t2 = '\0';
696
- label_s = t2 + 1;
697
- t3 = strchr(label_s, '\t');
698
- if (t3) {
699
- *t3 = '\0';
700
- flags_s = t3 + 1;
701
- t4 = strchr(flags_s, '\t');
702
- if (t4) {
703
- *t4 = '\0';
704
- accel_s = t4 + 1;
705
- }
706
- }
707
- }
708
- }
709
- if (kind_s[0]) { /* kind 为空的行整行跳过;缺字段按空串 / 0——容错优于崩溃 */
710
- fn(ctx, depth, kind_s[0], id_s ? id_s : "", label_s ? label_s : "",
711
- flags_s ? atoi(flags_s) : 0, accel_s ? accel_s : "");
712
- }
713
-
714
- if (!nl) {
715
- break;
716
- }
717
- line = nl + 1;
718
- }
719
- free(copy);
720
- }
721
-
722
- /* shell 事件编成 JSON(RFC-002 §5.5):id 做最小转义——应用给的标识里出现 \ 或 " 时
723
- 不至于把载荷弄坏。(拖放的 paths 到 7.C 才落地,届时照此扩一份路径转义。) */
724
- static void cj_json_escape_into(char *dst, size_t cap, const char *src) {
725
- size_t o = 0;
726
- const char *p;
727
- if (!dst || cap == 0) return;
728
- for (p = src ? src : ""; *p; p++) {
729
- if (o + 2 >= cap) break;
730
- if (*p == '\\' || *p == '"') {
731
- dst[o++] = '\\';
732
- }
733
- dst[o++] = *p;
734
- }
735
- dst[o] = '\0';
736
- }
737
-
738
- void cj_core_menu_clicked(cj_host *h, const char *id, int enabled, int checked) {
739
- char idbuf[256];
740
- char payload[384];
741
- if (!h) return;
742
- if (!id || !id[0]) {
743
- return; /* 没有 id 就没有「被点的项」(分隔线与子菜单不会走到这里)*/
744
- }
745
- cj_logf("[cj-bridge] menu clicked: id=%s enabled=%d checked=%d\n",
746
- id, enabled ? 1 : 0, checked ? 1 : 0);
747
- if (!h->on_shell) {
748
- return; /* 未注册回调:上面那行日志已经落盘,够诊断了 */
749
- }
750
- cj_json_escape_into(idbuf, sizeof(idbuf), id);
751
- snprintf(payload, sizeof(payload),
752
- "{\"kind\":\"menu\",\"id\":\"%s\",\"enabled\":%s,\"checked\":%s}",
753
- idbuf, enabled ? "true" : "false", checked ? "true" : "false");
754
- /* 带上宿主句柄:仓颉侧按句柄分发到对应窗口,不再依赖进程级静态槽 */
755
- h->on_shell(h, payload);
756
- }
757
-
758
- /* 全局快捷键触发:平台在 X11/Win32 事件回调里调,id 是应用注册时给的标识。
759
- 载荷形状与菜单点击同族({"kind":"hotkey","id":…}),同走 on_shell 通道。 */
760
- void cj_core_hotkey_pressed(cj_host *h, const char *id) {
761
- char idbuf[256];
762
- char payload[320];
763
- if (!h) return;
764
- if (!id || !id[0]) {
765
- return; /* 没有 id 的触发无法路由,直接丢弃(平台侧不该产生) */
766
- }
767
- cj_logf("[cj-bridge] hotkey pressed: id=%s\n", id);
768
- if (!h->on_shell) {
769
- return; /* 未注册回调:上面那行日志已经落盘,够诊断了 */
770
- }
771
- cj_json_escape_into(idbuf, sizeof(idbuf), id);
772
- snprintf(payload, sizeof(payload), "{\"kind\":\"hotkey\",\"id\":\"%s\"}", idbuf);
773
- h->on_shell(h, payload);
774
- }
775
-
776
- /* 托盘菜单项被点:载荷形状与菜单/快捷键同族,同走 on_shell 通道(RFC-002 §5.5) */
777
- void cj_core_tray_clicked(cj_host *h, const char *id) {
778
- char idbuf[256];
779
- char payload[320];
780
- if (!h) return;
781
- if (!id || !id[0]) {
782
- return; /* 没有 id 的点击无法路由(托盘图标本体点击不带 id,平台侧自行决定语义) */
783
- }
784
- cj_logf("[cj-bridge] tray clicked: id=%s\n", id);
785
- if (!h->on_shell) {
786
- return;
787
- }
788
- cj_json_escape_into(idbuf, sizeof(idbuf), id);
789
- snprintf(payload, sizeof(payload), "{\"kind\":\"tray\",\"id\":\"%s\"}", idbuf);
790
- h->on_shell(h, payload);
791
- }
792
-
793
- /* ===== 拖放落地(RFC-002 §5;URI 解析在 core,桩自检可测)===== */
794
-
795
- #define DROP_MAX_PATHS 32
796
- #define DROP_MAX_PATH_SZ 4096
797
-
798
- /* 把一行 URI 变成路径(就地写到 out,返回写入字节数,超上限返回 -1)。
799
- file:// URI:剥前缀 + %XX 十六进制解码(file:// 路径里空格等必须转义成 %20);
800
- 非 file:// URI:原样保留(v1 载荷语义由页面决定,core 不猜)。 */
801
- static int drop_uri_to_path(const char *uri, char *out, int out_cap) {
802
- int o = 0;
803
- const char *p = uri;
804
- static const char hex[] = "0123456789ABCDEF";
805
- if (strncmp(uri, "file://", 7) == 0) {
806
- p = uri + 7;
807
- /* file://localhost/... 也算本地路径(RFC 1738 形态,GTK 偶发) */
808
- if (strncmp(p, "localhost/", 10) == 0) {
809
- p += 9; /* 保留后面的 '/' */
810
- }
811
- }
812
- for (; *p; p++) {
813
- if (o >= out_cap - 1) {
814
- return -1; /* 超限:丢弃这条(调用方记日志) */
815
- }
816
- if (p[0] == '%' && p[1] && p[2]) {
817
- const char *h1 = strchr(hex, p[1]);
818
- const char *h2 = strchr(hex, p[2]);
819
- if (h1 && h2) {
820
- /* 十六进制位必须严格配对解析;大小写都收(strchr 命中即合法) */
821
- out[o++] = (char)((h1 - hex) * 16 + (h2 - hex));
822
- p += 2;
823
- continue;
824
- }
825
- /* 伪 % 序列:原样保留,交给页面/文件系统报错,core 不替它猜 */
826
- }
827
- out[o++] = p[0];
828
- }
829
- out[o] = '\0';
830
- return o;
831
- }
832
-
833
- void cj_core_drop_files(cj_host *h, const char *uri_list) {
834
- char paths[DROP_MAX_PATHS][DROP_MAX_PATH_SZ];
835
- const char *escaped[DROP_MAX_PATHS];
836
- char escaped_buf[DROP_MAX_PATHS][DROP_MAX_PATH_SZ];
837
- int count = 0, dropped = 0;
838
- const char *line;
839
- if (!h || !uri_list) return;
840
-
841
- line = uri_list;
842
- while (line && *line && count < DROP_MAX_PATHS) {
843
- const char *eol = strchr(line, '\n');
844
- size_t len = eol ? (size_t)(eol - line) : strlen(line);
845
- char uri[DROP_MAX_PATH_SZ];
846
- int n;
847
- if (len >= sizeof(uri)) {
848
- len = sizeof(uri) - 1; /* 截断后多半解析不出合法路径,走正常丢弃路径 */
849
- }
850
- memcpy(uri, line, len);
851
- uri[len] = '\0';
852
- line = eol ? eol + 1 : NULL;
853
- /* 去 \r(text/uri-list 行尾常是 \r\n)与空白 */
854
- while (len > 0 && (uri[len - 1] == '\r' || uri[len - 1] == ' ')) {
855
- uri[--len] = '\0';
856
- }
857
- /* 注释行(# 开头)与空行跳过(RFC 2483 的 text/uri-list 规范) */
858
- if (len == 0 || uri[0] == '#') {
859
- continue;
860
- }
861
- n = drop_uri_to_path(uri, paths[count], DROP_MAX_PATH_SZ);
862
- if (n < 0) {
863
- dropped++;
864
- cj_logf("[cj-bridge] drop: path too long, dropped: %.60s...\n", uri);
865
- continue;
866
- }
867
- count++;
868
- }
869
- /* 行数超上限:多出的丢弃并计数(本循环因 count 满退出,剩余行都算 dropped) */
870
- if (line && *line) {
871
- for (; line; line = strchr(line, '\n') ? strchr(line, '\n') + 1 : NULL) {
872
- if (*line && *line != '#') {
873
- dropped++;
874
- }
875
- }
876
- }
877
-
878
- cj_logf("[cj-bridge] drop: paths=%d first=%.80s%s\n",
879
- count, count > 0 ? paths[0] : "",
880
- dropped > 0 ? " (some dropped)" : "");
881
- if (!h->on_shell || count == 0) {
882
- return; /* 未注册回调或空拖放:日志已落盘,够诊断 */
883
- }
884
- /* 拼 JSON:paths 数组每项转义(文件名可含引号/反斜杠) */
885
- {
886
- char payload[8192];
887
- int o = 0, i;
888
- o += snprintf(payload + o, sizeof(payload) - (size_t)o,
889
- "{\"kind\":\"drop\",\"paths\":[");
890
- for (i = 0; i < count && o < (int)sizeof(payload) - 16; i++) {
891
- cj_json_escape_into(escaped_buf[i], DROP_MAX_PATH_SZ, paths[i]);
892
- escaped[i] = escaped_buf[i];
893
- o += snprintf(payload + o, sizeof(payload) - (size_t)o,
894
- "%s\"%s\"", i > 0 ? "," : "", escaped[i]);
895
- }
896
- snprintf(payload + o, sizeof(payload) - (size_t)o, "]}");
897
- h->on_shell(h, payload);
898
- }
899
- }
900
-
901
- void cj_core_dialog_abort(cj_host *h) {
902
- if (!h || !h->dlg_lock) return;
903
- cj_plat_sync_lock(h->dlg_lock);
904
- if (h->dlg_cur && !h->dlg_ready) {
905
- h->dlg_cur->ok = 0;
906
- h->dlg_ready = 1;
907
- h->dlg_cur = NULL;
908
- cj_plat_sync_signal(h->dlg_lock);
909
- }
910
- cj_plat_sync_unlock(h->dlg_lock);
911
- }
912
-
913
- /* ===== 回投队列:一次空闲窗口拼批回投(UI 线程调用)===== */
914
-
915
- void cj_core_flush_js(cj_host *h) {
916
- js_node *batch[CJ_JS_BATCH_MAX];
917
- char *single = NULL; /* 快路径:接管字符串所有权,省掉一次拷贝 */
918
- char *script = NULL;
919
- size_t len = 0;
920
- size_t cap = 0;
921
- int n = 0;
922
- int more = 0;
923
- int i, j;
924
-
925
- if (!h || !h->js_lock) return;
926
-
927
- cj_plat_sync_lock(h->js_lock);
928
- if (h->js_head) {
929
- js_node *node = h->js_head;
930
- h->js_head = node->next;
931
- if (!h->js_head) {
932
- h->js_tail = NULL;
933
- single = node->js; /* 队里只有这一条 */
934
- free(node);
935
- } else {
936
- batch[n++] = node;
937
- while (h->js_head && n < CJ_JS_BATCH_MAX) {
938
- node = h->js_head;
939
- h->js_head = node->next;
940
- if (!h->js_head) {
941
- h->js_tail = NULL;
942
- }
943
- batch[n++] = node;
944
- }
945
- }
946
- }
947
- more = (h->js_head != NULL);
948
- cj_plat_sync_unlock(h->js_lock);
949
-
950
- if (single) {
951
- cj_plat_post_js(h, single);
952
- free(single);
953
- } else if (n > 0) {
954
- /* 拼批:一段脚本只跨一次语言边界,省掉 n-1 次固定开销。
955
- 为什么不用 GString 之类的缓冲:core 不依赖平台库(glib / Win32)。 */
956
- for (i = 0; i < n; i++) {
957
- size_t nl = strlen(batch[i]->js);
958
- if (len + nl + 2 > cap) {
959
- size_t ncap = (len + nl + 2) * 2;
960
- char *grown = (char *)realloc(script, ncap);
961
- if (!grown) {
962
- /* 内存不够:退回逐条执行——宁可多花开销,也不丢回投(回投丢了前端会一直 pending)*/
963
- for (j = i; j < n; j++) {
964
- cj_plat_post_js(h, batch[j]->js);
965
- }
966
- for (j = 0; j < n; j++) {
967
- free(batch[j]->js);
968
- free(batch[j]);
969
- }
970
- free(script);
971
- if (more) cj_plat_wake_ui(h);
972
- return;
973
- }
974
- script = grown;
975
- cap = ncap;
976
- }
977
- memcpy(script + len, batch[i]->js, nl);
978
- len += nl;
979
- script[len++] = '\n';
980
- script[len] = '\0';
981
- free(batch[i]->js);
982
- free(batch[i]);
983
- }
984
- cj_plat_post_js(h, script);
985
- free(script);
986
- }
987
-
988
- /* 队列没取空:自续一次,剩下的下一批走 */
989
- if (more) {
990
- cj_plat_wake_ui(h);
991
- }
992
- }
993
-
994
- /* ===== 原生对话框:单槽状态机(模态,同时只允许一个)===== */
995
-
996
- static dlg_req *cj_dlg_req_new(int kind, const char *title, const char *message, const char *filter) {
997
- dlg_req *r = (dlg_req *)calloc(1, sizeof(dlg_req));
998
- if (!r) return NULL;
999
- r->kind = kind;
1000
- r->title = cj_strdup(title ? title : "");
1001
- r->message = cj_strdup(message ? message : "");
1002
- r->filter = cj_strdup(filter ? filter : "");
1003
- return r;
1004
- }
1005
-
1006
- static void cj_dlg_req_free(dlg_req *r) {
1007
- if (!r) return;
1008
- free(r->title);
1009
- free(r->message);
1010
- free(r->filter);
1011
- free(r->path);
1012
- free(r);
1013
- }
1014
-
1015
- static void cj_dlg_log_closed(const dlg_req *r) {
1016
- if (r->kind == 0 || r->kind == 1) { /* 文件类才有路径 */
1017
- cj_logf("[cj-bridge] dialog closed: kind=%d ok=%d path=%s\n",
1018
- r->kind, r->ok, r->path ? r->path : "(none)");
1019
- } else {
1020
- cj_logf("[cj-bridge] dialog closed: kind=%d ok=%d\n", r->kind, r->ok);
1021
- }
1022
- }
1023
-
1024
- void cj_bridge_set_dialog_callback(cj_host *h, cj_on_dialog_fn cb) {
1025
- if (!h) return;
1026
- h->on_dialog = cb;
1027
- }
1028
-
1029
- void cj_bridge_set_shell_callback(cj_host *h, cj_on_shell_fn cb) {
1030
- if (!h) return;
1031
- h->on_shell = cb;
1032
- }
1033
-
1034
- int cj_bridge_host_capabilities(cj_host *h) {
1035
- return h ? h->capabilities : 0;
1036
- }
1037
-
1038
- int cj_bridge_host_eq(cj_host *a, cj_host *b) {
1039
- /* 只比指针本身:句柄由 core 分配、生命周期也归 core,仓颉侧只当不透明凭据使 */
1040
- return (a && b && a == b) ? 1 : 0;
1041
- }
1042
-
1043
- int cj_bridge_show_dialog(cj_host *h, int kind, const char *title,
1044
- const char *message, const char *filter) {
1045
- dlg_req *r;
1046
- int ok;
1047
-
1048
- if (!h) return 0;
1049
- if (!h->ready) { /* 没有窗口就没有父窗口,模态对话框无处安放 */
1050
- cj_logf("[cj-bridge] dialog ignored: host not ready\n");
1051
- return 0;
1052
- }
1053
-
1054
- cj_plat_sync_lock(h->dlg_lock);
1055
- if (h->dlg_busy) {
1056
- cj_plat_sync_unlock(h->dlg_lock);
1057
- cj_logf("[cj-bridge] dialog rejected: another dialog is open\n");
1058
- return 0;
1059
- }
1060
- h->dlg_busy = 1;
1061
- h->dlg_ready = 0;
1062
- cj_plat_sync_unlock(h->dlg_lock);
1063
-
1064
- /* 调用方已经站在 UI 线程上(WebKit 的 script-message / WebView2 的 WebMessageReceived 就跑在那里):
1065
- 直接弹。这里**不能**走「投递 + 阻塞等待」——等待的事件得由 UI 线程处理,而 UI 线程正卡在这次调用里,
1066
- 等于自己把自己锁死。 */
1067
- if (cj_plat_is_ui_thread(h)) {
1068
- cj_logf("[cj-bridge] dialog: kind=%d title=%s (caller on ui thread)\n",
1069
- kind, title ? title : "");
1070
- r = cj_dlg_req_new(kind, title, message, filter);
1071
- if (!r) {
1072
- cj_plat_sync_lock(h->dlg_lock);
1073
- h->dlg_busy = 0;
1074
- cj_plat_sync_unlock(h->dlg_lock);
1075
- return 0;
1076
- }
1077
- r->ok = cj_plat_run_dialog(h, r);
1078
- ok = r->ok;
1079
- cj_dlg_log_closed(r);
1080
- if (h->on_dialog) {
1081
- h->on_dialog(h, r->path ? r->path : ""); /* 带上句柄:结果只落发起这次对话框的窗口 */
1082
- }
1083
- cj_dlg_req_free(r);
1084
- cj_plat_sync_lock(h->dlg_lock);
1085
- h->dlg_busy = 0;
1086
- cj_plat_sync_unlock(h->dlg_lock);
1087
- return ok;
1088
- }
1089
-
1090
- r = cj_dlg_req_new(kind, title, message, filter);
1091
- if (!r) {
1092
- cj_plat_sync_lock(h->dlg_lock);
1093
- h->dlg_busy = 0;
1094
- cj_plat_sync_unlock(h->dlg_lock);
1095
- return 0;
1096
- }
1097
- cj_logf("[cj-bridge] dialog: kind=%d title=%s\n", kind, r->title);
1098
- cj_plat_sync_lock(h->dlg_lock);
1099
- h->dlg_cur = r;
1100
- cj_plat_sync_unlock(h->dlg_lock);
1101
-
1102
- cj_plat_post_dialog(h, r);
1103
-
1104
- /* 等结果:窗口先关的话,cj_core_window_destroyed 会作废并唤醒(ok=0) */
1105
- cj_plat_sync_lock(h->dlg_lock);
1106
- cj_plat_sync_wait(h->dlg_lock, &h->dlg_ready);
1107
- ok = r->ok;
1108
- h->dlg_busy = 0;
1109
- cj_plat_sync_unlock(h->dlg_lock);
1110
- cj_dlg_req_free(r);
1111
- return ok;
1112
- }
1113
-
1114
- void cj_core_dialog_tick(cj_host *h, dlg_req *req) {
1115
- int alive;
1116
- if (!h || !req) return;
1117
-
1118
- cj_plat_sync_lock(h->dlg_lock);
1119
- alive = (h->dlg_cur == req);
1120
- cj_plat_sync_unlock(h->dlg_lock);
1121
- if (!alive) {
1122
- /* 窗口先关了:槽位已交回调用方(它会 free 这个请求)——这里连碰都不能碰 */
1123
- return;
1124
- }
1125
-
1126
- req->ok = cj_plat_run_dialog(h, req);
1127
- cj_dlg_log_closed(req);
1128
- if (h->on_dialog) {
1129
- h->on_dialog(h, req->path ? req->path : "");
1130
- }
1131
-
1132
- cj_plat_sync_lock(h->dlg_lock);
1133
- h->dlg_cur = NULL;
1134
- h->dlg_ready = 1;
1135
- cj_plat_sync_signal(h->dlg_lock);
1136
- cj_plat_sync_unlock(h->dlg_lock);
1137
- }
1138
-
1139
- /* =========================================================================
1140
- * 串口(plugin_serial):路径白名单 + 参数校验 + 导出转发
1141
- *
1142
- * 句柄表与 fd 全在平台层(那边本来就有 pthread / CS),本文件**不持有任何串口状态**——
1143
- * 因此也不需要「一次性初始化」这种钩子,导出函数可以在没有宿主的进程里直接用。
1144
- * ========================================================================= */
1145
-
1146
- #if defined(_MSC_VER)
1147
- #define CJ_THREAD_LOCAL __declspec(thread)
1148
- #else
1149
- #define CJ_THREAD_LOCAL __thread
1150
- #endif
1151
-
1152
- /* 线程局部:命令各自跑在 worker 线程上,静态单槽会被并发调用互相覆盖(AGENTS §4 的静态单槽坑) */
1153
- static CJ_THREAD_LOCAL char g_serial_err[256];
1154
-
1155
- static void cj_serial_err_set(const char *msg) {
1156
- if (!msg) {
1157
- g_serial_err[0] = '\0';
1158
- return;
1159
- }
1160
- snprintf(g_serial_err, sizeof(g_serial_err), "%s", msg);
1161
- }
1162
-
1163
- /* 白名单前缀表。**只放行设备节点类路径**:页面递进来的是页面可控字符串,放行面越小越好。
1164
- 注意 "\\.\COM" 与 "COM" 两条要额外约束「后面全是数字」,否则 COMfoo 这种也会被放行。 */
1165
- static const char *const CJ_SERIAL_PREFIXES[] = {
1166
- "/dev/ttyUSB", "/dev/ttyACM", "/dev/ttyS", "/dev/ttyAMA", "/dev/ttyPS",
1167
- "/dev/ttyTHS", "/dev/ttyXRUSB", "/dev/ttyV", /* Linux 串口(含虚拟 ttyV*)*/
1168
- "/dev/tty.", "/dev/cu.", /* macOS:调制解调器 / 呼出设备 */
1169
- "/dev/serial/by-id/", "/dev/serial/by-path/", /* udev 稳定符号链接(推荐写法)*/
1170
- "/dev/pts/", /* 伪终端:socat / openpty 造的虚拟串口 */
1171
- "COM", "\\\\.\\COM", /* Windows:COM3 / 长名形式 */
1172
- };
1173
-
1174
- #define CJ_SERIAL_MAX_PATH 255
1175
-
1176
- int cj_serial_path_allowed(const char *path) {
1177
- size_t i, n;
1178
-
1179
- if (!path || !path[0]) return 0;
1180
- n = strlen(path);
1181
- if (n > CJ_SERIAL_MAX_PATH) return 0;
1182
- /* 含 ".." 一律拒绝:否则 "/dev/ttyUSB0/../../etc/shadow" 这种前缀命中就能穿越出去 */
1183
- if (strstr(path, "..")) return 0;
1184
-
1185
- for (i = 0; i < sizeof(CJ_SERIAL_PREFIXES) / sizeof(CJ_SERIAL_PREFIXES[0]); i++) {
1186
- const char *pfx = CJ_SERIAL_PREFIXES[i];
1187
- size_t pl = strlen(pfx);
1188
- const char *rest;
1189
- size_t k;
1190
-
1191
- if (strncmp(path, pfx, pl) != 0) continue;
1192
- rest = path + pl;
1193
- if (!rest[0]) return 0; /* 前缀本身不是设备名("/dev/ttyS" 这种不收)*/
1194
-
1195
- if (strcmp(pfx, "COM") != 0 && strcmp(pfx, "\\\\.\\COM") != 0) {
1196
- return 1;
1197
- }
1198
- for (k = 0; rest[k]; k++) {
1199
- if (rest[k] < '0' || rest[k] > '9') return 0;
1200
- if (k >= 3) return 0; /* COM1..COM999(Windows 实际到 COM256)*/
1201
- }
1202
- return 1;
1203
- }
1204
- return 0;
1205
- }
1206
-
1207
- static int cj_serial_cfg_ok(const cj_serial_cfg *cfg) {
1208
- if (!cfg) return 0;
1209
- if (cfg->baud <= 0) return 0;
1210
- if (cfg->data_bits < 5 || cfg->data_bits > 8) return 0;
1211
- if (cfg->parity < 0 || cfg->parity > 2) return 0;
1212
- if (cfg->stop_bits != 1 && cfg->stop_bits != 2) return 0;
1213
- return 1;
1214
- }
1215
-
1216
- long long cj_bridge_serial_open(const char *path, int baud, int data_bits, int parity, int stop_bits) {
1217
- cj_serial_cfg cfg;
1218
- char err[256];
1219
- long long h;
1220
-
1221
- g_serial_err[0] = '\0';
1222
- if (!path || !path[0]) {
1223
- cj_serial_err_set("serial: 设备路径为空");
1224
- return -1;
1225
- }
1226
- if (!cj_serial_path_allowed(path)) {
1227
- snprintf(g_serial_err, sizeof(g_serial_err),
1228
- "serial: 路径不在白名单内(只放行串口设备节点 / 伪终端,且不得含 ..): %s", path);
1229
- return -1;
1230
- }
1231
- cfg.baud = baud;
1232
- cfg.data_bits = data_bits;
1233
- cfg.parity = parity;
1234
- cfg.stop_bits = stop_bits;
1235
- if (!cj_serial_cfg_ok(&cfg)) {
1236
- cj_serial_err_set("serial: 串口参数非法(baud>0 / dataBits 5..8 / parity 0|1|2 / stopBits 1|2)");
1237
- return -1;
1238
- }
1239
-
1240
- err[0] = '\0';
1241
- h = cj_plat_serial_open(path, &cfg, err, (int)sizeof(err));
1242
- if (h <= 0) {
1243
- cj_serial_err_set(err[0] ? err : "serial: 平台打开失败");
1244
- /* 失败也留一行:串口问题多半要拿 stderr 对账(AGENTS §1 的取证口径)*/
1245
- cj_logf("[cj-bridge] serial open failed: path=%s baud=%d code=%lld (%s)\n",
1246
- path, baud, h, g_serial_err);
1247
- return h < 0 ? h : -3;
1248
- }
1249
- cj_logf("[cj-bridge] serial open: handle=%lld path=%s baud=%d data=%d parity=%d stop=%d\n",
1250
- h, path, baud, data_bits, parity, stop_bits);
1251
- return h;
1252
- }
1253
-
1254
- int cj_bridge_serial_read(long long h, unsigned char *buf, int len, int timeout_ms) {
1255
- char err[256];
1256
- int n;
1257
-
1258
- g_serial_err[0] = '\0';
1259
- if (h <= 0 || !buf || len <= 0 || len > (1 << 20)) {
1260
- cj_serial_err_set("serial: read 参数非法(handle>0 / len 1..1048576)");
1261
- return -1;
1262
- }
1263
- if (timeout_ms < 0) timeout_ms = 0;
1264
- err[0] = '\0';
1265
- n = cj_plat_serial_read(h, buf, len, timeout_ms, err, (int)sizeof(err));
1266
- if (n < 0) {
1267
- cj_serial_err_set(err[0] ? err : "serial: 读失败");
1268
- cj_logf("[cj-bridge] serial read failed: handle=%lld (%s)\n", h, g_serial_err);
1269
- }
1270
- return n;
1271
- }
1272
-
1273
- int cj_bridge_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms) {
1274
- char err[256];
1275
- int n;
1276
-
1277
- g_serial_err[0] = '\0';
1278
- if (h <= 0 || !buf || len <= 0 || len > (1 << 20)) {
1279
- cj_serial_err_set("serial: write 参数非法(handle>0 / len 1..1048576)");
1280
- return -1;
1281
- }
1282
- if (timeout_ms < 0) timeout_ms = 0;
1283
- err[0] = '\0';
1284
- n = cj_plat_serial_write(h, buf, len, timeout_ms, err, (int)sizeof(err));
1285
- if (n < 0) {
1286
- cj_serial_err_set(err[0] ? err : "serial: 写失败");
1287
- cj_logf("[cj-bridge] serial write failed: handle=%lld (%s)\n", h, g_serial_err);
1288
- }
1289
- return n;
1290
- }
1291
-
1292
- int cj_bridge_serial_close(long long h) {
1293
- char err[256];
1294
- int rc;
1295
-
1296
- g_serial_err[0] = '\0';
1297
- if (h <= 0) {
1298
- cj_serial_err_set("serial: close 参数非法(handle 必须 >0)");
1299
- return -1;
1300
- }
1301
- err[0] = '\0';
1302
- rc = cj_plat_serial_close(h, err, (int)sizeof(err));
1303
- if (rc != 0) {
1304
- cj_serial_err_set(err[0] ? err : "serial: 关句柄失败");
1305
- return rc < 0 ? rc : -4;
1306
- }
1307
- cj_logf("[cj-bridge] serial close: handle=%lld\n", h);
1308
- return 0;
1309
- }
1310
-
1311
- const char *cj_bridge_serial_last_error(void) {
1312
- return g_serial_err;
1313
- }
1314
-
1315
- int cj_bridge_serial_list(char *out, int out_len) {
1316
- if (!out || out_len <= 0) {
1317
- cj_serial_err_set("serial: list 参数非法(out 缓冲为空)");
1318
- return -1;
1319
- }
1320
- out[0] = '\0';
1321
- return cj_plat_serial_list(out, out_len); /* 负码原样交回(-4 = 本平台未实现) */
1322
- }
1323
-
1324
- /* =========================================================================
1325
- * 日志落盘(RFC-003 第 5 期)
1326
- *
1327
- * 为什么在 fd 层接管:单文件产物是 Windows GUI 子系统,双击时 stderr 无处可去,而
1328
- * 诊断输出分布在 core、平台实现与仓颉侧上百处。把 fd 2 指向日志文件,**所有** stderr
1329
- * 写者自动落进同一个 FILE*(顺序天然一致),两端调用点一处都不用改;stdout 不碰。
1330
- * 为什么用 `dup2` 而不是 `freopen`:`freopen` 失败会**把 stderr 关掉**,那就连失败原因
1331
- * 都没地方写了;`dup2` 只在 open 成功后才动 fd 2,失败路径 stderr 完好。
1332
- * 为什么轮转不 rename 打开中的文件:Windows 上 rename 一个还开着的文件会失败。
1333
- * 做法是「先拷贝再截断」——把当前内容拷进 `log.1`,已关闭的档案 `.1 → .2` 平移
1334
- * (`keep` 决定份数),最后 `_chsize` / `ftruncate` 把当前文件清零。这样无平台分支、
1335
- * 无空设备名,桩自检也能直接测。
1336
- * ========================================================================= */
1337
-
1338
- #define CJ_LOG_PATH_MAX 1024
1339
- #define CJ_LOG_LINE_SLACK 256 /* 每行预留,免得刚好压线时写到 max 之上 */
1340
-
1341
- static char g_log_path[CJ_LOG_PATH_MAX];
1342
- static int g_log_active = 0;
1343
- static long g_log_max = 0; /* 字节;0 = 不轮转(交给外部 logrotate) */
1344
- static long g_log_keep = 0;
1345
- static volatile int g_log_rotating = 0;
1346
-
1347
- /* 轮转互斥:只用编译器内建原子,core 不引平台同步设施(命令 worker 线程也会走到这里)。
1348
- 抢不到就当「这一行不轮转」,不阻塞调用方。 */
1349
- static int cj_log_lock(void) {
1350
- return __sync_lock_test_and_set(&g_log_rotating, 1) == 0;
1351
- }
1352
- static void cj_log_unlock(void) {
1353
- __sync_lock_release(&g_log_rotating);
1354
- }
1355
-
1356
- /* 时间戳:localtime 的静态缓冲可能被别的线程踩,取回来立刻拷进本地副本。 */
1357
- static void cj_log_prefix(void) {
1358
- time_t now = time(NULL);
1359
- struct tm tmv;
1360
- const struct tm *p = localtime(&now);
1361
- char buf[40];
1362
- size_t n;
1363
-
1364
- if (!p) return;
1365
- tmv = *p;
1366
- n = strftime(buf, sizeof(buf), "[%Y-%m-%d %H:%M:%S] ", &tmv);
1367
- if (n > 0) fwrite(buf, 1, n, stderr);
1368
- }
1369
-
1370
- /* 真实大小以文件为准(不自己记数:平台与仓颉侧也在往同一个文件写)。
1371
- *
1372
- * 为什么不用 stat():单文件包(scripts/pack-win-single.sh)里桥由 llvm-mingw(ucrt) 编译、
1373
- * 却由 SDK 自带 mingw 的 CRT 链接——ucrt 把 stat 映射成 stat64i32,而那边的导入库只认
1374
- * _stat,于是链接期报 `undefined symbol: stat64i32`(2026-10-21 实测,只有静态单文件包会炸,
1375
- * 平时的动态构建不受影响)。fopen/fseek/ftell 在 ucrt、msvcrt 与 mingw 三套 CRT 里符号名
1376
- * 一致,绕开这层方言差异;语义相同(文件不存在 → 0)。 */
1377
- static long cj_log_size(void) {
1378
- FILE *f;
1379
- long size;
1380
-
1381
- if (!g_log_path[0]) return 0;
1382
- fflush(stderr);
1383
- f = fopen(g_log_path, "rb");
1384
- if (!f) return 0;
1385
- if (fseek(f, 0, SEEK_END) != 0) {
1386
- fclose(f);
1387
- return 0;
1388
- }
1389
- size = ftell(f);
1390
- fclose(f);
1391
- return size > 0 ? size : 0;
1392
- }
1393
-
1394
- /* 当前文件整份拷进 `<path>.<n>`(Windows 上不能 rename 打开中的文件,只能拷贝)。 */
1395
- static void cj_log_copy_archive(long n) {
1396
- FILE *in;
1397
- FILE *out;
1398
- char dst[CJ_LOG_PATH_MAX + 24];
1399
- char buf[8192];
1400
- size_t got;
1401
- long written = 0;
1402
-
1403
- snprintf(dst, sizeof(dst), "%s.%ld", g_log_path, n);
1404
- in = fopen(g_log_path, "rb");
1405
- if (!in) return;
1406
- out = fopen(dst, "wb");
1407
- if (!out) {
1408
- fclose(in);
1409
- return;
1410
- }
1411
- while ((got = fread(buf, 1, sizeof(buf), in)) > 0) {
1412
- if (g_log_max > 0) {
1413
- /* 档案不超过预算,免得轮转频繁时档案总占用翻倍 */
1414
- if (written + (long)got > g_log_max) got = (size_t)(g_log_max - written);
1415
- if ((long)got <= 0) break;
1416
- }
1417
- if (fwrite(buf, 1, got, out) != got) break;
1418
- written += (long)got;
1419
- }
1420
- fclose(out);
1421
- fclose(in);
1422
- }
1423
-
1424
- /* 已关闭的档案平移:`.keep-1 → .keep` … `.1 → .2`(最老那份先删,Windows 上 rename
1425
- 到已存在的文件会失败)。keep ≤ 0 时不动档案,轮转只截断。 */
1426
- static void cj_log_shift(void) {
1427
- long i;
1428
- char from[CJ_LOG_PATH_MAX + 24];
1429
- char to[CJ_LOG_PATH_MAX + 24];
1430
-
1431
- if (g_log_keep <= 0) return;
1432
- snprintf(to, sizeof(to), "%s.%ld", g_log_path, g_log_keep);
1433
- remove(to);
1434
- for (i = g_log_keep - 1; i >= 1; i--) {
1435
- snprintf(from, sizeof(from), "%s.%ld", g_log_path, i);
1436
- snprintf(to, sizeof(to), "%s.%ld", g_log_path, i + 1);
1437
- rename(from, to); /* 不存在就静默跳过 */
1438
- }
1439
- }
1440
-
1441
- static int cj_log_rotate(void) {
1442
- if (!g_log_active) return 0;
1443
- if (!cj_log_lock()) return 0; /* 别人正在轮转:这一行让给下一轮 */
1444
- if (g_log_keep > 0) {
1445
- cj_log_shift();
1446
- cj_log_copy_archive(1);
1447
- }
1448
- fflush(stderr);
1449
- if (cj_log_truncate(cj_log_fileno(stderr)) != 0) {
1450
- /* 截断失败(有的文件系统不给改长度):如实说一句,别假装轮转过 */
1451
- fprintf(stderr, "[cj-bridge] log rotate failed: truncate errno=%d\n", errno);
1452
- }
1453
- fprintf(stderr, "[cj-bridge] log rotated: keep=%ld archive=%s.1\n", g_log_keep, g_log_path);
1454
- cj_log_unlock();
1455
- return 1;
1456
- }
1457
-
1458
- void cj_core_logf(const char *fmt, ...) {
1459
- va_list ap;
1460
-
1461
- if (g_log_active) {
1462
- if (g_log_max > 0 && cj_log_size() + CJ_LOG_LINE_SLACK >= g_log_max) cj_log_rotate();
1463
- cj_log_prefix();
1464
- }
1465
- va_start(ap, fmt);
1466
- vfprintf(stderr, fmt, ap);
1467
- va_end(ap);
1468
- if (g_log_active) fflush(stderr); /* 每行即时落盘:进程被强杀也不丢 */
1469
- }
1470
-
1471
- /* 父目录不在就逐个建出来:默认目录(`~/.local/state/cj-tauri/` 这类)常常还不存在。
1472
- 建目录失败不在这里报——紧接着的 open 会带 errno 如实说,少一条重复的日志。 */
1473
- static void cj_log_ensure_dirs(const char *path) {
1474
- char buf[CJ_LOG_PATH_MAX];
1475
- size_t i;
1476
- size_t n = strlen(path);
1477
-
1478
- if (n == 0 || n >= sizeof(buf)) return;
1479
- memcpy(buf, path, n + 1);
1480
- for (i = 1; i < n; i++) {
1481
- char sep;
1482
- if (buf[i] != '/' && buf[i] != '\\') continue;
1483
- sep = buf[i];
1484
- buf[i] = '\0';
1485
- /* 跳过盘符(`C:`)与别名的根:它们建不了也不需要建 */
1486
- if (!(i == 2 && buf[1] == ':')) cj_log_mkdir(buf);
1487
- buf[i] = sep;
1488
- }
1489
- }
1490
-
1491
- CJ_BRIDGE_API int cj_bridge_set_log_file(const char *path, int max_kb, int keep) {
1492
- int fd;
1493
-
1494
- if (g_log_active) {
1495
- fprintf(stderr, "[cj-bridge] log file already active: %s (restart to change)\n", g_log_path);
1496
- return 0;
1497
- }
1498
- if (!path || !path[0]) {
1499
- fprintf(stderr, "[cj-bridge] log file refused: empty path\n");
1500
- return 0;
1501
- }
1502
- if (strlen(path) >= CJ_LOG_PATH_MAX) {
1503
- fprintf(stderr, "[cj-bridge] log file refused: path too long\n");
1504
- return 0;
1505
- }
1506
- /* 先 open 成功再动 fd 2:失败时 stderr 完好,失败原因才有人看得见 */
1507
- fd = cj_log_open(path, CJ_LOG_OFLAGS, CJ_LOG_OMODE);
1508
- if (fd < 0 && errno == ENOENT) {
1509
- /* 只有「父目录根本不存在」才顺手建出来(默认目录 `~/.local/state/cj-tauri` 常常还不存在)。
1510
- 别的 errno(没权限、路径中间穿了普通文件、名字太长)一律原样失败——
1511
- 照着一个开不了的路径去建目录,等于把一次手滑变成磁盘上的垃圾。 */
1512
- cj_log_ensure_dirs(path);
1513
- fd = cj_log_open(path, CJ_LOG_OFLAGS, CJ_LOG_OMODE);
1514
- }
1515
- if (fd < 0) {
1516
- fprintf(stderr, "[cj-bridge] log file open failed: %s (errno=%d)\n", path, errno);
1517
- return 0;
1518
- }
1519
- if (fd != 2) {
1520
- if (cj_log_dup2(fd, 2) < 0) {
1521
- fprintf(stderr, "[cj-bridge] log file open failed: %s (dup2 errno=%d)\n", path, errno);
1522
- cj_log_close(fd);
1523
- return 0;
1524
- }
1525
- cj_log_close(fd);
1526
- }
1527
- snprintf(g_log_path, sizeof(g_log_path), "%s", path);
1528
- g_log_max = max_kb > 0 ? (long)max_kb * 1024L : 0;
1529
- g_log_keep = keep > 0 ? (long)keep : 0;
1530
- g_log_active = 1;
1531
- /* 第一行就落在新文件里:开了、开在哪、预算多少,都能自查 */
1532
- cj_logf("[cj-bridge] log file: %s (max=%ldKB keep=%ld)\n", g_log_path, g_log_max / 1024, g_log_keep);
1533
- return 1;
1534
- }
1535
-
1536
- CJ_BRIDGE_API const char *cj_bridge_log_file(void) {
1537
- return g_log_active ? g_log_path : "";
1538
- }
1539
-
1540
- CJ_BRIDGE_API int cj_bridge_log_write(const char *line) {
1541
- if (!line) return -1;
1542
- cj_logf("%s\n", line);
1543
- return 0;
1544
- }
1545
-
1546
- /* =========================================================================
1547
- * 静默子进程(为什么下沉到 C 桥见头文件同名段落)
1548
- *
1549
- * core 只做参数校验、错误出口与日志;创建 / 终止 / 探活是平台原语。这样两平台导出名与
1550
- * 签名逐字一致,仓颉侧没有平台分叉的调用点(AGENTS §2 的分工铁律)。
1551
- * ========================================================================= */
1552
-
1553
- CJ_BRIDGE_API long long cj_bridge_spawn_hidden(const char *exe, const char *args) {
1554
- char err[512];
1555
- long long pid;
1556
-
1557
- err[0] = '\0';
1558
- if (!exe || !*exe) {
1559
- cj_logf("[cj-bridge] spawn hidden: 程序路径为空\n");
1560
- return 0;
1561
- }
1562
- pid = cj_plat_spawn_hidden(exe, args ? args : "", err, (int)sizeof(err));
1563
- if (pid <= 0) {
1564
- cj_logf("[cj-bridge] spawn hidden 失败(%s):%s\n", exe,
1565
- err[0] ? err : "平台没有给出原因");
1566
- return 0;
1567
- }
1568
- cj_logf("[cj-bridge] spawn hidden: %s -> pid=%lld\n", exe, pid);
1569
- return pid;
1570
- }
1571
-
1572
- CJ_BRIDGE_API int cj_bridge_kill_pid(long long pid) {
1573
- if (pid <= 0) {
1574
- cj_logf("[cj-bridge] kill pid: 非法 pid=%lld\n", pid);
1575
- return 0;
1576
- }
1577
- return cj_plat_kill_pid(pid);
1578
- }
1579
-
1580
- CJ_BRIDGE_API int cj_bridge_pid_alive(long long pid) {
1581
- if (pid <= 0) return 0;
1582
- return cj_plat_pid_alive(pid);
1583
- }
1584
-
1585
- /* 内嵌资源释放:core 只做参数校验与错误出口,取资源写盘是平台原语
1586
- (为什么用字符串资源名、为什么幂等,见头文件同名段落与平台实现)。 */
1587
- CJ_BRIDGE_API int cj_bridge_extract_assets(const char *dir) {
1588
- char err[512];
1589
- int n;
1590
-
1591
- err[0] = '\0';
1592
- if (!dir || !*dir) {
1593
- cj_logf("[cj-bridge] extract assets: 目录为空\n");
1594
- return -1;
1595
- }
1596
- n = cj_plat_extract_assets(dir, err, (int)sizeof(err));
1597
- if (n < 0) {
1598
- cj_logf("[cj-bridge] extract assets 失败(%s):%s\n", dir, err[0] ? err : "平台没有给出原因");
1599
- return -1;
1600
- }
1601
- if (n > 0) cj_logf("[cj-bridge] extract assets: %d 个文件就绪(%s)\n", n, dir);
1602
- return n;
1603
- }
1604
-
1605
- /* 预加载动态库:core 只做参数校验与错误出口,LoadLibrary/dlopen 是平台原语。
1606
- 为什么要它见头文件同名段落(2026-10-21:单文件包在别人的机器上连明文 ws:// 都挂在
1607
- `Can not load openssl library or function RAND_bytes.`——运行时的随机数默认走 OpenSSL,
1608
- 而它按裸名找 DLL,只有开发机的 PATH 恰好有 Git 自带那份)。 */
1609
- CJ_BRIDGE_API int cj_bridge_preload_library(const char *path) {
1610
- char err[512];
1611
- int ok;
1612
-
1613
- err[0] = '\0';
1614
- if (!path || !*path) {
1615
- cj_logf("[cj-bridge] preload library: 路径为空\n");
1616
- return 0;
1617
- }
1618
- ok = cj_plat_preload_library(path, err, (int)sizeof(err));
1619
- if (ok <= 0) {
1620
- cj_logf("[cj-bridge] preload 失败(%s):%s\n", path, err[0] ? err : "平台没有给出原因");
1621
- return 0;
1622
- }
1623
- cj_logf("[cj-bridge] preload: %s\n", path);
1624
- return 1;
1625
- }
1626
-
1
+ /*
2
+ * cj-tauri C 桥公共核心(批次 1):句柄化的宿主状态 + 平台无关逻辑
3
+ *
4
+ * 契约见 native/bridge_core.h。本文件只放「两平台逐字相同」的东西:
5
+ * 回投队列与批处理(含单条快路径)、对话框单槽状态机、窗口配置与预执行脚本存储、
6
+ * 生命周期标志、以及**全部 cj_bridge_* 导出**(单一来源,两平台导出一致由此保证)。
7
+ * 任何平台能力都经 cj_plat_*,本文件不 include 任何平台头。
8
+ */
9
+ /* 日志落盘要用 ftruncate / fileno 这些 POSIX 名,而桩自检是按 `-std=c99` 编译的,
10
+ 该标准档位下它们默认被隐藏(表象是 implicit declaration 警告)。特性宏必须写在
11
+ 任何 include 之前,否则不生效。 */
12
+ #define _POSIX_C_SOURCE 200809L
13
+
14
+ #include "bridge_core.h"
15
+
16
+ /* 日志落盘要用到的 CRT 原语(RFC-003 第 5 期)。这是**编译器运行库**的名字差异,
17
+ 不是平台实现差异——core 依旧不碰 pthread / windows.h / gtk 这些宿主头。 */
18
+ #include <stdarg.h>
19
+ #include <errno.h>
20
+ #include <time.h>
21
+ #include <sys/stat.h>
22
+ #include <fcntl.h>
23
+ #if defined(_WIN32)
24
+ #include <io.h>
25
+ #include <direct.h>
26
+ #define cj_log_open _open
27
+ #define cj_log_dup2 _dup2
28
+ #define cj_log_close _close
29
+ #define cj_log_fileno _fileno
30
+ #define cj_log_mkdir(path) _mkdir(path)
31
+ #define cj_log_truncate(fd) _chsize((fd), 0)
32
+ #define CJ_LOG_OFLAGS (_O_WRONLY | _O_CREAT | _O_APPEND)
33
+ #define CJ_LOG_OMODE (_S_IREAD | _S_IWRITE)
34
+ #else
35
+ #include <unistd.h>
36
+ #define cj_log_open open
37
+ #define cj_log_dup2 dup2
38
+ #define cj_log_close close
39
+ #define cj_log_fileno fileno
40
+ #define cj_log_mkdir(path) mkdir((path), 0755)
41
+ #define cj_log_truncate(fd) ftruncate((fd), 0)
42
+ #define CJ_LOG_OFLAGS (O_WRONLY | O_CREAT | O_APPEND)
43
+ #define CJ_LOG_OMODE 0644
44
+ #endif
45
+
46
+ /* ===== 内部小工具 ===== */
47
+
48
+ static char *cj_strdup(const char *s) {
49
+ size_t n;
50
+ char *p;
51
+ if (!s) return NULL;
52
+ n = strlen(s) + 1;
53
+ p = (char *)malloc(n);
54
+ if (!p) return NULL;
55
+ memcpy(p, s, n);
56
+ return p;
57
+ }
58
+
59
+ /* 回投队列整体释放(宿主销毁时兜底) */
60
+ static void cj_js_queue_free(cj_host *h) {
61
+ js_node *n = h->js_head;
62
+ while (n) {
63
+ js_node *next = n->next;
64
+ free(n->js);
65
+ free(n);
66
+ n = next;
67
+ }
68
+ h->js_head = NULL;
69
+ h->js_tail = NULL;
70
+ }
71
+
72
+ /* ===== 建 / 释放宿主 ===== */
73
+
74
+ cj_host *cj_bridge_create(void) {
75
+ cj_host *h = (cj_host *)calloc(1, sizeof(cj_host));
76
+ if (!h) {
77
+ cj_logf("[cj-bridge] create host failed: out of memory\n");
78
+ return NULL;
79
+ }
80
+ /* 默认与两平台原先的静态初值一致 */
81
+ h->win_w = 900;
82
+ h->win_h = 640;
83
+ h->devtools = 1;
84
+ h->win_visible = 1; /* 默认显示:没碰窗口可见性的应用行为不变 */
85
+ h->js_lock = cj_plat_sync_create();
86
+ h->dlg_lock = cj_plat_sync_create();
87
+ h->clip_lock = cj_plat_sync_create();
88
+ if (!h->js_lock || !h->dlg_lock || !h->clip_lock) {
89
+ if (h->js_lock) cj_plat_sync_destroy(h->js_lock);
90
+ if (h->dlg_lock) cj_plat_sync_destroy(h->dlg_lock);
91
+ if (h->clip_lock) cj_plat_sync_destroy(h->clip_lock);
92
+ free(h);
93
+ cj_logf("[cj-bridge] create host failed: sync init failed\n");
94
+ return NULL;
95
+ }
96
+ cj_logf("[cj-bridge] host created: %p\n", (void *)h);
97
+ return h;
98
+ }
99
+
100
+ void cj_bridge_destroy(cj_host *h) {
101
+ int i;
102
+ if (!h) return;
103
+ /* 平台资源先走:COM 环境引用 / 模块句柄等(幂等)。返回 0=宿主线程还在收尾,
104
+ 它可能仍在用 h(GTK/WebView2 回调的 user_data 就是它)——这时连句柄一起留着不释放。 */
105
+ if (!cj_plat_fini(h)) {
106
+ cj_logf("[cj-bridge] destroy: host thread still running, handle kept\n");
107
+ return;
108
+ }
109
+ if (h->js_lock) cj_plat_sync_destroy(h->js_lock);
110
+ if (h->dlg_lock) cj_plat_sync_destroy(h->dlg_lock);
111
+ if (h->clip_lock) cj_plat_sync_destroy(h->clip_lock);
112
+ cj_js_queue_free(h);
113
+ /* dlg_cur 此时应为空:窗口销毁路径已作废并交了槽位;故意不碰,免得与等待方抢 free */
114
+ free(h->win_title);
115
+ free(h->win_icon);
116
+ free(h->pending_menu_text);
117
+ free(h->pending_hotkeys);
118
+ free(h->pending_tray);
119
+ for (i = 0; i < h->init_script_count; i++) {
120
+ free(h->init_scripts[i]);
121
+ }
122
+ free(h->init_scripts);
123
+ free(h->pending_html);
124
+ free(h->pending_url);
125
+ cj_logf("[cj-bridge] host destroyed: %p\n", (void *)h);
126
+ free(h);
127
+ }
128
+
129
+ void cj_bridge_init(cj_host *h, cj_on_message_fn m, cj_on_destroy_fn d) {
130
+ if (!h) return;
131
+ h->on_message = m;
132
+ h->on_destroy = d;
133
+ cj_plat_init(h); /* 平台一次性初始化(Windows 在这里装 COM vtbl 并打 init 日志) */
134
+ /* 能力位:平台给静态事实(有没有菜单 / 托盘 / 拖放),core 只缓存——仓颉侧 system:host 如实上报 */
135
+ h->capabilities = cj_plat_host_capabilities(h);
136
+ cj_logf("[cj-bridge] host capabilities: 0x%x (menu=%d tray=%d dragDrop=%d notify=%d hotkey=%d)\n",
137
+ (unsigned)h->capabilities,
138
+ (h->capabilities & CJ_CAP_MENU) ? 1 : 0,
139
+ (h->capabilities & CJ_CAP_TRAY) ? 1 : 0,
140
+ (h->capabilities & CJ_CAP_DRAG_DROP) ? 1 : 0,
141
+ (h->capabilities & CJ_CAP_NOTIFICATION) ? 1 : 0,
142
+ (h->capabilities & CJ_CAP_HOTKEY) ? 1 : 0);
143
+ }
144
+
145
+ /* ===== 窗口配置 / 预执行脚本(必须在 cj_bridge_start 之前注入)===== */
146
+
147
+ void cj_bridge_set_window(cj_host *h, const char *title, int width, int height) {
148
+ if (!h) return;
149
+ cj_logf("[cj-bridge] set window: title=%s size=%dx%d\n",
150
+ title ? title : "(default)", width, height);
151
+ free(h->win_title);
152
+ h->win_title = NULL;
153
+ if (title) {
154
+ h->win_title = cj_strdup(title);
155
+ }
156
+ if (width > 0) h->win_w = width;
157
+ if (height > 0) h->win_h = height;
158
+ }
159
+
160
+ void cj_bridge_set_icon(cj_host *h, const char *path) {
161
+ if (!h) return;
162
+ cj_logf("[cj-bridge] set icon: path=%s\n", (path && path[0]) ? path : "(default)");
163
+ free(h->win_icon);
164
+ h->win_icon = NULL;
165
+ if (path && path[0]) {
166
+ h->win_icon = cj_strdup(path);
167
+ }
168
+ }
169
+
170
+ void cj_bridge_set_devtools(cj_host *h, int enabled) {
171
+ if (!h) return;
172
+ h->devtools = enabled ? 1 : 0;
173
+ cj_logf("[cj-bridge] devtools %s\n", h->devtools ? "enabled" : "disabled");
174
+ }
175
+
176
+ void cj_bridge_add_init_script(cj_host *h, const char *js) {
177
+ if (!h || !js || !js[0]) {
178
+ return;
179
+ }
180
+ if (h->init_script_count == h->init_script_cap) {
181
+ int cap = h->init_script_cap ? h->init_script_cap * 2 : 4;
182
+ char **grown = (char **)realloc(h->init_scripts, (size_t)cap * sizeof(char *));
183
+ if (!grown) {
184
+ cj_logf("[cj-bridge] add init script failed: out of memory\n");
185
+ return;
186
+ }
187
+ h->init_scripts = grown;
188
+ h->init_script_cap = cap;
189
+ }
190
+ h->init_scripts[h->init_script_count] = cj_strdup(js);
191
+ if (!h->init_scripts[h->init_script_count]) {
192
+ cj_logf("[cj-bridge] add init script failed: out of memory\n");
193
+ return;
194
+ }
195
+ h->init_script_count++;
196
+ cj_logf("[cj-bridge] init script queued: %d bytes (total %d)\n",
197
+ (int)strlen(js), h->init_script_count);
198
+ }
199
+
200
+ /* ===== 菜单栏(RFC-002;v1 只做菜单栏,托盘 / 拖放在后续档期)===== */
201
+
202
+ void cj_bridge_set_menu(cj_host *h, const char *wire) {
203
+ int rows = 0;
204
+ const char *p;
205
+ if (!h || !wire) return;
206
+ for (p = wire; *p; p++) {
207
+ if (*p == '\n') rows++;
208
+ }
209
+ cj_logf("[cj-bridge] set menu: %d bytes, %d row(s)\n", (int)strlen(wire), rows);
210
+ free(h->pending_menu_text);
211
+ h->pending_menu_text = cj_strdup(wire);
212
+ /* 两头都要能用:未就绪时平台只记下(建窗时读 h->pending_menu_text),就绪后平台投到 UI 线程重建 */
213
+ cj_plat_set_menu(h, h->pending_menu_text ? h->pending_menu_text : "");
214
+ }
215
+
216
+ int cj_bridge_set_titlebar_subtitle(cj_host *h, const char *text) {
217
+ int rc;
218
+
219
+ if (!h || !text) return -1;
220
+ if (strlen(text) > 1024) return -1;
221
+ rc = cj_plat_set_titlebar_subtitle(h, text);
222
+ /* 失败必须留痕:页面上的状态行不出现时,这一行是唯一能区分
223
+ 「应用没调」与「平台没这块」的凭证 */
224
+ if (rc != 0) {
225
+ cj_logf("[cj-bridge] titlebar subtitle rejected: rc=%d text=\"%s\"\n", rc, text);
226
+ }
227
+ return rc;
228
+ }
229
+
230
+ void cj_bridge_set_menu_item_state(cj_host *h, const char *id, int enabled, int checked) {
231
+ if (!h || !id || !id[0]) return;
232
+ cj_logf("[cj-bridge] set menu item: id=%s enabled=%d checked=%d\n",
233
+ id, enabled ? 1 : 0, checked ? 1 : 0);
234
+ cj_plat_menu_item_state(h, id, enabled ? 1 : 0, checked ? 1 : 0);
235
+ }
236
+
237
+ /* ===== 拖放(RFC-002 §5;v1 载荷只有文件路径)===== */
238
+
239
+ void cj_bridge_set_drag_drop(cj_host *h, int enabled, int files) {
240
+ if (!h) return;
241
+ cj_logf("[cj-bridge] set drag drop: enabled=%d files=%d\n",
242
+ enabled ? 1 : 0, files ? 1 : 0);
243
+ h->pending_drop_enabled = enabled ? 1 : 0;
244
+ h->pending_drop_files = files ? 1 : 0;
245
+ /* 两头都能用:未就绪时平台只记(本函数已写进 pending 字段,建 view 时读);
246
+ 就绪后平台投 UI 线程重挂 drag dest */
247
+ cj_plat_set_drag_drop(h, enabled ? 1 : 0, files ? 1 : 0);
248
+ }
249
+
250
+ /* ===== 全局快捷键(RFC-003 第 5 期首个落地项)===== */
251
+
252
+ void cj_bridge_set_global_hotkeys(cj_host *h, const char *wire) {
253
+ if (!h) return;
254
+ cj_logf("[cj-bridge] set global hotkeys: %s\n",
255
+ (wire && wire[0]) ? wire : "(none)");
256
+ free(h->pending_hotkeys);
257
+ h->pending_hotkeys = cj_strdup(wire ? wire : "");
258
+ /* 两头都能用:未就绪时平台只记(建宿主线程后读 h->pending_hotkeys);
259
+ 就绪后整表替换(ungrab 旧表再 grab 新表)。平台不支持时在 cj_plat_* 里打一行如实说明。 */
260
+ cj_plat_set_global_hotkeys(h, h->pending_hotkeys ? h->pending_hotkeys : "");
261
+ }
262
+
263
+ /* ===== 托盘(RFC-002 §7.C)===== */
264
+
265
+ void cj_bridge_set_tray(cj_host *h, const char *wire) {
266
+ if (!h) return;
267
+ cj_logf("[cj-bridge] set tray: %d bytes\n",
268
+ (int)(wire ? strlen(wire) : 0));
269
+ free(h->pending_tray);
270
+ h->pending_tray = cj_strdup(wire ? wire : "");
271
+ /* 两头都能用:未就绪时平台只记(宿主线程装配时读 h->pending_tray);
272
+ 就绪后投 UI 线程重建。平台没编托盘支持时在 cj_plat_* 里打一行如实说明。 */
273
+ cj_plat_set_tray(h, h->pending_tray ? h->pending_tray : "");
274
+ }
275
+
276
+ /* ===== 窗口可见性 / 最小化到托盘(RFC-003 第 5 期余项)===== */
277
+
278
+ void cj_bridge_set_window_visible(cj_host *h, int visible) {
279
+ if (!h) return;
280
+ cj_logf("[cj-bridge] set window visible: %d\n", visible ? 1 : 0);
281
+ h->win_visible = visible ? 1 : 0;
282
+ /* 两头都能用:**未就绪时到此为止**——平台建窗时读 h->win_visible 定稿(建窗那一刻就定下来,
283
+ 比「先显示再藏起来」少一次闪烁);就绪后平台投 UI 线程执行。 */
284
+ if (h->ready) {
285
+ cj_plat_set_window_visible(h, h->win_visible);
286
+ }
287
+ }
288
+
289
+ void cj_bridge_set_minimize_to_tray(cj_host *h, int enabled) {
290
+ if (!h) return;
291
+ h->minimize_to_tray = enabled ? 1 : 0;
292
+ cj_logf("[cj-bridge] set minimize to tray: %d\n", h->minimize_to_tray);
293
+ /* 这里只记 + 转给平台,**不**在此刻查能力位:能力位(cj_plat_init)与「应用有没有真的 setTray」
294
+ 的先后顺序不稳定,提前拒绝会误伤。策略在**真要最小化那一刻**由平台复核托盘是否装上了。 */
295
+ cj_plat_set_minimize_to_tray(h, h->minimize_to_tray);
296
+ }
297
+
298
+ void cj_bridge_set_owner_window(cj_host *h, cj_host *owner) {
299
+ if (!h) return;
300
+ h->owner = owner;
301
+ cj_logf("[cj-bridge] set owner window: %s\n", owner ? "yes" : "none");
302
+ /* 与 set_window_visible 同形:未就绪时到此为止,平台建窗时读 h->owner 定稿
303
+ (建窗那一刻就挂好,不会先占一个任务栏按钮再消失)。 */
304
+ if (h->ready) {
305
+ cj_plat_set_owner_window(h, owner);
306
+ }
307
+ }
308
+
309
+ /* ===== 同窗内嵌视图(PLT-006 真标签页)=====
310
+ core 只做参数校验与留痕,句柄与生命周期全在平台(它才认得自己的 controller / widget)。 */
311
+
312
+ long long cj_bridge_view_create(cj_host *h, const char *url, int x, int y, int w, int hgt) {
313
+ long long view;
314
+
315
+ if (!h || !url || !url[0]) return 0;
316
+ if (w <= 0 || hgt <= 0) return 0;
317
+ if (strlen(url) > 2048) return 0;
318
+ view = cj_plat_view_create(h, url, x, y, w, hgt);
319
+ /* 成功失败都留痕:应用侧只会看到「点了没反应」,这一行是唯一能区分
320
+ 「平台没这块」与「环境复用 / 宿主未就绪」的凭证 */
321
+ cj_logf("[cj-bridge] view create: rect=%d,%d %dx%d url=%s -> %lld\n",
322
+ x, y, w, hgt, url, view);
323
+ return view;
324
+ }
325
+
326
+ void cj_bridge_view_set_visible(cj_host *h, long long view, int visible) {
327
+ if (!h || view <= 0) return;
328
+ cj_logf("[cj-bridge] view visible: id=%lld visible=%d\n", view, visible ? 1 : 0);
329
+ cj_plat_view_set_visible(h, view, visible ? 1 : 0);
330
+ }
331
+
332
+ /* 高频调用(页面 resize / 布局变化都要同步),**不打日志**:一行一个矩形会把桥的日志冲掉,
333
+ 真出问题看的是「切换后有没有显示」而不是矩形的每一次微调。 */
334
+ void cj_bridge_view_set_bounds(cj_host *h, long long view, int x, int y, int w, int hgt) {
335
+ if (!h || view <= 0) return;
336
+ if (w <= 0 || hgt <= 0) return;
337
+ cj_plat_view_set_bounds(h, view, x, y, w, hgt);
338
+ }
339
+
340
+ void cj_bridge_view_navigate(cj_host *h, long long view, const char *url) {
341
+ if (!h || view <= 0 || !url || !url[0]) return;
342
+ if (strlen(url) > 2048) return;
343
+ cj_logf("[cj-bridge] view navigate: id=%lld url=%s\n", view, url);
344
+ cj_plat_view_navigate(h, view, url);
345
+ }
346
+
347
+ void cj_bridge_view_destroy(cj_host *h, long long view) {
348
+ if (!h || view <= 0) return;
349
+ cj_logf("[cj-bridge] view destroy: id=%lld\n", view);
350
+ cj_plat_view_destroy(h, view);
351
+ }
352
+
353
+ /* ===== 运行期操作(投递细节留给平台:各平台的原生时机与日志口径不同)===== */
354
+
355
+ void cj_bridge_start(cj_host *h, const char *html) {
356
+ if (!h) return;
357
+ if (html && html[0]) { /* 空串表示「页面由 cj_bridge_load_url 指定」 */
358
+ free(h->pending_html);
359
+ h->pending_html = cj_strdup(html);
360
+ }
361
+ cj_plat_start(h);
362
+ }
363
+
364
+ void cj_bridge_load_url(cj_host *h, const char *url) {
365
+ if (!h || !url || !url[0]) return;
366
+ free(h->pending_url);
367
+ h->pending_url = cj_strdup(url);
368
+ cj_plat_load_url(h, url);
369
+ }
370
+
371
+ void cj_bridge_reload(cj_host *h) {
372
+ if (!h) return;
373
+ cj_plat_reload(h);
374
+ }
375
+
376
+ void cj_bridge_open_devtools(cj_host *h) {
377
+ if (!h) return;
378
+ cj_plat_open_devtools(h);
379
+ }
380
+
381
+ void cj_bridge_quit(cj_host *h) {
382
+ if (!h) return;
383
+ h->should_quit = 1;
384
+ /* 平台走与「用户关窗」同一条销毁路径,onDestroy 因此恰好触发一次(批次 1 验收项) */
385
+ cj_plat_quit(h);
386
+ }
387
+
388
+ /* ===== 原生 → JS:入队(执行在 UI 线程的 cj_core_flush_js 里)===== */
389
+
390
+ void cj_bridge_run_js(cj_host *h, const char *js) {
391
+ js_node *node;
392
+ int was_empty;
393
+ if (!h || !js) return;
394
+ node = (js_node *)calloc(1, sizeof(js_node));
395
+ if (!node) return;
396
+ node->js = cj_strdup(js);
397
+ if (!node->js) {
398
+ free(node);
399
+ return;
400
+ }
401
+ if (!h->js_lock) {
402
+ /* 未 init 时直接丢弃,避免未初始化锁 */
403
+ free(node->js);
404
+ free(node);
405
+ return;
406
+ }
407
+ cj_plat_sync_lock(h->js_lock);
408
+ if (h->js_tail) {
409
+ h->js_tail->next = node;
410
+ } else {
411
+ h->js_head = node;
412
+ }
413
+ h->js_tail = node;
414
+ was_empty = (h->js_head == node); /* 队列由空转非空:需要唤醒 UI 线程 */
415
+ cj_plat_sync_unlock(h->js_lock);
416
+ if (was_empty) {
417
+ cj_plat_wake_ui(h); /* 平台自己判断窗口是否已就绪(未就绪就攒着) */
418
+ }
419
+ }
420
+
421
+ int cj_bridge_is_ready(cj_host *h) {
422
+ return h ? (int)h->ready : 0;
423
+ }
424
+
425
+ int cj_bridge_should_quit(cj_host *h) {
426
+ return h ? (int)h->should_quit : 0;
427
+ }
428
+
429
+ /* ===== core 提供给平台的状态迁移 ===== */
430
+
431
+ void cj_core_set_ready(cj_host *h) {
432
+ if (h) h->ready = 1;
433
+ }
434
+
435
+ void cj_core_window_destroyed(cj_host *h) {
436
+ if (!h) return;
437
+ h->window_gone = 1;
438
+ h->should_quit = 1;
439
+ /* 每个宿主恰好通知一次:无论是用户关窗还是 cj_bridge_quit,都汇到这条路径 */
440
+ if (!h->destroy_notified) {
441
+ h->destroy_notified = 1;
442
+ if (h->on_destroy) {
443
+ h->on_destroy(h); /* 带上句柄:仓颉侧据此认领「是哪扇窗没了」(多窗口不许再走静态槽) */
444
+ }
445
+ }
446
+ /* 有对话框开着时先作废并唤醒等待方:事件循环退出后挂起的回调不再执行,等待方会永远挂住 */
447
+ cj_core_dialog_abort(h);
448
+ /* 剪贴板同理:投递进 UI 线程的读写不会再被执行 */
449
+ cj_core_clipboard_abort(h);
450
+ }
451
+
452
+ /* ===== 剪贴板(plugin_clipboard):单槽 + UI 线程投递 ===== */
453
+
454
+ /* UI 线程在 GTK 线程上执行真正的剪贴板调用(经平台原语),结果放回槽位并唤醒等待方 */
455
+ /* 返回 1 = 成功(含读出空),0 = 平台失败。UI 线程直行路径靠它取结果。 */
456
+ int cj_core_clipboard_tick(cj_host *h, int is_write, char *text) {
457
+ int ok = 0;
458
+ char *readback = NULL;
459
+
460
+ if (!h) {
461
+ free(text);
462
+ return 0;
463
+ }
464
+ if (is_write) {
465
+ ok = (cj_plat_clipboard_write_text(h, text ? text : "", NULL, 0) == 0);
466
+ if (ok) {
467
+ cj_logf("[cj-bridge] clipboard write: %d bytes\n",
468
+ text ? (int)strlen(text) : 0);
469
+ } else {
470
+ cj_logf("[cj-bridge] clipboard write failed\n");
471
+ }
472
+ free(text);
473
+ } else {
474
+ readback = cj_plat_clipboard_read_text(h, NULL, 0);
475
+ ok = 1; /* 读失败按「空结果」处理:NULL 是合法回值(剪贴板为空/非文本)*/
476
+ cj_logf("[cj-bridge] clipboard read: %s\n",
477
+ readback ? "got text" : "empty");
478
+ }
479
+
480
+ cj_plat_sync_lock(h->clip_lock);
481
+ h->clip_ok = ok;
482
+ h->clip_text = readback;
483
+ h->clip_ready = 1;
484
+ h->clip_busy = 0;
485
+ cj_plat_sync_signal(h->clip_lock);
486
+ cj_plat_sync_unlock(h->clip_lock);
487
+ return ok;
488
+ }
489
+
490
+ /* 作废进行中的剪贴板请求并唤醒等待方(ok=0;等待方知道宿主已不在,不再碰槽位)*/
491
+ void cj_core_clipboard_abort(cj_host *h) {
492
+ if (!h || !h->clip_lock) return;
493
+ cj_plat_sync_lock(h->clip_lock);
494
+ if (h->clip_busy && !h->clip_ready) {
495
+ h->clip_ok = 0;
496
+ h->clip_text = NULL;
497
+ h->clip_ready = 1;
498
+ h->clip_busy = 0;
499
+ cj_plat_sync_signal(h->clip_lock);
500
+ }
501
+ cj_plat_sync_unlock(h->clip_lock);
502
+ }
503
+
504
+ /* UI 线程投递的载荷:一个指针 + 一个标志 */
505
+ typedef struct clip_call {
506
+ cj_host *h;
507
+ int is_write;
508
+ char *text;
509
+ } clip_call;
510
+
511
+ static void clip_idle(void *d) {
512
+ clip_call *c = (clip_call *)d;
513
+ if (c) {
514
+ cj_core_clipboard_tick(c->h, c->is_write, c->text);
515
+ free(c);
516
+ }
517
+ }
518
+
519
+ static int clipboard_execute(cj_host *h, int is_write, char *text, char **out_text) {
520
+ if (!h || !h->clip_lock) {
521
+ free(text);
522
+ return -1;
523
+ }
524
+ if (h->window_gone) {
525
+ free(text);
526
+ return -3; /* 宿主已销毁:不再往已退出的 GTK 循环投东西 */
527
+ }
528
+ cj_plat_sync_lock(h->clip_lock);
529
+ if (h->clip_busy) {
530
+ cj_plat_sync_unlock(h->clip_lock);
531
+ free(text);
532
+ return -3; /* 剪贴板是单槽:同时只允许一次(并发写抢剪贴板本身就没有意义)*/
533
+ }
534
+ h->clip_busy = 1;
535
+ h->clip_ready = 0;
536
+ h->clip_ok = 0;
537
+ h->clip_text = NULL;
538
+ cj_plat_sync_unlock(h->clip_lock);
539
+
540
+ if (cj_plat_is_ui_thread(h)) {
541
+ /* 已在 UI 线程(对话框同类):直接做,不投递——投递 + 等待会自锁(AGENTS §4)*/
542
+ int ok = cj_core_clipboard_tick(h, is_write, text);
543
+ if (!ok) {
544
+ return -3;
545
+ }
546
+ if (out_text) {
547
+ *out_text = h->clip_text; /* 所有权交调用方(tick 已把槽位清待取)*/
548
+ h->clip_text = NULL;
549
+ }
550
+ } else {
551
+ /* 投递的载荷必须堆分配:UI 线程处理完由 clip_idle free(通知路径同款)。
552
+ 早前这里是**栈上**结构体 + clip_idle 里 free,实机当场 munmap_chunk 崩掉
553
+ (AGENTS §4:跨线程投递的载荷谁 post 谁分配,UI 线程释放)。 */
554
+ clip_call *call = (clip_call *)malloc(sizeof(clip_call));
555
+ if (!call) {
556
+ free(text);
557
+ cj_core_clipboard_abort(h);
558
+ return -3;
559
+ }
560
+ call->h = h;
561
+ call->is_write = is_write;
562
+ call->text = text;
563
+ if (!cj_plat_post_ui(h, clip_idle, call)) {
564
+ /* 没投出去 = 回调永不执行:载荷与 text 仍归本函数,就地回收 */
565
+ free(call);
566
+ free(text);
567
+ cj_core_clipboard_abort(h);
568
+ return -3;
569
+ }
570
+ /* 等结果:窗口先关的话,cj_core_window_destroyed 会作废并唤醒(ok=0)*/
571
+ cj_plat_sync_lock(h->clip_lock);
572
+ cj_plat_sync_wait(h->clip_lock, &h->clip_ready);
573
+ if (out_text && h->clip_ok) {
574
+ *out_text = h->clip_text; /* 所有权交调用方 */
575
+ h->clip_text = NULL;
576
+ }
577
+ cj_plat_sync_unlock(h->clip_lock);
578
+ if (!h->clip_ok) {
579
+ return -3;
580
+ }
581
+ }
582
+ return 0;
583
+ }
584
+
585
+ int cj_bridge_clipboard_write_text(cj_host *h, const char *text) {
586
+ char *copy = NULL;
587
+ size_t len;
588
+ int rc;
589
+
590
+ if (!text) {
591
+ return -1;
592
+ }
593
+ len = strlen(text);
594
+ if (len >= (8u << 20)) {
595
+ cj_logf("[cj-bridge] clipboard write rejected: %zu bytes exceeds 8 MiB limit\n", len);
596
+ return -1;
597
+ }
598
+ copy = (char *)malloc(len + 1);
599
+ if (!copy) {
600
+ return -3;
601
+ }
602
+ memcpy(copy, text, len + 1);
603
+ rc = clipboard_execute(h, 1, copy, NULL);
604
+ if (rc == 0) {
605
+ /* 成功行在 tick 里打(字节口径以投递载荷为准);这里只补参数被拒的取证 */
606
+ }
607
+ return rc;
608
+ }
609
+
610
+ char *cj_bridge_clipboard_read_text(cj_host *h) {
611
+ char *out = NULL;
612
+ int rc;
613
+
614
+ rc = clipboard_execute(h, 0, NULL, &out);
615
+ if (rc == 0) {
616
+ return out; /* 可能为 NULL = 剪贴板为空 / 非文本 */
617
+ }
618
+ return NULL;
619
+ }
620
+
621
+ void cj_bridge_clipboard_free(char *s) {
622
+ free(s);
623
+ }
624
+
625
+ /* ===== 通知(plugin_notification):校验 + 发后即忘 ===== */
626
+
627
+ /* UI 线程投递的载荷(谁 post 谁分配,UI 线程处理完释放)*/
628
+ typedef struct note_call {
629
+ cj_host *h;
630
+ char *title;
631
+ char *body;
632
+ } note_call;
633
+
634
+ static void note_idle(void *d) {
635
+ note_call *c = (note_call *)d;
636
+ if (c) {
637
+ int ok = cj_plat_notification_notify(c->h, c->title, c->body, NULL, 0) == 0;
638
+ cj_logf("[cj-bridge] notification: title=%d bytes -> %s\n",
639
+ c->title ? (int)strlen(c->title) : 0, ok ? "sent" : "failed");
640
+ free(c->title);
641
+ free(c->body);
642
+ free(c);
643
+ }
644
+ }
645
+
646
+ int cj_bridge_notification_notify(cj_host *h, const char *title, const char *body) {
647
+ char *t_copy = NULL;
648
+ char *b_copy = NULL;
649
+ size_t t_len, b_len;
650
+
651
+ if (!h || !title || !title[0]) {
652
+ return -1;
653
+ }
654
+ t_len = strlen(title);
655
+ b_len = body ? strlen(body) : 0;
656
+ if (t_len > 256 || b_len > (8u << 10)) {
657
+ cj_logf("[cj-bridge] notification rejected: title=%zu (<=256) body=%zu (<=8192)\n",
658
+ t_len, b_len);
659
+ return -1;
660
+ }
661
+ if (h->window_gone) {
662
+ return -3;
663
+ }
664
+ /* 平台没声明通知能力(如 Windows 桩阶段)就当场拒绝,别走「发后即忘」:
665
+ 那条路径投递成功就返回 0,UI 线程实际的失败没人带回命令层——桩会被读成成功。 */
666
+ if (!(h->capabilities & CJ_CAP_NOTIFICATION)) {
667
+ cj_logf("[cj-bridge] notification rejected: platform does not declare CJ_CAP_NOTIFICATION\n");
668
+ return -4;
669
+ }
670
+ t_copy = (char *)malloc(t_len + 1);
671
+ if (t_copy) {
672
+ memcpy(t_copy, title, t_len + 1);
673
+ }
674
+ if (body && b_len) {
675
+ b_copy = (char *)malloc(b_len + 1);
676
+ if (b_copy) {
677
+ memcpy(b_copy, body, b_len + 1);
678
+ }
679
+ }
680
+ if (!t_copy || (body && b_len && !b_copy)) {
681
+ free(t_copy);
682
+ free(b_copy);
683
+ return -3;
684
+ }
685
+ if (cj_plat_is_ui_thread(h)) {
686
+ int ok = cj_plat_notification_notify(h, t_copy, b_copy, NULL, 0) == 0;
687
+ cj_logf("[cj-bridge] notification: title=%d bytes -> %s\n",
688
+ (int)t_len, ok ? "sent" : "failed");
689
+ free(t_copy);
690
+ free(b_copy);
691
+ return ok ? 0 : -3;
692
+ }
693
+ /* 发后即忘:投递后立即返回。载荷归 UI 线程释放(不等待、不占剪贴板那套同步槽)*/
694
+ {
695
+ note_call *call = (note_call *)malloc(sizeof(note_call));
696
+ if (!call) {
697
+ free(t_copy);
698
+ free(b_copy);
699
+ return -3;
700
+ }
701
+ call->h = h;
702
+ call->title = t_copy;
703
+ call->body = b_copy;
704
+ if (!cj_plat_post_ui(h, note_idle, call)) {
705
+ free(t_copy);
706
+ free(b_copy);
707
+ free(call);
708
+ return -3;
709
+ }
710
+ }
711
+ return 0;
712
+ }
713
+
714
+ /* 线路格式的共享解析(契约见 bridge_core.h 的 cj_menu_row_fn 注释)。
715
+ 就地切分一份副本:字段指针只在回调期间有效,拷贝与释放都在这里管,平台不用操心。 */
716
+ void cj_core_menu_walk(const char *wire, cj_menu_row_fn fn, void *ctx) {
717
+ char *copy;
718
+ char *line;
719
+
720
+ if (!wire || !fn) return;
721
+ copy = cj_strdup(wire);
722
+ if (!copy) return;
723
+
724
+ line = copy;
725
+ while (*line) {
726
+ char *nl = strchr(line, '\n');
727
+ char *cur;
728
+ int depth = 0;
729
+ char *t1, *t2, *t3, *t4;
730
+ char *kind_s, *id_s, *label_s, *flags_s, *accel_s;
731
+
732
+ if (nl) {
733
+ *nl = '\0';
734
+ }
735
+ while (line[depth] == '\t') {
736
+ depth++;
737
+ }
738
+ cur = line + depth;
739
+ kind_s = cur;
740
+ id_s = NULL;
741
+ label_s = NULL;
742
+ flags_s = NULL;
743
+ accel_s = NULL;
744
+ t1 = strchr(kind_s, '\t');
745
+ if (t1) {
746
+ *t1 = '\0';
747
+ id_s = t1 + 1;
748
+ t2 = strchr(id_s, '\t');
749
+ if (t2) {
750
+ *t2 = '\0';
751
+ label_s = t2 + 1;
752
+ t3 = strchr(label_s, '\t');
753
+ if (t3) {
754
+ *t3 = '\0';
755
+ flags_s = t3 + 1;
756
+ t4 = strchr(flags_s, '\t');
757
+ if (t4) {
758
+ *t4 = '\0';
759
+ accel_s = t4 + 1;
760
+ }
761
+ }
762
+ }
763
+ }
764
+ if (kind_s[0]) { /* kind 为空的行整行跳过;缺字段按空串 / 0——容错优于崩溃 */
765
+ fn(ctx, depth, kind_s[0], id_s ? id_s : "", label_s ? label_s : "",
766
+ flags_s ? atoi(flags_s) : 0, accel_s ? accel_s : "");
767
+ }
768
+
769
+ if (!nl) {
770
+ break;
771
+ }
772
+ line = nl + 1;
773
+ }
774
+ free(copy);
775
+ }
776
+
777
+ /* shell 事件编成 JSON(RFC-002 §5.5):id 做最小转义——应用给的标识里出现 \ 或 " 时
778
+ 不至于把载荷弄坏。(拖放的 paths 到 7.C 才落地,届时照此扩一份路径转义。) */
779
+ static void cj_json_escape_into(char *dst, size_t cap, const char *src) {
780
+ size_t o = 0;
781
+ const char *p;
782
+ if (!dst || cap == 0) return;
783
+ for (p = src ? src : ""; *p; p++) {
784
+ if (o + 2 >= cap) break;
785
+ if (*p == '\\' || *p == '"') {
786
+ dst[o++] = '\\';
787
+ }
788
+ dst[o++] = *p;
789
+ }
790
+ dst[o] = '\0';
791
+ }
792
+
793
+ void cj_core_menu_clicked(cj_host *h, const char *id, int enabled, int checked) {
794
+ char idbuf[256];
795
+ char payload[384];
796
+ if (!h) return;
797
+ if (!id || !id[0]) {
798
+ return; /* 没有 id 就没有「被点的项」(分隔线与子菜单不会走到这里)*/
799
+ }
800
+ cj_logf("[cj-bridge] menu clicked: id=%s enabled=%d checked=%d\n",
801
+ id, enabled ? 1 : 0, checked ? 1 : 0);
802
+ if (!h->on_shell) {
803
+ return; /* 未注册回调:上面那行日志已经落盘,够诊断了 */
804
+ }
805
+ cj_json_escape_into(idbuf, sizeof(idbuf), id);
806
+ snprintf(payload, sizeof(payload),
807
+ "{\"kind\":\"menu\",\"id\":\"%s\",\"enabled\":%s,\"checked\":%s}",
808
+ idbuf, enabled ? "true" : "false", checked ? "true" : "false");
809
+ /* 带上宿主句柄:仓颉侧按句柄分发到对应窗口,不再依赖进程级静态槽 */
810
+ h->on_shell(h, payload);
811
+ }
812
+
813
+ /* 全局快捷键触发:平台在 X11/Win32 事件回调里调,id 是应用注册时给的标识。
814
+ 载荷形状与菜单点击同族({"kind":"hotkey","id":…}),同走 on_shell 通道。 */
815
+ void cj_core_hotkey_pressed(cj_host *h, const char *id) {
816
+ char idbuf[256];
817
+ char payload[320];
818
+ if (!h) return;
819
+ if (!id || !id[0]) {
820
+ return; /* 没有 id 的触发无法路由,直接丢弃(平台侧不该产生) */
821
+ }
822
+ cj_logf("[cj-bridge] hotkey pressed: id=%s\n", id);
823
+ if (!h->on_shell) {
824
+ return; /* 未注册回调:上面那行日志已经落盘,够诊断了 */
825
+ }
826
+ cj_json_escape_into(idbuf, sizeof(idbuf), id);
827
+ snprintf(payload, sizeof(payload), "{\"kind\":\"hotkey\",\"id\":\"%s\"}", idbuf);
828
+ h->on_shell(h, payload);
829
+ }
830
+
831
+ /* 托盘菜单项被点:载荷形状与菜单/快捷键同族,同走 on_shell 通道(RFC-002 §5.5) */
832
+ void cj_core_tray_clicked(cj_host *h, const char *id) {
833
+ char idbuf[256];
834
+ char payload[320];
835
+ if (!h) return;
836
+ if (!id || !id[0]) {
837
+ return; /* 没有 id 的点击无法路由(托盘图标本体点击不带 id,平台侧自行决定语义) */
838
+ }
839
+ cj_logf("[cj-bridge] tray clicked: id=%s\n", id);
840
+ if (!h->on_shell) {
841
+ return;
842
+ }
843
+ cj_json_escape_into(idbuf, sizeof(idbuf), id);
844
+ snprintf(payload, sizeof(payload), "{\"kind\":\"tray\",\"id\":\"%s\"}", idbuf);
845
+ h->on_shell(h, payload);
846
+ }
847
+
848
+ /* ===== 拖放落地(RFC-002 §5;URI 解析在 core,桩自检可测)===== */
849
+
850
+ #define DROP_MAX_PATHS 32
851
+ #define DROP_MAX_PATH_SZ 4096
852
+
853
+ /* 把一行 URI 变成路径(就地写到 out,返回写入字节数,超上限返回 -1)。
854
+ file:// URI:剥前缀 + %XX 十六进制解码(file:// 路径里空格等必须转义成 %20);
855
+ 非 file:// URI:原样保留(v1 载荷语义由页面决定,core 不猜)。 */
856
+ static int drop_uri_to_path(const char *uri, char *out, int out_cap) {
857
+ int o = 0;
858
+ const char *p = uri;
859
+ static const char hex[] = "0123456789ABCDEF";
860
+ if (strncmp(uri, "file://", 7) == 0) {
861
+ p = uri + 7;
862
+ /* file://localhost/... 也算本地路径(RFC 1738 形态,GTK 偶发) */
863
+ if (strncmp(p, "localhost/", 10) == 0) {
864
+ p += 9; /* 保留后面的 '/' */
865
+ }
866
+ }
867
+ for (; *p; p++) {
868
+ if (o >= out_cap - 1) {
869
+ return -1; /* 超限:丢弃这条(调用方记日志) */
870
+ }
871
+ if (p[0] == '%' && p[1] && p[2]) {
872
+ const char *h1 = strchr(hex, p[1]);
873
+ const char *h2 = strchr(hex, p[2]);
874
+ if (h1 && h2) {
875
+ /* 十六进制位必须严格配对解析;大小写都收(strchr 命中即合法) */
876
+ out[o++] = (char)((h1 - hex) * 16 + (h2 - hex));
877
+ p += 2;
878
+ continue;
879
+ }
880
+ /* 伪 % 序列:原样保留,交给页面/文件系统报错,core 不替它猜 */
881
+ }
882
+ out[o++] = p[0];
883
+ }
884
+ out[o] = '\0';
885
+ return o;
886
+ }
887
+
888
+ void cj_core_drop_files(cj_host *h, const char *uri_list) {
889
+ char paths[DROP_MAX_PATHS][DROP_MAX_PATH_SZ];
890
+ const char *escaped[DROP_MAX_PATHS];
891
+ char escaped_buf[DROP_MAX_PATHS][DROP_MAX_PATH_SZ];
892
+ int count = 0, dropped = 0;
893
+ const char *line;
894
+ if (!h || !uri_list) return;
895
+
896
+ line = uri_list;
897
+ while (line && *line && count < DROP_MAX_PATHS) {
898
+ const char *eol = strchr(line, '\n');
899
+ size_t len = eol ? (size_t)(eol - line) : strlen(line);
900
+ char uri[DROP_MAX_PATH_SZ];
901
+ int n;
902
+ if (len >= sizeof(uri)) {
903
+ len = sizeof(uri) - 1; /* 截断后多半解析不出合法路径,走正常丢弃路径 */
904
+ }
905
+ memcpy(uri, line, len);
906
+ uri[len] = '\0';
907
+ line = eol ? eol + 1 : NULL;
908
+ /* 去 \r(text/uri-list 行尾常是 \r\n)与空白 */
909
+ while (len > 0 && (uri[len - 1] == '\r' || uri[len - 1] == ' ')) {
910
+ uri[--len] = '\0';
911
+ }
912
+ /* 注释行(# 开头)与空行跳过(RFC 2483 的 text/uri-list 规范) */
913
+ if (len == 0 || uri[0] == '#') {
914
+ continue;
915
+ }
916
+ n = drop_uri_to_path(uri, paths[count], DROP_MAX_PATH_SZ);
917
+ if (n < 0) {
918
+ dropped++;
919
+ cj_logf("[cj-bridge] drop: path too long, dropped: %.60s...\n", uri);
920
+ continue;
921
+ }
922
+ count++;
923
+ }
924
+ /* 行数超上限:多出的丢弃并计数(本循环因 count 满退出,剩余行都算 dropped) */
925
+ if (line && *line) {
926
+ for (; line; line = strchr(line, '\n') ? strchr(line, '\n') + 1 : NULL) {
927
+ if (*line && *line != '#') {
928
+ dropped++;
929
+ }
930
+ }
931
+ }
932
+
933
+ cj_logf("[cj-bridge] drop: paths=%d first=%.80s%s\n",
934
+ count, count > 0 ? paths[0] : "",
935
+ dropped > 0 ? " (some dropped)" : "");
936
+ if (!h->on_shell || count == 0) {
937
+ return; /* 未注册回调或空拖放:日志已落盘,够诊断 */
938
+ }
939
+ /* 拼 JSON:paths 数组每项转义(文件名可含引号/反斜杠) */
940
+ {
941
+ char payload[8192];
942
+ int o = 0, i;
943
+ o += snprintf(payload + o, sizeof(payload) - (size_t)o,
944
+ "{\"kind\":\"drop\",\"paths\":[");
945
+ for (i = 0; i < count && o < (int)sizeof(payload) - 16; i++) {
946
+ cj_json_escape_into(escaped_buf[i], DROP_MAX_PATH_SZ, paths[i]);
947
+ escaped[i] = escaped_buf[i];
948
+ o += snprintf(payload + o, sizeof(payload) - (size_t)o,
949
+ "%s\"%s\"", i > 0 ? "," : "", escaped[i]);
950
+ }
951
+ snprintf(payload + o, sizeof(payload) - (size_t)o, "]}");
952
+ h->on_shell(h, payload);
953
+ }
954
+ }
955
+
956
+ void cj_core_dialog_abort(cj_host *h) {
957
+ if (!h || !h->dlg_lock) return;
958
+ cj_plat_sync_lock(h->dlg_lock);
959
+ if (h->dlg_cur && !h->dlg_ready) {
960
+ h->dlg_cur->ok = 0;
961
+ h->dlg_ready = 1;
962
+ h->dlg_cur = NULL;
963
+ cj_plat_sync_signal(h->dlg_lock);
964
+ }
965
+ cj_plat_sync_unlock(h->dlg_lock);
966
+ }
967
+
968
+ /* ===== 回投队列:一次空闲窗口拼批回投(UI 线程调用)===== */
969
+
970
+ void cj_core_flush_js(cj_host *h) {
971
+ js_node *batch[CJ_JS_BATCH_MAX];
972
+ char *single = NULL; /* 快路径:接管字符串所有权,省掉一次拷贝 */
973
+ char *script = NULL;
974
+ size_t len = 0;
975
+ size_t cap = 0;
976
+ int n = 0;
977
+ int more = 0;
978
+ int i, j;
979
+
980
+ if (!h || !h->js_lock) return;
981
+
982
+ cj_plat_sync_lock(h->js_lock);
983
+ if (h->js_head) {
984
+ js_node *node = h->js_head;
985
+ h->js_head = node->next;
986
+ if (!h->js_head) {
987
+ h->js_tail = NULL;
988
+ single = node->js; /* 队里只有这一条 */
989
+ free(node);
990
+ } else {
991
+ batch[n++] = node;
992
+ while (h->js_head && n < CJ_JS_BATCH_MAX) {
993
+ node = h->js_head;
994
+ h->js_head = node->next;
995
+ if (!h->js_head) {
996
+ h->js_tail = NULL;
997
+ }
998
+ batch[n++] = node;
999
+ }
1000
+ }
1001
+ }
1002
+ more = (h->js_head != NULL);
1003
+ cj_plat_sync_unlock(h->js_lock);
1004
+
1005
+ if (single) {
1006
+ cj_plat_post_js(h, single);
1007
+ free(single);
1008
+ } else if (n > 0) {
1009
+ /* 拼批:一段脚本只跨一次语言边界,省掉 n-1 次固定开销。
1010
+ 为什么不用 GString 之类的缓冲:core 不依赖平台库(glib / Win32)。 */
1011
+ for (i = 0; i < n; i++) {
1012
+ size_t nl = strlen(batch[i]->js);
1013
+ if (len + nl + 2 > cap) {
1014
+ size_t ncap = (len + nl + 2) * 2;
1015
+ char *grown = (char *)realloc(script, ncap);
1016
+ if (!grown) {
1017
+ /* 内存不够:退回逐条执行——宁可多花开销,也不丢回投(回投丢了前端会一直 pending)*/
1018
+ for (j = i; j < n; j++) {
1019
+ cj_plat_post_js(h, batch[j]->js);
1020
+ }
1021
+ for (j = 0; j < n; j++) {
1022
+ free(batch[j]->js);
1023
+ free(batch[j]);
1024
+ }
1025
+ free(script);
1026
+ if (more) cj_plat_wake_ui(h);
1027
+ return;
1028
+ }
1029
+ script = grown;
1030
+ cap = ncap;
1031
+ }
1032
+ memcpy(script + len, batch[i]->js, nl);
1033
+ len += nl;
1034
+ script[len++] = '\n';
1035
+ script[len] = '\0';
1036
+ free(batch[i]->js);
1037
+ free(batch[i]);
1038
+ }
1039
+ cj_plat_post_js(h, script);
1040
+ free(script);
1041
+ }
1042
+
1043
+ /* 队列没取空:自续一次,剩下的下一批走 */
1044
+ if (more) {
1045
+ cj_plat_wake_ui(h);
1046
+ }
1047
+ }
1048
+
1049
+ /* ===== 原生对话框:单槽状态机(模态,同时只允许一个)===== */
1050
+
1051
+ static dlg_req *cj_dlg_req_new(int kind, const char *title, const char *message, const char *filter) {
1052
+ dlg_req *r = (dlg_req *)calloc(1, sizeof(dlg_req));
1053
+ if (!r) return NULL;
1054
+ r->kind = kind;
1055
+ r->title = cj_strdup(title ? title : "");
1056
+ r->message = cj_strdup(message ? message : "");
1057
+ r->filter = cj_strdup(filter ? filter : "");
1058
+ return r;
1059
+ }
1060
+
1061
+ static void cj_dlg_req_free(dlg_req *r) {
1062
+ if (!r) return;
1063
+ free(r->title);
1064
+ free(r->message);
1065
+ free(r->filter);
1066
+ free(r->path);
1067
+ free(r);
1068
+ }
1069
+
1070
+ static void cj_dlg_log_closed(const dlg_req *r) {
1071
+ if (r->kind == 0 || r->kind == 1) { /* 文件类才有路径 */
1072
+ cj_logf("[cj-bridge] dialog closed: kind=%d ok=%d path=%s\n",
1073
+ r->kind, r->ok, r->path ? r->path : "(none)");
1074
+ } else {
1075
+ cj_logf("[cj-bridge] dialog closed: kind=%d ok=%d\n", r->kind, r->ok);
1076
+ }
1077
+ }
1078
+
1079
+ void cj_bridge_set_dialog_callback(cj_host *h, cj_on_dialog_fn cb) {
1080
+ if (!h) return;
1081
+ h->on_dialog = cb;
1082
+ }
1083
+
1084
+ void cj_bridge_set_shell_callback(cj_host *h, cj_on_shell_fn cb) {
1085
+ if (!h) return;
1086
+ h->on_shell = cb;
1087
+ }
1088
+
1089
+ int cj_bridge_host_capabilities(cj_host *h) {
1090
+ return h ? h->capabilities : 0;
1091
+ }
1092
+
1093
+ int cj_bridge_host_eq(cj_host *a, cj_host *b) {
1094
+ /* 只比指针本身:句柄由 core 分配、生命周期也归 core,仓颉侧只当不透明凭据使 */
1095
+ return (a && b && a == b) ? 1 : 0;
1096
+ }
1097
+
1098
+ int cj_bridge_show_dialog(cj_host *h, int kind, const char *title,
1099
+ const char *message, const char *filter) {
1100
+ dlg_req *r;
1101
+ int ok;
1102
+
1103
+ if (!h) return 0;
1104
+ if (!h->ready) { /* 没有窗口就没有父窗口,模态对话框无处安放 */
1105
+ cj_logf("[cj-bridge] dialog ignored: host not ready\n");
1106
+ return 0;
1107
+ }
1108
+
1109
+ cj_plat_sync_lock(h->dlg_lock);
1110
+ if (h->dlg_busy) {
1111
+ cj_plat_sync_unlock(h->dlg_lock);
1112
+ cj_logf("[cj-bridge] dialog rejected: another dialog is open\n");
1113
+ return 0;
1114
+ }
1115
+ h->dlg_busy = 1;
1116
+ h->dlg_ready = 0;
1117
+ cj_plat_sync_unlock(h->dlg_lock);
1118
+
1119
+ /* 调用方已经站在 UI 线程上(WebKit 的 script-message / WebView2 的 WebMessageReceived 就跑在那里):
1120
+ 直接弹。这里**不能**走「投递 + 阻塞等待」——等待的事件得由 UI 线程处理,而 UI 线程正卡在这次调用里,
1121
+ 等于自己把自己锁死。 */
1122
+ if (cj_plat_is_ui_thread(h)) {
1123
+ cj_logf("[cj-bridge] dialog: kind=%d title=%s (caller on ui thread)\n",
1124
+ kind, title ? title : "");
1125
+ r = cj_dlg_req_new(kind, title, message, filter);
1126
+ if (!r) {
1127
+ cj_plat_sync_lock(h->dlg_lock);
1128
+ h->dlg_busy = 0;
1129
+ cj_plat_sync_unlock(h->dlg_lock);
1130
+ return 0;
1131
+ }
1132
+ r->ok = cj_plat_run_dialog(h, r);
1133
+ ok = r->ok;
1134
+ cj_dlg_log_closed(r);
1135
+ if (h->on_dialog) {
1136
+ h->on_dialog(h, r->path ? r->path : ""); /* 带上句柄:结果只落发起这次对话框的窗口 */
1137
+ }
1138
+ cj_dlg_req_free(r);
1139
+ cj_plat_sync_lock(h->dlg_lock);
1140
+ h->dlg_busy = 0;
1141
+ cj_plat_sync_unlock(h->dlg_lock);
1142
+ return ok;
1143
+ }
1144
+
1145
+ r = cj_dlg_req_new(kind, title, message, filter);
1146
+ if (!r) {
1147
+ cj_plat_sync_lock(h->dlg_lock);
1148
+ h->dlg_busy = 0;
1149
+ cj_plat_sync_unlock(h->dlg_lock);
1150
+ return 0;
1151
+ }
1152
+ cj_logf("[cj-bridge] dialog: kind=%d title=%s\n", kind, r->title);
1153
+ cj_plat_sync_lock(h->dlg_lock);
1154
+ h->dlg_cur = r;
1155
+ cj_plat_sync_unlock(h->dlg_lock);
1156
+
1157
+ cj_plat_post_dialog(h, r);
1158
+
1159
+ /* 等结果:窗口先关的话,cj_core_window_destroyed 会作废并唤醒(ok=0) */
1160
+ cj_plat_sync_lock(h->dlg_lock);
1161
+ cj_plat_sync_wait(h->dlg_lock, &h->dlg_ready);
1162
+ ok = r->ok;
1163
+ h->dlg_busy = 0;
1164
+ cj_plat_sync_unlock(h->dlg_lock);
1165
+ cj_dlg_req_free(r);
1166
+ return ok;
1167
+ }
1168
+
1169
+ void cj_core_dialog_tick(cj_host *h, dlg_req *req) {
1170
+ int alive;
1171
+ if (!h || !req) return;
1172
+
1173
+ cj_plat_sync_lock(h->dlg_lock);
1174
+ alive = (h->dlg_cur == req);
1175
+ cj_plat_sync_unlock(h->dlg_lock);
1176
+ if (!alive) {
1177
+ /* 窗口先关了:槽位已交回调用方(它会 free 这个请求)——这里连碰都不能碰 */
1178
+ return;
1179
+ }
1180
+
1181
+ req->ok = cj_plat_run_dialog(h, req);
1182
+ cj_dlg_log_closed(req);
1183
+ if (h->on_dialog) {
1184
+ h->on_dialog(h, req->path ? req->path : "");
1185
+ }
1186
+
1187
+ cj_plat_sync_lock(h->dlg_lock);
1188
+ h->dlg_cur = NULL;
1189
+ h->dlg_ready = 1;
1190
+ cj_plat_sync_signal(h->dlg_lock);
1191
+ cj_plat_sync_unlock(h->dlg_lock);
1192
+ }
1193
+
1194
+ /* =========================================================================
1195
+ * 串口(plugin_serial):路径白名单 + 参数校验 + 导出转发
1196
+ *
1197
+ * 句柄表与 fd 全在平台层(那边本来就有 pthread / CS),本文件**不持有任何串口状态**——
1198
+ * 因此也不需要「一次性初始化」这种钩子,导出函数可以在没有宿主的进程里直接用。
1199
+ * ========================================================================= */
1200
+
1201
+ #if defined(_MSC_VER)
1202
+ #define CJ_THREAD_LOCAL __declspec(thread)
1203
+ #else
1204
+ #define CJ_THREAD_LOCAL __thread
1205
+ #endif
1206
+
1207
+ /* 线程局部:命令各自跑在 worker 线程上,静态单槽会被并发调用互相覆盖(AGENTS §4 的静态单槽坑) */
1208
+ static CJ_THREAD_LOCAL char g_serial_err[256];
1209
+
1210
+ static void cj_serial_err_set(const char *msg) {
1211
+ if (!msg) {
1212
+ g_serial_err[0] = '\0';
1213
+ return;
1214
+ }
1215
+ snprintf(g_serial_err, sizeof(g_serial_err), "%s", msg);
1216
+ }
1217
+
1218
+ /* 白名单前缀表。**只放行设备节点类路径**:页面递进来的是页面可控字符串,放行面越小越好。
1219
+ 注意 "\\.\COM" 与 "COM" 两条要额外约束「后面全是数字」,否则 COMfoo 这种也会被放行。 */
1220
+ static const char *const CJ_SERIAL_PREFIXES[] = {
1221
+ "/dev/ttyUSB", "/dev/ttyACM", "/dev/ttyS", "/dev/ttyAMA", "/dev/ttyPS",
1222
+ "/dev/ttyTHS", "/dev/ttyXRUSB", "/dev/ttyV", /* Linux 串口(含虚拟 ttyV*)*/
1223
+ "/dev/tty.", "/dev/cu.", /* macOS:调制解调器 / 呼出设备 */
1224
+ "/dev/serial/by-id/", "/dev/serial/by-path/", /* udev 稳定符号链接(推荐写法)*/
1225
+ "/dev/pts/", /* 伪终端:socat / openpty 造的虚拟串口 */
1226
+ "COM", "\\\\.\\COM", /* Windows:COM3 / 长名形式 */
1227
+ };
1228
+
1229
+ #define CJ_SERIAL_MAX_PATH 255
1230
+
1231
+ int cj_serial_path_allowed(const char *path) {
1232
+ size_t i, n;
1233
+
1234
+ if (!path || !path[0]) return 0;
1235
+ n = strlen(path);
1236
+ if (n > CJ_SERIAL_MAX_PATH) return 0;
1237
+ /* 含 ".." 一律拒绝:否则 "/dev/ttyUSB0/../../etc/shadow" 这种前缀命中就能穿越出去 */
1238
+ if (strstr(path, "..")) return 0;
1239
+
1240
+ for (i = 0; i < sizeof(CJ_SERIAL_PREFIXES) / sizeof(CJ_SERIAL_PREFIXES[0]); i++) {
1241
+ const char *pfx = CJ_SERIAL_PREFIXES[i];
1242
+ size_t pl = strlen(pfx);
1243
+ const char *rest;
1244
+ size_t k;
1245
+
1246
+ if (strncmp(path, pfx, pl) != 0) continue;
1247
+ rest = path + pl;
1248
+ if (!rest[0]) return 0; /* 前缀本身不是设备名("/dev/ttyS" 这种不收)*/
1249
+
1250
+ if (strcmp(pfx, "COM") != 0 && strcmp(pfx, "\\\\.\\COM") != 0) {
1251
+ return 1;
1252
+ }
1253
+ for (k = 0; rest[k]; k++) {
1254
+ if (rest[k] < '0' || rest[k] > '9') return 0;
1255
+ if (k >= 3) return 0; /* COM1..COM999(Windows 实际到 COM256)*/
1256
+ }
1257
+ return 1;
1258
+ }
1259
+ return 0;
1260
+ }
1261
+
1262
+ static int cj_serial_cfg_ok(const cj_serial_cfg *cfg) {
1263
+ if (!cfg) return 0;
1264
+ if (cfg->baud <= 0) return 0;
1265
+ if (cfg->data_bits < 5 || cfg->data_bits > 8) return 0;
1266
+ if (cfg->parity < 0 || cfg->parity > 2) return 0;
1267
+ if (cfg->stop_bits != 1 && cfg->stop_bits != 2) return 0;
1268
+ return 1;
1269
+ }
1270
+
1271
+ long long cj_bridge_serial_open(const char *path, int baud, int data_bits, int parity, int stop_bits) {
1272
+ cj_serial_cfg cfg;
1273
+ char err[256];
1274
+ long long h;
1275
+
1276
+ g_serial_err[0] = '\0';
1277
+ if (!path || !path[0]) {
1278
+ cj_serial_err_set("serial: 设备路径为空");
1279
+ return -1;
1280
+ }
1281
+ if (!cj_serial_path_allowed(path)) {
1282
+ snprintf(g_serial_err, sizeof(g_serial_err),
1283
+ "serial: 路径不在白名单内(只放行串口设备节点 / 伪终端,且不得含 ..): %s", path);
1284
+ return -1;
1285
+ }
1286
+ cfg.baud = baud;
1287
+ cfg.data_bits = data_bits;
1288
+ cfg.parity = parity;
1289
+ cfg.stop_bits = stop_bits;
1290
+ if (!cj_serial_cfg_ok(&cfg)) {
1291
+ cj_serial_err_set("serial: 串口参数非法(baud>0 / dataBits 5..8 / parity 0|1|2 / stopBits 1|2)");
1292
+ return -1;
1293
+ }
1294
+
1295
+ err[0] = '\0';
1296
+ h = cj_plat_serial_open(path, &cfg, err, (int)sizeof(err));
1297
+ if (h <= 0) {
1298
+ cj_serial_err_set(err[0] ? err : "serial: 平台打开失败");
1299
+ /* 失败也留一行:串口问题多半要拿 stderr 对账(AGENTS §1 的取证口径)*/
1300
+ cj_logf("[cj-bridge] serial open failed: path=%s baud=%d code=%lld (%s)\n",
1301
+ path, baud, h, g_serial_err);
1302
+ return h < 0 ? h : -3;
1303
+ }
1304
+ cj_logf("[cj-bridge] serial open: handle=%lld path=%s baud=%d data=%d parity=%d stop=%d\n",
1305
+ h, path, baud, data_bits, parity, stop_bits);
1306
+ return h;
1307
+ }
1308
+
1309
+ int cj_bridge_serial_read(long long h, unsigned char *buf, int len, int timeout_ms) {
1310
+ char err[256];
1311
+ int n;
1312
+
1313
+ g_serial_err[0] = '\0';
1314
+ if (h <= 0 || !buf || len <= 0 || len > (1 << 20)) {
1315
+ cj_serial_err_set("serial: read 参数非法(handle>0 / len 1..1048576)");
1316
+ return -1;
1317
+ }
1318
+ if (timeout_ms < 0) timeout_ms = 0;
1319
+ err[0] = '\0';
1320
+ n = cj_plat_serial_read(h, buf, len, timeout_ms, err, (int)sizeof(err));
1321
+ if (n < 0) {
1322
+ cj_serial_err_set(err[0] ? err : "serial: 读失败");
1323
+ cj_logf("[cj-bridge] serial read failed: handle=%lld (%s)\n", h, g_serial_err);
1324
+ }
1325
+ return n;
1326
+ }
1327
+
1328
+ int cj_bridge_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms) {
1329
+ char err[256];
1330
+ int n;
1331
+
1332
+ g_serial_err[0] = '\0';
1333
+ if (h <= 0 || !buf || len <= 0 || len > (1 << 20)) {
1334
+ cj_serial_err_set("serial: write 参数非法(handle>0 / len 1..1048576)");
1335
+ return -1;
1336
+ }
1337
+ if (timeout_ms < 0) timeout_ms = 0;
1338
+ err[0] = '\0';
1339
+ n = cj_plat_serial_write(h, buf, len, timeout_ms, err, (int)sizeof(err));
1340
+ if (n < 0) {
1341
+ cj_serial_err_set(err[0] ? err : "serial: 写失败");
1342
+ cj_logf("[cj-bridge] serial write failed: handle=%lld (%s)\n", h, g_serial_err);
1343
+ }
1344
+ return n;
1345
+ }
1346
+
1347
+ int cj_bridge_serial_close(long long h) {
1348
+ char err[256];
1349
+ int rc;
1350
+
1351
+ g_serial_err[0] = '\0';
1352
+ if (h <= 0) {
1353
+ cj_serial_err_set("serial: close 参数非法(handle 必须 >0)");
1354
+ return -1;
1355
+ }
1356
+ err[0] = '\0';
1357
+ rc = cj_plat_serial_close(h, err, (int)sizeof(err));
1358
+ if (rc != 0) {
1359
+ cj_serial_err_set(err[0] ? err : "serial: 关句柄失败");
1360
+ return rc < 0 ? rc : -4;
1361
+ }
1362
+ cj_logf("[cj-bridge] serial close: handle=%lld\n", h);
1363
+ return 0;
1364
+ }
1365
+
1366
+ const char *cj_bridge_serial_last_error(void) {
1367
+ return g_serial_err;
1368
+ }
1369
+
1370
+ int cj_bridge_serial_list(char *out, int out_len) {
1371
+ if (!out || out_len <= 0) {
1372
+ cj_serial_err_set("serial: list 参数非法(out 缓冲为空)");
1373
+ return -1;
1374
+ }
1375
+ out[0] = '\0';
1376
+ return cj_plat_serial_list(out, out_len); /* 负码原样交回(-4 = 本平台未实现) */
1377
+ }
1378
+
1379
+ /* =========================================================================
1380
+ * 日志落盘(RFC-003 第 5 期)
1381
+ *
1382
+ * 为什么在 fd 层接管:单文件产物是 Windows GUI 子系统,双击时 stderr 无处可去,而
1383
+ * 诊断输出分布在 core、平台实现与仓颉侧上百处。把 fd 2 指向日志文件,**所有** stderr
1384
+ * 写者自动落进同一个 FILE*(顺序天然一致),两端调用点一处都不用改;stdout 不碰。
1385
+ * 为什么用 `dup2` 而不是 `freopen`:`freopen` 失败会**把 stderr 关掉**,那就连失败原因
1386
+ * 都没地方写了;`dup2` 只在 open 成功后才动 fd 2,失败路径 stderr 完好。
1387
+ * 为什么轮转不 rename 打开中的文件:Windows 上 rename 一个还开着的文件会失败。
1388
+ * 做法是「先拷贝再截断」——把当前内容拷进 `log.1`,已关闭的档案 `.1 → .2` 平移
1389
+ * (`keep` 决定份数),最后 `_chsize` / `ftruncate` 把当前文件清零。这样无平台分支、
1390
+ * 无空设备名,桩自检也能直接测。
1391
+ * ========================================================================= */
1392
+
1393
+ #define CJ_LOG_PATH_MAX 1024
1394
+ #define CJ_LOG_LINE_SLACK 256 /* 每行预留,免得刚好压线时写到 max 之上 */
1395
+
1396
+ static char g_log_path[CJ_LOG_PATH_MAX];
1397
+ static int g_log_active = 0;
1398
+ static long g_log_max = 0; /* 字节;0 = 不轮转(交给外部 logrotate) */
1399
+ static long g_log_keep = 0;
1400
+ static volatile int g_log_rotating = 0;
1401
+
1402
+ /* 轮转互斥:只用编译器内建原子,core 不引平台同步设施(命令 worker 线程也会走到这里)。
1403
+ 抢不到就当「这一行不轮转」,不阻塞调用方。 */
1404
+ static int cj_log_lock(void) {
1405
+ return __sync_lock_test_and_set(&g_log_rotating, 1) == 0;
1406
+ }
1407
+ static void cj_log_unlock(void) {
1408
+ __sync_lock_release(&g_log_rotating);
1409
+ }
1410
+
1411
+ /* 时间戳:localtime 的静态缓冲可能被别的线程踩,取回来立刻拷进本地副本。 */
1412
+ static void cj_log_prefix(void) {
1413
+ time_t now = time(NULL);
1414
+ struct tm tmv;
1415
+ const struct tm *p = localtime(&now);
1416
+ char buf[40];
1417
+ size_t n;
1418
+
1419
+ if (!p) return;
1420
+ tmv = *p;
1421
+ n = strftime(buf, sizeof(buf), "[%Y-%m-%d %H:%M:%S] ", &tmv);
1422
+ if (n > 0) fwrite(buf, 1, n, stderr);
1423
+ }
1424
+
1425
+ /* 真实大小以文件为准(不自己记数:平台与仓颉侧也在往同一个文件写)。
1426
+ *
1427
+ * 为什么不用 stat():单文件包(scripts/pack-win-single.sh)里桥由 llvm-mingw(ucrt) 编译、
1428
+ * 却由 SDK 自带 mingw 的 CRT 链接——ucrt 把 stat 映射成 stat64i32,而那边的导入库只认
1429
+ * _stat,于是链接期报 `undefined symbol: stat64i32`(2026-10-21 实测,只有静态单文件包会炸,
1430
+ * 平时的动态构建不受影响)。fopen/fseek/ftell 在 ucrt、msvcrt 与 mingw 三套 CRT 里符号名
1431
+ * 一致,绕开这层方言差异;语义相同(文件不存在 → 0)。 */
1432
+ static long cj_log_size(void) {
1433
+ FILE *f;
1434
+ long size;
1435
+
1436
+ if (!g_log_path[0]) return 0;
1437
+ fflush(stderr);
1438
+ f = fopen(g_log_path, "rb");
1439
+ if (!f) return 0;
1440
+ if (fseek(f, 0, SEEK_END) != 0) {
1441
+ fclose(f);
1442
+ return 0;
1443
+ }
1444
+ size = ftell(f);
1445
+ fclose(f);
1446
+ return size > 0 ? size : 0;
1447
+ }
1448
+
1449
+ /* 当前文件整份拷进 `<path>.<n>`(Windows 上不能 rename 打开中的文件,只能拷贝)。 */
1450
+ static void cj_log_copy_archive(long n) {
1451
+ FILE *in;
1452
+ FILE *out;
1453
+ char dst[CJ_LOG_PATH_MAX + 24];
1454
+ char buf[8192];
1455
+ size_t got;
1456
+ long written = 0;
1457
+
1458
+ snprintf(dst, sizeof(dst), "%s.%ld", g_log_path, n);
1459
+ in = fopen(g_log_path, "rb");
1460
+ if (!in) return;
1461
+ out = fopen(dst, "wb");
1462
+ if (!out) {
1463
+ fclose(in);
1464
+ return;
1465
+ }
1466
+ while ((got = fread(buf, 1, sizeof(buf), in)) > 0) {
1467
+ if (g_log_max > 0) {
1468
+ /* 档案不超过预算,免得轮转频繁时档案总占用翻倍 */
1469
+ if (written + (long)got > g_log_max) got = (size_t)(g_log_max - written);
1470
+ if ((long)got <= 0) break;
1471
+ }
1472
+ if (fwrite(buf, 1, got, out) != got) break;
1473
+ written += (long)got;
1474
+ }
1475
+ fclose(out);
1476
+ fclose(in);
1477
+ }
1478
+
1479
+ /* 已关闭的档案平移:`.keep-1 → .keep` … `.1 → .2`(最老那份先删,Windows 上 rename
1480
+ 到已存在的文件会失败)。keep ≤ 0 时不动档案,轮转只截断。 */
1481
+ static void cj_log_shift(void) {
1482
+ long i;
1483
+ char from[CJ_LOG_PATH_MAX + 24];
1484
+ char to[CJ_LOG_PATH_MAX + 24];
1485
+
1486
+ if (g_log_keep <= 0) return;
1487
+ snprintf(to, sizeof(to), "%s.%ld", g_log_path, g_log_keep);
1488
+ remove(to);
1489
+ for (i = g_log_keep - 1; i >= 1; i--) {
1490
+ snprintf(from, sizeof(from), "%s.%ld", g_log_path, i);
1491
+ snprintf(to, sizeof(to), "%s.%ld", g_log_path, i + 1);
1492
+ rename(from, to); /* 不存在就静默跳过 */
1493
+ }
1494
+ }
1495
+
1496
+ static int cj_log_rotate(void) {
1497
+ if (!g_log_active) return 0;
1498
+ if (!cj_log_lock()) return 0; /* 别人正在轮转:这一行让给下一轮 */
1499
+ if (g_log_keep > 0) {
1500
+ cj_log_shift();
1501
+ cj_log_copy_archive(1);
1502
+ }
1503
+ fflush(stderr);
1504
+ if (cj_log_truncate(cj_log_fileno(stderr)) != 0) {
1505
+ /* 截断失败(有的文件系统不给改长度):如实说一句,别假装轮转过 */
1506
+ fprintf(stderr, "[cj-bridge] log rotate failed: truncate errno=%d\n", errno);
1507
+ }
1508
+ fprintf(stderr, "[cj-bridge] log rotated: keep=%ld archive=%s.1\n", g_log_keep, g_log_path);
1509
+ cj_log_unlock();
1510
+ return 1;
1511
+ }
1512
+
1513
+ void cj_core_logf(const char *fmt, ...) {
1514
+ va_list ap;
1515
+
1516
+ if (g_log_active) {
1517
+ if (g_log_max > 0 && cj_log_size() + CJ_LOG_LINE_SLACK >= g_log_max) cj_log_rotate();
1518
+ cj_log_prefix();
1519
+ }
1520
+ va_start(ap, fmt);
1521
+ vfprintf(stderr, fmt, ap);
1522
+ va_end(ap);
1523
+ if (g_log_active) fflush(stderr); /* 每行即时落盘:进程被强杀也不丢 */
1524
+ }
1525
+
1526
+ /* 父目录不在就逐个建出来:默认目录(`~/.local/state/cj-tauri/` 这类)常常还不存在。
1527
+ 建目录失败不在这里报——紧接着的 open 会带 errno 如实说,少一条重复的日志。 */
1528
+ static void cj_log_ensure_dirs(const char *path) {
1529
+ char buf[CJ_LOG_PATH_MAX];
1530
+ size_t i;
1531
+ size_t n = strlen(path);
1532
+
1533
+ if (n == 0 || n >= sizeof(buf)) return;
1534
+ memcpy(buf, path, n + 1);
1535
+ for (i = 1; i < n; i++) {
1536
+ char sep;
1537
+ if (buf[i] != '/' && buf[i] != '\\') continue;
1538
+ sep = buf[i];
1539
+ buf[i] = '\0';
1540
+ /* 跳过盘符(`C:`)与别名的根:它们建不了也不需要建 */
1541
+ if (!(i == 2 && buf[1] == ':')) cj_log_mkdir(buf);
1542
+ buf[i] = sep;
1543
+ }
1544
+ }
1545
+
1546
+ CJ_BRIDGE_API int cj_bridge_set_log_file(const char *path, int max_kb, int keep) {
1547
+ int fd;
1548
+
1549
+ if (g_log_active) {
1550
+ fprintf(stderr, "[cj-bridge] log file already active: %s (restart to change)\n", g_log_path);
1551
+ return 0;
1552
+ }
1553
+ if (!path || !path[0]) {
1554
+ fprintf(stderr, "[cj-bridge] log file refused: empty path\n");
1555
+ return 0;
1556
+ }
1557
+ if (strlen(path) >= CJ_LOG_PATH_MAX) {
1558
+ fprintf(stderr, "[cj-bridge] log file refused: path too long\n");
1559
+ return 0;
1560
+ }
1561
+ /* 先 open 成功再动 fd 2:失败时 stderr 完好,失败原因才有人看得见 */
1562
+ fd = cj_log_open(path, CJ_LOG_OFLAGS, CJ_LOG_OMODE);
1563
+ if (fd < 0 && errno == ENOENT) {
1564
+ /* 只有「父目录根本不存在」才顺手建出来(默认目录 `~/.local/state/cj-tauri` 常常还不存在)。
1565
+ 别的 errno(没权限、路径中间穿了普通文件、名字太长)一律原样失败——
1566
+ 照着一个开不了的路径去建目录,等于把一次手滑变成磁盘上的垃圾。 */
1567
+ cj_log_ensure_dirs(path);
1568
+ fd = cj_log_open(path, CJ_LOG_OFLAGS, CJ_LOG_OMODE);
1569
+ }
1570
+ if (fd < 0) {
1571
+ fprintf(stderr, "[cj-bridge] log file open failed: %s (errno=%d)\n", path, errno);
1572
+ return 0;
1573
+ }
1574
+ if (fd != 2) {
1575
+ if (cj_log_dup2(fd, 2) < 0) {
1576
+ fprintf(stderr, "[cj-bridge] log file open failed: %s (dup2 errno=%d)\n", path, errno);
1577
+ cj_log_close(fd);
1578
+ return 0;
1579
+ }
1580
+ cj_log_close(fd);
1581
+ }
1582
+ snprintf(g_log_path, sizeof(g_log_path), "%s", path);
1583
+ g_log_max = max_kb > 0 ? (long)max_kb * 1024L : 0;
1584
+ g_log_keep = keep > 0 ? (long)keep : 0;
1585
+ g_log_active = 1;
1586
+ /* 第一行就落在新文件里:开了、开在哪、预算多少,都能自查 */
1587
+ cj_logf("[cj-bridge] log file: %s (max=%ldKB keep=%ld)\n", g_log_path, g_log_max / 1024, g_log_keep);
1588
+ return 1;
1589
+ }
1590
+
1591
+ CJ_BRIDGE_API const char *cj_bridge_log_file(void) {
1592
+ return g_log_active ? g_log_path : "";
1593
+ }
1594
+
1595
+ CJ_BRIDGE_API int cj_bridge_log_write(const char *line) {
1596
+ if (!line) return -1;
1597
+ cj_logf("%s\n", line);
1598
+ return 0;
1599
+ }
1600
+
1601
+ /* =========================================================================
1602
+ * 静默子进程(为什么下沉到 C 桥见头文件同名段落)
1603
+ *
1604
+ * core 只做参数校验、错误出口与日志;创建 / 终止 / 探活是平台原语。这样两平台导出名与
1605
+ * 签名逐字一致,仓颉侧没有平台分叉的调用点(AGENTS §2 的分工铁律)。
1606
+ * ========================================================================= */
1607
+
1608
+ CJ_BRIDGE_API long long cj_bridge_spawn_hidden(const char *exe, const char *args) {
1609
+ char err[512];
1610
+ long long pid;
1611
+
1612
+ err[0] = '\0';
1613
+ if (!exe || !*exe) {
1614
+ cj_logf("[cj-bridge] spawn hidden: 程序路径为空\n");
1615
+ return 0;
1616
+ }
1617
+ pid = cj_plat_spawn_hidden(exe, args ? args : "", err, (int)sizeof(err));
1618
+ if (pid <= 0) {
1619
+ cj_logf("[cj-bridge] spawn hidden 失败(%s):%s\n", exe,
1620
+ err[0] ? err : "平台没有给出原因");
1621
+ return 0;
1622
+ }
1623
+ cj_logf("[cj-bridge] spawn hidden: %s -> pid=%lld\n", exe, pid);
1624
+ return pid;
1625
+ }
1626
+
1627
+ CJ_BRIDGE_API int cj_bridge_kill_pid(long long pid) {
1628
+ if (pid <= 0) {
1629
+ cj_logf("[cj-bridge] kill pid: 非法 pid=%lld\n", pid);
1630
+ return 0;
1631
+ }
1632
+ return cj_plat_kill_pid(pid);
1633
+ }
1634
+
1635
+ CJ_BRIDGE_API int cj_bridge_pid_alive(long long pid) {
1636
+ if (pid <= 0) return 0;
1637
+ return cj_plat_pid_alive(pid);
1638
+ }
1639
+
1640
+ /* 内嵌资源释放:core 只做参数校验与错误出口,取资源写盘是平台原语
1641
+ (为什么用字符串资源名、为什么幂等,见头文件同名段落与平台实现)。 */
1642
+ CJ_BRIDGE_API int cj_bridge_extract_assets(const char *dir) {
1643
+ char err[512];
1644
+ int n;
1645
+
1646
+ err[0] = '\0';
1647
+ if (!dir || !*dir) {
1648
+ cj_logf("[cj-bridge] extract assets: 目录为空\n");
1649
+ return -1;
1650
+ }
1651
+ n = cj_plat_extract_assets(dir, err, (int)sizeof(err));
1652
+ if (n < 0) {
1653
+ cj_logf("[cj-bridge] extract assets 失败(%s):%s\n", dir, err[0] ? err : "平台没有给出原因");
1654
+ return -1;
1655
+ }
1656
+ if (n > 0) cj_logf("[cj-bridge] extract assets: %d 个文件就绪(%s)\n", n, dir);
1657
+ return n;
1658
+ }
1659
+
1660
+ /* 预加载动态库:core 只做参数校验与错误出口,LoadLibrary/dlopen 是平台原语。
1661
+ 为什么要它见头文件同名段落(2026-10-21:单文件包在别人的机器上连明文 ws:// 都挂在
1662
+ `Can not load openssl library or function RAND_bytes.`——运行时的随机数默认走 OpenSSL,
1663
+ 而它按裸名找 DLL,只有开发机的 PATH 恰好有 Git 自带那份)。 */
1664
+ CJ_BRIDGE_API int cj_bridge_preload_library(const char *path) {
1665
+ char err[512];
1666
+ int ok;
1667
+
1668
+ err[0] = '\0';
1669
+ if (!path || !*path) {
1670
+ cj_logf("[cj-bridge] preload library: 路径为空\n");
1671
+ return 0;
1672
+ }
1673
+ ok = cj_plat_preload_library(path, err, (int)sizeof(err));
1674
+ if (ok <= 0) {
1675
+ cj_logf("[cj-bridge] preload 失败(%s):%s\n", path, err[0] ? err : "平台没有给出原因");
1676
+ return 0;
1677
+ }
1678
+ cj_logf("[cj-bridge] preload: %s\n", path);
1679
+ return 1;
1680
+ }
1681
+