github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/status/stream.go (about) 1 // Copyright 2018 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache 2.0 3 // license that can be found in the LICENSE file. 4 5 package status 6 7 import ( 8 "fmt" 9 "io" 10 "os" 11 "os/signal" 12 "syscall" 13 "text/tabwriter" 14 "time" 15 ) 16 17 const ( 18 refreshPeriod = 10 * time.Second 19 // the smallest interval between reports in "simple" mode 20 minSimpleReportingPeriod = time.Minute 21 ) 22 23 var minDisplayInterval = 50 * time.Millisecond 24 25 type result struct { 26 n int 27 err error 28 } 29 30 type kind int 31 32 const ( 33 noop kind = iota 34 write 35 stop 36 ) 37 38 type req struct { 39 kind kind 40 p []byte 41 w io.Writer 42 rc chan result 43 } 44 45 type writer struct { 46 r Reporter 47 w io.Writer 48 } 49 50 func (w *writer) Write(p []byte) (n int, err error) { 51 if len(p) == 0 { 52 // To check EOF? 53 return w.w.Write(p) 54 } 55 c := make(chan result, 1) 56 w.r <- req{write, p, w.w, c} 57 r := <-c 58 return r.n, r.err 59 } 60 61 // Reporter displays regular updates of a Status. When updates are 62 // displayed on a terminal, each update replaces the previous, so 63 // only one update remains visible at a time. Otherwise update 64 // snapshots are written periodically. 65 type Reporter chan req 66 67 // Wrap returns a writer whose writes are serviced by the reporter 68 // and written to the underlying writer w. Wrap is used to allow an 69 // application to write to the same set of file descriptors as are 70 // used to render status updates. This permits the reporter's 71 // terminal handling code to properly write log messages while also 72 // rendering regular status updates. 73 func (r Reporter) Wrap(w io.Writer) io.Writer { 74 return &writer{r: r, w: w} 75 } 76 77 // Go starts the Reporter's service routine, and will write regular 78 // updates to the provided writer. 79 func (r Reporter) Go(w io.Writer, status *Status) { 80 if term, err := openTerm(w); err == nil { 81 r.displayTerm(w, term, status) 82 } else { 83 r.displaySimple(w, status) 84 } 85 } 86 87 // Stop halts rendering of status updates; writes to writers 88 // returned by Wrap are still serviced. 89 func (r Reporter) Stop() { 90 c := make(chan result, 1) 91 r <- req{kind: stop, rc: c} 92 <-c 93 } 94 95 // displayTerm updates a status on the terminal w, with terminal 96 // capabilities as described by term. displayTerm draws the status on 97 // each update of status, and also at a regular refresh period to 98 // update task elapsed times. Writes wrapped by the reporter are 99 // serviced after first clearing the screen. This ensures that these 100 // writes appear consistently on the screen above a persistent status 101 // display. displayTerm handles window resize events. 102 func (r Reporter) displayTerm(w io.Writer, term *term, status *Status) { 103 var nlines int 104 // TODO(marius): limit the maximum number of subtasks displayed 105 // Cursor is always at the end. 106 var ( 107 tick = time.NewTicker(refreshPeriod) 108 stopped bool 109 v = -1 110 winch = make(chan os.Signal, 1) 111 ) 112 signal.Notify(winch, syscall.SIGWINCH) 113 defer tick.Stop() 114 defer signal.Stop(winch) 115 width, height := term.Dim() 116 for { 117 var req req 118 select { 119 case v = <-status.Wait(v): 120 case req = <-r: 121 case <-tick.C: 122 case <-winch: 123 width, height = term.Dim() 124 } 125 if nlines > height { 126 nlines = height 127 } 128 for i := 0; i < nlines; i++ { 129 term.Move(w, -1) 130 term.Clear(w) 131 } 132 nlines = 0 133 switch req.kind { 134 case noop: 135 case write: 136 n, err := req.w.Write(req.p) 137 req.rc <- result{n, err} 138 case stop: 139 // We stop reporting status but keep servicing writes. 140 stopped = true 141 close(req.rc) 142 } 143 if stopped { 144 continue 145 } 146 groups := status.Groups() 147 if len(groups) == 0 { 148 continue 149 } 150 // Take a snapshot of all the values to be rendered. The 0th value 151 // in each group is the group toplevel status. We then accomodate 152 // for our height budget by trimming task statuses (oldest first). 153 // We coalesce tasks with the same status to the first mention of 154 // the task. 155 var snapshot [][]Value 156 for _, g := range groups { 157 v := g.Value() 158 tasks := g.Tasks() 159 if v.Status == "" && len(tasks) == 0 { 160 continue 161 } 162 statuses := make(map[string]int) 163 values := []Value{v} 164 for _, task := range tasks { 165 value := task.Value() 166 key := value.Title + value.Status 167 if i, ok := statuses[key]; ok { 168 values[i].Count++ 169 values[i].LastBegin = value.Begin 170 } else { 171 value.Count = 1 172 statuses[key] = len(values) 173 values = append(values, value) 174 } 175 } 176 snapshot = append(snapshot, values) 177 } 178 var n int 179 for _, g := range snapshot { 180 n += len(g) - 1 181 } 182 // Always make room for the toplevel status. 183 // We also need one extra line for the last newline. 184 for n > height-len(snapshot)-1 { 185 var ( 186 mini = -1 187 min time.Time 188 ) 189 for i, g := range snapshot { 190 if len(g) > 1 && (mini < 0 || g[1].Begin.Before(min)) { 191 min = g[1].Begin 192 mini = i 193 } 194 } 195 if mini < 0 { 196 // Nothing we can do. 197 break 198 } 199 snapshot[mini] = append(snapshot[mini][:1], snapshot[mini][2:]...) 200 n-- 201 } 202 now := time.Now() 203 for _, group := range snapshot { 204 v, tasks := group[0], group[1:] 205 top := fmt.Sprintf("%s: %s", v.Title, v.Status) 206 if len(top) > width { 207 top = top[:width] 208 } 209 fmt.Fprintln(w, top) 210 nlines++ 211 tw := tabwriter.NewWriter(w, 2, 4, 2, ' ', 0) 212 type row struct{ title, value, elapsed string } 213 rows := make([]row, len(tasks)) 214 var maxtitle, maxvalue, maxtime int 215 for i, v := range tasks { 216 elapsed := now.Sub(v.Begin) 217 row := row{ 218 title: v.Title, 219 value: v.Status, 220 elapsed: round(elapsed).String(), 221 } 222 if v.Count > 1 { 223 row.title += fmt.Sprintf("[%d]", v.Count) 224 if lastElapsed := round(now.Sub(v.LastBegin)); elapsed-lastElapsed > time.Minute { 225 row.elapsed = fmt.Sprintf("%s-%s", lastElapsed, row.elapsed) 226 } 227 } 228 maxtitle = max(maxtitle, len(row.title)) 229 maxvalue = max(maxvalue, len(row.value)) 230 maxtime = max(maxtime, len(row.elapsed)) 231 rows[i] = row 232 } 233 if trim := 2 + maxtitle + 3 + maxvalue + 2 + maxtime - width; trim > 0 { 234 if trim > maxvalue { 235 trim -= maxvalue 236 maxvalue = 0 237 } else { 238 maxvalue -= trim 239 trim = 0 240 } 241 if trim > 0 && maxtitle > 10 { 242 n := maxtitle - trim 243 if n < 10 { 244 n = 10 245 } 246 maxtitle = n 247 } 248 } 249 for _, row := range rows { 250 fmt.Fprintf(tw, "\t%s:\t%s\t%s\n", 251 trim(row.title, maxtitle), 252 trim(row.value, maxvalue), 253 trim(row.elapsed, maxtime), 254 ) 255 nlines++ 256 } 257 tw.Flush() 258 } 259 } 260 } 261 262 // displaySimple writes the provided status to writer w whenever 263 // the status is updated, but at a minimum interval defined by 264 // minSimpleReportingPeriod. Writes wrapped by the reporter 265 // are serviced directly by displaySimple. 266 func (r Reporter) displaySimple(w io.Writer, status *Status) { 267 var ( 268 stopped bool 269 v = -1 270 lastReport time.Time 271 nextReport <-chan time.Time 272 ) 273 for { 274 var req req 275 select { 276 case v = <-status.Wait(v): 277 case req = <-r: 278 case <-nextReport: 279 nextReport = nil 280 } 281 switch req.kind { 282 case noop: 283 case write: 284 n, err := req.w.Write(req.p) 285 req.rc <- result{n, err} 286 continue 287 case stop: 288 stopped = true 289 close(req.rc) 290 continue 291 } 292 if stopped { 293 continue 294 } 295 // In this case we're already waiting to report. 296 if nextReport != nil { 297 continue 298 } 299 if elapsed := time.Since(lastReport); elapsed < minSimpleReportingPeriod { 300 nextReport = time.After(minSimpleReportingPeriod - elapsed) 301 continue 302 } 303 // If writing fails, there's not much we can do besides try again next 304 // time. 305 _ = status.Marshal(w) 306 lastReport = time.Now() 307 } 308 } 309 310 func max(i, j int) int { 311 if i > j { 312 return i 313 } 314 return j 315 } 316 317 func trim(s string, n int) string { 318 if len(s) < n { 319 return s 320 } 321 return s[:n] 322 } 323 324 func round(d time.Duration) time.Duration { 325 return d - d%time.Second 326 }