github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/update/update.go (about)

     1  // +build !bootstrap
     2  
     3  // Package update contains code for Please auto-updating itself.
     4  // At startup, Please can check a version set in the config file. If that doesn't
     5  // match the version of the current binary, it will download the appropriate
     6  // version from the website and swap to using that instead.
     7  //
     8  // This feature is fairly directly cribbed from Buck since we found it very useful,
     9  // albeit implemented differently so it plays nicer with multiple simultaneous
    10  // builds on the same machine.
    11  package update
    12  
    13  import (
    14  	"archive/tar"
    15  	"compress/gzip"
    16  	"fmt"
    17  	"io"
    18  	"io/ioutil"
    19  	"net/http"
    20  	"os"
    21  	"os/signal"
    22  	"path"
    23  	"runtime"
    24  	"strings"
    25  	"syscall"
    26  
    27  	"github.com/coreos/go-semver/semver"
    28  	"gopkg.in/op/go-logging.v1"
    29  
    30  	"cli"
    31  	"core"
    32  )
    33  
    34  var log = logging.MustGetLogger("update")
    35  
    36  // minSignedVersion is the earliest version of Please that has a signature.
    37  var minSignedVersion = semver.Version{Major: 9, Minor: 2}
    38  
    39  // CheckAndUpdate checks whether we should update Please and does so if needed.
    40  // If it requires an update it will never return, it will either die on failure or on success will exec the new Please.
    41  // Conversely, if an update isn't required it will return. It may adjust the version in the configuration.
    42  // updatesEnabled indicates whether updates are enabled (i.e. not run with --noupdate)
    43  // updateCommand indicates whether an update is specifically requested (due to e.g. `plz update`)
    44  // forceUpdate indicates whether the user passed --force on the command line, in which case we
    45  // will always update even if the version exists.
    46  func CheckAndUpdate(config *core.Configuration, updatesEnabled, updateCommand, forceUpdate, verify bool) {
    47  	if !shouldUpdate(config, updatesEnabled, updateCommand) && !forceUpdate {
    48  		clean(config, updateCommand)
    49  		return
    50  	}
    51  	word := describe(config.Please.Version.Semver(), core.PleaseVersion, true)
    52  	if !updateCommand {
    53  		log.Warning("%s to Please version %s (currently %s)", word, config.Please.Version.VersionString(), core.PleaseVersion)
    54  	}
    55  
    56  	// Must lock here so that the update process doesn't race when running two instances
    57  	// simultaneously.
    58  	core.AcquireRepoLock()
    59  	defer core.ReleaseRepoLock()
    60  
    61  	// If the destination exists and the user passed --force, remove it to force a redownload.
    62  	newDir := core.ExpandHomePath(path.Join(config.Please.Location, config.Please.Version.VersionString()))
    63  	if forceUpdate && core.PathExists(newDir) {
    64  		if err := os.RemoveAll(newDir); err != nil {
    65  			log.Fatalf("Failed to remove existing directory: %s", err)
    66  		}
    67  	}
    68  
    69  	// Download it.
    70  	newPlease := downloadAndLinkPlease(config, verify)
    71  
    72  	// Clean out any old ones
    73  	clean(config, updateCommand)
    74  
    75  	// Now run the new one.
    76  	args := filterArgs(forceUpdate, append([]string{newPlease}, os.Args[1:]...))
    77  	log.Info("Executing %s", strings.Join(args, " "))
    78  	if err := syscall.Exec(newPlease, args, os.Environ()); err != nil {
    79  		log.Fatalf("Failed to exec new Please version %s: %s", newPlease, err)
    80  	}
    81  	// Shouldn't ever get here. We should have either exec'd or died above.
    82  	panic("please update failed in an an unexpected and exciting way")
    83  }
    84  
    85  // shouldUpdate determines whether we should run an update or not. It returns true iff one is required.
    86  func shouldUpdate(config *core.Configuration, updatesEnabled, updateCommand bool) bool {
    87  	if config.Please.Version.Semver() == core.PleaseVersion {
    88  		return false // Version matches, nothing to do here.
    89  	} else if config.Please.Version.IsGTE && config.Please.Version.LessThan(core.PleaseVersion) {
    90  		if !updateCommand {
    91  			return false // Version specified is >= and we are above it, nothing to do unless it's `plz update`
    92  		}
    93  		// Find the latest available version. Update if it's newer than the current one.
    94  		config.Please.Version = *findLatestVersion(config.Please.DownloadLocation.String())
    95  		return config.Please.Version.Semver() != core.PleaseVersion
    96  	} else if (!updatesEnabled || !config.Please.SelfUpdate) && !updateCommand {
    97  		// Update is required but has been skipped (--noupdate or whatever)
    98  		word := describe(config.Please.Version.Semver(), core.PleaseVersion, true)
    99  		log.Warning("%s to Please version %s skipped (current version: %s)", word, config.Please.Version, core.PleaseVersion)
   100  		return false
   101  	} else if config.Please.Location == "" {
   102  		log.Warning("Please location not set in config, cannot auto-update.")
   103  		return false
   104  	} else if config.Please.DownloadLocation == "" {
   105  		log.Warning("Please download location not set in config, cannot auto-update.")
   106  		return false
   107  	}
   108  	if config.Please.Version.Major == 0 {
   109  		// Specific version isn't set, only update on `plz update`.
   110  		if !updateCommand {
   111  			config.Please.Version.Set(core.PleaseVersion.String())
   112  			return false
   113  		}
   114  		config.Please.Version = *findLatestVersion(config.Please.DownloadLocation.String())
   115  		return shouldUpdate(config, updatesEnabled, updateCommand)
   116  	}
   117  	return true
   118  }
   119  
   120  // downloadAndLinkPlease downloads a new Please version and links it into place, if needed.
   121  // It returns the new location and dies on failure.
   122  func downloadAndLinkPlease(config *core.Configuration, verify bool) string {
   123  	config.Please.Location = core.ExpandHomePath(config.Please.Location)
   124  	newPlease := path.Join(config.Please.Location, config.Please.Version.VersionString(), "please")
   125  
   126  	if !core.PathExists(newPlease) {
   127  		downloadPlease(config, verify)
   128  	}
   129  	if !verifyNewPlease(newPlease, config.Please.Version.VersionString()) {
   130  		cleanDir(path.Join(config.Please.Location, config.Please.Version.VersionString()))
   131  		log.Fatalf("Not continuing.")
   132  	}
   133  	linkNewPlease(config)
   134  	return newPlease
   135  }
   136  
   137  func downloadPlease(config *core.Configuration, verify bool) {
   138  	newDir := path.Join(config.Please.Location, config.Please.Version.VersionString())
   139  	if err := os.MkdirAll(newDir, core.DirPermissions); err != nil {
   140  		log.Fatalf("Failed to create directory %s: %s", newDir, err)
   141  	}
   142  
   143  	// Make sure from here on that we don't leave partial directories hanging about.
   144  	// If someone ctrl+C's during this download then on re-running we might
   145  	// have partial files written there that don't really work.
   146  	defer func() {
   147  		if r := recover(); r != nil {
   148  			cleanDir(newDir)
   149  			log.Fatalf("Failed to download Please: %s", r)
   150  		}
   151  	}()
   152  	go handleSignals(newDir)
   153  	mustClose := func(closer io.Closer) {
   154  		if err := closer.Close(); err != nil {
   155  			panic(err)
   156  		}
   157  	}
   158  
   159  	url := strings.TrimSuffix(config.Please.DownloadLocation.String(), "/")
   160  	url = fmt.Sprintf("%s/%s_%s/%s/please_%s.tar.gz", url, runtime.GOOS, runtime.GOARCH, config.Please.Version.VersionString(), config.Please.Version.VersionString())
   161  	rc := mustDownload(url, true)
   162  	defer mustClose(rc)
   163  	var r io.Reader = rc
   164  
   165  	if verify && config.Please.Version.LessThan(minSignedVersion) {
   166  		log.Warning("Won't verify signature of download, version is too old to be signed.")
   167  	} else if verify {
   168  		r = verifyDownload(r, url)
   169  	} else {
   170  		log.Warning("Signature verification disabled for %s", url)
   171  	}
   172  
   173  	gzreader, err := gzip.NewReader(r)
   174  	if err != nil {
   175  		panic(fmt.Sprintf("%s isn't a valid gzip file: %s", url, err))
   176  	}
   177  	defer mustClose(gzreader)
   178  
   179  	tarball := tar.NewReader(gzreader)
   180  	for {
   181  		hdr, err := tarball.Next()
   182  		if err == io.EOF {
   183  			break // End of archive
   184  		} else if err != nil {
   185  			panic(fmt.Sprintf("Error un-tarring %s: %s", url, err))
   186  		} else if err := writeTarFile(hdr, tarball, newDir); err != nil {
   187  			panic(err)
   188  		}
   189  	}
   190  }
   191  
   192  // mustDownload downloads the contents of the given URL and returns its body
   193  // The caller must close the reader when done.
   194  // It panics if the download fails.
   195  func mustDownload(url string, progress bool) io.ReadCloser {
   196  	log.Info("Downloading %s", url)
   197  	response, err := http.Get(url)
   198  	if err != nil {
   199  		panic(fmt.Sprintf("Failed to download %s: %s", url, err))
   200  	} else if response.StatusCode < 200 || response.StatusCode > 299 {
   201  		panic(fmt.Sprintf("Failed to download %s: got response %s", url, response.Status))
   202  	} else if progress {
   203  		return cli.NewProgressReader(response.Body, response.Header.Get("Content-Length"))
   204  	}
   205  	return response.Body
   206  }
   207  
   208  func linkNewPlease(config *core.Configuration) {
   209  	if files, err := ioutil.ReadDir(path.Join(config.Please.Location, config.Please.Version.VersionString())); err != nil {
   210  		log.Fatalf("Failed to read directory: %s", err)
   211  	} else {
   212  		for _, file := range files {
   213  			linkNewFile(config, file.Name())
   214  		}
   215  	}
   216  }
   217  
   218  func linkNewFile(config *core.Configuration, file string) {
   219  	newDir := path.Join(config.Please.Location, config.Please.Version.VersionString())
   220  	globalFile := path.Join(config.Please.Location, file)
   221  	downloadedFile := path.Join(newDir, file)
   222  	if err := os.RemoveAll(globalFile); err != nil {
   223  		log.Fatalf("Failed to remove existing file %s: %s", globalFile, err)
   224  	}
   225  	if err := os.Symlink(downloadedFile, globalFile); err != nil {
   226  		log.Fatalf("Error linking %s -> %s: %s", downloadedFile, globalFile, err)
   227  	}
   228  	log.Info("Linked %s -> %s", globalFile, downloadedFile)
   229  }
   230  
   231  func fileMode(filename string) os.FileMode {
   232  	if strings.HasSuffix(filename, ".jar") || strings.HasSuffix(filename, ".so") {
   233  		return 0664 // The .jar files obviously aren't executable
   234  	}
   235  	return 0775 // Everything else we download is.
   236  }
   237  
   238  func cleanDir(newDir string) {
   239  	log.Notice("Attempting to clean directory %s", newDir)
   240  	if err := os.RemoveAll(newDir); err != nil {
   241  		log.Errorf("Failed to clean %s: %s", newDir, err)
   242  	}
   243  }
   244  
   245  // handleSignals traps SIGINT and SIGKILL (if possible) and on receiving one cleans the given directory.
   246  func handleSignals(newDir string) {
   247  	c := make(chan os.Signal, 1)
   248  	signal.Notify(c, os.Interrupt, os.Kill)
   249  	s := <-c
   250  	log.Notice("Got signal %s", s)
   251  	cleanDir(newDir)
   252  	log.Fatalf("Got signal %s", s)
   253  }
   254  
   255  // findLatestVersion attempts to find the latest available version of plz.
   256  func findLatestVersion(downloadLocation string) *cli.Version {
   257  	url := strings.TrimRight(downloadLocation, "/") + "/latest_version"
   258  	log.Info("Downloading %s", url)
   259  	response, err := http.Get(url)
   260  	if err != nil {
   261  		log.Fatalf("Failed to find latest plz version: %s", err)
   262  	} else if response.StatusCode < 200 || response.StatusCode > 299 {
   263  		log.Fatalf("Failed to find latest plz version: %s", response.Status)
   264  	}
   265  	defer response.Body.Close()
   266  	data, err := ioutil.ReadAll(response.Body)
   267  	if err != nil {
   268  		log.Fatalf("Failed to find latest plz version: %s", err)
   269  	}
   270  	v := &cli.Version{}
   271  	if err := v.UnmarshalFlag(strings.TrimSpace(string(data))); err != nil {
   272  		log.Fatalf("Failed to parse version: %s", string(data))
   273  	}
   274  	return v
   275  }
   276  
   277  // describe returns a word describing the process we're about to do ("update", "downgrading", etc)
   278  func describe(a, b semver.Version, verb bool) string {
   279  	if verb && a.LessThan(b) {
   280  		return "Downgrading"
   281  	} else if verb {
   282  		return "Upgrading"
   283  	} else if a.LessThan(b) {
   284  		return "Downgrade"
   285  	}
   286  	return "Upgrade"
   287  }
   288  
   289  // verifyNewPlease calls a newly downloaded Please version to verify it's the expected version.
   290  // It returns true iff the version is as expected.
   291  func verifyNewPlease(newPlease, version string) bool {
   292  	version = "Please version " + version // Output is prefixed with this.
   293  	cmd := core.ExecCommand(newPlease, "--version")
   294  	output, err := cmd.Output()
   295  	if err != nil {
   296  		log.Errorf("Failed to run new Please: %s", err)
   297  		return false
   298  	}
   299  	if strings.TrimSpace(string(output)) != version {
   300  		log.Errorf("Bad version of Please downloaded: expected %s, but it's actually %s", version, string(output))
   301  		return false
   302  	}
   303  	return true
   304  }
   305  
   306  // writeTarFile writes a file from a tarball to the filesystem in the corresponding location.
   307  func writeTarFile(hdr *tar.Header, r io.Reader, destination string) error {
   308  	// Strip the first directory component in the tarball
   309  	stripped := hdr.Name[strings.IndexRune(hdr.Name, os.PathSeparator)+1:]
   310  	dest := path.Join(destination, stripped)
   311  	if err := os.MkdirAll(path.Dir(dest), core.DirPermissions); err != nil {
   312  		return fmt.Errorf("Can't make destination directory: %s", err)
   313  	}
   314  	// Handle symlinks, but not other non-file things.
   315  	if hdr.Typeflag == tar.TypeSymlink {
   316  		return os.Symlink(hdr.Linkname, dest)
   317  	} else if hdr.Typeflag != tar.TypeReg {
   318  		return nil // Don't write directory entries, or rely on them being present.
   319  	}
   320  	log.Info("Extracting %s to %s", hdr.Name, dest)
   321  	f, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, os.FileMode(hdr.Mode))
   322  	if err != nil {
   323  		return err
   324  	}
   325  	defer f.Close()
   326  	_, err = io.Copy(f, r)
   327  	return err
   328  }
   329  
   330  // filterArgs filters out the --force update if forced updates were specified.
   331  // This is important so that we don't end up in a loop of repeatedly forcing re-downloads.
   332  func filterArgs(forceUpdate bool, args []string) []string {
   333  	if !forceUpdate {
   334  		return args
   335  	}
   336  	ret := args[:0]
   337  	for _, arg := range args {
   338  		if arg != "--force" {
   339  			ret = append(ret, arg)
   340  		}
   341  	}
   342  	return ret
   343  }