Skip to content

Refactor updater API #767

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 2 additions & 27 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
_ "embed"
"encoding/json"
"flag"
"io/ioutil"
"os"
"runtime"
"runtime/debug"
Expand Down Expand Up @@ -149,39 +148,15 @@ func main() {
ConfigDir: configDir,
}

// If the executable is temporary, copy it to the full path, then restart
if src, err := os.Executable(); err != nil {
panic(err)
} else if strings.Contains(src, "-temp") {
newPath := updater.RemoveTempSuffixFromPath(src)
if err := copyExe(src, newPath); err != nil {
log.Println("Copy error: ", err)
panic(err)
}
Systray.RestartWith(newPath)
} else if restartPath := updater.Start(src); restartPath != "" {
Systray.RestartWith(restartPath)
} else {
// Otherwise copy to a path with -temp suffix
if err := copyExe(src, updater.AddTempSuffixToPath(src)); err != nil {
panic(err)
}
Systray.Start()
}
}

func copyExe(from, to string) error {
data, err := ioutil.ReadFile(from)
if err != nil {
log.Println("Cannot read file: ", from)
return err
}
err = ioutil.WriteFile(to, data, 0755)
if err != nil {
log.Println("Cannot write file: ", to)
return err
}
return nil
}

func loop() {
if *hibernate {
return
Expand Down
28 changes: 2 additions & 26 deletions update.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,40 +30,16 @@
package main

import (
"os"

"github.com/arduino/arduino-create-agent/updater"
"github.com/gin-gonic/gin"
)

func updateHandler(c *gin.Context) {

path, err := os.Executable()

if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}

var up = &updater.Updater{
CurrentVersion: version,
APIURL: *updateURL,
BinURL: *updateURL,
DiffURL: "",
Dir: "update/",
CmdName: *appName,
}

err = up.BackgroundRun()

restartPath, err := updater.CheckForUpdates(version, *updateURL, *updateURL, *appName)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}

path = updater.AddTempSuffixToPath(path)

c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"})

Systray.RestartWith(path)
Systray.RestartWith(restartPath)
}
265 changes: 27 additions & 238 deletions updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,121 +16,54 @@
package updater

import (
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/kr/binarydist"
log "github.com/sirupsen/logrus"
"gopkg.in/inconshreveable/go-update.v0"
)

// Update protocol:
//
// GET hk.heroku.com/hk/linux-amd64.json
//
// 200 ok
// {
// "Version": "2",
// "Sha256": "..." // base64
// }
//
// then
//
// GET hkpatch.s3.amazonaws.com/hk/1/2/linux-amd64
//
// 200 ok
// [bsdiff data]
//
// or
//
// GET hkdist.s3.amazonaws.com/hk/2/linux-amd64.gz
//
// 200 ok
// [gzipped executable data]
//
//
// Start checks if an update has been downloaded and if so returns the path to the
// binary to be executed to perform the update. If no update has been downloaded
// it returns an empty string.
func Start(src string) string {
return start(src)
}

// CheckForUpdates checks if there is a new version of the binary available and
// if so downloads it.
func CheckForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
return checkForUpdates(currentVersion, updateAPIURL, updateBinURL, cmdName)
}

const (
plat = runtime.GOOS + "-" + runtime.GOARCH
)

var errHashMismatch = errors.New("new file hash mismatch after patch")
var errDiffURLUndefined = errors.New("DiffURL is not defined, I cannot fetch and apply patch, reverting to full bin")
var up = update.New()

