Skip to content

Commit f36e389

Browse files
committed
Added infrastructure for generic updater procedure
1 parent 7ecb5ae commit f36e389

File tree

3 files changed

+377
-320
lines changed

3 files changed

+377
-320
lines changed

updater/updater.go

+7-320
Original file line numberDiff line numberDiff line change
@@ -15,328 +15,15 @@
1515

1616
package updater
1717

18-
import (
19-
"bytes"
20-
"compress/gzip"
21-
"crypto/sha256"
22-
"encoding/json"
23-
"errors"
24-
"fmt"
25-
"io"
26-
"net/http"
27-
"os"
28-
"path/filepath"
29-
"runtime"
30-
"strings"
31-
32-
"github.com/kr/binarydist"
33-
log "github.com/sirupsen/logrus"
34-
"gopkg.in/inconshreveable/go-update.v0"
35-
)
36-
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-
//
62-
63-
const (
64-
plat = runtime.GOOS + "-" + runtime.GOARCH
65-
)
66-
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-
18+
// Start checks if an update has been downloaded and if so returns the path to the
19+
// binary to be executed to perform the update. If no update has been downloaded
20+
// it returns an empty string.
7121
func Start(src string) string {
72-
// If the executable is temporary, copy it to the full path, then restart
73-
if strings.Contains(src, "-temp") {
74-
newPath := removeTempSuffixFromPath(src)
75-
if err := copyExe(src, newPath); err != nil {
76-
log.Println("Copy error: ", err)
77-
panic(err)
78-
}
79-
return newPath
80-
}
81-
82-
// Otherwise copy to a path with -temp suffix
83-
if err := copyExe(src, addTempSuffixToPath(src)); err != nil {
84-
panic(err)
85-
}
86-
return ""
22+
return start(src)
8723
}
8824

