@yufengtadian/freedom-cli 1.12.17 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/lib/build.js +74 -9
  2. package/lib/cli.js +61 -3
  3. package/lib/init.js +9 -1
  4. package/lib/security.js +2 -2
  5. package/lib/utils.js +5 -2
  6. package/lib/verify.js +236 -0
  7. package/package.json +43 -39
  8. package/shell/darwin-arm64/freedom-shell +0 -0
  9. package/shell/linux-x64/freedom-shell +0 -0
  10. package/shell/win-x64/freedom-shell.exe +0 -0
  11. package/templates/go/go.mod +1 -7
  12. package/templates/go/go.sum +2 -2
  13. package/templates/go/main.go +22 -8
  14. package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
  15. package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
  16. package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
  17. package/templates/go/pkg/freedom/assets/freedom.js +219 -21
  18. package/templates/go/pkg/freedom/assets_embed.go +11 -13
  19. package/templates/go/pkg/freedom/authenticode_other.go +11 -0
  20. package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
  21. package/templates/go/pkg/freedom/backend_proc.go +295 -51
  22. package/templates/go/pkg/freedom/bridge.go +13 -2
  23. package/templates/go/pkg/freedom/capability.go +77 -0
  24. package/templates/go/pkg/freedom/center_other.go +3 -0
  25. package/templates/go/pkg/freedom/center_windows.go +64 -36
  26. package/templates/go/pkg/freedom/dispatch.go +60 -0
  27. package/templates/go/pkg/freedom/events_windows.go +319 -0
  28. package/templates/go/pkg/freedom/freedom.go +242 -92
  29. package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
  30. package/templates/go/pkg/freedom/osver_other.go +15 -0
  31. package/templates/go/pkg/freedom/osver_windows.go +121 -0
  32. package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
  33. package/templates/go/pkg/freedom/{configfile.go → resources.go} +56 -32
  34. package/templates/go/pkg/freedom/security.go +19 -12
  35. package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
  36. package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
  37. package/templates/go/pkg/freedom/spawn_other.go +11 -0
  38. package/templates/go/pkg/freedom/spawn_windows.go +23 -0
  39. package/templates/go/pkg/freedom/store.go +458 -0
  40. package/templates/go/pkg/freedom/syscap_linux.go +278 -0
  41. package/templates/go/pkg/freedom/syscap_other.go +22 -0
  42. package/templates/go/pkg/freedom/syscap_windows.go +845 -0
  43. package/templates/go/pkg/freedom/sysint_common.go +70 -0
  44. package/templates/go/pkg/freedom/sysint_windows.go +426 -0
  45. package/templates/go/pkg/freedom/tray_common.go +12 -0
  46. package/templates/go/pkg/freedom/tray_linux.go +302 -0
  47. package/templates/go/pkg/freedom/tray_other.go +10 -0
  48. package/templates/go/pkg/freedom/tray_windows.go +536 -0
  49. package/templates/go/pkg/freedom/updater.go +372 -0
  50. package/templates/go/pkg/freedom/window_mgr.go +385 -0
  51. package/templates/go/pkg/freedom/window_other.go +32 -66
  52. package/templates/go/pkg/freedom/window_windows.go +406 -251
  53. package/templates/project-minimal/freedom.config.js +58 -0
  54. package/templates/project-minimal/index.html +41 -0
  55. package/templates/project-minimal/package.json +14 -0
  56. package/templates/project-minimal/src/main.js +19 -0
  57. package/templates/project-minimal/vite.config.js +18 -0
  58. package/templates/go/gen_config.go +0 -22
  59. package/templates/go/pkg/freedom/security_test.go +0 -153
  60. package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
  61. package/templates/go/webview_go/CHANGELOG.md +0 -15
  62. package/templates/go/webview_go/LICENSE +0 -22
  63. package/templates/go/webview_go/README.md +0 -50
  64. package/templates/go/webview_go/examples/basic/main.go +0 -12
  65. package/templates/go/webview_go/examples/bind/main.go +0 -38
  66. package/templates/go/webview_go/glue.c +0 -36
  67. package/templates/go/webview_go/go.mod +0 -3
  68. package/templates/go/webview_go/go.sum +0 -0
  69. package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
  70. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
  71. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
  72. package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
  73. package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
  74. package/templates/go/webview_go/libs/webview/LICENSE +0 -22
  75. package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
  76. package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
  77. package/templates/go/webview_go/libs/webview/vendor.go +0 -2
  78. package/templates/go/webview_go/libs/webview/version.txt +0 -1
  79. package/templates/go/webview_go/webview.cc +0 -1
  80. package/templates/go/webview_go/webview.go +0 -481
  81. 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
