blob: 9589a77d75c383c5034f6c44f591c1868123c598 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
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
48from __future__ import division
49import logging
50import os, sys, itertools, time, subprocess
51
52try:
53 import curses
54except ImportError:
55 sys.exit("FATAL: The ncurses ui could not load the required curses python module.")
56
57import bb
58import xmlrpclib
59from bb import ui
60from bb.ui import uihelper
61
62parsespin = itertools.cycle( r'|/-\\' )
63
64X = 0
65Y = 1
66WIDTH = 2
67HEIGHT = 3
68
69MAXSTATUSLENGTH = 32
70
71class 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
364def 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()