25+
// CheckForUpdates checks if there is a new version of the binary available and
26+
// if so downloads it.
8927
func CheckForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
90-
path, err := os.Executable()
91-
if err != nil {
92-
return "", err
93-
}
94-
var up = &Updater{
95-
CurrentVersion: currentVersion,
96-
APIURL: updateAPIURL,
97-
BinURL: updateBinURL,
98-
DiffURL: "",
99-
Dir: "update/",
100-
CmdName: cmdName,
101-
}
102-
103-
if err := up.BackgroundRun(); err != nil {
104-
return "", err
105-
}
106-
return addTempSuffixToPath(path), nil
107-
}
108-
109-
func copyExe(from, to string) error {
110-
data, err := os.ReadFile(from)
111-
if err != nil {
112-
log.Println("Cannot read file: ", from)
113-
return err
114-
}
115-
err = os.WriteFile(to, data, 0755)
116-
if err != nil {
117-
log.Println("Cannot write file: ", to)
118-
return err
119-
}
120-
return nil
121-
}
122-
123-
// addTempSuffixToPath adds the "-temp" suffix to the path to an executable file (a ".exe" extension is replaced with "-temp.exe")
124-
func addTempSuffixToPath(path string) string {
125-
if filepath.Ext(path) == "exe" {
126-
path = strings.Replace(path, ".exe", "-temp.exe", -1)
127-
} else {
128-
path = path + "-temp"
129-
}
130-
131-
return path
132-
}
133-
134-
// removeTempSuffixFromPath removes "-temp" suffix from the path to an executable file (a "-temp.exe" extension is replaced with ".exe")
135-
func removeTempSuffixFromPath(path string) string {
136-
return strings.Replace(path, "-temp", "", -1)
137-
}
138-
139-
// Updater is the configuration and runtime data for doing an update.
140-
//
141-
// Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location.
142-
//
143-
// Example:
144-
//
145-
// updater := &selfupdate.Updater{
146-
// CurrentVersion: version,
147-
// ApiURL: "http://updates.yourdomain.com/",
148-
// BinURL: "http://updates.yourdownmain.com/",
149-
// DiffURL: "http://updates.yourdomain.com/",
150-
// Dir: "update/",
151-
// CmdName: "myapp", // app name
152-
// }
153-
// if updater != nil {
154-
// go updater.BackgroundRun()
155-
// }
156-
type Updater struct {
157-
CurrentVersion string // Currently running version.
158-
APIURL string // Base URL for API requests (json files).
159-
CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
160-
BinURL string // Base URL for full binary downloads.
161-
DiffURL string // Base URL for diff downloads.
162-
Dir string // Directory to store selfupdate state.
163-
Info struct {
164-
Version string
165-
Sha256 []byte
166-
}
167-
}
168-
169-
// BackgroundRun starts the update check and apply cycle.
170-
func (u *Updater) BackgroundRun() error {
171-
os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777)
172-
if err := up.CanUpdate(); err != nil {
173-
log.Println(err)
174-
return err
175-
}
176-
//self, err := os.Executable()
177-
//if err != nil {
178-
// fail update, couldn't figure out path to self
179-
//return
180-
//}
181-
// TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports.
182-
if err := u.update(); err != nil {
183-
return err
184-
}
185-
return nil
186-
}
187-
188-
func fetch(url string) (io.ReadCloser, error) {
189-
resp, err := http.Get(url)
190-
if err != nil {
191-
return nil, err
192-
}
193-
if resp.StatusCode != 200 {
194-
log.Errorf("bad http status from %s: %v", url, resp.Status)
195-
return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status)
196-
}
197-
return resp.Body, nil
198-
}
199-
200-
func verifySha(bin []byte, sha []byte) bool {
201-
h := sha256.New()
202-
h.Write(bin)
203-
return bytes.Equal(h.Sum(nil), sha)
204-
}
205-
206-
func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) {
207-
if u.DiffURL == "" {
208-
return nil, errDiffURLUndefined
209-
}
210-
r, err := fetch(u.DiffURL + u.CmdName + "/" + u.CurrentVersion + "/" + u.Info.Version + "/" + plat)
211-
if err != nil {
212-
return nil, err
213-
}
214-
defer r.Close()
215-
var buf bytes.Buffer
216-
err = binarydist.Patch(old, &buf, r)
217-
return buf.Bytes(), err
218-
}
219-
220-
func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) {
221-
bin, err := u.fetchAndApplyPatch(old)
222-
if err != nil {
223-
return nil, err
224-
}
225-
if !verifySha(bin, u.Info.Sha256) {
226-
return nil, errHashMismatch
227-
}
228-
return bin, nil
229-
}
230-
231-
func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) {
232-
bin, err := u.fetchBin()
233-
if err != nil {
234-
return nil, err
235-
}
236-
verified := verifySha(bin, u.Info.Sha256)
237-
if !verified {
238-
return nil, errHashMismatch
239-
}
240-
return bin, nil
241-
}
242-
243-
func (u *Updater) fetchBin() ([]byte, error) {
244-
r, err := fetch(u.BinURL + u.CmdName + "/" + u.Info.Version + "/" + plat + ".gz")
245-
if err != nil {
246-
return nil, err
247-
}
248-
defer r.Close()
249-
buf := new(bytes.Buffer)
250-
gz, err := gzip.NewReader(r)
251-
if err != nil {
252-
return nil, err
253-
}
254-
if _, err = io.Copy(buf, gz); err != nil {
255-
return nil, err
256-
}
257-
258-
return buf.Bytes(), nil
259-
}
260-
261-
func (u *Updater) fetchInfo() error {
262-
r, err := fetch(u.APIURL + u.CmdName + "/" + plat + ".json")
263-
if err != nil {
264-
return err
265-
}
266-
defer r.Close()
267-
err = json.NewDecoder(r).Decode(&u.Info)
268-
if err != nil {
269-
return err
270-
}
271-
if len(u.Info.Sha256) != sha256.Size {
272-
return errors.New("bad cmd hash in info")
273-
}
274-
return nil
275-
}
276-
277-
func (u *Updater) getExecRelativeDir(dir string) string {
278-
filename, _ := os.Executable()
279-
path := filepath.Join(filepath.Dir(filename), dir)
280-
return path
281-
}
282-
283-
func (u *Updater) update() error {
284-
path, err := os.Executable()
285-
if err != nil {
286-
return err
287-
}
288-
289-
path = addTempSuffixToPath(path)
290-
291-
old, err := os.Open(path)
292-
if err != nil {
293-
return err
294-
}
295-
defer old.Close()
296-
297-
err = u.fetchInfo()
298-
if err != nil {
299-
log.Println(err)
300-
return err
301-
}
302-
if u.Info.Version == u.CurrentVersion {
303-
return nil
304-
}
305-
bin, err := u.fetchAndVerifyPatch(old)
306-
if err != nil {
307-
switch err {
308-
case errHashMismatch:
309-
log.Println("update: hash mismatch from patched binary")
310-
case errDiffURLUndefined:
311-
log.Println("update: ", err)
312-
default:
313-
log.Println("update: patching binary, ", err)
314-
}
315-
316-
bin, err = u.fetchAndVerifyFullBin()
317-
if err != nil {
318-
if err == errHashMismatch {
319-
log.Println("update: hash mismatch from full binary")
320-
} else {
321-
log.Println("update: fetching full binary,", err)
322-
}
323-
return err
324-
}
325-
}
326-
327-
// close the old binary before installing because on windows
328-
// it can't be renamed if a handle to the file is still open
329-
old.Close()
330-
331-
up.TargetPath = path
332-
err, errRecover := up.FromStream(bytes.NewBuffer(bin))
333-
if errRecover != nil {
334-
log.Errorf("update and recovery errors: %q %q", err, errRecover)
335-
return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
336-
}
337-
if err != nil {
338-
return err
339-
}
340-
341-
return nil
28+
return checkForUpdates(currentVersion, updateAPIURL, updateBinURL, cmdName)
34229
}

0 commit comments

Comments
 (0)