blob: 3880b04a8add3289f51a35896fd35cba6c1f03e2 [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 {
Jeongik Chaadbdbc32023-11-30 08:58:26 +090034 action *status.Action
35 startTime time.Time
Colin Cross3dac80e2019-06-11 11:19:06 -070036}
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
Joe Onoratoeadb0fb2023-06-24 15:03:28 -070056
57 // Once there is a failure, we stop printing command output so the error
58 // is easier to find
59 haveFailures bool
60 // If we are dropping errors, then at the end, we report a message to go
61 // look in the verbose log if you want that command output.
62 postFailureActionCount int
Colin Crossce525352019-06-08 18:58:00 -070063}
64
65// NewSmartStatusOutput returns a StatusOutput that represents the
66// current build status similarly to Ninja's built-in terminal
67// output.
Colin Cross097ed2a2019-06-08 21:48:58 -070068func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
Colin Cross4355ee62019-06-11 23:01:36 -070069 s := &smartStatusOutput{
Colin Crossce525352019-06-08 18:58:00 -070070 writer: w,
71 formatter: formatter,
72
73 haveBlankLine: true,
Colin Cross4355ee62019-06-11 23:01:36 -070074
Dan Willemsen1eee1542019-07-30 13:44:03 -070075 tableMode: true,
Colin Cross3dac80e2019-06-11 11:19:06 -070076
77 done: make(chan bool),
Colin Cross4355ee62019-06-11 23:01:36 -070078 sigwinch: make(chan os.Signal),
Colin Crossce525352019-06-08 18:58:00 -070079 }
Colin Cross4355ee62019-06-11 23:01:36 -070080
Dan Willemsen1eee1542019-07-30 13:44:03 -070081 if env, ok := os.LookupEnv(tableHeightEnVar); ok {
82 h, _ := strconv.Atoi(env)
83 s.tableMode = h > 0
84 s.requestedTableHeight = h
85 }
86
Colin Cross72a28a62021-02-10 13:12:41 -080087 if w, h, ok := termSize(s.writer); ok {
88 s.termWidth, s.termHeight = w, h
89 s.computeTableHeight()
90 } else {
91 s.tableMode = false
92 }
Colin Cross4355ee62019-06-11 23:01:36 -070093
Colin Cross3dac80e2019-06-11 11:19:06 -070094 if s.tableMode {
95 // Add empty lines at the bottom of the screen to scroll back the existing history
96 // and make room for the action table.
97 // TODO: read the cursor position to see if the empty lines are necessary?
98 for i := 0; i < s.tableHeight; i++ {
99 fmt.Fprintln(w)
100 }
101
102 // Hide the cursor to prevent seeing it bouncing around
103 fmt.Fprintf(s.writer, ansi.hideCursor())
104
105 // Configure the empty action table
106 s.actionTable()
107
108 // Start a tick to update the action table periodically
109 s.startActionTableTick()
110 }
111
Colin Cross4355ee62019-06-11 23:01:36 -0700112 s.startSigwinch()
113
114 return s
Colin Crossce525352019-06-08 18:58:00 -0700115}
116
117func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
118 if level < status.StatusLvl {
119 return
120 }
121
122 str := s.formatter.message(level, message)
123
124 s.lock.Lock()
125 defer s.lock.Unlock()
126
127 if level > status.StatusLvl {
128 s.print(str)
129 } else {
130 s.statusLine(str)
131 }
132}
133
134func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
Colin Cross3dac80e2019-06-11 11:19:06 -0700135 startTime := time.Now()
136
Colin Crossce525352019-06-08 18:58:00 -0700137 str := action.Description
138 if str == "" {
139 str = action.Command
140 }
141
142 progress := s.formatter.progress(counts)
143
144 s.lock.Lock()
145 defer s.lock.Unlock()
146
Colin Cross3dac80e2019-06-11 11:19:06 -0700147 s.runningActions = append(s.runningActions, actionTableEntry{
148 action: action,
149 startTime: startTime,
150 })
151
Colin Crossce525352019-06-08 18:58:00 -0700152 s.statusLine(progress + str)
153}
154
155func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
156 str := result.Description
157 if str == "" {
158 str = result.Command
159 }
160
161 progress := s.formatter.progress(counts) + str
162
163 output := s.formatter.result(result)
164
165 s.lock.Lock()
166 defer s.lock.Unlock()
167
Colin Cross3dac80e2019-06-11 11:19:06 -0700168 for i, runningAction := range s.runningActions {
169 if runningAction.action == result.Action {
170 s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
171 break
172 }
173 }
174
Joe Onoratoeadb0fb2023-06-24 15:03:28 -0700175 s.statusLine(progress)
176
177 // Stop printing when there are failures, but don't skip actions that also have their own errors.
Colin Crossce525352019-06-08 18:58:00 -0700178 if output != "" {
Joe Onoratoeadb0fb2023-06-24 15:03:28 -0700179 if !s.haveFailures || result.Error != nil {
180 s.requestLine()
181 s.print(output)
182 } else {
183 s.postFailureActionCount++
184 }
185 }
186
187 if result.Error != nil {
188 s.haveFailures = true
Colin Crossce525352019-06-08 18:58:00 -0700189 }
190}
191
192func (s *smartStatusOutput) Flush() {
Colin Cross8cc19912019-10-29 15:22:04 -0700193 if s.tableMode {
194 // Stop the action table tick outside of the lock to avoid lock ordering issues between s.done and
195 // s.lock, the goroutine in startActionTableTick can get blocked on the lock and be unable to read
196 // from the channel.
197 s.stopActionTableTick()
198 }
199
Colin Crossce525352019-06-08 18:58:00 -0700200 s.lock.Lock()
201 defer s.lock.Unlock()
202
Colin Cross4355ee62019-06-11 23:01:36 -0700203 s.stopSigwinch()
204
Joe Onoratoeadb0fb2023-06-24 15:03:28 -0700205 if s.postFailureActionCount > 0 {
206 s.requestLine()
207 if s.postFailureActionCount == 1 {
208 s.print(fmt.Sprintf("There was 1 action that completed after the action that failed. See verbose.log.gz for its output."))
209 } else {
210 s.print(fmt.Sprintf("There were %d actions that completed after the action that failed. See verbose.log.gz for their output.", s.postFailureActionCount))
211 }
212 }
213
Colin Crossce525352019-06-08 18:58:00 -0700214 s.requestLine()
Colin Cross3dac80e2019-06-11 11:19:06 -0700215
216 s.runningActions = nil
217
218 if s.tableMode {
Colin Cross3dac80e2019-06-11 11:19:06 -0700219 // Update the table after clearing runningActions to clear it
220 s.actionTable()
221
222 // Reset the scrolling region to the whole terminal
223 fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
224 _, height, _ := termSize(s.writer)
225 // Move the cursor to the top of the now-blank, previously non-scrolling region
Colin Crossbf8f57e2019-09-20 15:00:22 -0700226 fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700227 // Turn the cursor back on
228 fmt.Fprintf(s.writer, ansi.showCursor())
229 }
Colin Crossce525352019-06-08 18:58:00 -0700230}
231
Colin Crosse0df1a32019-06-09 19:40:08 -0700232func (s *smartStatusOutput) Write(p []byte) (int, error) {
233 s.lock.Lock()
234 defer s.lock.Unlock()
235 s.print(string(p))
236 return len(p), nil
237}
238
Colin Crossce525352019-06-08 18:58:00 -0700239func (s *smartStatusOutput) requestLine() {
240 if !s.haveBlankLine {
241 fmt.Fprintln(s.writer)
242 s.haveBlankLine = true
243 }
244}
245
246func (s *smartStatusOutput) print(str string) {
247 if !s.haveBlankLine {
Colin Cross3dac80e2019-06-11 11:19:06 -0700248 fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
Colin Crossce525352019-06-08 18:58:00 -0700249 s.haveBlankLine = true
250 }
251 fmt.Fprint(s.writer, str)
252 if len(str) == 0 || str[len(str)-1] != '\n' {
253 fmt.Fprint(s.writer, "\n")
254 }
255}
256
257func (s *smartStatusOutput) statusLine(str string) {
258 idx := strings.IndexRune(str, '\n')
259 if idx != -1 {
260 str = str[0:idx]
261 }
262
263 // Limit line width to the terminal width, otherwise we'll wrap onto
264 // another line and we won't delete the previous line.
Colin Cross5137de02019-06-20 15:22:50 -0700265 str = elide(str, s.termWidth)
Colin Crossce525352019-06-08 18:58:00 -0700266
Colin Cross00bdfd82019-06-11 11:16:23 -0700267 // Move to the beginning on the line, turn on bold, print the output,
268 // turn off bold, then clear the rest of the line.
Colin Cross3dac80e2019-06-11 11:19:06 -0700269 start := "\r" + ansi.bold()
270 end := ansi.regular() + ansi.clearToEndOfLine()
Colin Cross00bdfd82019-06-11 11:16:23 -0700271 fmt.Fprint(s.writer, start, str, end)
Colin Crossce525352019-06-08 18:58:00 -0700272 s.haveBlankLine = false
273}
Colin Cross4355ee62019-06-11 23:01:36 -0700274
Colin Cross5137de02019-06-20 15:22:50 -0700275func elide(str string, width int) string {
276 if width > 0 && len(str) > width {
Colin Cross4355ee62019-06-11 23:01:36 -0700277 // TODO: Just do a max. Ninja elides the middle, but that's
278 // more complicated and these lines aren't that important.
Colin Cross5137de02019-06-20 15:22:50 -0700279 str = str[:width]
Colin Cross4355ee62019-06-11 23:01:36 -0700280 }
281
282 return str
283}
284
Colin Cross3dac80e2019-06-11 11:19:06 -0700285func (s *smartStatusOutput) startActionTableTick() {
286 s.ticker = time.NewTicker(time.Second)
287 go func() {
288 for {
289 select {
290 case <-s.ticker.C:
291 s.lock.Lock()
292 s.actionTable()
293 s.lock.Unlock()
294 case <-s.done:
295 return
296 }
297 }
298 }()
299}
300
301func (s *smartStatusOutput) stopActionTableTick() {
302 s.ticker.Stop()
303 s.done <- true
304}
305
Colin Cross4355ee62019-06-11 23:01:36 -0700306func (s *smartStatusOutput) startSigwinch() {
307 signal.Notify(s.sigwinch, syscall.SIGWINCH)
308 go func() {
309 for _ = range s.sigwinch {
310 s.lock.Lock()
311 s.updateTermSize()
Colin Cross3dac80e2019-06-11 11:19:06 -0700312 if s.tableMode {
313 s.actionTable()
314 }
Colin Cross4355ee62019-06-11 23:01:36 -0700315 s.lock.Unlock()
316 if s.sigwinchHandled != nil {
317 s.sigwinchHandled <- true
318 }
319 }
320 }()
321}
322
323func (s *smartStatusOutput) stopSigwinch() {
324 signal.Stop(s.sigwinch)
325 close(s.sigwinch)
326}
327
Colin Cross72a28a62021-02-10 13:12:41 -0800328// computeTableHeight recomputes s.tableHeight based on s.termHeight and s.requestedTableHeight.
329func (s *smartStatusOutput) computeTableHeight() {
330 tableHeight := s.requestedTableHeight
331 if tableHeight == 0 {
332 tableHeight = s.termHeight / 4
333 if tableHeight < 1 {
334 tableHeight = 1
335 } else if tableHeight > 10 {
336 tableHeight = 10
337 }
338 }
339 if tableHeight > s.termHeight-1 {
340 tableHeight = s.termHeight - 1
341 }
342 s.tableHeight = tableHeight
343}
344
345// updateTermSize recomputes the table height after a SIGWINCH and pans any existing text if
346// necessary.
Colin Cross4355ee62019-06-11 23:01:36 -0700347func (s *smartStatusOutput) updateTermSize() {
Colin Cross3dac80e2019-06-11 11:19:06 -0700348 if w, h, ok := termSize(s.writer); ok {
Colin Cross3dac80e2019-06-11 11:19:06 -0700349 oldScrollingHeight := s.termHeight - s.tableHeight
350
351 s.termWidth, s.termHeight = w, h
352
353 if s.tableMode {
Colin Cross72a28a62021-02-10 13:12:41 -0800354 s.computeTableHeight()
Colin Cross3dac80e2019-06-11 11:19:06 -0700355
356 scrollingHeight := s.termHeight - s.tableHeight
357
Colin Cross72a28a62021-02-10 13:12:41 -0800358 // If the scrolling region has changed, attempt to pan the existing text so that it is
359 // not overwritten by the table.
360 if scrollingHeight < oldScrollingHeight {
361 pan := oldScrollingHeight - scrollingHeight
362 if pan > s.tableHeight {
363 pan = s.tableHeight
Colin Cross3dac80e2019-06-11 11:19:06 -0700364 }
Colin Cross72a28a62021-02-10 13:12:41 -0800365 fmt.Fprint(s.writer, ansi.panDown(pan))
Colin Cross3dac80e2019-06-11 11:19:06 -0700366 }
367 }
Colin Cross4355ee62019-06-11 23:01:36 -0700368 }
369}
Colin Cross3dac80e2019-06-11 11:19:06 -0700370
371func (s *smartStatusOutput) actionTable() {
372 scrollingHeight := s.termHeight - s.tableHeight
373
374 // Update the scrolling region in case the height of the terminal changed
Colin Crossf0b987e2019-09-20 15:01:51 -0700375
Colin Crossbf8f57e2019-09-20 15:00:22 -0700376 fmt.Fprint(s.writer, ansi.setScrollingMargins(1, scrollingHeight))
Colin Cross3dac80e2019-06-11 11:19:06 -0700377
378 // Write as many status lines as fit in the table
Colin Crossf0b987e2019-09-20 15:01:51 -0700379 for tableLine := 0; tableLine < s.tableHeight; tableLine++ {
Colin Cross3dac80e2019-06-11 11:19:06 -0700380 if tableLine >= s.tableHeight {
381 break
382 }
Colin Crossf0b987e2019-09-20 15:01:51 -0700383 // Move the cursor to the correct line of the non-scrolling region
384 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1+tableLine, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700385
Colin Crossf0b987e2019-09-20 15:01:51 -0700386 if tableLine < len(s.runningActions) {
387 runningAction := s.runningActions[tableLine]
Colin Cross3dac80e2019-06-11 11:19:06 -0700388
Colin Crossf0b987e2019-09-20 15:01:51 -0700389 seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
390
391 desc := runningAction.action.Description
392 if desc == "" {
393 desc = runningAction.action.Command
394 }
395
396 color := ""
397 if seconds >= 60 {
398 color = ansi.red() + ansi.bold()
399 } else if seconds >= 30 {
400 color = ansi.yellow() + ansi.bold()
401 }
402
403 durationStr := fmt.Sprintf(" %2d:%02d ", seconds/60, seconds%60)
404 desc = elide(desc, s.termWidth-len(durationStr))
405 durationStr = color + durationStr + ansi.regular()
406 fmt.Fprint(s.writer, durationStr, desc)
Colin Cross3dac80e2019-06-11 11:19:06 -0700407 }
Colin Cross3dac80e2019-06-11 11:19:06 -0700408 fmt.Fprint(s.writer, ansi.clearToEndOfLine())
Colin Cross3dac80e2019-06-11 11:19:06 -0700409 }
410
411 // Move the cursor back to the last line of the scrolling region
Colin Crossbf8f57e2019-09-20 15:00:22 -0700412 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700413}
414
415var ansi = ansiImpl{}
416
417type ansiImpl struct{}
418
419func (ansiImpl) clearToEndOfLine() string {
420 return "\x1b[K"
421}
422
423func (ansiImpl) setCursor(row, column int) string {
424 // Direct cursor address
425 return fmt.Sprintf("\x1b[%d;%dH", row, column)
426}
427
428func (ansiImpl) setScrollingMargins(top, bottom int) string {
429 // Set Top and Bottom Margins DECSTBM
430 return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
431}
432
433func (ansiImpl) resetScrollingMargins() string {
434 // Set Top and Bottom Margins DECSTBM
435 return fmt.Sprintf("\x1b[r")
436}
437
Colin Cross5137de02019-06-20 15:22:50 -0700438func (ansiImpl) red() string {
439 return "\x1b[31m"
440}
441
442func (ansiImpl) yellow() string {
443 return "\x1b[33m"
444}
445
Colin Cross3dac80e2019-06-11 11:19:06 -0700446func (ansiImpl) bold() string {
447 return "\x1b[1m"
448}
449
450func (ansiImpl) regular() string {
451 return "\x1b[0m"
452}
453
454func (ansiImpl) showCursor() string {
455 return "\x1b[?25h"
456}
457
458func (ansiImpl) hideCursor() string {
459 return "\x1b[?25l"
460}
461
462func (ansiImpl) panDown(lines int) string {
463 return fmt.Sprintf("\x1b[%dS", lines)
464}
465
466func (ansiImpl) panUp(lines int) string {
467 return fmt.Sprintf("\x1b[%dT", lines)
468}