blob: 4575ccfdd00af93134f1d1c83b6e5c02e4dee711 [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 Cha3622b342023-11-27 11:00:52 +090034 action *status.Action
35 startTime time.Time
36 estimatedEndTime time.Time
Colin Cross3dac80e2019-06-11 11:19:06 -070037}
38
Colin Crossce525352019-06-08 18:58:00 -070039type smartStatusOutput struct {
Colin Cross097ed2a2019-06-08 21:48:58 -070040 writer io.Writer
Colin Crossce525352019-06-08 18:58:00 -070041 formatter formatter
42
43 lock sync.Mutex
44
45 haveBlankLine bool
Colin Cross4355ee62019-06-11 23:01:36 -070046
Colin Cross3dac80e2019-06-11 11:19:06 -070047 tableMode bool
48 tableHeight int
49 requestedTableHeight int
50 termWidth, termHeight int
51
52 runningActions []actionTableEntry
53 ticker *time.Ticker
54 done chan bool
Colin Cross4355ee62019-06-11 23:01:36 -070055 sigwinch chan os.Signal
56 sigwinchHandled chan bool
Joe Onoratoeadb0fb2023-06-24 15:03:28 -070057
58 // Once there is a failure, we stop printing command output so the error
59 // is easier to find
60 haveFailures bool
61 // If we are dropping errors, then at the end, we report a message to go
62 // look in the verbose log if you want that command output.
63 postFailureActionCount int
Colin Crossce525352019-06-08 18:58:00 -070064}
65
66// NewSmartStatusOutput returns a StatusOutput that represents the
67// current build status similarly to Ninja's built-in terminal
68// output.
Colin Cross097ed2a2019-06-08 21:48:58 -070069func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
Colin Cross4355ee62019-06-11 23:01:36 -070070 s := &smartStatusOutput{
Colin Crossce525352019-06-08 18:58:00 -070071 writer: w,
72 formatter: formatter,
73
74 haveBlankLine: true,
Colin Cross4355ee62019-06-11 23:01:36 -070075
Dan Willemsen1eee1542019-07-30 13:44:03 -070076 tableMode: true,
Colin Cross3dac80e2019-06-11 11:19:06 -070077
78 done: make(chan bool),
Colin Cross4355ee62019-06-11 23:01:36 -070079 sigwinch: make(chan os.Signal),
Colin Crossce525352019-06-08 18:58:00 -070080 }
Colin Cross4355ee62019-06-11 23:01:36 -070081
Dan Willemsen1eee1542019-07-30 13:44:03 -070082 if env, ok := os.LookupEnv(tableHeightEnVar); ok {
83 h, _ := strconv.Atoi(env)
84 s.tableMode = h > 0
85 s.requestedTableHeight = h
86 }
87
Colin Cross72a28a62021-02-10 13:12:41 -080088 if w, h, ok := termSize(s.writer); ok {
89 s.termWidth, s.termHeight = w, h
90 s.computeTableHeight()
91 } else {
92 s.tableMode = false
93 }
Colin Cross4355ee62019-06-11 23:01:36 -070094
Colin Cross3dac80e2019-06-11 11:19:06 -070095 if s.tableMode {
96 // Add empty lines at the bottom of the screen to scroll back the existing history
97 // and make room for the action table.
98 // TODO: read the cursor position to see if the empty lines are necessary?
99 for i := 0; i < s.tableHeight; i++ {
100 fmt.Fprintln(w)
101 }
102
103 // Hide the cursor to prevent seeing it bouncing around
104 fmt.Fprintf(s.writer, ansi.hideCursor())
105
106 // Configure the empty action table
107 s.actionTable()
108
109 // Start a tick to update the action table periodically
110 s.startActionTableTick()
111 }
112
Colin Cross4355ee62019-06-11 23:01:36 -0700113 s.startSigwinch()
114
115 return s
Colin Crossce525352019-06-08 18:58:00 -0700116}
117
118func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
119 if level < status.StatusLvl {
120 return
121 }
122
123 str := s.formatter.message(level, message)
124
125 s.lock.Lock()
126 defer s.lock.Unlock()
127
128 if level > status.StatusLvl {
129 s.print(str)
130 } else {
131 s.statusLine(str)
132 }
133}
134
135func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
Colin Cross3dac80e2019-06-11 11:19:06 -0700136 startTime := time.Now()
137
Colin Crossce525352019-06-08 18:58:00 -0700138 str := action.Description
139 if str == "" {
140 str = action.Command
141 }
142
143 progress := s.formatter.progress(counts)
144
145 s.lock.Lock()
146 defer s.lock.Unlock()
147
Colin Cross3dac80e2019-06-11 11:19:06 -0700148 s.runningActions = append(s.runningActions, actionTableEntry{
149 action: action,
150 startTime: startTime,
151 })
152
Colin Crossce525352019-06-08 18:58:00 -0700153 s.statusLine(progress + str)
154}
155
156func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
157 str := result.Description
158 if str == "" {
159 str = result.Command
160 }
161
162 progress := s.formatter.progress(counts) + str
163
164 output := s.formatter.result(result)
165
166 s.lock.Lock()
167 defer s.lock.Unlock()
168
Colin Cross3dac80e2019-06-11 11:19:06 -0700169 for i, runningAction := range s.runningActions {
170 if runningAction.action == result.Action {
171 s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
172 break
173 }
174 }
175
Joe Onoratoeadb0fb2023-06-24 15:03:28 -0700176 s.statusLine(progress)
177
178 // Stop printing when there are failures, but don't skip actions that also have their own errors.
Colin Crossce525352019-06-08 18:58:00 -0700179 if output != "" {
Joe Onoratoeadb0fb2023-06-24 15:03:28 -0700180 if !s.haveFailures || result.Error != nil {
181 s.requestLine()
182 s.print(output)
183 } else {
184 s.postFailureActionCount++
185 }
186 }
187
188 if result.Error != nil {
189 s.haveFailures = true
Colin Crossce525352019-06-08 18:58:00 -0700190 }
191}
192
193func (s *smartStatusOutput) Flush() {
Colin Cross8cc19912019-10-29 15:22:04 -0700194 if s.tableMode {
195 // Stop the action table tick outside of the lock to avoid lock ordering issues between s.done and
196 // s.lock, the goroutine in startActionTableTick can get blocked on the lock and be unable to read
197 // from the channel.
198 s.stopActionTableTick()
199 }
200
Colin Crossce525352019-06-08 18:58:00 -0700201 s.lock.Lock()
202 defer s.lock.Unlock()
203
Colin Cross4355ee62019-06-11 23:01:36 -0700204 s.stopSigwinch()
205
Joe Onoratoeadb0fb2023-06-24 15:03:28 -0700206 if s.postFailureActionCount > 0 {
207 s.requestLine()
208 if s.postFailureActionCount == 1 {
209 s.print(fmt.Sprintf("There was 1 action that completed after the action that failed. See verbose.log.gz for its output."))
210 } else {
211 s.print(fmt.Sprintf("There were %d actions that completed after the action that failed. See verbose.log.gz for their output.", s.postFailureActionCount))
212 }
213 }
214
Colin Crossce525352019-06-08 18:58:00 -0700215 s.requestLine()
Colin Cross3dac80e2019-06-11 11:19:06 -0700216
217 s.runningActions = nil
218
219 if s.tableMode {
Colin Cross3dac80e2019-06-11 11:19:06 -0700220 // Update the table after clearing runningActions to clear it
221 s.actionTable()
222
223 // Reset the scrolling region to the whole terminal
224 fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
225 _, height, _ := termSize(s.writer)
226 // Move the cursor to the top of the now-blank, previously non-scrolling region
Colin Crossbf8f57e2019-09-20 15:00:22 -0700227 fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700228 // Turn the cursor back on
229 fmt.Fprintf(s.writer, ansi.showCursor())
230 }
Colin Crossce525352019-06-08 18:58:00 -0700231}
232
Colin Crosse0df1a32019-06-09 19:40:08 -0700233func (s *smartStatusOutput) Write(p []byte) (int, error) {
234 s.lock.Lock()
235 defer s.lock.Unlock()
236 s.print(string(p))
237 return len(p), nil
238}
239
Colin Crossce525352019-06-08 18:58:00 -0700240func (s *smartStatusOutput) requestLine() {
241 if !s.haveBlankLine {
242 fmt.Fprintln(s.writer)
243 s.haveBlankLine = true
244 }
245}
246
247func (s *smartStatusOutput) print(str string) {
248 if !s.haveBlankLine {
Colin Cross3dac80e2019-06-11 11:19:06 -0700249 fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
Colin Crossce525352019-06-08 18:58:00 -0700250 s.haveBlankLine = true
251 }
252 fmt.Fprint(s.writer, str)
253 if len(str) == 0 || str[len(str)-1] != '\n' {
254 fmt.Fprint(s.writer, "\n")
255 }
256}
257
258func (s *smartStatusOutput) statusLine(str string) {
259 idx := strings.IndexRune(str, '\n')
260 if idx != -1 {
261 str = str[0:idx]
262 }
263
264 // Limit line width to the terminal width, otherwise we'll wrap onto
265 // another line and we won't delete the previous line.
Colin Cross5137de02019-06-20 15:22:50 -0700266 str = elide(str, s.termWidth)
Colin Crossce525352019-06-08 18:58:00 -0700267
Colin Cross00bdfd82019-06-11 11:16:23 -0700268 // Move to the beginning on the line, turn on bold, print the output,
269 // turn off bold, then clear the rest of the line.
Colin Cross3dac80e2019-06-11 11:19:06 -0700270 start := "\r" + ansi.bold()
271 end := ansi.regular() + ansi.clearToEndOfLine()
Colin Cross00bdfd82019-06-11 11:16:23 -0700272 fmt.Fprint(s.writer, start, str, end)
Colin Crossce525352019-06-08 18:58:00 -0700273 s.haveBlankLine = false
274}
Colin Cross4355ee62019-06-11 23:01:36 -0700275
Colin Cross5137de02019-06-20 15:22:50 -0700276func elide(str string, width int) string {
277 if width > 0 && len(str) > width {
Colin Cross4355ee62019-06-11 23:01:36 -0700278 // TODO: Just do a max. Ninja elides the middle, but that's
279 // more complicated and these lines aren't that important.
Colin Cross5137de02019-06-20 15:22:50 -0700280 str = str[:width]
Colin Cross4355ee62019-06-11 23:01:36 -0700281 }
282
283 return str
284}
285
Colin Cross3dac80e2019-06-11 11:19:06 -0700286func (s *smartStatusOutput) startActionTableTick() {
287 s.ticker = time.NewTicker(time.Second)
288 go func() {
289 for {
290 select {
291 case <-s.ticker.C:
292 s.lock.Lock()
293 s.actionTable()
294 s.lock.Unlock()
295 case <-s.done:
296 return
297 }
298 }
299 }()
300}
301
302func (s *smartStatusOutput) stopActionTableTick() {
303 s.ticker.Stop()
304 s.done <- true
305}
306
Colin Cross4355ee62019-06-11 23:01:36 -0700307func (s *smartStatusOutput) startSigwinch() {
308 signal.Notify(s.sigwinch, syscall.SIGWINCH)
309 go func() {
310 for _ = range s.sigwinch {
311 s.lock.Lock()
312 s.updateTermSize()
Colin Cross3dac80e2019-06-11 11:19:06 -0700313 if s.tableMode {
314 s.actionTable()
315 }
Colin Cross4355ee62019-06-11 23:01:36 -0700316 s.lock.Unlock()
317 if s.sigwinchHandled != nil {
318 s.sigwinchHandled <- true
319 }
320 }
321 }()
322}
323
324func (s *smartStatusOutput) stopSigwinch() {
325 signal.Stop(s.sigwinch)
326 close(s.sigwinch)
327}
328
Colin Cross72a28a62021-02-10 13:12:41 -0800329// computeTableHeight recomputes s.tableHeight based on s.termHeight and s.requestedTableHeight.
330func (s *smartStatusOutput) computeTableHeight() {
331 tableHeight := s.requestedTableHeight
332 if tableHeight == 0 {
333 tableHeight = s.termHeight / 4
334 if tableHeight < 1 {
335 tableHeight = 1
336 } else if tableHeight > 10 {
337 tableHeight = 10
338 }
339 }
340 if tableHeight > s.termHeight-1 {
341 tableHeight = s.termHeight - 1
342 }
343 s.tableHeight = tableHeight
344}
345
346// updateTermSize recomputes the table height after a SIGWINCH and pans any existing text if
347// necessary.
Colin Cross4355ee62019-06-11 23:01:36 -0700348func (s *smartStatusOutput) updateTermSize() {
Colin Cross3dac80e2019-06-11 11:19:06 -0700349 if w, h, ok := termSize(s.writer); ok {
Colin Cross3dac80e2019-06-11 11:19:06 -0700350 oldScrollingHeight := s.termHeight - s.tableHeight
351
352 s.termWidth, s.termHeight = w, h
353
354 if s.tableMode {
Colin Cross72a28a62021-02-10 13:12:41 -0800355 s.computeTableHeight()
Colin Cross3dac80e2019-06-11 11:19:06 -0700356
357 scrollingHeight := s.termHeight - s.tableHeight
358
Colin Cross72a28a62021-02-10 13:12:41 -0800359 // If the scrolling region has changed, attempt to pan the existing text so that it is
360 // not overwritten by the table.
361 if scrollingHeight < oldScrollingHeight {
362 pan := oldScrollingHeight - scrollingHeight
363 if pan > s.tableHeight {
364 pan = s.tableHeight
Colin Cross3dac80e2019-06-11 11:19:06 -0700365 }
Colin Cross72a28a62021-02-10 13:12:41 -0800366 fmt.Fprint(s.writer, ansi.panDown(pan))
Colin Cross3dac80e2019-06-11 11:19:06 -0700367 }
368 }
Colin Cross4355ee62019-06-11 23:01:36 -0700369 }
370}
Colin Cross3dac80e2019-06-11 11:19:06 -0700371
372func (s *smartStatusOutput) actionTable() {
373 scrollingHeight := s.termHeight - s.tableHeight
374
375 // Update the scrolling region in case the height of the terminal changed
Colin Crossf0b987e2019-09-20 15:01:51 -0700376
Colin Crossbf8f57e2019-09-20 15:00:22 -0700377 fmt.Fprint(s.writer, ansi.setScrollingMargins(1, scrollingHeight))
Colin Cross3dac80e2019-06-11 11:19:06 -0700378
379 // Write as many status lines as fit in the table
Colin Crossf0b987e2019-09-20 15:01:51 -0700380 for tableLine := 0; tableLine < s.tableHeight; tableLine++ {
Colin Cross3dac80e2019-06-11 11:19:06 -0700381 if tableLine >= s.tableHeight {
382 break
383 }
Colin Crossf0b987e2019-09-20 15:01:51 -0700384 // Move the cursor to the correct line of the non-scrolling region
385 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1+tableLine, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700386
Colin Crossf0b987e2019-09-20 15:01:51 -0700387 if tableLine < len(s.runningActions) {
388 runningAction := s.runningActions[tableLine]
Colin Cross3dac80e2019-06-11 11:19:06 -0700389
Colin Crossf0b987e2019-09-20 15:01:51 -0700390 seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
391
392 desc := runningAction.action.Description
393 if desc == "" {
394 desc = runningAction.action.Command
395 }
396
397 color := ""
398 if seconds >= 60 {
399 color = ansi.red() + ansi.bold()
400 } else if seconds >= 30 {
401 color = ansi.yellow() + ansi.bold()
402 }
403
404 durationStr := fmt.Sprintf(" %2d:%02d ", seconds/60, seconds%60)
405 desc = elide(desc, s.termWidth-len(durationStr))
406 durationStr = color + durationStr + ansi.regular()
407 fmt.Fprint(s.writer, durationStr, desc)
Colin Cross3dac80e2019-06-11 11:19:06 -0700408 }
Colin Cross3dac80e2019-06-11 11:19:06 -0700409 fmt.Fprint(s.writer, ansi.clearToEndOfLine())
Colin Cross3dac80e2019-06-11 11:19:06 -0700410 }
411
412 // Move the cursor back to the last line of the scrolling region
Colin Crossbf8f57e2019-09-20 15:00:22 -0700413 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 1))
Colin Cross3dac80e2019-06-11 11:19:06 -0700414}
415
416var ansi = ansiImpl{}
417
418type ansiImpl struct{}
419
420func (ansiImpl) clearToEndOfLine() string {
421 return "\x1b[K"
422}
423
424func (ansiImpl) setCursor(row, column int) string {
425 // Direct cursor address
426 return fmt.Sprintf("\x1b[%d;%dH", row, column)
427}
428
429func (ansiImpl) setScrollingMargins(top, bottom int) string {
430 // Set Top and Bottom Margins DECSTBM
431 return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
432}
433
434func (ansiImpl) resetScrollingMargins() string {
435 // Set Top and Bottom Margins DECSTBM
436 return fmt.Sprintf("\x1b[r")
437}
438
Colin Cross5137de02019-06-20 15:22:50 -0700439func (ansiImpl) red() string {
440 return "\x1b[31m"
441}
442
443func (ansiImpl) yellow() string {
444 return "\x1b[33m"
445}
446
Colin Cross3dac80e2019-06-11 11:19:06 -0700447func (ansiImpl) bold() string {
448 return "\x1b[1m"
449}
450
451func (ansiImpl) regular() string {
452 return "\x1b[0m"
453}
454
455func (ansiImpl) showCursor() string {
456 return "\x1b[?25h"
457}
458
459func (ansiImpl) hideCursor() string {
460 return "\x1b[?25l"
461}
462
463func (ansiImpl) panDown(lines int) string {
464 return fmt.Sprintf("\x1b[%dS", lines)
465}
466
467func (ansiImpl) panUp(lines int) string {
468 return fmt.Sprintf("\x1b[%dT", lines)
469}