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