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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
package cjTauri.plugins.sqlite
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SQLite C API 的 FFI 声明与最小包装(`sqlite` 插件的地基)
|
|
7
|
+
*
|
|
8
|
+
* **为什么直连系统库而不进 C 桥**(docs/设计决策记录.md D-018 第 2 条):现有 `cj_bridge_*` 之所以存在,
|
|
9
|
+
* 是因为平台差异集中在 C 桥里;SQLite 相反——三平台同一套 C API、没有平台分支,再包一层只是多一份要
|
|
10
|
+
* 维护的转发代码。代价是「链接谁」从「桥提供」变成「消费方各自提供」:Linux 链发行版系统库
|
|
11
|
+
* (`-lsqlite3`,本机 3.45.1),Windows 侧未落地(见 D-018 第 8 条)。
|
|
12
|
+
*
|
|
13
|
+
* **FFI 语法是实测出来的**(三条都进 AGENTS §4):
|
|
14
|
+
* 1. 仓颉**没有** `&x` 取地址(写它会报 `expected expression after ','`);C 出参在**调用处**写
|
|
15
|
+
* `inout <var>`,自动取址成 `CPointer<T>`——修饰对象只能是 `var`,且指针仅在本次调用期间有效。
|
|
16
|
+
* 2. 「可选传 NULL 的 C 字符串」(这里的 `zVfs`)声明成 `CPointer<UInt8>` 并传 `CPointer<UInt8>()`;
|
|
17
|
+
* 传空 `CString` 会被 SQLite 当成「名字为空的 VFS」,报的是 `no such vfs`(实测踩过)。
|
|
18
|
+
* 3. C 回调声明成 `CFunc<(...) -> 返回类型>`,回调本体是顶层 `@C func`(本仓 `cj_bridge_init` 先例)。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** SQLite 返回码(sqlite3.h 的子集,只列我们判过的) */
|
|
22
|
+
let SQLITE_OK: Int32 = 0
|
|
23
|
+
let SQLITE_ROW: Int32 = 100
|
|
24
|
+
let SQLITE_DONE: Int32 = 101
|
|
25
|
+
/** sqlite3_exec 在授权回调拒绝时给的是 SQLITE_AUTH(列出来便于断言时看明白) */
|
|
26
|
+
let SQLITE_AUTH: Int32 = 23
|
|
27
|
+
|
|
28
|
+
/** 列类型(sqlite3_column_type):界面要能说清「这格是数字还是文本」,所以逐格判 */
|
|
29
|
+
let SQLITE_INTEGER: Int32 = 1
|
|
30
|
+
let SQLITE_FLOAT: Int32 = 2
|
|
31
|
+
let SQLITE_TEXT: Int32 = 3
|
|
32
|
+
let SQLITE_BLOB: Int32 = 4
|
|
33
|
+
let SQLITE_NULL: Int32 = 5
|
|
34
|
+
|
|
35
|
+
/** 打开标志(sqlite3_open_v2):只读走 READONLY,写路径要显式给 READWRITE */
|
|
36
|
+
let SQLITE_OPEN_READONLY: Int32 = 1
|
|
37
|
+
let SQLITE_OPEN_READWRITE: Int32 = 2
|
|
38
|
+
let SQLITE_OPEN_CREATE: Int32 = 4
|
|
39
|
+
|
|
40
|
+
/** 授权动作码(sqlite3_set_authorizer):只列我们要拒的三类 */
|
|
41
|
+
let SQLITE_ATTACH: Int32 = 24
|
|
42
|
+
let SQLITE_DETACH: Int32 = 25
|
|
43
|
+
let SQLITE_FUNCTION: Int32 = 31
|
|
44
|
+
|
|
45
|
+
/** 授权回调返回值 */
|
|
46
|
+
let SQLITE_DENY: Int32 = 1
|
|
47
|
+
|
|
48
|
+
/** busy 等待上限(毫秒):另一个进程正写这个库时等着,而不是当场报 SQLITE_BUSY */
|
|
49
|
+
let SQLITE_BUSY_TIMEOUT_MS: Int32 = 3000
|
|
50
|
+
|
|
51
|
+
@C
|
|
52
|
+
foreign func sqlite3_libversion(): CString
|
|
53
|
+
|
|
54
|
+
@C
|
|
55
|
+
foreign func sqlite3_open_v2(filename: CString, ppDb: CPointer<CPointer<Unit>>, flags: Int32,
|
|
56
|
+
zVfs: CPointer<UInt8>): Int32
|
|
57
|
+
|
|
58
|
+
@C
|
|
59
|
+
foreign func sqlite3_close(db: CPointer<Unit>): Int32
|
|
60
|
+
|
|
61
|
+
@C
|
|
62
|
+
foreign func sqlite3_errmsg(db: CPointer<Unit>): CString
|
|
63
|
+
|
|
64
|
+
@C
|
|
65
|
+
foreign func sqlite3_exec(db: CPointer<Unit>, sql: CString, cb: CPointer<Unit>, arg: CPointer<Unit>,
|
|
66
|
+
errmsg: CPointer<Unit>): Int32
|
|
67
|
+
|
|
68
|
+
@C
|
|
69
|
+
foreign func sqlite3_changes(db: CPointer<Unit>): Int32
|
|
70
|
+
|
|
71
|
+
@C
|
|
72
|
+
foreign func sqlite3_last_insert_rowid(db: CPointer<Unit>): Int64
|
|
73
|
+
|
|
74
|
+
@C
|
|
75
|
+
foreign func sqlite3_busy_timeout(db: CPointer<Unit>, ms: Int32): Int32
|
|
76
|
+
|
|
77
|
+
@C
|
|
78
|
+
foreign func sqlite3_prepare_v2(db: CPointer<Unit>, sql: CString, nByte: Int32,
|
|
79
|
+
ppStmt: CPointer<CPointer<Unit>>, pzTail: CPointer<Unit>): Int32
|
|
80
|
+
|
|
81
|
+
@C
|
|
82
|
+
foreign func sqlite3_step(stmt: CPointer<Unit>): Int32
|
|
83
|
+
|
|
84
|
+
@C
|
|
85
|
+
foreign func sqlite3_finalize(stmt: CPointer<Unit>): Int32
|
|
86
|
+
|
|
87
|
+
@C
|
|
88
|
+
foreign func sqlite3_column_count(stmt: CPointer<Unit>): Int32
|
|
89
|
+
|
|
90
|
+
@C
|
|
91
|
+
foreign func sqlite3_column_name(stmt: CPointer<Unit>, i: Int32): CString
|
|
92
|
+
|
|
93
|
+
@C
|
|
94
|
+
foreign func sqlite3_column_type(stmt: CPointer<Unit>, i: Int32): Int32
|
|
95
|
+
|
|
96
|
+
@C
|
|
97
|
+
foreign func sqlite3_column_int64(stmt: CPointer<Unit>, i: Int32): Int64
|
|
98
|
+
|
|
99
|
+
@C
|
|
100
|
+
foreign func sqlite3_column_double(stmt: CPointer<Unit>, i: Int32): Float64
|
|
101
|
+
|
|
102
|
+
@C
|
|
103
|
+
foreign func sqlite3_column_text(stmt: CPointer<Unit>, i: Int32): CString
|
|
104
|
+
|
|
105
|
+
@C
|
|
106
|
+
foreign func sqlite3_column_bytes(stmt: CPointer<Unit>, i: Int32): Int32
|
|
107
|
+
|
|
108
|
+
@C
|
|
109
|
+
foreign func sqlite3_set_authorizer(
|
|
110
|
+
db: CPointer<Unit>,
|
|
111
|
+
cb: CFunc<(CPointer<Unit>, Int32, CString, CString, CString, CString) -> Int32>,
|
|
112
|
+
userdata: CPointer<Unit>): Int32
|
|
113
|
+
|
|
114
|
+
@C
|
|
115
|
+
foreign func sqlite3_enable_load_extension(db: CPointer<Unit>, onoff: Int32): Int32
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 授权回调:拒掉「跑到别的文件去」的动作——`ATTACH` / `DETACH` 换库文件、
|
|
119
|
+
* `load_extension()` 加载任意原生库。**必须按语句判**:路径 scope 只管得住本次命令收到的那个路径,
|
|
120
|
+
* 管不住 SQL 里写死的另一个路径(D-018 第 4 条)。被拒时留一行 stderr,取证时能对上账。
|
|
121
|
+
*/
|
|
122
|
+
@C
|
|
123
|
+
func sqliteAuthorizer(app: CPointer<Unit>, code: Int32, arg1: CString, arg2: CString,
|
|
124
|
+
dbName: CString, triggerName: CString): Int32 {
|
|
125
|
+
let _ = app
|
|
126
|
+
let _ = arg2
|
|
127
|
+
let _ = dbName
|
|
128
|
+
let _ = triggerName
|
|
129
|
+
if (code == SQLITE_ATTACH || code == SQLITE_DETACH) {
|
|
130
|
+
eprintln("[cj-tauri] sqlite denied: attach/detach is not allowed (code=${code})")
|
|
131
|
+
return SQLITE_DENY
|
|
132
|
+
}
|
|
133
|
+
if (code == SQLITE_FUNCTION) {
|
|
134
|
+
// arg1 是被调用的函数名:只拒能加载原生代码的那个,其余内置函数照常放行
|
|
135
|
+
let fnName = sqliteCString(arg1)
|
|
136
|
+
if (fnName == "load_extension") {
|
|
137
|
+
eprintln("[cj-tauri] sqlite denied: load_extension() is not allowed")
|
|
138
|
+
return SQLITE_DENY
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return SQLITE_OK
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** C 字符串 → 仓颉字符串;NULL 给空串(调用方要区分 NULL 与空串时另判归属) */
|
|
145
|
+
func sqliteCString(s: CString): String {
|
|
146
|
+
if (s.isNull()) {
|
|
147
|
+
return ""
|
|
148
|
+
}
|
|
149
|
+
return s.toString()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 取最近一次错误文案;db 为空时给固定文案,避免收尾路径上再炸一次 */
|
|
153
|
+
func sqliteLastError(db: CPointer<Unit>): String {
|
|
154
|
+
if (db.isNull()) {
|
|
155
|
+
return "unknown error (null db handle)"
|
|
156
|
+
}
|
|
157
|
+
return sqliteCString(unsafe { sqlite3_errmsg(db) })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 关掉句柄:对空指针安全(出错路径也要能收尾),关不掉只记一行 stderr 不抛 */
|
|
161
|
+
func sqliteCloseQuietly(db: CPointer<Unit>): Unit {
|
|
162
|
+
if (db.isNull()) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
let rc = unsafe { sqlite3_close(db) }
|
|
166
|
+
if (rc != SQLITE_OK) {
|
|
167
|
+
eprintln("[cj-tauri] sqlite close returned ${rc}")
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 打开数据库并装上护栏(每次命令短连接,见 D-018 第 9 条)。
|
|
173
|
+
*
|
|
174
|
+
* `readonly=true` 走 `SQLITE_OPEN_READONLY`——只读是**内核级**的,不是我们写的检查;
|
|
175
|
+
* 不存在的文件 + 只读 = 打开失败(不会顺手造一个空库)。
|
|
176
|
+
* 打开失败与护栏装不上都抛 `CommandException`(前端拿到可读文案)。
|
|
177
|
+
*/
|
|
178
|
+
func sqliteOpenGuarded(path: String, readonly: Bool): CPointer<Unit> {
|
|
179
|
+
var db: CPointer<Unit> = CPointer<Unit>()
|
|
180
|
+
let flags = if (readonly) {
|
|
181
|
+
SQLITE_OPEN_READONLY
|
|
182
|
+
} else {
|
|
183
|
+
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
|
|
184
|
+
}
|
|
185
|
+
// inout db 取的是「栈上那个指针变量」的地址:C 侧写入的 handle 直接落到 db 上
|
|
186
|
+
let rc = unsafe { sqlite3_open_v2(LibC.mallocCString(path), inout db, flags, CPointer<UInt8>()) }
|
|
187
|
+
if (rc != SQLITE_OK) {
|
|
188
|
+
let msg = sqliteLastError(db)
|
|
189
|
+
sqliteCloseQuietly(db)
|
|
190
|
+
throw CommandException("sqlite: cannot open ${path}: ${msg}")
|
|
191
|
+
}
|
|
192
|
+
// 双保险之一:先关掉扩展加载(authorizer 之外再加一道)
|
|
193
|
+
let _ = unsafe { sqlite3_enable_load_extension(db, 0) }
|
|
194
|
+
let rcAuthorizer = unsafe { sqlite3_set_authorizer(db, sqliteAuthorizer, CPointer<Unit>()) }
|
|
195
|
+
if (rcAuthorizer != SQLITE_OK) {
|
|
196
|
+
sqliteCloseQuietly(db)
|
|
197
|
+
throw CommandException("sqlite: cannot install authorizer (rc=${rcAuthorizer})")
|
|
198
|
+
}
|
|
199
|
+
let _ = unsafe { sqlite3_busy_timeout(db, SQLITE_BUSY_TIMEOUT_MS) }
|
|
200
|
+
return db
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 本机链接的 SQLite 版本(`sqlite:info` 回投给页面,对账时也用它) */
|
|
204
|
+
func sqliteLibVersion(): String {
|
|
205
|
+
return sqliteCString(unsafe { sqlite3_libversion() })
|
|
206
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
package cjTauri.plugins.store
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import std.fs.*
|
|
7
|
+
import std.sync.*
|
|
8
|
+
import stdx.encoding.json.*
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 官方插件 store:持久化键值存储(纯仓颉,零 C 桥;RFC-003 第 2 期,对标 tauri-plugin-store)
|
|
12
|
+
*
|
|
13
|
+
* 命令:store:get / store:set / store:delete / store:keys / store:clear
|
|
14
|
+
* 权限:**不自动放行**,应用要在 capabilities 里显式声明,两种写法等价:
|
|
15
|
+
* "commands": ["store:get", "store:keys"] // 明文逐条
|
|
16
|
+
* "permissions": ["store:readonly"] // 命名权限集(get + keys,一次放行读那组)
|
|
17
|
+
* 前端:window.__CJ_TAURI__.store.set({ key: "theme", value: "dark" })
|
|
18
|
+
*
|
|
19
|
+
* 值是**任意 JSON**(对象/数组/标量都行)——落盘形状就是 `{ key: value, ... }` 一个 JSON 对象。
|
|
20
|
+
* 落盘位置:`StorePlugin()` 默认 `<工作目录>/store.json`(与 capabilities/ 同一套项目根约定),
|
|
21
|
+
* `StorePlugin(path)` 自定义。v1 一应用一个 store(插件按 name 装配,多实例会撞名)。
|
|
22
|
+
*
|
|
23
|
+
* 原子写:先写 `path.tmp` 再 `rename(overwrite: true)`,进程中途被杀也不会留下半个 JSON;
|
|
24
|
+
* rename 失败回落直写并打 stderr(与 examples/music 的手搓存储同级兜底)。
|
|
25
|
+
* 损坏文件不是致命错误:读失败一行 stderr + 从空开始,下次 save 覆盖掉坏文件。
|
|
26
|
+
*/
|
|
27
|
+
public class StorePlugin <: Plugin {
|
|
28
|
+
private let store: StoreBackend
|
|
29
|
+
|
|
30
|
+
public init() {
|
|
31
|
+
this.store = StoreBackend("store.json")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public init(path: String) {
|
|
35
|
+
this.store = StoreBackend(path)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public func name(): String {
|
|
39
|
+
return "store"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
43
|
+
let m = HashMap<String, CommandHandler>()
|
|
44
|
+
m.add("get", StoreCommands(this.store))
|
|
45
|
+
m.add("set", StoreCommands(this.store))
|
|
46
|
+
m.add("delete", StoreCommands(this.store))
|
|
47
|
+
m.add("keys", StoreCommands(this.store))
|
|
48
|
+
m.add("clear", StoreCommands(this.store))
|
|
49
|
+
return m
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 命名权限集:readonly 只含「读」两条(get + keys)——只想读的应用引用它即可,
|
|
54
|
+
* **写能力不会被顺带打开**;要写必须显式引用 default 或把写命令写成明文。
|
|
55
|
+
*/
|
|
56
|
+
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
57
|
+
let m = HashMap<String, ArrayList<String>>()
|
|
58
|
+
let ro = ArrayList<String>()
|
|
59
|
+
ro.add("get")
|
|
60
|
+
ro.add("keys")
|
|
61
|
+
m.add("readonly", ro)
|
|
62
|
+
let all = ArrayList<String>()
|
|
63
|
+
all.add("get")
|
|
64
|
+
all.add("set")
|
|
65
|
+
all.add("delete")
|
|
66
|
+
all.add("keys")
|
|
67
|
+
all.add("clear")
|
|
68
|
+
m.add("default", all)
|
|
69
|
+
return m
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 前端 shim:只往 __CJ_TAURI__ 挂一个 store 命名空间,不动 invoke / listen / emit
|
|
74
|
+
*/
|
|
75
|
+
public func jsShim(): String {
|
|
76
|
+
return "(function () { var t = window.__CJ_TAURI__ || (window.__CJ_TAURI__ = {});" +
|
|
77
|
+
" t.store = {" +
|
|
78
|
+
" get: function (a) { return window.__CJ_TAURI__.invoke('store:get', a); }," +
|
|
79
|
+
" set: function (a) { return window.__CJ_TAURI__.invoke('store:set', a); }," +
|
|
80
|
+
" del: function (a) { return window.__CJ_TAURI__.invoke('store:delete', a); }," +
|
|
81
|
+
" keys: function () { return window.__CJ_TAURI__.invoke('store:keys', {}); }," +
|
|
82
|
+
" clear: function () { return window.__CJ_TAURI__.invoke('store:clear', {}); }" +
|
|
83
|
+
" }; })();\n"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 存储本体:内存 JsonObject + 锁 + 原子落盘。
|
|
89
|
+
*
|
|
90
|
+
* 独立成类而不是塞进命令处理器里:五条命令共享同一份内存与文件(命令处理器是按命令各 new 的),
|
|
91
|
+
* 单测也要能直接驱动它(并发写 / 损坏文件回落都不必过 IPC)。
|
|
92
|
+
*/
|
|
93
|
+
public class StoreBackend {
|
|
94
|
+
private var path: String
|
|
95
|
+
private var root: JsonObject
|
|
96
|
+
// 命令跑在 worker 线程上:内存对象与「读-改-写盘」必须整块互斥,否则并发 set 会互相覆盖
|
|
97
|
+
private let lock: Mutex = Mutex()
|
|
98
|
+
|
|
99
|
+
public init(path: String) {
|
|
100
|
+
this.path = path
|
|
101
|
+
this.root = JsonObject()
|
|
102
|
+
this.load()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public func pathOf(): String {
|
|
106
|
+
return this.path
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 读盘:读不到(第一次跑)与读坏都不是致命错误——后者一行 stderr 再从空开始(music 先例) */
|
|
110
|
+
private func load(): Unit {
|
|
111
|
+
try {
|
|
112
|
+
let text = String.fromUtf8(File.readFrom(this.path))
|
|
113
|
+
let parsed = JsonValue.fromStr(text)
|
|
114
|
+
match (parsed.kind()) {
|
|
115
|
+
case JsonKind.JsObject => this.root = parsed.asObject()
|
|
116
|
+
case _ => eprintln("[cj-tauri] store 文件不是 JSON 对象,本轮从空开始:${this.path}")
|
|
117
|
+
}
|
|
118
|
+
} catch (e: Exception) {
|
|
119
|
+
// 第一次跑就是这条路径——没有 store 文件是正常状态,不是错误
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 原子落盘:写 tmp → rename 覆盖。rename 失败回落直写(保住「能存上」这个主目标),
|
|
125
|
+
* 两种失败都留 stderr——写盘失败必须可观测,不能让「本轮只在内存里」无声发生。
|
|
126
|
+
*/
|
|
127
|
+
private func saveLocked(): Bool {
|
|
128
|
+
let text = this.root.toJsonString()
|
|
129
|
+
let tmp = this.path + ".tmp"
|
|
130
|
+
try {
|
|
131
|
+
File.writeTo(tmp, text.toArray())
|
|
132
|
+
try {
|
|
133
|
+
rename(tmp, to: this.path, overwrite: true)
|
|
134
|
+
} catch (e: Exception) {
|
|
135
|
+
// rename 失败(跨目录/被占用等):回落直写目标文件,别让数据丢在 tmp 里
|
|
136
|
+
File.writeTo(this.path, text.toArray())
|
|
137
|
+
eprintln("[cj-tauri] store 原子替换失败已回落直写:${e}")
|
|
138
|
+
}
|
|
139
|
+
return true
|
|
140
|
+
} catch (e: Exception) {
|
|
141
|
+
eprintln("[cj-tauri] store 写盘失败:${e}(路径 ${this.path},本轮改动只在内存里)")
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 读一个键。缺失时:给了 default 回 default,没给回 JSON null
|
|
148
|
+
* (用 fromStr("null") 构造——JsonNull 的构造契约未在仓内核实过,不猜)。
|
|
149
|
+
*/
|
|
150
|
+
public func get(key: String, hasDefault: Bool, defaultValue: JsonValue): JsonValue {
|
|
151
|
+
synchronized(this.lock) {
|
|
152
|
+
match (this.root.get(key)) {
|
|
153
|
+
case Some(v) => return v
|
|
154
|
+
case None =>
|
|
155
|
+
if (hasDefault) {
|
|
156
|
+
return defaultValue
|
|
157
|
+
}
|
|
158
|
+
return JsonValue.fromStr("null")
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 写一个键并落盘;返回落盘是否成功(false = 本轮只在内存里,页面据此提示) */
|
|
164
|
+
public func set(key: String, value: JsonValue): Bool {
|
|
165
|
+
synchronized(this.lock) {
|
|
166
|
+
this.root.put(key, value)
|
|
167
|
+
return this.saveLocked()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 删一个键并落盘;返回 (是否存在过, 落盘是否成功)——页面据此区分「删了」与「本来就没有」 */
|
|
172
|
+
public func delete(key: String): (Bool, Bool) {
|
|
173
|
+
synchronized(this.lock) {
|
|
174
|
+
// getFields + HashMap.remove + 重建:不管 getFields 给的是拷贝还是内部表,结果都正确
|
|
175
|
+
let fields = this.root.getFields()
|
|
176
|
+
let removed = fields.remove(key)
|
|
177
|
+
this.root = JsonObject(fields)
|
|
178
|
+
if (removed.isNone()) {
|
|
179
|
+
return (false, true)
|
|
180
|
+
}
|
|
181
|
+
return (true, this.saveLocked())
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 全部键,按字典序(输出稳定、可断言) */
|
|
186
|
+
public func keys(): ArrayList<String> {
|
|
187
|
+
synchronized(this.lock) {
|
|
188
|
+
let names = ArrayList<String>()
|
|
189
|
+
for ((k, _) in this.root.getFields()) {
|
|
190
|
+
names.add(k)
|
|
191
|
+
}
|
|
192
|
+
return sortedStrings(names)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 清空并落盘;返回清掉的键数与落盘是否成功 */
|
|
197
|
+
public func clear(): (Int64, Bool) {
|
|
198
|
+
synchronized(this.lock) {
|
|
199
|
+
let n = this.root.size()
|
|
200
|
+
this.root = JsonObject()
|
|
201
|
+
if (n == 0) {
|
|
202
|
+
return (0, true)
|
|
203
|
+
}
|
|
204
|
+
return (n, this.saveLocked())
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** store 的五条命令:分发 + 参数校验,逻辑都在 StoreBackend 里 */
|
|
210
|
+
public class StoreCommands <: CommandHandler {
|
|
211
|
+
private let store: StoreBackend
|
|
212
|
+
|
|
213
|
+
public init(store: StoreBackend) {
|
|
214
|
+
this.store = store
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
218
|
+
let _ = ipc // store 的命令都是纯本地读写,不回头投事件,用不上上下文
|
|
219
|
+
match (cmd) {
|
|
220
|
+
case "store:get" => this.getCmd(args)
|
|
221
|
+
case "store:set" => this.setCmd(args)
|
|
222
|
+
case "store:delete" => this.deleteCmd(args)
|
|
223
|
+
case "store:keys" => this.keysCmd()
|
|
224
|
+
case "store:clear" => this.clearCmd()
|
|
225
|
+
case _ => throw CommandException("unknown store command: ${cmd}")
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private func requireKey(args: JsonObject): String {
|
|
230
|
+
match (args.get("key")) {
|
|
231
|
+
case Some(v) =>
|
|
232
|
+
match (v.kind()) {
|
|
233
|
+
case JsonKind.JsString =>
|
|
234
|
+
let k = v.asString().getValue()
|
|
235
|
+
if (k.isEmpty()) {
|
|
236
|
+
throw CommandException("store: key 不能为空字符串")
|
|
237
|
+
}
|
|
238
|
+
return k
|
|
239
|
+
case _ => throw CommandException("store: 参数 \"key\" 必须是字符串")
|
|
240
|
+
}
|
|
241
|
+
case None => throw CommandException("store: 缺少参数 \"key\"")
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private func getCmd(args: JsonObject): JsonValue {
|
|
246
|
+
let key = this.requireKey(args)
|
|
247
|
+
match (args.get("default")) {
|
|
248
|
+
case Some(d) => return this.store.get(key, true, d)
|
|
249
|
+
case None => return this.store.get(key, false, JsonBool(false))
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private func setCmd(args: JsonObject): JsonValue {
|
|
254
|
+
let key = this.requireKey(args)
|
|
255
|
+
// 值是任意 JSON;没传 value 视为编程错误直接拒(想存 null 就显式传 value: null)
|
|
256
|
+
match (args.get("value")) {
|
|
257
|
+
case Some(v) => return JsonBool(this.store.set(key, v))
|
|
258
|
+
case None => throw CommandException("store: 缺少参数 \"value\"(想存 null 就显式传 value: null)")
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private func deleteCmd(args: JsonObject): JsonValue {
|
|
263
|
+
let key = this.requireKey(args)
|
|
264
|
+
let (existed, saved) = this.store.delete(key)
|
|
265
|
+
let out = JsonObject()
|
|
266
|
+
out.put("existed", JsonBool(existed))
|
|
267
|
+
out.put("saved", JsonBool(saved))
|
|
268
|
+
return out
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private func keysCmd(): JsonValue {
|
|
272
|
+
let names = this.store.keys()
|
|
273
|
+
let arr = ArrayList<JsonValue>()
|
|
274
|
+
for (k in names) {
|
|
275
|
+
arr.add(JsonString(k))
|
|
276
|
+
}
|
|
277
|
+
return JsonArray(arr)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func clearCmd(): JsonValue {
|
|
281
|
+
let (cleared, saved) = this.store.clear()
|
|
282
|
+
let out = JsonObject()
|
|
283
|
+
out.put("cleared", JsonInt(cleared))
|
|
284
|
+
out.put("saved", JsonBool(saved))
|
|
285
|
+
return out
|
|
286
|
+
}
|
|
287
|
+
}
|