From 5ba151da470f751323df13da66e6dcd2919de4f4 Mon Sep 17 00:00:00 2001 From: Cristian Maglie <c.maglie@bug.st> Date: Fri, 27 Jan 2023 13:24:34 +0100 Subject: [PATCH 1/6] Moved updater business logic in updater package --- main.go | 29 ++--------------------------- updater/updater.go | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/main.go b/main.go index 1cd386e07..f6bc3afd6 100755 --- a/main.go +++ b/main.go @@ -22,7 +22,6 @@ import ( _ "embed" "encoding/json" "flag" - "io/ioutil" "os" "runtime" "runtime/debug" @@ -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 diff --git a/updater/updater.go b/updater/updater.go index bfffe601d..044cf091f 100644 --- a/updater/updater.go +++ b/updater/updater.go @@ -68,6 +68,38 @@ 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() +func Start(src string) string { + // If the executable is temporary, copy it to the full path, then restart + if strings.Contains(src, "-temp") { + newPath := removeTempSuffixFromPath(src) + if err := copyExe(src, newPath); err != nil { + log.Println("Copy error: ", err) + panic(err) + } + return newPath + } + + // Otherwise copy to a path with -temp suffix + if err := copyExe(src, AddTempSuffixToPath(src)); err != nil { + panic(err) + } + return "" +} + +func copyExe(from, to string) error { + data, err := os.ReadFile(from) + if err != nil { + log.Println("Cannot read file: ", from) + return err + } + err = os.WriteFile(to, data, 0755) + if err != nil { + log.Println("Cannot write file: ", to) + return err + } + return nil +} + // 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" { @@ -79,8 +111,8 @@ func AddTempSuffixToPath(path string) string { 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 { +// 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) } From 7ecb5ae7161286eb071275cc927d0fd87d227eba Mon Sep 17 00:00:00 2001 From: Cristian Maglie <c.maglie@bug.st> Date: Fri, 27 Jan 2023 13:32:23 +0100 Subject: [PATCH 2/6] Moved updater business logic in updater package --- update.go | 28 ++-------------------------- updater/updater.go | 28 ++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/update.go b/update.go index 377f5e353..21569f36c 100644 --- a/update.go +++ b/update.go @@ -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) } diff --git a/updater/updater.go b/updater/updater.go index 044cf091f..4881942c4 100644 --- a/updater/updater.go +++ b/updater/updater.go @@ -80,12 +80,32 @@ func Start(src string) string { } // Otherwise copy to a path with -temp suffix - if err := copyExe(src, AddTempSuffixToPath(src)); err != nil { + if err := copyExe(src, addTempSuffixToPath(src)); err != nil { panic(err) } return "" } +func CheckForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) { + path, err := os.Executable() + if err != nil { + return "", err + } + var up = &Updater{ + CurrentVersion: currentVersion, + APIURL: updateAPIURL, + BinURL: updateBinURL, + DiffURL: "", + Dir: "update/", + CmdName: cmdName, + } + + if err := up.BackgroundRun(); err != nil { + return "", err + } + return addTempSuffixToPath(path), nil +} + func copyExe(from, to string) error { data, err := os.ReadFile(from) if err != nil { @@ -100,8 +120,8 @@ func copyExe(from, to string) error { return nil } -// 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 { +// 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 { @@ -266,7 +286,7 @@ func (u *Updater) update() error { return err } - path = AddTempSuffixToPath(path) + path = addTempSuffixToPath(path) old, err := os.Open(path) if err != nil { From f36e3891d07cc527ffd76b095fc0b13fd641bf2c Mon Sep 17 00:00:00 2001 From: Cristian Maglie <c.maglie@bug.st> Date: Fri, 27 Jan 2023 14:07:55 +0100 Subject: [PATCH 3/6] Added infrastructure for generic updater procedure --- updater/updater.go | 327 +---------------------------------- updater/updater_default.go | 344 +++++++++++++++++++++++++++++++++++++ updater/updater_macos.go | 26 +++ 3 files changed, 377 insertions(+), 320 deletions(-) create mode 100644 updater/updater_default.go create mode 100644 updater/updater_macos.go diff --git a/updater/updater.go b/updater/updater.go index 4881942c4..0308927d6 100644 --- a/updater/updater.go +++ b/updater/updater.go @@ -15,328 +15,15 @@ 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] -// -// - -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() - +// 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 { - // If the executable is temporary, copy it to the full path, then restart - if strings.Contains(src, "-temp") { - newPath := removeTempSuffixFromPath(src) - if err := copyExe(src, newPath); err != nil { - log.Println("Copy error: ", err) - panic(err) - } - return newPath - } - - // Otherwise copy to a path with -temp suffix - if err := copyExe(src, addTempSuffixToPath(src)); err != nil { - panic(err) - } - return "" + 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) { - path, err := os.Executable() - if err != nil { - return "", err - } - var up = &Updater{ - CurrentVersion: currentVersion, - APIURL: updateAPIURL, - BinURL: updateBinURL, - DiffURL: "", - Dir: "update/", - CmdName: cmdName, - } - - if err := up.BackgroundRun(); err != nil { - return "", err - } - return addTempSuffixToPath(path), nil -} - -func copyExe(from, to string) error { - data, err := os.ReadFile(from) - if err != nil { - log.Println("Cannot read file: ", from) - return err - } - err = os.WriteFile(to, data, 0755) - if err != nil { - log.Println("Cannot write file: ", to) - return err - } - return nil -} - -// 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" - } - - 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 - } -} - -// 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 -} - -func fetch(url string) (io.ReadCloser, error) { - resp, err := http.Get(url) - if err != nil { - return nil, err - } - if resp.StatusCode != 200 { - log.Errorf("bad http status from %s: %v", url, resp.Status) - return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status) - } - 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 + return checkForUpdates(currentVersion, updateAPIURL, updateBinURL, cmdName) } diff --git a/updater/updater_default.go b/updater/updater_default.go new file mode 100644 index 000000000..7ab71dcf0 --- /dev/null +++ b/updater/updater_default.go @@ -0,0 +1,344 @@ +// Copyright 2022 Arduino SA +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//go:build !darwin + +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] +// +// + +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() + +func start(src string) string { + // If the executable is temporary, copy it to the full path, then restart + if strings.Contains(src, "-temp") { + newPath := removeTempSuffixFromPath(src) + if err := copyExe(src, newPath); err != nil { + log.Println("Copy error: ", err) + panic(err) + } + return newPath + } + + // Otherwise copy to a path with -temp suffix + if err := copyExe(src, addTempSuffixToPath(src)); err != nil { + panic(err) + } + return "" +} + +func checkForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) { + path, err := os.Executable() + if err != nil { + return "", err + } + var up = &Updater{ + CurrentVersion: currentVersion, + APIURL: updateAPIURL, + BinURL: updateBinURL, + DiffURL: "", + Dir: "update/", + CmdName: cmdName, + } + + if err := up.BackgroundRun(); err != nil { + return "", err + } + return addTempSuffixToPath(path), nil +} + +func copyExe(from, to string) error { + data, err := os.ReadFile(from) + if err != nil { + log.Println("Cannot read file: ", from) + return err + } + err = os.WriteFile(to, data, 0755) + if err != nil { + log.Println("Cannot write file: ", to) + return err + } + return nil +} + +// 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" + } + + 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 + } +} + +// 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 +} + +func fetch(url string) (io.ReadCloser, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + log.Errorf("bad http status from %s: %v", url, resp.Status) + return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status) + } + 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 +} diff --git a/updater/updater_macos.go b/updater/updater_macos.go new file mode 100644 index 000000000..a4a8b5c9e --- /dev/null +++ b/updater/updater_macos.go @@ -0,0 +1,26 @@ +// Copyright 2022 Arduino SA +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//go:build darwin + +package updater + +func start(src string) string { + return "" +} + +func checkForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) { + return "", nil +} From 0aafda140b84fbc2f4fb7c0e039a4f3b910f0ddd Mon Sep 17 00:00:00 2001 From: Cristian Maglie <c.maglie@bug.st> Date: Fri, 27 Jan 2023 14:19:06 +0100 Subject: [PATCH 4/6] Factored fetchInfo function to retrieve information about the latest update --- updater/updater.go | 50 ++++++++++++++++++++++++++++++++++++++ updater/updater_default.go | 47 +++++++---------------------------- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/updater/updater.go b/updater/updater.go index 0308927d6..b15afe3f8 100644 --- a/updater/updater.go +++ b/updater/updater.go @@ -15,6 +15,18 @@ package updater +import ( + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "runtime" + + log "github.com/sirupsen/logrus" +) + // 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. @@ -27,3 +39,41 @@ func Start(src string) string { func CheckForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) { return checkForUpdates(currentVersion, updateAPIURL, updateBinURL, cmdName) } + +const ( + plat = runtime.GOOS + "-" + runtime.GOARCH +) + +func fetchInfo(updateAPIURL string, cmdName string) (*availableUpdateInfo, error) { + r, err := fetch(updateAPIURL + cmdName + "/" + plat + ".json") + if err != nil { + return nil, err + } + defer r.Close() + + 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 +} + +type availableUpdateInfo struct { + Version string + Sha256 []byte +} + +func fetch(url string) (io.ReadCloser, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + log.Errorf("bad http status from %s: %v", url, resp.Status) + return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status) + } + return resp.Body, nil +} diff --git a/updater/updater_default.go b/updater/updater_default.go index 7ab71dcf0..57dbd8187 100644 --- a/updater/updater_default.go +++ b/updater/updater_default.go @@ -21,14 +21,11 @@ import ( "bytes" "compress/gzip" "crypto/sha256" - "encoding/json" "errors" "fmt" "io" - "net/http" "os" "path/filepath" - "runtime" "strings" "github.com/kr/binarydist" @@ -62,10 +59,6 @@ import ( // // -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() @@ -156,16 +149,13 @@ func removeTempSuffixFromPath(path string) string { // 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 - } + 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 *availableUpdateInfo // Information about the available update. } // BackgroundRun starts the update check and apply cycle. @@ -187,18 +177,6 @@ func (u *Updater) BackgroundRun() error { return nil } -func fetch(url string) (io.ReadCloser, error) { - resp, err := http.Get(url) - if err != nil { - return nil, err - } - if resp.StatusCode != 200 { - log.Errorf("bad http status from %s: %v", url, resp.Status) - return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status) - } - return resp.Body, nil -} - func verifySha(bin []byte, sha []byte) bool { h := sha256.New() h.Write(bin) @@ -261,18 +239,11 @@ func (u *Updater) fetchBin() ([]byte, error) { } func (u *Updater) fetchInfo() error { - r, err := fetch(u.APIURL + u.CmdName + "/" + plat + ".json") + info, err := fetchInfo(u.APIURL, u.CmdName) 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") - } + u.Info = info return nil } From 5ae57a40317c9cdb663ff17e86a3bc41404e8db8 Mon Sep 17 00:00:00 2001 From: Cristian Maglie <c.maglie@bug.st> Date: Mon, 13 Feb 2023 10:35:30 +0100 Subject: [PATCH 5/6] Inlined Updater.fetchInfo method --- updater/updater_default.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/updater/updater_default.go b/updater/updater_default.go index 57dbd8187..4562907fc 100644 --- a/updater/updater_default.go +++ b/updater/updater_default.go @@ -238,15 +238,6 @@ func (u *Updater) fetchBin() ([]byte, error) { return buf.Bytes(), nil } -func (u *Updater) fetchInfo() error { - info, err := fetchInfo(u.APIURL, u.CmdName) - if err != nil { - return err - } - u.Info = info - return nil -} - func (u *Updater) getExecRelativeDir(dir string) string { filename, _ := os.Executable() path := filepath.Join(filepath.Dir(filename), dir) @@ -267,11 +258,12 @@ func (u *Updater) update() error { } defer old.Close() - err = u.fetchInfo() + info, err := fetchInfo(u.APIURL, u.CmdName) if err != nil { log.Println(err) return err } + u.Info = info if u.Info.Version == u.CurrentVersion { return nil } From 381507224cd9b77b58d5cb9f7ab38ed23080e2a7 Mon Sep 17 00:00:00 2001 From: Cristian Maglie <c.maglie@bug.st> Date: Mon, 13 Feb 2023 10:36:43 +0100 Subject: [PATCH 6/6] Renamed updater_macos -> updater_darwin and removed build flag --- updater/{updater_macos.go => updater_darwin.go} | 2 -- 1 file changed, 2 deletions(-) rename updater/{updater_macos.go => updater_darwin.go} (97%) diff --git a/updater/updater_macos.go b/updater/updater_darwin.go similarity index 97% rename from updater/updater_macos.go rename to updater/updater_darwin.go index a4a8b5c9e..45c67e5c7 100644 --- a/updater/updater_macos.go +++ b/updater/updater_darwin.go @@ -13,8 +13,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -//go:build darwin - package updater func start(src string) string {