// AddTempSuffixToPath adds the "-temp" suffix to the path to an executable file (a ".exe" extension is replaced with "-temp.exe")
func AddTempSuffixToPath(path string) string {
if filepath.Ext(path) == "exe" {
path = strings.Replace(path, ".exe", "-temp.exe", -1)
} else {
path = path + "-temp"
func fetchInfo(updateAPIURL string, cmdName string) (*availableUpdateInfo, error) {
r, err := fetch(updateAPIURL + cmdName + "/" + plat + ".json")
if err != nil {
return nil, err
}
defer r.Close()

return path
}

// RemoveTempSuffixFromPath removes "-temp" suffix from the path to an executable file (a "-temp.exe" extension is replaced with ".exe")
func RemoveTempSuffixFromPath(path string) string {
return strings.Replace(path, "-temp", "", -1)
}

// Updater is the configuration and runtime data for doing an update.
//
// Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location.
//
// Example:
//
// updater := &selfupdate.Updater{
// CurrentVersion: version,
// ApiURL: "http://updates.yourdomain.com/",
// BinURL: "http://updates.yourdownmain.com/",
// DiffURL: "http://updates.yourdomain.com/",
// Dir: "update/",
// CmdName: "myapp", // app name
// }
// if updater != nil {
// go updater.BackgroundRun()
// }
type Updater struct {
CurrentVersion string // Currently running version.
APIURL string // Base URL for API requests (json files).
CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
BinURL string // Base URL for full binary downloads.
DiffURL string // Base URL for diff downloads.
Dir string // Directory to store selfupdate state.
Info struct {
Version string
Sha256 []byte
var res availableUpdateInfo
if err := json.NewDecoder(r).Decode(&res); err != nil {
return nil, err
}
if len(res.Sha256) != sha256.Size {
return nil, errors.New("bad cmd hash in info")
}
return &res, nil
}

// BackgroundRun starts the update check and apply cycle.
func (u *Updater) BackgroundRun() error {
os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777)
if err := up.CanUpdate(); err != nil {
log.Println(err)
return err
}
//self, err := os.Executable()
//if err != nil {
// fail update, couldn't figure out path to self
//return
//}
// TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports.
if err := u.update(); err != nil {
return err
}
return nil
type availableUpdateInfo struct {
Version string
Sha256 []byte
}

func fetch(url string) (io.ReadCloser, error) {
Expand All @@ -144,147 +77,3 @@ func fetch(url string) (io.ReadCloser, error) {
}
return resp.Body, nil
}

func verifySha(bin []byte, sha []byte) bool {
h := sha256.New()
h.Write(bin)
return bytes.Equal(h.Sum(nil), sha)
}

func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) {
if u.DiffURL == "" {
return nil, errDiffURLUndefined
}
r, err := fetch(u.DiffURL + u.CmdName + "/" + u.CurrentVersion + "/" + u.Info.Version + "/" + plat)
if err != nil {
return nil, err
}
defer r.Close()
var buf bytes.Buffer
err = binarydist.Patch(old, &buf, r)
return buf.Bytes(), err
}

func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) {
bin, err := u.fetchAndApplyPatch(old)
if err != nil {
return nil, err
}
if !verifySha(bin, u.Info.Sha256) {
return nil, errHashMismatch
}
return bin, nil
}

func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) {
bin, err := u.fetchBin()
if err != nil {
return nil, err
}
verified := verifySha(bin, u.Info.Sha256)
if !verified {
return nil, errHashMismatch
}
return bin, nil
}

func (u *Updater) fetchBin() ([]byte, error) {
r, err := fetch(u.BinURL + u.CmdName + "/" + u.Info.Version + "/" + plat + ".gz")
if err != nil {
return nil, err
}
defer r.Close()
buf := new(bytes.Buffer)
gz, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
if _, err = io.Copy(buf, gz); err != nil {
return nil, err
}

return buf.Bytes(), nil
}

func (u *Updater) fetchInfo() error {
r, err := fetch(u.APIURL + u.CmdName + "/" + plat + ".json")
if err != nil {
return err
}
defer r.Close()
err = json.NewDecoder(r).Decode(&u.Info)
if err != nil {
return err
}
if len(u.Info.Sha256) != sha256.Size {
return errors.New("bad cmd hash in info")
}
return nil
}

func (u *Updater) getExecRelativeDir(dir string) string {
filename, _ := os.Executable()
path := filepath.Join(filepath.Dir(filename), dir)
return path
}

func (u *Updater) update() error {
path, err := os.Executable()
if err != nil {
return err
}

path = AddTempSuffixToPath(path)

old, err := os.Open(path)
if err != nil {
return err
}
defer old.Close()

err = u.fetchInfo()
if err != nil {
log.Println(err)
return err
}
if u.Info.Version == u.CurrentVersion {
return nil
}
bin, err := u.fetchAndVerifyPatch(old)
if err != nil {
switch err {
case errHashMismatch:
log.Println("update: hash mismatch from patched binary")
case errDiffURLUndefined:
log.Println("update: ", err)
default:
log.Println("update: patching binary, ", err)
}

bin, err = u.fetchAndVerifyFullBin()
if err != nil {
if err == errHashMismatch {
log.Println("update: hash mismatch from full binary")
} else {
log.Println("update: fetching full binary,", err)
}
return err
}
}

// close the old binary before installing because on windows
// it can't be renamed if a handle to the file is still open
old.Close()

up.TargetPath = path
err, errRecover := up.FromStream(bytes.NewBuffer(bin))
if errRecover != nil {
log.Errorf("update and recovery errors: %q %q", err, errRecover)
return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
}
if err != nil {
return err
}

return nil
}
Loading