github.com/metacubex/mihomo@v1.18.5/component/updater/update_core.go (about) 1 package updater 2 3 import ( 4 "archive/zip" 5 "compress/gzip" 6 "context" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "sync" 16 "time" 17 18 mihomoHttp "github.com/metacubex/mihomo/component/http" 19 C "github.com/metacubex/mihomo/constant" 20 "github.com/metacubex/mihomo/log" 21 22 "github.com/klauspost/cpuid/v2" 23 ) 24 25 // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/updater/updater.go 26 // Updater is the mihomo updater. 27 var ( 28 goarm string 29 gomips string 30 amd64Compatible string 31 32 workDir string 33 34 // mu protects all fields below. 35 mu sync.Mutex 36 37 currentExeName string // 当前可执行文件 38 updateDir string // 更新目录 39 packageName string // 更新压缩文件 40 backupDir string // 备份目录 41 backupExeName string // 备份文件名 42 updateExeName string // 更新后的可执行文件 43 44 baseURL string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/mihomo" 45 versionURL string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt" 46 packageURL string 47 latestVersion string 48 ) 49 50 func init() { 51 if runtime.GOARCH == "amd64" && cpuid.CPU.X64Level() < 3 { 52 amd64Compatible = "-compatible" 53 } 54 if !strings.HasPrefix(C.Version, "alpha") { 55 baseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/mihomo" 56 versionURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt" 57 } 58 } 59 60 type updateError struct { 61 Message string 62 } 63 64 func (e *updateError) Error() string { 65 return fmt.Sprintf("update error: %s", e.Message) 66 } 67 68 // Update performs the auto-updater. It returns an error if the updater failed. 69 // If firstRun is true, it assumes the configuration file doesn't exist. 70 func UpdateCore(execPath string) (err error) { 71 mu.Lock() 72 defer mu.Unlock() 73 74 latestVersion, err = getLatestVersion() 75 if err != nil { 76 return err 77 } 78 79 log.Infoln("current version %s, latest version %s", C.Version, latestVersion) 80 81 if latestVersion == C.Version { 82 err := &updateError{Message: "already using latest version"} 83 return err 84 } 85 86 updateDownloadURL() 87 88 defer func() { 89 if err != nil { 90 log.Errorln("updater: failed: %v", err) 91 } else { 92 log.Infoln("updater: finished") 93 } 94 }() 95 96 workDir = filepath.Dir(execPath) 97 98 err = prepare(execPath) 99 if err != nil { 100 return fmt.Errorf("preparing: %w", err) 101 } 102 103 defer clean() 104 105 err = downloadPackageFile() 106 if err != nil { 107 return fmt.Errorf("downloading package file: %w", err) 108 } 109 110 err = unpack() 111 if err != nil { 112 return fmt.Errorf("unpacking: %w", err) 113 } 114 115 err = backup() 116 if err != nil { 117 return fmt.Errorf("backuping: %w", err) 118 } 119 120 err = replace() 121 if err != nil { 122 return fmt.Errorf("replacing: %w", err) 123 } 124 125 return nil 126 } 127 128 // prepare fills all necessary fields in Updater object. 129 func prepare(exePath string) (err error) { 130 updateDir = filepath.Join(workDir, "meta-update") 131 currentExeName = exePath 132 _, pkgNameOnly := filepath.Split(packageURL) 133 if pkgNameOnly == "" { 134 return fmt.Errorf("invalid PackageURL: %q", packageURL) 135 } 136 137 packageName = filepath.Join(updateDir, pkgNameOnly) 138 //log.Infoln(packageName) 139 backupDir = filepath.Join(workDir, "meta-backup") 140 141 if runtime.GOOS == "windows" { 142 updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible + ".exe" 143 } else if runtime.GOOS == "android" && runtime.GOARCH == "arm64" { 144 updateExeName = "mihomo-android-arm64-v8" 145 } else { 146 updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible 147 } 148 149 log.Infoln("updateExeName: %s ", updateExeName) 150 151 backupExeName = filepath.Join(backupDir, filepath.Base(exePath)) 152 updateExeName = filepath.Join(updateDir, updateExeName) 153 154 log.Infoln( 155 "updater: updating using url: %s", 156 packageURL, 157 ) 158 159 currentExeName = exePath 160 _, err = os.Stat(currentExeName) 161 if err != nil { 162 return fmt.Errorf("checking %q: %w", currentExeName, err) 163 } 164 165 return nil 166 } 167 168 // unpack extracts the files from the downloaded archive. 169 func unpack() error { 170 var err error 171 _, pkgNameOnly := filepath.Split(packageURL) 172 173 log.Infoln("updater: unpacking package") 174 if strings.HasSuffix(pkgNameOnly, ".zip") { 175 _, err = zipFileUnpack(packageName, updateDir) 176 if err != nil { 177 return fmt.Errorf(".zip unpack failed: %w", err) 178 } 179 180 } else if strings.HasSuffix(pkgNameOnly, ".gz") { 181 _, err = gzFileUnpack(packageName, updateDir) 182 if err != nil { 183 return fmt.Errorf(".gz unpack failed: %w", err) 184 } 185 186 } else { 187 return fmt.Errorf("unknown package extension") 188 } 189 190 return nil 191 } 192 193 // backup makes a backup of the current executable file 194 func backup() (err error) { 195 log.Infoln("updater: backing up current ExecFile:%s to %s", currentExeName, backupExeName) 196 _ = os.Mkdir(backupDir, 0o755) 197 198 err = os.Rename(currentExeName, backupExeName) 199 if err != nil { 200 return err 201 } 202 203 return nil 204 } 205 206 // replace moves the current executable with the updated one 207 func replace() error { 208 var err error 209 210 log.Infoln("replacing: %s to %s", updateExeName, currentExeName) 211 if runtime.GOOS == "windows" { 212 // rename fails with "File in use" error 213 err = copyFile(updateExeName, currentExeName) 214 } else { 215 err = os.Rename(updateExeName, currentExeName) 216 } 217 if err != nil { 218 return err 219 } 220 221 log.Infoln("updater: renamed: %s to %s", updateExeName, currentExeName) 222 223 return nil 224 } 225 226 // clean removes the temporary directory itself and all it's contents. 227 func clean() { 228 _ = os.RemoveAll(updateDir) 229 } 230 231 // MaxPackageFileSize is a maximum package file length in bytes. The largest 232 // package whose size is limited by this constant currently has the size of 233 // approximately 9 MiB. 234 const MaxPackageFileSize = 32 * 1024 * 1024 235 236 // Download package file and save it to disk 237 func downloadPackageFile() (err error) { 238 ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) 239 defer cancel() 240 resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) 241 if err != nil { 242 return fmt.Errorf("http request failed: %w", err) 243 } 244 245 defer func() { 246 closeErr := resp.Body.Close() 247 if closeErr != nil && err == nil { 248 err = closeErr 249 } 250 }() 251 252 var r io.Reader 253 r, err = LimitReader(resp.Body, MaxPackageFileSize) 254 if err != nil { 255 return fmt.Errorf("http request failed: %w", err) 256 } 257 258 log.Debugln("updater: reading http body") 259 // This use of ReadAll is now safe, because we limited body's Reader. 260 body, err := io.ReadAll(r) 261 if err != nil { 262 return fmt.Errorf("io.ReadAll() failed: %w", err) 263 } 264 265 log.Debugln("updateDir %s", updateDir) 266 err = os.Mkdir(updateDir, 0o755) 267 if err != nil { 268 return fmt.Errorf("mkdir error: %w", err) 269 } 270 271 log.Debugln("updater: saving package to file %s", packageName) 272 err = os.WriteFile(packageName, body, 0o644) 273 if err != nil { 274 return fmt.Errorf("os.WriteFile() failed: %w", err) 275 } 276 return nil 277 } 278 279 // Unpack a single .gz file to the specified directory 280 // Existing files are overwritten 281 // All files are created inside outDir, subdirectories are not created 282 // Return the output file name 283 func gzFileUnpack(gzfile, outDir string) (string, error) { 284 f, err := os.Open(gzfile) 285 if err != nil { 286 return "", fmt.Errorf("os.Open(): %w", err) 287 } 288 289 defer func() { 290 closeErr := f.Close() 291 if closeErr != nil && err == nil { 292 err = closeErr 293 } 294 }() 295 296 gzReader, err := gzip.NewReader(f) 297 if err != nil { 298 return "", fmt.Errorf("gzip.NewReader(): %w", err) 299 } 300 301 defer func() { 302 closeErr := gzReader.Close() 303 if closeErr != nil && err == nil { 304 err = closeErr 305 } 306 }() 307 // Get the original file name from the .gz file header 308 originalName := gzReader.Header.Name 309 if originalName == "" { 310 // Fallback: remove the .gz extension from the input file name if the header doesn't provide the original name 311 originalName = filepath.Base(gzfile) 312 originalName = strings.TrimSuffix(originalName, ".gz") 313 } 314 315 outputName := filepath.Join(outDir, originalName) 316 317 // Create the output file 318 wc, err := os.OpenFile( 319 outputName, 320 os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 321 0o755, 322 ) 323 if err != nil { 324 return "", fmt.Errorf("os.OpenFile(%s): %w", outputName, err) 325 } 326 327 defer func() { 328 closeErr := wc.Close() 329 if closeErr != nil && err == nil { 330 err = closeErr 331 } 332 }() 333 334 // Copy the contents of the gzReader to the output file 335 _, err = io.Copy(wc, gzReader) 336 if err != nil { 337 return "", fmt.Errorf("io.Copy(): %w", err) 338 } 339 340 return outputName, nil 341 } 342 343 // Unpack a single file from .zip file to the specified directory 344 // Existing files are overwritten 345 // All files are created inside 'outDir', subdirectories are not created 346 // Return the output file name 347 func zipFileUnpack(zipfile, outDir string) (string, error) { 348 zrc, err := zip.OpenReader(zipfile) 349 if err != nil { 350 return "", fmt.Errorf("zip.OpenReader(): %w", err) 351 } 352 353 defer func() { 354 closeErr := zrc.Close() 355 if closeErr != nil && err == nil { 356 err = closeErr 357 } 358 }() 359 if len(zrc.File) == 0 { 360 return "", fmt.Errorf("no files in the zip archive") 361 } 362 363 // Assuming the first file in the zip archive is the target file 364 zf := zrc.File[0] 365 var rc io.ReadCloser 366 rc, err = zf.Open() 367 if err != nil { 368 return "", fmt.Errorf("zip file Open(): %w", err) 369 } 370 371 defer func() { 372 closeErr := rc.Close() 373 if closeErr != nil && err == nil { 374 err = closeErr 375 } 376 }() 377 fi := zf.FileInfo() 378 name := fi.Name() 379 outputName := filepath.Join(outDir, name) 380 381 if fi.IsDir() { 382 return "", fmt.Errorf("the target file is a directory") 383 } 384 385 var wc io.WriteCloser 386 wc, err = os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) 387 if err != nil { 388 return "", fmt.Errorf("os.OpenFile(): %w", err) 389 } 390 391 defer func() { 392 closeErr := wc.Close() 393 if closeErr != nil && err == nil { 394 err = closeErr 395 } 396 }() 397 _, err = io.Copy(wc, rc) 398 if err != nil { 399 return "", fmt.Errorf("io.Copy(): %w", err) 400 } 401 402 return outputName, nil 403 } 404 405 // Copy file on disk 406 func copyFile(src, dst string) error { 407 d, e := os.ReadFile(src) 408 if e != nil { 409 return e 410 } 411 e = os.WriteFile(dst, d, 0o644) 412 if e != nil { 413 return e 414 } 415 return nil 416 } 417 418 func getLatestVersion() (version string, err error) { 419 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 420 defer cancel() 421 resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) 422 if err != nil { 423 return "", fmt.Errorf("get Latest Version fail: %w", err) 424 } 425 defer func() { 426 closeErr := resp.Body.Close() 427 if closeErr != nil && err == nil { 428 err = closeErr 429 } 430 }() 431 432 body, err := io.ReadAll(resp.Body) 433 if err != nil { 434 return "", fmt.Errorf("get Latest Version fail: %w", err) 435 } 436 content := strings.TrimRight(string(body), "\n") 437 return content, nil 438 } 439 440 func updateDownloadURL() { 441 var middle string 442 443 if runtime.GOARCH == "arm" && probeGoARM() { 444 //-linux-armv7-alpha-e552b54.gz 445 middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, goarm, latestVersion) 446 } else if runtime.GOARCH == "arm64" { 447 //-linux-arm64-alpha-e552b54.gz 448 if runtime.GOOS == "android" { 449 middle = fmt.Sprintf("-%s-%s-v8-%s", runtime.GOOS, runtime.GOARCH, latestVersion) 450 } else { 451 middle = fmt.Sprintf("-%s-%s-%s", runtime.GOOS, runtime.GOARCH, latestVersion) 452 } 453 } else if isMIPS(runtime.GOARCH) && gomips != "" { 454 middle = fmt.Sprintf("-%s-%s-%s-%s", runtime.GOOS, runtime.GOARCH, gomips, latestVersion) 455 } else { 456 middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, amd64Compatible, latestVersion) 457 } 458 459 if runtime.GOOS == "windows" { 460 middle += ".zip" 461 } else { 462 middle += ".gz" 463 } 464 packageURL = baseURL + middle 465 //log.Infoln(packageURL) 466 } 467 468 // isMIPS returns true if arch is any MIPS architecture. 469 func isMIPS(arch string) (ok bool) { 470 switch arch { 471 case 472 "mips", 473 "mips64", 474 "mips64le", 475 "mipsle": 476 return true 477 default: 478 return false 479 } 480 } 481 482 // linux only 483 func probeGoARM() (ok bool) { 484 cmd := exec.Command("cat", "/proc/cpuinfo") 485 output, err := cmd.Output() 486 if err != nil { 487 log.Errorln("probe goarm error:%s", err) 488 return false 489 } 490 cpuInfo := string(output) 491 if strings.Contains(cpuInfo, "vfpv3") || strings.Contains(cpuInfo, "vfpv4") { 492 goarm = "v7" 493 } else if strings.Contains(cpuInfo, "vfp") { 494 goarm = "v6" 495 } else { 496 goarm = "v5" 497 } 498 return true 499 }