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 params.updateFromServer(server) 231 cmdline = params.parseActions() 232 if not cmdline: 233 print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") 234 return 1 235 if 'msg' in cmdline and cmdline['msg']: 236 logger.error(cmdline['msg']) 237 return 1 238 cmdline = cmdline['action'] 239 ret, error = server.runCommand(cmdline) 240 if error: 241 print("Error running command '%s': %s" % (cmdline, error)) 242 return 243 elif not ret: 244 print("Couldn't get default commandlind! %s" % ret) 245 return 246 except xmlrpc.client.Fault as x: 247 print("XMLRPC Fault getting commandline:\n %s" % x) 248 return 249 250 exitflag = False 251 while not exitflag: 252 try: 253 event = eventHandler.waitEvent(0.25) 254 if not event: 255 continue 256 257 helper.eventHandler(event) 258 if isinstance(event, bb.build.TaskBase): 259 mw.appendText("NOTE: %s\n" % event._message) 260 if isinstance(event, logging.LogRecord): 261 mw.appendText(logging.getLevelName(event.levelno) + ': ' + event.getMessage() + '\n') 262 263 if isinstance(event, bb.event.CacheLoadStarted): 264 self.parse_total = event.total 265 if isinstance(event, bb.event.CacheLoadProgress): 266 x = event.current 267 y = self.parse_total 268 mw.setStatus("Loading Cache: %s [%2d %%]" % ( next(parsespin), x*100/y ) ) 269 if isinstance(event, bb.event.CacheLoadCompleted): 270 mw.setStatus("Idle") 271 mw.appendText("Loaded %d entries from dependency cache.\n" 272 % ( event.num_entries)) 273 274 if isinstance(event, bb.event.ParseStarted): 275 self.parse_total = event.total 276 if isinstance(event, bb.event.ParseProgress): 277 x = event.current 278 y = self.parse_total 279 mw.setStatus("Parsing Recipes: %s [%2d %%]" % ( next(parsespin), x*100/y ) ) 280 if isinstance(event, bb.event.ParseCompleted): 281 mw.setStatus("Idle") 282 mw.appendText("Parsing finished. %d cached, %d parsed, %d skipped, %d masked.\n" 283 % ( event.cached, event.parsed, event.skipped, event.masked )) 284 285# if isinstance(event, bb.build.TaskFailed): 286# if event.logfile: 287# if data.getVar("BBINCLUDELOGS", d): 288# bb.error("log data follows (%s)" % logfile) 289# number_of_lines = data.getVar("BBINCLUDELOGS_LINES", d) 290# if number_of_lines: 291# subprocess.check_call('tail -n%s %s' % (number_of_lines, logfile), shell=True) 292# else: 293# f = open(logfile, "r") 294# while True: 295# l = f.readline() 296# if l == '': 297# break 298# l = l.rstrip() 299# print '| %s' % l 300# f.close() 301# else: 302# bb.error("see log in %s" % logfile) 303 304 if isinstance(event, bb.command.CommandCompleted): 305 # stop so the user can see the result of the build, but 306 # also allow them to now exit with a single ^C 307 shutdown = 2 308 if isinstance(event, bb.command.CommandFailed): 309 mw.appendText(str(event)) 310 time.sleep(2) 311 exitflag = True 312 if isinstance(event, bb.command.CommandExit): 313 exitflag = True 314 if isinstance(event, bb.cooker.CookerExit): 315 exitflag = True 316 317 if isinstance(event, bb.event.LogExecTTY): 318 mw.appendText('WARN: ' + event.msg + '\n') 319 if helper.needUpdate: 320 activetasks, failedtasks = helper.getTasks() 321 taw.erase() 322 taw.setText(0, 0, "") 323 if activetasks: 324 taw.appendText("Active Tasks:\n") 325 for task in activetasks.values(): 326 taw.appendText(task["title"] + '\n') 327 if failedtasks: 328 taw.appendText("Failed Tasks:\n") 329 for task in failedtasks: 330 taw.appendText(task["title"] + '\n') 331 332 curses.doupdate() 333 except EnvironmentError as ioerror: 334 # ignore interrupted io 335 if ioerror.args[0] == 4: 336 pass 337 338 except KeyboardInterrupt: 339 if shutdown == 2: 340 mw.appendText("Third Keyboard Interrupt, exit.\n") 341 exitflag = True 342 if shutdown == 1: 343 mw.appendText("Second Keyboard Interrupt, stopping...\n") 344 _, error = server.runCommand(["stateForceShutdown"]) 345 if error: 346 print("Unable to cleanly stop: %s" % error) 347 if shutdown == 0: 348 mw.appendText("Keyboard Interrupt, closing down...\n") 349 _, error = server.runCommand(["stateShutdown"]) 350 if error: 351 print("Unable to cleanly shutdown: %s" % error) 352 shutdown = shutdown + 1 353 pass 354 355def main(server, eventHandler, params): 356 if not os.isatty(sys.stdout.fileno()): 357 print("FATAL: Unable to run 'ncurses' UI without a TTY.") 358 return 359 ui = NCursesUI() 360 try: 361 curses.wrapper(ui.main, server, eventHandler, params) 362 except: 363 import traceback 364 traceback.print_exc() 365