/
update.go
182 lines (152 loc) · 5.18 KB
/
update.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
package update
// adapted from
// https://github.com/inconshreveable/go-update
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"time"
"github.com/kardianos/osext"
"github.com/Masterminds/semver"
"github.com/hasura/graphql-engine/cli/v2/internal/errors"
)
const updateCheckURL = "https://releases.hasura.io/graphql-engine?agent=cli"
type updateCheckResponse struct {
Latest *semver.Version `json:"latest"`
PreRelease *semver.Version `json:"prerelease"`
}
func getLatestVersion() (*semver.Version, *semver.Version, error) {
var op errors.Op = "update.getLatestVersion"
res, err := http.Get(updateCheckURL)
if err != nil {
return nil, nil, errors.E(op, fmt.Errorf("update check request: %w", err))
}
defer res.Body.Close()
var response updateCheckResponse
err = json.NewDecoder(res.Body).Decode(&response)
if err != nil {
return nil, nil, errors.E(op, fmt.Errorf("decoding update check response: %w", err))
}
if response.Latest == nil && response.PreRelease == nil {
return nil, nil, errors.E(op, fmt.Errorf("expected version info not found at %s", updateCheckURL))
}
return response.Latest, response.PreRelease, nil
}
func buildAssetURL(v string) string {
os := runtime.GOOS
arch := runtime.GOARCH
extension := ""
if os == "windows" {
extension = ".exe"
}
return fmt.Sprintf(
"https://github.com/hasura/graphql-engine/releases/download/v%s/cli-hasura-%s-%s%s",
v, os, arch, extension,
)
}
func downloadAsset(url, fileName, filePath string) (*os.File, error) {
var op errors.Op = "update.downloadAsset"
res, err := http.Get(url)
if err != nil {
return nil, errors.E(op, errors.KindNetwork, fmt.Errorf("downloading asset: %w", err))
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, errors.E(op, errors.E("could not find the release asset"))
}
asset, err := os.OpenFile(
filepath.Join(filePath, fileName),
os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
0755,
)
if err != nil {
return nil, errors.E(op, fmt.Errorf("creating new binary file: %w", err))
}
defer asset.Close()
_, err = io.Copy(asset, res.Body)
if err != nil {
return nil, errors.E(op, fmt.Errorf("saving downloaded file: %w", err))
}
return asset, nil
}
// HasUpdate tells us if there is a new stable or prerelease update available.
func HasUpdate(currentVersion *semver.Version, timeFile string) (bool, *semver.Version, bool, *semver.Version, error) {
var op errors.Op = "update.HasUpdate"
if timeFile != "" {
defer func() {
if err := writeTimeToFile(timeFile, time.Now().UTC()); err != nil {
fmt.Fprintln(os.Stderr, "failed writing last update check time: ", err)
}
}()
}
latestVersion, preReleaseVersion, err := getLatestVersion()
if err != nil {
return false, nil, false, nil, errors.E(op, fmt.Errorf("get latest version: %w", err))
}
return latestVersion.GreaterThan(currentVersion), latestVersion, preReleaseVersion.GreaterThan(currentVersion), preReleaseVersion, nil
}
// ApplyUpdate downloads and applies the update indicated by version v.
func ApplyUpdate(v *semver.Version) error {
var op errors.Op = "update.ApplyUpdate"
// get the current executable
exe, err := osext.Executable()
if err != nil {
return errors.E(op, fmt.Errorf("find executable: %w", err))
}
// extract the filename and path
exePath := filepath.Dir(exe)
exeName := filepath.Base(exe)
// download the new binary
asset, err := downloadAsset(
buildAssetURL(v.String()), "."+exeName+".new", exePath,
)
if err != nil {
return errors.E(op, fmt.Errorf("download asset: %w", err))
}
// get the downloaded binary name and build the absolute path
newExe := asset.Name()
// build name and absolute path for saving old binary
oldExeName := "." + exeName + ".old"
oldExe := filepath.Join(exePath, oldExeName)
// delete any existing old binary file - this is necessary on Windows for two reasons:
// 1. after a successful update, Windows can't remove the .old file because the process is still running
// 2. windows rename operations fail if the destination file already exists
_ = os.Remove(oldExe)
// rename the current binary as old binary
err = os.Rename(exe, oldExe)
if err != nil {
return errors.E(op, fmt.Errorf("rename exe to old: %w", err))
}
// rename the new binary as the current binary
err = os.Rename(newExe, exe)
if err != nil {
// rename unsuccessfull
//
// The filesystem is now in a bad state. We have successfully
// moved the existing binary to a new location, but we couldn't move the new
// binary to take its place. That means there is no file where the current executable binary
// used to be!
// Try to rollback by restoring the old binary to its original path.
rerr := os.Rename(oldExe, exe)
if rerr != nil {
// rolling back failed, ask user to re-install cli
return errors.E(op, fmt.Errorf(
"rename old to exe: inconsistent state, re-install cli: %w",
rerr))
}
// rolled back, throw update error
return errors.E(op, fmt.Errorf("rename new to exe: %w", err))
}
// rename success, remove the old binary
errRemove := os.Remove(oldExe)
// windows has trouble removing old binaries, so hide it instead
// it will be removed next time this code runs.
if errRemove != nil {
_ = hideFile(oldExe)
}
return nil
}