blob: 8659d4ddd7316c8d0001463cba128a58e378f3bf [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 Cross3dac80e2019-06-11 11:19:06 -070062 tableHeight, _ := strconv.Atoi(os.Getenv(tableHeightEnVar))
63
Colin Cross4355ee62019-06-11 23:01:36 -070064 s := &smartStatusOutput{
Colin Crossce525352019-06-08 18:58:00 -070065 writer: w,
66 formatter: formatter,
67
68 haveBlankLine: true,
Colin Cross4355ee62019-06-11 23:01:36 -070069
Colin Cross3dac80e2019-06-11 11:19:06 -070070 tableMode: tableHeight > 0,
71 requestedTableHeight: tableHeight,
72
73 done: make(chan bool),
Colin Cross4355ee62019-06-11 23:01:36 -070074 sigwinch: make(chan os.Signal),
Colin Crossce525352019-06-08 18:58:00 -070075 }
Colin Cross4355ee62019-06-11 23:01:36 -070076
77 s.updateTermSize()
78
Colin Cross3dac80e2019-06-11 11:19:06 -070079 if s.tableMode {
80 // Add empty lines at the bottom of the screen to scroll back the existing history
81 // and make room for the action table.
82 // TODO: read the cursor position to see if the empty lines are necessary?
83 for i := 0; i < s.tableHeight; i++ {
84 fmt.Fprintln(w)
85 }
86
87 // Hide the cursor to prevent seeing it bouncing around
88 fmt.Fprintf(s.writer, ansi.hideCursor())
89
90 // Configure the empty action table
91 s.actionTable()
92
93 // Start a tick to update the action table periodically
94 s.startActionTableTick()
95 }
96
Colin Cross4355ee62019-06-11 23:01:36 -070097 s.startSigwinch()
98
99 return s
Colin Crossce525352019-06-08 18:58:00 -0700100}
101
102func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
103 if level < status.StatusLvl {
104 return
105 }
106
107 str := s.formatter.message(level, message)
108
109 s.lock.Lock()
110 defer s.lock.Unlock()
111
112 if level > status.StatusLvl {
113 s.print(str)
114 } else {
115 s.statusLine(str)
116 }
117}
118
119func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
Colin Cross3dac80e2019-06-11 11:19:06 -0700120 startTime := time.Now()
121
Colin Crossce525352019-06-08 18:58:00 -0700122 str := action.Description
123 if str == "" {
124 str = action.Command
125 }
126
127 progress := s.formatter.progress(counts)
128
129 s.lock.Lock()
130 defer s.lock.Unlock()
131
Colin Cross3dac80e2019-06-11 11:19:06 -0700132 s.runningActions = append(s.runningActions, actionTableEntry{
133 action: action,
134 startTime: startTime,
135 })
136
Colin Crossce525352019-06-08 18:58:00 -0700137 s.statusLine(progress + str)
138}
139
140func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
141 str := result.Description
142 if str == "" {
143 str = result.Command
144 }
145
146 progress := s.formatter.progress(counts) + str
147
148 output := s.formatter.result(result)
149
150 s.lock.Lock()
151 defer s.lock.Unlock()
152
Colin Cross3dac80e2019-06-11 11:19:06 -0700153 for i, runningAction := range s.runningActions {
154 if runningAction.action == result.Action {
155 s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
156 break
157 }
158 }
159
Colin Crossce525352019-06-08 18:58:00 -0700160 if output != "" {
161 s.statusLine(progress)
162 s.requestLine()
163 s.print(output)
164 } else {
165 s.statusLine(progress)
166 }
167}
168
169func (s *smartStatusOutput) Flush() {
170 s.lock.Lock()
171 defer s.lock.Unlock()
172
Colin Cross4355ee62019-06-11 23:01:36 -0700173 s.stopSigwinch()
174
Colin Crossce525352019-06-08 18:58:00 -0700175 s.requestLine()
Colin Cross3dac80e2019-06-11 11:19:06 -0700176
177 s.runningActions = nil
178
179 if s.tableMode {
180 s.stopActionTableTick()
181
182 // Update the table after clearing runningActions to clear it
183 s.actionTable()
184
185 // Reset the scrolling region to the whole terminal
186 fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
187 _, height, _ := termSize(s.writer)
188 // Move the cursor to the top of the now-blank, previously non-scrolling region
189 fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 0))
190 // Turn the cursor back on
191 fmt.Fprintf(s.writer, ansi.showCursor())
192 }
Colin Crossce525352019-06-08 18:58:00 -0700193}
194
Colin Crosse0df1a32019-06-09 19:40:08 -0700195func (s *smartStatusOutput) Write(p []byte) (int, error) {
196 s.lock.Lock()
197 defer s.lock.Unlock()
198 s.print(string(p))
199 return len(p), nil
200}
201
Colin Crossce525352019-06-08 18:58:00 -0700202func (s *smartStatusOutput) requestLine() {
203 if !s.haveBlankLine {
204 fmt.Fprintln(s.writer)
205 s.haveBlankLine = true
206 }
207}
208
209func (s *smartStatusOutput) print(str string) {
210 if !s.haveBlankLine {
Colin Cross3dac80e2019-06-11 11:19:06 -0700211 fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
Colin Crossce525352019-06-08 18:58:00 -0700212 s.haveBlankLine = true
213 }
214 fmt.Fprint(s.writer, str)
215 if len(str) == 0 || str[len(str)-1] != '\n' {
216 fmt.Fprint(s.writer, "\n")
217 }
218}
219
220func (s *smartStatusOutput) statusLine(str string) {
221 idx := strings.IndexRune(str, '\n')
222 if idx != -1 {
223 str = str[0:idx]
224 }
225
226 // Limit line width to the terminal width, otherwise we'll wrap onto
227 // another line and we won't delete the previous line.
Colin Cross5137de02019-06-20 15:22:50 -0700228 str = elide(str, s.termWidth)
Colin Crossce525352019-06-08 18:58:00 -0700229
Colin Cross00bdfd82019-06-11 11:16:23 -0700230 // Move to the beginning on the line, turn on bold, print the output,
231 // turn off bold, then clear the rest of the line.
Colin Cross3dac80e2019-06-11 11:19:06 -0700232 start := "\r" + ansi.bold()
233 end := ansi.regular() + ansi.clearToEndOfLine()
Colin Cross00bdfd82019-06-11 11:16:23 -0700234 fmt.Fprint(s.writer, start, str, end)
Colin Crossce525352019-06-08 18:58:00 -0700235 s.haveBlankLine = false
236}
Colin Cross4355ee62019-06-11 23:01:36 -0700237
Colin Cross5137de02019-06-20 15:22:50 -0700238func elide(str string, width int) string {
239 if width > 0 && len(str) > width {
Colin Cross4355ee62019-06-11 23:01:36 -0700240 // TODO: Just do a max. Ninja elides the middle, but that's
241 // more complicated and these lines aren't that important.
Colin Cross5137de02019-06-20 15:22:50 -0700242 str = str[:width]
Colin Cross4355ee62019-06-11 23:01:36 -0700243 }
244
245 return str
246}
247
Colin Cross3dac80e2019-06-11 11:19:06 -0700248func (s *smartStatusOutput) startActionTableTick() {
249 s.ticker = time.NewTicker(time.Second)
250 go func() {
251 for {
252 select {
253 case <-s.ticker.C:
254 s.lock.Lock()
255 s.actionTable()
256 s.lock.Unlock()
257 case <-s.done:
258 return
259 }
260 }
261 }()
262}
263
264func (s *smartStatusOutput) stopActionTableTick() {
265 s.ticker.Stop()
266 s.done <- true
267}
268
Colin Cross4355ee62019-06-11 23:01:36 -0700269func (s *smartStatusOutput) startSigwinch() {
270 signal.Notify(s.sigwinch, syscall.SIGWINCH)
271 go func() {
272 for _ = range s.sigwinch {
273 s.lock.Lock()
274 s.updateTermSize()
Colin Cross3dac80e2019-06-11 11:19:06 -0700275 if s.tableMode {
276 s.actionTable()
277 }
Colin Cross4355ee62019-06-11 23:01:36 -0700278 s.lock.Unlock()
279 if s.sigwinchHandled != nil {
280 s.sigwinchHandled <- true
281 }
282 }
283 }()
284}
285
286func (s *smartStatusOutput) stopSigwinch() {
287 signal.Stop(s.sigwinch)
288 close(s.sigwinch)
289}
290
291func (s *smartStatusOutput) updateTermSize() {
Colin Cross3dac80e2019-06-11 11:19:06 -0700292 if w, h, ok := termSize(s.writer); ok {
293 firstUpdate := s.termHeight == 0 && s.termWidth == 0
294 oldScrollingHeight := s.termHeight - s.tableHeight
295
296 s.termWidth, s.termHeight = w, h
297
298 if s.tableMode {
299 tableHeight := s.requestedTableHeight
300 if tableHeight > s.termHeight-1 {
301 tableHeight = s.termHeight - 1
302 }
303 s.tableHeight = tableHeight
304
305 scrollingHeight := s.termHeight - s.tableHeight
306
307 if !firstUpdate {
308 // If the scrolling region has changed, attempt to pan the existing text so that it is
309 // not overwritten by the table.
310 if scrollingHeight < oldScrollingHeight {
311 pan := oldScrollingHeight - scrollingHeight
312 if pan > s.tableHeight {
313 pan = s.tableHeight
314 }
315 fmt.Fprint(s.writer, ansi.panDown(pan))
316 }
317 }
318 }
Colin Cross4355ee62019-06-11 23:01:36 -0700319 }
320}
Colin Cross3dac80e2019-06-11 11:19:06 -0700321
322func (s *smartStatusOutput) actionTable() {
323 scrollingHeight := s.termHeight - s.tableHeight
324
325 // Update the scrolling region in case the height of the terminal changed
326 fmt.Fprint(s.writer, ansi.setScrollingMargins(0, scrollingHeight))
327 // Move the cursor to the first line of the non-scrolling region
328 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1, 0))
329
330 // Write as many status lines as fit in the table
331 var tableLine int
332 var runningAction actionTableEntry
333 for tableLine, runningAction = range s.runningActions {
334 if tableLine >= s.tableHeight {
335 break
336 }
337
338 seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
339
340 desc := runningAction.action.Description
341 if desc == "" {
342 desc = runningAction.action.Command
343 }
344
Colin Cross5137de02019-06-20 15:22:50 -0700345 color := ""
346 if seconds >= 60 {
347 color = ansi.red() + ansi.bold()
348 } else if seconds >= 30 {
349 color = ansi.yellow() + ansi.bold()
350 }
351
352 durationStr := fmt.Sprintf(" %2d:%02d ", seconds/60, seconds%60)
353 desc = elide(desc, s.termWidth-len(durationStr))
354 durationStr = color + durationStr + ansi.regular()
355
356 fmt.Fprint(s.writer, durationStr, desc, ansi.clearToEndOfLine())
Colin Cross3dac80e2019-06-11 11:19:06 -0700357 if tableLine < s.tableHeight-1 {
358 fmt.Fprint(s.writer, "\n")
359 }
360 }
361
362 // Clear any remaining lines in the table
363 for ; tableLine < s.tableHeight; tableLine++ {
364 fmt.Fprint(s.writer, ansi.clearToEndOfLine())
365 if tableLine < s.tableHeight-1 {
366 fmt.Fprint(s.writer, "\n")
367 }
368 }
369
370 // Move the cursor back to the last line of the scrolling region
371 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 0))
372}
373
374var ansi = ansiImpl{}
375
376type ansiImpl struct{}
377
378func (ansiImpl) clearToEndOfLine() string {
379 return "\x1b[K"
380}
381
382func (ansiImpl) setCursor(row, column int) string {
383 // Direct cursor address
384 return fmt.Sprintf("\x1b[%d;%dH", row, column)
385}
386
387func (ansiImpl) setScrollingMargins(top, bottom int) string {
388 // Set Top and Bottom Margins DECSTBM
389 return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
390}
391
392func (ansiImpl) resetScrollingMargins() string {
393 // Set Top and Bottom Margins DECSTBM
394 return fmt.Sprintf("\x1b[r")
395}
396
Colin Cross5137de02019-06-20 15:22:50 -0700397func (ansiImpl) red() string {
398 return "\x1b[31m"
399}
400
401func (ansiImpl) yellow() string {
402 return "\x1b[33m"
403}
404
Colin Cross3dac80e2019-06-11 11:19:06 -0700405func (ansiImpl) bold() string {
406 return "\x1b[1m"
407}
408
409func (ansiImpl) regular() string {
410 return "\x1b[0m"
411}
412
413func (ansiImpl) showCursor() string {
414 return "\x1b[?25h"
415}
416
417func (ansiImpl) hideCursor() string {
418 return "\x1b[?25l"
419}
420
421func (ansiImpl) panDown(lines int) string {
422 return fmt.Sprintf("\x1b[%dS", lines)
423}
424
425func (ansiImpl) panUp(lines int) string {
426 return fmt.Sprintf("\x1b[%dT", lines)
427}