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 }