smart_status.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. // 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. package terminal
  15. import (
  16. "fmt"
  17. "io"
  18. "os"
  19. "os/signal"
  20. "strconv"
  21. "strings"
  22. "sync"
  23. "syscall"
  24. "time"
  25. "android/soong/ui/status"
  26. )
  27. const tableHeightEnVar = "SOONG_UI_TABLE_HEIGHT"
  28. type actionTableEntry struct {
  29. action *status.Action
  30. startTime time.Time
  31. }
  32. type smartStatusOutput struct {
  33. writer io.Writer
  34. formatter formatter
  35. lock sync.Mutex
  36. haveBlankLine bool
  37. tableMode bool
  38. tableHeight int
  39. requestedTableHeight int
  40. termWidth, termHeight int
  41. runningActions []actionTableEntry
  42. ticker *time.Ticker
  43. done chan bool
  44. sigwinch chan os.Signal
  45. sigwinchHandled chan bool
  46. // Once there is a failure, we stop printing command output so the error
  47. // is easier to find
  48. haveFailures bool
  49. // If we are dropping errors, then at the end, we report a message to go
  50. // look in the verbose log if you want that command output.
  51. postFailureActionCount int
  52. }
  53. // NewSmartStatusOutput returns a StatusOutput that represents the
  54. // current build status similarly to Ninja's built-in terminal
  55. // output.
  56. func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
  57. s := &smartStatusOutput{
  58. writer: w,
  59. formatter: formatter,
  60. haveBlankLine: true,
  61. tableMode: true,
  62. done: make(chan bool),
  63. sigwinch: make(chan os.Signal),
  64. }
  65. if env, ok := os.LookupEnv(tableHeightEnVar); ok {
  66. h, _ := strconv.Atoi(env)
  67. s.tableMode = h > 0
  68. s.requestedTableHeight = h
  69. }
  70. if w, h, ok := termSize(s.writer); ok {
  71. s.termWidth, s.termHeight = w, h
  72. s.computeTableHeight()
  73. } else {
  74. s.tableMode = false
  75. }
  76. if s.tableMode {
  77. // Add empty lines at the bottom of the screen to scroll back the existing history
  78. // and make room for the action table.
  79. // TODO: read the cursor position to see if the empty lines are necessary?
  80. for i := 0; i < s.tableHeight; i++ {
  81. fmt.Fprintln(w)
  82. }
  83. // Hide the cursor to prevent seeing it bouncing around
  84. fmt.Fprintf(s.writer, ansi.hideCursor())
  85. // Configure the empty action table
  86. s.actionTable()
  87. // Start a tick to update the action table periodically
  88. s.startActionTableTick()
  89. }
  90. s.startSigwinch()
  91. return s
  92. }
  93. func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
  94. if level < status.StatusLvl {
  95. return
  96. }
  97. str := s.formatter.message(level, message)
  98. s.lock.Lock()
  99. defer s.lock.Unlock()
  100. if level > status.StatusLvl {
  101. s.print(str)
  102. } else {
  103. s.statusLine(str)
  104. }
  105. }
  106. func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
  107. startTime := time.Now()
  108. str := action.Description
  109. if str == "" {
  110. str = action.Command
  111. }
  112. progress := s.formatter.progress(counts)
  113. s.lock.Lock()
  114. defer s.lock.Unlock()
  115. s.runningActions = append(s.runningActions, actionTableEntry{
  116. action: action,
  117. startTime: startTime,
  118. })
  119. s.statusLine(progress + str)
  120. }
  121. func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
  122. str := result.Description
  123. if str == "" {
  124. str = result.Command
  125. }
  126. progress := s.formatter.progress(counts) + str
  127. output := s.formatter.result(result)
  128. s.lock.Lock()
  129. defer s.lock.Unlock()
  130. for i, runningAction := range s.runningActions {
  131. if runningAction.action == result.Action {
  132. s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
  133. break
  134. }
  135. }
  136. s.statusLine(progress)
  137. // Stop printing when there are failures, but don't skip actions that also have their own errors.
  138. if output != "" {
  139. if !s.haveFailures || result.Error != nil {
  140. s.requestLine()
  141. s.print(output)
  142. } else {
  143. s.postFailureActionCount++
  144. }
  145. }
  146. if result.Error != nil {
  147. s.haveFailures = true
  148. }
  149. }
  150. func (s *smartStatusOutput) Flush() {
  151. if s.tableMode {
  152. // Stop the action table tick outside of the lock to avoid lock ordering issues between s.done and
  153. // s.lock, the goroutine in startActionTableTick can get blocked on the lock and be unable to read
  154. // from the channel.
  155. s.stopActionTableTick()
  156. }
  157. s.lock.Lock()
  158. defer s.lock.Unlock()
  159. s.stopSigwinch()
  160. if s.postFailureActionCount > 0 {
  161. s.requestLine()
  162. if s.postFailureActionCount == 1 {
  163. s.print(fmt.Sprintf("There was 1 action that completed after the action that failed. See verbose.log.gz for its output."))
  164. } else {
  165. s.print(fmt.Sprintf("There were %d actions that completed after the action that failed. See verbose.log.gz for their output.", s.postFailureActionCount))
  166. }
  167. }
  168. s.requestLine()
  169. s.runningActions = nil
  170. if s.tableMode {
  171. // Update the table after clearing runningActions to clear it
  172. s.actionTable()
  173. // Reset the scrolling region to the whole terminal
  174. fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
  175. _, height, _ := termSize(s.writer)
  176. // Move the cursor to the top of the now-blank, previously non-scrolling region
  177. fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 1))
  178. // Turn the cursor back on
  179. fmt.Fprintf(s.writer, ansi.showCursor())
  180. }
  181. }
  182. func (s *smartStatusOutput) Write(p []byte) (int, error) {
  183. s.lock.Lock()
  184. defer s.lock.Unlock()
  185. s.print(string(p))
  186. return len(p), nil
  187. }
  188. func (s *smartStatusOutput) requestLine() {
  189. if !s.haveBlankLine {
  190. fmt.Fprintln(s.writer)
  191. s.haveBlankLine = true
  192. }
  193. }
  194. func (s *smartStatusOutput) print(str string) {
  195. if !s.haveBlankLine {
  196. fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
  197. s.haveBlankLine = true
  198. }
  199. fmt.Fprint(s.writer, str)
  200. if len(str) == 0 || str[len(str)-1] != '\n' {
  201. fmt.Fprint(s.writer, "\n")
  202. }
  203. }
  204. func (s *smartStatusOutput) statusLine(str string) {
  205. idx := strings.IndexRune(str, '\n')
  206. if idx != -1 {
  207. str = str[0:idx]
  208. }
  209. // Limit line width to the terminal width, otherwise we'll wrap onto
  210. // another line and we won't delete the previous line.
  211. str = elide(str, s.termWidth)
  212. // Move to the beginning on the line, turn on bold, print the output,
  213. // turn off bold, then clear the rest of the line.
  214. start := "\r" + ansi.bold()
  215. end := ansi.regular() + ansi.clearToEndOfLine()
  216. fmt.Fprint(s.writer, start, str, end)
  217. s.haveBlankLine = false
  218. }
  219. func elide(str string, width int) string {
  220. if width > 0 && len(str) > width {
  221. // TODO: Just do a max. Ninja elides the middle, but that's
  222. // more complicated and these lines aren't that important.
  223. str = str[:width]
  224. }
  225. return str
  226. }
  227. func (s *smartStatusOutput) startActionTableTick() {
  228. s.ticker = time.NewTicker(time.Second)
  229. go func() {
  230. for {
  231. select {
  232. case <-s.ticker.C:
  233. s.lock.Lock()
  234. s.actionTable()
  235. s.lock.Unlock()
  236. case <-s.done:
  237. return
  238. }
  239. }
  240. }()
  241. }
  242. func (s *smartStatusOutput) stopActionTableTick() {
  243. s.ticker.Stop()
  244. s.done <- true
  245. }
  246. func (s *smartStatusOutput) startSigwinch() {
  247. signal.Notify(s.sigwinch, syscall.SIGWINCH)
  248. go func() {
  249. for _ = range s.sigwinch {
  250. s.lock.Lock()
  251. s.updateTermSize()
  252. if s.tableMode {
  253. s.actionTable()
  254. }
  255. s.lock.Unlock()
  256. if s.sigwinchHandled != nil {
  257. s.sigwinchHandled <- true
  258. }
  259. }
  260. }()
  261. }
  262. func (s *smartStatusOutput) stopSigwinch() {
  263. signal.Stop(s.sigwinch)
  264. close(s.sigwinch)
  265. }
  266. // computeTableHeight recomputes s.tableHeight based on s.termHeight and s.requestedTableHeight.
  267. func (s *smartStatusOutput) computeTableHeight() {
  268. tableHeight := s.requestedTableHeight
  269. if tableHeight == 0 {
  270. tableHeight = s.termHeight / 4
  271. if tableHeight < 1 {
  272. tableHeight = 1
  273. } else if tableHeight > 10 {
  274. tableHeight = 10
  275. }
  276. }
  277. if tableHeight > s.termHeight-1 {
  278. tableHeight = s.termHeight - 1
  279. }
  280. s.tableHeight = tableHeight
  281. }
  282. // updateTermSize recomputes the table height after a SIGWINCH and pans any existing text if
  283. // necessary.
  284. func (s *smartStatusOutput) updateTermSize() {
  285. if w, h, ok := termSize(s.writer); ok {
  286. oldScrollingHeight := s.termHeight - s.tableHeight
  287. s.termWidth, s.termHeight = w, h
  288. if s.tableMode {
  289. s.computeTableHeight()
  290. scrollingHeight := s.termHeight - s.tableHeight
  291. // If the scrolling region has changed, attempt to pan the existing text so that it is
  292. // not overwritten by the table.
  293. if scrollingHeight < oldScrollingHeight {
  294. pan := oldScrollingHeight - scrollingHeight
  295. if pan > s.tableHeight {
  296. pan = s.tableHeight
  297. }
  298. fmt.Fprint(s.writer, ansi.panDown(pan))
  299. }
  300. }
  301. }
  302. }
  303. func (s *smartStatusOutput) actionTable() {
  304. scrollingHeight := s.termHeight - s.tableHeight
  305. // Update the scrolling region in case the height of the terminal changed
  306. fmt.Fprint(s.writer, ansi.setScrollingMargins(1, scrollingHeight))
  307. // Write as many status lines as fit in the table
  308. for tableLine := 0; tableLine < s.tableHeight; tableLine++ {
  309. if tableLine >= s.tableHeight {
  310. break
  311. }
  312. // Move the cursor to the correct line of the non-scrolling region
  313. fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1+tableLine, 1))
  314. if tableLine < len(s.runningActions) {
  315. runningAction := s.runningActions[tableLine]
  316. seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
  317. desc := runningAction.action.Description
  318. if desc == "" {
  319. desc = runningAction.action.Command
  320. }
  321. color := ""
  322. if seconds >= 60 {
  323. color = ansi.red() + ansi.bold()
  324. } else if seconds >= 30 {
  325. color = ansi.yellow() + ansi.bold()
  326. }
  327. durationStr := fmt.Sprintf(" %2d:%02d ", seconds/60, seconds%60)
  328. desc = elide(desc, s.termWidth-len(durationStr))
  329. durationStr = color + durationStr + ansi.regular()
  330. fmt.Fprint(s.writer, durationStr, desc)
  331. }
  332. fmt.Fprint(s.writer, ansi.clearToEndOfLine())
  333. }
  334. // Move the cursor back to the last line of the scrolling region
  335. fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 1))
  336. }
  337. var ansi = ansiImpl{}
  338. type ansiImpl struct{}
  339. func (ansiImpl) clearToEndOfLine() string {
  340. return "\x1b[K"
  341. }
  342. func (ansiImpl) setCursor(row, column int) string {
  343. // Direct cursor address
  344. return fmt.Sprintf("\x1b[%d;%dH", row, column)
  345. }
  346. func (ansiImpl) setScrollingMargins(top, bottom int) string {
  347. // Set Top and Bottom Margins DECSTBM
  348. return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
  349. }
  350. func (ansiImpl) resetScrollingMargins() string {
  351. // Set Top and Bottom Margins DECSTBM
  352. return fmt.Sprintf("\x1b[r")
  353. }
  354. func (ansiImpl) red() string {
  355. return "\x1b[31m"
  356. }
  357. func (ansiImpl) yellow() string {
  358. return "\x1b[33m"
  359. }
  360. func (ansiImpl) bold() string {
  361. return "\x1b[1m"
  362. }
  363. func (ansiImpl) regular() string {
  364. return "\x1b[0m"
  365. }
  366. func (ansiImpl) showCursor() string {
  367. return "\x1b[?25h"
  368. }
  369. func (ansiImpl) hideCursor() string {
  370. return "\x1b[?25l"
  371. }
  372. func (ansiImpl) panDown(lines int) string {
  373. return fmt.Sprintf("\x1b[%dS", lines)
  374. }
  375. func (ansiImpl) panUp(lines int) string {
  376. return fmt.Sprintf("\x1b[%dT", lines)
  377. }