blob: 9638cdf746f41343ccb0cd12729b45b5dc47dd2b [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 Cross4355ee62019-06-11 23:01:36 -0700228 if s.termWidth > 0 {
229 str = s.elide(str)
Colin Crossce525352019-06-08 18:58:00 -0700230 }
231
Colin Cross00bdfd82019-06-11 11:16:23 -0700232 // Move to the beginning on the line, turn on bold, print the output,
233 // turn off bold, then clear the rest of the line.
Colin Cross3dac80e2019-06-11 11:19:06 -0700234 start := "\r" + ansi.bold()
235 end := ansi.regular() + ansi.clearToEndOfLine()
Colin Cross00bdfd82019-06-11 11:16:23 -0700236 fmt.Fprint(s.writer, start, str, end)
Colin Crossce525352019-06-08 18:58:00 -0700237 s.haveBlankLine = false
238}
Colin Cross4355ee62019-06-11 23:01:36 -0700239
240func (s *smartStatusOutput) elide(str string) string {
241 if len(str) > s.termWidth {
242 // TODO: Just do a max. Ninja elides the middle, but that's
243 // more complicated and these lines aren't that important.
244 str = str[:s.termWidth]
245 }
246
247 return str
248}
249
Colin Cross3dac80e2019-06-11 11:19:06 -0700250func (s *smartStatusOutput) startActionTableTick() {
251 s.ticker = time.NewTicker(time.Second)
252 go func() {
253 for {
254 select {
255 case <-s.ticker.C:
256 s.lock.Lock()
257 s.actionTable()
258 s.lock.Unlock()
259 case <-s.done:
260 return
261 }
262 }
263 }()
264}
265
266func (s *smartStatusOutput) stopActionTableTick() {
267 s.ticker.Stop()
268 s.done <- true
269}
270
Colin Cross4355ee62019-06-11 23:01:36 -0700271func (s *smartStatusOutput) startSigwinch() {
272 signal.Notify(s.sigwinch, syscall.SIGWINCH)
273 go func() {
274 for _ = range s.sigwinch {
275 s.lock.Lock()
276 s.updateTermSize()
Colin Cross3dac80e2019-06-11 11:19:06 -0700277 if s.tableMode {
278 s.actionTable()
279 }
Colin Cross4355ee62019-06-11 23:01:36 -0700280 s.lock.Unlock()
281 if s.sigwinchHandled != nil {
282 s.sigwinchHandled <- true
283 }
284 }
285 }()
286}
287
288func (s *smartStatusOutput) stopSigwinch() {
289 signal.Stop(s.sigwinch)
290 close(s.sigwinch)
291}
292
293func (s *smartStatusOutput) updateTermSize() {
Colin Cross3dac80e2019-06-11 11:19:06 -0700294 if w, h, ok := termSize(s.writer); ok {
295 firstUpdate := s.termHeight == 0 && s.termWidth == 0
296 oldScrollingHeight := s.termHeight - s.tableHeight
297
298 s.termWidth, s.termHeight = w, h
299
300 if s.tableMode {
301 tableHeight := s.requestedTableHeight
302 if tableHeight > s.termHeight-1 {
303 tableHeight = s.termHeight - 1
304 }
305 s.tableHeight = tableHeight
306
307 scrollingHeight := s.termHeight - s.tableHeight
308
309 if !firstUpdate {
310 // If the scrolling region has changed, attempt to pan the existing text so that it is
311 // not overwritten by the table.
312 if scrollingHeight < oldScrollingHeight {
313 pan := oldScrollingHeight - scrollingHeight
314 if pan > s.tableHeight {
315 pan = s.tableHeight
316 }
317 fmt.Fprint(s.writer, ansi.panDown(pan))
318 }
319 }
320 }
Colin Cross4355ee62019-06-11 23:01:36 -0700321 }
322}
Colin Cross3dac80e2019-06-11 11:19:06 -0700323
324func (s *smartStatusOutput) actionTable() {
325 scrollingHeight := s.termHeight - s.tableHeight
326
327 // Update the scrolling region in case the height of the terminal changed
328 fmt.Fprint(s.writer, ansi.setScrollingMargins(0, scrollingHeight))
329 // Move the cursor to the first line of the non-scrolling region
330 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1, 0))
331
332 // Write as many status lines as fit in the table
333 var tableLine int
334 var runningAction actionTableEntry
335 for tableLine, runningAction = range s.runningActions {
336 if tableLine >= s.tableHeight {
337 break
338 }
339
340 seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
341
342 desc := runningAction.action.Description
343 if desc == "" {
344 desc = runningAction.action.Command
345 }
346
347 str := fmt.Sprintf(" %2d:%02d %s", seconds/60, seconds%60, desc)
348 str = s.elide(str)
349 fmt.Fprint(s.writer, str, ansi.clearToEndOfLine())
350 if tableLine < s.tableHeight-1 {
351 fmt.Fprint(s.writer, "\n")
352 }
353 }
354
355 // Clear any remaining lines in the table
356 for ; tableLine < s.tableHeight; tableLine++ {
357 fmt.Fprint(s.writer, ansi.clearToEndOfLine())
358 if tableLine < s.tableHeight-1 {
359 fmt.Fprint(s.writer, "\n")
360 }
361 }
362
363 // Move the cursor back to the last line of the scrolling region
364 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 0))
365}
366
367var ansi = ansiImpl{}
368
369type ansiImpl struct{}
370
371func (ansiImpl) clearToEndOfLine() string {
372 return "\x1b[K"
373}
374
375func (ansiImpl) setCursor(row, column int) string {
376 // Direct cursor address
377 return fmt.Sprintf("\x1b[%d;%dH", row, column)
378}
379
380func (ansiImpl) setScrollingMargins(top, bottom int) string {
381 // Set Top and Bottom Margins DECSTBM
382 return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
383}
384
385func (ansiImpl) resetScrollingMargins() string {
386 // Set Top and Bottom Margins DECSTBM
387 return fmt.Sprintf("\x1b[r")
388}
389
390func (ansiImpl) bold() string {
391 return "\x1b[1m"
392}
393
394func (ansiImpl) regular() string {
395 return "\x1b[0m"
396}
397
398func (ansiImpl) showCursor() string {
399 return "\x1b[?25h"
400}
401
402func (ansiImpl) hideCursor() string {
403 return "\x1b[?25l"
404}
405
406func (ansiImpl) panDown(lines int) string {
407 return fmt.Sprintf("\x1b[%dS", lines)
408}
409
410func (ansiImpl) panUp(lines int) string {
411 return fmt.Sprintf("\x1b[%dT", lines)
412}