2019-06-09 03:58:00 +02:00
// Copyright 2019 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package terminal
import (
"fmt"
2019-06-09 06:48:58 +02:00
"io"
2019-06-12 08:01:36 +02:00
"os"
"os/signal"
2019-06-11 20:19:06 +02:00
"strconv"
2019-06-09 03:58:00 +02:00
"strings"
"sync"
2019-06-12 08:01:36 +02:00
"syscall"
2019-06-11 20:19:06 +02:00
"time"
2019-06-09 03:58:00 +02:00
"android/soong/ui/status"
)
2019-06-11 20:19:06 +02:00
const tableHeightEnVar = "SOONG_UI_TABLE_HEIGHT"
type actionTableEntry struct {
action * status . Action
startTime time . Time
}
2019-06-09 03:58:00 +02:00
type smartStatusOutput struct {
2019-06-09 06:48:58 +02:00
writer io . Writer
2019-06-09 03:58:00 +02:00
formatter formatter
lock sync . Mutex
haveBlankLine bool
2019-06-12 08:01:36 +02:00
2019-06-11 20:19:06 +02:00
tableMode bool
tableHeight int
requestedTableHeight int
termWidth , termHeight int
runningActions [ ] actionTableEntry
ticker * time . Ticker
done chan bool
2019-06-12 08:01:36 +02:00
sigwinch chan os . Signal
sigwinchHandled chan bool
2023-06-25 00:03:28 +02:00
// Once there is a failure, we stop printing command output so the error
// is easier to find
haveFailures bool
// If we are dropping errors, then at the end, we report a message to go
// look in the verbose log if you want that command output.
postFailureActionCount int
2019-06-09 03:58:00 +02:00
}
// NewSmartStatusOutput returns a StatusOutput that represents the
// current build status similarly to Ninja's built-in terminal
// output.
2019-06-09 06:48:58 +02:00
func NewSmartStatusOutput ( w io . Writer , formatter formatter ) status . StatusOutput {
2019-06-12 08:01:36 +02:00
s := & smartStatusOutput {
2019-06-09 03:58:00 +02:00
writer : w ,
formatter : formatter ,
haveBlankLine : true ,
2019-06-12 08:01:36 +02:00
2019-07-30 22:44:03 +02:00
tableMode : true ,
2019-06-11 20:19:06 +02:00
done : make ( chan bool ) ,
2019-06-12 08:01:36 +02:00
sigwinch : make ( chan os . Signal ) ,
2019-06-09 03:58:00 +02:00
}
2019-06-12 08:01:36 +02:00
2019-07-30 22:44:03 +02:00
if env , ok := os . LookupEnv ( tableHeightEnVar ) ; ok {
h , _ := strconv . Atoi ( env )
s . tableMode = h > 0
s . requestedTableHeight = h
}
2021-02-10 22:12:41 +01:00
if w , h , ok := termSize ( s . writer ) ; ok {
s . termWidth , s . termHeight = w , h
s . computeTableHeight ( )
} else {
s . tableMode = false
}
2019-06-12 08:01:36 +02:00
2019-06-11 20:19:06 +02:00
if s . tableMode {
// Add empty lines at the bottom of the screen to scroll back the existing history
// and make room for the action table.
// TODO: read the cursor position to see if the empty lines are necessary?
for i := 0 ; i < s . tableHeight ; i ++ {
fmt . Fprintln ( w )
}
// Hide the cursor to prevent seeing it bouncing around
fmt . Fprintf ( s . writer , ansi . hideCursor ( ) )
// Configure the empty action table
s . actionTable ( )
// Start a tick to update the action table periodically
s . startActionTableTick ( )
}
2019-06-12 08:01:36 +02:00
s . startSigwinch ( )
return s
2019-06-09 03:58:00 +02:00
}
func ( s * smartStatusOutput ) Message ( level status . MsgLevel , message string ) {
if level < status . StatusLvl {
return
}
str := s . formatter . message ( level , message )
s . lock . Lock ( )
defer s . lock . Unlock ( )
if level > status . StatusLvl {
s . print ( str )
} else {
s . statusLine ( str )
}
}
func ( s * smartStatusOutput ) StartAction ( action * status . Action , counts status . Counts ) {
2019-06-11 20:19:06 +02:00
startTime := time . Now ( )
2019-06-09 03:58:00 +02:00
str := action . Description
if str == "" {
str = action . Command
}
progress := s . formatter . progress ( counts )
s . lock . Lock ( )
defer s . lock . Unlock ( )
2019-06-11 20:19:06 +02:00
s . runningActions = append ( s . runningActions , actionTableEntry {
action : action ,
startTime : startTime ,
} )
2019-06-09 03:58:00 +02:00
s . statusLine ( progress + str )
}
func ( s * smartStatusOutput ) FinishAction ( result status . ActionResult , counts status . Counts ) {
str := result . Description
if str == "" {
str = result . Command
}
progress := s . formatter . progress ( counts ) + str
output := s . formatter . result ( result )
s . lock . Lock ( )
defer s . lock . Unlock ( )
2019-06-11 20:19:06 +02:00
for i , runningAction := range s . runningActions {
if runningAction . action == result . Action {
s . runningActions = append ( s . runningActions [ : i ] , s . runningActions [ i + 1 : ] ... )
break
}
}
2023-06-25 00:03:28 +02:00
s . statusLine ( progress )
// Stop printing when there are failures, but don't skip actions that also have their own errors.
2019-06-09 03:58:00 +02:00
if output != "" {
2023-06-25 00:03:28 +02:00
if ! s . haveFailures || result . Error != nil {
s . requestLine ( )
s . print ( output )
} else {
s . postFailureActionCount ++
}
}
if result . Error != nil {
s . haveFailures = true
2019-06-09 03:58:00 +02:00
}
}
func ( s * smartStatusOutput ) Flush ( ) {
2019-10-29 23:22:04 +01:00
if s . tableMode {
// Stop the action table tick outside of the lock to avoid lock ordering issues between s.done and
// s.lock, the goroutine in startActionTableTick can get blocked on the lock and be unable to read
// from the channel.
s . stopActionTableTick ( )
}
2019-06-09 03:58:00 +02:00
s . lock . Lock ( )
defer s . lock . Unlock ( )
2019-06-12 08:01:36 +02:00
s . stopSigwinch ( )
2023-06-25 00:03:28 +02:00
if s . postFailureActionCount > 0 {
s . requestLine ( )
if s . postFailureActionCount == 1 {
s . print ( fmt . Sprintf ( "There was 1 action that completed after the action that failed. See verbose.log.gz for its output." ) )
} else {
s . print ( fmt . Sprintf ( "There were %d actions that completed after the action that failed. See verbose.log.gz for their output." , s . postFailureActionCount ) )
}
}
2019-06-09 03:58:00 +02:00
s . requestLine ( )
2019-06-11 20:19:06 +02:00
s . runningActions = nil
if s . tableMode {
// Update the table after clearing runningActions to clear it
s . actionTable ( )
// Reset the scrolling region to the whole terminal
fmt . Fprintf ( s . writer , ansi . resetScrollingMargins ( ) )
_ , height , _ := termSize ( s . writer )
// Move the cursor to the top of the now-blank, previously non-scrolling region
2019-09-21 00:00:22 +02:00
fmt . Fprintf ( s . writer , ansi . setCursor ( height - s . tableHeight , 1 ) )
2019-06-11 20:19:06 +02:00
// Turn the cursor back on
fmt . Fprintf ( s . writer , ansi . showCursor ( ) )
}
2019-06-09 03:58:00 +02:00
}
2019-06-10 04:40:08 +02:00
func ( s * smartStatusOutput ) Write ( p [ ] byte ) ( int , error ) {
s . lock . Lock ( )
defer s . lock . Unlock ( )
s . print ( string ( p ) )
return len ( p ) , nil
}
2019-06-09 03:58:00 +02:00
func ( s * smartStatusOutput ) requestLine ( ) {
if ! s . haveBlankLine {
fmt . Fprintln ( s . writer )
s . haveBlankLine = true
}
}
func ( s * smartStatusOutput ) print ( str string ) {
if ! s . haveBlankLine {
2019-06-11 20:19:06 +02:00
fmt . Fprint ( s . writer , "\r" , ansi . clearToEndOfLine ( ) )
2019-06-09 03:58:00 +02:00
s . haveBlankLine = true
}
fmt . Fprint ( s . writer , str )
if len ( str ) == 0 || str [ len ( str ) - 1 ] != '\n' {
fmt . Fprint ( s . writer , "\n" )
}
}
func ( s * smartStatusOutput ) statusLine ( str string ) {
idx := strings . IndexRune ( str , '\n' )
if idx != - 1 {
str = str [ 0 : idx ]
}
// Limit line width to the terminal width, otherwise we'll wrap onto
// another line and we won't delete the previous line.
2019-06-21 00:22:50 +02:00
str = elide ( str , s . termWidth )
2019-06-09 03:58:00 +02:00
2019-06-11 20:16:23 +02:00
// Move to the beginning on the line, turn on bold, print the output,
// turn off bold, then clear the rest of the line.
2019-06-11 20:19:06 +02:00
start := "\r" + ansi . bold ( )
end := ansi . regular ( ) + ansi . clearToEndOfLine ( )
2019-06-11 20:16:23 +02:00
fmt . Fprint ( s . writer , start , str , end )
2019-06-09 03:58:00 +02:00
s . haveBlankLine = false
}
2019-06-12 08:01:36 +02:00
2019-06-21 00:22:50 +02:00
func elide ( str string , width int ) string {
if width > 0 && len ( str ) > width {
2019-06-12 08:01:36 +02:00
// TODO: Just do a max. Ninja elides the middle, but that's
// more complicated and these lines aren't that important.
2019-06-21 00:22:50 +02:00
str = str [ : width ]
2019-06-12 08:01:36 +02:00
}
return str
}
2019-06-11 20:19:06 +02:00
func ( s * smartStatusOutput ) startActionTableTick ( ) {
s . ticker = time . NewTicker ( time . Second )
go func ( ) {
for {
select {
case <- s . ticker . C :
s . lock . Lock ( )
s . actionTable ( )
s . lock . Unlock ( )
case <- s . done :
return
}
}
} ( )
}
func ( s * smartStatusOutput ) stopActionTableTick ( ) {
s . ticker . Stop ( )
s . done <- true
}
2019-06-12 08:01:36 +02:00
func ( s * smartStatusOutput ) startSigwinch ( ) {
signal . Notify ( s . sigwinch , syscall . SIGWINCH )
go func ( ) {
for _ = range s . sigwinch {
s . lock . Lock ( )
s . updateTermSize ( )
2019-06-11 20:19:06 +02:00
if s . tableMode {
s . actionTable ( )
}
2019-06-12 08:01:36 +02:00
s . lock . Unlock ( )
if s . sigwinchHandled != nil {
s . sigwinchHandled <- true
}
}
} ( )
}
func ( s * smartStatusOutput ) stopSigwinch ( ) {
signal . Stop ( s . sigwinch )
close ( s . sigwinch )
}
2021-02-10 22:12:41 +01:00
// computeTableHeight recomputes s.tableHeight based on s.termHeight and s.requestedTableHeight.
func ( s * smartStatusOutput ) computeTableHeight ( ) {
tableHeight := s . requestedTableHeight
if tableHeight == 0 {
tableHeight = s . termHeight / 4
if tableHeight < 1 {
tableHeight = 1
} else if tableHeight > 10 {
tableHeight = 10
}
}
if tableHeight > s . termHeight - 1 {
tableHeight = s . termHeight - 1
}
s . tableHeight = tableHeight
}
// updateTermSize recomputes the table height after a SIGWINCH and pans any existing text if
// necessary.
2019-06-12 08:01:36 +02:00
func ( s * smartStatusOutput ) updateTermSize ( ) {
2019-06-11 20:19:06 +02:00
if w , h , ok := termSize ( s . writer ) ; ok {
oldScrollingHeight := s . termHeight - s . tableHeight
s . termWidth , s . termHeight = w , h
if s . tableMode {
2021-02-10 22:12:41 +01:00
s . computeTableHeight ( )
2019-06-11 20:19:06 +02:00
scrollingHeight := s . termHeight - s . tableHeight
2021-02-10 22:12:41 +01:00
// If the scrolling region has changed, attempt to pan the existing text so that it is
// not overwritten by the table.
if scrollingHeight < oldScrollingHeight {
pan := oldScrollingHeight - scrollingHeight
if pan > s . tableHeight {
pan = s . tableHeight
2019-06-11 20:19:06 +02:00
}
2021-02-10 22:12:41 +01:00
fmt . Fprint ( s . writer , ansi . panDown ( pan ) )
2019-06-11 20:19:06 +02:00
}
}
}
}
func ( s * smartStatusOutput ) actionTable ( ) {
scrollingHeight := s . termHeight - s . tableHeight
// Update the scrolling region in case the height of the terminal changed
2019-09-21 00:01:51 +02:00
2019-09-21 00:00:22 +02:00
fmt . Fprint ( s . writer , ansi . setScrollingMargins ( 1 , scrollingHeight ) )
2019-06-11 20:19:06 +02:00
// Write as many status lines as fit in the table
2019-09-21 00:01:51 +02:00
for tableLine := 0 ; tableLine < s . tableHeight ; tableLine ++ {
2019-06-11 20:19:06 +02:00
if tableLine >= s . tableHeight {
break
}
2019-09-21 00:01:51 +02:00
// Move the cursor to the correct line of the non-scrolling region
fmt . Fprint ( s . writer , ansi . setCursor ( scrollingHeight + 1 + tableLine , 1 ) )
2019-06-11 20:19:06 +02:00
2019-09-21 00:01:51 +02:00
if tableLine < len ( s . runningActions ) {
runningAction := s . runningActions [ tableLine ]
2019-06-11 20:19:06 +02:00
2019-09-21 00:01:51 +02:00
seconds := int ( time . Since ( runningAction . startTime ) . Round ( time . Second ) . Seconds ( ) )
2019-06-11 20:19:06 +02:00
2019-09-21 00:01:51 +02:00
desc := runningAction . action . Description
if desc == "" {
desc = runningAction . action . Command
}
2019-06-21 00:22:50 +02:00
2019-09-21 00:01:51 +02:00
color := ""
if seconds >= 60 {
color = ansi . red ( ) + ansi . bold ( )
} else if seconds >= 30 {
color = ansi . yellow ( ) + ansi . bold ( )
}
2019-06-21 00:22:50 +02:00
2019-09-21 00:01:51 +02:00
durationStr := fmt . Sprintf ( " %2d:%02d " , seconds / 60 , seconds % 60 )
desc = elide ( desc , s . termWidth - len ( durationStr ) )
durationStr = color + durationStr + ansi . regular ( )
fmt . Fprint ( s . writer , durationStr , desc )
2019-06-11 20:19:06 +02:00
}
fmt . Fprint ( s . writer , ansi . clearToEndOfLine ( ) )
}
// Move the cursor back to the last line of the scrolling region
2019-09-21 00:00:22 +02:00
fmt . Fprint ( s . writer , ansi . setCursor ( scrollingHeight , 1 ) )
2019-06-11 20:19:06 +02:00
}
var ansi = ansiImpl { }
type ansiImpl struct { }
func ( ansiImpl ) clearToEndOfLine ( ) string {
return "\x1b[K"
}
func ( ansiImpl ) setCursor ( row , column int ) string {
// Direct cursor address
return fmt . Sprintf ( "\x1b[%d;%dH" , row , column )
}
func ( ansiImpl ) setScrollingMargins ( top , bottom int ) string {
// Set Top and Bottom Margins DECSTBM
return fmt . Sprintf ( "\x1b[%d;%dr" , top , bottom )
}
func ( ansiImpl ) resetScrollingMargins ( ) string {
// Set Top and Bottom Margins DECSTBM
return fmt . Sprintf ( "\x1b[r" )
}
2019-06-21 00:22:50 +02:00
func ( ansiImpl ) red ( ) string {
return "\x1b[31m"
}
func ( ansiImpl ) yellow ( ) string {
return "\x1b[33m"
}
2019-06-11 20:19:06 +02:00
func ( ansiImpl ) bold ( ) string {
return "\x1b[1m"
}
func ( ansiImpl ) regular ( ) string {
return "\x1b[0m"
}
func ( ansiImpl ) showCursor ( ) string {
return "\x1b[?25h"
}
func ( ansiImpl ) hideCursor ( ) string {
return "\x1b[?25l"
}
func ( ansiImpl ) panDown ( lines int ) string {
return fmt . Sprintf ( "\x1b[%dS" , lines )
}
func ( ansiImpl ) panUp ( lines int ) string {
return fmt . Sprintf ( "\x1b[%dT" , lines )
2019-06-12 08:01:36 +02:00
}