|
15 | 15 |
|
16 | 16 | package updater
|
17 | 17 |
|
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. |
71 | 21 | 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) |
87 | 23 | }
|
88 | 24 |
|
| 25 | +// CheckForUpdates checks if there is a new version of the binary available and |
| 26 | +// if so downloads it. |
89 | 27 | 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) |
342 | 29 | }
|
0 commit comments