@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.
- package/lib/build.js +74 -9
- package/lib/cli.js +61 -3
- package/lib/init.js +9 -1
- package/lib/security.js +2 -2
- package/lib/utils.js +5 -2
- package/lib/verify.js +236 -0
- package/package.json +43 -39
- 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 +219 -21
- 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 +295 -51
- 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 +64 -36
- 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 +242 -92
- 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} +56 -32
- 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 +22 -0
- package/templates/go/pkg/freedom/syscap_windows.go +845 -0
- 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 +10 -0
- package/templates/go/pkg/freedom/tray_windows.go +536 -0
- 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 +406 -251
- package/templates/project-minimal/freedom.config.js +58 -0
- package/templates/project-minimal/index.html +41 -0
- package/templates/project-minimal/package.json +14 -0
- package/templates/project-minimal/src/main.js +19 -0
- package/templates/project-minimal/vite.config.js +18 -0
- package/templates/go/gen_config.go +0 -22
- package/templates/go/pkg/freedom/security_test.go +0 -153
- 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
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
// 编译三平台。
|
|
8
8
|
// - backend:后端抽象。任意时刻绑定一个后端,前端 window.freedom.call 全部路由过去。
|
|
9
9
|
// 内置两种实现:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// - EmbedBackend:Go 方法直接注册在壳进程内(app.Bind,兼容 v1 用法)。
|
|
11
|
+
// - ProcBackend:后端是任意语言实现的独立进程(Go/Rust/Python/Node/Java…),
|
|
12
|
+
// 通过 stdin/stdout 上的 NDJSON/JSON-RPC 与壳通信。协议语言无关,
|
|
13
|
+
// 换一种后端语言无需改壳与前端。
|
|
14
14
|
// - ipc:双向桥接。前端 window.freedom.call(method, ...args) -> Promise;
|
|
15
15
|
// 后端 app.Emit(event, data) 向所有前端监听器推送事件。
|
|
16
16
|
// - assets:前端编译产物(Vite 单文件 HTML)通过 go:embed 嵌入二进制,运行时
|
|
@@ -19,22 +19,31 @@ package freedom
|
|
|
19
19
|
|
|
20
20
|
import (
|
|
21
21
|
"encoding/json"
|
|
22
|
+
"errors"
|
|
22
23
|
"fmt"
|
|
24
|
+
"os"
|
|
23
25
|
"sync"
|
|
24
|
-
"
|
|
26
|
+
"sync/atomic"
|
|
25
27
|
|
|
26
28
|
webview "github.com/webview/webview_go"
|
|
27
29
|
)
|
|
28
30
|
|
|
31
|
+
// Version 是应用版本号,构建期经 -ldflags "-X freedom.Version=x.y.z" 注入
|
|
32
|
+
// (见 build.ps1/build.sh 的 -Version/VERSION)。前端经 os.info 的 appVersion 读取。
|
|
33
|
+
var Version = "dev"
|
|
34
|
+
|
|
29
35
|
// TitleBarMode 描述窗口标题栏策略。
|
|
30
36
|
type TitleBarMode string
|
|
31
37
|
|
|
32
38
|
const (
|
|
33
|
-
// TitleBarNative
|
|
39
|
+
// TitleBarNative 保留系统原生标题栏(非默认;默认见 TitleBarFrameless)。
|
|
34
40
|
TitleBarNative TitleBarMode = "native"
|
|
41
|
+
// TitleBarHidden 隐藏标题栏视觉,但保留系统原生最小化 / 最大化 / 关闭按钮。
|
|
42
|
+
// 当前仅 Windows 生效(基于 DWM 扩展实现);macOS / Linux 上回退为原生标题栏。
|
|
43
|
+
TitleBarHidden TitleBarMode = "hidden"
|
|
35
44
|
// TitleBarFrameless 完全无边框,客户区铺满整个窗口。
|
|
36
|
-
//
|
|
37
|
-
//
|
|
45
|
+
// 最小化 / 最大化 / 关闭按钮需由前端自绘(通过 window.freedom.window.* 控制),
|
|
46
|
+
// 三平台行为一致。
|
|
38
47
|
TitleBarFrameless TitleBarMode = "frameless"
|
|
39
48
|
)
|
|
40
49
|
|
|
@@ -42,14 +51,15 @@ const (
|
|
|
42
51
|
type Config struct {
|
|
43
52
|
// Title 是窗口标题。
|
|
44
53
|
Title string
|
|
45
|
-
// TitleBar 指定标题栏策略(默认
|
|
54
|
+
// TitleBar 指定标题栏策略(默认 TitleBarFrameless)。可通过 freedom CLI 一键切换。
|
|
46
55
|
TitleBar TitleBarMode
|
|
47
56
|
// Width / Height 是窗口初始尺寸(像素)。
|
|
48
57
|
Width int
|
|
49
58
|
Height int
|
|
50
59
|
// Center 为 true 时窗口在屏幕居中(Windows 生效;macOS/Linux 由窗口管理器决定)。
|
|
51
60
|
Center bool
|
|
52
|
-
// MinWidth / MinHeight
|
|
61
|
+
// MinWidth / MinHeight 为窗口最小尺寸。两个字段均 >0 时作为一组约束生效
|
|
62
|
+
//(webview 的 HintMin 按整组应用);任一 <=0 表示不启用最小尺寸限制。
|
|
53
63
|
MinWidth int
|
|
54
64
|
MinHeight int
|
|
55
65
|
// Debug 为 true 时开启 WebView 开发者工具(目标平台支持时)。
|
|
@@ -59,17 +69,86 @@ type Config struct {
|
|
|
59
69
|
// HTML 返回要加载到窗口的前端页面内容(内存加载,无本地端口)。
|
|
60
70
|
// 为 nil 时使用框架内置的默认占位页。
|
|
61
71
|
HTML func() (string, error)
|
|
72
|
+
// AppID 是应用标识,决定数据/配置目录名与单实例窗口类标题。
|
|
73
|
+
// 为空时回退可执行文件名(去扩展名)。一经发布不要更改,否则用户数据"丢失"。
|
|
74
|
+
AppID string
|
|
75
|
+
// RememberWindowState 为 true 时窗口位置/尺寸/最大化状态跨启动记忆
|
|
76
|
+
// (存于 <dataDir>/window-state.json;Windows 生效,其他平台暂不持久化)。
|
|
77
|
+
RememberWindowState bool
|
|
78
|
+
// Update 配置自动更新(ed25519 验签 manifest + sha256 校验产物)。
|
|
79
|
+
// 为 nil 时更新能力关闭。见 updater.go。
|
|
80
|
+
Update *UpdateConfig
|
|
81
|
+
// Capabilities 收口前端 sys/tray/window 三桥的可调用面(M3)。
|
|
82
|
+
// nil(默认)全开;语义见 capability.go。os.info 回显生效配置。
|
|
83
|
+
Capabilities *Capabilities
|
|
62
84
|
}
|
|
63
85
|
|
|
64
86
|
// App 是 Freedom 应用实例。
|
|
65
87
|
type App struct {
|
|
66
|
-
cfg
|
|
67
|
-
|
|
68
|
-
view
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
onReady
|
|
72
|
-
|
|
88
|
+
cfg Config
|
|
89
|
+
view webview.WebView
|
|
90
|
+
viewMu sync.RWMutex // 保护 view:Emit/Quit/WindowHandle 承诺可从任意 goroutine 调用
|
|
91
|
+
wsMu sync.Mutex // 串行化 window-state 落盘(拖拽异步保存可与销毁保存交叠)
|
|
92
|
+
backend Backend
|
|
93
|
+
onReady func(a *App)
|
|
94
|
+
|
|
95
|
+
// 运行时资源覆盖(resources.go):secure = high 模式(app.bin 解密成功),
|
|
96
|
+
// backendExplicit = 用户经 Bind 显式使用内嵌后端(config.json 的 backend 不得再替换)。
|
|
97
|
+
secure bool
|
|
98
|
+
backendExplicit bool
|
|
99
|
+
|
|
100
|
+
// upMu 保护 upPending:CheckUpdate 验签通过的更新条目,install 桥接只认它
|
|
101
|
+
//(前端无法注入未验签的 URL/哈希)。见 updater.go。
|
|
102
|
+
upMu sync.Mutex
|
|
103
|
+
upPending *UpdateInfo
|
|
104
|
+
upInstalling bool // 单飞:并发 update.install 只放行一个换装
|
|
105
|
+
|
|
106
|
+
// M2 多窗口注册表:windows 含 "main",次级窗口关闭后自行摘除。
|
|
107
|
+
// winMu 同时保护 map、winSeq 与 running 标志(runningMu 并入,减少锁面)。
|
|
108
|
+
winMu sync.Mutex
|
|
109
|
+
windows map[string]*Window
|
|
110
|
+
winSeq int
|
|
111
|
+
running bool
|
|
112
|
+
|
|
113
|
+
// viewTearing 在 Run 的 Destroy 前置位(语义同 Window.tearing):
|
|
114
|
+
// 主窗口已入队的 dispatch 回调会被销毁路径泵出执行,闭包凭此原子量自我作废。
|
|
115
|
+
viewTearing atomic.Bool
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// appForEvents 指向当前运行的 App,供平台层回调(单实例/热键等)向 Emit 事件。
|
|
119
|
+
// 契约:一个进程只跑一个 App——托盘/消息窗口/热键等平台层状态均为进程级单例,
|
|
120
|
+
// 并发创建多个 App 会让事件串门。
|
|
121
|
+
var appForEvents atomic.Pointer[App]
|
|
122
|
+
|
|
123
|
+
func currentApp() *App { return appForEvents.Load() }
|
|
124
|
+
|
|
125
|
+
// setView 设置主窗口 webview 引用。
|
|
126
|
+
func (a *App) setView(w webview.WebView) {
|
|
127
|
+
a.viewMu.Lock()
|
|
128
|
+
a.view = w
|
|
129
|
+
a.viewMu.Unlock()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// withView 在 viewMu 读锁内对主窗口 webview 执行 f;未创建/已摘除时静默跳过。
|
|
133
|
+
// 与 Run 的拆除序列(写锁内摘引用 + Destroy)互斥:调用方拿到引用并 Dispatch 的
|
|
134
|
+
// 窗口期内实例不可能被销毁,杜绝 check-then-destroy 竞态下的 use-after-free。
|
|
135
|
+
func (a *App) withView(f func(view webview.WebView)) {
|
|
136
|
+
a.viewMu.RLock()
|
|
137
|
+
defer a.viewMu.RUnlock()
|
|
138
|
+
if a.view != nil {
|
|
139
|
+
f(a.view)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// teardownView 是 Run 的回收 defer 与测试的同构入口:写锁内立拆除旗标、摘引用、
|
|
144
|
+
// Destroy。Destroy 会泵出队列里的 dispatch 回调(闭包读 viewTearing 自我作废,
|
|
145
|
+
// 不得再取 viewMu——回调就跑在本临界区内)。
|
|
146
|
+
func (a *App) teardownView(view webview.WebView) {
|
|
147
|
+
a.viewMu.Lock()
|
|
148
|
+
defer a.viewMu.Unlock()
|
|
149
|
+
a.viewTearing.Store(true)
|
|
150
|
+
a.view = nil
|
|
151
|
+
view.Destroy()
|
|
73
152
|
}
|
|
74
153
|
|
|
75
154
|
// New 创建并初始化一个 Freedom 应用。调用 Run() 之前不会显示窗口。
|
|
@@ -77,8 +156,11 @@ func New(cfg Config) *App {
|
|
|
77
156
|
if cfg.Title == "" {
|
|
78
157
|
cfg.Title = "Freedom App"
|
|
79
158
|
}
|
|
159
|
+
// M1:默认无边框(frameless),与 freedom-cli 模板默认一致。
|
|
160
|
+
// 此前默认 native 而 CLI 默认 frameless,resources 缺失/配置不完整时
|
|
161
|
+
// 会出现"系统原生标题栏 + 页面自绘标题栏"双标题栏错位。
|
|
80
162
|
if cfg.TitleBar == "" {
|
|
81
|
-
cfg.TitleBar =
|
|
163
|
+
cfg.TitleBar = TitleBarFrameless
|
|
82
164
|
}
|
|
83
165
|
if cfg.Width <= 0 {
|
|
84
166
|
cfg.Width = 1024
|
|
@@ -86,11 +168,10 @@ func New(cfg Config) *App {
|
|
|
86
168
|
if cfg.Height <= 0 {
|
|
87
169
|
cfg.Height = 768
|
|
88
170
|
}
|
|
89
|
-
|
|
90
|
-
if !explicit {
|
|
171
|
+
if cfg.Backend == nil {
|
|
91
172
|
cfg.Backend = NewEmbedBackend()
|
|
92
173
|
}
|
|
93
|
-
return &App{cfg: cfg, backend: cfg.Backend,
|
|
174
|
+
return &App{cfg: cfg, backend: cfg.Backend, windows: map[string]*Window{}}
|
|
94
175
|
}
|
|
95
176
|
|
|
96
177
|
// OnReady 注册一个回调,在窗口与桥接层就绪、页面加载前执行。
|
|
@@ -106,6 +187,9 @@ func (a *App) OnReady(fn func(a *App)) {
|
|
|
106
187
|
// - 参数与返回值通过 JSON 编解码,前端用 window.freedom.call(name, ...args) 调用,
|
|
107
188
|
// 返回 Promise(resolve 为返回值,reject 为 error 的字符串表示)。
|
|
108
189
|
// - 返回值约定:可返回 (T, error) 或仅 (T) 或仅 error 或空。
|
|
190
|
+
// - 执行线程(M1 起):绑定函数在 worker goroutine 中执行,UI 线程不被
|
|
191
|
+
// 长任务阻塞;多次调用可并发、完成顺序不保证。handler 内调用 Emit 等
|
|
192
|
+
// 线程安全 API 即可回推结果。
|
|
109
193
|
func (a *App) Bind(name string, fn interface{}) error {
|
|
110
194
|
eb, ok := a.backend.(*EmbedBackend)
|
|
111
195
|
if !ok {
|
|
@@ -114,9 +198,8 @@ func (a *App) Bind(name string, fn interface{}) error {
|
|
|
114
198
|
if err := eb.Bind(name, fn); err != nil {
|
|
115
199
|
return err
|
|
116
200
|
}
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// 避免"用户 Bind 的方法被 config.json 的进程后端静默替换而全部失效"。
|
|
201
|
+
// 用户显式绑定方法 = 显式指定内嵌后端:resources/config.json 的 backend
|
|
202
|
+
// 配置不再覆盖,避免内嵌方法被进程后端静默替换而全部失效。
|
|
120
203
|
a.backendExplicit = true
|
|
121
204
|
return nil
|
|
122
205
|
}
|
|
@@ -128,35 +211,42 @@ func (a *App) Unbind(name string) {
|
|
|
128
211
|
}
|
|
129
212
|
}
|
|
130
213
|
|
|
131
|
-
// getView / setView 以互斥锁保护 view 字段:Run 在主 goroutine 写,
|
|
132
|
-
// Emit/Quit/WindowHandle 可能被任意 goroutine(如后端事件推送)读,
|
|
133
|
-
// 无保护时 race detector 报数据竞争。
|
|
134
|
-
func (a *App) getView() webview.WebView {
|
|
135
|
-
a.viewMu.Lock()
|
|
136
|
-
defer a.viewMu.Unlock()
|
|
137
|
-
return a.view
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
func (a *App) setView(v webview.WebView) {
|
|
141
|
-
a.viewMu.Lock()
|
|
142
|
-
a.view = v
|
|
143
|
-
a.viewMu.Unlock()
|
|
144
|
-
}
|
|
145
|
-
|
|
146
214
|
// Run 启动窗口并进入主事件循环,阻塞直到窗口被关闭。
|
|
147
215
|
func (a *App) Run() {
|
|
148
|
-
// 通用壳:先加载 exe 同目录 resources/
|
|
149
|
-
|
|
150
|
-
// 配置存在但非法时打印告警(不中断启动),避免用户手改配置出错时静默无感。
|
|
216
|
+
// 通用壳:先加载 exe 同目录 resources/ 覆盖窗口与后端配置(CLI build 写入;
|
|
217
|
+
// 缺失则使用编译期/默认配置)。配置存在但非法时打印告警不中断启动。
|
|
151
218
|
if err := a.loadRuntimeConfig(); err != nil {
|
|
219
|
+
// H2:high 模式资源解密/完整性校验失败必须拒绝运行——不显示窗口、
|
|
220
|
+
// 不回退占位页,防止资源被篡改/替换后静默降级运行。
|
|
221
|
+
var se *secureFatalError
|
|
222
|
+
if errors.As(err, &se) {
|
|
223
|
+
fmt.Printf("freedom: 安全模式资源校验失败,拒绝运行:%v\n", err)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
152
226
|
fmt.Printf("freedom: warning: %v\n", err)
|
|
153
227
|
}
|
|
154
|
-
|
|
155
|
-
// high 安全模式:反调试检测(调试器下静默退出,防止逆向解密逻辑)。
|
|
228
|
+
// high 模式:反调试检测(调试器下静默退出,防止逆向解密逻辑)。
|
|
156
229
|
if a.secure {
|
|
157
230
|
antiDebugCheck()
|
|
158
231
|
}
|
|
159
|
-
|
|
232
|
+
// 平台层回调(单实例转发/热键)经 currentApp 找到本实例推送事件。
|
|
233
|
+
appForEvents.Store(a)
|
|
234
|
+
defer appForEvents.Store(nil)
|
|
235
|
+
// M2:标记运行中(NewWindow 的前置条件)并登记主窗口;退出前回收次级窗口。
|
|
236
|
+
a.winMu.Lock()
|
|
237
|
+
a.running = true
|
|
238
|
+
a.windows[mainWindowID] = &Window{id: mainWindowID, app: a, doneCh: make(chan struct{})}
|
|
239
|
+
a.winMu.Unlock()
|
|
240
|
+
defer func() {
|
|
241
|
+
a.CloseWindows()
|
|
242
|
+
a.winMu.Lock()
|
|
243
|
+
if mw := a.windows[mainWindowID]; mw != nil {
|
|
244
|
+
close(mw.doneCh) // 主窗口随 Run 退出回收:Window("main").WaitClosed() 可等待
|
|
245
|
+
}
|
|
246
|
+
delete(a.windows, mainWindowID)
|
|
247
|
+
a.running = false
|
|
248
|
+
a.winMu.Unlock()
|
|
249
|
+
}()
|
|
160
250
|
html, err := a.resolveHTML()
|
|
161
251
|
if err != nil {
|
|
162
252
|
fmt.Printf("freedom: failed to resolve HTML: %v\n", err)
|
|
@@ -165,21 +255,18 @@ func (a *App) Run() {
|
|
|
165
255
|
|
|
166
256
|
w := webview.New(a.cfg.Debug)
|
|
167
257
|
if w == nil {
|
|
168
|
-
|
|
169
|
-
fmt.Println("freedom: failed to create webview: 未检测到 WebView2 Runtime,请安装 Microsoft Edge WebView2 Runtime(https://go.microsoft.com/fwlink/?linkid=2124703)后重试")
|
|
170
|
-
} else {
|
|
171
|
-
fmt.Println("freedom: failed to create webview")
|
|
172
|
-
}
|
|
258
|
+
fmt.Println("freedom: failed to create webview")
|
|
173
259
|
return
|
|
174
260
|
}
|
|
175
261
|
a.setView(w)
|
|
176
262
|
defer func() {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
263
|
+
if err := a.backend.Close(); err != nil {
|
|
264
|
+
fmt.Fprintf(os.Stderr, "freedom: backend close: %v\n", err)
|
|
265
|
+
}
|
|
266
|
+
a.uninstallWindowEvents()
|
|
267
|
+
// 摘引用与 Destroy 必须同处一个写锁临界区:读锁侧(withView)判空后
|
|
268
|
+
// Dispatch 的窗口期内实例不会被销毁(否则 Emit/Quit/pushResolve UAF)。
|
|
269
|
+
a.teardownView(w)
|
|
183
270
|
}()
|
|
184
271
|
|
|
185
272
|
// 进程后端:启动后端进程并把其推送的事件转发到前端。
|
|
@@ -195,21 +282,24 @@ func (a *App) Run() {
|
|
|
195
282
|
|
|
196
283
|
w.SetTitle(a.cfg.Title)
|
|
197
284
|
w.SetSize(a.cfg.Width, a.cfg.Height, webview.HintNone)
|
|
198
|
-
// 最小尺寸:任一项 >0 即生效;未设置的一项传 0
|
|
199
|
-
//(GTK gtk_widget_set_size_request / Cocoa minSize 对 0 均视为"未指定")。
|
|
285
|
+
// 最小尺寸:任一项 >0 即生效;未设置的一项传 0 表示该维度不限制。
|
|
200
286
|
if a.cfg.MinWidth > 0 || a.cfg.MinHeight > 0 {
|
|
201
287
|
w.SetSize(a.cfg.MinWidth, a.cfg.MinHeight, webview.HintMin)
|
|
202
288
|
}
|
|
203
289
|
a.applyCenter()
|
|
204
290
|
a.applyTitleBar()
|
|
205
|
-
|
|
291
|
+
// 窗口事件与关闭拦截(Windows 经 WndProc 子类化;其他平台 no-op)。
|
|
292
|
+
a.installWindowEvents()
|
|
293
|
+
// 恢复上次退出的窗口几何(仅 Config.RememberWindowState 时生效)。
|
|
294
|
+
a.restoreWindowState()
|
|
206
295
|
|
|
207
296
|
// 注入前端 SDK:window.freedom 全局对象。
|
|
208
297
|
w.Init(jsSDK)
|
|
209
298
|
|
|
210
|
-
// 绑定 IPC 桥接入口:前端 window.__freedom_bridge(method, paramsJson)。
|
|
211
|
-
//
|
|
212
|
-
|
|
299
|
+
// 绑定 IPC 桥接入口:前端 window.__freedom_bridge(id, method, paramsJson)。
|
|
300
|
+
// M1 异步化:调用立即返回,handler 在 worker goroutine 执行,结果经
|
|
301
|
+
// __freedom__resolve 回写前端 Promise(见 dispatch.go)。
|
|
302
|
+
if err := w.Bind("__freedom_bridge", a.bridgeAsync); err != nil {
|
|
213
303
|
fmt.Printf("freedom: failed to bind bridge: %v\n", err)
|
|
214
304
|
return
|
|
215
305
|
}
|
|
@@ -218,17 +308,50 @@ func (a *App) Run() {
|
|
|
218
308
|
fmt.Printf("freedom: failed to bind ping: %v\n", err)
|
|
219
309
|
return
|
|
220
310
|
}
|
|
221
|
-
//
|
|
222
|
-
// 支持动作:minimize / maximize / unmaximize / toggleMaximize / close / isMaximized /
|
|
311
|
+
// 框架内置方法:窗口控制(无边框模式下前端自绘按钮使用 + 运行时窗口能力)。
|
|
312
|
+
// 支持动作:minimize / maximize / unmaximize / toggleMaximize / close / isMaximized /
|
|
313
|
+
// isFrameless / setPosition / setSize / getPosition / getSize / innerSize / center /
|
|
314
|
+
// setTitle / show / hide / focus / isVisible / isFocused / isMinimized /
|
|
315
|
+
// setAlwaysOnTop / setSkipTaskbar / setResizable / setMaximizable / setMinimizable /
|
|
316
|
+
// setFullscreen / isFullscreen / interceptClose / getInfo。
|
|
317
|
+
// 第二参数为 JSON object 字符串(SDK 始终传入,可解析为空对象)。
|
|
223
318
|
if err := w.Bind("__freedom_window", a.windowControl); err != nil {
|
|
224
319
|
fmt.Printf("freedom: failed to bind window control: %v\n", err)
|
|
225
320
|
return
|
|
226
321
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
322
|
+
// 框架内置方法:原生系统能力(wails v3 对标层)。
|
|
323
|
+
// 支持方法:taskbar.*(进度/状态/角标)、window.backdrop/corner/borderColor、
|
|
324
|
+
// dialog.message/open/save。平台实现分文件:syscap_windows.go / syscap_other.go。
|
|
325
|
+
// 绑定入口带 M3 能力闸(见 capability.go)。
|
|
326
|
+
if err := w.Bind("__freedom_sys", a.sysCapGated); err != nil {
|
|
327
|
+
fmt.Printf("freedom: failed to bind sys capability: %v\n", err)
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
// 框架内置方法:系统托盘与原生菜单栏。
|
|
331
|
+
// 支持方法:tray.create/destroy/tooltip/menu、menu.set。
|
|
332
|
+
// 平台实现分文件:tray_windows.go / tray_other.go。绑定入口带 M3 能力闸。
|
|
333
|
+
if err := w.Bind("__freedom_tray", a.trayGated); err != nil {
|
|
334
|
+
fmt.Printf("freedom: failed to bind tray: %v\n", err)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
// 框架内置方法:页面就绪回调(H3)。前端 SDK 在 DOMContentLoaded 后调用,
|
|
338
|
+
// 保证 onReady 触发时页面已加载、监听器已注册;此前 onReady 在 SetHtml 前
|
|
339
|
+
// 触发,期间 Emit 的初始化事件因 window.freedom 尚未建立而丢失。
|
|
340
|
+
// 页面可能因导航/重载多次触发 DOMContentLoaded,用 once 兜底只执行一次。
|
|
341
|
+
var readyOnce sync.Once
|
|
342
|
+
if err := w.Bind("__freedom__ready", func() {
|
|
343
|
+
readyOnce.Do(func() {
|
|
344
|
+
if a.onReady != nil {
|
|
345
|
+
a.onReady(a)
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
}); err != nil {
|
|
349
|
+
fmt.Printf("freedom: failed to bind ready: %v\n", err)
|
|
350
|
+
return
|
|
230
351
|
}
|
|
231
352
|
|
|
353
|
+
// 注:onReady 不再在 SetHtml 前触发(H3),改由前端 SDK 在页面
|
|
354
|
+
// DOMContentLoaded 后经 __freedom__ready 回调,保证初始化事件不丢失。
|
|
232
355
|
w.SetHtml(html)
|
|
233
356
|
w.Run()
|
|
234
357
|
}
|
|
@@ -245,6 +368,12 @@ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage,
|
|
|
245
368
|
}
|
|
246
369
|
}()
|
|
247
370
|
|
|
371
|
+
// 框架内置健康检查:__freedom__ping 同时以 JS 直调全局函数与桥接路由两种方式
|
|
372
|
+
// 暴露(占位页/文档示例走桥接路由),此处兜底保证两条链路行为一致。
|
|
373
|
+
if method == "__freedom__ping" {
|
|
374
|
+
return json.RawMessage(`"pong"`), nil
|
|
375
|
+
}
|
|
376
|
+
|
|
248
377
|
var params []json.RawMessage
|
|
249
378
|
if len(paramsJSON) > 0 && paramsJSON != "null" {
|
|
250
379
|
if err := json.Unmarshal([]byte(paramsJSON), ¶ms); err != nil {
|
|
@@ -252,13 +381,6 @@ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage,
|
|
|
252
381
|
}
|
|
253
382
|
}
|
|
254
383
|
|
|
255
|
-
// 框架内置方法特判:__freedom__ping 由 webview Bind 注册为全局函数
|
|
256
|
-
// window.__freedom__ping(),同时兼容经 freedom.call()/__freedom_bridge 路由的旧写法,
|
|
257
|
-
// 避免模板"测试桥接"自检报"method not bound"。
|
|
258
|
-
if method == "__freedom__ping" {
|
|
259
|
-
return json.RawMessage(`"pong"`), nil
|
|
260
|
-
}
|
|
261
|
-
|
|
262
384
|
raw, err := a.backend.Handle(method, params)
|
|
263
385
|
if err != nil {
|
|
264
386
|
return nil, err
|
|
@@ -273,50 +395,78 @@ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage,
|
|
|
273
395
|
// Emit 把事件推送到前端。前端通过 window.freedom.on(event, cb) 订阅。
|
|
274
396
|
// 线程安全:可从任意 goroutine 调用(进程后端推送的事件亦经由本函数)。
|
|
275
397
|
func (a *App) Emit(event string, data interface{}) {
|
|
276
|
-
v := a.getView()
|
|
277
|
-
if v == nil {
|
|
278
|
-
return
|
|
279
|
-
}
|
|
280
398
|
eb, _ := json.Marshal(event)
|
|
281
399
|
db, err := json.Marshal(data)
|
|
282
400
|
if err != nil {
|
|
283
401
|
db = []byte("null")
|
|
284
402
|
}
|
|
285
403
|
js := "window.freedom && window.freedom.emit(" + string(eb) + "," + string(db) + ");"
|
|
286
|
-
|
|
287
|
-
|
|
404
|
+
a.withView(func(view webview.WebView) {
|
|
405
|
+
view.Dispatch(func() {
|
|
406
|
+
if !a.viewTearing.Load() {
|
|
407
|
+
view.Eval(js)
|
|
408
|
+
}
|
|
409
|
+
})
|
|
288
410
|
})
|
|
411
|
+
a.emitSecondary(js) // M2:事件广播到全部次级窗口
|
|
289
412
|
}
|
|
290
413
|
|
|
291
414
|
// Quit 关闭窗口并退出应用。可从任意 goroutine 调用。
|
|
292
415
|
func (a *App) Quit() {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
416
|
+
a.withView(func(view webview.WebView) {
|
|
417
|
+
view.Dispatch(func() {
|
|
418
|
+
if !a.viewTearing.Load() {
|
|
419
|
+
view.Terminate()
|
|
420
|
+
}
|
|
296
421
|
})
|
|
297
|
-
}
|
|
422
|
+
})
|
|
298
423
|
}
|
|
299
424
|
|
|
300
425
|
// WindowHandle 返回底层原生窗口句柄(Windows 上为 HWND)。
|
|
301
426
|
func (a *App) WindowHandle() uintptr {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
427
|
+
var h uintptr
|
|
428
|
+
a.withView(func(view webview.WebView) {
|
|
429
|
+
h = uintptr(view.Window())
|
|
430
|
+
})
|
|
431
|
+
return h
|
|
306
432
|
}
|
|
307
433
|
|
|
308
434
|
// windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
|
|
309
435
|
// 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
|
|
310
|
-
func (a *App) windowControl(action string) (interface{}, error) {
|
|
311
|
-
|
|
436
|
+
func (a *App) windowControl(action string, paramsJSON string) (result interface{}, err error) {
|
|
437
|
+
// H1:windowControl 直接经 w.Bind 暴露给前端,不经 bridge 的 recover 兜底;
|
|
438
|
+
// 平台实现(如 appIcon → dibToPNG 解析越界)一旦 panic 会直接崩掉整个壳进程。
|
|
439
|
+
// 这里统一加 recover,把 panic 转成错误回传前端(页面 catch 后提示,进程不崩)。
|
|
440
|
+
defer func() {
|
|
441
|
+
if r := recover(); r != nil {
|
|
442
|
+
result = nil
|
|
443
|
+
err = fmt.Errorf("freedom: window action %q panicked: %v", action, r)
|
|
444
|
+
}
|
|
445
|
+
}()
|
|
446
|
+
// M3 能力闸先于一切动作处理(拒绝路径不触平台层,无副作用)。
|
|
447
|
+
if err := a.capCheckWindow(action); err != nil {
|
|
448
|
+
return nil, err
|
|
449
|
+
}
|
|
450
|
+
if res, ok, merr := a.windowManage(action, paramsJSON); ok {
|
|
451
|
+
return res, merr
|
|
452
|
+
}
|
|
453
|
+
return windowControl(a.WindowHandle(), action, a.cfg.TitleBar, paramsJSON)
|
|
312
454
|
}
|
|
313
455
|
|
|
314
456
|
// resolveHTML 依据配置返回页面内容。
|
|
315
|
-
// 优先级:exe 同目录 resources
|
|
457
|
+
// 优先级:exe 同目录 resources/(high 模式 app.bin 或 index.html,预编译通用壳)
|
|
458
|
+
// > cfg.HTML(go:embed 源码构建)> 内置占位页。
|
|
316
459
|
func (a *App) resolveHTML() (string, error) {
|
|
317
|
-
|
|
460
|
+
html, err := loadRuntimeHTML()
|
|
461
|
+
if err == nil && html != "" {
|
|
318
462
|
return html, nil
|
|
319
463
|
}
|
|
464
|
+
// H2 双保险:high 模式校验失败时禁止回退 cfg.HTML/占位页(拒绝运行)。
|
|
465
|
+
// 正常路径下 Run 已捕获 secureFatalError 提前 return,此处防御调用顺序调整。
|
|
466
|
+
var se *secureFatalError
|
|
467
|
+
if errors.As(err, &se) {
|
|
468
|
+
return "", err
|
|
469
|
+
}
|
|
320
470
|
if a.cfg.HTML != nil {
|
|
321
471
|
return a.cfg.HTML()
|
|
322
472
|
}
|