Skip to content

Commit 6c9aec1

Browse files
authored
Refactor updater API (#767)
* Moved updater business logic in updater package * Moved updater business logic in updater package * Added infrastructure for generic updater procedure * Factored fetchInfo function to retrieve information about the latest update * Inlined Updater.fetchInfo method * Renamed updater_macos -> updater_darwin and removed build flag
1 parent 3d7780f commit 6c9aec1

File tree

5 files changed

+362
-291
lines changed

5 files changed

+362
-291
lines changed

Diff for: main.go

+2-27
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
_ "embed"
2323
"encoding/json"
2424
"flag"
25-
"io/ioutil"
2625
"os"
2726
"runtime"
2827
"runtime/debug"
@@ -149,39 +148,15 @@ func main() {
149148
ConfigDir: configDir,
150149
}
151150

152-
// If the executable is temporary, copy it to the full path, then restart
153151
if src, err := os.Executable(); err != nil {
154152
panic(err)
155-
} else if strings.Contains(src, "-temp") {
156-
newPath := updater.RemoveTempSuffixFromPath(src)
157-
if err := copyExe(src, newPath); err != nil {
158-
log.Println("Copy error: ", err)
159-
panic(err)
160-
}
161-
Systray.RestartWith(newPath)
153+
} else if restartPath := updater.Start(src); restartPath != "" {
154+
Systray.RestartWith(restartPath)
162155
} else {
163-
// Otherwise copy to a path with -temp suffix
164-
if err := copyExe(src, updater.AddTempSuffixToPath(src)); err != nil {
165-
panic(err)
166-
}
167156
Systray.Start()
168157
}
169158
}
170159

