forked from fynelabs/selfupdate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
updater.go
227 lines (191 loc) · 6.91 KB
/
updater.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
package selfupdate
import (
"crypto/ed25519"
"errors"
"io"
"sync"
"time"
)
// ErrNotSupported is returned by `Manage` when it is not possible to manage the current application.
var ErrNotSupported = errors.New("operating system not supported")
// Source define an interface that is able to get an update
type Source interface {
Get(*Version) (io.ReadCloser, int64, error) // Get the executable to be updated to
GetSignature() ([64]byte, error) // Get the signature that match the executable
LatestVersion() (*Version, error) // Get the latest version information to determine if we should trigger an update
}
// Config define extra parameter necessary to manage the updating process
type Config struct {
Current *Version // If present will define the current version of the executable that need update
Source Source // Necessary Source for update
Schedule Schedule // Define when to trigger an update
PublicKey ed25519.PublicKey // The public key that match the private key used to generate the signature of future update
ProgressCallback func(float64, error) // if present will call back with 0.0 at the start, rising through to 1.0 at the end if the progress is known. A negative start number will be sent if size is unknown, any error will pass as is and the process is considered done
RestartConfirmCallback func() bool // if present will ask for user acceptance before restarting app
UpgradeConfirmCallback func(string) bool // if present will ask for user acceptance, it can present the message passed
ExitCallback func(error) // if present will be expected to handle app exit procedure
}
// Repeating pattern for scheduling update at a specific time
type Repeating int
const (
// None will not schedule
None Repeating = iota
// Hourly will schedule in the next hour and repeat it every hour after
Hourly
// Daily will schedule next day and repeat it every day after
Daily
// Monthly will schedule next month and repeat it every month after
Monthly
)
// ScheduleAt define when a repeating update at a specific time should be triggered
type ScheduleAt struct {
Repeating // The pattern to enforce for the repeating schedule
time.Time // Offset time used to define when in a minute/hour/day/month to actually trigger the schedule
}
// Schedule define when to trigger an update
type Schedule struct {
FetchOnStart bool // Trigger when the updater is created
Interval time.Duration // Trigger at regular interval
At ScheduleAt // Trigger at a specific time
}
// Version define an executable versionning information
type Version struct {
Number string // if the app knows its version and supports checking metadata
Build int // if the app has a build number this could be compared
Date time.Time // last update, could be mtime
}
// Updater is managing update for your application in the background
type Updater struct {
lock sync.Mutex
conf *Config
executable string
}
// CheckNow will manually trigger a check of an update and if one is present will start the update process
func (u *Updater) CheckNow() error {
u.lock.Lock()
defer u.lock.Unlock()
v := u.conf.Current
if v == nil {
mtime, err := lastModifiedExecutable()
if err != nil {
return err
}
v = &Version{Date: mtime.In(time.UTC)}
}
latest, err := u.conf.Source.LatestVersion()
if err != nil {
return err
}
if !latest.Date.After(v.Date) {
logDebug("Local binary time (%v) is recent enough compared to the online version (%v).\n", v.Date.Format(time.RFC1123Z), latest.Date.Format(time.RFC1123Z))
return nil
}
if ask := u.conf.UpgradeConfirmCallback; ask != nil {
if !ask("New version found") {
logInfo("The user didn't confirm the upgrade.\n")
return nil
}
}
s, err := u.conf.Source.GetSignature()
if err != nil {
return err
}
r, contentLength, err := u.conf.Source.Get(v)
if err != nil {
return err
}
defer r.Close()
pr := &progressReader{Reader: r, progressCallback: u.conf.ProgressCallback, contentLength: contentLength}
u.executable, err = applyUpdate(pr, u.conf.PublicKey, s)
if err != nil {
return err
}
if ask := u.conf.RestartConfirmCallback; ask != nil {
if !ask() {
logInfo("The user didn't confirm restarting the application after upgrade.\n")
return nil
}
}
return u.Restart()
}
// Restart once an update is done can trigger a restart of the binary. This is useful to implement a restart later policy.
func (u *Updater) Restart() error {
return restart(u.conf.ExitCallback, u.executable)
}
// Manage sets up an Updater and runs it to manage the current executable.
func Manage(conf *Config) (*Updater, error) {
updater := &Updater{conf: conf}
go func() {
if updater.conf.Schedule.FetchOnStart {
logInfo("Doing an initial upgrade check.\n")
err := updater.CheckNow()
if err != nil {
logError("Upgrade error: %v\n", err)
}
}
if updater.conf.Schedule.Interval != 0 || updater.conf.Schedule.At.Repeating != None {
go func() {
triggerSchedule(updater)
}()
}
}()
// TODO check if we can support the current app!
return updater, nil
}
// ManualUpdate applies a specific update manually instead of managing the update of this app automatically.
func ManualUpdate(s Source, publicKey ed25519.PublicKey) error {
v := &Version{}
r, _, err := s.Get(v)
if err != nil {
return err
}
signature, err := s.GetSignature()
if err != nil {
return err
}
_, err = applyUpdate(r, publicKey, signature)
return err
}
func applyUpdate(r io.Reader, publicKey ed25519.PublicKey, signature [64]byte) (string, error) {
opts := &Options{}
opts.Signature = signature[:]
opts.PublicKey = publicKey
err := apply(r, opts)
if err != nil {
return "", err
}
return opts.TargetPath, nil
}
func triggerSchedule(updater *Updater) {
for {
var delay time.Duration
if updater.conf.Schedule.Interval != 0 {
delay = updater.conf.Schedule.Interval
}
if updater.conf.Schedule.At.Repeating != None {
at := delayUntilNextTriggerAt(updater.conf.Schedule.At.Repeating, updater.conf.Schedule.At.Time)
if delay == 0 || at < delay {
delay = at
}
}
time.Sleep(delay)
logInfo("Scheduled upgrade check after %s.\n", delay)
err := updater.CheckNow()
if err != nil {
logError("Upgrade error: %v\n", err)
}
}
}
func delayUntilNextTriggerAt(repeating Repeating, offset time.Time) time.Duration {
now := time.Now().In(offset.Location())
var next time.Time
switch repeating {
case Hourly:
next = time.Date(now.Year(), now.Month(), now.Day(), now.Hour()+1, offset.Minute(), offset.Second(), offset.Nanosecond(), offset.Location())
case Daily:
next = time.Date(now.Year(), now.Month(), now.Day()+1, offset.Hour(), offset.Minute(), offset.Second(), offset.Nanosecond(), offset.Location())
case Monthly:
next = time.Date(now.Year(), now.Month()+1, offset.Day(), offset.Hour(), offset.Minute(), offset.Second(), offset.Nanosecond(), offset.Location())
}
return next.Sub(now)
}