| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package selfupdate |
| |
| import ( |
| "archive/zip" |
| "bytes" |
| "context" |
| "fmt" |
| "io" |
| "net/http" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "syscall" |
| |
| "go.fuchsia.dev/jiri/gitutil" |
| "go.fuchsia.dev/jiri/log" |
| "go.fuchsia.dev/jiri/osutil" |
| "go.fuchsia.dev/jiri/version" |
| ) |
| |
| const ( |
| JiriRepository = "https://fuchsia.googlesource.com/jiri" |
| JiriCIPDEndPoint = "https://chrome-infra-packages.appspot.com/dl/fuchsia/tools/jiri" |
| ) |
| |
| var ( |
| updateTestVersionErr = fmt.Errorf("jiri has test version") |
| updateVersionErr = fmt.Errorf("jiri is already at latest version") |
| updateNotAvailableErr = fmt.Errorf("latest version of jiri not available") |
| ) |
| |
| // Update checks whether a new version of Jiri is available and if so, |
| // it will download it and replace the current version with the new one. |
| func Update(ctx context.Context, force bool) error { |
| if !force && version.GitCommit == "" { |
| return updateTestVersionErr |
| } |
| commit, err := getCurrentCommit(ctx, JiriRepository) |
| if err != nil { |
| return err |
| } |
| if force || commit != version.GitCommit { |
| // CIPD HTTP endpoint does not allow HTTP HEAD. |
| // Download the Jiri archive directly. |
| b, err := downloadBinary(ctx, JiriCIPDEndPoint, commit) |
| if err == updateNotAvailableErr { |
| return err |
| } |
| if err != nil { |
| return fmt.Errorf("cannot download latest jiri binary, %s", err) |
| } |
| unarchivedBinary, err := unarchiveJiri(b) |
| if err != nil { |
| return err |
| } |
| path, err := osutil.Executable() |
| if err != nil { |
| return fmt.Errorf("cannot get executable path, %s", err) |
| } |
| return updateExecutable(path, unarchivedBinary) |
| } |
| return updateVersionErr |
| } |
| |
| func unarchiveJiri(b []byte) ([]byte, error) { |
| zipReader, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) |
| if err != nil { |
| return nil, fmt.Errorf("Failed to read jiri archive: %v", err) |
| } |
| for _, file := range zipReader.File { |
| if file.Name == "jiri" { |
| fileReader, err := file.Open() |
| defer fileReader.Close() |
| if err != nil { |
| return nil, fmt.Errorf("Failed to read jiri archive: %v", err) |
| } |
| return io.ReadAll(fileReader) |
| } |
| } |
| return nil, fmt.Errorf("Cannot find jiri in update archive") |
| } |
| |
| func UpdateAndExecute(ctx context.Context, force bool) error { |
| // Capture executable path before it is replaced in Update func |
| path, err := osutil.Executable() |
| if err != nil { |
| return fmt.Errorf("cannot get executable path, %s", err) |
| } |
| if err := Update(ctx, force); err != nil { |
| if err != updateNotAvailableErr && err != updateVersionErr && |
| err != updateTestVersionErr { |
| return err |
| } |
| log.Debugf(ctx, "selfupdate not possible: %s", err) |
| // No update available, so continue normal execution with the current |
| // executable instead of doing a redundant exec of the same executable. |
| return nil |
| } |
| |
| args := []string{} |
| for _, a := range os.Args { |
| if !strings.HasPrefix(a, "-force-autoupdate") { |
| args = append(args, a) |
| } |
| } |
| |
| // Run the updated version. |
| if err = syscall.Exec(path, args, os.Environ()); err != nil { |
| return fmt.Errorf("cannot execute %s: %s", path, err) |
| } |
| return nil |
| } |
| |
| func getCurrentCommit(ctx context.Context, repository string) (string, error) { |
| out, err := gitutil.New(ctx).LsRemote(ctx, repository, "refs/heads/main") |
| if err != nil { |
| return "", err |
| } |
| return strings.Fields(out)[0], nil |
| } |
| |
| func downloadBinary(ctx context.Context, endpoint, version string) ([]byte, error) { |
| os := runtime.GOOS |
| if os == "darwin" { |
| os = "mac" |
| } |
| url := fmt.Sprintf("%s/%s-%s/+/git_revision:%s", endpoint, os, runtime.GOARCH, version) |
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) |
| if err != nil { |
| return nil, err |
| } |
| res, err := http.DefaultClient.Do(req) |
| if err != nil { |
| return nil, err |
| } |
| defer res.Body.Close() |
| if res.StatusCode == http.StatusNotFound { |
| return nil, updateNotAvailableErr |
| } |
| if err := checkHTTPResponseErrors(res); err != nil { |
| return nil, err |
| } |
| |
| bytes, err := io.ReadAll(res.Body) |
| if err != nil { |
| return nil, err |
| } |
| |
| return bytes, nil |
| } |
| |
| func checkHTTPResponseErrors(res *http.Response) error { |
| if res.StatusCode == http.StatusOK { |
| return nil |
| } |
| // don't read the full body to protect against large errors |
| body, err := io.ReadAll(io.LimitReader(res.Body, 1024)) |
| if err != nil { |
| return fmt.Errorf("HTTP request failed: %s (failed to read body: %w)", res.Status, err) |
| } |
| trimmed := strings.TrimSpace(string(body)) |
| if trimmed != "" { |
| return fmt.Errorf("HTTP request failed: %s: %s", res.Status, maybeTruncateString(trimmed, 200)) |
| } |
| return fmt.Errorf("HTTP request failed: %s", res.Status) |
| } |
| |
| func maybeTruncateString(s string, maxLen int) string { |
| count := 0 |
| for i := range s { |
| if count == maxLen { |
| return s[:i] + "..." |
| } |
| count++ |
| } |
| return s |
| } |
| |
| func updateExecutable(path string, b []byte) error { |
| fi, err := os.Stat(path) |
| if err != nil { |
| return err |
| } |
| |
| dir := filepath.Dir(path) |
| |
| // Write the new version to a file. |
| newfile, err := os.CreateTemp(dir, "jiri") |
| if err != nil { |
| return err |
| } |
| |
| if _, err := newfile.Write(b); err != nil { |
| return err |
| } |
| |
| if err := newfile.Chmod(fi.Mode()); err != nil { |
| return err |
| } |
| |
| if err := newfile.Close(); err != nil { |
| return err |
| } |
| |
| // Backup the existing version. |
| oldfile, err := os.CreateTemp(dir, "jiri") |
| if err != nil { |
| return err |
| } |
| defer os.Remove(oldfile.Name()) |
| |
| if err := oldfile.Close(); err != nil { |
| return err |
| } |
| |
| err = osutil.Rename(path, oldfile.Name()) |
| if err != nil { |
| return err |
| } |
| |
| // Replace the existing version. |
| err = osutil.Rename(newfile.Name(), path) |
| if err != nil { |
| // Try to rollback the change in case of error. |
| rerr := osutil.Rename(oldfile.Name(), path) |
| if rerr != nil { |
| return rerr |
| } |
| return err |
| } |
| |
| return nil |
| } |