diff --git a/tools/asynq/cmd/dash.go b/tools/asynq/cmd/dash.go new file mode 100644 index 0000000..506c72a --- /dev/null +++ b/tools/asynq/cmd/dash.go @@ -0,0 +1,490 @@ +// Copyright 2022 Kentaro Hibino. All rights reserved. +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file. + +package cmd + +import ( + "fmt" + "math" + "math/rand" + "os" + "strconv" + "strings" + "time" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/gdamore/tcell/v2" + "github.com/hibiken/asynq" + "github.com/mattn/go-runewidth" + "github.com/spf13/cobra" +) + +var dashCmd = &cobra.Command{ + Use: "dash", + Short: "View dashboard", + Long: heredoc.Doc(` + Displays dashboard.`), + Args: cobra.NoArgs, + Run: dash, +} + +var ( + flagDebug = false + flagUseRealData = false +) + +func init() { + rootCmd.AddCommand(dashCmd) + // TODO: Remove this debug once we're done + dashCmd.Flags().BoolVar(&flagDebug, "debug", false, "Print debug info") + dashCmd.Flags().BoolVar(&flagUseRealData, "realdata", false, "Use real data in redis") +} + +type dashState struct { + queues []*asynq.QueueInfo + err error + rowIdx int // highlighted row +} + +func dash(cmd *cobra.Command, args []string) { + s, err := tcell.NewScreen() + if err != nil { + fmt.Println("failed to create a screen: %v", err) + os.Exit(1) + } + if err := s.Init(); err != nil { + fmt.Println("failed to initialize screen: %v", err) + os.Exit(1) + } + + inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: ":6379"}) + + // Set default text style + baseStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset) + s.SetStyle(baseStyle) + + queues, err := getQueueInfo(inspector) + state := dashState{ + queues: queues, + err: err, + } + // draw initial screen + drawDash(s, baseStyle, &state) + + eventCh := make(chan tcell.Event) + done := make(chan struct{}) + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + // TODO: Double check that we are not leaking goroutine with this one. + go s.ChannelEvents(eventCh, done) + + quit := func() { + s.Fini() + close(done) + os.Exit(0) + } + for { + // Update screen + s.Show() + + select { + case ev := <-eventCh: + // Process event + switch ev := ev.(type) { + case *tcell.EventResize: + s.Sync() + case *tcell.EventKey: + if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC || ev.Rune() == 'q' { + quit() + } else if ev.Key() == tcell.KeyCtrlL { + s.Sync() + } else if ev.Key() == tcell.KeyDown || ev.Rune() == 'j' { + if state.rowIdx < len(state.queues) { + state.rowIdx++ + } else { + state.rowIdx = 0 // loop back + } + drawDash(s, baseStyle, &state) + } else if ev.Key() == tcell.KeyUp || ev.Rune() == 'k' { + if state.rowIdx == 0 { + state.rowIdx = len(state.queues) + } else { + state.rowIdx-- + } + drawDash(s, baseStyle, &state) + } + } + + case <-ticker.C: + state.queues, state.err = getQueueInfo(inspector) + drawDash(s, baseStyle, &state) + } + } + +} + +func getQueueInfo(i *asynq.Inspector) ([]*asynq.QueueInfo, error) { + if !flagUseRealData { + n := rand.Intn(100) + return []*asynq.QueueInfo{ + {Queue: "default", Size: 1800 + n, Pending: 700 + n, Active: 300, Aggregating: 300, Scheduled: 200, Retry: 100, Archived: 200}, + {Queue: "critical", Size: 2300 + n, Pending: 1000 + n, Active: 500, Retry: 400, Completed: 400}, + {Queue: "low", Size: 900 + n, Pending: n, Active: 300, Scheduled: 400, Completed: 200}, + }, nil + } + queues, err := i.Queues() + if err != nil { + return nil, err + } + var res []*asynq.QueueInfo + for _, q := range queues { + info, err := i.GetQueueInfo(q) + if err != nil { + return nil, err + } + res = append(res, info) + } + return res, nil + +} + +func drawDash(s tcell.Screen, style tcell.Style, state *dashState) { + s.Clear() + // Simulate data update on every render + d := NewScreenDrawer(s) + d.Println("=== Queues ===", style.Bold(true)) + d.NL() // empty line + drawQueueSizeGraphs(d, style, state) + d.NL() // empty line + drawQueueTable(d, style, state) + d.GoToBottom() + drawFooter(d, style, state) +} + +func drawQueueSizeGraphs(d *ScreenDrawer, style tcell.Style, state *dashState) { + var ( + activeStyle = tcell.StyleDefault.Foreground(tcell.GetColor("blue")).Background(tcell.ColorReset) + pendingStyle = tcell.StyleDefault.Foreground(tcell.GetColor("green")).Background(tcell.ColorReset) + aggregatingStyle = tcell.StyleDefault.Foreground(tcell.GetColor("lightgreen")).Background(tcell.ColorReset) + scheduledStyle = tcell.StyleDefault.Foreground(tcell.GetColor("yellow")).Background(tcell.ColorReset) + retryStyle = tcell.StyleDefault.Foreground(tcell.GetColor("pink")).Background(tcell.ColorReset) + archivedStyle = tcell.StyleDefault.Foreground(tcell.GetColor("purple")).Background(tcell.ColorReset) + completedStyle = tcell.StyleDefault.Foreground(tcell.GetColor("darkgreen")).Background(tcell.ColorReset) + ) + + var qnames []string + var qsizes []string // queue size in strings + maxSize := 1 // not zero to avoid division by zero + for _, q := range state.queues { + qnames = append(qnames, q.Queue) + qsizes = append(qsizes, strconv.Itoa(q.Size)) + if q.Size > maxSize { + maxSize = q.Size + } + } + qnameWidth := maxwidth(qnames) + qsizeWidth := maxwidth(qsizes) + + // Calculate the multipler to scale the graph + screenWidth, _ := d.Screen().Size() + graphMaxWidth := screenWidth - (qnameWidth + qsizeWidth + 3) // | + multipiler := 1.0 + if graphMaxWidth < maxSize { + multipiler = float64(graphMaxWidth) / float64(maxSize) + } + + const tick = '▇' + for _, q := range state.queues { + d.Print(q.Queue, style) + d.Print(strings.Repeat(" ", qnameWidth-runewidth.StringWidth(q.Queue)+1), style) // padding between qname and graph + d.Print("|", style) + d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Completed)*multipiler))), completedStyle) + d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Archived)*multipiler))), archivedStyle) + d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Retry)*multipiler))), retryStyle) + d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Scheduled)*multipiler))), scheduledStyle) + d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Aggregating)*multipiler))), aggregatingStyle) + d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Pending)*multipiler))), pendingStyle) + d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Active)*multipiler))), activeStyle) + d.Print(fmt.Sprintf(" %d", q.Size), style) + d.NL() + } + d.NL() + d.Print("completed=", style) + d.Print(string(tick), completedStyle) + d.Print(" archived=", style) + d.Print(string(tick), archivedStyle) + d.Print(" retry=", style) + d.Print(string(tick), retryStyle) + d.Print(" scheduled=", style) + d.Print(string(tick), scheduledStyle) + d.Print(" aggregating=", style) + d.Print(string(tick), aggregatingStyle) + d.Print(" pending=", style) + d.Print(string(tick), pendingStyle) + d.Print(" active=", style) + d.Print(string(tick), activeStyle) + d.NL() +} + +func drawFooter(d *ScreenDrawer, baseStyle tcell.Style, state *dashState) { + if state.err != nil { + style := baseStyle.Background(tcell.ColorDarkRed) + d.Print(state.err.Error(), style) + d.FillLine(' ', style) + return + } + style := baseStyle.Background(tcell.ColorDarkSlateGray) + d.Print("F1=HELP", style) + d.FillLine(' ', style) +} + +// returns the maximum width from the given list of names +func maxwidth(names []string) int { + max := 0 + for _, s := range names { + if w := runewidth.StringWidth(s); w > max { + max = w + } + } + return max +} + +const colBuffer = 4 // extra buffer between columns + +type columnAlignment int + +const ( + alignRight columnAlignment = iota + alignLeft +) + +type column struct { + name string + width int + align columnAlignment + displayValues []string // TODO: Can we use these displayValues to display stuff? +} + +func newColumn(name string, align columnAlignment) *column { + return &column{ + name: name, + width: runewidth.StringWidth(name), + align: align, + } +} + +func (c *column) accommodate(v string) { + c.displayValues = append(c.displayValues, v) + if w := runewidth.StringWidth(v); w > c.width { + c.width = w + } +} + +type table struct { + cols []*column +} + +// QueueInfoFormatter exposes API to return display values for QueueInfo properties. +type QueueInfoFormatter struct { + q *asynq.QueueInfo +} + +func (f *QueueInfoFormatter) Queue() string { return f.q.Queue } +func (f *QueueInfoFormatter) Size() string { return strconv.Itoa(f.q.Size) } +func (f *QueueInfoFormatter) Processed() string { return strconv.Itoa(f.q.Processed) } +func (f *QueueInfoFormatter) Failed() string { return strconv.Itoa(f.q.Failed) } + +func (f *QueueInfoFormatter) State() string { + if f.q.Paused { + return "PAUSED" + } + return "RUN" +} + +func (f *QueueInfoFormatter) Latency() string { + return f.q.Latency.String() +} + +func (f *QueueInfoFormatter) ErrorRate() string { + return "0.23%" // TODO: Implement this +} + +func (f *QueueInfoFormatter) MemoryUsage() string { + return ByteCount(f.q.MemoryUsage) +} + +// ByteCount converts the given bytes into human readable string +func ByteCount(b int64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%d B", b) + + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + + } + return fmt.Sprintf("%.1f% cB", + float64(b)/float64(div), "kMGTPE"[exp]) + +} + +func drawQueueTable(d *ScreenDrawer, style tcell.Style, state *dashState) { + columns := []*column{ + newColumn("Queue", alignLeft), + newColumn("State", alignLeft), + newColumn("Size", alignRight), + newColumn("Latency", alignRight), + newColumn("MemoryUsage", alignRight), + newColumn("Processed", alignRight), + newColumn("Failed", alignRight), + newColumn("ErrorRate", alignRight), + } + + // Adjust the column widths to accomodate the values + for _, q := range state.queues { + f := QueueInfoFormatter{q} + for _, col := range columns { + switch col.name { + case "Queue": + col.accommodate(f.Queue()) + case "State": + col.accommodate(f.State()) + case "Size": + col.accommodate(f.Size()) + case "MemoryUsage": + col.accommodate(f.MemoryUsage()) + case "Latency": + col.accommodate(f.Latency()) + case "Processed": + col.accommodate(f.Processed()) + case "Failed": + col.accommodate(f.Failed()) + case "ErrorRate": + col.accommodate(f.ErrorRate()) + } + } + } + + // Header + headerStyle := style.Background(tcell.ColorDimGray).Foreground(tcell.ColorWhite) + width, _ := d.Screen().Size() + var b strings.Builder + for _, col := range columns { + if col.align == alignRight { + b.WriteString(lpad(col.name, col.width+colBuffer)) + } else { + b.WriteString(rpad(col.name, col.width+colBuffer)) + } + } + b.WriteString(strings.Repeat(" ", width-b.Len())) // span the full width + d.Println(b.String(), headerStyle) + + // Body + for i, q := range state.queues { + rowStyle := style + if state.rowIdx == i+1 { + rowStyle = style.Background(tcell.ColorDarkOliveGreen) + } + f := QueueInfoFormatter{q} + for _, col := range columns { + switch col.name { + case "Queue": + d.Print(rpad(f.Queue(), col.width+colBuffer), rowStyle) + case "State": + d.Print(rpad(f.State(), col.width+colBuffer), rowStyle) + case "Size": + d.Print(lpad(f.Size(), col.width+colBuffer), rowStyle) + case "MemoryUsage": + d.Print(lpad(f.MemoryUsage(), col.width+colBuffer), rowStyle) + case "Latency": + d.Print(lpad(f.Latency(), col.width+colBuffer), rowStyle) + case "Processed": + d.Print(lpad(f.Processed(), col.width+colBuffer), rowStyle) + case "Failed": + d.Print(lpad(f.Failed(), col.width+colBuffer), rowStyle) + case "ErrorRate": + d.Print(lpad(f.ErrorRate(), col.width+colBuffer), rowStyle) + } + } + d.FillLine(' ', rowStyle) + } + + if flagDebug { + d.Println(fmt.Sprintf("DEBUG: rowIdx = %d", state.rowIdx), style) + } +} + +/*** Screen Drawer ***/ + +// ScreenDrawer is used to draw contents on screen. +// +// Usage example: +// d := NewScreenDrawer(s) +// d.Println("Hello world", mystyle) +// d.NL() // adds newline +// d.Print("foo", mystyle.Bold(true)) +// d.Print("bar", mystyle.Italic(true)) +type ScreenDrawer struct { + l *LineDrawer +} + +func NewScreenDrawer(s tcell.Screen) *ScreenDrawer { + return &ScreenDrawer{l: NewLineDrawer(0, s)} +} + +func (d *ScreenDrawer) Print(s string, style tcell.Style) { + d.l.Draw(s, style) +} + +func (d *ScreenDrawer) Println(s string, style tcell.Style) { + d.Print(s, style) + d.NL() +} + +// FillLine prints the given run until the end of the current line +// and adds a newline. +func (d *ScreenDrawer) FillLine(r rune, style tcell.Style) { + w, _ := d.Screen().Size() + s := strings.Repeat(string(r), w-d.l.col) + d.Print(s, style) + d.NL() +} + +// NL adds a newline (i.e., moves to the next line). +func (d *ScreenDrawer) NL() { + d.l.row++ + d.l.col = 0 +} + +func (d *ScreenDrawer) Screen() tcell.Screen { + return d.l.s +} + +// Go to the bottom of the screen. +func (d *ScreenDrawer) GoToBottom() { + _, h := d.Screen().Size() + d.l.row = h - 1 + d.l.col = 0 +} + +type LineDrawer struct { + s tcell.Screen + row int + col int +} + +func NewLineDrawer(row int, s tcell.Screen) *LineDrawer { + return &LineDrawer{row: row, col: 0, s: s} +} + +func (d *LineDrawer) Draw(s string, style tcell.Style) { + for _, r := range s { + d.s.SetContent(d.col, d.row, r, nil, style) + d.col += runewidth.RuneWidth(r) + } +} diff --git a/tools/asynq/cmd/root.go b/tools/asynq/cmd/root.go index bc1795a..377c784 100644 --- a/tools/asynq/cmd/root.go +++ b/tools/asynq/cmd/root.go @@ -259,6 +259,12 @@ func rpad(s string, padding int) string { } +// lpad adds padding to the left of a string. +func lpad(s string, padding int) string { + tmpl := fmt.Sprintf("%%%ds ", padding) + return fmt.Sprintf(tmpl, s) +} + // indent indents the given text by given spaces. func indent(text string, space int) string { if len(text) == 0 { diff --git a/tools/go.mod b/tools/go.mod index 0e4c6ef..16b8442 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -5,9 +5,12 @@ go 1.13 require ( github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/fatih/color v1.9.0 + github.com/gdamore/tcell v1.4.0 // indirect + github.com/gdamore/tcell/v2 v2.5.1 // indirect github.com/go-redis/redis/v8 v8.11.4 github.com/hibiken/asynq v0.23.0 github.com/hibiken/asynq/x v0.0.0-20220131170841-349f4c50fb1d + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/prometheus/client_golang v1.11.0 github.com/spf13/cobra v1.1.1 diff --git a/tools/go.sum b/tools/go.sum index c2e3f56..0b45354 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -59,6 +59,12 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= +github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= +github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I= +github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -169,6 +175,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -178,6 +188,10 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -244,6 +258,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -372,6 +388,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -387,13 +404,19 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4= +golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=