feat: 提交详情能看 diff,也能丢给 AI 审这一次改动。文件检查可单独排除路径,TODO 只认大写,避免把 todo 表和模块当成待办标记。
热力图按容器宽度铺满一年,不再横向滚动。托盘右键换成可换皮肤的弹层;更新下载显示进度,退出后再由脚本拉起安装器,避免还占着 exe。
This commit is contained in:
123
update.go
123
update.go
@@ -13,6 +13,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -70,7 +71,7 @@ func (a *App) SkipAppUpdate(version string) error {
|
||||
return a.store.SetMeta("app_update_skipped", strings.TrimSpace(version))
|
||||
}
|
||||
|
||||
// DownloadAndInstallUpdate 下载最新安装包、校验哈希后启动并退出应用。
|
||||
// DownloadAndInstallUpdate 下载最新安装包、校验后退出应用并由独立脚本拉起安装器。
|
||||
func (a *App) DownloadAndInstallUpdate() error {
|
||||
if e := a.ready(); e != nil {
|
||||
return e
|
||||
@@ -86,20 +87,12 @@ func (a *App) DownloadAndInstallUpdate() error {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
cmd := exec.Command(path, "/S")
|
||||
platform.ConfigureHidden(cmd)
|
||||
if e := cmd.Start(); e != nil {
|
||||
// 静默失败则普通启动
|
||||
cmd2 := exec.Command(path)
|
||||
if e2 := cmd2.Start(); e2 != nil {
|
||||
return errors.New("INSTALLER_START_FAILED")
|
||||
}
|
||||
if e := launchDownloadedInstaller(path); e != nil {
|
||||
return e
|
||||
}
|
||||
a.store.Log("info", "系统", "开始安装更新", info.Version)
|
||||
go func() {
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
a.QuitApp()
|
||||
}()
|
||||
// 安装脚本会等本进程退出后再 start 安装器,避免占用 exe / 作业对象杀掉子进程。
|
||||
a.QuitApp()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -152,9 +145,8 @@ func (a *App) downloadRelease(info *appLatestInfo) (string, error) {
|
||||
if e != nil {
|
||||
return "", errors.New("SYNC_OFFLINE")
|
||||
}
|
||||
if tok := strings.TrimSpace(a.store.Meta("sync_access_token")); tok != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+tok)
|
||||
}
|
||||
// 公开接口不带过期 JWT,避免网关/反代因 Authorization 直接 401。
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
client := &http.Client{Timeout: 30 * time.Minute}
|
||||
resp, e := client.Do(req)
|
||||
if e != nil {
|
||||
@@ -171,15 +163,23 @@ func (a *App) downloadRelease(info *appLatestInfo) (string, error) {
|
||||
}
|
||||
return "", errors.New("SYNC_HTTP_" + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
total := info.SizeBytes
|
||||
if resp.ContentLength > 0 {
|
||||
total = resp.ContentLength
|
||||
}
|
||||
a.emit("app:update-progress", map[string]any{"received": int64(0), "total": total})
|
||||
dir := filepath.Join(os.TempDir(), "code-count-updates")
|
||||
_ = os.MkdirAll(dir, 0755)
|
||||
outPath := filepath.Join(dir, info.Version+"-installer.exe")
|
||||
if e := os.MkdirAll(dir, 0755); e != nil {
|
||||
return "", errors.New("SAVE_FAILED")
|
||||
}
|
||||
outPath := filepath.Join(dir, fmt.Sprintf("%s-installer-%d.exe", info.Version, os.Getpid()))
|
||||
f, e := os.Create(outPath)
|
||||
if e != nil {
|
||||
return "", errors.New("SAVE_FAILED")
|
||||
}
|
||||
h := sha256.New()
|
||||
n, e := io.Copy(io.MultiWriter(f, h), resp.Body)
|
||||
prog := &updateProgressWriter{a: a, total: total}
|
||||
n, e := io.Copy(io.MultiWriter(f, h, prog), resp.Body)
|
||||
_ = f.Close()
|
||||
if e != nil {
|
||||
_ = os.Remove(outPath)
|
||||
@@ -191,11 +191,94 @@ func (a *App) downloadRelease(info *appLatestInfo) (string, error) {
|
||||
return "", errors.New("SHA256_MISMATCH")
|
||||
}
|
||||
if info.SizeBytes > 0 && n != info.SizeBytes {
|
||||
a.store.Log("warning", "系统", "更新包大小与元数据不一致", fmt.Sprintf("%d vs %d", n, info.SizeBytes))
|
||||
_ = os.Remove(outPath)
|
||||
return "", errors.New("SIZE_MISMATCH")
|
||||
}
|
||||
if !isWindowsPE(outPath) {
|
||||
_ = os.Remove(outPath)
|
||||
return "", errors.New("NOT_INSTALLER")
|
||||
}
|
||||
a.emit("app:update-progress", map[string]any{"received": n, "total": total})
|
||||
return outPath, nil
|
||||
}
|
||||
|
||||
type updateProgressWriter struct {
|
||||
a *App
|
||||
total int64
|
||||
written int64
|
||||
lastEmit time.Time
|
||||
}
|
||||
|
||||
func (w *updateProgressWriter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
w.written += int64(n)
|
||||
now := time.Now()
|
||||
if w.lastEmit.IsZero() || now.Sub(w.lastEmit) >= 200*time.Millisecond || (w.total > 0 && w.written >= w.total) {
|
||||
w.lastEmit = now
|
||||
if w.a != nil {
|
||||
w.a.emit("app:update-progress", map[string]any{"received": w.written, "total": w.total})
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func isWindowsPE(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
var hdr [2]byte
|
||||
if _, err := io.ReadFull(f, hdr[:]); err != nil {
|
||||
return false
|
||||
}
|
||||
return hdr[0] == 'M' && hdr[1] == 'Z'
|
||||
}
|
||||
|
||||
// launchDownloadedInstaller 用独立脚本等本进程退出后再打开安装器。
|
||||
// 不能对安装器本身用 CREATE_NO_WINDOW + /S:需要 UAC 的 NSIS 会静默失败,且文件仍被占用。
|
||||
func launchDownloadedInstaller(path string) error {
|
||||
path = strings.TrimPrefix(filepath.Clean(path), `\\?\`)
|
||||
if runtime.GOOS != "windows" {
|
||||
cmd := exec.Command(path)
|
||||
if e := cmd.Start(); e != nil {
|
||||
return errors.New("INSTALLER_START_FAILED")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
bat := filepath.Join(os.TempDir(), fmt.Sprintf("cc-update-%d.bat", os.Getpid()))
|
||||
if e := os.WriteFile(bat, []byte(updateLaunchScript(os.Getpid(), path)), 0644); e != nil {
|
||||
return errors.New("SAVE_FAILED")
|
||||
}
|
||||
cmd := exec.Command("cmd", "/C", bat)
|
||||
platform.ConfigureUpdaterHelper(cmd)
|
||||
if e := cmd.Start(); e != nil {
|
||||
cmd = exec.Command("cmd", "/C", bat)
|
||||
platform.ConfigureHidden(cmd)
|
||||
if e2 := cmd.Start(); e2 != nil {
|
||||
return errors.New("INSTALLER_START_FAILED")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateLaunchScript(pid int, installer string) string {
|
||||
return fmt.Sprintf(""+
|
||||
"@echo off\r\n"+
|
||||
"set \"PID=%d\"\r\n"+
|
||||
"set /a N=0\r\n"+
|
||||
":wait\r\n"+
|
||||
"set /a N+=1\r\n"+
|
||||
"if %%N%% GEQ 90 goto launch\r\n"+
|
||||
"ping -n 2 127.0.0.1 >nul\r\n"+
|
||||
"tasklist /FI \"PID eq %%PID%%\" /NH 2>nul | find \"%%PID%%\" >nul\r\n"+
|
||||
"if not errorlevel 1 goto wait\r\n"+
|
||||
":launch\r\n"+
|
||||
"ping -n 3 127.0.0.1 >nul\r\n"+
|
||||
"start \"\" \"%s\"\r\n"+
|
||||
"del \"%%~f0\"\r\n", pid, installer)
|
||||
}
|
||||
|
||||
// versionNewer 判断 remote 是否比 local 新(简单 x.y.z 比较)。
|
||||
func versionNewer(remote, local string) bool {
|
||||
rp := parseSemver(remote)
|
||||
|
||||
Reference in New Issue
Block a user