blob: 6bdf1407414064851bf0ba1977262bbeef686ba5 [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() {
Colin Cross8cc19912019-10-29 15:22:04 -0700173 if s.tableMode {
174 // Stop the action table tick outside of the lock to avoid lock ordering issues between s.done and
175 // s.lock, the goroutine in startActionTableTick can get blocked on the lock and be unable to read
176 // from the channel.
177 s.stopActionTableTick()
178 }
179
Colin Crossce525352019-06-08 18:58:00 -0700180 s.lock.Lock()
181 defer s.lock.Unlock()
182
Colin Cross4355ee62019-06-11 23:01:36 -0700183 s.stopSigwinch()
184
Colin Crossce525352019-06-08 18:58:00 -0700185 s.requestLine()
Colin Cross3dac80e2019-06-11 11:19:06 -0700186
187 s.runningActions = nil
188
189 if s.tableMode {
Colin Cross3dac80e2019-06-11 11:19:06 -0700190 // Update the table after clearing runningActions to clear it
191 s.actionTable()
192
193 // Reset the scrolling region to the whole terminal
194 fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
195 _, height, _ := termSize(s.writer)
196 // Move the cursor to the top of the now-blank, previously non-scrolling region
Colin Crossbf8f57e2019-09-20 15:00:22 -0700197 fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700198 // Turn the cursor back on
199 fmt.Fprintf(s.writer, ansi.showCursor())
200 }
Colin Crossce525352019-06-08 18:58:00 -0700201}
202
Colin Crosse0df1a32019-06-09 19:40:08 -0700203func (s *smartStatusOutput) Write(p []byte) (int, error) {
204 s.lock.Lock()
205 defer s.lock.Unlock()
206 s.print(string(p))
207 return len(p), nil
208}
209
Colin Crossce525352019-06-08 18:58:00 -0700210func (s *smartStatusOutput) requestLine() {
211 if !s.haveBlankLine {
212 fmt.Fprintln(s.writer)
213 s.haveBlankLine = true
214 }
215}
216
217func (s *smartStatusOutput) print(str string) {
218 if !s.haveBlankLine {
Colin Cross3dac80e2019-06-11 11:19:06 -0700219 fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
Colin Crossce525352019-06-08 18:58:00 -0700220 s.haveBlankLine = true
221 }
222 fmt.Fprint(s.writer, str)
223 if len(str) == 0 || str[len(str)-1] != '\n' {
224 fmt.Fprint(s.writer, "\n")
225 }
226}
227
228func (s *smartStatusOutput) statusLine(str string) {
229 idx := strings.IndexRune(str, '\n')
230 if idx != -1 {
231 str = str[0:idx]
232 }
233
234 // Limit line width to the terminal width, otherwise we'll wrap onto
235 // another line and we won't delete the previous line.
Colin Cross5137de02019-06-20 15:22:50 -0700236 str = elide(str, s.termWidth)
Colin Crossce525352019-06-08 18:58:00 -0700237
Colin Cross00bdfd82019-06-11 11:16:23 -0700238 // Move to the beginning on the line, turn on bold, print the output,
239 // turn off bold, then clear the rest of the line.
Colin Cross3dac80e2019-06-11 11:19:06 -0700240 start := "\r" + ansi.bold()
241 end := ansi.regular() + ansi.clearToEndOfLine()
Colin Cross00bdfd82019-06-11 11:16:23 -0700242 fmt.Fprint(s.writer, start, str, end)
Colin Crossce525352019-06-08 18:58:00 -0700243 s.haveBlankLine = false
244}
Colin Cross4355ee62019-06-11 23:01:36 -0700245
Colin Cross5137de02019-06-20 15:22:50 -0700246func elide(str string, width int) string {
247 if width > 0 && len(str) > width {
Colin Cross4355ee62019-06-11 23:01:36 -0700248 // TODO: Just do a max. Ninja elides the middle, but that's
249 // more complicated and these lines aren't that important.
Colin Cross5137de02019-06-20 15:22:50 -0700250 str = str[:width]
Colin Cross4355ee62019-06-11 23:01:36 -0700251 }
252
253 return str
254}
255
Colin Cross3dac80e2019-06-11 11:19:06 -0700256func (s *smartStatusOutput) startActionTableTick() {
257 s.ticker = time.NewTicker(time.Second)
258 go func() {
259 for {
260 select {
261 case <-s.ticker.C:
262 s.lock.Lock()
263 s.actionTable()
264 s.lock.Unlock()
265 case <-s.done:
266 return
267 }
268 }
269 }()
270}
271
272func (s *smartStatusOutput) stopActionTableTick() {
273 s.ticker.Stop()
274 s.done <- true
275}
276
Colin Cross4355ee62019-06-11 23:01:36 -0700277func (s *smartStatusOutput) startSigwinch() {
278 signal.Notify(s.sigwinch, syscall.SIGWINCH)
279 go func() {
280 for _ = range s.sigwinch {
281 s.lock.Lock()
282 s.updateTermSize()
Colin Cross3dac80e2019-06-11 11:19:06 -0700283 if s.tableMode {
284 s.actionTable()
285 }
Colin Cross4355ee62019-06-11 23:01:36 -0700286 s.lock.Unlock()
287 if s.sigwinchHandled != nil {
288 s.sigwinchHandled <- true
289 }
290 }
291 }()
292}
293
294func (s *smartStatusOutput) stopSigwinch() {
295 signal.Stop(s.sigwinch)
296 close(s.sigwinch)
297}
298
299func (s *smartStatusOutput) updateTermSize() {
Colin Cross3dac80e2019-06-11 11:19:06 -0700300 if w, h, ok := termSize(s.writer); ok {
301 firstUpdate := s.termHeight == 0 && s.termWidth == 0
302 oldScrollingHeight := s.termHeight - s.tableHeight
303
304 s.termWidth, s.termHeight = w, h
305
306 if s.tableMode {
307 tableHeight := s.requestedTableHeight
Dan Willemsen1eee1542019-07-30 13:44:03 -0700308 if tableHeight == 0 {
309 tableHeight = s.termHeight / 4
310 if tableHeight < 1 {
311 tableHeight = 1
312 } else if tableHeight > 10 {
313 tableHeight = 10
314 }
315 }
Colin Cross3dac80e2019-06-11 11:19:06 -0700316 if tableHeight > s.termHeight-1 {
317 tableHeight = s.termHeight - 1
318 }
319 s.tableHeight = tableHeight
320
321 scrollingHeight := s.termHeight - s.tableHeight
322
323 if !firstUpdate {
324 // If the scrolling region has changed, attempt to pan the existing text so that it is
325 // not overwritten by the table.
326 if scrollingHeight < oldScrollingHeight {
327 pan := oldScrollingHeight - scrollingHeight
328 if pan > s.tableHeight {
329 pan = s.tableHeight
330 }
331 fmt.Fprint(s.writer, ansi.panDown(pan))
332 }
333 }
334 }
Colin Cross4355ee62019-06-11 23:01:36 -0700335 }
336}
Colin Cross3dac80e2019-06-11 11:19:06 -0700337
338func (s *smartStatusOutput) actionTable() {
339 scrollingHeight := s.termHeight - s.tableHeight
340
341 // Update the scrolling region in case the height of the terminal changed
Colin Crossf0b987e2019-09-20 15:01:51 -0700342
Colin Crossbf8f57e2019-09-20 15:00:22 -0700343 fmt.Fprint(s.writer, ansi.setScrollingMargins(1, scrollingHeight))
Colin Cross3dac80e2019-06-11 11:19:06 -0700344
345 // Write as many status lines as fit in the table
Colin Crossf0b987e2019-09-20 15:01:51 -0700346 for tableLine := 0; tableLine < s.tableHeight; tableLine++ {
Colin Cross3dac80e2019-06-11 11:19:06 -0700347 if tableLine >= s.tableHeight {
348 break
349 }
Colin Crossf0b987e2019-09-20 15:01:51 -0700350 // Move the cursor to the correct line of the non-scrolling region
351 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1+tableLine, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700352
Colin Crossf0b987e2019-09-20 15:01:51 -0700353 if tableLine < len(s.runningActions) {
354 runningAction := s.runningActions[tableLine]
Colin Cross3dac80e2019-06-11 11:19:06 -0700355
Colin Crossf0b987e2019-09-20 15:01:51 -0700356 seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
357
358 desc := runningAction.action.Description
359 if desc == "" {
360 desc = runningAction.action.Command
361 }
362
363 color := ""
364 if seconds >= 60 {
365 color = ansi.red() + ansi.bold()
366 } else if seconds >= 30 {
367 color = ansi.yellow() + ansi.bold()
368 }
369
370 durationStr := fmt.Sprintf(" %2d:%02d ", seconds/60, seconds%60)
371 desc = elide(desc, s.termWidth-len(durationStr))
372 durationStr = color + durationStr + ansi.regular()
373 fmt.Fprint(s.writer, durationStr, desc)
Colin Cross3dac80e2019-06-11 11:19:06 -0700374 }
Colin Cross3dac80e2019-06-11 11:19:06 -0700375 fmt.Fprint(s.writer, ansi.clearToEndOfLine())
Colin Cross3dac80e2019-06-11 11:19:06 -0700376 }
377
378 // Move the cursor back to the last line of the scrolling region
Colin Crossbf8f57e2019-09-20 15:00:22 -0700379 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700380}
381
382var ansi = ansiImpl{}
383
384type ansiImpl struct{}
385
386func (ansiImpl) clearToEndOfLine() string {
387 return "\x1b[K"
388}
389
390func (ansiImpl) setCursor(row, column int) string {
391 // Direct cursor address
392 return fmt.Sprintf("\x1b[%d;%dH", row, column)
393}
394
395func (ansiImpl) setScrollingMargins(top, bottom int) string {
396 // Set Top and Bottom Margins DECSTBM
397 return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
398}
399
400func (ansiImpl) resetScrollingMargins() string {
401 // Set Top and Bottom Margins DECSTBM
402 return fmt.Sprintf("\x1b[r")
403}
404
Colin Cross5137de02019-06-20 15:22:50 -0700405func (ansiImpl) red() string {
406 return "\x1b[31m"
407}
408
409func (ansiImpl) yellow() string {
410 return "\x1b[33m"
411}
412
Colin Cross3dac80e2019-06-11 11:19:06 -0700413func (ansiImpl) bold() string {
414 return "\x1b[1m"
415}
416
417func (ansiImpl) regular() string {
418 return "\x1b[0m"
419}
420
421func (ansiImpl) showCursor() string {
422 return "\x1b[?25h"
423}
424
425func (ansiImpl) hideCursor() string {
426 return "\x1b[?25l"
427}
428
429func (ansiImpl) panDown(lines int) string {
430 return fmt.Sprintf("\x1b[%dS", lines)
431}
432
433func (ansiImpl) panUp(lines int) string {
434 return fmt.Sprintf("\x1b[%dT", lines)
435}