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