171-
func copyExe(from, to string) error {
172-
data, err := ioutil.ReadFile(from)
173-
if err != nil {
174-
log.Println("Cannot read file: ", from)
175-
return err
176-
}
177-
err = ioutil.WriteFile(to, data, 0755)
178-
if err != nil {
179-
log.Println("Cannot write file: ", to)
180-
return err
181-
}
182-
return nil
183-
}
184-
185160
func loop() {
186161
if *hibernate {
187162
return

Diff for: update.go

+2-26
Original file line numberDiff line numberDiff line change
@@ -30,40 +30,16 @@
3030
package main
3131

3232
import (
33-
"os"
34-
3533
"github.com/arduino/arduino-create-agent/updater"
3634
"github.com/gin-gonic/gin"
3735
)
3836

3937
func updateHandler(c *gin.Context) {
40-
41-
path, err := os.Executable()
42-
43-
if err != nil {
44-
c.JSON(500, gin.H{"error": err.Error()})
45-
return
46-
}
47-
48-
var up = &updater.Updater{
49-
CurrentVersion: version,
50-
APIURL: *updateURL,
51-
BinURL: *updateURL,
52-
DiffURL: "",
53-
Dir: "update/",
54-
CmdName: *appName,
55-
}
56-
57-
err = up.BackgroundRun()
58-
38+
restartPath, err := updater.CheckForUpdates(version, *updateURL, *updateURL, *appName)
5939
if err != nil {
6040
c.JSON(500, gin.H{"error": err.Error()})
6141
return
6242
}
63-
64-
path = updater.AddTempSuffixToPath(path)
65-
6643
c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"})
67-
68-
Systray.RestartWith(path)
44+
Systray.RestartWith(restartPath)
6945
}

Diff for: updater/updater.go

+27-238
Original file line numberDiff line numberDiff line change
@@ -16,121 +16,54 @@
1616
package updater
1717

1818
import (
19-
"bytes"
20-
"compress/gzip"
2119
"crypto/sha256"
2220
"encoding/json"
2321
"errors"
2422
"fmt"
2523
"io"
2624
"net/http"
27-
"os"
28-
"path/filepath"
2925
"runtime"
30-
"strings"
3126

32-
"github.com/kr/binarydist"
3327
log "github.com/sirupsen/logrus"
34-
"gopkg.in/inconshreveable/go-update.v0"
3528
)
3629

37-
// Update protocol:
38-
//
39-
// GET hk.heroku.com/hk/linux-amd64.json
40-
//
41-
// 200 ok
42-
// {
43-
// "Version": "2",
44-
// "Sha256": "..." // base64
45-
// }
46-
//
47-
// then
48-
//
49-
// GET hkpatch.s3.amazonaws.com/hk/1/2/linux-amd64
50-
//
51-
// 200 ok
52-
// [bsdiff data]
53-
//
54-
// or
55-
//
56-
// GET hkdist.s3.amazonaws.com/hk/2/linux-amd64.gz
57-
//
58-
// 200 ok
59-
// [gzipped executable data]
60-
//
61-
//
30+
// Start checks if an update has been downloaded and if so returns the path to the
31+
// binary to be executed to perform the update. If no update has been downloaded
32+
// it returns an empty string.
33+
func Start(src string) string {
34+
return start(src)
35+
}
36+
37+
// CheckForUpdates checks if there is a new version of the binary available and
38+
// if so downloads it.
39+
func CheckForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
40+
return checkForUpdates(currentVersion, updateAPIURL, updateBinURL, cmdName)
41+
}
6242

6343
const (
6444
plat = runtime.GOOS + "-" + runtime.GOARCH
6545
)
6646

67-
var errHashMismatch = errors.New("new file hash mismatch after patch")
68-
var errDiffURLUndefined = errors.New("DiffURL is not defined, I cannot fetch and apply patch, reverting to full bin")
69-
var up = update.New()
70-
71-
// AddTempSuffixToPath adds the "-temp" suffix to the path to an executable file (a ".exe" extension is replaced with "-temp.exe")
72-
func AddTempSuffixToPath(path string) string {
73-
if filepath.Ext(path) == "exe" {
74-
path = strings.Replace(path, ".exe", "-temp.exe", -1)
75-
} else {
76-
path = path + "-temp"
47+
func fetchInfo(updateAPIURL string, cmdName string) (*availableUpdateInfo, error) {
48+
r, err := fetch(updateAPIURL + cmdName + "/" + plat + ".json")
49+
if err != nil {
50+
return nil, err
7751
}
52+
defer r.Close()
7853

79-
return path
80-
}
81-
82-
// RemoveTempSuffixFromPath removes "-temp" suffix from the path to an executable file (a "-temp.exe" extension is replaced with ".exe")
83-
func RemoveTempSuffixFromPath(path string) string {
84-
return strings.Replace(path, "-temp", "", -1)
85-
}
86-
87-
// Updater is the configuration and runtime data for doing an update.
88-
//
89-
// Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location.
90-
//
91-
// Example:
92-
//
93-
// updater := &selfupdate.Updater{
94-
// CurrentVersion: version,
95-
// ApiURL: "http://updates.yourdomain.com/",
96-
// BinURL: "http://updates.yourdownmain.com/",
97-
// DiffURL: "http://updates.yourdomain.com/",
98-
// Dir: "update/",
99-
// CmdName: "myapp", // app name
100-
// }
101-
// if updater != nil {
102-
// go updater.BackgroundRun()
103-
// }
104-
type Updater struct {
105-
CurrentVersion string // Currently running version.
106-
APIURL string // Base URL for API requests (json files).
107-
CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
108-
BinURL string // Base URL for full binary downloads.
109-
DiffURL string // Base URL for diff downloads.
110-
Dir string // Directory to store selfupdate state.
111-
Info struct {
112-
Version string
113-
Sha256 []byte
54+
var res availableUpdateInfo
55+
if err := json.NewDecoder(r).Decode(&res); err != nil {
56+
return nil, err
57+
}
58+
if len(res.Sha256) != sha256.Size {
59+
return nil, errors.New("bad cmd hash in info")
11460
}
61+
return &res, nil
11562
}
11663

117-
// BackgroundRun starts the update check and apply cycle.
118-
func (u *Updater) BackgroundRun() error {
119-
os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777)
120-
if err := up.CanUpdate(); err != nil {
121-
log.Println(err)
122-
return err
123-
}
124-
//self, err := os.Executable()
125-
//if err != nil {
126-
// fail update, couldn't figure out path to self
127-
//return
128-
//}
129-
// TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports.
130-
if err := u.update(); err != nil {
131-
return err
132-
}
133-
return nil
64+
type availableUpdateInfo struct {
65+
Version string
66+
Sha256 []byte
13467
}
13568

13669
func fetch(url string) (io.ReadCloser, error) {
@@ -144,147 +77,3 @@ func fetch(url string) (io.ReadCloser, error) {
14477
}
14578
return resp.Body, nil
14679
}
147-
148-
func verifySha(bin []byte, sha []byte) bool {
149-
h := sha256.New()
150-
h.Write(bin)
151-
return bytes.Equal(h.Sum(nil), sha)
152-
}
153-
154-
func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) {
155-
if u.DiffURL == "" {
156-
return nil, errDiffURLUndefined
157-
}
158-
r, err := fetch(u.DiffURL + u.CmdName + "/" + u.CurrentVersion + "/" + u.Info.Version + "/" + plat)
159-
if err != nil {
160-
return nil, err
161-
}
162-
defer r.Close()
163-
var buf bytes.Buffer
164-
err = binarydist.Patch(old, &buf, r)
165-
return buf.Bytes(), err
166-
}
167-
168-
func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) {
169-
bin, err := u.fetchAndApplyPatch(old)
170-
if err != nil {
171-
return nil, err
172-
}
173-
if !verifySha(bin, u.Info.Sha256) {
174-
return nil, errHashMismatch
175-
}
176-
return bin, nil
177-
}
178-
179-
func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) {
180-
bin, err := u.fetchBin()
181-
if err != nil {
182-
return nil, err
183-
}
184-
verified := verifySha(bin, u.Info.Sha256)
185-
if !verified {
186-
return nil, errHashMismatch
187-
}
188-
return bin, nil
189-
}
190-
191-
func (u *Updater) fetchBin() ([]byte, error) {
192-
r, err := fetch(u.BinURL + u.CmdName + "/" + u.Info.Version + "/" + plat + ".gz")
193-
if err != nil {
194-
return nil, err
195-
}
196-
defer r.Close()
197-
buf := new(bytes.Buffer)
198-
gz, err := gzip.NewReader(r)
199-
if err != nil {
200-
return nil, err
201-
}
202-
if _, err = io.Copy(buf, gz); err != nil {
203-
return nil, err
204-
}
205-
206-
return buf.Bytes(), nil
207-
}
208-
209-
func (u *Updater) fetchInfo() error {
210-
r, err := fetch(u.APIURL + u.CmdName + "/" + plat + ".json")
211-
if err != nil {
212-
return err
213-
}
214-
defer r.Close()
215-
err = json.NewDecoder(r).Decode(&u.Info)
216-
if err != nil {
217-
return err
218-
}
219-
if len(u.Info.Sha256) != sha256.Size {
220-
return errors.New("bad cmd hash in info")
221-
}
222-
return nil
223-
}
224-
225-
func (u *Updater) getExecRelativeDir(dir string) string {
226-
filename, _ := os.Executable()
227-
path := filepath.Join(filepath.Dir(filename), dir)
228-
return path
229-
}
230-
231-
func (u *Updater) update() error {
232-
path, err := os.Executable()
233-
if err != nil {
234-
return err
235-
}
236-
237-
path = AddTempSuffixToPath(path)
238-
239-
old, err := os.Open(path)
240-
if err != nil {
241-
return err
242-
}
243-
defer old.Close()
244-
245-
err = u.fetchInfo()
246-
if err != nil {
247-
log.Println(err)
248-
return err
249-
}
250-
if u.Info.Version == u.CurrentVersion {
251-
return nil
252-
}
253-
bin, err := u.fetchAndVerifyPatch(old)
254-
if err != nil {
255-
switch err {
256-
case errHashMismatch:
257-
log.Println("update: hash mismatch from patched binary")
258-
case errDiffURLUndefined:
259-
log.Println("update: ", err)
260-
default:
261-
log.Println("update: patching binary, ", err)
262-
}
263-
264-
bin, err = u.fetchAndVerifyFullBin()
265-
if err != nil {
266-
if err == errHashMismatch {
267-
log.Println("update: hash mismatch from full binary")
268-
} else {
269-
log.Println("update: fetching full binary,", err)
270-
}
271-
return err
272-
}
273-
}
274-
275-
// close the old binary before installing because on windows
276-
// it can't be renamed if a handle to the file is still open
277-
old.Close()
278-
279-
up.TargetPath = path
280-
err, errRecover := up.FromStream(bytes.NewBuffer(bin))
281-
if errRecover != nil {
282-
log.Errorf("update and recovery errors: %q %q", err, errRecover)
283-
return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
284-
}
285-
if err != nil {
286-
return err
287-
}
288-
289-
return nil
290-
}

0 commit comments

Comments
 (0)