@yufengtadian/freedom-cli 1.12.18 → 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 +1 -1
- package/lib/security.js +1 -1
- 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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
// 自动更新(对标 Tauri v2 updater 插件):
|
|
4
|
+
// manifest(JSON)→ ed25519 验签 → 下载产物 → sha256 校验 → 换装 → 下次启动生效。
|
|
5
|
+
// 安全底线(packaging-parity 裁定,不可绕过):
|
|
6
|
+
// 1) manifest 必须通过 Config.Update.PublicKey 的 ed25519 验签,签名覆盖
|
|
7
|
+
// version/url/sha256 三字段(规范串见 manifestPayload);
|
|
8
|
+
// 2) 下载产物必须命中 manifest 的 sha256,边下边哈希,不落地未校验文件;
|
|
9
|
+
// 3) 换装只经"当前 exe 改名让位"完成——前端只能触发,无法注入 URL/哈希
|
|
10
|
+
// (install 使用 check 时验签通过的缓存条目)。
|
|
11
|
+
// 运行中的 exe 在 Windows 可改名不可覆写,故换装后需重启生效,不做热替换。
|
|
12
|
+
|
|
13
|
+
import (
|
|
14
|
+
"context"
|
|
15
|
+
"crypto/ed25519"
|
|
16
|
+
"crypto/sha256"
|
|
17
|
+
"encoding/base64"
|
|
18
|
+
"encoding/hex"
|
|
19
|
+
"encoding/json"
|
|
20
|
+
"errors"
|
|
21
|
+
"fmt"
|
|
22
|
+
"io"
|
|
23
|
+
"net/http"
|
|
24
|
+
"net/url"
|
|
25
|
+
"os"
|
|
26
|
+
"path/filepath"
|
|
27
|
+
"strconv"
|
|
28
|
+
"strings"
|
|
29
|
+
"time"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// UpdateConfig 是 Config.Update 的自动更新配置;为 nil 时更新能力整体关闭。
|
|
33
|
+
type UpdateConfig struct {
|
|
34
|
+
// ManifestURL 是更新清单的 https 地址(http 仅允许 loopback,供测试/内网源)。
|
|
35
|
+
ManifestURL string
|
|
36
|
+
// PublicKey 为 base64(std) 的 ed25519 公钥(32 字节)。签名私钥属发布方资产,
|
|
37
|
+
// 公钥编译期注入壳——没有它整个 updater 拒绝工作。
|
|
38
|
+
PublicKey string
|
|
39
|
+
// Timeout 是单次网络操作上限;0 取默认 30s。
|
|
40
|
+
Timeout time.Duration
|
|
41
|
+
// MaxDownloadBytes 是更新产物大小上限(DoS 防线);0 取默认 512MB。
|
|
42
|
+
MaxDownloadBytes int64
|
|
43
|
+
// RequireSignature 开启后,产物在 ed25519+sha256 之外还须通过平台代码签名
|
|
44
|
+
// 复核(Windows Authenticode/WinVerifyTrust;其他平台无此能力则直接拒绝安装)。
|
|
45
|
+
RequireSignature bool
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// UpdateInfo 是一条已通过验签的更新描述。
|
|
49
|
+
type UpdateInfo struct {
|
|
50
|
+
Version string `json:"version"`
|
|
51
|
+
URL string `json:"url"`
|
|
52
|
+
Sha256 string `json:"sha256"`
|
|
53
|
+
Notes string `json:"notes,omitempty"`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const updateManifestMax = 1 << 20 // manifest 上限 1MB,防异常响应撑爆内存
|
|
57
|
+
|
|
58
|
+
// manifestPayload 是签名的规范覆盖串:换序/加字段都会使既有私钥失效,故固定。
|
|
59
|
+
func manifestPayload(version, url, sha256hex string) []byte {
|
|
60
|
+
return []byte("freedom-update-v1\n" + version + "\n" + url + "\n" + strings.ToLower(sha256hex))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// updateURLAllowed:https 恒放行;http 仅 loopback(httptest/内网源)。
|
|
64
|
+
func updateURLAllowed(raw string) bool {
|
|
65
|
+
u, err := url.Parse(raw)
|
|
66
|
+
if err != nil || u.Host == "" {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
switch u.Scheme {
|
|
70
|
+
case "https":
|
|
71
|
+
return true
|
|
72
|
+
case "http":
|
|
73
|
+
h := u.Hostname()
|
|
74
|
+
return h == "127.0.0.1" || h == "::1" || h == "localhost"
|
|
75
|
+
}
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// compareVersions 比较点分数字版本(1.2 < 1.10;段数不等时缺位补 0)。
|
|
80
|
+
func compareVersions(a, b string) (int, error) {
|
|
81
|
+
as, bs := strings.Split(a, "."), strings.Split(b, ".")
|
|
82
|
+
n := max(len(as), len(bs))
|
|
83
|
+
for i := 0; i < n; i++ {
|
|
84
|
+
x, y := 0, 0
|
|
85
|
+
if i < len(as) {
|
|
86
|
+
v, err := strconv.Atoi(as[i])
|
|
87
|
+
if err != nil || v < 0 {
|
|
88
|
+
return 0, fmt.Errorf("bad version segment %q", as[i])
|
|
89
|
+
}
|
|
90
|
+
x = v
|
|
91
|
+
}
|
|
92
|
+
if i < len(bs) {
|
|
93
|
+
v, err := strconv.Atoi(bs[i])
|
|
94
|
+
if err != nil || v < 0 {
|
|
95
|
+
return 0, fmt.Errorf("bad version segment %q", bs[i])
|
|
96
|
+
}
|
|
97
|
+
y = v
|
|
98
|
+
}
|
|
99
|
+
if x != y {
|
|
100
|
+
if x < y {
|
|
101
|
+
return -1, nil
|
|
102
|
+
}
|
|
103
|
+
return 1, nil
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return 0, nil
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func (a *App) updateHTTPClient() *http.Client {
|
|
110
|
+
timeout := 30 * time.Second
|
|
111
|
+
if a.cfg.Update != nil && a.cfg.Update.Timeout > 0 {
|
|
112
|
+
timeout = a.cfg.Update.Timeout
|
|
113
|
+
}
|
|
114
|
+
return &http.Client{
|
|
115
|
+
Timeout: timeout,
|
|
116
|
+
// 重定向逐跳复验:默认 client 会跟随 https→http 降级与 loopback→任意外部,
|
|
117
|
+
// 虽被签名+sha256 兜住(非 RCE),但仍是 SSRF/降级面,须在策略层挡。
|
|
118
|
+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
119
|
+
if len(via) > 5 {
|
|
120
|
+
return errors.New("freedom: too many redirects")
|
|
121
|
+
}
|
|
122
|
+
if !updateURLAllowed(req.URL.String()) {
|
|
123
|
+
return errors.New("freedom: redirect to disallowed URL")
|
|
124
|
+
}
|
|
125
|
+
return nil
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// CheckUpdate 拉取并验签更新 manifest。返回 (nil, nil) 表示已最新;
|
|
131
|
+
// 任何验签/格式/协议失败都返回错误且不产生副作用。
|
|
132
|
+
// 成功时结果缓存进 App(upPending),供 InstallUpdate/桥接使用。
|
|
133
|
+
func (a *App) CheckUpdate(ctx context.Context) (*UpdateInfo, error) {
|
|
134
|
+
uc := a.cfg.Update
|
|
135
|
+
if uc == nil || uc.ManifestURL == "" || uc.PublicKey == "" {
|
|
136
|
+
return nil, errors.New("freedom: update not configured (Config.Update)")
|
|
137
|
+
}
|
|
138
|
+
pub, err := base64.StdEncoding.DecodeString(uc.PublicKey)
|
|
139
|
+
if err != nil || len(pub) != ed25519.PublicKeySize {
|
|
140
|
+
return nil, errors.New("freedom: Config.Update.PublicKey must be base64 ed25519 (32 bytes)")
|
|
141
|
+
}
|
|
142
|
+
if !updateURLAllowed(uc.ManifestURL) {
|
|
143
|
+
return nil, fmt.Errorf("freedom: manifest URL scheme rejected: %q", uc.ManifestURL)
|
|
144
|
+
}
|
|
145
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uc.ManifestURL, nil)
|
|
146
|
+
if err != nil {
|
|
147
|
+
return nil, err
|
|
148
|
+
}
|
|
149
|
+
resp, err := a.updateHTTPClient().Do(req)
|
|
150
|
+
if err != nil {
|
|
151
|
+
return nil, fmt.Errorf("freedom: fetch manifest: %w", err)
|
|
152
|
+
}
|
|
153
|
+
defer resp.Body.Close()
|
|
154
|
+
if resp.StatusCode != http.StatusOK {
|
|
155
|
+
return nil, fmt.Errorf("freedom: manifest fetch status %d", resp.StatusCode)
|
|
156
|
+
}
|
|
157
|
+
body, err := io.ReadAll(io.LimitReader(resp.Body, updateManifestMax+1))
|
|
158
|
+
if err != nil {
|
|
159
|
+
return nil, fmt.Errorf("freedom: read manifest: %w", err)
|
|
160
|
+
}
|
|
161
|
+
if len(body) > updateManifestMax {
|
|
162
|
+
return nil, errors.New("freedom: manifest exceeds 1MB limit")
|
|
163
|
+
}
|
|
164
|
+
var m struct {
|
|
165
|
+
UpdateInfo
|
|
166
|
+
Signature string `json:"signature"`
|
|
167
|
+
}
|
|
168
|
+
if err := json.Unmarshal(body, &m); err != nil {
|
|
169
|
+
return nil, fmt.Errorf("freedom: manifest json: %w", err)
|
|
170
|
+
}
|
|
171
|
+
sig, err := base64.StdEncoding.DecodeString(m.Signature)
|
|
172
|
+
if err != nil {
|
|
173
|
+
return nil, errors.New("freedom: manifest signature not base64")
|
|
174
|
+
}
|
|
175
|
+
if !ed25519.Verify(pub, manifestPayload(m.Version, m.URL, m.Sha256), sig) {
|
|
176
|
+
return nil, errors.New("freedom: manifest signature invalid")
|
|
177
|
+
}
|
|
178
|
+
if !updateURLAllowed(m.URL) {
|
|
179
|
+
return nil, fmt.Errorf("freedom: artifact URL scheme rejected: %q", m.URL)
|
|
180
|
+
}
|
|
181
|
+
cmp, err := compareVersions(Version, m.Version)
|
|
182
|
+
if err != nil {
|
|
183
|
+
return nil, fmt.Errorf("freedom: current version %q uncomparable: %w", Version, err)
|
|
184
|
+
}
|
|
185
|
+
a.upMu.Lock()
|
|
186
|
+
if cmp >= 0 {
|
|
187
|
+
// 已最新/发布方撤回版本:清掉旧缓存,防 install 装上已被撤回的构建
|
|
188
|
+
a.upPending = nil
|
|
189
|
+
a.upMu.Unlock()
|
|
190
|
+
return nil, nil
|
|
191
|
+
}
|
|
192
|
+
a.upPending = &UpdateInfo{Version: m.Version, URL: m.URL, Sha256: strings.ToLower(m.Sha256), Notes: m.Notes}
|
|
193
|
+
pending := a.upPending
|
|
194
|
+
a.upMu.Unlock()
|
|
195
|
+
return pending, nil
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// downloadArtifact 把 info.URL 的产物下载到 dir 下临时文件,边下边算 sha256,
|
|
199
|
+
// 不匹配即删除并报错——磁盘上不存在"未校验且可用"的更新文件。
|
|
200
|
+
func (a *App) downloadArtifact(ctx context.Context, info *UpdateInfo, dir string) (string, error) {
|
|
201
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, info.URL, nil)
|
|
202
|
+
if err != nil {
|
|
203
|
+
return "", err
|
|
204
|
+
}
|
|
205
|
+
resp, err := a.updateHTTPClient().Do(req)
|
|
206
|
+
if err != nil {
|
|
207
|
+
return "", fmt.Errorf("freedom: download artifact: %w", err)
|
|
208
|
+
}
|
|
209
|
+
defer resp.Body.Close()
|
|
210
|
+
if resp.StatusCode != http.StatusOK {
|
|
211
|
+
return "", fmt.Errorf("freedom: artifact download status %d", resp.StatusCode)
|
|
212
|
+
}
|
|
213
|
+
tmp, err := os.CreateTemp(dir, ".freedom-update-*")
|
|
214
|
+
if err != nil {
|
|
215
|
+
return "", err
|
|
216
|
+
}
|
|
217
|
+
tmpName := tmp.Name()
|
|
218
|
+
defer func() {
|
|
219
|
+
if tmpName != "" {
|
|
220
|
+
os.Remove(tmpName)
|
|
221
|
+
}
|
|
222
|
+
}()
|
|
223
|
+
h := sha256.New()
|
|
224
|
+
// 产物大小硬顶:sha256 保完整性不保可用性——不设限则恶意/被攻陷源可流式撑爆磁盘。
|
|
225
|
+
limit := int64(512 << 20)
|
|
226
|
+
if a.cfg.Update != nil && a.cfg.Update.MaxDownloadBytes > 0 {
|
|
227
|
+
limit = a.cfg.Update.MaxDownloadBytes
|
|
228
|
+
}
|
|
229
|
+
if resp.ContentLength > limit {
|
|
230
|
+
return "", fmt.Errorf("freedom: artifact content-length %d exceeds limit %d", resp.ContentLength, limit)
|
|
231
|
+
}
|
|
232
|
+
n, err := io.Copy(io.MultiWriter(tmp, h), io.LimitReader(resp.Body, limit+1))
|
|
233
|
+
if err != nil {
|
|
234
|
+
tmp.Close()
|
|
235
|
+
return "", fmt.Errorf("freedom: write artifact: %w", err)
|
|
236
|
+
}
|
|
237
|
+
if n > limit {
|
|
238
|
+
tmp.Close()
|
|
239
|
+
return "", fmt.Errorf("freedom: artifact exceeds size limit %d bytes", limit)
|
|
240
|
+
}
|
|
241
|
+
if err := tmp.Close(); err != nil {
|
|
242
|
+
return "", err
|
|
243
|
+
}
|
|
244
|
+
got := hex.EncodeToString(h.Sum(nil))
|
|
245
|
+
if !strings.EqualFold(got, info.Sha256) {
|
|
246
|
+
return "", fmt.Errorf("freedom: artifact sha256 mismatch (want %s got %s)", info.Sha256, got)
|
|
247
|
+
}
|
|
248
|
+
if a.cfg.Update != nil && a.cfg.Update.RequireSignature {
|
|
249
|
+
// M6:二级发布者信号,失败即弃(staged 由 defer 清理)
|
|
250
|
+
if err := authenticodeCheck(tmpName); err != nil {
|
|
251
|
+
return "", err
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if err := os.Chmod(tmpName, 0o755); err != nil {
|
|
255
|
+
return "", err
|
|
256
|
+
}
|
|
257
|
+
name := tmpName
|
|
258
|
+
tmpName = "" // 校验通过,交给调用方处置
|
|
259
|
+
return name, nil
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// swapExecutable 把 staged 换到 target 位置:运行中的 exe 改名让位(Windows 允许
|
|
263
|
+
// 改名不允许覆写),新文件顶上;顶位失败则回滚改名,目标保持旧版可用。
|
|
264
|
+
func swapExecutable(target, staged string) error {
|
|
265
|
+
old := target + ".old"
|
|
266
|
+
if err := os.Rename(target, old); err != nil {
|
|
267
|
+
return fmt.Errorf("freedom: move current exe aside: %w", err)
|
|
268
|
+
}
|
|
269
|
+
if err := os.Rename(staged, target); err != nil {
|
|
270
|
+
// 回滚:改名复原;改名也失败(目标路径被第三方占)则退化为内容复制。
|
|
271
|
+
if rb := os.Rename(old, target); rb != nil {
|
|
272
|
+
if wb := copyFileBack(old, target); wb != nil {
|
|
273
|
+
return fmt.Errorf("freedom: install new exe: %w (rollback failed: %v / copy-back failed: %v — 旧版仍在 %s)", err, rb, wb, old)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return fmt.Errorf("freedom: install new exe: %w", err)
|
|
277
|
+
}
|
|
278
|
+
_ = os.Remove(old) // 旧镜像可能仍被系统持有,删除失败无害(下次换装 REPLACE 复用同名)
|
|
279
|
+
return nil
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// copyFileBack 是回滚兜底:把 .old 内容原样写回 target。
|
|
283
|
+
func copyFileBack(src, dst string) error {
|
|
284
|
+
s, err := os.Open(src)
|
|
285
|
+
if err != nil {
|
|
286
|
+
return err
|
|
287
|
+
}
|
|
288
|
+
defer s.Close()
|
|
289
|
+
d, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
|
|
290
|
+
if err != nil {
|
|
291
|
+
return err
|
|
292
|
+
}
|
|
293
|
+
if _, err := io.Copy(d, s); err != nil {
|
|
294
|
+
d.Close()
|
|
295
|
+
return err
|
|
296
|
+
}
|
|
297
|
+
return d.Close()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// InstallUpdate 下载验签缓存的更新并换装到当前 exe 位置,下次启动生效。
|
|
301
|
+
// info 必须来自 CheckUpdate 的成功结果(同一进程内),防止未验签参数注入。
|
|
302
|
+
func (a *App) InstallUpdate(ctx context.Context, info *UpdateInfo) error {
|
|
303
|
+
if info == nil {
|
|
304
|
+
return errors.New("freedom: no update info (run CheckUpdate first)")
|
|
305
|
+
}
|
|
306
|
+
exe, err := os.Executable()
|
|
307
|
+
if err != nil {
|
|
308
|
+
return err
|
|
309
|
+
}
|
|
310
|
+
staged, err := a.downloadArtifact(ctx, info, filepath.Dir(exe))
|
|
311
|
+
if err != nil {
|
|
312
|
+
return err
|
|
313
|
+
}
|
|
314
|
+
defer os.Remove(staged) // 换装成功后 staged 已不在原位,删除为 no-op
|
|
315
|
+
return swapExecutable(exe, staged)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// pendingUpdate 取出并保留 check 缓存(install 桥接用)。
|
|
319
|
+
func (a *App) pendingUpdate() *UpdateInfo {
|
|
320
|
+
a.upMu.Lock()
|
|
321
|
+
defer a.upMu.Unlock()
|
|
322
|
+
return a.upPending
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---- 前端桥接(sysGeneric 分发;长任务 goroutine 化,结果走事件,
|
|
326
|
+
// 避免在 UI 线程消息泵内做网络 IO 冻结窗口)----
|
|
327
|
+
|
|
328
|
+
func (a *App) updateCheckAsync() interface{} {
|
|
329
|
+
go func() {
|
|
330
|
+
info, err := a.CheckUpdate(context.Background())
|
|
331
|
+
if err != nil {
|
|
332
|
+
a.Emit("update.error", map[string]string{"message": err.Error()})
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
if info == nil {
|
|
336
|
+
a.Emit("update.upToDate", map[string]string{"current": Version})
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
a.Emit("update.available", info)
|
|
340
|
+
}()
|
|
341
|
+
return map[string]bool{"requested": true}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
func (a *App) updateInstallAsync() (interface{}, error) {
|
|
345
|
+
a.upMu.Lock()
|
|
346
|
+
info := a.upPending
|
|
347
|
+
if info == nil {
|
|
348
|
+
a.upMu.Unlock()
|
|
349
|
+
return nil, errors.New("no verified update available; call update.check first")
|
|
350
|
+
}
|
|
351
|
+
if a.upInstalling {
|
|
352
|
+
a.upMu.Unlock()
|
|
353
|
+
return map[string]bool{"started": false, "inProgress": true}, nil
|
|
354
|
+
}
|
|
355
|
+
a.upInstalling = true
|
|
356
|
+
a.upMu.Unlock()
|
|
357
|
+
go func() {
|
|
358
|
+
err := a.InstallUpdate(context.Background(), info)
|
|
359
|
+
a.upMu.Lock()
|
|
360
|
+
a.upInstalling = false
|
|
361
|
+
if err == nil {
|
|
362
|
+
a.upPending = nil
|
|
363
|
+
}
|
|
364
|
+
a.upMu.Unlock()
|
|
365
|
+
if err != nil {
|
|
366
|
+
a.Emit("update.error", map[string]string{"message": err.Error()})
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
a.Emit("update.installed", map[string]interface{}{"version": info.Version, "restartRequired": true})
|
|
370
|
+
}()
|
|
371
|
+
return map[string]bool{"started": true}, nil
|
|
372
|
+
}
|