- cmd: cmd,
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
- // 相对路径参数(如 "backend/main.mjs")将相对该目录解析。
75
- // 通用壳在加载 resources/config.json 时自动设为 resources 目录。
103
+ // SetDir 设置后端子进程的工作目录(链式返回自身)。通用壳用它把后端 CWD 定到
104
+ // resources/,使 config.json 里的相对路径参数按 resources/ 解析。
76
105
  func (p *ProcBackend) SetDir(dir string) *ProcBackend {
77
- if dir != "" {
78
- p.cmd.Dir = dir
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()
131
+ return p
132
+ }
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
+ }
86
143
  return p
87
144
  }
88
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
- return fmt.Errorf("freedom: proc backend is closed")
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.cmd.Env = append(os.Environ(), "FREEDOM_BACKEND=1", "FREEDOM_IPC=stdio")
107
- p.cmd.Stderr = os.Stderr // 后端 stderr 日志原样转发
108
- stdin, err := p.cmd.StdinPipe()
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 := p.cmd.StdoutPipe()
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 := p.cmd.Start(); err != nil {
117
- return fmt.Errorf("freedom: proc backend start %q: %w", p.cmd.Path, err)
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
- go p.readLoop(stdout)
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
- if _, err := w.Write(append(line, '\n')); err != nil {
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 p.timeout > 0 {
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 <-time.After(p.timeout):
155
- p.cancel(id, fmt.Errorf("freedom: proc backend: method %q timed out after %s", method, p.timeout))
156
- return nil, fmt.Errorf("freedom: proc backend: method %q timed out after %s", method, p.timeout)
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
- func (p *ProcBackend) readLoop(stdout io.Reader) {
186
- sc := bufio.NewScanner(stdout)
187
- sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
188
- for sc.Scan() {
189
- var msg procMessage
190
- if err := json.Unmarshal(sc.Bytes(), &msg); err != nil {
191
- fmt.Fprintf(os.Stderr, "freedom: proc backend: bad message: %v\n", err)
192
- continue
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 msg.Event != "" {
195
- p.dispatchEvent(msg.Event, msg.Data)
196
- continue
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
- _ = json.Unmarshal(dataJSON, &data)
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
- // 事件回调在 readLoop goroutine 执行;内部若 panic(如推送期间窗口已销毁)
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,19 +481,31 @@ func (p *ProcBackend) Close() error {
249
481
  }
250
482
  p.closed = true
251
483
  cmd := p.cmd
252
- if p.stdin != nil {
253
- _ = p.stdin.Close() // stdin EOF,后端可自行退出
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 := make(chan struct{})
258
- go func() { _ = cmd.Wait(); close(done) }()
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
- _ = cmd.Process.Kill()
264
- <-done
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 永久阻塞。
504
+ select {
505
+ case <-done:
506
+ case <-time.After(time.Second):
507
+ fmt.Fprintf(os.Stderr, "freedom: backend: process did not exit after kill\n")
508
+ }
265
509
  }
266
510
  }
267
511
  return nil
@@ -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].IsNil() {
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].IsNil() {
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) {}