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: