cj-tauri 0.8.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/cjpm.toml +52 -25
- package/cli/cjpm.toml +1 -1
- package/cli/src/doctor.cj +304 -0
- package/cli/src/main.cj +33 -7
- package/cli/src/project.cj +59 -1
- package/cli/src/resolve.cj +348 -304
- package/cli/src/scaffold.cj +139 -22
- package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/README.md +79 -0
- package/cli/templates/ohos-pc/build-hap.sh +190 -0
- package/cli/templates/ohos-pc/build-profile.json5 +46 -0
- package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
- package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
- package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
- package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
- package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
- package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/oh-package.json5 +10 -0
- package/native/build_linux.sh +30 -2
- package/native/build_win.bat +62 -61
- package/native/core/bridge_core.c +1626 -0
- package/native/{bridge_core.h → core/bridge_core.h} +246 -6
- package/native/platforms/harmonyos/README.md +106 -0
- package/native/platforms/linux/bridge_linux.c +2485 -0
- package/native/platforms/windows/bridge_win.c +3248 -0
- package/native/tests/test_bridge_core.c +528 -1
- package/package.json +12 -3
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/api_system.cj +10 -2
- package/src/app.cj +248 -85
- package/src/arkweb_proxy.cj +56 -0
- package/src/arkweb_script.cj +154 -0
- package/src/capability.cj +140 -0
- package/src/clipboard_bridge.cj +55 -0
- package/src/dragdrop.cj +26 -0
- package/src/embedded_assets.cj +98 -0
- package/src/hexutil.cj +53 -0
- package/src/host.cj +197 -26
- package/src/host_arkweb.cj +740 -0
- package/src/host_factory.cj +48 -0
- package/src/host_webkit.cj +202 -26
- package/src/host_webview2.cj +176 -3
- package/src/hotkey.cj +66 -0
- package/src/ipc_hub.cj +49 -0
- package/src/ipc_message.cj +2 -1
- package/src/jsutil.cj +33 -0
- package/src/log_bridge.cj +107 -0
- package/src/logging.cj +127 -0
- package/src/menu.cj +197 -196
- package/src/plugin.cj +40 -0
- package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
- package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
- package/src/plugins/fs/plugin_fs.cj +194 -0
- package/src/plugins/http/plugin_http.cj +459 -0
- package/src/plugins/log/plugin_log.cj +105 -0
- package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
- package/src/plugins/mqtt/mqtt_codec.cj +661 -0
- package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
- package/src/plugins/notification/plugin_notification.cj +146 -0
- package/src/plugins/plugins.cj +5 -0
- package/src/plugins/redis/plugin_redis.cj +2409 -0
- package/src/plugins/redis/redis_codec.cj +556 -0
- package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
- package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
- package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
- package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
- package/src/plugins/store/plugin_store.cj +287 -0
- package/src/plugins/ws/plugin_ws.cj +470 -0
- package/src/process_hidden.cj +174 -0
- package/src/scope.cj +391 -0
- package/src/shell_event.cj +69 -68
- package/src/tests/capability_test.cj +53 -0
- package/src/tests/fake_host.cj +85 -20
- package/src/tests/ipc_hub_test.cj +13 -0
- package/src/tests/jsutil_test.cj +62 -0
- package/src/tests/logging_test.cj +62 -0
- package/src/tests/mqtt_codec_test.cj +405 -0
- package/src/tests/plugin_clipboard_test.cj +222 -0
- package/src/tests/plugin_dialog_test.cj +98 -4
- package/src/tests/plugin_http_test.cj +124 -0
- package/src/tests/plugin_menu_test.cj +1 -0
- package/src/tests/plugin_notification_test.cj +205 -0
- package/src/tests/plugin_serial_test.cj +269 -267
- package/src/tests/plugin_shell_test.cj +189 -188
- package/src/tests/plugin_sqlite_test.cj +230 -0
- package/src/tests/plugin_store_test.cj +260 -0
- package/src/tests/plugin_test.cj +542 -524
- package/src/tests/plugin_ws_test.cj +40 -0
- package/src/tests/process_hidden_test.cj +129 -0
- package/src/tests/redis_codec_test.cj +396 -0
- package/src/tests/scope_test.cj +350 -0
- package/src/tests/shell_event_test.cj +0 -1
- package/src/tests/system_host_test.cj +81 -79
- package/src/tests/urlutil_test.cj +53 -0
- package/src/tests/version_test.cj +75 -65
- package/src/tray.cj +90 -0
- package/src/urlutil.cj +159 -0
- package/src/version.cj +23 -2
- package/src/window.cj +51 -42
- package/native/bridge_core.c +0 -797
- package/native/bridge_linux.c +0 -1268
- package/native/bridge_win.c +0 -1633
- package/src/plugin_fs.cj +0 -118
- /package/native/{bridge_js.h → core/bridge_js.h} +0 -0
package/native/bridge_linux.c
DELETED
|
@@ -1,1268 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* cj-tauri C 桥(Linux 平台层):GTK / WebKitGTK 宿主原语
|
|
3
|
-
*
|
|
4
|
-
* 本文件只实现 bridge_core.h 的「平台原语清单」+ 宿主线程装配;平台无关逻辑
|
|
5
|
-
* (IPC 回投队列与批处理、对话框单槽状态机、窗口配置存储、生命周期标志、全部导出)
|
|
6
|
-
* 都在 native/bridge_core.c,两平台共用。分层原因见 docs/架构演进-多平台与多窗口.md §2.2。
|
|
7
|
-
*
|
|
8
|
-
* 背景(关键):仓颉 cjnative 的 main 运行在 M:N 轻量级线程的堆上协程栈,
|
|
9
|
-
* JSC 的 sanitizeStackForVM 用 pthread 栈边界校验 SP 会失败并 abort。
|
|
10
|
-
* 因此 GTK/WebKit 全部调用必须留在本文件创建的原生 pthread(标准 8MB 栈)内,
|
|
11
|
-
* 仓颉侧经 FFI 与本桥交互,消息回调经 CFunc 回到仓颉。
|
|
12
|
-
*
|
|
13
|
-
* 编译(见 native/build_linux.sh):
|
|
14
|
-
* gcc -shared -fPIC -fstack-protector-all bridge_core.c bridge_linux.c \
|
|
15
|
-
* -o libcjtbridge.so $(pkg-config --cflags --libs webkit2gtk-4.1)
|
|
16
|
-
*/
|
|
17
|
-
#include <gtk/gtk.h>
|
|
18
|
-
#include <webkit2/webkit2.h>
|
|
19
|
-
#include <pthread.h>
|
|
20
|
-
#include <stdio.h>
|
|
21
|
-
#include <stdlib.h>
|
|
22
|
-
#include <string.h>
|
|
23
|
-
#include <unistd.h>
|
|
24
|
-
/* 串口(plugin_serial):termios 配置 + 超时读写 */
|
|
25
|
-
#include <errno.h>
|
|
26
|
-
#include <fcntl.h>
|
|
27
|
-
#include <poll.h>
|
|
28
|
-
#include <stdarg.h>
|
|
29
|
-
#include <sys/types.h>
|
|
30
|
-
#include <termios.h>
|
|
31
|
-
#include <time.h>
|
|
32
|
-
#include "bridge_core.h"
|
|
33
|
-
|
|
34
|
-
/* ===== 菜单栏(RFC-002 §5.4 / §5.5)=====
|
|
35
|
-
core 把线路文本交下来,本文件翻译成 GTK 菜单栏。菜单栏是**每窗口**资源(GtkMenuBar 挂在
|
|
36
|
-
窗口里那层 GtkBox 上),建 / 换 / 改都必须在 GTK 线程上做——跨线程一律 g_idle_add 投过去。 */
|
|
37
|
-
#define CJ_MENU_MAX_ITEMS 64
|
|
38
|
-
#define CJ_MENU_MAX_DEPTH 8
|
|
39
|
-
|
|
40
|
-
typedef struct menu_item_ctx {
|
|
41
|
-
cj_host *h;
|
|
42
|
-
char *app_id; /* 应用给的稳定 id:菜单项被激活时回投仓颉侧 */
|
|
43
|
-
char kind; /* 'n' 普通 / 'c' 勾选(勾选项靠 toggled 信号回投)*/
|
|
44
|
-
int enabled;
|
|
45
|
-
int checked;
|
|
46
|
-
int suppress; /* 程序化改勾选时置 1:把随之而来的 toggled 当噪声挡掉 */
|
|
47
|
-
} menu_item_ctx;
|
|
48
|
-
|
|
49
|
-
typedef struct menu_slot {
|
|
50
|
-
char *app_id; /* 与 menu_item_ctx 各存一份:槽会被重建复用,ctx 随 widget 存亡 */
|
|
51
|
-
GtkWidget *widget; /* 对应的 GtkMenuItem / GtkCheckMenuItem */
|
|
52
|
-
menu_item_ctx *ctx; /* 该 widget 的信号回调上下文(改状态要动它的 suppress)*/
|
|
53
|
-
char kind;
|
|
54
|
-
int enabled;
|
|
55
|
-
int checked;
|
|
56
|
-
} menu_slot;
|
|
57
|
-
|
|
58
|
-
/* ===== 每个宿主的平台私有状态 =====
|
|
59
|
-
原先是一堆进程级单例(g_window / g_view / g_gtk_thread …):多窗口一落地就互相覆盖,
|
|
60
|
-
而且「哪个窗口」这件事在 ABI 上根本传不进来。现在挂在自己的 cj_host->plat 上,
|
|
61
|
-
平台函数一律从入参句柄取状态——平台原语只认句柄,不看全局。 */
|
|
62
|
-
typedef struct plat_host {
|
|
63
|
-
GtkWidget *window; /* 主窗口;窗口销毁时置空(含用户关窗路径)*/
|
|
64
|
-
GtkWidget *view; /* WebKitWebView;宿主线程收尾时 unref 并置空 */
|
|
65
|
-
/* --- 菜单栏(只在 GTK 线程上建 / 换 / 改)--- */
|
|
66
|
-
GtkWidget *box; /* window 里那层竖向 GtkBox:装菜单栏(若有)+ view */
|
|
67
|
-
GtkWidget *menubar; /* 当前菜单栏;NULL = 没有(空菜单栏不留空条)*/
|
|
68
|
-
menu_slot menu_items[CJ_MENU_MAX_ITEMS];
|
|
69
|
-
int menu_item_count;
|
|
70
|
-
pthread_t thread; /* 跑 gtk_main 的原生线程(对话框判断「调用方是否已在 GTK 线程」用)*/
|
|
71
|
-
int thread_started;
|
|
72
|
-
int thread_set;
|
|
73
|
-
volatile int thread_done; /* 宿主线程跑完(收尾最后一步):只有它为 1 才敢 free 这个结构,
|
|
74
|
-
见 cj_plat_fini 里「为什么不 join」的注释 */
|
|
75
|
-
} plat_host;
|
|
76
|
-
|
|
77
|
-
/* ===== 同步原语(core 只用这些,绝不直接碰 pthread)===== */
|
|
78
|
-
|
|
79
|
-
struct cj_sync {
|
|
80
|
-
pthread_mutex_t mutex;
|
|
81
|
-
pthread_cond_t cond;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
cj_sync *cj_plat_sync_create(void) {
|
|
85
|
-
cj_sync *s = (cj_sync *)calloc(1, sizeof(cj_sync));
|
|
86
|
-
if (!s) return NULL;
|
|
87
|
-
if (pthread_mutex_init(&s->mutex, NULL) != 0) {
|
|
88
|
-
free(s);
|
|
89
|
-
return NULL;
|
|
90
|
-
}
|
|
91
|
-
if (pthread_cond_init(&s->cond, NULL) != 0) {
|
|
92
|
-
pthread_mutex_destroy(&s->mutex);
|
|
93
|
-
free(s);
|
|
94
|
-
return NULL;
|
|
95
|
-
}
|
|
96
|
-
return s;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
void cj_plat_sync_destroy(cj_sync *s) {
|
|
100
|
-
if (!s) return;
|
|
101
|
-
pthread_cond_destroy(&s->cond);
|
|
102
|
-
pthread_mutex_destroy(&s->mutex);
|
|
103
|
-
free(s);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
void cj_plat_sync_lock(cj_sync *s) {
|
|
107
|
-
if (s) pthread_mutex_lock(&s->mutex);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
void cj_plat_sync_unlock(cj_sync *s) {
|
|
111
|
-
if (s) pthread_mutex_unlock(&s->mutex);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
void cj_plat_sync_wait(cj_sync *s, int *ready) {
|
|
115
|
-
if (!s || !ready) return;
|
|
116
|
-
while (!*ready) {
|
|
117
|
-
pthread_cond_wait(&s->cond, &s->mutex);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
void cj_plat_sync_signal(cj_sync *s) {
|
|
122
|
-
if (s) pthread_cond_signal(&s->cond);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/* ===== 投递到 GTK 线程的小工具 ===== */
|
|
126
|
-
|
|
127
|
-
/* 带一个字符串参数的投递(URL 需要,且参数所有权要跟着走) */
|
|
128
|
-
typedef struct ui_call {
|
|
129
|
-
cj_host *h;
|
|
130
|
-
char *arg;
|
|
131
|
-
} ui_call;
|
|
132
|
-
|
|
133
|
-
static gboolean open_devtools_idle(gpointer d) {
|
|
134
|
-
cj_host *h = (cj_host *)d;
|
|
135
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
136
|
-
if (p && p->view) {
|
|
137
|
-
WebKitWebInspector *insp = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(p->view));
|
|
138
|
-
if (insp) {
|
|
139
|
-
webkit_web_inspector_show(insp);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return G_SOURCE_REMOVE;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
static gboolean load_url_idle(gpointer d) {
|
|
146
|
-
ui_call *c = (ui_call *)d;
|
|
147
|
-
plat_host *p = (c && c->h) ? (plat_host *)c->h->plat : NULL;
|
|
148
|
-
if (p && p->view && c->arg) {
|
|
149
|
-
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(p->view), c->arg);
|
|
150
|
-
}
|
|
151
|
-
if (c) {
|
|
152
|
-
free(c->arg);
|
|
153
|
-
free(c);
|
|
154
|
-
}
|
|
155
|
-
return G_SOURCE_REMOVE;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
static gboolean reload_idle(gpointer d) {
|
|
159
|
-
cj_host *h = (cj_host *)d;
|
|
160
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
161
|
-
if (p && p->view) {
|
|
162
|
-
webkit_web_view_reload(WEBKIT_WEB_VIEW(p->view));
|
|
163
|
-
}
|
|
164
|
-
return G_SOURCE_REMOVE;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/* 退出:只收主循环,窗口交给 gtk_thread_main 的收尾去销毁(见那段收尾 + on_destroy 信号)。
|
|
168
|
-
不能在空闲回调里 gtk_widget_destroy:实测「主循环内销毁 WebKit 窗口」会让 libwebkit2gtk 的
|
|
169
|
-
退出期析构函数在 g_object_unref 里 abort——栈是 StartMainTask → exit → __run_exit_handlers
|
|
170
|
-
→ libwebkit2gtk * → g_object_unref → abort,退出码 134(6 轮里 5 轮),而主循环退出后再销毁
|
|
171
|
-
同一份活儿照样干完,6 轮里干净退出(AB 对照)。onDestroy 仍由 destroy 信号触发一次,
|
|
172
|
-
收尾里那次 cj_core_window_destroyed 只是幂等兜底;与 Windows 的 DestroyWindow → WM_DESTROY
|
|
173
|
-
语义仍对齐:quit 一定触发一次 onDestroy。 */
|
|
174
|
-
static gboolean quit_idle(gpointer d) {
|
|
175
|
-
(void)d; /* 窗口销毁已不在这里做,入参用不上了 */
|
|
176
|
-
gtk_main_quit();
|
|
177
|
-
return G_SOURCE_REMOVE;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/* 回投:UI 线程取队列拼批执行 */
|
|
181
|
-
static gboolean flush_idle(gpointer d) {
|
|
182
|
-
cj_core_flush_js((cj_host *)d);
|
|
183
|
-
return G_SOURCE_REMOVE;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/* ===== 宿主生命周期 ===== */
|
|
187
|
-
|
|
188
|
-
/* 菜单:实现集中在文件末尾;装配路径与 g_idle 回调都要用,先声明 */
|
|
189
|
-
static void menu_apply_wire(cj_host *h, plat_host *p, const char *wire);
|
|
190
|
-
static void menu_apply_item_state(plat_host *p, const char *id, int enabled, int checked);
|
|
191
|
-
static void menu_clear_slots(plat_host *p);
|
|
192
|
-
/* 菜单几何的「事后读回」(低优先级 idle 里读 GTK 的分配值):建完菜单、以及每次用户点击之后
|
|
193
|
-
各投一次,实机驱动脚本按它把鼠标点到项的中心——见函数定义处的注释 */
|
|
194
|
-
static void menu_post_layout(cj_host *h);
|
|
195
|
-
|
|
196
|
-
void cj_plat_init(cj_host *h) {
|
|
197
|
-
plat_host *p;
|
|
198
|
-
if (!h) return;
|
|
199
|
-
p = (plat_host *)calloc(1, sizeof(plat_host));
|
|
200
|
-
if (!p) {
|
|
201
|
-
fprintf(stderr, "[cj-bridge] init failed: out of memory\n");
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
h->plat = p;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
int cj_plat_fini(cj_host *h) {
|
|
208
|
-
plat_host *p;
|
|
209
|
-
if (!h) return 1;
|
|
210
|
-
p = (plat_host *)h->plat;
|
|
211
|
-
if (!p) return 1;
|
|
212
|
-
/* 这里**不能 join**:宿主线程收尾时要进入仓颉运行时(窗口销毁回调 on_destroy),
|
|
213
|
-
而在仓颉线程里阻塞 join 会与运行时的「停处理器」握手互等——实测约 1/6 概率整进程挂死,
|
|
214
|
-
栈是 cj_plat_fini → pthread_join ↔ CJ_CJThreadMexit → CJ_ProcessorStopWithLastCheck。
|
|
215
|
-
改成 detach + 让宿主线程自己立 thread_done 标记:跑完了才回收状态,没跑完就留着
|
|
216
|
-
(进程随即退出;泄漏一次状态远比死锁安全,也与批次 1 之前的 detach 行为一致)。 */
|
|
217
|
-
if (p->thread_started) {
|
|
218
|
-
pthread_detach(p->thread);
|
|
219
|
-
p->thread_started = 0;
|
|
220
|
-
}
|
|
221
|
-
/* 收尾顺序同样要命:窗口销毁是 GTK/WebKit 一整串多线程拆解,若它与仓颉运行时的收尾
|
|
222
|
-
并发(run() 返回 → 运行时关停),实测几乎必然以 SIGABRT 收尾(`schd-worker` unhandled
|
|
223
|
-
SIGABRT from runtime frame)。所以给宿主线程一个有界的等待窗口——纯 usleep 轮询标记,
|
|
224
|
-
不碰 join(join 会与运行时握手互等,见上);等不到也不阻塞进程退出,照旧留着状态。 */
|
|
225
|
-
if (!p->thread_done) {
|
|
226
|
-
int spin = 0;
|
|
227
|
-
while (!p->thread_done && spin < 2000) { /* 最多约 2s */
|
|
228
|
-
usleep(1000);
|
|
229
|
-
spin++;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (!p->thread_done) {
|
|
233
|
-
fprintf(stderr, "[cj-bridge] fini: host thread still running, platform state kept\n");
|
|
234
|
-
return 0;
|
|
235
|
-
}
|
|
236
|
-
h->plat = NULL;
|
|
237
|
-
free(p);
|
|
238
|
-
return 1;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
int cj_plat_is_ui_thread(cj_host *h) {
|
|
242
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
243
|
-
if (!p || !p->thread_set) return 0;
|
|
244
|
-
return pthread_equal(pthread_self(), p->thread) ? 1 : 0;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
void cj_plat_wake_ui(cj_host *h) {
|
|
248
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
249
|
-
/* 窗口还没建好:消息留在 core 的队列里,等宿主就绪后的第一次投递带走 */
|
|
250
|
-
if (!p || !p->view) return;
|
|
251
|
-
g_idle_add(flush_idle, h);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/* ===== 平台原语:投递 / 运行时操作 ===== */
|
|
255
|
-
|
|
256
|
-
/* 回投结果校验:这条 eval 是「仓颉 → 前端」的唯一出口,静默失败会让前端 promise 永久 pending。
|
|
257
|
-
Windows 侧有 log_hr("ExecuteScript", …) 兜同一件事(两平台对称)。 */
|
|
258
|
-
static void on_js_done(GObject *src, GAsyncResult *res, gpointer ud) {
|
|
259
|
-
GError *err = NULL;
|
|
260
|
-
WebKitJavascriptResult *r =
|
|
261
|
-
webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(src), res, &err);
|
|
262
|
-
if (r) {
|
|
263
|
-
g_object_unref(r); /* 每次回投都会新建一个结果对象,不看也要释放 */
|
|
264
|
-
}
|
|
265
|
-
if (err) {
|
|
266
|
-
fprintf(stderr, "[cj-bridge] run js failed: %s\n", err->message);
|
|
267
|
-
g_error_free(err);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
void cj_plat_post_js(cj_host *h, const char *script_utf8) {
|
|
272
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
273
|
-
if (!p || !p->view || !script_utf8) return;
|
|
274
|
-
webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(p->view), script_utf8, NULL, on_js_done, NULL);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
void cj_plat_open_devtools(cj_host *h) {
|
|
278
|
-
if (!h) return;
|
|
279
|
-
g_idle_add(open_devtools_idle, h);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
void cj_plat_load_url(cj_host *h, const char *url) {
|
|
283
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
284
|
-
if (!h || !url || !url[0]) return;
|
|
285
|
-
fprintf(stderr, "[cj-bridge] load url: %s\n", url);
|
|
286
|
-
/* 已启动:投递到 GTK 线程切页面;未启动:pending_url 由宿主线程建窗后加载 */
|
|
287
|
-
if (p && p->view) {
|
|
288
|
-
ui_call *c = (ui_call *)calloc(1, sizeof(ui_call));
|
|
289
|
-
if (!c) return;
|
|
290
|
-
c->h = h;
|
|
291
|
-
c->arg = strdup(url);
|
|
292
|
-
if (!c->arg) {
|
|
293
|
-
free(c);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
g_idle_add(load_url_idle, c);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
void cj_plat_reload(cj_host *h) {
|
|
301
|
-
if (!h) return;
|
|
302
|
-
g_idle_add(reload_idle, h);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
void cj_plat_quit(cj_host *h) {
|
|
306
|
-
if (!h) return;
|
|
307
|
-
g_idle_add(quit_idle, h);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/* ===== 原生对话框:这里只管「弹」,状态机(单槽/互斥/等待/作废)在 core ===== */
|
|
311
|
-
|
|
312
|
-
/* "描述|模式|描述|模式" → GTK 过滤器(与 Win32 OPENFILENAME 用同一套写法) */
|
|
313
|
-
static void apply_filter(GtkFileChooser *chooser, const char *filter) {
|
|
314
|
-
char *buf, *seg;
|
|
315
|
-
if (!filter || !filter[0]) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
buf = strdup(filter);
|
|
319
|
-
if (!buf) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
seg = buf;
|
|
323
|
-
while (seg && seg[0]) {
|
|
324
|
-
char *pat = strchr(seg, '|');
|
|
325
|
-
char *next = NULL;
|
|
326
|
-
if (!pat) {
|
|
327
|
-
break; /* 落单的描述段:忽略 */
|
|
328
|
-
}
|
|
329
|
-
*pat = 0;
|
|
330
|
-
pat += 1;
|
|
331
|
-
next = strchr(pat, '|');
|
|
332
|
-
if (next) {
|
|
333
|
-
*next = 0;
|
|
334
|
-
}
|
|
335
|
-
{
|
|
336
|
-
GtkFileFilter *f = gtk_file_filter_new();
|
|
337
|
-
char *p = pat;
|
|
338
|
-
/* 一段里可以有多个空格分隔的 glob(如 "*.txt *.md") */
|
|
339
|
-
while (p && p[0]) {
|
|
340
|
-
char *sp = strchr(p, ' ');
|
|
341
|
-
if (sp) {
|
|
342
|
-
*sp = 0;
|
|
343
|
-
}
|
|
344
|
-
if (p[0]) {
|
|
345
|
-
gtk_file_filter_add_pattern(f, p);
|
|
346
|
-
}
|
|
347
|
-
p = sp ? sp + 1 : NULL;
|
|
348
|
-
}
|
|
349
|
-
gtk_file_filter_set_name(f, seg);
|
|
350
|
-
gtk_file_chooser_add_filter(chooser, f);
|
|
351
|
-
}
|
|
352
|
-
seg = next ? next + 1 : NULL;
|
|
353
|
-
}
|
|
354
|
-
{
|
|
355
|
-
GtkFileFilter *all = gtk_file_filter_new();
|
|
356
|
-
gtk_file_filter_set_name(all, "所有文件");
|
|
357
|
-
gtk_file_filter_add_pattern(all, "*");
|
|
358
|
-
gtk_file_chooser_add_filter(chooser, all);
|
|
359
|
-
}
|
|
360
|
-
free(buf);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/* 已经站在 GTK 线程上:直接弹一次,返回 1 = 确认;选中路径写入 *out_path(由调用方 free) */
|
|
364
|
-
static int show_dialog_here(GtkWidget *parent_w, int kind, const char *title, const char *message,
|
|
365
|
-
const char *filter, char **out_path) {
|
|
366
|
-
GtkWindow *parent = parent_w ? GTK_WINDOW(parent_w) : NULL;
|
|
367
|
-
int ok = 0;
|
|
368
|
-
|
|
369
|
-
*out_path = NULL;
|
|
370
|
-
if (kind <= 1) {
|
|
371
|
-
GtkWidget *dlg = gtk_file_chooser_dialog_new(
|
|
372
|
-
title[0] ? title : (kind == 1 ? "保存文件" : "打开文件"),
|
|
373
|
-
parent,
|
|
374
|
-
(kind == 1) ? GTK_FILE_CHOOSER_ACTION_SAVE : GTK_FILE_CHOOSER_ACTION_OPEN,
|
|
375
|
-
"取消", GTK_RESPONSE_CANCEL,
|
|
376
|
-
(kind == 1) ? "保存" : "打开", GTK_RESPONSE_ACCEPT,
|
|
377
|
-
NULL);
|
|
378
|
-
apply_filter(GTK_FILE_CHOOSER(dlg), filter);
|
|
379
|
-
if (kind == 1) {
|
|
380
|
-
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE);
|
|
381
|
-
}
|
|
382
|
-
if (gtk_dialog_run(GTK_DIALOG(dlg)) == GTK_RESPONSE_ACCEPT) {
|
|
383
|
-
char *p = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg));
|
|
384
|
-
if (p) {
|
|
385
|
-
*out_path = strdup(p);
|
|
386
|
-
ok = 1;
|
|
387
|
-
g_free(p);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
gtk_widget_destroy(dlg);
|
|
391
|
-
} else {
|
|
392
|
-
GtkMessageType type = GTK_MESSAGE_INFO;
|
|
393
|
-
GtkWidget *dlg;
|
|
394
|
-
int res;
|
|
395
|
-
if (kind == 3) {
|
|
396
|
-
type = GTK_MESSAGE_WARNING;
|
|
397
|
-
} else if (kind == 4) {
|
|
398
|
-
type = GTK_MESSAGE_ERROR;
|
|
399
|
-
} else if (kind == 5) {
|
|
400
|
-
type = GTK_MESSAGE_QUESTION;
|
|
401
|
-
}
|
|
402
|
-
/* 用 "%s" 占位:提示文案里带 % 也不会被当成格式串 */
|
|
403
|
-
dlg = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL, type,
|
|
404
|
-
(kind == 5) ? GTK_BUTTONS_OK_CANCEL : GTK_BUTTONS_OK,
|
|
405
|
-
"%s", message[0] ? message : " ");
|
|
406
|
-
if (title[0]) {
|
|
407
|
-
gtk_window_set_title(GTK_WINDOW(dlg), title);
|
|
408
|
-
}
|
|
409
|
-
res = gtk_dialog_run(GTK_DIALOG(dlg));
|
|
410
|
-
ok = (kind == 5) ? (res == GTK_RESPONSE_OK) : 1;
|
|
411
|
-
gtk_widget_destroy(dlg);
|
|
412
|
-
}
|
|
413
|
-
return ok;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
int cj_plat_run_dialog(cj_host *h, dlg_req *req) {
|
|
417
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
418
|
-
if (!req) return 0;
|
|
419
|
-
req->ok = show_dialog_here(p ? p->window : NULL, req->kind, req->title, req->message,
|
|
420
|
-
req->filter, &req->path);
|
|
421
|
-
return req->ok;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/* 别的线程投递进来的对话框请求:g_idle_add 只带一个参数,包一层带上句柄 */
|
|
425
|
-
typedef struct dialog_call {
|
|
426
|
-
cj_host *h;
|
|
427
|
-
dlg_req *req;
|
|
428
|
-
} dialog_call;
|
|
429
|
-
|
|
430
|
-
static gboolean dialog_idle(gpointer d) {
|
|
431
|
-
dialog_call *c = (dialog_call *)d;
|
|
432
|
-
if (c) {
|
|
433
|
-
cj_core_dialog_tick(c->h, c->req);
|
|
434
|
-
free(c);
|
|
435
|
-
}
|
|
436
|
-
return G_SOURCE_REMOVE;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
void cj_plat_post_dialog(cj_host *h, dlg_req *req) {
|
|
440
|
-
dialog_call *c = (dialog_call *)calloc(1, sizeof(dialog_call));
|
|
441
|
-
if (!c) {
|
|
442
|
-
/* 投不出去就别让调用方干等:作废请求并唤醒等待方 */
|
|
443
|
-
fprintf(stderr, "[cj-bridge] dialog failed: out of memory\n");
|
|
444
|
-
cj_core_dialog_abort(h);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
c->h = h;
|
|
448
|
-
c->req = req;
|
|
449
|
-
g_idle_add(dialog_idle, c);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/* ===== GTK 线程 ===== */
|
|
453
|
-
|
|
454
|
-
static void on_load_changed(WebKitWebView *wv, WebKitLoadEvent ev, gpointer ud) {
|
|
455
|
-
/* no-op */
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
static void on_script_message(WebKitUserContentManager *mgr,
|
|
459
|
-
WebKitJavascriptResult *result, gpointer ud) {
|
|
460
|
-
cj_host *h = (cj_host *)ud;
|
|
461
|
-
JSCValue *v = webkit_javascript_result_get_js_value(result);
|
|
462
|
-
char *s = jsc_value_to_string(v);
|
|
463
|
-
/* 前端 __CJ_TAURI__.reload():宿主控制消息,不进 IPC hub */
|
|
464
|
-
if (s && strcmp(s, CJT_RELOAD_MSG) == 0) {
|
|
465
|
-
fprintf(stderr, "[cj-bridge] frontend requested reload\n");
|
|
466
|
-
g_idle_add(reload_idle, h);
|
|
467
|
-
g_free(s);
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
/* 入站日志:与 Windows 侧同一格式(bridge_win.c 的 js -> native (%d bytes))。
|
|
471
|
-
此前只有 Windows 打这行,端到端取证时两平台不对称(AGENTS.md §1 的交付凭证就看它)。 */
|
|
472
|
-
fprintf(stderr, "[cj-bridge] js -> native (%d bytes)\n", (int)strlen(s ? s : ""));
|
|
473
|
-
if (h && h->on_message) {
|
|
474
|
-
h->on_message(s);
|
|
475
|
-
}
|
|
476
|
-
g_free(s);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
static void on_destroy(GtkWidget *w, gpointer ud) {
|
|
480
|
-
cj_host *h = (cj_host *)ud;
|
|
481
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
482
|
-
if (p && p->window == w) {
|
|
483
|
-
p->window = NULL; /* 窗口已自毁:别在收尾里再碰它 */
|
|
484
|
-
/* view 是窗口的子 widget(gtk_container_add 把浮动引用沉给容器):父窗口销毁时子 widget
|
|
485
|
-
一并销毁、容器那份引用也释放,指针随即失效。收尾要是再 unref 它,就是摸已释放内存——
|
|
486
|
-
实测打出 g_object_unref: assertion 'G_IS_OBJECT (object)' failed。故一并置空。 */
|
|
487
|
-
p->view = NULL;
|
|
488
|
-
}
|
|
489
|
-
/* 置 should_quit + 触发 onDestroy(每个宿主恰好一次)+ 作废进行中的对话框 */
|
|
490
|
-
cj_core_window_destroyed(h);
|
|
491
|
-
gtk_main_quit();
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/* 注入前端桥接脚本:暴露 window.__CJ_TAURI__ */
|
|
495
|
-
/* 平台唯一差异行(其余 JS 见 native/bridge_js.h,两平台共用一份,防漂移):
|
|
496
|
-
post 通道 = webkit.messageHandlers;reload 控制消息同通道发字面量。 */
|
|
497
|
-
#define CJ_LIT(s) s
|
|
498
|
-
#define CJ_POST_STMT " window.webkit.messageHandlers.cjtauri.postMessage(JSON.stringify(obj));"
|
|
499
|
-
#define CJ_RELOAD_STMT " window.webkit.messageHandlers.cjtauri.postMessage('__cj_tauri_reload__');"
|
|
500
|
-
#include "bridge_js.h"
|
|
501
|
-
|
|
502
|
-
static const char *BRIDGE_JS = CJ_BRIDGE_JS(CJ_POST_STMT, CJ_RELOAD_STMT);
|
|
503
|
-
|
|
504
|
-
static void *gtk_thread_main(void *arg) {
|
|
505
|
-
cj_host *h = (cj_host *)arg;
|
|
506
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
507
|
-
WebKitUserContentManager *mgr;
|
|
508
|
-
int i;
|
|
509
|
-
|
|
510
|
-
if (!p) return NULL;
|
|
511
|
-
/* 先记下 GTK 线程身份:对话框要靠它判断「调用方是不是已经在本线程上」 */
|
|
512
|
-
p->thread = pthread_self();
|
|
513
|
-
p->thread_set = 1;
|
|
514
|
-
gtk_init(NULL, NULL);
|
|
515
|
-
|
|
516
|
-
p->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
|
517
|
-
gtk_window_set_title(GTK_WINDOW(p->window), h->win_title ? h->win_title : "cj-tauri");
|
|
518
|
-
gtk_window_set_default_size(GTK_WINDOW(p->window), h->win_w, h->win_h);
|
|
519
|
-
/* 窗口图标:失败只意味着用系统默认图标,不阻断启动(GTK 的 err 要自己释放) */
|
|
520
|
-
if (h->win_icon) {
|
|
521
|
-
GError *icon_err = NULL;
|
|
522
|
-
gboolean icon_ok =
|
|
523
|
-
gtk_window_set_icon_from_file(GTK_WINDOW(p->window), h->win_icon, &icon_err);
|
|
524
|
-
fprintf(stderr, "[cj-bridge] window icon: path=%s ok=%d%s%s\n",
|
|
525
|
-
h->win_icon, icon_ok ? 1 : 0,
|
|
526
|
-
icon_err ? " err=" : "", icon_err ? icon_err->message : "");
|
|
527
|
-
if (icon_err) g_error_free(icon_err);
|
|
528
|
-
}
|
|
529
|
-
g_signal_connect(p->window, "destroy", G_CALLBACK(on_destroy), h);
|
|
530
|
-
|
|
531
|
-
/* 注入时机与 Windows 侧对齐:那边是 AddScriptToExecuteOnDocumentCreated(页面脚本执行前)。
|
|
532
|
-
这里原先用 AT_DOCUMENT_END,会让发行态单文件里的内联 <script type="module"> 抢在注入之前执行
|
|
533
|
-
—— 前端在模块作用域 / onMounted 直接读 window.__CJ_TAURI__ 会拿到 undefined
|
|
534
|
-
(开发态从 URL 加载、模块要现下载,反而掩盖了这个竞态)。所以必须 document-start。 */
|
|
535
|
-
mgr = webkit_user_content_manager_new();
|
|
536
|
-
webkit_user_content_manager_add_script(
|
|
537
|
-
mgr,
|
|
538
|
-
webkit_user_script_new(BRIDGE_JS,
|
|
539
|
-
WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
|
|
540
|
-
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
|
|
541
|
-
NULL, NULL));
|
|
542
|
-
/* 预执行脚本按注册顺序追加在桥之后(同一 document-start 通道,脚本里可直接用 window.__CJ_TAURI__) */
|
|
543
|
-
for (i = 0; i < h->init_script_count; i++) {
|
|
544
|
-
webkit_user_content_manager_add_script(
|
|
545
|
-
mgr,
|
|
546
|
-
webkit_user_script_new(h->init_scripts[i],
|
|
547
|
-
WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
|
|
548
|
-
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
|
|
549
|
-
NULL, NULL));
|
|
550
|
-
}
|
|
551
|
-
fprintf(stderr, "[cj-bridge] init scripts injected: %d\n", h->init_script_count);
|
|
552
|
-
webkit_user_content_manager_register_script_message_handler(mgr, "cjtauri");
|
|
553
|
-
g_signal_connect(mgr, "script-message-received::cjtauri",
|
|
554
|
-
G_CALLBACK(on_script_message), h);
|
|
555
|
-
|
|
556
|
-
p->view = webkit_web_view_new_with_user_content_manager(mgr);
|
|
557
|
-
{
|
|
558
|
-
/* 开发者工具开关:settings 交由视图持有(单实例、随窗口存续,不再额外 unref) */
|
|
559
|
-
WebKitSettings *settings = webkit_settings_new();
|
|
560
|
-
webkit_settings_set_enable_developer_extras(settings, h->devtools ? TRUE : FALSE);
|
|
561
|
-
webkit_web_view_set_settings(WEBKIT_WEB_VIEW(p->view), settings);
|
|
562
|
-
}
|
|
563
|
-
g_signal_connect(p->view, "load-changed", G_CALLBACK(on_load_changed), NULL);
|
|
564
|
-
/* 装配:window 里套一层竖向 GtkBox(菜单栏 + view)。固定套一层而不是「有菜单才套」——
|
|
565
|
-
少一种装配分支就少一类时序坑,代价只是一个 GtkBox 的开销。 */
|
|
566
|
-
p->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
|
567
|
-
gtk_box_pack_start(GTK_BOX(p->box), p->view, TRUE, TRUE, 0);
|
|
568
|
-
gtk_container_add(GTK_CONTAINER(p->window), p->box);
|
|
569
|
-
/* 装配期就已经 set_menu 过:这时把菜单栏建出来(运行期再改的走 g_idle_add 到本线程)*/
|
|
570
|
-
if (h->pending_menu_text && h->pending_menu_text[0]) {
|
|
571
|
-
menu_apply_wire(h, p, h->pending_menu_text);
|
|
572
|
-
}
|
|
573
|
-
gtk_widget_show_all(p->window);
|
|
574
|
-
cj_core_set_ready(h);
|
|
575
|
-
|
|
576
|
-
/* 页面来源:URL 优先(cj_bridge_load_url),否则内联 HTML(cj_bridge_start) */
|
|
577
|
-
if (h->pending_url) {
|
|
578
|
-
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(p->view), h->pending_url);
|
|
579
|
-
} else if (h->pending_html) {
|
|
580
|
-
webkit_web_view_load_html(WEBKIT_WEB_VIEW(p->view), h->pending_html, NULL);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
gtk_main();
|
|
584
|
-
|
|
585
|
-
/* 主循环退出:收尾。用户关窗时窗口已自毁(on_destroy 里已置空),这里只收还活着的对象;
|
|
586
|
-
一律置空,免得后续 quit_idle / cj_plat_fini 摸到已释放的 widget(此前悬垂指针的来源)。 */
|
|
587
|
-
if (p->window) {
|
|
588
|
-
/* 销毁必须留在主循环之外(quit_idle 只 gtk_main_quit):主循环内销毁 WebKit 窗口会让
|
|
589
|
-
libwebkit2gtk 的退出期析构函数 abort(退出码 134,见 quit_idle 上的注释)*/
|
|
590
|
-
gtk_widget_destroy(p->window); /* 子 widget 随父销毁:容器持有的那份引用这时才释放 */
|
|
591
|
-
p->window = NULL;
|
|
592
|
-
}
|
|
593
|
-
/* 我们从不额外持 view 的引用(那是容器的引用),所以这里没有可 unref 的东西:
|
|
594
|
-
窗口一旦销毁,view 指针就已失效,只能置空。 */
|
|
595
|
-
p->view = NULL;
|
|
596
|
-
p->box = NULL; /* 随窗口一起销毁了 */
|
|
597
|
-
p->menubar = NULL;
|
|
598
|
-
menu_clear_slots(p); /* 槽里的 app_id 是我们 malloc 的,跟平台状态一起收 */
|
|
599
|
-
/* 兜底:非关窗路径退出主循环时也要通知一次(幂等,正常路径已触发过) */
|
|
600
|
-
cj_core_window_destroyed(h);
|
|
601
|
-
/* 最后一步才立这个标记:cj_plat_fini 见到它才 free 平台状态(此刻本线程已不再碰 p)*/
|
|
602
|
-
p->thread_done = 1;
|
|
603
|
-
return NULL;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
void cj_plat_start(cj_host *h) {
|
|
607
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
608
|
-
pthread_t t;
|
|
609
|
-
if (!p) {
|
|
610
|
-
fprintf(stderr, "[cj-bridge] start failed: platform not initialized\n");
|
|
611
|
-
if (h) h->should_quit = 1; /* 起不来就别让 run() 干等 */
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
if (pthread_create(&t, NULL, gtk_thread_main, h) != 0) {
|
|
615
|
-
fprintf(stderr, "[cj-bridge] start failed: cannot create host thread\n");
|
|
616
|
-
h->should_quit = 1;
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
/* 不再 detach:cj_plat_fini 需要 join 它,确认线程收尾完毕再释放平台状态 */
|
|
620
|
-
p->thread = t;
|
|
621
|
-
p->thread_started = 1;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/* ===== 菜单栏翻译层(RFC-002 §5.4):线路文本 → GtkMenuBar =====
|
|
625
|
-
格式解析不在这里重复实现:走 core 的 cj_core_menu_walk(两平台共用一份,桩平台已自检)。
|
|
626
|
-
本层只做三件事:把行翻译成 GTK 控件、把「用户激活」经 core 回投、维护运行期改状态的槽。 */
|
|
627
|
-
|
|
628
|
-
static void menu_clear_slots(plat_host *p) {
|
|
629
|
-
int i;
|
|
630
|
-
if (!p) return;
|
|
631
|
-
for (i = 0; i < p->menu_item_count; i++) {
|
|
632
|
-
free(p->menu_items[i].app_id);
|
|
633
|
-
p->menu_items[i].app_id = NULL;
|
|
634
|
-
p->menu_items[i].widget = NULL;
|
|
635
|
-
p->menu_items[i].ctx = NULL;
|
|
636
|
-
}
|
|
637
|
-
p->menu_item_count = 0;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/* 普通项:activate = 用户点选(没有勾选状态可报,checked 恒 0)*/
|
|
641
|
-
static void on_menu_item_activate(GtkMenuItem *item, gpointer data) {
|
|
642
|
-
menu_item_ctx *c = (menu_item_ctx *)data;
|
|
643
|
-
(void)item;
|
|
644
|
-
if (!c || !c->h || c->suppress) return;
|
|
645
|
-
/* 勾选项改状态会挪动菜单栏里的位置,所以每次点击后重投一次几何读回(见 menu_post_layout) */
|
|
646
|
-
menu_post_layout(c->h);
|
|
647
|
-
cj_core_menu_clicked(c->h, c->app_id ? c->app_id : "", c->enabled, c->checked);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/* 勾选项:toggled 在 active 变化之后发(含用户点击与程序化设置,后者被 suppress 挡掉)*/
|
|
651
|
-
static void on_menu_item_toggled(GtkCheckMenuItem *item, gpointer data) {
|
|
652
|
-
menu_item_ctx *c = (menu_item_ctx *)data;
|
|
653
|
-
if (!c || !c->h || c->suppress) return;
|
|
654
|
-
c->checked = gtk_check_menu_item_get_active(item) ? 1 : 0;
|
|
655
|
-
menu_post_layout(c->h); /* 同上:勾选指示器状态变了,重读一次几何 */
|
|
656
|
-
cj_core_menu_clicked(c->h, c->app_id ? c->app_id : "", c->enabled, c->checked);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/* 菜单项随菜单栏销毁:ctx 归本次构建所有,在这里释放(槽里的那份 app_id 另算)*/
|
|
660
|
-
static void on_menu_item_destroy(GtkWidget *w, gpointer data) {
|
|
661
|
-
menu_item_ctx *c = (menu_item_ctx *)data;
|
|
662
|
-
(void)w;
|
|
663
|
-
if (!c) return;
|
|
664
|
-
free(c->app_id);
|
|
665
|
-
free(c);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
static void menu_apply_item_state(plat_host *p, const char *id, int enabled, int checked) {
|
|
669
|
-
int i;
|
|
670
|
-
if (!p || !id || !id[0]) return;
|
|
671
|
-
for (i = 0; i < p->menu_item_count; i++) {
|
|
672
|
-
menu_slot *s = &p->menu_items[i];
|
|
673
|
-
if (!s->widget || !s->app_id || strcmp(s->app_id, id) != 0) continue;
|
|
674
|
-
s->enabled = enabled ? 1 : 0;
|
|
675
|
-
s->checked = checked ? 1 : 0;
|
|
676
|
-
gtk_widget_set_sensitive(s->widget, s->enabled ? TRUE : FALSE);
|
|
677
|
-
if (s->ctx) {
|
|
678
|
-
s->ctx->enabled = s->enabled;
|
|
679
|
-
s->ctx->checked = s->checked;
|
|
680
|
-
}
|
|
681
|
-
if (s->kind == 'c') {
|
|
682
|
-
/* 程序化改勾选同样会发 toggled:置 suppress 把它挡掉,免得被当成一次用户点击 */
|
|
683
|
-
if (s->ctx) s->ctx->suppress = 1;
|
|
684
|
-
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->widget),
|
|
685
|
-
s->checked ? TRUE : FALSE);
|
|
686
|
-
if (s->ctx) s->ctx->suppress = 0;
|
|
687
|
-
}
|
|
688
|
-
/* 同上:core 那句 `set menu item:` 只说明「我调过」,这里从 GTK 侧读回真正生效的值。
|
|
689
|
-
两者不一致(例如 widget 已随菜单栏销毁)就说明这条改动没落到菜单上。 */
|
|
690
|
-
fprintf(stderr, "[cj-bridge] menu item readback: id=%s sensitive=%d active=%d\n", s->app_id,
|
|
691
|
-
gtk_widget_get_sensitive(s->widget) ? 1 : 0,
|
|
692
|
-
(s->kind == 'c')
|
|
693
|
-
? (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(s->widget)) ? 1 : 0)
|
|
694
|
-
: 0);
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
fprintf(stderr, "[cj-bridge] menu item not found: %s\n", id);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/* 建菜单时的上下文:层级用一张 GtkMenu 栈表示(行按文档顺序来,深度只会 +1 或回退)*/
|
|
701
|
-
typedef struct menu_build_ctx {
|
|
702
|
-
cj_host *h;
|
|
703
|
-
plat_host *p;
|
|
704
|
-
GtkWidget *menubar;
|
|
705
|
-
GtkWidget *stack[CJ_MENU_MAX_DEPTH]; /* stack[d]:深度 d 的行往哪个 GtkMenu 里追加 */
|
|
706
|
-
int ok[CJ_MENU_MAX_DEPTH]; /* 该层是否已有子菜单可挂(没有就退回上一层)*/
|
|
707
|
-
} menu_build_ctx;
|
|
708
|
-
|
|
709
|
-
static void menu_add_row(void *ctx, int depth, char kind, const char *id,
|
|
710
|
-
const char *label, int flags, const char *accel) {
|
|
711
|
-
menu_build_ctx *c = (menu_build_ctx *)ctx;
|
|
712
|
-
plat_host *p;
|
|
713
|
-
GtkWidget *parent_menu;
|
|
714
|
-
GtkWidget *item;
|
|
715
|
-
menu_item_ctx *ic;
|
|
716
|
-
menu_slot *slot;
|
|
717
|
-
char *text;
|
|
718
|
-
int enabled = (flags & 1) ? 1 : 0;
|
|
719
|
-
int checked = (flags & 2) ? 1 : 0;
|
|
720
|
-
|
|
721
|
-
if (!c || !c->p) return;
|
|
722
|
-
p = c->p;
|
|
723
|
-
if (depth < 0) depth = 0;
|
|
724
|
-
if (depth >= CJ_MENU_MAX_DEPTH) depth = CJ_MENU_MAX_DEPTH - 1;
|
|
725
|
-
parent_menu = (depth > 0 && c->ok[depth]) ? c->stack[depth] : c->menubar;
|
|
726
|
-
|
|
727
|
-
if (kind == 's') { /* 分隔线:既没有命令 id,也没有文字 */
|
|
728
|
-
gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), gtk_separator_menu_item_new());
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
/* 标签:accel 非空时接在文字后面(GTK3 的快捷键要走 accel_group,v1 只做提示文本;
|
|
732
|
-
这里用空格而不是制表符——GTK 的标签不像 Win32 那样把 \t 之后的部分右对齐)*/
|
|
733
|
-
{
|
|
734
|
-
size_t n = strlen(label) + (accel[0] ? strlen(accel) + 2 : 0) + 1;
|
|
735
|
-
text = (char *)malloc(n);
|
|
736
|
-
if (!text) return;
|
|
737
|
-
snprintf(text, n, "%s%s%s", label, accel[0] ? " " : "", accel);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (kind == 'm') { /* 子菜单:建出 GtkMenu 挂到子菜单项上,并让深一层的行知道往哪挂 */
|
|
741
|
-
GtkWidget *submenu = gtk_menu_new();
|
|
742
|
-
item = gtk_menu_item_new_with_label(text);
|
|
743
|
-
free(text);
|
|
744
|
-
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), submenu);
|
|
745
|
-
gtk_widget_set_sensitive(item, enabled ? TRUE : FALSE);
|
|
746
|
-
gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), item);
|
|
747
|
-
if (depth + 1 < CJ_MENU_MAX_DEPTH) {
|
|
748
|
-
c->stack[depth + 1] = submenu;
|
|
749
|
-
c->ok[depth + 1] = 1;
|
|
750
|
-
}
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
if (p->menu_item_count >= CJ_MENU_MAX_ITEMS) {
|
|
755
|
-
fprintf(stderr, "[cj-bridge] menu: item limit reached, dropped: %s\n", id);
|
|
756
|
-
free(text);
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
item = (kind == 'c') ? gtk_check_menu_item_new_with_label(text)
|
|
760
|
-
: gtk_menu_item_new_with_label(text);
|
|
761
|
-
free(text);
|
|
762
|
-
gtk_widget_set_sensitive(item, enabled ? TRUE : FALSE);
|
|
763
|
-
if (kind == 'c') {
|
|
764
|
-
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), checked ? TRUE : FALSE);
|
|
765
|
-
}
|
|
766
|
-
ic = (menu_item_ctx *)calloc(1, sizeof(*ic));
|
|
767
|
-
if (ic) {
|
|
768
|
-
ic->h = c->h;
|
|
769
|
-
ic->app_id = strdup(id);
|
|
770
|
-
ic->kind = kind;
|
|
771
|
-
ic->enabled = enabled;
|
|
772
|
-
ic->checked = checked;
|
|
773
|
-
if (kind == 'c') {
|
|
774
|
-
g_signal_connect(item, "toggled", G_CALLBACK(on_menu_item_toggled), ic);
|
|
775
|
-
} else {
|
|
776
|
-
g_signal_connect(item, "activate", G_CALLBACK(on_menu_item_activate), ic);
|
|
777
|
-
}
|
|
778
|
-
g_signal_connect(item, "destroy", G_CALLBACK(on_menu_item_destroy), ic);
|
|
779
|
-
} else {
|
|
780
|
-
fprintf(stderr, "[cj-bridge] menu: out of memory for item ctx (%s)\n", id);
|
|
781
|
-
}
|
|
782
|
-
gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), item);
|
|
783
|
-
|
|
784
|
-
slot = &p->menu_items[p->menu_item_count++];
|
|
785
|
-
free(slot->app_id); /* 槽是复用的,清掉上一轮残留 */
|
|
786
|
-
slot->app_id = strdup(id);
|
|
787
|
-
slot->widget = item;
|
|
788
|
-
slot->ctx = ic;
|
|
789
|
-
slot->kind = kind;
|
|
790
|
-
slot->enabled = enabled;
|
|
791
|
-
slot->checked = checked;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
/* ===== 菜单几何的事后读回(实机驱动用) =====
|
|
795
|
-
为什么需要:鼠标驱动的探针要把点击落在「项的几何中心」,而**像素偏移是写不住的**——
|
|
796
|
-
窗口位置每轮都可能不同(实测同一份脚本两轮:一轮窗口在 0,0,一轮在 60,83),
|
|
797
|
-
写死 +45/+135 这类偏移会点到隔壁项(菜单轮实机第一版就是这么点歪的)。
|
|
798
|
-
所以让 GTK 自己说:id=x+w+h,按菜单栏顺序打一行;分隔线与子菜单项没有应用 id,打 '-'。
|
|
799
|
-
为什么走**低优先级** idle:刚 pack 上去那一刻还没走完分配,读到的可能是 0;排到低优先级
|
|
800
|
-
就等于「当前待处理的事件与重绘先做完」,这时拿到的才是最终值。每次用户点击后再投一次,
|
|
801
|
-
这样驱动脚本每次点击前都能从日志里取到**最新**几何(勾选指示器一类的状态变化会挪位置)。 */
|
|
802
|
-
typedef struct menu_layout_idle {
|
|
803
|
-
cj_host *h;
|
|
804
|
-
} menu_layout_idle;
|
|
805
|
-
|
|
806
|
-
static gboolean menu_layout_idle_cb(gpointer data) {
|
|
807
|
-
menu_layout_idle *m = (menu_layout_idle *)data;
|
|
808
|
-
GString *s;
|
|
809
|
-
if (!m) return G_SOURCE_REMOVE;
|
|
810
|
-
s = g_string_new(NULL);
|
|
811
|
-
if (m->h) {
|
|
812
|
-
plat_host *p = (plat_host *)m->h->plat;
|
|
813
|
-
/* 窗口可能已经销毁(销毁后 p->menubar 为 NULL):这里不再碰 GTK */
|
|
814
|
-
if (p && p->window && p->menubar) {
|
|
815
|
-
GList *kids = gtk_container_get_children(GTK_CONTAINER(p->menubar));
|
|
816
|
-
GList *it;
|
|
817
|
-
for (it = kids; it; it = it->next) {
|
|
818
|
-
GtkWidget *w = GTK_WIDGET(it->data);
|
|
819
|
-
GtkAllocation a;
|
|
820
|
-
const char *id = "-";
|
|
821
|
-
int i;
|
|
822
|
-
for (i = 0; i < p->menu_item_count; i++) {
|
|
823
|
-
if (p->menu_items[i].widget == w) {
|
|
824
|
-
id = p->menu_items[i].app_id;
|
|
825
|
-
break;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
gtk_widget_get_allocation(w, &a);
|
|
829
|
-
g_string_append_printf(s, "%s=%d+%d+%d ", id, a.x, a.width, a.height);
|
|
830
|
-
}
|
|
831
|
-
g_list_free(kids);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
fprintf(stderr, "[cj-bridge] menu layout: %s\n", s->str);
|
|
835
|
-
g_string_free(s, TRUE);
|
|
836
|
-
free(m);
|
|
837
|
-
return G_SOURCE_REMOVE;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
static void menu_post_layout(cj_host *h) {
|
|
841
|
-
menu_layout_idle *m = (menu_layout_idle *)calloc(1, sizeof(*m));
|
|
842
|
-
if (!m) return;
|
|
843
|
-
m->h = h;
|
|
844
|
-
g_idle_add_full(G_PRIORITY_LOW, menu_layout_idle_cb, m, NULL);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
static void menu_apply_wire(cj_host *h, plat_host *p, const char *wire) {
|
|
848
|
-
menu_build_ctx c;
|
|
849
|
-
|
|
850
|
-
/* p->window 为 NULL 说明窗口已销毁(on_destroy 会置空):晚到的 idle 就别再碰 GTK 了 */
|
|
851
|
-
if (!p || !p->window || !p->box) return;
|
|
852
|
-
/* 旧菜单栏整体拆掉:子项随父销毁,各自的 ctx 由 destroy 处理器释放 */
|
|
853
|
-
if (p->menubar) {
|
|
854
|
-
gtk_widget_destroy(p->menubar);
|
|
855
|
-
p->menubar = NULL;
|
|
856
|
-
}
|
|
857
|
-
menu_clear_slots(p);
|
|
858
|
-
if (!wire || !wire[0]) { /* 空串 = 清空菜单栏(不留一条空条)*/
|
|
859
|
-
fprintf(stderr, "[cj-bridge] menu cleared\n");
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
memset(&c, 0, sizeof(c)); /* 不用 ZeroMemory:那是 Windows 宏,本文件没有它 */
|
|
863
|
-
c.h = h;
|
|
864
|
-
c.p = p;
|
|
865
|
-
c.menubar = gtk_menu_bar_new();
|
|
866
|
-
c.stack[0] = c.menubar;
|
|
867
|
-
c.ok[0] = 1;
|
|
868
|
-
cj_core_menu_walk(wire, menu_add_row, &c);
|
|
869
|
-
p->menubar = c.menubar;
|
|
870
|
-
gtk_box_pack_start(GTK_BOX(p->box), p->menubar, FALSE, FALSE, 0);
|
|
871
|
-
gtk_box_reorder_child(GTK_BOX(p->box), p->menubar, 0); /* 菜单栏必须在 view 之上 */
|
|
872
|
-
gtk_widget_show_all(p->menubar);
|
|
873
|
-
/* 凭证是「事后从 GTK 侧读回来」,不是「我调过 gtk_menu_bar_new 了」:菜单栏没挂进 box、
|
|
874
|
-
或条目一个都没建出来时,光打一句 applied 会把人骗过去(Windows 侧 SetMenu 静默失败
|
|
875
|
-
正是这么踩的,见 bridge_win.c 的 GetMenu 复核)。bar_children 是菜单栏的顶层项数
|
|
876
|
-
(含分隔线),items 是全部槽位(含子菜单里的项),两者不等是正常的。 */
|
|
877
|
-
{
|
|
878
|
-
GList *kids = gtk_container_get_children(GTK_CONTAINER(p->menubar));
|
|
879
|
-
fprintf(stderr,
|
|
880
|
-
"[cj-bridge] menu applied: items=%d bar=%p parent=%p bar_children=%u visible=%d\n",
|
|
881
|
-
p->menu_item_count, (void *)p->menubar, (void *)gtk_widget_get_parent(p->menubar),
|
|
882
|
-
(unsigned)g_list_length(kids), gtk_widget_get_visible(p->menubar) ? 1 : 0);
|
|
883
|
-
g_list_free(kids);
|
|
884
|
-
}
|
|
885
|
-
menu_post_layout(h); /* 布局走完再读一次几何(低优先级 idle),驱动脚本据此定位 */
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
/* 跨线程换菜单:线路文本进 g_idle 队列,由 GTK 线程消费(payload 谁分配谁在 idle 里释放)*/
|
|
889
|
-
typedef struct menu_wire_idle {
|
|
890
|
-
cj_host *h;
|
|
891
|
-
char *wire;
|
|
892
|
-
} menu_wire_idle;
|
|
893
|
-
|
|
894
|
-
static gboolean menu_wire_idle_cb(gpointer data) {
|
|
895
|
-
menu_wire_idle *m = (menu_wire_idle *)data;
|
|
896
|
-
if (!m) return G_SOURCE_REMOVE;
|
|
897
|
-
{
|
|
898
|
-
plat_host *p = m->h ? (plat_host *)m->h->plat : NULL;
|
|
899
|
-
if (p) menu_apply_wire(m->h, p, m->wire);
|
|
900
|
-
}
|
|
901
|
-
free(m->wire);
|
|
902
|
-
free(m);
|
|
903
|
-
return G_SOURCE_REMOVE;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
void cj_plat_set_menu(cj_host *h, const char *wire) {
|
|
907
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
908
|
-
if (!p || !wire) return;
|
|
909
|
-
if (!p->window) {
|
|
910
|
-
/* 未就绪:core 已把线路文本记在 h->pending_menu_text,宿主线程装配时取用 */
|
|
911
|
-
fprintf(stderr, "[cj-bridge] menu deferred: window not ready\n");
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
if (cj_plat_is_ui_thread(h)) {
|
|
915
|
-
menu_apply_wire(h, p, wire);
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
{
|
|
919
|
-
menu_wire_idle *m = (menu_wire_idle *)calloc(1, sizeof(*m));
|
|
920
|
-
if (!m) return;
|
|
921
|
-
m->h = h;
|
|
922
|
-
m->wire = strdup(wire);
|
|
923
|
-
if (!m->wire) {
|
|
924
|
-
free(m);
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
g_idle_add(menu_wire_idle_cb, m);
|
|
928
|
-
fprintf(stderr, "[cj-bridge] menu posted to gtk thread (%d bytes)\n", (int)strlen(wire));
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
/* 跨线程改单项状态:同样走 g_idle(与换菜单分开一个包裹,互不阻塞)*/
|
|
933
|
-
typedef struct menu_state_idle {
|
|
934
|
-
cj_host *h;
|
|
935
|
-
char *id;
|
|
936
|
-
int enabled;
|
|
937
|
-
int checked;
|
|
938
|
-
} menu_state_idle;
|
|
939
|
-
|
|
940
|
-
static gboolean menu_state_idle_cb(gpointer data) {
|
|
941
|
-
menu_state_idle *m = (menu_state_idle *)data;
|
|
942
|
-
if (!m) return G_SOURCE_REMOVE;
|
|
943
|
-
{
|
|
944
|
-
plat_host *p = m->h ? (plat_host *)m->h->plat : NULL;
|
|
945
|
-
if (p) menu_apply_item_state(p, m->id, m->enabled, m->checked);
|
|
946
|
-
}
|
|
947
|
-
free(m->id);
|
|
948
|
-
free(m);
|
|
949
|
-
return G_SOURCE_REMOVE;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
void cj_plat_menu_item_state(cj_host *h, const char *id, int enabled, int checked) {
|
|
953
|
-
plat_host *p = h ? (plat_host *)h->plat : NULL;
|
|
954
|
-
if (!p || !id || !id[0]) return;
|
|
955
|
-
if (!p->window || !p->menubar) {
|
|
956
|
-
fprintf(stderr, "[cj-bridge] menu item state ignored: menu not ready (%s)\n", id);
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
if (cj_plat_is_ui_thread(h)) {
|
|
960
|
-
menu_apply_item_state(p, id, enabled, checked);
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
{
|
|
964
|
-
menu_state_idle *m = (menu_state_idle *)calloc(1, sizeof(*m));
|
|
965
|
-
if (!m) return;
|
|
966
|
-
m->h = h;
|
|
967
|
-
m->id = strdup(id);
|
|
968
|
-
m->enabled = enabled ? 1 : 0;
|
|
969
|
-
m->checked = checked ? 1 : 0;
|
|
970
|
-
if (!m->id) {
|
|
971
|
-
free(m);
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
g_idle_add(menu_state_idle_cb, m);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
int cj_plat_host_capabilities(cj_host *h) {
|
|
979
|
-
(void)h;
|
|
980
|
-
/* 本平台的静态能力:菜单栏已落地(GTK 的菜单栏不需要可选依赖);托盘 / 拖放到 7.B / 7.C */
|
|
981
|
-
return CJ_CAP_MENU;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
/* =========================================================================
|
|
985
|
-
* 串口(plugin_serial 的平台层):POSIX termios
|
|
986
|
-
*
|
|
987
|
-
* 句柄表与 fd **只活在本文件**:仓颉侧拿到的是表里分配的序号而不是 fd —— 页面即使拿着句柄乱试,
|
|
988
|
-
* 也够不到 fd 1/2 这类「另有含义」的值。表锁用 PTHREAD_MUTEX_INITIALIZER 静态初始化,
|
|
989
|
-
* 所以不需要 core 提供「一次性初始化」钩子,无头进程(没有窗口)里也能直接用。
|
|
990
|
-
*
|
|
991
|
-
* 打开一律 O_RDWR | O_NOCTTY | O_NONBLOCK:不阻塞(不为等载波信号钉住 worker 线程),
|
|
992
|
-
* 也不把串口变成进程的控制终端。读取的超时交给 poll,不用 VTIME/VMIN——两套超时混用会让
|
|
993
|
-
* 「timeout=0 就是不等待」这件事变得不可预测。
|
|
994
|
-
* ========================================================================= */
|
|
995
|
-
#define CJ_SERIAL_MAX_HANDLES 8
|
|
996
|
-
|
|
997
|
-
typedef struct serial_slot {
|
|
998
|
-
int used;
|
|
999
|
-
long long id;
|
|
1000
|
-
int fd;
|
|
1001
|
-
char path[256];
|
|
1002
|
-
} serial_slot;
|
|
1003
|
-
|
|
1004
|
-
static serial_slot g_serial_slots[CJ_SERIAL_MAX_HANDLES];
|
|
1005
|
-
static long long g_serial_next_id = 1;
|
|
1006
|
-
static pthread_mutex_t g_serial_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
1007
|
-
|
|
1008
|
-
static void serial_err(char *err, int err_len, const char *fmt, ...) {
|
|
1009
|
-
va_list ap;
|
|
1010
|
-
if (!err || err_len <= 0) return;
|
|
1011
|
-
va_start(ap, fmt);
|
|
1012
|
-
vsnprintf(err, (size_t)err_len, fmt, ap);
|
|
1013
|
-
va_end(ap);
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
/* 数值波特率 → speed_t:只收 Linux 明确有宏的档位,不认识就拒绝(不猜、不近似) */
|
|
1017
|
-
static int serial_speed_of(int baud, speed_t *out) {
|
|
1018
|
-
switch (baud) {
|
|
1019
|
-
case 300: *out = B300; return 1;
|
|
1020
|
-
case 600: *out = B600; return 1;
|
|
1021
|
-
case 1200: *out = B1200; return 1;
|
|
1022
|
-
case 2400: *out = B2400; return 1;
|
|
1023
|
-
case 4800: *out = B4800; return 1;
|
|
1024
|
-
case 9600: *out = B9600; return 1;
|
|
1025
|
-
case 19200: *out = B19200; return 1;
|
|
1026
|
-
case 38400: *out = B38400; return 1;
|
|
1027
|
-
case 57600: *out = B57600; return 1;
|
|
1028
|
-
case 115200: *out = B115200; return 1;
|
|
1029
|
-
case 230400: *out = B230400; return 1;
|
|
1030
|
-
case 460800: *out = B460800; return 1;
|
|
1031
|
-
case 500000: *out = B500000; return 1;
|
|
1032
|
-
case 921600: *out = B921600; return 1;
|
|
1033
|
-
case 1000000: *out = B1000000; return 1;
|
|
1034
|
-
default: return 0;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
static serial_slot *serial_find_locked(long long id) {
|
|
1039
|
-
int i;
|
|
1040
|
-
for (i = 0; i < CJ_SERIAL_MAX_HANDLES; i++) {
|
|
1041
|
-
if (g_serial_slots[i].used && g_serial_slots[i].id == id) return &g_serial_slots[i];
|
|
1042
|
-
}
|
|
1043
|
-
return NULL;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/* 把句柄翻成 fd;句柄无效时返回 -1 并填 err。调用方负责在 poll/read/write 期间不再持锁。 */
|
|
1047
|
-
static int serial_fd_of(long long h, char *err, int err_len) {
|
|
1048
|
-
serial_slot *s;
|
|
1049
|
-
int fd;
|
|
1050
|
-
pthread_mutex_lock(&g_serial_lock);
|
|
1051
|
-
s = serial_find_locked(h);
|
|
1052
|
-
fd = s ? s->fd : -1;
|
|
1053
|
-
pthread_mutex_unlock(&g_serial_lock);
|
|
1054
|
-
if (fd < 0) serial_err(err, err_len, "serial: 句柄 %lld 无效(未打开或已关闭)", h);
|
|
1055
|
-
return fd;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
static long long serial_now_ms(void) {
|
|
1059
|
-
struct timespec ts;
|
|
1060
|
-
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
1061
|
-
return (long long)ts.tv_sec * 1000 + (long long)ts.tv_nsec / 1000000;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
long long cj_plat_serial_open(const char *path, const cj_serial_cfg *cfg, char *err, int err_len) {
|
|
1065
|
-
struct termios tty;
|
|
1066
|
-
speed_t sp;
|
|
1067
|
-
tcflag_t dbits;
|
|
1068
|
-
int fd;
|
|
1069
|
-
int i;
|
|
1070
|
-
long long id;
|
|
1071
|
-
|
|
1072
|
-
if (!path || !cfg) {
|
|
1073
|
-
serial_err(err, err_len, "serial: 空路径 / 空配置");
|
|
1074
|
-
return -1;
|
|
1075
|
-
}
|
|
1076
|
-
if (!serial_speed_of(cfg->baud, &sp)) {
|
|
1077
|
-
serial_err(err, err_len, "serial: 本平台不支持的波特率 %d(只收常见档位)", cfg->baud);
|
|
1078
|
-
return -1;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
/* open + 配置整段留在锁内:O_NONBLOCK 下 open 不会久等,这样也免了「先占槽再开」的竞态窗口 */
|
|
1082
|
-
pthread_mutex_lock(&g_serial_lock);
|
|
1083
|
-
|
|
1084
|
-
for (i = 0; i < CJ_SERIAL_MAX_HANDLES; i++) {
|
|
1085
|
-
if (!g_serial_slots[i].used) break;
|
|
1086
|
-
}
|
|
1087
|
-
if (i == CJ_SERIAL_MAX_HANDLES) {
|
|
1088
|
-
pthread_mutex_unlock(&g_serial_lock);
|
|
1089
|
-
serial_err(err, err_len, "serial: 句柄表满(最多 %d 个并发串口)", CJ_SERIAL_MAX_HANDLES);
|
|
1090
|
-
return -2;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
|
|
1094
|
-
if (fd < 0) {
|
|
1095
|
-
serial_err(err, err_len, "serial: open(%s) 失败: %s", path, strerror(errno));
|
|
1096
|
-
pthread_mutex_unlock(&g_serial_lock);
|
|
1097
|
-
return -3;
|
|
1098
|
-
}
|
|
1099
|
-
if (tcgetattr(fd, &tty) != 0) {
|
|
1100
|
-
serial_err(err, err_len, "serial: tcgetattr(%s) 失败: %s(不是终端类设备?)",
|
|
1101
|
-
path, strerror(errno));
|
|
1102
|
-
close(fd);
|
|
1103
|
-
pthread_mutex_unlock(&g_serial_lock);
|
|
1104
|
-
return -3;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
/* 裸模式:串口上多半是二进制帧,回显 / 换行转换 / 信号解释都是破坏性加工。
|
|
1108
|
-
cfmakeraw 顺带设好 CS8 与清 PARENB,所以后面再按调用方参数覆盖一遍。 */
|
|
1109
|
-
cfmakeraw(&tty);
|
|
1110
|
-
tty.c_cflag |= (CLOCAL | CREAD);
|
|
1111
|
-
switch (cfg->data_bits) {
|
|
1112
|
-
case 5: dbits = CS5; break;
|
|
1113
|
-
case 6: dbits = CS6; break;
|
|
1114
|
-
case 7: dbits = CS7; break;
|
|
1115
|
-
default: dbits = CS8; break;
|
|
1116
|
-
}
|
|
1117
|
-
tty.c_cflag &= ~CSIZE;
|
|
1118
|
-
tty.c_cflag |= dbits;
|
|
1119
|
-
switch (cfg->parity) {
|
|
1120
|
-
case 1: tty.c_cflag |= (PARENB | PARODD); break; /* 奇校验 */
|
|
1121
|
-
case 2: tty.c_cflag |= PARENB; tty.c_cflag &= ~PARODD; break; /* 偶校验 */
|
|
1122
|
-
default: tty.c_cflag &= ~PARENB; break; /* 无校验 */
|
|
1123
|
-
}
|
|
1124
|
-
if (cfg->stop_bits == 2) {
|
|
1125
|
-
tty.c_cflag |= CSTOPB;
|
|
1126
|
-
} else {
|
|
1127
|
-
tty.c_cflag &= ~CSTOPB;
|
|
1128
|
-
}
|
|
1129
|
-
/* 硬件流控不替应用开:RTS/CTS 在不少设备上是手工握手的信号,框架自作主张会改语义 */
|
|
1130
|
-
tty.c_cflag &= ~CRTSCTS;
|
|
1131
|
-
tty.c_iflag &= ~(IXON | IXOFF | IXANY);
|
|
1132
|
-
tty.c_cc[VMIN] = 0;
|
|
1133
|
-
tty.c_cc[VTIME] = 0;
|
|
1134
|
-
if (cfsetispeed(&tty, sp) != 0 || cfsetospeed(&tty, sp) != 0) {
|
|
1135
|
-
serial_err(err, err_len, "serial: 设置波特率 %d 失败: %s", cfg->baud, strerror(errno));
|
|
1136
|
-
close(fd);
|
|
1137
|
-
pthread_mutex_unlock(&g_serial_lock);
|
|
1138
|
-
return -3;
|
|
1139
|
-
}
|
|
1140
|
-
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
|
|
1141
|
-
serial_err(err, err_len, "serial: tcsetattr(%s) 失败: %s(设备不接受这组参数?)",
|
|
1142
|
-
path, strerror(errno));
|
|
1143
|
-
close(fd);
|
|
1144
|
-
pthread_mutex_unlock(&g_serial_lock);
|
|
1145
|
-
return -3;
|
|
1146
|
-
}
|
|
1147
|
-
tcflush(fd, TCIOFLUSH);
|
|
1148
|
-
|
|
1149
|
-
id = g_serial_next_id++;
|
|
1150
|
-
if (g_serial_next_id <= 0) g_serial_next_id = 1; /* 溢出兜底:句柄永不为 0 */
|
|
1151
|
-
g_serial_slots[i].used = 1;
|
|
1152
|
-
g_serial_slots[i].id = id;
|
|
1153
|
-
g_serial_slots[i].fd = fd;
|
|
1154
|
-
snprintf(g_serial_slots[i].path, sizeof(g_serial_slots[i].path), "%s", path);
|
|
1155
|
-
pthread_mutex_unlock(&g_serial_lock);
|
|
1156
|
-
|
|
1157
|
-
/* 落地一行:参数与 fd 都打全,串口问题基本都是拿这行对账(AGENTS §1 的取证口径) */
|
|
1158
|
-
fprintf(stderr, "[cj-bridge] serial termios: path=%s baud=%d data=%d parity=%d stop=%d fd=%d\n",
|
|
1159
|
-
path, cfg->baud, cfg->data_bits, cfg->parity, cfg->stop_bits, fd);
|
|
1160
|
-
return id;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
int cj_plat_serial_read(long long h, unsigned char *buf, int len, int timeout_ms, char *err, int err_len) {
|
|
1164
|
-
struct pollfd pfd;
|
|
1165
|
-
int fd = serial_fd_of(h, err, err_len);
|
|
1166
|
-
int pr;
|
|
1167
|
-
ssize_t n;
|
|
1168
|
-
|
|
1169
|
-
if (fd < 0) return -4;
|
|
1170
|
-
|
|
1171
|
-
/* poll 在锁外等:同一把锁若被「某个句柄的读超时」占住,别的串口就全废了 */
|
|
1172
|
-
pfd.fd = fd;
|
|
1173
|
-
pfd.events = POLLIN;
|
|
1174
|
-
pfd.revents = 0;
|
|
1175
|
-
pr = poll(&pfd, 1, timeout_ms > 0 ? timeout_ms : 0);
|
|
1176
|
-
if (pr < 0) {
|
|
1177
|
-
if (errno == EINTR) return 0; /* 被打断按「本轮无数据」处理,由上层决定要不要再读 */
|
|
1178
|
-
serial_err(err, err_len, "serial: poll 失败: %s", strerror(errno));
|
|
1179
|
-
return -3;
|
|
1180
|
-
}
|
|
1181
|
-
if (pr == 0) return 0; /* 超时:确实没数据,不是错误 */
|
|
1182
|
-
|
|
1183
|
-
n = read(fd, buf, (size_t)len);
|
|
1184
|
-
if (n < 0) {
|
|
1185
|
-
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return 0;
|
|
1186
|
-
serial_err(err, err_len, "serial: read 失败: %s", strerror(errno));
|
|
1187
|
-
return -3;
|
|
1188
|
-
}
|
|
1189
|
-
return (int)n; /* n == 0:对端已关闭(pty 侧退出)*/
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
int cj_plat_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms,
|
|
1193
|
-
char *err, int err_len) {
|
|
1194
|
-
struct pollfd pfd;
|
|
1195
|
-
long long deadline = serial_now_ms() + (timeout_ms > 0 ? (long long)timeout_ms : 0);
|
|
1196
|
-
int budget = timeout_ms > 0 ? timeout_ms : 0;
|
|
1197
|
-
int fd = serial_fd_of(h, err, err_len);
|
|
1198
|
-
int written = 0;
|
|
1199
|
-
|
|
1200
|
-
if (fd < 0) return -4;
|
|
1201
|
-
|
|
1202
|
-
/* 有界写完:每轮 poll 只等「剩余预算」,所以总等待不会超出 timeout_ms(不是每轮都等满一遍) */
|
|
1203
|
-
while (written < len) {
|
|
1204
|
-
ssize_t n;
|
|
1205
|
-
int pr;
|
|
1206
|
-
pfd.fd = fd;
|
|
1207
|
-
pfd.events = POLLOUT;
|
|
1208
|
-
pfd.revents = 0;
|
|
1209
|
-
pr = poll(&pfd, 1, budget);
|
|
1210
|
-
if (pr < 0) {
|
|
1211
|
-
if (errno == EINTR) continue;
|
|
1212
|
-
serial_err(err, err_len, "serial: poll(POLLOUT) 失败: %s", strerror(errno));
|
|
1213
|
-
return written > 0 ? written : -3;
|
|
1214
|
-
}
|
|
1215
|
-
if (pr == 0) break; /* 预算用尽:返回已写入量,由上层决定要不要再写 */
|
|
1216
|
-
|
|
1217
|
-
n = write(fd, buf + written, (size_t)(len - written));
|
|
1218
|
-
if (n < 0) {
|
|
1219
|
-
if (errno == EINTR) continue;
|
|
1220
|
-
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
1221
|
-
if (budget <= 0) break; /* 不忙等:预算已尽就交回上层 */
|
|
1222
|
-
budget = (int)(deadline - serial_now_ms());
|
|
1223
|
-
if (budget < 0) budget = 0;
|
|
1224
|
-
continue;
|
|
1225
|
-
}
|
|
1226
|
-
serial_err(err, err_len, "serial: write 失败: %s", strerror(errno));
|
|
1227
|
-
return written > 0 ? written : -3;
|
|
1228
|
-
}
|
|
1229
|
-
written += (int)n;
|
|
1230
|
-
budget = (int)(deadline - serial_now_ms());
|
|
1231
|
-
if (budget < 0) budget = 0;
|
|
1232
|
-
}
|
|
1233
|
-
return written;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
int cj_plat_serial_close(long long h, char *err, int err_len) {
|
|
1237
|
-
int fd = -1;
|
|
1238
|
-
int i;
|
|
1239
|
-
|
|
1240
|
-
pthread_mutex_lock(&g_serial_lock);
|
|
1241
|
-
for (i = 0; i < CJ_SERIAL_MAX_HANDLES; i++) {
|
|
1242
|
-
if (g_serial_slots[i].used && g_serial_slots[i].id == h) {
|
|
1243
|
-
fd = g_serial_slots[i].fd;
|
|
1244
|
-
g_serial_slots[i].used = 0;
|
|
1245
|
-
g_serial_slots[i].id = 0;
|
|
1246
|
-
g_serial_slots[i].fd = -1;
|
|
1247
|
-
g_serial_slots[i].path[0] = '\0';
|
|
1248
|
-
break;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
pthread_mutex_unlock(&g_serial_lock);
|
|
1252
|
-
|
|
1253
|
-
if (fd < 0) {
|
|
1254
|
-
serial_err(err, err_len, "serial: 句柄 %lld 无效(未打开或已关闭)", h);
|
|
1255
|
-
return -4;
|
|
1256
|
-
}
|
|
1257
|
-
close(fd); /* close 放锁外:它可能触发 pty 侧的清理,不该占着表锁 */
|
|
1258
|
-
return 0;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
/* Linux 侧不实现 C 桥枚举:serial:list 仍走仓颉侧的 /dev 目录扫描(pty / by-id 都能收),
|
|
1262
|
-
这里按「未实现」口径返回 -4,core 原样交回。 */
|
|
1263
|
-
int cj_plat_serial_list(char *out, int out_len) {
|
|
1264
|
-
if (out && out_len > 0) {
|
|
1265
|
-
serial_err(out, out_len, "%s", "serial: list 在本平台未实现(走仓颉侧 /dev 扫描)");
|
|
1266
|
-
}
|
|
1267
|
-
return -4;
|
|
1268
|
-
}
|