@yufengtadian/freedom-cli 1.12.18 → 1.13.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.
- package/README.md +11 -3
- package/lib/build.js +1 -1
- package/lib/security.js +2 -2
- package/package.json +5 -1
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/go.mod +1 -7
- package/templates/go/go.sum +2 -2
- package/templates/go/main.go +22 -8
- package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
- package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
- package/templates/go/pkg/freedom/assets/freedom.js +200 -5
- package/templates/go/pkg/freedom/assets_embed.go +11 -13
- package/templates/go/pkg/freedom/authenticode_other.go +11 -0
- package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
- package/templates/go/pkg/freedom/backend_proc.go +291 -53
- package/templates/go/pkg/freedom/bridge.go +13 -2
- package/templates/go/pkg/freedom/capability.go +77 -0
- package/templates/go/pkg/freedom/center_other.go +3 -0
- package/templates/go/pkg/freedom/center_windows.go +45 -35
- package/templates/go/pkg/freedom/dispatch.go +60 -0
- package/templates/go/pkg/freedom/events_windows.go +319 -0
- package/templates/go/pkg/freedom/freedom.go +193 -99
- package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
- package/templates/go/pkg/freedom/osver_other.go +15 -0
- package/templates/go/pkg/freedom/osver_windows.go +121 -0
- package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
- package/templates/go/pkg/freedom/{configfile.go → resources.go} +38 -35
- package/templates/go/pkg/freedom/security.go +19 -12
- package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
- package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
- package/templates/go/pkg/freedom/spawn_other.go +11 -0
- package/templates/go/pkg/freedom/spawn_windows.go +23 -0
- package/templates/go/pkg/freedom/store.go +458 -0
- package/templates/go/pkg/freedom/syscap_linux.go +278 -0
- package/templates/go/pkg/freedom/syscap_other.go +15 -3
- package/templates/go/pkg/freedom/syscap_windows.go +228 -36
- package/templates/go/pkg/freedom/sysint_common.go +70 -0
- package/templates/go/pkg/freedom/sysint_windows.go +426 -0
- package/templates/go/pkg/freedom/tray_common.go +12 -0
- package/templates/go/pkg/freedom/tray_linux.go +302 -0
- package/templates/go/pkg/freedom/tray_other.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +69 -39
- package/templates/go/pkg/freedom/updater.go +372 -0
- package/templates/go/pkg/freedom/window_mgr.go +385 -0
- package/templates/go/pkg/freedom/window_other.go +32 -66
- package/templates/go/pkg/freedom/window_windows.go +397 -254
- package/templates/go/build_all.txt +0 -0
- package/templates/go/build_err.txt +0 -0
- package/templates/go/gen_config.go +0 -22
- package/templates/go/pkg/freedom/center_windows_test.go +0 -32
- package/templates/go/pkg/freedom/security_test.go +0 -153
- package/templates/go/temp_vet_test/main.go +0 -40
- package/templates/go/test_err.txt +0 -0
- package/templates/go/vet_err.txt +0 -1
- package/templates/go/vet_err2.txt +0 -2
- package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
- package/templates/go/webview_go/CHANGELOG.md +0 -15
- package/templates/go/webview_go/LICENSE +0 -22
- package/templates/go/webview_go/README.md +0 -50
- package/templates/go/webview_go/examples/basic/main.go +0 -12
- package/templates/go/webview_go/examples/bind/main.go +0 -38
- package/templates/go/webview_go/glue.c +0 -36
- package/templates/go/webview_go/go.mod +0 -3
- package/templates/go/webview_go/go.sum +0 -0
- package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
- package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
- package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
- package/templates/go/webview_go/libs/webview/LICENSE +0 -22
- package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
- package/templates/go/webview_go/libs/webview/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/version.txt +0 -1
- package/templates/go/webview_go/webview.cc +0 -1
- package/templates/go/webview_go/webview.go +0 -481
- package/templates/go/webview_go/webview_test.go +0 -50
|
@@ -3,6 +3,7 @@ package freedom
|
|
|
3
3
|
import (
|
|
4
4
|
"bufio"
|
|
5
5
|
"encoding/json"
|
|
6
|
+
"errors"
|
|
6
7
|
"fmt"
|
|
7
8
|
"io"
|
|
8
9
|
"os"
|
|
@@ -27,16 +28,46 @@ import (
|
|
|
27
28
|
// 壳启动后端进程时注入环境变量:FREEDOM_BACKEND=1、FREEDOM_IPC=stdio,
|
|
28
29
|
// 后端可据此判断自己运行在 Freedom 壳内。
|
|
29
30
|
type ProcBackend struct {
|
|
31
|
+
command []string // 启动 argv,重启策略据此重建子进程
|
|
32
|
+
dir string // 子进程工作目录(SetDir);空则继承壳进程当前目录
|
|
30
33
|
cmd *exec.Cmd
|
|
31
34
|
stdin io.WriteCloser
|
|
32
35
|
mu sync.Mutex
|
|
36
|
+
writeMu sync.Mutex // 串行化 stdin 写入;与 mu 分离,避免大消息阻塞写管道时与 readLoop/Close 形成环形死锁
|
|
33
37
|
pending map[int64]chan procResp
|
|
34
38
|
nextID int64
|
|
35
39
|
closed bool
|
|
36
40
|
onEvent func(event string, data interface{})
|
|
37
41
|
timeout time.Duration
|
|
42
|
+
maxLine int // 单条 stdout 行(一次响应)上限字节;超限即那条响应过大,不得中断整个通道
|
|
43
|
+
|
|
44
|
+
// —— W4 崩溃自愈(对标 Tauri shell spawn 的失败处理 / sidecar 重启)——
|
|
45
|
+
rp RestartPolicy // 重启策略(零值=不重启,保持旧行为)
|
|
46
|
+
gen int // 每次拉起递增;readLoop 据此识别"自己那一期",过期实例不触发重启
|
|
47
|
+
pdone chan struct{} // 当前这期进程 Wait 完成后由 readLoop 关闭(Close 等它,不再自己 Wait,避免双重 Wait)
|
|
48
|
+
attempts int // 连续崩溃重启计数
|
|
49
|
+
startedAt time.Time // 本期进程启动时刻(存活够久则重置 attempts)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// RestartPolicy 是后端进程崩溃后的自动重启策略(经 SetRestartPolicy 启用)。
|
|
53
|
+
// 非零退出/被信号杀视为崩溃;退出码 0 视为后端主动结束,不重启。
|
|
54
|
+
type RestartPolicy struct {
|
|
55
|
+
// MaxRetries 是"连续崩溃"允许的最大重启次数;0 表示禁用重启。
|
|
56
|
+
MaxRetries int
|
|
57
|
+
// Backoff 是首次重启前的等待时间,其后按 2 倍指数退避,封顶 30s。
|
|
58
|
+
// <=0 时取默认 250ms。
|
|
59
|
+
Backoff time.Duration
|
|
60
|
+
// ResetAfter 是进程存活超过该时长后清零连续崩溃计数(默认 60s),
|
|
61
|
+
// 让"运行几小时后崩一次"不被历史崩溃拖入重启耗尽。
|
|
62
|
+
ResetAfter time.Duration
|
|
38
63
|
}
|
|
39
64
|
|
|
65
|
+
const (
|
|
66
|
+
defaultRestartBackoff = 250 * time.Millisecond
|
|
67
|
+
defaultRestartResetAfter = 60 * time.Second
|
|
68
|
+
maxRestartBackoff = 30 * time.Second
|
|
69
|
+
)
|
|
70
|
+
|
|
40
71
|
// procResp 是一次调用在壳侧的等待结果。
|
|
41
72
|
type procResp struct {
|
|
42
73
|
result json.RawMessage
|
|
@@ -61,31 +92,64 @@ func NewProcBackend(command ...string) *ProcBackend {
|
|
|
61
92
|
if len(command) == 0 {
|
|
62
93
|
panic("freedom: NewProcBackend requires at least one command argument")
|
|
63
94
|
}
|
|
64
|
-
cmd := exec.Command(command[0], command[1:]...)
|
|
65
|
-
hideWindow(cmd) // Windows 下隐藏后端的 cmd 黑窗(跨平台空实现)
|
|
66
95
|
return &ProcBackend{
|
|
67
|
-
|
|
96
|
+
command: command,
|
|
68
97
|
pending: map[int64]chan procResp{},
|
|
69
98
|
timeout: 60 * time.Second,
|
|
99
|
+
maxLine: 16 * 1024 * 1024, // 默认单条响应上限 16MB(见 SetMaxLine)
|
|
70
100
|
}
|
|
71
101
|
}
|
|
72
102
|
|
|
73
|
-
// SetDir
|
|
74
|
-
//
|
|
75
|
-
// 通用壳在加载 resources/config.json 时自动设为 resources 目录。
|
|
103
|
+
// SetDir 设置后端子进程的工作目录(链式返回自身)。通用壳用它把后端 CWD 定到
|
|
104
|
+
// resources/,使 config.json 里的相对路径参数按 resources/ 解析。
|
|
76
105
|
func (p *ProcBackend) SetDir(dir string) *ProcBackend {
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
p.dir = dir
|
|
107
|
+
return p
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// SetRestartPolicy 启用后端进程崩溃自动重启(默认关闭)。
|
|
111
|
+
// 崩溃时向壳广播事件 "backend.crashed" {code, attempt, restarting},
|
|
112
|
+
// 重启成功拉起后广播 "backend.restarted" {attempt}。
|
|
113
|
+
func (p *ProcBackend) SetRestartPolicy(rp RestartPolicy) *ProcBackend {
|
|
114
|
+
if rp.Backoff <= 0 {
|
|
115
|
+
rp.Backoff = defaultRestartBackoff
|
|
116
|
+
}
|
|
117
|
+
if rp.ResetAfter <= 0 {
|
|
118
|
+
rp.ResetAfter = defaultRestartResetAfter
|
|
79
119
|
}
|
|
120
|
+
p.mu.Lock()
|
|
121
|
+
p.rp = rp
|
|
122
|
+
p.mu.Unlock()
|
|
80
123
|
return p
|
|
81
124
|
}
|
|
82
125
|
|
|
83
126
|
// SetTimeout 设置单次调用的最大等待时间(默认 60s)。<=0 表示不超时。
|
|
84
127
|
func (p *ProcBackend) SetTimeout(d time.Duration) *ProcBackend {
|
|
128
|
+
p.mu.Lock()
|
|
85
129
|
p.timeout = d
|
|
130
|
+
p.mu.Unlock()
|
|
86
131
|
return p
|
|
87
132
|
}
|
|
88
133
|
|
|
134
|
+
// SetMaxLine 设置单条后端响应(stdout 行)的字节上限(默认 16MB)。
|
|
135
|
+
// 超过上限的单条响应会被判为"行过大":仅该条调用收到错误,随后继续读取后续行,
|
|
136
|
+
// 不会像旧实现那样把 bufio.ErrTooLong 误判为"进程退出"而打崩整个 IPC 通道。
|
|
137
|
+
func (p *ProcBackend) SetMaxLine(n int) *ProcBackend {
|
|
138
|
+
if n > 0 {
|
|
139
|
+
p.mu.Lock()
|
|
140
|
+
p.maxLine = n
|
|
141
|
+
p.mu.Unlock()
|
|
142
|
+
}
|
|
143
|
+
return p
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// getMaxLine 读取行上限配置(readLoop 每行读取,与 SetMaxLine 并发)。
|
|
147
|
+
func (p *ProcBackend) getMaxLine() int {
|
|
148
|
+
p.mu.Lock()
|
|
149
|
+
defer p.mu.Unlock()
|
|
150
|
+
return p.maxLine
|
|
151
|
+
}
|
|
152
|
+
|
|
89
153
|
// OnEvent 注册后端进程推送事件时的回调(由框架在 Run 时注入)。
|
|
90
154
|
func (p *ProcBackend) OnEvent(fn func(event string, data interface{})) {
|
|
91
155
|
p.mu.Lock()
|
|
@@ -98,26 +162,42 @@ func (p *ProcBackend) start() error {
|
|
|
98
162
|
p.mu.Lock()
|
|
99
163
|
defer p.mu.Unlock()
|
|
100
164
|
if p.closed {
|
|
101
|
-
|
|
165
|
+
// closed 后再 start 会拉起一个永远无人 Wait/Kill 的孤儿子进程,必须拒绝。
|
|
166
|
+
return fmt.Errorf("freedom: proc backend already closed")
|
|
102
167
|
}
|
|
103
168
|
if p.stdin != nil {
|
|
104
169
|
return nil // 已启动
|
|
105
170
|
}
|
|
106
|
-
p.
|
|
107
|
-
|
|
108
|
-
|
|
171
|
+
return p.launchLocked()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// launchLocked 按 command 重建子进程并启动 readLoop。崩溃重启与首次启动共用。
|
|
175
|
+
// 调用方须持有 p.mu。
|
|
176
|
+
func (p *ProcBackend) launchLocked() error {
|
|
177
|
+
cmd := exec.Command(p.command[0], p.command[1:]...)
|
|
178
|
+
if p.dir != "" {
|
|
179
|
+
cmd.Dir = p.dir // 相对路径参数(backend/main.mjs)据此目录解析
|
|
180
|
+
}
|
|
181
|
+
hideWindow(cmd) // Windows 下隐藏后端进程的 cmd 黑窗(跨平台空实现)
|
|
182
|
+
cmd.Env = append(os.Environ(), "FREEDOM_BACKEND=1", "FREEDOM_IPC=stdio")
|
|
183
|
+
cmd.Stderr = os.Stderr // 后端 stderr 日志原样转发
|
|
184
|
+
stdin, err := cmd.StdinPipe()
|
|
109
185
|
if err != nil {
|
|
110
186
|
return fmt.Errorf("freedom: proc backend stdin pipe: %w", err)
|
|
111
187
|
}
|
|
112
|
-
stdout, err :=
|
|
188
|
+
stdout, err := cmd.StdoutPipe()
|
|
113
189
|
if err != nil {
|
|
114
190
|
return fmt.Errorf("freedom: proc backend stdout pipe: %w", err)
|
|
115
191
|
}
|
|
116
|
-
if err :=
|
|
117
|
-
return fmt.Errorf("freedom: proc backend start %q: %w",
|
|
192
|
+
if err := cmd.Start(); err != nil {
|
|
193
|
+
return fmt.Errorf("freedom: proc backend start %q: %w", cmd.Path, err)
|
|
118
194
|
}
|
|
195
|
+
p.cmd = cmd
|
|
119
196
|
p.stdin = stdin
|
|
120
|
-
|
|
197
|
+
p.gen++
|
|
198
|
+
p.pdone = make(chan struct{})
|
|
199
|
+
p.startedAt = time.Now()
|
|
200
|
+
go p.readLoop(p.gen, stdout, cmd, p.pdone)
|
|
121
201
|
return nil
|
|
122
202
|
}
|
|
123
203
|
|
|
@@ -137,23 +217,33 @@ func (p *ProcBackend) Handle(method string, params []json.RawMessage) (interface
|
|
|
137
217
|
id := p.nextID
|
|
138
218
|
ch := make(chan procResp, 1)
|
|
139
219
|
p.pending[id] = ch
|
|
140
|
-
w := p.stdin // 锁内取引用,锁外写,避免持锁阻塞(后端不消费 stdin 时 Write 可能挂起)
|
|
141
|
-
p.mu.Unlock()
|
|
142
|
-
|
|
143
220
|
msg := procMessage{ID: id, Method: method, Params: paramsJSON}
|
|
144
221
|
line, _ := json.Marshal(msg)
|
|
145
|
-
|
|
222
|
+
stdin := p.stdin
|
|
223
|
+
timeout := p.timeout // 在锁内快照,SetTimeout 并发改值不影响本次调用
|
|
224
|
+
p.mu.Unlock()
|
|
225
|
+
|
|
226
|
+
// 写管道不持 mu:大消息阻塞在 Write 时,readLoop/Close 仍可拿锁推进,
|
|
227
|
+
// 超时唤醒路径也不会被卡死(写失败由下方 cancel 兜底唤醒)。
|
|
228
|
+
p.writeMu.Lock()
|
|
229
|
+
_, err = stdin.Write(append(line, '\n'))
|
|
230
|
+
p.writeMu.Unlock()
|
|
231
|
+
|
|
232
|
+
if err != nil {
|
|
146
233
|
p.cancel(id, fmt.Errorf("freedom: proc backend write: %w", err))
|
|
147
234
|
return nil, err
|
|
148
235
|
}
|
|
149
236
|
|
|
150
237
|
var resp procResp
|
|
151
|
-
if
|
|
238
|
+
if timeout > 0 {
|
|
239
|
+
// NewTimer 便于超时后 Stop 释放;time.After 的 timer 会滞留到期才回收。
|
|
240
|
+
timer := time.NewTimer(timeout)
|
|
241
|
+
defer timer.Stop()
|
|
152
242
|
select {
|
|
153
243
|
case resp = <-ch:
|
|
154
|
-
case <-
|
|
155
|
-
p.cancel(id, fmt.Errorf("freedom: proc backend: method %q timed out after %s", method,
|
|
156
|
-
return nil, fmt.Errorf("freedom: proc backend: method %q timed out after %s", method,
|
|
244
|
+
case <-timer.C:
|
|
245
|
+
p.cancel(id, fmt.Errorf("freedom: proc backend: method %q timed out after %s", method, timeout))
|
|
246
|
+
return nil, fmt.Errorf("freedom: proc backend: method %q timed out after %s", method, timeout)
|
|
157
247
|
}
|
|
158
248
|
} else {
|
|
159
249
|
resp = <-ch
|
|
@@ -182,22 +272,31 @@ func (p *ProcBackend) cancel(id int64, err error) {
|
|
|
182
272
|
}
|
|
183
273
|
|
|
184
274
|
// readLoop 持续读取后端 stdout,解析协议消息并分发。
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
275
|
+
//
|
|
276
|
+
// 行切分用受限读取 readLimitedLine(而非 ReadBytes/Scanner):
|
|
277
|
+
// ReadBytes 对无换行的巨量输出会不设上限地攒进内存;Scanner 超限后永久罢工。
|
|
278
|
+
// 某条响应超限时仅丢弃该条并留痕,随后继续读后续行,
|
|
279
|
+
// 绝不因单条大消息而中断整个 IPC 通道。
|
|
280
|
+
func (p *ProcBackend) readLoop(gen int, stdout io.Reader, cmd *exec.Cmd, done chan struct{}) {
|
|
281
|
+
rd := bufio.NewReaderSize(stdout, 64*1024)
|
|
282
|
+
for {
|
|
283
|
+
limit := p.getMaxLine()
|
|
284
|
+
line, dropped, readErr := readLimitedLine(rd, limit)
|
|
285
|
+
if dropped {
|
|
286
|
+
fmt.Fprintf(os.Stderr, "freedom: proc backend: line too large (> maxLine %d), dropped\n", limit)
|
|
287
|
+
} else if len(line) > 0 {
|
|
288
|
+
p.dispatchLine(line)
|
|
193
289
|
}
|
|
194
|
-
if
|
|
195
|
-
|
|
196
|
-
|
|
290
|
+
if readErr != nil {
|
|
291
|
+
if readErr == io.EOF {
|
|
292
|
+
// 正常结束:后端进程关闭了 stdout。
|
|
293
|
+
} else {
|
|
294
|
+
fmt.Fprintf(os.Stderr, "freedom: proc backend: read stdout: %v\n", readErr)
|
|
295
|
+
}
|
|
296
|
+
break
|
|
197
297
|
}
|
|
198
|
-
p.finish(msg)
|
|
199
298
|
}
|
|
200
|
-
//
|
|
299
|
+
// 后端进程已退出(或 stdout 不可读):唤醒所有仍等待中的调用。
|
|
201
300
|
err := fmt.Errorf("freedom: proc backend exited unexpectedly")
|
|
202
301
|
p.mu.Lock()
|
|
203
302
|
for id, ch := range p.pending {
|
|
@@ -205,6 +304,140 @@ func (p *ProcBackend) readLoop(stdout io.Reader) {
|
|
|
205
304
|
ch <- procResp{err: err}
|
|
206
305
|
}
|
|
207
306
|
p.mu.Unlock()
|
|
307
|
+
|
|
308
|
+
if cmd == nil {
|
|
309
|
+
// 无真实子进程(协议层单测直接注入坏 reader):唤醒 pending 即使命完成。
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
waitErr := cmd.Wait() // stdout 已 EOF,Wait 安全(见 os/exec StdoutPipe 约定)
|
|
313
|
+
if done != nil {
|
|
314
|
+
close(done) // 通知 Close():本期进程已回收
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
p.mu.Lock()
|
|
318
|
+
if p.closed || p.gen != gen {
|
|
319
|
+
// 已 Close 或这是被重启淘汰的过期实例:不做崩溃处理。
|
|
320
|
+
p.mu.Unlock()
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
p.stdin = nil
|
|
324
|
+
p.cmd = nil
|
|
325
|
+
if p.rp.MaxRetries <= 0 {
|
|
326
|
+
p.mu.Unlock()
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
if waitErr == nil {
|
|
330
|
+
// 退出码 0:后端主动正常结束,不算崩溃、不重启(防退出型后端被无限拉起)。
|
|
331
|
+
p.mu.Unlock()
|
|
332
|
+
p.notifyFrontend("backend.exited", map[string]interface{}{"code": 0})
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
if time.Since(p.startedAt) >= p.rp.ResetAfter {
|
|
336
|
+
p.attempts = 0
|
|
337
|
+
}
|
|
338
|
+
p.attempts++
|
|
339
|
+
attempt := p.attempts
|
|
340
|
+
maxR := p.rp.MaxRetries
|
|
341
|
+
backoff := p.rp.Backoff << (attempt - 1)
|
|
342
|
+
if backoff > maxRestartBackoff || backoff <= 0 {
|
|
343
|
+
backoff = maxRestartBackoff
|
|
344
|
+
}
|
|
345
|
+
restarting := attempt <= maxR
|
|
346
|
+
p.mu.Unlock()
|
|
347
|
+
|
|
348
|
+
exitCode := -1
|
|
349
|
+
var ee *exec.ExitError
|
|
350
|
+
if errors.As(waitErr, &ee) {
|
|
351
|
+
exitCode = ee.ExitCode()
|
|
352
|
+
}
|
|
353
|
+
p.notifyFrontend("backend.crashed", map[string]interface{}{
|
|
354
|
+
"code": exitCode, "attempt": attempt, "restarting": restarting,
|
|
355
|
+
})
|
|
356
|
+
if !restarting {
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
time.AfterFunc(backoff, func() {
|
|
360
|
+
p.mu.Lock()
|
|
361
|
+
if p.closed || p.stdin != nil {
|
|
362
|
+
p.mu.Unlock()
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
launchErr := p.launchLocked()
|
|
366
|
+
p.mu.Unlock()
|
|
367
|
+
if launchErr != nil {
|
|
368
|
+
fmt.Fprintf(os.Stderr, "freedom: proc backend restart failed: %v\n", launchErr)
|
|
369
|
+
p.notifyFrontend("backend.crashed", map[string]interface{}{
|
|
370
|
+
"code": exitCode, "attempt": attempt, "restarting": false, "error": launchErr.Error(),
|
|
371
|
+
})
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
p.notifyFrontend("backend.restarted", map[string]interface{}{"attempt": attempt})
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// notifyFrontend 经 onEvent 回调把框架级事件(backend.crashed 等)转发给壳(App.Emit)。
|
|
379
|
+
func (p *ProcBackend) notifyFrontend(event string, data interface{}) {
|
|
380
|
+
p.mu.Lock()
|
|
381
|
+
fn := p.onEvent
|
|
382
|
+
p.mu.Unlock()
|
|
383
|
+
if fn != nil {
|
|
384
|
+
fn(event, data)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// readLimitedLine 读取一行('\n' 结尾、返回时去掉换行)。
|
|
389
|
+
// limit>0 时最多保留 limit 字节:超长行整体丢弃(dropped=true),
|
|
390
|
+
// 内存占用有界——后端失控狂吐无换行数据时不会拖垮壳。
|
|
391
|
+
// 返回的 err 为底层读取错误(行尾EOF时携带最后一行残余)。
|
|
392
|
+
func readLimitedLine(rd *bufio.Reader, limit int) (line []byte, dropped bool, err error) {
|
|
393
|
+
var buf []byte
|
|
394
|
+
for {
|
|
395
|
+
chunk, e := rd.ReadSlice('\n')
|
|
396
|
+
if !dropped {
|
|
397
|
+
if limit > 0 && len(buf)+len(chunk) > limit {
|
|
398
|
+
dropped = true
|
|
399
|
+
buf = nil // 超限时不再保留内容,只消费到行尾
|
|
400
|
+
} else {
|
|
401
|
+
buf = append(buf, chunk...) // ReadSlice 的切片下次读会失效,必须复制
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if e == nil {
|
|
405
|
+
break // 完整行到手
|
|
406
|
+
}
|
|
407
|
+
if e == bufio.ErrBufferFull {
|
|
408
|
+
continue // 行还没读完
|
|
409
|
+
}
|
|
410
|
+
err = e
|
|
411
|
+
break
|
|
412
|
+
}
|
|
413
|
+
if dropped {
|
|
414
|
+
return nil, true, err
|
|
415
|
+
}
|
|
416
|
+
if n := len(buf); n > 0 && buf[n-1] == '\n' {
|
|
417
|
+
buf = buf[:n-1]
|
|
418
|
+
}
|
|
419
|
+
return buf, false, err
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// dispatchLine 处理一条(已去换行的)stdout 行。
|
|
423
|
+
// 行字节上限由 readLimitedLine 在读侧守卫:超限行整体丢弃并留痕,
|
|
424
|
+
// 后续行照常处理——单条大消息不会级联打崩其它在途调用。
|
|
425
|
+
// 注意:超限行若恰好是某次调用的响应,该调用由 Handle 侧超时兜底。
|
|
426
|
+
func (p *ProcBackend) dispatchLine(line []byte) {
|
|
427
|
+
// 空行:协议无意义,跳过。
|
|
428
|
+
if len(line) == 0 {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
var msg procMessage
|
|
432
|
+
if err := json.Unmarshal(line, &msg); err != nil {
|
|
433
|
+
fmt.Fprintf(os.Stderr, "freedom: proc backend: bad message: %v\n", err)
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
if msg.Event != "" {
|
|
437
|
+
p.dispatchEvent(msg.Event, msg.Data)
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
p.finish(msg)
|
|
208
441
|
}
|
|
209
442
|
|
|
210
443
|
func (p *ProcBackend) finish(msg procMessage) {
|
|
@@ -225,22 +458,21 @@ func (p *ProcBackend) finish(msg procMessage) {
|
|
|
225
458
|
func (p *ProcBackend) dispatchEvent(event string, dataJSON json.RawMessage) {
|
|
226
459
|
var data interface{}
|
|
227
460
|
if len(dataJSON) > 0 {
|
|
228
|
-
|
|
461
|
+
if err := json.Unmarshal(dataJSON, &data); err != nil {
|
|
462
|
+
// 非 JSON 事件负载降级为原始字符串,保留信息而非静默丢弃。
|
|
463
|
+
data = string(dataJSON)
|
|
464
|
+
}
|
|
229
465
|
}
|
|
230
466
|
p.mu.Lock()
|
|
231
467
|
fn := p.onEvent
|
|
232
468
|
p.mu.Unlock()
|
|
233
469
|
if fn != nil {
|
|
234
|
-
|
|
235
|
-
// 一旦外泄到 readLoop 会崩掉整个壳进程,此处统一兜底。
|
|
236
|
-
func() {
|
|
237
|
-
defer func() { _ = recover() }()
|
|
238
|
-
fn(event, data)
|
|
239
|
-
}()
|
|
470
|
+
fn(event, data)
|
|
240
471
|
}
|
|
241
472
|
}
|
|
242
473
|
|
|
243
474
|
// Close 终止后端进程。先关闭 stdin 通知其优雅退出,超时则强杀。线程安全。
|
|
475
|
+
// 进程回收(Wait)统一由 readLoop 负责,Close 只等它的 done 信号,避免双重 Wait。
|
|
244
476
|
func (p *ProcBackend) Close() error {
|
|
245
477
|
p.mu.Lock()
|
|
246
478
|
if p.closed {
|
|
@@ -249,24 +481,30 @@ func (p *ProcBackend) Close() error {
|
|
|
249
481
|
}
|
|
250
482
|
p.closed = true
|
|
251
483
|
cmd := p.cmd
|
|
252
|
-
|
|
253
|
-
|
|
484
|
+
stdin := p.stdin
|
|
485
|
+
done := p.pdone
|
|
486
|
+
if stdin != nil {
|
|
487
|
+
if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
|
488
|
+
fmt.Fprintf(os.Stderr, "freedom: close backend stdin: %v\n", err)
|
|
489
|
+
}
|
|
254
490
|
}
|
|
255
491
|
p.mu.Unlock()
|
|
256
492
|
|
|
257
|
-
done
|
|
258
|
-
|
|
493
|
+
if done == nil {
|
|
494
|
+
return nil // 从未成功启动过,没有子进程需要回收
|
|
495
|
+
}
|
|
259
496
|
select {
|
|
260
497
|
case <-done:
|
|
261
498
|
case <-time.After(3 * time.Second):
|
|
262
|
-
if cmd.Process != nil {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
499
|
+
if cmd != nil && cmd.Process != nil {
|
|
500
|
+
if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
|
501
|
+
fmt.Fprintf(os.Stderr, "freedom: backend kill: %v\n", err)
|
|
502
|
+
}
|
|
503
|
+
// Kill 也可能失败(权限等),等待必须有上限,否则 Close 永久阻塞。
|
|
266
504
|
select {
|
|
267
505
|
case <-done:
|
|
268
506
|
case <-time.After(time.Second):
|
|
269
|
-
fmt.Fprintf(os.Stderr, "freedom:
|
|
507
|
+
fmt.Fprintf(os.Stderr, "freedom: backend: process did not exit after kill\n")
|
|
270
508
|
}
|
|
271
509
|
}
|
|
272
510
|
}
|
|
@@ -109,7 +109,7 @@ func collectResults(res []reflect.Value) (interface{}, error) {
|
|
|
109
109
|
return nil, nil
|
|
110
110
|
case 1:
|
|
111
111
|
if res[0].Type().Implements(errorType) {
|
|
112
|
-
if !res[0]
|
|
112
|
+
if !isNilValue(res[0]) {
|
|
113
113
|
return nil, res[0].Interface().(error)
|
|
114
114
|
}
|
|
115
115
|
return nil, nil
|
|
@@ -119,7 +119,7 @@ func collectResults(res []reflect.Value) (interface{}, error) {
|
|
|
119
119
|
if !res[1].Type().Implements(errorType) {
|
|
120
120
|
return nil, fmt.Errorf("freedom: second return value must be error")
|
|
121
121
|
}
|
|
122
|
-
if !res[1]
|
|
122
|
+
if !isNilValue(res[1]) {
|
|
123
123
|
return res[0].Interface(), res[1].Interface().(error)
|
|
124
124
|
}
|
|
125
125
|
return res[0].Interface(), nil
|
|
@@ -127,3 +127,14 @@ func collectResults(res []reflect.Value) (interface{}, error) {
|
|
|
127
127
|
return nil, fmt.Errorf("freedom: function may return at most a value and an error")
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
// isNilValue 判空。绑定函数可返回实现了 error 接口的值类型
|
|
132
|
+
// (如带 Error() 方法的 struct),对这类 Kind 调 reflect.Value.IsNil 会 panic。
|
|
133
|
+
func isNilValue(v reflect.Value) bool {
|
|
134
|
+
switch v.Kind() {
|
|
135
|
+
case reflect.Chan, reflect.Func, reflect.Interface,
|
|
136
|
+
reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
|
|
137
|
+
return v.IsNil()
|
|
138
|
+
}
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
// M3 声明式能力模型(对标 Tauri capabilities):对前端 sys/tray/window 三桥的
|
|
4
|
+
// 可调用面做白/黑名单收口。语义:
|
|
5
|
+
// - Config.Capabilities == nil(默认):全开,兼容既有应用。
|
|
6
|
+
// - Deny 命中 → 拒绝(优先于 Allow)。
|
|
7
|
+
// - Allow 非空且无一命中 → 拒绝。
|
|
8
|
+
// 模式用 path.Match 语法(* / ?;"*" 全放开);模式非法按不命中处理。
|
|
9
|
+
// 判定发生在派发进平台层之前,拒绝路径零副作用;错误串统一以
|
|
10
|
+
// "freedom: capability denied" 开头供前端识别。
|
|
11
|
+
|
|
12
|
+
import (
|
|
13
|
+
"fmt"
|
|
14
|
+
"path"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// Capabilities 是前端桥接面的声明式收口配置。
|
|
18
|
+
type Capabilities struct {
|
|
19
|
+
// Allow 非空时为白名单:仅命中项可调用。
|
|
20
|
+
Allow []string
|
|
21
|
+
// Deny 黑名单:命中即拒绝,优先于 Allow。
|
|
22
|
+
Deny []string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// capCheck 按全名判定一次调用是否放行。三桥的名字域:
|
|
26
|
+
// sys 用方法原名("clipboard.read"、"dialog.open"…),tray 用 "tray.*"/"menu.*",
|
|
27
|
+
// window 动作经 capCheckWindow 补 "window." 前缀——与 sys 里的 window.monitors /
|
|
28
|
+
// window.backdrop 等同名前缀方法共享名字域,"window.*" 一条即可整族收口。
|
|
29
|
+
func (a *App) capCheck(full string) error {
|
|
30
|
+
c := a.cfg.Capabilities
|
|
31
|
+
if c == nil {
|
|
32
|
+
return nil
|
|
33
|
+
}
|
|
34
|
+
for _, p := range c.Deny {
|
|
35
|
+
if matchCap(p, full) {
|
|
36
|
+
return capDenied(full)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if len(c.Allow) == 0 {
|
|
40
|
+
return nil
|
|
41
|
+
}
|
|
42
|
+
for _, p := range c.Allow {
|
|
43
|
+
if matchCap(p, full) {
|
|
44
|
+
return nil
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return capDenied(full)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// capCheckWindow 判定窗口桥动作(action 补 window. 前缀后进名字域)。
|
|
51
|
+
func (a *App) capCheckWindow(action string) error {
|
|
52
|
+
return a.capCheck("window." + action)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func matchCap(pattern, name string) bool {
|
|
56
|
+
ok, err := path.Match(pattern, name)
|
|
57
|
+
return err == nil && ok
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func capDenied(name string) error {
|
|
61
|
+
return fmt.Errorf("freedom: capability denied: %q", name)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// sysCapGated / trayGated 是 __freedom_sys / __freedom_tray 的带闸绑定入口。
|
|
65
|
+
func (a *App) sysCapGated(method, paramsJSON string) (interface{}, error) {
|
|
66
|
+
if err := a.capCheck(method); err != nil {
|
|
67
|
+
return nil, err
|
|
68
|
+
}
|
|
69
|
+
return a.sysCapCall(method, paramsJSON)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func (a *App) trayGated(method, paramsJSON string) (interface{}, error) {
|
|
73
|
+
if err := a.capCheck(method); err != nil {
|
|
74
|
+
return nil, err
|
|
75
|
+
}
|
|
76
|
+
return a.trayCall(method, paramsJSON)
|
|
77
|
+
}
|
|
@@ -5,3 +5,6 @@ package freedom
|
|
|
5
5
|
// applyCenter 在 macOS / Linux 下保持空实现:窗口初始位置由窗口管理器决定,
|
|
6
6
|
// 如需精确居中可在目标平台扩展(macOS 用 NSWindow center,Linux 用 Gtk 定位)。
|
|
7
7
|
func (a *App) applyCenter() {}
|
|
8
|
+
|
|
9
|
+
// centerHWND 非 Windows 平台空实现(M2 次级窗口居中,与 applyCenter 同语义)。
|
|
10
|
+
func (a *App) centerHWND(hwnd uintptr) {}
|