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 }