github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/update.go (about)

     1  // Copyright 2016 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package jiri
     6  
     7  import (
     8  	"archive/zip"
     9  	"bufio"
    10  	"bytes"
    11  	"encoding/json"
    12  	"fmt"
    13  	"io/ioutil"
    14  	"net/http"
    15  	"net/url"
    16  	"os"
    17  	"path"
    18  	"path/filepath"
    19  	"runtime"
    20  	"strings"
    21  	"syscall"
    22  
    23  	"github.com/btwiuse/jiri/osutil"
    24  	"github.com/btwiuse/jiri/version"
    25  )
    26  
    27  const (
    28  	JiriRepository   = "https://github.com/btwiuse/jiri"
    29  	JiriCIPDEndPoint = "https://chrome-infra-packages.appspot.com/dl/fuchsia/tools/jiri"
    30  )
    31  
    32  var (
    33  	updateTestVersionErr  = fmt.Errorf("jiri has test version")
    34  	updateVersionErr      = fmt.Errorf("jiri is already at latest version")
    35  	updateNotAvailableErr = fmt.Errorf("latest version of jiri not available")
    36  )
    37  
    38  // Update checks whether a new version of Jiri is available and if so,
    39  // it will download it and replace the current version with the new one.
    40  func Update(force bool) error {
    41  	if !force && version.GitCommit == "" {
    42  		return updateTestVersionErr
    43  	}
    44  	commit, err := getCurrentCommit(JiriRepository)
    45  	if err != nil {
    46  		return err
    47  	}
    48  	if force || commit != version.GitCommit {
    49  		// CIPD HTTP endpoint does not allow HTTP HEAD.
    50  		// Download the Jiri archive directly.
    51  		b, err := downloadBinary(JiriCIPDEndPoint, commit)
    52  		if err == updateNotAvailableErr {
    53  			return err
    54  		}
    55  		if err != nil {
    56  			return fmt.Errorf("cannot download latest jiri binary, %s", err)
    57  		}
    58  		unarchivedBinary, err := unarchiveJiri(b)
    59  		if err != nil {
    60  			return err
    61  		}
    62  		path, err := osutil.Executable()
    63  		if err != nil {
    64  			return fmt.Errorf("cannot get executable path, %s", err)
    65  		}
    66  		return updateExecutable(path, unarchivedBinary)
    67  	}
    68  	return updateVersionErr
    69  }
    70  
    71  func unarchiveJiri(b []byte) ([]byte, error) {
    72  	zipReader, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
    73  	if err != nil {
    74  		return nil, fmt.Errorf("Failed to read jiri archive: %v", err)
    75  	}
    76  	for _, file := range zipReader.File {
    77  		if file.Name == "jiri" {
    78  			fileReader, err := file.Open()
    79  			defer fileReader.Close()
    80  			if err != nil {
    81  				return nil, fmt.Errorf("Failed to read jiri archive: %v", err)
    82  			}
    83  			return ioutil.ReadAll(fileReader)
    84  		}
    85  	}
    86  	return nil, fmt.Errorf("Cannot find jiri in update archive")
    87  }
    88  
    89  func UpdateAndExecute(force bool) error {
    90  	// Capture executable path before it is replaced in Update func
    91  	path, err := osutil.Executable()
    92  	if err != nil {
    93  		return fmt.Errorf("cannot get executable path, %s", err)
    94  	}
    95  	if err := Update(force); err != nil {
    96  		if err != updateNotAvailableErr && err != updateVersionErr &&
    97  			err != updateTestVersionErr {
    98  			return err
    99  		}
   100  		return nil
   101  	}
   102  
   103  	args := []string{}
   104  	for _, a := range os.Args {
   105  		if !strings.HasPrefix(a, "-force-autoupdate") {
   106  			args = append(args, a)
   107  		}
   108  	}
   109  
   110  	// Run the update version.
   111  	if err = syscall.Exec(path, args, os.Environ()); err != nil {
   112  		return fmt.Errorf("cannot execute %s: %s", path, err)
   113  	}
   114  	return nil
   115  }
   116  
   117  func getCurrentCommit(repository string) (string, error) {
   118  	u, err := url.Parse(repository)
   119  	if err != nil {
   120  		return "", err
   121  	}
   122  	if u.Scheme != "http" && u.Scheme != "https" {
   123  		return "", fmt.Errorf("remote host scheme is not http(s): %s", repository)
   124  	}
   125  	u.Path = path.Join(u.Path, "+refs/heads/master")
   126  	q := u.Query()
   127  	q.Set("format", "json")
   128  	u.RawQuery = q.Encode()
   129  
   130  	// Use Gitiles to find out the latest revision.
   131  	req, err := http.NewRequest("GET", u.String(), nil)
   132  	if err != nil {
   133  		return "", err
   134  	}
   135  	req.Header.Add("Accept", "application/json")
   136  	res, err := http.DefaultClient.Do(req)
   137  	if err != nil {
   138  		return "", err
   139  	}
   140  	defer res.Body.Close()
   141  	if res.StatusCode != http.StatusOK {
   142  		return "", fmt.Errorf("HTTP request failed: %v", http.StatusText(res.StatusCode))
   143  	}
   144  
   145  	r := bufio.NewReader(res.Body)
   146  
   147  	// The first line of the input is the XSSI guard ")]}'".
   148  	if _, err := r.ReadSlice('\n'); err != nil {
   149  		return "", err
   150  	}
   151  
   152  	var result map[string]struct {
   153  		Value string `json:"value"`
   154  	}
   155  	if err := json.NewDecoder(r).Decode(&result); err != nil {
   156  		return "", err
   157  	}
   158  	if v, ok := result["refs/heads/master"]; ok {
   159  		return v.Value, nil
   160  	} else {
   161  		return "", fmt.Errorf("cannot find current commit")
   162  	}
   163  }
   164  
   165  func downloadBinary(endpoint, version string) ([]byte, error) {
   166  	os := runtime.GOOS
   167  	if os == "darwin" {
   168  		os = "mac"
   169  	}
   170  	url := fmt.Sprintf("%s/%s-%s/+/git_revision:%s", endpoint, os, runtime.GOARCH, version)
   171  	res, err := http.Get(url)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	if res.StatusCode == http.StatusNotFound {
   177  		return nil, updateNotAvailableErr
   178  	}
   179  	if res.StatusCode != http.StatusOK {
   180  		return nil, fmt.Errorf("HTTP request failed: %v", http.StatusText(res.StatusCode))
   181  	}
   182  	defer res.Body.Close()
   183  
   184  	bytes, err := ioutil.ReadAll(res.Body)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  
   189  	return bytes, nil
   190  }
   191  
   192  func updateExecutable(path string, b []byte) error {
   193  	fi, err := os.Stat(path)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	dir := filepath.Dir(path)
   199  
   200  	// Write the new version to a file.
   201  	newfile, err := ioutil.TempFile(dir, "jiri")
   202  	if err != nil {
   203  		return err
   204  	}
   205  
   206  	if _, err := newfile.Write(b); err != nil {
   207  		return err
   208  	}
   209  
   210  	if err := newfile.Chmod(fi.Mode()); err != nil {
   211  		return err
   212  	}
   213  
   214  	if err := newfile.Close(); err != nil {
   215  		return err
   216  	}
   217  
   218  	// Backup the existing version.
   219  	oldfile, err := ioutil.TempFile(dir, "jiri")
   220  	if err != nil {
   221  		return err
   222  	}
   223  	defer os.Remove(oldfile.Name())
   224  
   225  	if err := oldfile.Close(); err != nil {
   226  		return err
   227  	}
   228  
   229  	err = osutil.Rename(path, oldfile.Name())
   230  	if err != nil {
   231  		return err
   232  	}
   233  
   234  	// Replace the existing version.
   235  	err = osutil.Rename(newfile.Name(), path)
   236  	if err != nil {
   237  		// Try to rollback the change in case of error.
   238  		rerr := osutil.Rename(oldfile.Name(), path)
   239  		if rerr != nil {
   240  			return rerr
   241  		}
   242  		return err
   243  	}
   244  
   245  	return nil
   246  }