1# 2# BitBake Curses UI Implementation 3# 4# Implements an ncurses frontend for the BitBake utility. 5# 6# Copyright (C) 2006 Michael 'Mickey' Lauer 7# Copyright (C) 2006-2007 Richard Purdie 8# 9# SPDX-License-Identifier: GPL-2.0-only 10# 11 12""" 13 We have the following windows: 14 15 1.) Main Window: Shows what we are ultimately building and how far we are. Includes status bar 16 2.) Thread Activity Window: Shows one status line for every concurrent bitbake thread. 17 3.) Command Line Window: Contains an interactive command line where you can interact w/ Bitbake. 18 19 Basic window layout is like that: 20 21 |---------------------------------------------------------| 22 | <Main Window> | <Thread Activity Window> | 23 | | 0: foo do_compile complete| 24 | Building Gtk+-2.6.10 | 1: bar do_patch complete | 25 | Status: 60% | ... | 26 | | ... | 27 | | ... | 28 |---------------------------------------------------------| 29 |<Command Line Window> | 30 |>>> which virtual/kernel | 31 |openzaurus-kernel | 32 |>>> _ | 33 |---------------------------------------------------------| 34 35""" 36 37 38 39import logging 40import os, sys, itertools, time 41 42try: 43 import curses 44except ImportError: 45 sys.exit("FATAL: The ncurses ui could not load the required curses python module.") 46 47import bb 48import xmlrpc.client 49from bb.ui import uihelper 50 51logger = logging.getLogger(__name__) 52 53parsespin = itertools.cycle( r'|/-\\' ) 54 55X = 0 56Y = 1 57WIDTH = 2 58HEIGHT = 3 59 60MAXSTATUSLENGTH = 32 61 62class NCursesUI: 63 """ 64 NCurses UI Class 65 """ 66 class Window: 67 """Base Window Class""" 68 def __init__( self, x, y, width, height, fg=curses.COLOR_BLACK, bg=curses.COLOR_WHITE ): 69 self.win = curses.newwin( height, width, y, x ) 70 self.dimensions = ( x, y, width, height ) 71 """ 72 if curses.has_colors(): 73 color = 1 74 curses.init_pair( color, fg, bg ) 75 self.win.bkgdset( ord(' '), curses.color_pair(color) ) 76 else: 77 self.win.bkgdset( ord(' '), curses.A_BOLD ) 78 """ 79 self.erase() 80 self.setScrolling() 81 self.win.noutrefresh() 82 83 def erase( self ): 84 self.win.erase() 85 86 def setScrolling( self, b = True ): 87 self.win.scrollok( b ) 88 self.win.idlok( b ) 89 90 def setBoxed( self ): 91 self.boxed = True 92 self.win.box() 93 self.win.noutrefresh() 94 95 def setText( self, x, y, text, *args ): 96 self.win.addstr( y, x, text, *args ) 97 self.win.noutrefresh() 98 99 def appendText( self, text, *args ): 100 self.win.addstr( text, *args ) 101 self.win.noutrefresh() 102 103 def drawHline( self, y ): 104 self.win.hline( y, 0, curses.ACS_HLINE, self.dimensions[WIDTH] ) 105 self.win.noutrefresh() 106 107 class DecoratedWindow( Window ): 108 """Base class for windows with a box and a title bar""" 109 def __init__( self, title, x, y, width, height, fg=curses.COLOR_BLACK, bg=curses.COLOR_WHITE ): 110 NCursesUI.Window.__init__( self, x+1, y+3, width-2, height-4, fg, bg ) 111 self.decoration = NCursesUI.Window( x, y, width, height, fg, bg ) 112 self.decoration.setBoxed() 113 self.decoration.win.hline( 2, 1, curses.ACS_HLINE, width-2 ) 114 self.setTitle( title ) 115 116 def setTitle( self, title ): 117 self.decoration.setText( 1, 1, title.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD ) 118 119 #-------------------------------------------------------------------------# 120# class TitleWindow( Window ): 121 #-------------------------------------------------------------------------# 122# """Title Window""" 123# def __init__( self, x, y, width, height ): 124# NCursesUI.Window.__init__( self, x, y, width, height ) 125# version = bb.__version__ 126# title = "BitBake %s" % version 127# credit = "(C) 2003-2007 Team BitBake" 128# #self.win.hline( 2, 1, curses.ACS_HLINE, width-2 ) 129# self.win.border() 130# self.setText( 1, 1, title.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD ) 131# self.setText( 1, 2, credit.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD ) 132 133 #-------------------------------------------------------------------------# 134 class ThreadActivityWindow( DecoratedWindow ): 135 #-------------------------------------------------------------------------# 136 """Thread Activity Window""" 137 def __init__( self, x, y, width, height ): 138 NCursesUI.DecoratedWindow.__init__( self, "Thread Activity", x, y, width, height ) 139 140 def setStatus( self, thread, text ): 141 line = "%02d: %s" % ( thread, text ) 142 width = self.dimensions[WIDTH] 143 if ( len(line) > width ): 144 line = line[:width-3] + "..." 145 else: 146 line = line.ljust( width ) 147 self.setText( 0, thread, line ) 148 149 #-------------------------------------------------------------------------# 150 class MainWindow( DecoratedWindow ): 151 #-------------------------------------------------------------------------# 152 """Main Window""" 153 def __init__( self, x, y, width, height ): 154 self.StatusPosition = width - MAXSTATUSLENGTH 155 NCursesUI.DecoratedWindow.__init__( self, None, x, y, width, height ) 156 curses.nl() 157 158 def setTitle( self, title ): 159 title = "BitBake %s" % bb.__version__ 160 self.decoration.setText( 2, 1, title, curses.A_BOLD ) 161 self.decoration.setText( self.StatusPosition - 8, 1, "Status:", curses.A_BOLD ) 162 163 def setStatus(self, status): 164 while len(status) < MAXSTATUSLENGTH: 165 status = status + " " 166 self.decoration.setText( self.StatusPosition, 1, status, curses.A_BOLD ) 167 168 169 #-------------------------------------------------------------------------# 170 class ShellOutputWindow( DecoratedWindow ): 171 #-------------------------------------------------------------------------# 172 """Interactive Command Line Output""" 173 def __init__( self, x, y, width, height ): 174 NCursesUI.DecoratedWindow.__init__( self, "Command Line Window", x, y, width, height ) 175 176 #-------------------------------------------------------------------------# 177 class ShellInputWindow( Window ): 178 #-------------------------------------------------------------------------# 179 """Interactive Command Line Input""" 180 def __init__( self, x, y, width, height ): 181 NCursesUI.Window.__init__( self, x, y, width, height ) 182 183# put that to the top again from curses.textpad import Textbox 184# self.textbox = Textbox( self.win ) 185# t = threading.Thread() 186# t.run = self.textbox.edit 187# t.start() 188 189 #-------------------------------------------------------------------------# 190 def main(self, stdscr, server, eventHandler, params): 191 #-------------------------------------------------------------------------# 192 height, width = stdscr.getmaxyx() 193 194 # for now split it like that: 195 # MAIN_y + THREAD_y = 2/3 screen at the top 196 # MAIN_x = 2/3 left, THREAD_y = 1/3 right 197 # CLI_y = 1/3 of screen at the bottom 198 # CLI_x = full 199 200 main_left = 0 201 main_top = 0 202 main_height = ( height // 3 * 2 ) 203 main_width = ( width // 3 ) * 2 204 clo_left = main_left 205 clo_top = main_top + main_height 206 clo_height = height - main_height - main_top - 1 207 clo_width = width 208 cli_left = main_left 209 cli_top = clo_top + clo_height 210 cli_height = 1 211 cli_width = width 212 thread_left = main_left + main_width 213 thread_top = main_top 214 thread_height = main_height 215 thread_width = width - main_width 216 217 #tw = self.TitleWindow( 0, 0, width, main_top ) 218 mw = self.MainWindow( main_left, main_top, main_width, main_height ) 219 taw = self.ThreadActivityWindow( thread_left, thread_top, thread_width, thread_height ) 220 clo = self.ShellOutputWindow( clo_left, clo_top, clo_width, clo_height ) 221 cli = self.ShellInputWindow( cli_left, cli_top, cli_width, cli_height ) 222 cli.setText( 0, 0, "BB>" ) 223 224 mw.setStatus("Idle") 225 226 helper = uihelper.BBUIHelper() 227 shutdown = 0 228 229 try: 230 if not params.observe_only: 231 params.updateToServer(server, os.environ.copy()) 232 233 params.updateFromServer(server) 234 cmdline = params.parseActions() 235 if not cmdline: 236 print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") 237 return 1 238 if 'msg' in cmdline and cmdline['msg']: 239 logger.error(cmdline['msg']) 240 return 1 241 cmdline = cmdline['action'] 242 ret, error = server.runCommand(cmdline) 243 if error: 244 print("Error running command '%s': %s" % (cmdline, error)) 245 return 246 elif not ret: 247 print("Couldn't get default commandlind! %s" % ret) 248 return 249 except xmlrpc.client.Fault as x: 250 print("XMLRPC Fault getting commandline:\n %s" % x) 251 return 252 253 exitflag = False 254 while not exitflag: 255 try: 256 event = eventHandler.waitEvent(0.25) 257 if not event: 258 continue 259 260 helper.eventHandler(event) 261 if isinstance(event, bb.build.TaskBase): 262 mw.appendText("NOTE: %s\n" % event._message) 263 if isinstance(event, logging.LogRecord): 264 mw.appendText(logging.getLevelName(event.levelno) + ': ' + event.getMessage() + '\n') 265 266 if isinstance(event, bb.event.CacheLoadStarted): 267 self.parse_total = event.total 268 if isinstance(event, bb.event.CacheLoadProgress): 269 x = event.current 270 y = self.parse_total 271 mw.setStatus("Loading Cache: %s [%2d %%]" % ( next(parsespin), x*100/y ) ) 272 if isinstance(event, bb.event.CacheLoadCompleted): 273 mw.setStatus("Idle") 274 mw.appendText("Loaded %d entries from dependency cache.\n" 275 % ( event.num_entries)) 276 277 if isinstance(event, bb.event.ParseStarted): 278 self.parse_total = event.total 279 if isinstance(event, bb.event.ParseProgress): 280 x = event.current 281 y = self.parse_total 282 mw.setStatus("Parsing Recipes: %s [%2d %%]" % ( next(parsespin), x*100/y ) ) 283 if isinstance(event, bb.event.ParseCompleted): 284 mw.setStatus("Idle") 285 mw.appendText("Parsing finished. %d cached, %d parsed, %d skipped, %d masked.\n" 286 % ( event.cached, event.parsed, event.skipped, event.masked )) 287 288# if isinstance(event, bb.build.TaskFailed): 289# if event.logfile: 290# if data.getVar("BBINCLUDELOGS", d): 291# bb.error("log data follows (%s)" % logfile) 292# number_of_lines = data.getVar("BBINCLUDELOGS_LINES", d) 293# if number_of_lines: 294# subprocess.check_call('tail -n%s %s' % (number_of_lines, logfile), shell=True) 295# else: 296# f = open(logfile, "r") 297# while True: 298# l = f.readline() 299# if l == '': 300# break 301# l = l.rstrip() 302# print '| %s' % l 303# f.close() 304# else: 305# bb.error("see log in %s" % logfile) 306 307 if isinstance(event, bb.command.CommandCompleted): 308 # stop so the user can see the result of the build, but 309 # also allow them to now exit with a single ^C 310 shutdown = 2 311 if isinstance(event, bb.command.CommandFailed): 312 mw.appendText(str(event)) 313 time.sleep(2) 314 exitflag = True 315 if isinstance(event, bb.command.CommandExit): 316 exitflag = True 317 if isinstance(event, bb.cooker.CookerExit): 318 exitflag = True 319 320 if isinstance(event, bb.event.LogExecTTY): 321 mw.appendText('WARN: ' + event.msg + '\n') 322 if helper.needUpdate: 323 activetasks, failedtasks = helper.getTasks() 324 taw.erase() 325 taw.setText(0, 0, "") 326 if activetasks: 327 taw.appendText("Active Tasks:\n") 328 for task in activetasks.values(): 329 taw.appendText(task["title"] + '\n') 330 if failedtasks: 331 taw.appendText("Failed Tasks:\n") 332 for task in failedtasks: 333 taw.appendText(task["title"] + '\n') 334 335 curses.doupdate() 336 except EnvironmentError as ioerror: 337 # ignore interrupted io 338 if ioerror.args[0] == 4: 339 pass 340 341 except KeyboardInterrupt: 342 if shutdown == 2: 343 mw.appendText("Third Keyboard Interrupt, exit.\n") 344 exitflag = True 345 if shutdown == 1: 346 mw.appendText("Second Keyboard Interrupt, stopping...\n") 347 _, error = server.runCommand(["stateForceShutdown"]) 348 if error: 349 print("Unable to cleanly stop: %s" % error) 350 if shutdown == 0: 351 mw.appendText("Keyboard Interrupt, closing down...\n") 352 _, error = server.runCommand(["stateShutdown"]) 353 if error: 354 print("Unable to cleanly shutdown: %s" % error) 355 shutdown = shutdown + 1 356 pass 357 358def main(server, eventHandler, params): 359 if not os.isatty(sys.stdout.fileno()): 360 print("FATAL: Unable to run 'ncurses' UI without a TTY.") 361 return 362 ui = NCursesUI() 363 try: 364 curses.wrapper(ui.main, server, eventHandler, params) 365 except: 366 import traceback 367 traceback.print_exc() 368