blob: aa7e50a4d02cd16d333cbb2d470c2cb0ee6d0063 [file] [log] [blame]
Colin Crossce525352019-06-08 18:58:00 -07001// Copyright 2019 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package terminal
16
17import (
18 "fmt"
Colin Cross097ed2a2019-06-08 21:48:58 -070019 "io"
Colin Cross4355ee62019-06-11 23:01:36 -070020 "os"
21 "os/signal"
Colin Cross3dac80e2019-06-11 11:19:06 -070022 "strconv"
Colin Crossce525352019-06-08 18:58:00 -070023 "strings"
24 "sync"
Colin Cross4355ee62019-06-11 23:01:36 -070025 "syscall"
Colin Cross3dac80e2019-06-11 11:19:06 -070026 "time"
Colin Crossce525352019-06-08 18:58:00 -070027
28 "android/soong/ui/status"
29)
30
Colin Cross3dac80e2019-06-11 11:19:06 -070031const tableHeightEnVar = "SOONG_UI_TABLE_HEIGHT"
32
33type actionTableEntry struct {
34 action *status.Action
35 startTime time.Time
36}
37
Colin Crossce525352019-06-08 18:58:00 -070038type smartStatusOutput struct {
Colin Cross097ed2a2019-06-08 21:48:58 -070039 writer io.Writer
Colin Crossce525352019-06-08 18:58:00 -070040 formatter formatter
41
42 lock sync.Mutex
43
44 haveBlankLine bool
Colin Cross4355ee62019-06-11 23:01:36 -070045
Colin Cross3dac80e2019-06-11 11:19:06 -070046 tableMode bool
47 tableHeight int
48 requestedTableHeight int
49 termWidth, termHeight int
50
51 runningActions []actionTableEntry
52 ticker *time.Ticker
53 done chan bool
Colin Cross4355ee62019-06-11 23:01:36 -070054 sigwinch chan os.Signal
55 sigwinchHandled chan bool
Colin Crossce525352019-06-08 18:58:00 -070056}
57
58// NewSmartStatusOutput returns a StatusOutput that represents the
59// current build status similarly to Ninja's built-in terminal
60// output.
Colin Cross097ed2a2019-06-08 21:48:58 -070061func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
Colin Cross4355ee62019-06-11 23:01:36 -070062 s := &smartStatusOutput{
Colin Crossce525352019-06-08 18:58:00 -070063 writer: w,
64 formatter: formatter,
65
66 haveBlankLine: true,
Colin Cross4355ee62019-06-11 23:01:36 -070067
Dan Willemsen1eee1542019-07-30 13:44:03 -070068 tableMode: true,
Colin Cross3dac80e2019-06-11 11:19:06 -070069
70 done: make(chan bool),
Colin Cross4355ee62019-06-11 23:01:36 -070071 sigwinch: make(chan os.Signal),
Colin Crossce525352019-06-08 18:58:00 -070072 }
Colin Cross4355ee62019-06-11 23:01:36 -070073
Dan Willemsen1eee1542019-07-30 13:44:03 -070074 if env, ok := os.LookupEnv(tableHeightEnVar); ok {
75 h, _ := strconv.Atoi(env)
76 s.tableMode = h > 0
77 s.requestedTableHeight = h
78 }
79
Colin Cross4355ee62019-06-11 23:01:36 -070080 s.updateTermSize()
81
Colin Cross3dac80e2019-06-11 11:19:06 -070082 if s.tableMode {
83 // Add empty lines at the bottom of the screen to scroll back the existing history
84 // and make room for the action table.
85 // TODO: read the cursor position to see if the empty lines are necessary?
86 for i := 0; i < s.tableHeight; i++ {
87 fmt.Fprintln(w)
88 }
89
90 // Hide the cursor to prevent seeing it bouncing around
91 fmt.Fprintf(s.writer, ansi.hideCursor())
92
93 // Configure the empty action table
94 s.actionTable()
95
96 // Start a tick to update the action table periodically
97 s.startActionTableTick()
98 }
99
Colin Cross4355ee62019-06-11 23:01:36 -0700100 s.startSigwinch()
101
102 return s
Colin Crossce525352019-06-08 18:58:00 -0700103}
104
105func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
106 if level < status.StatusLvl {
107 return
108 }
109
110 str := s.formatter.message(level, message)
111
112 s.lock.Lock()
113 defer s.lock.Unlock()
114
115 if level > status.StatusLvl {
116 s.print(str)
117 } else {
118 s.statusLine(str)
119 }
120}
121
122func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
Colin Cross3dac80e2019-06-11 11:19:06 -0700123 startTime := time.Now()
124
Colin Crossce525352019-06-08 18:58:00 -0700125 str := action.Description
126 if str == "" {
127 str = action.Command
128 }
129
130 progress := s.formatter.progress(counts)
131
132 s.lock.Lock()
133 defer s.lock.Unlock()
134
Colin Cross3dac80e2019-06-11 11:19:06 -0700135 s.runningActions = append(s.runningActions, actionTableEntry{
136 action: action,
137 startTime: startTime,
138 })
139
Colin Crossce525352019-06-08 18:58:00 -0700140 s.statusLine(progress + str)
141}
142
143func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
144 str := result.Description
145 if str == "" {
146 str = result.Command
147 }
148
149 progress := s.formatter.progress(counts) + str
150
151 output := s.formatter.result(result)
152
153 s.lock.Lock()
154 defer s.lock.Unlock()
155
Colin Cross3dac80e2019-06-11 11:19:06 -0700156 for i, runningAction := range s.runningActions {
157 if runningAction.action == result.Action {
158 s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
159 break
160 }
161 }
162
Colin Crossce525352019-06-08 18:58:00 -0700163 if output != "" {
164 s.statusLine(progress)
165 s.requestLine()
166 s.print(output)
167 } else {
168 s.statusLine(progress)
169 }
170}
171
172func (s *smartStatusOutput) Flush() {
173 s.lock.Lock()
174 defer s.lock.Unlock()
175
Colin Cross4355ee62019-06-11 23:01:36 -0700176 s.stopSigwinch()
177
Colin Crossce525352019-06-08 18:58:00 -0700178 s.requestLine()
Colin Cross3dac80e2019-06-11 11:19:06 -0700179
180 s.runningActions = nil
181
182 if s.tableMode {
183 s.stopActionTableTick()
184
185 // Update the table after clearing runningActions to clear it
186 s.actionTable()
187
188 // Reset the scrolling region to the whole terminal
189 fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
190 _, height, _ := termSize(s.writer)
191 // Move the cursor to the top of the now-blank, previously non-scrolling region
Colin Crossbf8f57e2019-09-20 15:00:22 -0700192 fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700193 // Turn the cursor back on
194 fmt.Fprintf(s.writer, ansi.showCursor())
195 }
Colin Crossce525352019-06-08 18:58:00 -0700196}
197
Colin Crosse0df1a32019-06-09 19:40:08 -0700198func (s *smartStatusOutput) Write(p []byte) (int, error) {
199 s.lock.Lock()
200 defer s.lock.Unlock()
201 s.print(string(p))
202 return len(p), nil
203}
204
Colin Crossce525352019-06-08 18:58:00 -0700205func (s *smartStatusOutput) requestLine() {
206 if !s.haveBlankLine {
207 fmt.Fprintln(s.writer)
208 s.haveBlankLine = true
209 }
210}
211
212func (s *smartStatusOutput) print(str string) {
213 if !s.haveBlankLine {
Colin Cross3dac80e2019-06-11 11:19:06 -0700214 fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
Colin Crossce525352019-06-08 18:58:00 -0700215 s.haveBlankLine = true
216 }
217 fmt.Fprint(s.writer, str)
218 if len(str) == 0 || str[len(str)-1] != '\n' {
219 fmt.Fprint(s.writer, "\n")
220 }
221}
222
223func (s *smartStatusOutput) statusLine(str string) {
224 idx := strings.IndexRune(str, '\n')
225 if idx != -1 {
226 str = str[0:idx]
227 }
228
229 // Limit line width to the terminal width, otherwise we'll wrap onto
230 // another line and we won't delete the previous line.
Colin Cross5137de02019-06-20 15:22:50 -0700231 str = elide(str, s.termWidth)
Colin Crossce525352019-06-08 18:58:00 -0700232
Colin Cross00bdfd82019-06-11 11:16:23 -0700233 // Move to the beginning on the line, turn on bold, print the output,
234 // turn off bold, then clear the rest of the line.
Colin Cross3dac80e2019-06-11 11:19:06 -0700235 start := "\r" + ansi.bold()
236 end := ansi.regular() + ansi.clearToEndOfLine()
Colin Cross00bdfd82019-06-11 11:16:23 -0700237 fmt.Fprint(s.writer, start, str, end)
Colin Crossce525352019-06-08 18:58:00 -0700238 s.haveBlankLine = false
239}
Colin Cross4355ee62019-06-11 23:01:36 -0700240
Colin Cross5137de02019-06-20 15:22:50 -0700241func elide(str string, width int) string {
242 if width > 0 && len(str) > width {
Colin Cross4355ee62019-06-11 23:01:36 -0700243 // TODO: Just do a max. Ninja elides the middle, but that's
244 // more complicated and these lines aren't that important.
Colin Cross5137de02019-06-20 15:22:50 -0700245 str = str[:width]
Colin Cross4355ee62019-06-11 23:01:36 -0700246 }
247
248 return str
249}
250
Colin Cross3dac80e2019-06-11 11:19:06 -0700251func (s *smartStatusOutput) startActionTableTick() {
252 s.ticker = time.NewTicker(time.Second)
253 go func() {
254 for {
255 select {
256 case <-s.ticker.C:
257 s.lock.Lock()
258 s.actionTable()
259 s.lock.Unlock()
260 case <-s.done:
261 return
262 }
263 }
264 }()
265}
266
267func (s *smartStatusOutput) stopActionTableTick() {
268 s.ticker.Stop()
269 s.done <- true
270}
271
Colin Cross4355ee62019-06-11 23:01:36 -0700272func (s *smartStatusOutput) startSigwinch() {
273 signal.Notify(s.sigwinch, syscall.SIGWINCH)
274 go func() {
275 for _ = range s.sigwinch {
276 s.lock.Lock()
277 s.updateTermSize()
Colin Cross3dac80e2019-06-11 11:19:06 -0700278 if s.tableMode {
279 s.actionTable()
280 }
Colin Cross4355ee62019-06-11 23:01:36 -0700281 s.lock.Unlock()
282 if s.sigwinchHandled != nil {
283 s.sigwinchHandled <- true
284 }
285 }
286 }()
287}
288
289func (s *smartStatusOutput) stopSigwinch() {
290 signal.Stop(s.sigwinch)
291 close(s.sigwinch)
292}
293
294func (s *smartStatusOutput) updateTermSize() {
Colin Cross3dac80e2019-06-11 11:19:06 -0700295 if w, h, ok := termSize(s.writer); ok {
296 firstUpdate := s.termHeight == 0 && s.termWidth == 0
297 oldScrollingHeight := s.termHeight - s.tableHeight
298
299 s.termWidth, s.termHeight = w, h
300
301 if s.tableMode {
302 tableHeight := s.requestedTableHeight
Dan Willemsen1eee1542019-07-30 13:44:03 -0700303 if tableHeight == 0 {
304 tableHeight = s.termHeight / 4
305 if tableHeight < 1 {
306 tableHeight = 1
307 } else if tableHeight > 10 {
308 tableHeight = 10
309 }
310 }
Colin Cross3dac80e2019-06-11 11:19:06 -0700311 if tableHeight > s.termHeight-1 {
312 tableHeight = s.termHeight - 1
313 }
314 s.tableHeight = tableHeight
315
316 scrollingHeight := s.termHeight - s.tableHeight
317
318 if !firstUpdate {
319 // If the scrolling region has changed, attempt to pan the existing text so that it is
320 // not overwritten by the table.
321 if scrollingHeight < oldScrollingHeight {
322 pan := oldScrollingHeight - scrollingHeight
323 if pan > s.tableHeight {
324 pan = s.tableHeight
325 }
326 fmt.Fprint(s.writer, ansi.panDown(pan))
327 }
328 }
329 }
Colin Cross4355ee62019-06-11 23:01:36 -0700330 }
331}
Colin Cross3dac80e2019-06-11 11:19:06 -0700332
333func (s *smartStatusOutput) actionTable() {
334 scrollingHeight := s.termHeight - s.tableHeight
335
336 // Update the scrolling region in case the height of the terminal changed
Colin Crossbf8f57e2019-09-20 15:00:22 -0700337 fmt.Fprint(s.writer, ansi.setScrollingMargins(1, scrollingHeight))
Colin Cross3dac80e2019-06-11 11:19:06 -0700338 // Move the cursor to the first line of the non-scrolling region
Colin Crossbf8f57e2019-09-20 15:00:22 -0700339 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700340
341 // Write as many status lines as fit in the table
342 var tableLine int
343 var runningAction actionTableEntry
344 for tableLine, runningAction = range s.runningActions {
345 if tableLine >= s.tableHeight {
346 break
347 }
348
349 seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
350
351 desc := runningAction.action.Description
352 if desc == "" {
353 desc = runningAction.action.Command
354 }
355
Colin Cross5137de02019-06-20 15:22:50 -0700356 color := ""
357 if seconds >= 60 {
358 color = ansi.red() + ansi.bold()
359 } else if seconds >= 30 {
360 color = ansi.yellow() + ansi.bold()
361 }
362
363 durationStr := fmt.Sprintf(" %2d:%02d ", seconds/60, seconds%60)
364 desc = elide(desc, s.termWidth-len(durationStr))
365 durationStr = color + durationStr + ansi.regular()
366
367 fmt.Fprint(s.writer, durationStr, desc, ansi.clearToEndOfLine())
Colin Cross3dac80e2019-06-11 11:19:06 -0700368 if tableLine < s.tableHeight-1 {
369 fmt.Fprint(s.writer, "\n")
370 }
371 }
372
373 // Clear any remaining lines in the table
374 for ; tableLine < s.tableHeight; tableLine++ {
375 fmt.Fprint(s.writer, ansi.clearToEndOfLine())
376 if tableLine < s.tableHeight-1 {
377 fmt.Fprint(s.writer, "\n")
378 }
379 }
380
381 // Move the cursor back to the last line of the scrolling region
Colin Crossbf8f57e2019-09-20 15:00:22 -0700382 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700383}
384
385var ansi = ansiImpl{}
386
387type ansiImpl struct{}
388
389func (ansiImpl) clearToEndOfLine() string {
390 return "\x1b[K"
391}
392
393func (ansiImpl) setCursor(row, column int) string {
394 // Direct cursor address
395 return fmt.Sprintf("\x1b[%d;%dH", row, column)
396}
397
398func (ansiImpl) setScrollingMargins(top, bottom int) string {
399 // Set Top and Bottom Margins DECSTBM
400 return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
401}
402
403func (ansiImpl) resetScrollingMargins() string {
404 // Set Top and Bottom Margins DECSTBM
405 return fmt.Sprintf("\x1b[r")
406}
407
Colin Cross5137de02019-06-20 15:22:50 -0700408func (ansiImpl) red() string {
409 return "\x1b[31m"
410}
411
412func (ansiImpl) yellow() string {
413 return "\x1b[33m"
414}
415
Colin Cross3dac80e2019-06-11 11:19:06 -0700416func (ansiImpl) bold() string {
417 return "\x1b[1m"
418}
419
420func (ansiImpl) regular() string {
421 return "\x1b[0m"
422}
423
424func (ansiImpl) showCursor() string {
425 return "\x1b[?25h"
426}
427
428func (ansiImpl) hideCursor() string {
429 return "\x1b[?25l"
430}
431
432func (ansiImpl) panDown(lines int) string {
433 return fmt.Sprintf("\x1b[%dS", lines)
434}
435
436func (ansiImpl) panUp(lines int) string {
437 return fmt.Sprintf("\x1b[%dT", lines)
438}