1: #############################################################################
     2: #############################################################################
     3: ##
     4: ## installergui.py : ZyXLiveInstallerGUI main class
     5: ##
     6: ## Copyright 2009 Douglas McClendon <dmc AT viros DOT org>
     7: ##
     8: #############################################################################
     9: #############################################################################
    10: 
    11: 
    12: #############################################################################
    13: #############################################################################
    14: #
    15: # This program is free software; you can redistribute it and/or modify
    16: # it under the terms of the GNU General Public License as published by
    17: # the Free Software Foundation; version 3 of the License.
    18: #
    19: # This program is distributed in the hope that it will be useful,
    20: # but WITHOUT ANY WARRANTY; without even the implied warranty of
    21: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    22: # GNU Library General Public License for more details.
    23: #
    24: # You should have received a copy of the GNU General Public License
    25: # along with this program; if not, write to the Free Software
    26: # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
    27: #
    28: #############################################################################
    29: #############################################################################
    30: 
    31: 
    32: #############################################################################
    33: #############################################################################
    34: ##
    35: ## abbreviations (for code readability- you need to know these)
    36: ##
    37: #############################################################################
    38: #############################################################################
    39: #
    40: # rps -- Root Partition Selection
    41: # bps -- Boot Partition Selection
    42: # sps -- Swap Partition Selection
    43: 
    44: 
    45: #############################################################################
    46: #############################################################################
    47: ##
    48: ## developer tree detection and setup
    49: ##
    50: #############################################################################
    51: #############################################################################
    52: 
    53: #
    54: # setup application specific python module/package search path
    55: #
    56: 
    57: # for os.path
    58: import os
    59: # for sys.path
    60: import sys
    61: # for string.join
    62: import string
    63: 
    64: # get absolute directory of the running code file
    65: __abs_dir = os.path.dirname(os.path.abspath(__file__))
    66: 
    67: # check if this was invoked from a development tree
    68: if __abs_dir.endswith("/gui"):
    69:     _developer_tree = True
    70: else:
    71:     _developer_tree = False
    72:     
    73: if _developer_tree is True:
    74:     # add devtree root path to python search path to find rli
    75:     sys.path.insert(0, __abs_dir + "/..")
    76:     # set up resource locations
    77:     PIXMAPS_PATH = __abs_dir + "/../art"
    78:     UI_PATH = __abs_dir
    79:     DEVELOPER_DEBUG = True
    80: else:
    81:     # set up resource locations
    82:     PIXMAPS_PATH = "/usr/share/zyx-liveinstaller/pixmaps"
    83:     UI_PATH = "/usr/share/zyx-liveinstaller/ui"
    84:     DEVELOPER_DEBUG = False
    85:     
    86: 
    87: #############################################################################
    88: #############################################################################
    89: ##
    90: ## constants
    91: ##
    92: #############################################################################
    93: #############################################################################
    94: 
    95: # resource files
    96: LOGO_IMAGE = PIXMAPS_PATH + "/banner.png"
    97: ICON_IMAGE = PIXMAPS_PATH + "/icon.png"
    98: UI_SPEC = UI_PATH + "/zyx-liveinstaller.glade"
    99: 
   100: # TODO: file/fix glade-3 ui bug, in that it lets you set the visible
   101: #       page to a higher value than the numpages property.
   102: 
   103: # from glade xml ui, main_notebook page values
   104: MODES = {
   105:     "error" : 0,
   106:     "intro" : 1,
   107:     "partitioner" : 2,
   108:     "rps" : 3,
   109:     "bps" : 4,
   110:     "sps" : 5,
   111:     "review" : 6,
   112:     "installing" : 7,
   113:     "success" : 8,
   114: }
   115: 
   116: 
   117: # period in ms for idle polling function
   118: POLL_INTERVAL = 333
   119: 
   120: 
   121: #############################################################################
   122: #############################################################################
   123: ##
   124: ## libraries
   125: ##
   126: #############################################################################
   127: #############################################################################
   128: 
   129: 
   130: # for commandline arguments, exit, ... 
   131: import sys
   132: # edunote: thread is the lower level interface, which seems to lack join(),
   133: # edunote: and net wisdom is threading is effectively always the right choice.
   134: import threading
   135: # for gobject (treeviewmodel, threads_init...)
   136: import gobject
   137: # for various fs access, etc.
   138: import os
   139: # for spawning helper processes
   140: import subprocess
   141: # for simple extended widget text font properties
   142: import pango
   143: # for sleep
   144: import time
   145: 
   146: 
   147: try:
   148:     # for gui
   149:     import pygtk
   150:     pygtk.require("2.0")
   151: except:
   152:     pass
   153: try:
   154:     # for gui
   155:     import gtk
   156:     # for rad gui
   157:     import gtk.glade
   158: except:
   159:     sys.exit(1)
   160: 
   161: # for rebootless installer backend
   162: import rli
   163: 
   164: 
   165: #############################################################################
   166: #############################################################################
   167: ##
   168: ## Class Definitions
   169: ##
   170: #############################################################################
   171: #############################################################################
   172: 
   173: 
   174: #############################################################################
   175: ##
   176: ## ZyXLiveInstallerGUIError class
   177: ##
   178: #############################################################################
   179: class ZyXLiveInstallerGUIError(Exception):
   180:     """An exception class for all ZyXLiveInstallerGUI errors."""
   181: 
   182:     # installer is the gui object needed to display the error message
   183:     def __init__(self, installer, error_msg):
   184:         gtk.gdk.threads_enter()
   185:         try:
   186:             # set the error_msg to be visible on the error mode/page
   187:             installer.set_error_text(error_msg)
   188:             # set the mode to the error page
   189:             installer.set_mode("error")
   190:         finally:
   191:             gtk.gdk.threads_leave()
   192:         # finish with the base class initialization function
   193:         Exception.__init__(self, error_msg)
   194: 
   195: 
   196: #############################################################################
   197: ##
   198: ## ZyX-LiveInstaller GUI class
   199: ##
   200: #############################################################################
   201: class ZyXLiveInstallerGUI(object):
   202:     """A GUI for rebootless LiveOS installation.
   203: 
   204:     An instance of the this class will launch a GUI wizard allowing 
   205:     a user to choose installation partition/volume options, and then 
   206:     initiate a *Rebootless* LiveOS installation.  
   207: 
   208:     """
   209: 
   210:     ##
   211:     ## initialization
   212:     ##
   213:     
   214:     def __init__(self):
   215:         # TODO: there is enough here readability wise to justify breaking
   216:         #       all this up into a set of self.init_* functions
   217:         
   218:         #####################################################################
   219:         ##
   220:         ## pygtk-glade stuff
   221:         ##
   222:         #####################################################################
   223: 
   224:         ##
   225:         ## basic gtk threading init (required)
   226:         ##
   227:         # edunote: from 
   228:         # edunote: http://faq.pygtk.org/index.py?req=show&file=faq20.001.htp
   229:         # edunote: "Other threads can do window modification, processing, 
   230:         # edunote: etc. Each of those other threads needs to wrap any GTK+ 
   231:         # edunote: method calls in a gtk.gdk.threads_enter() / 
   232:         # edunote: gtk.gdk.threads_leave() pair. Preferably, this should be 
   233:         # edunote: done in try..finally -- if you miss threads_leave() due 
   234:         # edunote: to exception, your program will most likely deadlock."
   235:         gobject.threads_init()
   236: 
   237:         ##
   238:         ## pygtk-glade: load the glade designed gui 
   239:         ##
   240:         # TODO: catch exceptions
   241:         self.gladefile = UI_SPEC
   242:         self.wTree = gtk.glade.XML(self.gladefile)
   243: 
   244:         ##
   245:         ## pygtk-glade: get needed global widget handles
   246:         ##
   247: 
   248:         # TODO: eval loop, cough cough
   249:         #       (would have to change a few widget names, i.e. _treeview)
   250: 
   251:         self.main_window = self.wTree.get_widget("main_window")
   252:         self.main_notebook = self.wTree.get_widget("main_notebook")
   253: 
   254:         self.error = self.wTree.get_widget("error")
   255:         self.error_logo = self.wTree.get_widget("error_logo")
   256:         self.error_text = self.wTree.get_widget("error_text")
   257: 
   258:         self.intro = self.wTree.get_widget("intro")
   259:         self.intro_logo = self.wTree.get_widget("intro_logo")
   260:         self.intro_text = self.wTree.get_widget("intro_text")
   261: 
   262:         self.partitioner = self.wTree.get_widget("partitioner")
   263:         self.partitioner_logo = self.wTree.get_widget("partitioner_logo")
   264:         self.partitioner_text = self.wTree.get_widget("partitioner_text")
   265: 
   266:         self.rps = self.wTree.get_widget("rps")
   267:         self.rps_logo = self.wTree.get_widget("rps_logo")
   268:         self.rps_choices = self.wTree.get_widget("rps_choices_treeview")
   269:         self.rps_button_next = self.wTree.get_widget("rps_button_next")
   270: 
   271:         self.bps = self.wTree.get_widget("bps")
   272:         self.bps_logo = self.wTree.get_widget("bps_logo")
   273:         self.bps_choices = self.wTree.get_widget("bps_choices_treeview")
   274:         self.bps_button_next = self.wTree.get_widget("bps_button_next")
   275: 
   276:         self.sps = self.wTree.get_widget("sps")
   277:         self.sps_logo = self.wTree.get_widget("sps_logo")
   278:         self.sps_choices = self.wTree.get_widget("sps_choices_treeview")
   279: 
   280:         self.review = self.wTree.get_widget("review")
   281:         self.review_logo = self.wTree.get_widget("review_logo")
   282:         self.review_text = self.wTree.get_widget("review_text")
   283: 
   284:         self.installing = self.wTree.get_widget("installing")
   285:         self.installing_logo = self.wTree.get_widget("installing_logo")
   286:         self.installing_text = self.wTree.get_widget("installing_text")
   287:         self.installing_progressbar = \
   288:             self.wTree.get_widget("installing_progressbar")
   289: 
   290:         self.success = self.wTree.get_widget("success")
   291:         self.success_logo = self.wTree.get_widget("success_logo")
   292:         self.success_text = self.wTree.get_widget("success_text")
   293: 
   294:         # set the icon
   295:         self.main_window.set_icon_from_file(ICON_IMAGE)
   296: 
   297:         # set to be maximized by default
   298:         self.main_window.maximize()
   299: 
   300:         ##
   301:         ## pygtk-glade: create GUI event signal handler dict and connect it
   302:         ##
   303:         # note: this is basically just a few windowmanagerish things, 
   304:         #       followed by all the wizard buttons roughly in the order the 
   305:         #       user would see them.(Though starting with the size_allocates)
   306:         # TODO: as above, modes eval iteration for appropriate entries, or 
   307:         #       alternate rework (i.e. size_allocate)
   308:         signal_handler_mappings_hash = {
   309:             "on_error_logo_size_allocate" : \
   310:                 self.on_logo_size_allocate,
   311:             "on_intro_logo_size_allocate" : \
   312:                 self.on_logo_size_allocate,
   313:             "on_partitioner_logo_size_allocate" : \
   314:                 self.on_logo_size_allocate,
   315:             "on_rps_logo_size_allocate" : \
   316:                 self.on_logo_size_allocate,
   317:             "on_bps_logo_size_allocate" : \
   318:                 self.on_logo_size_allocate,
   319:             "on_sps_logo_size_allocate" : \
   320:                 self.on_logo_size_allocate,
   321:             "on_review_logo_size_allocate" : \
   322:                 self.on_logo_size_allocate,
   323:             "on_installing_logo_size_allocate" : \
   324:                 self.on_logo_size_allocate,
   325:             "on_success_logo_size_allocate" : \
   326:                 self.on_logo_size_allocate,
   327:             "on_main_window_destroy" : \
   328:                 self.on_main_window_destroy,
   329: #            "on_main_window_configure_event" : \
   330: #                self.on_main_window_configure_event,
   331:             "on_error_button_exit_clicked" : \
   332:                 self.on_intro_button_exit_clicked,
   333:             "on_intro_button_exit_clicked" : \
   334:                 self.on_intro_button_exit_clicked,
   335:            "on_intro_button_partitioner_clicked" : \
   336:                 self.on_intro_button_partitioner_clicked,
   337:             "on_intro_button_next_clicked" : \
   338:                 self.on_intro_button_next_clicked,
   339:             "on_partitioner_button_exit_clicked" : \
   340:                 self.on_partitioner_button_exit_clicked,
   341:             "on_partitioner_button_abort_clicked" : \
   342:                 self.on_partitioner_button_abort_clicked,
   343:             "on_rps_button_exit_clicked" : \
   344:                 self.on_rps_button_exit_clicked,
   345:             "on_rps_button_back_clicked" : \
   346:                 self.on_rps_button_back_clicked,
   347:             "on_rps_button_next_clicked" : \
   348:                 self.on_rps_button_next_clicked,
   349:             "on_bps_button_exit_clicked" : \
   350:                 self.on_bps_button_exit_clicked,
   351:             "on_bps_button_back_clicked" : \
   352:                 self.on_bps_button_back_clicked,
   353:             "on_bps_button_next_clicked" : \
   354:                 self.on_bps_button_next_clicked,
   355:             "on_sps_button_exit_clicked" : \
   356:                 self.on_sps_button_exit_clicked,
   357:             "on_sps_button_back_clicked" : \
   358:                 self.on_sps_button_back_clicked,
   359:             "on_sps_button_next_clicked" : \
   360:                 self.on_sps_button_next_clicked,
   361:             "on_review_button_exit_clicked" : \
   362:                 self.on_review_button_exit_clicked,
   363:             "on_review_button_back_clicked" : \
   364:                 self.on_review_button_back_clicked,
   365:             "on_review_button_next_clicked" : \
   366:                 self.on_review_button_next_clicked,
   367:             "on_installing_button_abort_exit_clicked" : \
   368:                 self.on_installing_button_abort_exit_clicked,
   369:             "on_installing_button_abort_back_clicked" : \
   370:                 self.on_installing_button_abort_back_clicked,
   371:             "on_success_button_viewdocs_clicked" : \
   372:                 self.on_success_button_viewdocs_clicked,
   373:             "on_success_button_done_clicked" : \
   374:                 self.on_success_button_done_clicked,
   375:             }
   376:         self.wTree.signal_autoconnect(signal_handler_mappings_hash)
   377: 
   378:         #####################################################################
   379:         ##
   380:         ## parse commandline
   381:         ##
   382:         #####################################################################
   383:         # TODO: gui-less mode available from commandline
   384:         #        if len(sys.argv) == 2:
   385:         #            self.something = sys.argv[1]
   386:         #        else:
   387:         #            self.something = DEFAULT
   388: 
   389: 
   390:         #####################################################################
   391:         ##
   392:         ## check for seperate /boot volume requirement
   393:         ##
   394:         #####################################################################
   395:         self.must_have_seperate_boot = False
   396:         if os.path.exists("/etc/fedora-release"):
   397:             fedora_release = open("/etc/fedora-release")
   398:             if fedora_release.readlines()[0].startswith("Fedora release 11 (Leonidas)") \
   399:                     is True:
   400:                 self.must_have_seperate_boot = True
   401:             # todo: close file? (this work?)
   402:             fedora_release.close()
   403:             
   404:         #####################################################################
   405:         ##
   406:         ## initialize installation progress text
   407:         ##
   408:         #####################################################################
   409:             
   410:         if os.path.exists("/etc/zyx-liveinstaller.install.txt"):
   411:             install_text_file = open("/etc/zyx-liveinstaller.install.txt")
   412:             self.set_installing_text(install_text_file.read())
   413: 
   414:         self.install_text=(self.installing_text.get_buffer()).get_text( \
   415:             (self.installing_text.get_buffer()).get_start_iter(),
   416:             (self.installing_text.get_buffer()).get_end_iter())
   417:             
   418: 
   419:         #####################################################################
   420:         ##
   421:         ## set up widget infrastructure 
   422:         ##
   423:         #####################################################################
   424: 
   425:         ##
   426:         ## load the banner image into a GdkPixBuf to be shared by 
   427:         ## the *_logo widgets
   428:         ##
   429: 
   430:         if os.path.exists("/etc/zyx-liveinstaller.banner.png"):
   431:             self.gpb = gtk.gdk.pixbuf_new_from_file(\
   432:                 "/etc/zyx-liveinstaller.banner.png")
   433:         else:
   434:             self.gpb = gtk.gdk.pixbuf_new_from_file(LOGO_IMAGE)
   435: 
   436:         self.gpb_aspect_ratio = (self.gpb.get_width() * 1.0) / \
   437:             (self.gpb.get_height() * 1.0)
   438: 
   439:         # tell the app that the logo only really needs 1x1(560x420), thus can 
   440:         # be sized that small by the user
   441:         self.main_window.set_size_request(560, 420)
   442:         self.main_notebook.set_size_request(560, 420)
   443: 
   444:         # force initial detection of new logo widget allocation w&h 
   445:         # in signal handler
   446:         self.logo_old_allocation_width = -1
   447:         self.logo_old_allocation_height = -1
   448: 
   449:         # set the logo from the pixbuf
   450:         # TODO: use eval iterator or alternate rework
   451:         # TODO: check if these are causing slow startup on my aspire one
   452:         self.error_logo.set_from_pixbuf(self.gpb)
   453:         self.intro_logo.set_from_pixbuf(self.gpb)
   454:         self.partitioner_logo.set_from_pixbuf(self.gpb)
   455:         self.rps_logo.set_from_pixbuf(self.gpb)
   456:         self.bps_logo.set_from_pixbuf(self.gpb)
   457:         self.sps_logo.set_from_pixbuf(self.gpb)
   458:         self.review_logo.set_from_pixbuf(self.gpb)
   459:         self.installing_logo.set_from_pixbuf(self.gpb)
   460:         self.success_logo.set_from_pixbuf(self.gpb)
   461: 
   462:         # set up various aspects of the multicolumn volume selection
   463:         # widget used for root/boot/swap choice
   464:         self.init_dest_vol_stuff()
   465: 
   466:         # set larger than default fonts on textview widgets
   467:         self.error_text.modify_font(pango.FontDescription("Sans 12"))
   468:         self.intro_text.modify_font(pango.FontDescription("Sans 12"))
   469:         self.review_text.modify_font(pango.FontDescription("Sans 12"))
   470:         self.installing_text.modify_font(pango.FontDescription("Sans 12"))
   471:         self.success_text.modify_font(pango.FontDescription("Sans 12"))
   472: 
   473: 
   474:         #####################################################################
   475:         ##
   476:         ## initialize global state
   477:         ##
   478:         #####################################################################
   479: 
   480:         # create an installer (the non-gui thing that does the real work)
   481:         self.installer = rli.ZyXLiveInstaller()
   482: 
   483:         # find preferred external partitioner
   484:         if os.path.exists("/usr/bin/palimpsest"):
   485:             self.ext_partitioner = "/usr/bin/palimpsest"
   486:         elif os.path.exists("/usr/bin/gparted"):
   487:             self.ext_partitioner = "/usr/bin/gparted"
   488:         else:
   489:             self.ext_partitioner = "nopartitioner"
   490: 
   491:         # global periodic timer/counter
   492:         self.numticks = 0
   493: 
   494:         # current/previous wizard page/mode state
   495:         self.current_mode = "intro"
   496:         self.last_mode = "intro"
   497: 
   498:         # used by periodic function to check for the external 
   499:         # partitioner having terminated
   500:         self.waiting_on_partitioner = False
   501: 
   502:         # the goods: the user choices for rebootless live installation options
   503:         self.rps_choice = "none"
   504:         self.bps_choice = "none"
   505:         self.sps_choice = "none"
   506: 
   507:         ## 
   508:         ## add a timeout handler to run periodic function
   509:         ##
   510:         self.timeout_handler_id = gobject.timeout_add(POLL_INTERVAL, 
   511:                                                       self.do_periodic)
   512: 
   513:         # set initial mode
   514:         self.set_mode("intro")
   515: 
   516:         # show the GUI to the user
   517:         # note: seems better to explicitly decide when to show the GUI
   518:         self.main_window.show()
   519: 
   520:         # work around problems seen on f10 derivative, perhaps related
   521:         # to zenity's inspirational problem.  I.e. window starting up
   522:         # underneath others that are not keep_above.
   523:         time.sleep(1.23)
   524:         self.main_window.present()
   525: 
   526:     # run main GTK GUI infinite event processing loop
   527:     def handle_gui_events(self):
   528:         # edunote: faq.pygtk.org suggests that this being in the main
   529:         # edunote: thread, and it needn't be wrapped with threads_enter
   530:         # edunote: and threads_leave, despite the fact that I ran across
   531:         # edunote: some random code on the net that did that.
   532:         gtk.main()
   533: 
   534: 
   535:     # change wizard page to new mode
   536:     def set_mode(self, newmode):
   537:         # set last_mode
   538:         self.last_mode = self.current_mode
   539: 
   540:         # disconnect all treestore from all treeviews
   541:         self.rps_choices.set_model()
   542:         self.bps_choices.set_model()
   543:         self.sps_choices.set_model()
   544: 
   545:         # clear all entries in the treestore
   546:         for choice in self.dest_vol_choices_iters.values():
   547:             self.dest_vol_choices_treestore.remove(choice)
   548: 
   549:         # note: this method seems not to work
   550:         #
   551:         # treeiter = self.dest_vol_choices_treestore.get_iter_first()
   552:         # while treeiter:
   553:         #     nextiter = self.dest_vol_choices_treestore.iter_next(treeiter)
   554:         #     self.dest_vol_choices_treestore.remove(treeiter)
   555:         #     treeiter = nextiter
   556:         #
   557:         # note: this seemed like a segfaultingly bad alternative as well
   558:         #
   559:         # self.dest_vol_choices_treestore.clear()
   560: 
   561:         # all the iters have been removed 
   562:         # TODO: figure out some way to double check, raising exception
   563:         self.dest_vol_choices_iters = {}
   564: 
   565:         #
   566:         # flip gui main notebook page
   567:         #
   568:         self.main_notebook.set_current_page(MODES[newmode])
   569: 
   570:         #
   571:         # run per mode custom initialization (post pageflip)
   572:         #
   573: 
   574:         # generate list of choices to present user
   575:         if (newmode == "rps"):
   576:             # connect the treestore to the rps treeview widget
   577:             self.rps_choices.set_model(self.dest_vol_choices_treestore)
   578:             # exit this func immediately, i.e. spawn worker thread to
   579:             # do this after mode change so user doesn't see hung UI
   580:             self.init_rps_vol_choices_thread = \
   581:                 threading.Thread(target=self.init_rps_vol_choices)
   582:             self.init_rps_vol_choices_thread.start()
   583:         elif (newmode == "bps"):
   584:             # connect the treestore to the bps treeview widget
   585:             self.bps_choices.set_model(self.dest_vol_choices_treestore)
   586:             # no need for threadsafe_ wrapper, because this is the main thread
   587:             self.gen_vol_choices(newmode)
   588:         elif (newmode == "sps"):
   589:             # connect the treestore to the sps treeview widget
   590:             self.sps_choices.set_model(self.dest_vol_choices_treestore)
   591:             # no need for threadsafe_ wrapper, because this is the main thread
   592:             self.gen_vol_choices(newmode)
   593: 
   594:         # set current_mode
   595:         self.current_mode = newmode
   596: 
   597:     # initialize the data structures related to user choices of storage
   598:     # volumes
   599:     def init_dest_vol_stuff(self):
   600:         """Create treestoremodel for *_choices and attach to the 
   601:         *_choices widgets.
   602: 
   603:         """
   604: 
   605:         # the iters are stored in a hash keyed by shortname, in order
   606:         # to be able to remove them later 
   607:         # (treestore.clear() seems to cause serious pygtk bug/segfault)
   608:         # TODO: add some raise exception if flag testing shows iters are 
   609:         #       not persistent
   610:         self.dest_vol_choices_iters = {}
   611: 
   612:         # initialize dest_vol_choices until scan_volumes does it better
   613:         self.dest_vol_choices = []
   614: 
   615:         # note: here we set up for 4 columns of strings
   616:         self.dest_vol_choices_treestore = gtk.TreeStore(gobject.TYPE_STRING, 
   617:                                                         gobject.TYPE_STRING,
   618:                                                         gobject.TYPE_STRING,
   619:                                                         gobject.TYPE_STRING)
   620: 
   621: 
   622:         # get selection objects, and attach event handlers
   623:         self.rps_choices_selection = \
   624:             self.rps_choices.get_selection()
   625:         self.rps_choices_selection.connect( \
   626:             'changed', self.on_rps_choices_selection_changed)
   627:         self.bps_choices_selection = \
   628:             self.bps_choices.get_selection()
   629:         self.bps_choices_selection.connect( \
   630:             'changed', self.on_bps_choices_selection_changed)
   631:         self.sps_choices_selection = \
   632:             self.sps_choices.get_selection()
   633:         self.sps_choices_selection.connect( \
   634:             'changed', self.on_sps_choices_selection_changed)
   635: 
   636: 
   637:         # XXX: there has to be a better way to clean up below seeming code 
   638:         #      repitition but for now, there should never be a scale up 
   639:         #      from N=3 selectionscreens.  Could use copy(x) (or try 
   640:         #      deepcopy(x))of the column objects perhaps.
   641: 
   642:         ###
   643:         ### main column
   644:         ###
   645: 
   646:         # for rps
   647:         main_column_renderer = gtk.CellRendererText()
   648:         main_column = \
   649:             gtk.TreeViewColumn("Storage Volume Options", 
   650:                                main_column_renderer,
   651:                                markup=0)
   652:         # only the main column is expand = True
   653:         main_column.set_expand(True)
   654:         self.rps_choices.append_column(main_column)
   655: 
   656:         # for bps
   657:         main_column_renderer = gtk.CellRendererText()
   658:         main_column = \
   659:             gtk.TreeViewColumn("Storage Volume Options", 
   660:                                main_column_renderer,
   661:                                markup=0)
   662:         main_column.set_expand(True)
   663:         self.bps_choices.append_column(main_column)
   664: 
   665:         # for sps
   666:         main_column_renderer = gtk.CellRendererText()
   667:         main_column = \
   668:             gtk.TreeViewColumn("Storage Volume Options", 
   669:                                main_column_renderer,
   670:                                markup=0)
   671:         main_column.set_expand(True)
   672:         self.sps_choices.append_column(main_column)
   673: 
   674: 
   675:         ###
   676:         ### shortname column
   677:         ###
   678: 
   679:         # shortname column: e.g. 'sda1'
   680: 
   681:         # for rps
   682:         shortname_column_renderer = gtk.CellRendererText()
   683:         shortname_column = gtk.TreeViewColumn("Short", 
   684:                                                   shortname_column_renderer, 
   685:                                                   markup=1)
   686:         shortname_column.set_expand(False)
   687:         self.rps_choices.append_column(shortname_column)
   688: 
   689:         # for bps
   690:         shortname_column_renderer = gtk.CellRendererText()
   691:         shortname_column = gtk.TreeViewColumn("Short", 
   692:                                                   shortname_column_renderer, 
   693:                                                   markup=1)
   694:         shortname_column.set_expand(False)
   695:         self.bps_choices.append_column(shortname_column)
   696: 
   697:         # for sps
   698:         shortname_column_renderer = gtk.CellRendererText()
   699:         shortname_column = gtk.TreeViewColumn("Short", 
   700:                                                   shortname_column_renderer, 
   701:                                                   markup=1)
   702:         shortname_column.set_expand(False)
   703:         self.sps_choices.append_column(shortname_column)
   704: 
   705: 
   706:         ##
   707:         ## size column
   708:         ##
   709: 
   710:         # size column: size of destination volume option
   711: 
   712:         # for rps
   713:         size_column_renderer = gtk.CellRendererText()
   714: 
   715:         # this works ...
   716:         #size_column_renderer.set_property('foreground', 'red')
   717:         #
   718:         # ... so why doesn't this?
   719:         # (see unanswered plea - 
   720:         # http://www.python-forum.org/pythonforum/viewtopic.php?f=4&t=2151)
   721:         size_column_renderer.set_property('alignment', pango.ALIGN_RIGHT)
   722:         # ... which is why I'm using this annoying workaround 
   723:         # (see db_size return formatting too)
   724:         # (i.e. the goal here is simple right justification)
   725:         size_column_renderer.set_property('family', 'Monospace')
   726: 
   727:         # set up the Size column
   728:         size_column = gtk.TreeViewColumn("Size(GB)", 
   729:                                          size_column_renderer, 
   730:                                          markup=2)
   731: 
   732:         # this right justifies only the column header, not the column data
   733:         size_column.set_alignment(1.0)
   734: 
   735:         size_column.set_expand(False)
   736: 
   737:         self.rps_choices.append_column(size_column)
   738: 
   739: 
   740:         # for bps
   741:         size_column_renderer = gtk.CellRendererText()
   742:         size_column_renderer.set_property('alignment', pango.ALIGN_RIGHT)
   743:         size_column_renderer.set_property('family', 'Monospace')
   744:         size_column = gtk.TreeViewColumn("Size(GB)", 
   745:                                          size_column_renderer, 
   746:                                          markup=2)
   747:         size_column.set_alignment(1.0)
   748:         size_column.set_expand(False)
   749:         self.bps_choices.append_column(size_column)
   750: 
   751: 
   752:         # for sps
   753:         size_column_renderer = gtk.CellRendererText()
   754:         size_column_renderer.set_property('alignment', pango.ALIGN_RIGHT)
   755:         size_column_renderer.set_property('family', 'Monospace')
   756:         size_column = gtk.TreeViewColumn("Size(GB)", 
   757:                                          size_column_renderer, 
   758:                                          markup=2)
   759:         size_column.set_alignment(1.0)
   760:         size_column.set_expand(False)
   761:         self.sps_choices.append_column(size_column)
   762: 
   763: 
   764:         ##
   765:         ## type column
   766:         ##
   767: 
   768:         # type column: currenttype of destination volume option
   769: 
   770:         # for rps
   771:         type_column_renderer = gtk.CellRendererText()
   772:         type_column = gtk.TreeViewColumn("Type", 
   773:                                              type_column_renderer, 
   774:                                              markup=3)
   775:         type_column.set_expand(False)
   776:         self.rps_choices.append_column(type_column)
   777: 
   778:         # for bps
   779:         type_column_renderer = gtk.CellRendererText()
   780:         type_column = gtk.TreeViewColumn("Type", 
   781:                                              type_column_renderer, 
   782:                                              markup=3)
   783:         type_column.set_expand(False)
   784:         self.bps_choices.append_column(type_column)
   785: 
   786:         # for sps
   787:         type_column_renderer = gtk.CellRendererText()
   788:         type_column = gtk.TreeViewColumn("Type", 
   789:                                              type_column_renderer, 
   790:                                              markup=3)
   791:         type_column.set_expand(False)
   792:         self.sps_choices.append_column(type_column)
   793: 
   794:         ##
   795:         ## exclude root choice from boot selection
   796:         ##
   797:         self.bps_choices_selection.set_select_function( \
   798:             self.bps_choices_selection_func)
   799: 
   800:         ##
   801:         ## exclude root and boot choices from swap selection
   802:         ##
   803:         self.sps_choices_selection.set_select_function( \
   804:             self.sps_choices_selection_func)
   805: 
   806:     # this function checks a potentially selectable item, and disallows 
   807:     # it if it matches the rps or bps choice
   808:     def bps_choices_selection_func(self, info):
   809:         # get the item's path
   810:         (path_to_test,) = info
   811:         # get the item's iter from the path
   812:         iter_to_test = self.dest_vol_choices_treestore.get_iter(path_to_test)
   813:         # get the value from the first column of the item
   814:         short_to_test = \
   815:             self.dest_vol_choices_treestore.get_value(iter_to_test, 0)
   816:         # remove markup
   817:         short_to_test = remove_markup(short_to_test)
   818:         # compare against lvm which /boot cannot yet live on
   819:         if (self.dest_vol_choices_longnames[short_to_test].startswith("/dev/mapper")):
   820:             return False
   821:         # compare against rps choice
   822:         if self.must_have_seperate_boot is False:
   823:             return True
   824:         else:
   825:             if (self.dest_vol_choices_longnames[short_to_test] == self.rps_choice):
   826:                 return False
   827:             else:
   828:                 return True
   829: 
   830:     # this function checks a potentially selectable item, and disallows 
   831:     # it if it matches the rps or bps choice
   832:     def sps_choices_selection_func(self, info):
   833:         # get the item's path
   834:         (path_to_test,) = info
   835:         # get the item's iter from the path
   836:         iter_to_test = self.dest_vol_choices_treestore.get_iter(path_to_test)
   837:         # get the value from the first column of the item
   838:         short_to_test = \
   839:             self.dest_vol_choices_treestore.get_value(iter_to_test, 0)
   840:         short_to_test = remove_markup(short_to_test)
   841:         # compare against rps and bps choices
   842:         if (self.dest_vol_choices_longnames[short_to_test] == \
   843:                 self.rps_choice) or \
   844:                 (self.dest_vol_choices_longnames[short_to_test] == \
   845:                      self.bps_choice):
   846:             return False
   847:         else:
   848:             return True
   849: 
   850: 
   851:     #########################################################################
   852:     #########################################################################
   853:     ##
   854:     ## GUI event signal handlers
   855:     ##
   856:     #########################################################################
   857:     #########################################################################
   858: 
   859: 
   860:     def on_logo_size_allocate(self, widget, allocation):
   861:         # This took me a couple days to figure out.  Perhaps it was
   862:         # all really just the container size_requests, and doing this
   863:         # in main_window configure event handler would work as well.
   864: 
   865:         # reviewer: "gtk.Image has no attribute visible"???  
   866:         # note: this was an attempt to only futz with the currently shown logo
   867:         #        if self.intro_logo.visible is not True:
   868:         #        if widget.visible is not True:
   869:         #            return False
   870: 
   871:         # only bother with image resizing of the allocated dimensions
   872:         # of the gtk.Image widget have changed.
   873:         if ((allocation.height != self.logo_old_allocation_height) or
   874:             (allocation.width != self.logo_old_allocation_width)):
   875: 
   876:             # maintain aspect ratio
   877:             new_aspect_ratio = allocation.width / allocation.height
   878:             if (new_aspect_ratio > self.gpb_aspect_ratio):
   879:                 new_logo_height = allocation.height
   880:                 new_logo_width = new_logo_height * self.gpb_aspect_ratio
   881:             else:
   882:                 new_logo_width = allocation.width
   883:                 new_logo_height = allocation.width / self.gpb_aspect_ratio
   884:                 
   885:             # scale the logo to the new appropriate/calculated dimensions
   886:             self.ngpb = self.gpb.scale_simple(int(new_logo_width),
   887:                                               int(new_logo_height),
   888:                                               gtk.gdk.INTERP_BILINEAR)
   889: 
   890:             # TODO: replace this with a better mechanism.  See above.
   891:             #       Until figuring out that solution, maybe indulge in
   892:             #       an eval for loop here, and elsewhere.
   893:             self.error_logo.set_from_pixbuf(self.ngpb)
   894:             self.intro_logo.set_from_pixbuf(self.ngpb)
   895:             self.partitioner_logo.set_from_pixbuf(self.ngpb)
   896:             self.rps_logo.set_from_pixbuf(self.ngpb)
   897:             self.bps_logo.set_from_pixbuf(self.ngpb)
   898:             self.sps_logo.set_from_pixbuf(self.ngpb)
   899:             self.review_logo.set_from_pixbuf(self.ngpb)
   900:             self.installing_logo.set_from_pixbuf(self.ngpb)
   901:             self.success_logo.set_from_pixbuf(self.ngpb)
   902: 
   903:             # done
   904:             self.logo_old_allocation_width = allocation.width
   905:             self.logo_old_allocation_height = allocation.height
   906: 
   907:         # TODO: document what this means (forgot and don't care already)
   908:         return False
   909: 
   910: 
   911:     # function to handle window resize events (and first size initialization)
   912:     def on_main_window_configure_event(self, widget, event):
   913:         return False
   914:         
   915:     def on_main_window_destroy(self, widget):
   916:         # nice, but not necessary
   917:         # gtk.main_quit()
   918:         sys.exit(0)
   919: 
   920:     def on_error_button_exit_clicked(self, widget):
   921:         sys.exit(0)
   922: 
   923:     def on_intro_button_exit_clicked(self, widget):
   924:         sys.exit(0)
   925: 
   926:     # TODO: maybe this becomes DeviceKit at some point?
   927:     def launch_partitioner(self):
   928:         arglist = [self.ext_partitioner]
   929: 
   930:         # supress gparted telling us about kernel unable to
   931:         # reread part table.  
   932:         # TODO: use fdisk -l vs blockdev output to detect that
   933:         #       situation and handle as best as possible
   934:         dev_null = os.open("/dev/null", os.O_WRONLY)
   935:         try:
   936:             ext_part_proc = subprocess.Popen(arglist,
   937:                                              stdout=dev_null,
   938:                                              stderr=dev_null)
   939:             ext_part_proc.wait()
   940:         finally:
   941:             os.close(dev_null)
   942: 
   943: 
   944:         # move on
   945:         # edunote: could also put the mode change in the main
   946:         # edunote: thread, polling a global flag set here.  That
   947:         # edunote: technique is used for the installation progress
   948:         # edunote: bar, but this will stay, for the bonus educational
   949:         # edunote: value.
   950:         gtk.gdk.threads_enter()
   951:         try:
   952:             # note: could just do "intro" instead of last_mode
   953:             self.set_mode(self.last_mode)
   954:         finally:
   955:             gtk.gdk.threads_leave()
   956: 
   957: 
   958:     def on_intro_button_partitioner_clicked(self, widget):
   959:         # create a thread for seperate partitioner utility
   960:         if (self.ext_partitioner == "nopartitioner"):
   961:             raise ZyXLiveInstallerGUIError( \
   962:                 self, 
   963:                 "neither palimsest nor gparted external partitioners are" +
   964:                 " available.  Advanced users are welcome to try fdisk" +
   965:                 " from the commandline and restart zyx-liveinstaller.")
   966:         else:
   967:             self.partitioner_thread = \
   968:                 threading.Thread(target=self.launch_partitioner)
   969:             self.set_mode("partitioner")
   970:             # let the thread run
   971:             self.partitioner_thread.start()
   972:             # perhaps pointless watchdog facility
   973:             self.waiting_on_partitioner = True
   974:         
   975: 
   976:     def on_intro_button_next_clicked(self, widget):
   977:         # exit if not a running LiveOS and not a developer tree
   978:         if not os.path.exists("/.liveimg-configured"):
   979: #            if not DEVELOPER_DEBUG:
   980:             if not _developer_tree:
   981:                 raise ZyXLiveInstallerGUIError( \
   982:                     self, 
   983:                     "This does not appear to be a running LiveOS.  The file" +
   984:                     " /.liveimg-configured does not exist")
   985:         # initially, user can't hit next on rps or bps until they choose a 
   986:         # volume
   987:         self.rps_button_next.set_sensitive(False)
   988:         self.bps_button_next.set_sensitive(False)
   989:         # ???: an attempt to unselect everything.  Either of the below
   990:         #      two methods leave the previously selected item highlighted.
   991:         #      As a result, I just went with an extra invalid selection at 
   992:         #      the top, doubling as a header.
   993:         # note: that the new method of removing all treestore entrys/iters 
   994:         #       on every mode change, results in nothing selected by default
   995: #        self.rps_choices_selection.unselect_all()
   996: #        self.rps_choices_selection.unselect_iter(self.top_iter)
   997:         self.set_mode("rps")
   998: 
   999:     def on_partitioner_button_exit_clicked(self, widget):
  1000:         sys.exit(0)
  1001: 
  1002:     def on_partitioner_button_abort_clicked(self, widget):
  1003:         # TODO: notimplementedyet (send kill signal to gparted)
  1004:         sys.exit(0)
  1005: 
  1006:     def on_rps_button_exit_clicked(self, widget):
  1007:         sys.exit(0)
  1008: 
  1009:     def on_rps_button_back_clicked(self, widget):
  1010:         self.set_mode("intro")
  1011: 
  1012:     def on_rps_choices_selection_changed(self, widget):
  1013:         # get the appropriate selection object
  1014:         selection = self.rps_choices.get_selection()
  1015:         
  1016:         # first easiest check is that a row is selected
  1017:         if (selection.count_selected_rows() == 0):
  1018:             self.rps_button_next.set_sensitive(False)
  1019:             return
  1020: 
  1021:         # now make sure it isn't the top row, because for rps, 
  1022:         # a choice is mandatory
  1023:         (selected_treemodel, selected_treeiter) = selection.get_selected()
  1024:         if selected_treeiter is not None:
  1025:             selected_first_column_value = \
  1026:                 selected_treemodel.get_value(selected_treeiter, 0)
  1027:             selected_second_column_value = \
  1028:                 selected_treemodel.get_value(selected_treeiter, 1)
  1029:             if (selected_first_column_value.find( \
  1030:                     "Select one of the following") != -1):
  1031:                 # disable the next button widget until a valid selection 
  1032:                 # is available
  1033:                 self.rps_button_next.set_sensitive(False)
  1034:             else:
  1035:                 # record the valid current selection and ...
  1036:                 self.rps_choice = \
  1037:                     self.dest_vol_choices_longnames[\
  1038:                     selected_first_column_value]
  1039:                 # ... allow the user to move on
  1040:                 self.rps_button_next.set_sensitive(True)
  1041:                 # and if seperate /boot is not required, make it default
  1042:                 # to the root choice (but only if rps is non lvm)
  1043:                 if self.must_have_seperate_boot is False:
  1044:                     if not self.rps_choice.startswith("/dev/mapper"):
  1045:                         self.bps_choice = self.rps_choice
  1046:                         self.bps_button_next.set_sensitive(True)
  1047: 
  1048:     def on_rps_button_next_clicked(self, widget):
  1049:         self.unmount_if_needed(self.rps_choice)
  1050:         self.set_mode("bps")
  1051: 
  1052:     def on_bps_button_exit_clicked(self, widget):
  1053:         sys.exit(0)
  1054: 
  1055:     def on_bps_button_back_clicked(self, widget):
  1056:         self.set_mode("rps")
  1057: 
  1058:     def on_bps_choices_selection_changed(self, widget):
  1059:         # see similar rps function
  1060: 
  1061:         # get the appropriate selection object
  1062:         selection = self.bps_choices.get_selection()
  1063:         
  1064:         # first easiest check is that a row is selected
  1065:         if (selection.count_selected_rows() == 0):
  1066:             self.bps_button_next.set_sensitive(False)
  1067:             return
  1068: 
  1069:         # now make sure it isn't the top row, because for bps, 
  1070:         # a choice is mandatory
  1071:         (selected_treemodel, selected_treeiter) = selection.get_selected()
  1072:         # perhaps this and similar checks for None are pointless as above
  1073:         # already exits this function if there are no selected rows.  But
  1074:         # I added these checks while fighting a segfault dealing with 
  1075:         # the apparent use of an invalid iter
  1076:         if selected_treeiter is not None:
  1077:             selected_first_column_value = \
  1078:                 selected_treemodel.get_value(selected_treeiter, 0)
  1079:             selected_second_column_value = \
  1080:                 selected_treemodel.get_value(selected_treeiter, 1)
  1081:             if (selected_first_column_value.find( \
  1082:                     "Select one of the following") != -1):
  1083:                 # disable the next button widget until a valid selection 
  1084:                 # is available
  1085:                 self.bps_button_next.set_sensitive(False)
  1086:             else:
  1087:                 # record the valid current selection and ...
  1088:                 self.bps_choice = \
  1089:                     self.dest_vol_choices_longnames[\
  1090:                         remove_markup(selected_first_column_value)]
  1091:                 # ... allow the user to move on
  1092:                 self.bps_button_next.set_sensitive(True)
  1093: 
  1094:     def on_bps_button_next_clicked(self, widget):
  1095:         self.unmount_if_needed(self.bps_choice)
  1096:         self.set_mode("sps")
  1097: 
  1098:     def on_sps_button_exit_clicked(self, widget):
  1099:         sys.exit(0)
  1100: 
  1101:     def on_sps_button_back_clicked(self, widget):
  1102:         self.set_mode("bps")
  1103: 
  1104:     def on_sps_choices_selection_changed(self, widget):
  1105:         # see similar rps/bps functions
  1106:         selection = self.sps_choices.get_selection()
  1107:         
  1108:         (selected_treemodel, selected_treeiter) = selection.get_selected()
  1109:         if selected_treeiter is not None:
  1110:             selected_first_column_value = \
  1111:                 selected_treemodel.get_value(selected_treeiter, 0)
  1112:             selected_second_column_value = \
  1113:                 selected_treemodel.get_value(selected_treeiter, 1)
  1114:             self.sps_choice = \
  1115:                 self.dest_vol_choices_longnames[selected_first_column_value]
  1116:             if (self.sps_choice == ""):
  1117:                 self.sps_choice = "none"
  1118:         
  1119:     def on_sps_button_next_clicked(self, widget):
  1120: 
  1121:         self.unmount_if_needed(self.bps_choice)
  1122: 
  1123:         #
  1124:         # prepare review and installing text widget text
  1125:         #
  1126: 
  1127:         # swap is optional, so its part in this text is as well
  1128:         if (self.sps_choice == "none"):
  1129:             opt_swap_text_first = ""
  1130:             opt_swap_text_second = ""
  1131:         else:
  1132: 
  1133:             opt_swap_text_first = \
  1134: """WARNING: about to WIPE and format swap on storage volume
  1135: %(swap_name)s
  1136: """ % { 'swap_name' : os.path.basename(self.sps_choice), }
  1137: 
  1138:             opt_swap_text_second = \
  1139: """Swap Storage Volume Choice:
  1140: %(swap_long)s
  1141: will have all data WIPED, then be used for this LiveOS installation (swap).
  1142: 
  1143: """ % { 'swap_long' : self.sps_choice, }
  1144:         
  1145: 
  1146:         # note: possibly excessive attempt to maintain 79 character code width
  1147:         self.set_review_text( \
  1148: ("""WARNING: about to WIPE and install root filesystem on storage volume 
  1149: %(root_name)s
  1150: 
  1151: WARNING: about to WIPE and install boot filesystem on storage volume
  1152: %(boot_name)s
  1153: 
  1154: %(opt_swap_text_first)s
  1155: WARNING WARNING WARNING -- This is your final warning before beginning""" + \
  1156: """ a 'destructive' installation of the running LiveOS.  All existing""" + \
  1157: """ data on the following volumes will be be lost during installation.""" + \
  1158: """  If you require an installation that preserves data on the""" + \
  1159: """ destination volumes, use the traditional Anaconda installer.
  1160: 
  1161: Root Storage Volume Choice:
  1162:  %(root_long)s 
  1163: will have all data WIPED, then be used for this LiveOS installation""" + \
  1164: """ (root-'/').
  1165: 
  1166: Boot Storage Volume Choice:
  1167:  %(boot_long)s
  1168: will have all data WIPED, then be used for this LiveOS installation""" + \
  1169: """ (boot-'/boot').
  1170: 
  1171: %(opt_swap_text_second)s
  1172: Click 'next' to proceed with installation, or 'back' to change target""" + \
  1173: """ volume selections, or 'exit' to close this installer without""" + \
  1174: """ installing.
  1175: """) % { 'root_name' : os.path.basename(self.rps_choice),
  1176:          'boot_name' : os.path.basename(self.bps_choice),
  1177:          'root_long' : self.rps_choice,
  1178:          'boot_long' : self.bps_choice,
  1179:          'opt_swap_text_first' : opt_swap_text_first,
  1180:          'opt_swap_text_second' : opt_swap_text_second,
  1181:          })
  1182: 
  1183: 
  1184:         self.set_installing_text( \
  1185:             ("Installation is in progress to the following" + \
  1186:                  " partitions/volumes:\n\n" + \
  1187:                  "root filesystem ::\n%(rchoice)s\n\n" + \
  1188:                  "boot filesystem ::\n%(bchoice)s\n\n" + \
  1189:                  "swap space      ::\n%(schoice)s\n\n" + \
  1190:                  "\n" + self.install_text + \
  1191:                  "") % { 'rchoice' : self.rps_choice,
  1192:                          'bchoice' : self.bps_choice,
  1193:                          'schoice' : self.sps_choice,
  1194:                          })
  1195: 
  1196:         self.set_mode("review")
  1197: 
  1198:     def on_review_button_exit_clicked(self, widget):
  1199:         sys.exit(0)
  1200: 
  1201:     def on_review_button_back_clicked(self, widget):
  1202:         self.set_mode("sps")
  1203: 
  1204:     def on_review_button_next_clicked(self, widget):
  1205:         # create installer thread
  1206:         self.installer_thread = threading.Thread(target=self.run_installer)
  1207:         # start installer thread
  1208:         self.installer_thread.start()
  1209:         # change GUI mode
  1210:         self.set_mode("installing")
  1211: 
  1212:     def on_installing_button_abort_exit_clicked(self, widget):
  1213:         # todo: are-you-sure mechanism (too easy to abort)
  1214:         # note: this may be a race condition, perhaps create installer 
  1215:         #       object when self/guiinstaller is being initialized (but 
  1216:         #       pretty unlikely)
  1217:         self.installer.request_installation_abort()
  1218:         # FIXME: need mech to wait on abort, not just request
  1219:         sys.exit(0)
  1220: 
  1221:     def on_installing_button_abort_back_clicked(self, widget):
  1222:         self.installer.request_installation_abort()
  1223:         # reset the GUI state for the installer, for now, just this
  1224:         self.installing_progressbar.set_fraction(0.0)
  1225:         # return to the previous installation wizard page
  1226:         self.set_mode("review")
  1227: 
  1228:     def on_success_button_viewdocs_clicked(self, widget):
  1229:         # notyetimplemented
  1230:         sys.exit(0)
  1231: 
  1232:     def on_success_button_done_clicked(self, widget):
  1233:         # done
  1234:         sys.exit(0)
  1235: 
  1236: 
  1237:     #########################################################################
  1238:     #########################################################################
  1239:     ##
  1240:     ## non event handling functions (called by event signal handlers)
  1241:     ##
  1242:     #########################################################################
  1243:     #########################################################################
  1244: 
  1245:     # set the text in the error mode/page
  1246:     def set_error_text(self, new_text):
  1247:         (self.error_text.get_buffer()).set_text(new_text)
  1248: 
  1249:     # set the text in the final review mode/page
  1250:     def set_review_text(self, new_text):
  1251:         (self.review_text.get_buffer()).set_text(new_text)
  1252: 
  1253:     # set the text in the installation mode/page
  1254:     def set_installing_text(self, new_text):
  1255:         (self.installing_text.get_buffer()).set_text(new_text)
  1256: 
  1257:     def run_installer(self):
  1258:         self.installer.root_device = self.rps_choice
  1259:         self.installer.boot_device = self.bps_choice
  1260:         self.installer.swap_device = self.sps_choice
  1261:         try:
  1262:             self.installer.do_install()
  1263:         except rli.ZyXLiveInstallerError, e:
  1264:             raise ZyXLiveInstallerGUIError( \
  1265:                 self, 
  1266:                 "*INSTALLER BACKEND ERROR*\n\nIf possible, please " +
  1267:                 "send the file\n\n/var/log/zyx-liveinstaller.log\n\nto\n\nbugs AT " +
  1268:                 "viros DOT org\n\n--\n\n" + str(e) )
  1269: 
  1270:     # pre-rps initialization of all volume choices infrastructure
  1271:     # (that which can be done prior to volume scan)
  1272:     def init_vol_choices(self):
  1273:         # ... to populate volume/partition choices widget(s)
  1274:         self.dest_vol_choices_treestore.clear()
  1275: 
  1276:         # this adds the first fake entry
  1277:         self.top_iter = self.dest_vol_choices_treestore.append(None)
  1278:         self.dest_vol_choices_treestore.set_value(self.top_iter, 
  1279:                                                   0, 
  1280:  '<span size="large">Scanning storage volumes, please wait a few ' + \
  1281:  'seconds...</span>')
  1282:         self.dest_vol_choices_treestore.set_value(self.top_iter, 
  1283:                                                   1, 
  1284:                                                   "")
  1285:         self.dest_vol_choices_treestore.set_value(self.top_iter, 
  1286:                                                   2, 
  1287:                                                   "")
  1288:         self.dest_vol_choices_treestore.set_value(self.top_iter, 
  1289:                                                   3, 
  1290:                                                   "")
  1291:     def gen_vol_choices(self, mode):
  1292:         # craft header string appropriate to mode
  1293:         if (mode == "rps"):
  1294:             header_string = \
  1295:                 '<span size="large">Select one of the following -</span>'
  1296:         elif (mode == "bps"):
  1297:             # when bootloader handles ext4, this becomes optional or gone
  1298:             header_string = \
  1299:                 '<span size="large">Select one of the following -</span>'
  1300:         elif (mode == "sps"):
  1301:             header_string = \
  1302:                 '<span size="large" foreground="#007700"><u>Optionally' + \
  1303:                 '</u></span><span size="large"> select one of the' + \
  1304:                 ' following -</span>'
  1305:         else:
  1306:             raise ZyXLiveInstallerGUIError( \
  1307:                 self, 
  1308:                 "unknown mode ... %s ..." % mode)
  1309: 
  1310:         # 
  1311:         # craft header entry/iter
  1312:         # 
  1313:         self.dest_vol_choices_treestore.set_value(self.top_iter, 
  1314:                                                   0, 
  1315:                                                   header_string)
  1316:         self.dest_vol_choices_treestore.set_value(self.top_iter, 
  1317:                                                   1, 
  1318:                                                   "")
  1319:         self.dest_vol_choices_treestore.set_value(self.top_iter, 
  1320:                                                   2, 
  1321:                                                   "")
  1322:         self.dest_vol_choices_treestore.set_value(self.top_iter, 
  1323:                                                   3, 
  1324:                                                   "")
  1325:                     
  1326:         # a hash of the longnames keyed by short will be needed to
  1327:         # unmangle the text when the user goes backward through the wizard
  1328:         self.dest_vol_choices_longnames = {}
  1329:             
  1330:         for target_vol in self.dest_vol_choices:
  1331:             # get the shortname from the longname
  1332:             target_vol_shortname = os.path.basename(target_vol)
  1333:             # create an iter for this potential choice
  1334:             self.dest_vol_choices_iters[target_vol_shortname] = \
  1335:                 self.dest_vol_choices_treestore.append(None)
  1336: 
  1337:             # populate the shortname->longname hash
  1338:             # NOTE: implicit assumption you won't see non identical 
  1339:             #       duplicates between /dev/disk/by-id and /dev/mapper
  1340:             self.dest_vol_choices_longnames[target_vol_shortname] = \
  1341:                 target_vol
  1342: 
  1343:             #
  1344:             # craft the main and type column string values,
  1345:             # 
  1346: 
  1347:             # fist, set the default values
  1348:             main_column_string = target_vol_shortname
  1349:             type_column_string = get_device_partition_type(target_vol)
  1350: 
  1351:             # first, check to see if it needs to be 'new root'
  1352:             if ((mode is "bps") or (mode is "sps")):
  1353:                 # edunote: can't use 'is' instead of == for strings
  1354:                 if (target_vol == self.rps_choice):
  1355:                     main_column_string = \
  1356:                         '<span foreground="#FF0000">%s</span>' % \
  1357:                         target_vol_shortname
  1358:                     type_column_string = \
  1359:                         '<span foreground="#FF0000">new root</span>'
  1360:                     
  1361:             # second, check to see if it needs to be 'new boot'
  1362:             if (mode == "sps"):
  1363:                 if (target_vol == self.bps_choice):
  1364:                     main_column_string = \
  1365:                         '<span foreground="#FF0000">%s</span>' % \
  1366:                         target_vol_shortname
  1367:                     type_column_string = \
  1368:                         '<span foreground="#FF0000">new boot</span>'
  1369:                     
  1370:             # set the values for each column for this choice
  1371:             self.dest_vol_choices_treestore.set_value( \
  1372:                 self.dest_vol_choices_iters[target_vol_shortname],
  1373:                 0, 
  1374:                 main_column_string)
  1375:             self.dest_vol_choices_treestore.set_value( \
  1376:                 self.dest_vol_choices_iters[target_vol_shortname],
  1377:                 1, 
  1378:                 get_device_realshortname(target_vol))
  1379:             self.dest_vol_choices_treestore.set_value( \
  1380:                 self.dest_vol_choices_iters[target_vol_shortname],
  1381:                 2, 
  1382:                 blockdev_size(target_vol))
  1383:             self.dest_vol_choices_treestore.set_value( \
  1384:                 self.dest_vol_choices_iters[target_vol_shortname],
  1385:                 3, 
  1386:                 type_column_string)
  1387: 
  1388:             # if an entry was previously selected, have it be the 
  1389:             # default selection
  1390:             if (mode == "rps"):
  1391:                 if (os.path.basename(self.rps_choice) == target_vol_shortname):
  1392:                     self.rps_choices_selection.select_iter( \
  1393:                         self.dest_vol_choices_iters[target_vol_shortname])
  1394:             elif (mode == "bps"):
  1395:                 if (os.path.basename(self.bps_choice) == target_vol_shortname):
  1396:                     self.bps_choices_selection.select_iter( \
  1397:                         self.dest_vol_choices_iters[target_vol_shortname])
  1398:             elif (mode == "sps"):
  1399:                 if (os.path.basename(self.sps_choice) == target_vol_shortname):
  1400:                     self.sps_choices_selection.select_iter( \
  1401:                         self.dest_vol_choices_iters[target_vol_shortname])
  1402:         
  1403:     def threadsafe_gen_vol_choices(self, mode):
  1404:         # reviewer: Is this wrapping necessary/appropriate?  
  1405:         #           This is 'directly' effecting the GUI
  1406:         gtk.gdk.threads_enter()
  1407:         try:
  1408:             self.gen_vol_choices(mode)
  1409:         finally:
  1410:             gtk.gdk.threads_leave()
  1411: 
  1412:     def init_rps_vol_choices(self):
  1413:         # pre volume scan initialization
  1414:         gtk.gdk.threads_enter()
  1415:         try:
  1416:             self.init_vol_choices()
  1417:         finally:
  1418:             gtk.gdk.threads_leave()
  1419: 
  1420:         # disk device scan ...
  1421:         self.scan_volumes()
  1422: 
  1423:         # post volume scan initialization
  1424:         self.threadsafe_gen_vol_choices("rps")
  1425: 
  1426: 
  1427:     # this could be moved to a utility function outside the class
  1428:     # (DeviceKit/pyparted perhaps)
  1429:     def scan_volumes(self):
  1430:         unfiltered_choices_list = []
  1431:         if os.path.exists("/dev/mapper"):
  1432:             for entry in os.listdir("/dev/mapper"):
  1433:                 unfiltered_choices_list.append("/dev/mapper/" + entry)
  1434:         if os.path.exists("/dev/disk/by-id"):
  1435:             for entry in os.listdir("/dev/disk/by-id"):
  1436:                 unfiltered_choices_list.append("/dev/disk/by-id/" + entry)
  1437: #        unfiltered_choices_list.sort()
  1438:         self.dest_vol_choices = []
  1439:         for candidate_vol in unfiltered_choices_list:
  1440:             keep = True
  1441:             # ignore f11 style LiveOS devicemapper volumes
  1442:             if (candidate_vol.startswith("/dev/disk/by-id/dm-name-live-") or
  1443:                 candidate_vol.startswith("/dev/mapper/live-") or
  1444:                 candidate_vol.startswith(\
  1445:                     "/dev/disk/by-id/dm-name-zyx-liveos-") or
  1446:                 candidate_vol.startswith("/dev/mapper/zyx-liveos-") or
  1447:                 candidate_vol.startswith("/dev/mapper/control")):
  1448:                 keep = False
  1449:             else:
  1450:                 for target_vol in self.dest_vol_choices:
  1451:                     #
  1452:                     # don't keep if already in dest_vol_choices (not unique)
  1453:                     #
  1454: 
  1455:                     # first check symlink equivalence
  1456:                     if (os.path.realpath(candidate_vol) == 
  1457:                         os.path.realpath(target_vol)):
  1458:                         keep = False
  1459:                         
  1460:                     # finally check for equivalent device nodes
  1461:                     candidate_vol_major = \
  1462:                         os.major(\
  1463:                         os.stat(os.path.realpath(candidate_vol)).st_rdev)
  1464:                     candidate_vol_minor = \
  1465:                         os.minor(\
  1466:                         os.stat(os.path.realpath(candidate_vol)).st_rdev)
  1467:                     target_vol_major = \
  1468:                         os.major(\
  1469:                         os.stat(os.path.realpath(target_vol)).st_rdev)
  1470:                     target_vol_minor = \
  1471:                         os.minor(os.stat(os.path.realpath(target_vol)).st_rdev)
  1472:                     if ((candidate_vol_major == target_vol_major) and
  1473:                         (candidate_vol_minor == target_vol_minor)):
  1474:                         keep = False
  1475: 
  1476:             # keep the entry if it is unique
  1477:             if keep:
  1478:                 self.dest_vol_choices.append(candidate_vol)
  1479: 
  1480:         self.dest_vol_choices.sort()
  1481: 
  1482: 
  1483:     # if the given device is mounted under /media and unused, unmount it,
  1484:     # otherwise, raise an exception.
  1485:     def unmount_if_needed(self, device):
  1486:         # check procselfmountinfo against maj/min, then look at /media, 
  1487:         # then try to unmount
  1488:         device_major = \
  1489:             os.major(os.stat(os.path.realpath(device)).st_rdev)
  1490:         device_minor = \
  1491:             os.minor(os.stat(os.path.realpath(device)).st_rdev)
  1492: 
  1493:         if os.path.exists("/proc/self/mountinfo"):
  1494:             mountinfo = open("/proc/self/mountinfo", "r")
  1495:             have_mountinfo = True
  1496:         elif os.path.exists("/proc/self/mounts"):
  1497:             mountinfo = open("/proc/self/mounts", "r")
  1498:             have_mountinfo = False
  1499:         else:
  1500:             raise ZyXLiveInstallerGUIError( \
  1501:                 self, 
  1502:                 "no /proc/self/mountinfo OR /proc/self/mounts")
  1503: 
  1504:         
  1505:         for mount in mountinfo.readlines():
  1506:             if have_mountinfo is True:
  1507:                 mount_major = int(mount.split()[2].split(":")[0])
  1508:                 mount_minor = int(mount.split()[2].split(":")[1])
  1509:                 mount_point = mount.split()[4]
  1510:             else:
  1511:                 if os.path.exists(mount.split()[0]) is True:
  1512:                     mount_major = os.major( \
  1513:                         os.stat(os.path.realpath(mount.split()[0])).st_rdev)   
  1514:                     mount_minor = os.minor( \
  1515:                         os.stat(os.path.realpath(mount.split()[0])).st_rdev)   
  1516:                 else:
  1517:                     mount_major = -1
  1518:                     mount_minor = -1
  1519: 
  1520:                 mount_point = mount.split()[1]
  1521:                     
  1522: 
  1523:             if ((device_major == mount_major) and
  1524:                 (device_minor == mount_minor)):
  1525: 
  1526:                 # /mnt/disc(lvm)/ is centos-5.4's version of /media for nonremovable discs
  1527:                 if mount_point.startswith("/media/") or \
  1528:                         mount_point.startswith("/mnt/disc/") or \
  1529:                         mount_point.startswith("/mnt/lvm/"):
  1530:                     # mount_point is /media/*, try to unmount if possible
  1531: 
  1532:                     dev_null = os.open("/dev/null", os.O_WRONLY)
  1533:                     try:
  1534:                         fuser_proc = subprocess.Popen(["/sbin/fuser", 
  1535:                                                         "-m",
  1536:                                                         mount_point],
  1537:                                                        stdout=subprocess.PIPE,
  1538:                                                        stderr=dev_null)
  1539:                         fuser_out = fuser_proc.communicate()[0]
  1540:                     finally:
  1541:                         os.close(dev_null)
  1542:         
  1543:                     # check the return value of fuser -m /media/point
  1544:                     if fuser_proc.returncode:
  1545:                         # nobody using the mount, try to unmount it
  1546:                         dev_null = os.open("/dev/null", os.O_WRONLY)
  1547:                         try:
  1548:                             umount_proc = \
  1549:                                 subprocess.Popen(["/bin/umount", 
  1550:                                                   mount_point], 
  1551:                                                  stdout=subprocess.PIPE,
  1552:                                                  stderr=dev_null)
  1553:                             umount_out = umount_proc.communicate()[0]
  1554:                         finally:
  1555:                             os.close(dev_null)
  1556: 
  1557:                         if umount_proc.returncode:
  1558:                             # umount failed, raise exception
  1559:                             raise ZyXLiveInstallerGUIError( \
  1560:                                 self, 
  1561:                                 ("the volume you selected - %s - is " +
  1562:                                  "currently mounted as %s, and cannot be " +
  1563:                                  "unmounted.") % (device, mount_point))
  1564:                         
  1565:                         else:
  1566:                             # umount succeeded, done
  1567:                             return True
  1568:                             
  1569:                         
  1570:                         True
  1571:                     else:
  1572:                         # some process(es) using the mount, exception
  1573:                         raise ZyXLiveInstallerGUIError( \
  1574:                             self, 
  1575:                             ("the volume you selected - %s - is currently " +
  1576:                              "mounted as %s, and cannot be unmounted because" +
  1577:                              "it is in use by these processes - %s") %
  1578:                             (device, mount_point, fuser_out))
  1579:                 else:
  1580:                     # mount_point is not /media/*, raise exception
  1581:                     raise ZyXLiveInstallerGUIError( \
  1582:                         self, 
  1583:                         ("the volume you selected - %s - is currently " +
  1584:                          "mounted as %s") % (device, mount_point))
  1585: 
  1586:         return True
  1587: 
  1588:     ##
  1589:     ## periodic/timer function(s)
  1590:     ##
  1591: 
  1592:     def do_periodic(self):
  1593:         # not actually using timekeeping at the moment
  1594: #        self.numticks = self.numticks + 1
  1595: 
  1596:         #
  1597:         # handle being idle during external partitioner mode
  1598:         #
  1599:         # this should not strictly be necessary, and may be removed
  1600:         # (as the thread itself changes mode right before exiting)
  1601:         if self.waiting_on_partitioner is True:
  1602:             # in case (?) the thread exploded before exiting 
  1603:             if self.partitioner_thread.isAlive() is False:
  1604:                 if (self.current_mode != "intro"):
  1605:                     self.set_mode("intro")
  1606:                 self.waiting_on_partitioner = False
  1607:  
  1608:         #
  1609:         # handle being idle during external installation in progress mode
  1610:         #
  1611:         if (self.current_mode == "installing"):
  1612:             # check to see if the installer thread completed
  1613:             if self.installer_thread.isAlive() is not True:
  1614:                 self.set_mode("success")
  1615:                 
  1616:             # note: this is the main thread, presuming it is ok to not
  1617:             #       wrap in gtk.gdk.threads_enter/leave()
  1618:             # edunote: set_fraction seems broken, setting with 1.000000 seemed
  1619:             # edunote: to trigger assertion on 0.0 <= set_percentage(?) <= 1.0
  1620:             if (self.installer.progress >= 1.0):
  1621:                 # edunote: WTF?: this hits when/if 
  1622:                 # edunote: self.installer.progress == 1.00000
  1623:                 # edunote: Answer seems to be float precision, i.e. 
  1624:                 # edunote: 1.0 != 1.000000 (?)
  1625:                 raise ZyXLiveInstallerGUIError( \
  1626:                     self, 
  1627:                     "progress 'greater' than 1.0 -- %f" \
  1628:                         % self.installer.progress)
  1629:             else:
  1630:                 self.installing_progressbar.set_fraction( \
  1631:                     self.installer.progress)
  1632:         
  1633:         # return true so the function will be called again next period
  1634:         return True
  1635: 
  1636: #############################################################################
  1637: #############################################################################
  1638: ##
  1639: ## utility functions
  1640: ##
  1641: ## TODO: at least these can be moved to a different source file
  1642: ##
  1643: #############################################################################
  1644: #############################################################################
  1645: 
  1646: 
  1647: def remove_markup(string):
  1648:     """Remove <> style markup from a string.
  1649: 
  1650:     string -- a string possibly containing <> style markups
  1651: 
  1652:     """
  1653:     result=""
  1654:     markup_depth=0
  1655:     for char in list(string):
  1656:         if char is '<':
  1657:             markup_depth = markup_depth + 1
  1658:         elif char is '>':
  1659:             markup_depth = markup_depth - 1
  1660:         elif (markup_depth == 0):
  1661:             result = result + char
  1662: 
  1663:     return result
  1664: 
  1665: 
  1666: def blockdev_size(block_device):
  1667:     """Input a block device and output its size in GB.
  1668: 
  1669:     block_device -- a block device given by its path, e.g. /dev/sda1
  1670: 
  1671:     """
  1672:        
  1673:     dev_null = os.open("/dev/null", os.O_WRONLY)
  1674:     try:
  1675:         helper_proc = subprocess.Popen(["/sbin/blockdev", 
  1676:                                         "--getsize64",
  1677:                                         block_device],
  1678:                                        stdout=subprocess.PIPE,
  1679:                                        stderr=dev_null)
  1680:         helper_out = helper_proc.communicate()[0]
  1681:     finally:
  1682:         os.close(dev_null)
  1683:         
  1684:     # note: I had this above the helper_out= code, and I'd never see
  1685:     #       the clause hit, i.e. when not run as root.  My best 
  1686:     #       understanding thus far is that the process does not really
  1687:     #       launch until the .communicate()[0] is called ??
  1688:     if helper_proc.returncode:
  1689:         return "n/a"
  1690:     else:
  1691:  
  1692:         try:
  1693:             # web has copyrighted 'your first python program' examples of 
  1694:             # the full monty
  1695:             bd_answer = str(int(helper_out.split()[0]) / 1024 / 1024 / 1024)
  1696:         except:
  1697:             bd_answer = "n/a"
  1698: 
  1699:         if (bd_answer == "n/a"):
  1700:             return bd_answer
  1701:         else:
  1702:             # XXX: pango.ALIGN_RIGHT workaround: using fixed width font and 
  1703:             #      right justification here instead.
  1704:             return "%5d" % (int(bd_answer))
  1705: 
  1706: 
  1707: def get_device_realshortname(block_device):
  1708:     """Input a block device and output its shortname, e.g. sda1.
  1709: 
  1710:     block_device -- a block device given by its path, 
  1711:                     e.g. /dev/disk/by-id/something
  1712: 
  1713:     """
  1714: 
  1715:     device_abspath = os.path.realpath(block_device)
  1716:     if device_abspath.startswith("/dev/sd"):
  1717:         return os.path.basename(device_abspath)
  1718:     elif device_abspath.startswith("/dev/hd"):
  1719:         return os.path.basename(device_abspath)
  1720:     elif device_abspath.startswith("/dev/vd"):
  1721:         return os.path.basename(device_abspath)
  1722:     elif device_abspath.startswith("/dev/mapper"):
  1723: #        return "*lvm*"
  1724:         return '<span foreground="#00FF00">lvm</span>'
  1725:     else:
  1726:         return "n/a"
  1727: 
  1728: # TODO: replace with pyparted-2+ once documented and VirOS is rebased to f11
  1729: def get_device_partition_type(block_device):
  1730:     """Input a block device and output its partition flag string.
  1731: 
  1732:     block_device -- a block device given by its path name,
  1733:                     e.g. /dev/disk/by-id/something-part2
  1734: 
  1735:     note: 'wholedisk' is returned for nonpartition devices
  1736: 
  1737:     """
  1738:    
  1739:     block_device_real = os.path.realpath(block_device)
  1740: 
  1741:     if block_device.startswith("/dev/mapper"):
  1742:         return "n/a"
  1743:     end_base_index = block_device.find("-part")
  1744:     
  1745:     if (end_base_index == -1):
  1746:         return '<span foreground="#FF0000">whole disk</span>'
  1747:     else:
  1748:         base_device = block_device[0:end_base_index]
  1749: 
  1750:         base_device_real = os.path.realpath(base_device)
  1751: 
  1752:         dev_null = os.open("/dev/null", os.O_WRONLY)
  1753:         try:
  1754:             helper_proc = subprocess.Popen(["/sbin/fdisk", 
  1755:                                             "-l",
  1756:                                             base_device_real],
  1757:                                            stdout=subprocess.PIPE,
  1758:                                            stderr=dev_null)
  1759:             helper_out = helper_proc.communicate()[0]
  1760:         finally:
  1761:             os.close(dev_null)
  1762:         
  1763:         if helper_proc.returncode:
  1764:             return "n/a"
  1765:         else:
  1766:             # extract desired string from output
  1767:             for outline in helper_out.splitlines():
  1768:                 # check for the target partition in the first word of this 
  1769:                 # line of output
  1770:                 outwords = outline.split()
  1771:                 if (len(outwords) and 
  1772:                     (outline.split()[0].find(block_device_real) != -1)):
  1773:                     try:
  1774:                         outwords.remove("*")
  1775:                     except:
  1776:                         pass
  1777:                     return string.join(outwords[5:], ' ')
  1778: 
  1779:         # TODO: handle with exception (really, check for root privs up front)
  1780:         return 'n/a'
  1781: 
  1782: 
  1783: #############################################################################
  1784: #############################################################################
  1785: ##
  1786: ## end code -- just notes below
  1787: ##
  1788: #############################################################################
  1789: #############################################################################
  1790: #
  1791: # edunote: I'm going to believe this, and lose this comment when I see it
  1792: #          in a more official document.  Seems obviously correct unless
  1793: #          value update is not atomic.
  1794: #  
  1795: #   http://mail.python.org/pipermail/python-list/2008-December/693764.html
  1796: #   Is this a theoretical question? What do your readers/writers actually do?
  1797: #   Just reading a global variable does not require any locking. If it is  
  1798: #   being modified, you either get the previous value, or the new value.
  1799: 
  1800: