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/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 bfffe601d..b15afe3f8 100644
--- a/updater/updater.go
+++ b/updater/updater.go
@@ -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) {
@@ -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
-}
diff --git a/updater/updater_darwin.go b/updater/updater_darwin.go
new file mode 100644
index 000000000..45c67e5c7
--- /dev/null
+++ b/updater/updater_darwin.go
@@ -0,0 +1,24 @@
+// 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 .
+
+package updater
+
+func start(src string) string {
+ return ""
+}
+
+func checkForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
+ return "", nil
+}
diff --git a/updater/updater_default.go b/updater/updater_default.go
new file mode 100644
index 000000000..4562907fc
--- /dev/null
+++ b/updater/updater_default.go
@@ -0,0 +1,307 @@
+// 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 .
+
+//go:build !darwin
+
+package updater
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/sha256"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "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]
+//
+//
+
+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 *availableUpdateInfo // Information about the available update.
+}
+
+// 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 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) 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()
+
+ 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
+ }
+ 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
+}