Skip to content

Commit 6820f67

Browse files
DASungtaclaude
andcommitted
fix(update): 权限不足时提权替换二进制文件
安装在 /usr/local/bin(root 所有)时 update 命令报 permission denied: - Download() 改写到 os.TempDir() 避免安装目录写权限问题 - Replace() 失败且为权限错误时自动提权(osascript/sudo mv + chmod 755) 与 uninstall 的 removePrivileged 模式一致。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2326d03 commit 6820f67

3 files changed

Lines changed: 38 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [v0.4.4] - 2026-04-17
6+
7+
### Bug Fixes
8+
9+
- **`update` 命令 permission denied**:二进制安装在 `/usr/local/bin`(root 所有)时,`update` 写临时文件和替换二进制均因权限不足失败。
10+
- `Download()` 改为写入系统临时目录(`os.TempDir()`),彻底绕开安装目录写权限问题
11+
- `Replace()` 失败且错误为 permission denied 时,自动通过 osascript(macOS GUI)或 sudo(SSH/Linux)提权执行 `mv + chmod 755`,与 `uninstall` 的处理模式一致
12+
13+
---
14+
515
## [v0.4.3] - 2026-04-17
616

717
### Bug Fixes

cmd/trae-proxy/main.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,13 @@ func updateCmd() *cobra.Command {
424424

425425
fmt.Printf("[update] installing to %s...\n", exePath)
426426
if err := updater.Replace(exePath, tmpPath); err != nil {
427-
return err
427+
if !os.IsPermission(err) {
428+
return err
429+
}
430+
fmt.Println("[update] 权限不足,正在请求系统授权...")
431+
if err2 := replacePrivileged(tmpPath, exePath); err2 != nil {
432+
return fmt.Errorf("replace binary: %w", err2)
433+
}
428434
}
429435

430436
fmt.Printf("[update] updated from %s to %s\n", oldVersion, tag)
@@ -758,6 +764,21 @@ func removePrivileged(path string) error {
758764
}
759765
}
760766

767+
func replacePrivileged(src, dest string) error {
768+
switch runtime.GOOS {
769+
case "darwin":
770+
return privilege.RunPrivileged("mv -f " + shellQuote(src) + " " + shellQuote(dest) + " && chmod 755 " + shellQuote(dest))
771+
case "linux":
772+
cmd := exec.Command("sudo", "sh", "-c", "mv -f "+shellQuote(src)+" "+shellQuote(dest)+" && chmod 755 "+shellQuote(dest))
773+
cmd.Stdin = os.Stdin
774+
cmd.Stdout = os.Stdout
775+
cmd.Stderr = os.Stderr
776+
return cmd.Run()
777+
default:
778+
return exec.Command("cmd", "/C", "move", "/Y", src, dest).Run()
779+
}
780+
}
781+
761782
func findPort443Process() int {
762783
if runtime.GOOS == "windows" {
763784
return 0

internal/updater/updater.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,10 @@ func (u *Updater) FetchChecksum(tag, assetName string) (string, error) {
108108
return "", fmt.Errorf("checksum for %q not found in checksums.txt", assetName)
109109
}
110110

111-
// Download fetches the release asset to a temp file in the same directory as
112-
// destPath (guarantees same filesystem for atomic rename). Returns the temp path.
113-
func (u *Updater) Download(tag, assetName, destPath string) (string, error) {
111+
// Download fetches the release asset to a temp file in os.TempDir() and returns
112+
// the temp path. Using the system temp dir avoids permission issues when the
113+
// current binary lives in a root-owned directory (e.g. /usr/local/bin).
114+
func (u *Updater) Download(tag, assetName, _ string) (string, error) {
114115
url := fmt.Sprintf("%s/%s/releases/download/%s/%s", ghBase, repo, tag, assetName)
115116
resp, err := u.Client.Get(url)
116117
if err != nil {
@@ -122,11 +123,11 @@ func (u *Updater) Download(tag, assetName, destPath string) (string, error) {
122123
return "", fmt.Errorf("download returned HTTP %d", resp.StatusCode)
123124
}
124125

125-
tmpPath := destPath + ".new"
126-
f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
126+
f, err := os.CreateTemp("", "trae-proxy-*.new")
127127
if err != nil {
128128
return "", fmt.Errorf("create temp file: %w", err)
129129
}
130+
tmpPath := f.Name()
130131

131132
if _, err := io.Copy(f, resp.Body); err != nil {
132133
f.Close()

0 commit comments

Comments
 (0)