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