Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 74bec3b

Browse files
committedJan 27, 2023
Added infrastructure for generic updater procedure
1 parent e7a8800 commit 74bec3b

File tree

3 files changed

+377
-320
lines changed

3 files changed

+377
-320
lines changed
 

‎updater/updater.go

Lines changed: 7 additions & 320 deletions
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
}

‎updater/updater_default.go

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
// Copyright 2022 Arduino SA
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU Affero General Public License as published
5+
// by the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU Affero General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Affero General Public License
14+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
16+
//go:build !darwin
17+
18+
package updater
19+
20+
import (
21+
"bytes"
22+
"compress/gzip"
23+
"crypto/sha256"
24+
"encoding/json"
25+
"errors"
26+
"fmt"
27+
"io"
28+
"net/http"
29+
"os"
30+
"path/filepath"
31+
"runtime"
32+
"strings"
33+
34+
"github.com/kr/binarydist"
35+
log "github.com/sirupsen/logrus"
36+
"gopkg.in/inconshreveable/go-update.v0"
37+
)
38+
39+
// Update protocol:
40+
//
41+
// GET hk.heroku.com/hk/linux-amd64.json
42+
//
43+
// 200 ok
44+
// {
45+
// "Version": "2",
46+
// "Sha256": "..." // base64
47+
// }
48+
//
49+
// then
50+
//
51+
// GET hkpatch.s3.amazonaws.com/hk/1/2/linux-amd64
52+
//
53+
// 200 ok
54+
// [bsdiff data]
55+
//
56+
// or
57+
//
58+
// GET hkdist.s3.amazonaws.com/hk/2/linux-amd64.gz
59+
//
60+
// 200 ok
61+
// [gzipped executable data]
62+
//
63+
//
64+
65+
const (
66+
plat = runtime.GOOS + "-" + runtime.GOARCH
67+
)
68+
69+
var errHashMismatch = errors.New("new file hash mismatch after patch")
70+
var errDiffURLUndefined = errors.New("DiffURL is not defined, I cannot fetch and apply patch, reverting to full bin")
71+
var up = update.New()
72+
73+
func start(src string) string {
74+
// If the executable is temporary, copy it to the full path, then restart
75+
if strings.Contains(src, "-temp") {
76+
newPath := removeTempSuffixFromPath(src)
77+
if err := copyExe(src, newPath); err != nil {
78+
log.Println("Copy error: ", err)
79+
panic(err)
80+
}
81+
return newPath
82+
}
83+
84+
// Otherwise copy to a path with -temp suffix
85+
if err := copyExe(src, addTempSuffixToPath(src)); err != nil {
86+
panic(err)
87+
}
88+
return ""
89+
}
90+
91+
func checkForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
92+
path, err := os.Executable()
93+
if err != nil {
94+
return "", err
95+
}
96+
var up = &Updater{
97+
CurrentVersion: currentVersion,
98+
APIURL: updateAPIURL,
99+
BinURL: updateBinURL,
100+
DiffURL: "",
101+
Dir: "update/",
102+
CmdName: cmdName,
103+
}
104+
105+
if err := up.BackgroundRun(); err != nil {
106+
return "", err
107+
}
108+
return addTempSuffixToPath(path), nil
109+
}
110+
111+
func copyExe(from, to string) error {
112+
data, err := os.ReadFile(from)
113+
if err != nil {
114+
log.Println("Cannot read file: ", from)
115+
return err
116+
}
117+
err = os.WriteFile(to, data, 0755)
118+
if err != nil {
119+
log.Println("Cannot write file: ", to)
120+
return err
121+
}
122+
return nil
123+
}
124+
125+
// addTempSuffixToPath adds the "-temp" suffix to the path to an executable file (a ".exe" extension is replaced with "-temp.exe")
126+
func addTempSuffixToPath(path string) string {
127+
if filepath.Ext(path) == "exe" {
128+
path = strings.Replace(path, ".exe", "-temp.exe", -1)
129+
} else {
130+
path = path + "-temp"
131+
}
132+
133+
return path
134+
}
135+
136+
// removeTempSuffixFromPath removes "-temp" suffix from the path to an executable file (a "-temp.exe" extension is replaced with ".exe")
137+
func removeTempSuffixFromPath(path string) string {
138+
return strings.Replace(path, "-temp", "", -1)
139+
}
140+
141+
// Updater is the configuration and runtime data for doing an update.
142+
//
143+
// Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location.
144+
//
145+
// Example:
146+
//
147+
// updater := &selfupdate.Updater{
148+
// CurrentVersion: version,
149+
// ApiURL: "http://updates.yourdomain.com/",
150+
// BinURL: "http://updates.yourdownmain.com/",
151+
// DiffURL: "http://updates.yourdomain.com/",
152+
// Dir: "update/",
153+
// CmdName: "myapp", // app name
154+
// }
155+
// if updater != nil {
156+
// go updater.BackgroundRun()
157+
// }
158+
type Updater struct {
159+
CurrentVersion string // Currently running version.
160+
APIURL string // Base URL for API requests (json files).
161+
CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
162+
BinURL string // Base URL for full binary downloads.
163+
DiffURL string // Base URL for diff downloads.
164+
Dir string // Directory to store selfupdate state.
165+
Info struct {
166+
Version string
167+
Sha256 []byte
168+
}
169+
}
170+
171+
// BackgroundRun starts the update check and apply cycle.
172+
func (u *Updater) BackgroundRun() error {
173+
os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777)
174+
if err := up.CanUpdate(); err != nil {
175+
log.Println(err)
176+
return err
177+
}
178+
//self, err := os.Executable()
179+
//if err != nil {
180+
// fail update, couldn't figure out path to self
181+
//return
182+
//}
183+
// TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports.
184+
if err := u.update(); err != nil {
185+
return err
186+
}
187+
return nil
188+
}
189+
190+
func fetch(url string) (io.ReadCloser, error) {
191+
resp, err := http.Get(url)
192+
if err != nil {
193+
return nil, err
194+
}
195+
if resp.StatusCode != 200 {
196+
log.Errorf("bad http status from %s: %v", url, resp.Status)
197+
return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status)
198+
}
199+
return resp.Body, nil
200+
}
201+
202+
func verifySha(bin []byte, sha []byte) bool {
203+
h := sha256.New()
204+
h.Write(bin)
205+
return bytes.Equal(h.Sum(nil), sha)
206+
}
207+
208+
func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) {
209+
if u.DiffURL == "" {
210+
return nil, errDiffURLUndefined
211+
}
212+
r, err := fetch(u.DiffURL + u.CmdName + "/" + u.CurrentVersion + "/" + u.Info.Version + "/" + plat)
213+
if err != nil {
214+
return nil, err
215+
}
216+
defer r.Close()
217+
var buf bytes.Buffer
218+
err = binarydist.Patch(old, &buf, r)
219+
return buf.Bytes(), err
220+
}
221+
222+
func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) {
223+
bin, err := u.fetchAndApplyPatch(old)
224+
if err != nil {
225+
return nil, err
226+
}
227+
if !verifySha(bin, u.Info.Sha256) {
228+
return nil, errHashMismatch
229+
}
230+
return bin, nil
231+
}
232+
233+
func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) {
234+
bin, err := u.fetchBin()
235+
if err != nil {
236+
return nil, err
237+
}
238+
verified := verifySha(bin, u.Info.Sha256)
239+
if !verified {
240+
return nil, errHashMismatch
241+
}
242+
return bin, nil
243+
}
244+
245+
func (u *Updater) fetchBin() ([]byte, error) {
246+
r, err := fetch(u.BinURL + u.CmdName + "/" + u.Info.Version + "/" + plat + ".gz")
247+
if err != nil {
248+
return nil, err
249+
}
250+
defer r.Close()
251+
buf := new(bytes.Buffer)
252+
gz, err := gzip.NewReader(r)
253+
if err != nil {
254+
return nil, err
255+
}
256+
if _, err = io.Copy(buf, gz); err != nil {
257+
return nil, err
258+
}
259+
260+
return buf.Bytes(), nil
261+
}
262+
263+
func (u *Updater) fetchInfo() error {
264+
r, err := fetch(u.APIURL + u.CmdName + "/" + plat + ".json")
265+
if err != nil {
266+
return err
267+
}
268+
defer r.Close()
269+
err = json.NewDecoder(r).Decode(&u.Info)
270+
if err != nil {
271+
return err
272+
}
273+
if len(u.Info.Sha256) != sha256.Size {
274+
return errors.New("bad cmd hash in info")
275+
}
276+
return nil
277+
}
278+
279+
func (u *Updater) getExecRelativeDir(dir string) string {
280+
filename, _ := os.Executable()
281+
path := filepath.Join(filepath.Dir(filename), dir)
282+
return path
283+
}
284+
285+
func (u *Updater) update() error {
286+
path, err := os.Executable()
287+
if err != nil {
288+
return err
289+
}
290+
291+
path = addTempSuffixToPath(path)
292+
293+
old, err := os.Open(path)
294+
if err != nil {
295+
return err
296+
}
297+
defer old.Close()
298+
299+
err = u.fetchInfo()
300+
if err != nil {
301+
log.Println(err)
302+
return err
303+
}
304+
if u.Info.Version == u.CurrentVersion {
305+
return nil
306+
}
307+
bin, err := u.fetchAndVerifyPatch(old)
308+
if err != nil {
309+
switch err {
310+
case errHashMismatch:
311+
log.Println("update: hash mismatch from patched binary")
312+
case errDiffURLUndefined:
313+
log.Println("update: ", err)
314+
default:
315+
log.Println("update: patching binary, ", err)
316+
}
317+
318+
bin, err = u.fetchAndVerifyFullBin()
319+
if err != nil {
320+
if err == errHashMismatch {
321+
log.Println("update: hash mismatch from full binary")
322+
} else {
323+
log.Println("update: fetching full binary,", err)
324+
}
325+
return err
326+
}
327+
}
328+
329+
// close the old binary before installing because on windows
330+
// it can't be renamed if a handle to the file is still open
331+
old.Close()
332+
333+
up.TargetPath = path
334+
err, errRecover := up.FromStream(bytes.NewBuffer(bin))
335+
if errRecover != nil {
336+
log.Errorf("update and recovery errors: %q %q", err, errRecover)
337+
return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
338+
}
339+
if err != nil {
340+
return err
341+
}
342+
343+
return nil
344+
}

‎updater/updater_macos.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2022 Arduino SA
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU Affero General Public License as published
5+
// by the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU Affero General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Affero General Public License
14+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
16+
//go:build darwin
17+
18+
package updater
19+
20+
func start(src string) string {
21+
return ""
22+
}
23+
24+
func checkForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
25+
return "", nil
26+
}

0 commit comments

Comments
 (0)
Please sign in to comment.