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