ncurses.py 15 KB

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