/
jag.go
307 lines (267 loc) · 8.09 KB
/
jag.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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
// Copyright (C) 2021 Toitware ApS. All rights reserved.
// Use of this source code is governed by an MIT-style license that can be
// found in the LICENSE file.
package commands
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"runtime"
"strings"
"time"
"github.com/coreos/go-semver/semver"
segment "github.com/segmentio/analytics-go/v3"
"github.com/spf13/cobra"
"github.com/toitlang/jaguar/cmd/jag/analytics"
"github.com/toitlang/jaguar/cmd/jag/directory"
)
type ctxKey string
const (
ctxKeyInfo ctxKey = "info"
noAnalyticsFlagName string = "no-analytics"
)
type Info struct {
Version string `mapstructure:"version" yaml:"version" json:"version"`
Date string `mapstructure:"date" yaml:"date" json:"date"`
SDKVersion string `mapstructure:"sdkVersion" yaml:"sdkVersion" json:"sdkVersion"`
}
func SetInfo(ctx context.Context, info Info) context.Context {
return context.WithValue(ctx, ctxKeyInfo, info)
}
func GetInfo(ctx context.Context) Info {
return ctx.Value(ctxKeyInfo).(Info)
}
func JagCmd(info Info, isReleaseBuild bool) *cobra.Command {
var analyticsClient analytics.Client
configCmd := ConfigCmd(info)
cmd := &cobra.Command{
Use: "jag",
Short: "Fast development for your ESP32",
Long: "Jaguar is a Toit application for your ESP32 that gives you the fastest development cycle.\n\n" +
"Jaguar uses the capabilities of the Toit virtual machine to let you update and restart your\n" +
"ESP32 applications written in Toit over WiFi. Change your Toit code in your editor, update\n" +
"the application on your device, and restart it all within seconds. No need to flash over\n" +
"serial, reboot your device, or wait for it to reconnect to your network.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
noAnalytics, err := cmd.Flags().GetBool(noAnalyticsFlagName)
if err != nil || noAnalytics {
return
}
if isLikelyRunningOnBuildbot() {
return
}
// Avoid running the analytics and up-to-date check code when
// the command is a subcommand of 'config'.
current := cmd
for current.HasParent() {
if current == configCmd {
return
}
current = current.Parent()
}
// Be careful and assign to the outer analyticsClient, so
// we can close it correctly in the post-run action.
var analyticsErr error
analyticsClient, analyticsErr = analytics.GetClient()
if analyticsErr != nil {
return
}
command := (*cobra.Command)(cmd).UseLine()
enqueueAnalytics(analyticsClient, isReleaseBuild, info, command)
CheckUpToDate(info)
},
// The "post run" function on the 'jag' command needs to run also
// when the program exits with an error from main(). The cobra
// framework does not handle this automatically, so we special-case
// this to make sure we get a chance to close the analytics client.
PersistentPostRun: func(cmd *cobra.Command, args []string) {
if analyticsClient != nil {
analyticsClient.Close()
}
},
}
cmd.AddCommand(
ScanCmd(),
ContainerCmd(),
PingCmd(),
RunCmd(),
CompileCmd(),
SimulateCmd(),
DecodeCmd(),
SetupCmd(info),
FlashCmd(),
FirmwareCmd(),
MonitorCmd(),
WatchCmd(),
PortCmd(),
ToitCmd(),
PkgCmd(info),
configCmd,
VersionCmd(info, isReleaseBuild),
)
cmd.PersistentFlags().Bool(noAnalyticsFlagName, false, "do not send analytics")
cmd.PersistentFlags().MarkHidden(noAnalyticsFlagName)
return cmd
}
func enqueueAnalytics(client analytics.Client, isReleaseBuild bool, info Info, command string) {
now := time.Now()
first := client.First()
for {
properties := segment.Properties{
"jaguar": true,
"first": first,
"command": command,
"platform": runtime.GOOS,
}
if isReleaseBuild {
properties.Set("version", info.Version)
} else {
properties.Set("version", "development")
}
// Cleanly separate the events in time, so the order is guaranteed to be correct. We
// do this be pretending the first pseudo event happened a second ago, so the real
// event has the right timestamp.
timestamp := now
if first {
timestamp = timestamp.Add(-1 * time.Second)
}
client.Enqueue(segment.Page{
Name: "CLI Execute",
Properties: properties,
Timestamp: timestamp,
})
// When we generate the first analytics event, we treat it like a pseudo event
// to cleanly separate it from the other events for analysis purposes. This
// means that we need to send the same event again but without the first flag set,
// so we take another spin in the loop.
if !first {
break
}
first = false
}
}
type UpdateToDate struct {
Disabled bool `mapstructure:"disabled" yaml:"disabled" json:"disabled"`
LastSuccess string `mapstructure:"lastSuccess" yaml:"lastSuccess" json:"lastSuccess"`
LastAttempt string `mapstructure:"lastAttempt" yaml:"lastAttempt" json:"lastAttempt"`
}
const UpToDateKey = "up-to-date"
func CheckUpToDate(info Info) {
if !directory.IsReleaseBuild {
return
}
// Only run the update checks when we're outputting to a TTY.
stat, err := os.Stdout.Stat()
if err != nil || (stat.Mode()&os.ModeCharDevice) == 0 {
return
}
cfg, err := directory.GetUserConfig()
if err != nil {
return
}
var res UpdateToDate
rewrite := true
if cfg.IsSet(UpToDateKey) {
if err := cfg.UnmarshalKey(UpToDateKey, &res); err == nil {
rewrite = false
}
}
if rewrite {
res.Disabled = false
res.LastSuccess = ""
res.LastAttempt = ""
} else if res.Disabled {
return
}
now := time.Now()
if res.LastAttempt != "" {
if last, err := time.Parse(time.RFC3339, res.LastAttempt); err == nil {
elapsed := now.Sub(last)
// We don't want to use the network or ask GitHub too often, so we only
// attempt once every 5 minutes.
if elapsed < 5*time.Minute {
// Skip check. It is too soon to even try again.
return
}
}
}
if res.LastSuccess != "" {
if last, err := time.Parse(time.RFC3339, res.LastSuccess); err == nil {
elapsed := now.Sub(last)
if elapsed < 24*7*time.Hour {
// Skip check. We successfully checked not long ago.
return
}
} else {
res.LastSuccess = ""
}
}
res.LastAttempt = now.Format(time.RFC3339)
cfg.Set(UpToDateKey, res)
if err := directory.WriteConfig(cfg); err != nil {
return
}
// Construct the URL we're fetching version information from.
url := fmt.Sprintf("https://api.github.com/repos/%s/%s",
"toitlang/jaguar",
"releases/latest")
// Create an HTTP request with the bare minimum headers.
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return
}
request.Header.Add("User-Agent", "jaguar-cli")
client := http.Client{}
response, err := client.Do(request)
if err != nil || response.StatusCode < 200 || response.StatusCode > 299 {
return
}
bodyText, err := ioutil.ReadAll(response.Body)
if err != nil {
return
}
result := make(map[string]interface{})
json.Unmarshal(bodyText, &result)
tagNameBytes, err := json.Marshal(result["tag_name"])
if err != nil {
return
}
tagName := string(tagNameBytes)
matched, err := regexp.MatchString(`^\s*"?\s*v\d+\.\d+\.\d+\s*"?\s*$`, tagName)
if err != nil || !matched {
return
}
tagName = strings.TrimSpace(tagName)
tagName = strings.TrimPrefix(tagName, "\"")
tagName = strings.TrimSuffix(tagName, "\"")
currentVersion := semver.New(info.Version[1:])
latestVersion := semver.New(strings.TrimSpace(tagName)[1:])
if currentVersion.LessThan(*latestVersion) {
banner := strings.Repeat("-", 60)
fmt.Println()
fmt.Println(banner)
fmt.Println("There is a newer version of Jaguar available (v" + latestVersion.String() + "). You may")
fmt.Println("want to update using your package manager or download the new")
fmt.Println("version directly from:")
fmt.Println()
fmt.Println(" https://github.com/toitlang/jaguar/releases/latest")
fmt.Println()
fmt.Println("You can disable these periodic up-to-date checks through:")
fmt.Println()
fmt.Println(" $ jag config up-to-date disable")
fmt.Println()
fmt.Println("Have a great day.")
fmt.Println(banner)
fmt.Println()
}
res.LastSuccess = res.LastAttempt
cfg.Set(UpToDateKey, res)
if err := directory.WriteConfig(cfg); err != nil {
fmt.Println(err)
return
}
}