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