Merge pull request #7 from rogersachan/patch-1
[portablelinux.git] / portablelinux
blob11ebba5d0f646ae00c1043764d1c5ab0ebe037dd
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 # Copyright (C) 2009 Manuel Amador rudd-o@rudd-o.com
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import os
21 import sys
22 import gobject
23 import pygtk
24 pygtk.require("2.0")
25 import gtk
26 import gtk.glade
27 import commands
28 import glob
29 import tempfile
30 import subprocess
31 from subprocess import CalledProcessError
32 from threading import Thread
33 import StringIO
34 import time
35 import signal
36 import traceback
38 def get_shared_path():
39 testfile = 'portablelinux.glade'
40 sharedirs = [".",os.path.join(os.path.dirname(sys.argv[0]),"../share/portablelinux")]
41 sharepath = None
42 for sharedir in sharedirs:
43 fname = os.path.join(os.path.abspath(sharedir),testfile)
44 if os.path.exists(fname):
45 sharepath = os.path.abspath(sharedir)
46 break
48 if sharepath is None:
49 raise Exception, "Portable Linux shared files " + testfile + " cannot be found in any of " + str(sharedirs) + " default paths"
51 return sharepath
54 # hardware stuff
55 class UnsupportedISO(Exception): pass
56 class ISOTooLarge(Exception): pass
58 last_check_call_pid = None
59 def collect_check_call(*args,**kwargs):
60 global last_check_call_pid
62 print "Running command: %s"%args[0]
64 io = tempfile.TemporaryFile()
65 kwargs["stdin"] = None
66 kwargs["stdout"] = io
67 kwargs["stderr"] = io
68 p = subprocess.Popen(*args,**kwargs)
69 last_check_call_pid = p.pid
70 ret = p.wait()
71 io.seek(0)
72 output = io.read()
73 if ret != 0:
74 e = CalledProcessError(ret,args[0])
75 e.output = output
76 raise e
77 return output
79 def try_to_sudo():
80 # this snippet was inspired from usb-creator
81 try: collect_check_call(["which","gksu"])
82 except CalledProcessError: return #fail silently if no gksu
83 # oh, there IS gksu!
84 if os.getuid() != 0:
85 args = ['gksu', 'gksu', '--desktop',
86 os.path.join(get_shared_path(),'portablelinux.desktop'), '--']
87 args.extend(sys.argv)
88 os.execvp(args[0], args)
90 def sigterm_check_call():
91 global last_check_call_pid
92 try: os.kill(last_check_call_pid,15)
93 except Exception: pass
95 def get_devices():
96 for a in glob.glob("/sys/block/sd*"):
97 removable = file(a+"/removable").read().startswith("1")
98 mb_blocks = int(file(a+"/size").read()) / 2 / 1024
99 name = " ".join([ file(a+b).read().strip() for b in ["/device/vendor","/device/model"] ])
100 if removable and mb_blocks >= 1000:
101 a = a.replace("/sys/block","/dev")
102 yield (a,name,mb_blocks)
105 def mountloop(file,mntpnt,fstype=None):
106 cmd = ["mount","-o","loop",file,mntpnt]
107 if fstype: cmd.extend(["-t",fstype])
108 collect_check_call(cmd)
109 def mount(file,mntpnt,fstype=None):
110 cmd = ["mount",file,mntpnt]
111 if fstype: cmd.extend(["-t",fstype])
112 collect_check_call(cmd)
113 def umount(device): collect_check_call(["umount",device])
114 def badblocks(device): collect_check_call(["badblocks",device])
115 def sync(): collect_check_call(["sync"])
116 def mkdosfs(device,verify=True):
117 cmd = ["mkfs.vfat","-F","32"]
118 if verify: cmd.extend(["-c"])
119 cmd.extend(["-n","PORTABLELNX",device])
120 collect_check_call(cmd)
121 def installgrub(device,mountpoint):
122 print "Installing grub"
123 devicemap = \
125 (hd0) %s
127 dirs = ( os.path.join(mountpoint,"boot"), os.path.join(mountpoint,"boot","grub") )
128 for d in dirs:
129 if not os.path.isdir(d): os.mkdir(d)
130 fn = os.path.join(mountpoint,"boot","grub","device.map")
131 savefile(fn,devicemap%device)
132 collect_check_call(["grub-install","--no-floppy","--root-directory=%s"%mountpoint,device])
133 savefile(fn,devicemap%"/dev/sda")
134 def mke3fs(device): collect_check_call(["mkfs.ext3","-F",device])
135 def dd(infk,of,bs=None,count=None):
136 cmd = ["dd","if=%s"%infk,"of=%s"%of]
137 if bs: cmd.append("bs=%s"%bs)
138 if count: cmd.append("count=%s"%count)
139 collect_check_call(cmd)
140 def savefile(fn,text):
141 f = file(fn,"w")
142 f.write(text)
143 f.close()
145 class MakePortableLinux(Thread):
147 def __init__(self,reporter,isoimage,device,verify,persistent_size,repair):
148 """reporter is a function that takes one argument, either:
149 the name of a stage, or
150 an exception
152 Thread.__init__(self,name="MakePortableLinux")
153 self.setDaemon(True)
154 self.reporter = reporter
155 self.isoimage = isoimage
156 self.device = device
157 self.verify = verify
158 self.persistent_size = persistent_size
159 self.cancel = False
160 self.error = False
161 self.stage = None
162 self.justrepair = repair
164 def abort(self):
165 self.cancel = True
166 sigterm_check_call()
168 def run(self):
169 if self.justrepair: stages = "umountfs mountfs installgrub finalize"
170 else: "umountfs partition umountfs2 badblocks mkfs mountfs installgrub copykernel mkpersistent writeiso mkbootmenu finalize"
171 for stage in stages.split():
172 if self.cancel is True: return
173 self.stage = stage
174 self.reporter(stage)
175 func = getattr(self,stage)
176 try: func()
177 except Exception,e:
178 if self.cancel is not True: self.reporter(e)
179 self.error = True
180 self.cleanup()
181 return
182 self.reporter("done")
184 def umountfs(self):
185 mounted = [ a.split()[0] for a in file("/etc/mtab").readlines() if a.startswith(self.device) ]
186 for mntpnt in mounted: umount(mntpnt)
187 sync()
189 def partition(self):
190 dd("/dev/zero",self.device,"64K","1")
191 out,err = subprocess.Popen(["sfdisk","-l",self.device],
192 stdout=subprocess.PIPE).communicate()
193 text = out.splitlines()
194 cylcount = int(text[1].split()[2])
195 cylsize = int(text[2].split()[4])
196 isosize = 700*1024*1024
197 isocyls = isosize / cylsize + 1
198 rest = cylcount - isocyls
199 assert rest > 0
200 text = """0,%s,c,*
201 ,,83
203 """%rest
204 subprocess.Popen(["sfdisk",self.device],
205 stdin=subprocess.PIPE).communicate(text)
206 time.sleep(10)
208 def umountfs2(self):
209 self.umountfs()
211 def badblocks(self):
212 if self.verify: badblocks(self.device+"2")
214 def mkfs(self):
215 mkdosfs(self.device+"1",verify=self.verify)
217 def mountfs(self):
218 self.dospart = tempfile.mkdtemp()
219 mount(self.device+"1",self.dospart,fstype="vfat")
221 def installgrub(self):
222 installgrub(self.device,self.dospart)
224 def copykernel(self):
225 self.isopart = tempfile.mkdtemp()
226 mountloop(self.isoimage,self.isopart,fstype="iso9660")
227 collect_check_call([
228 "cp","-f",
229 "%s/casper/vmlinuz"%self.isopart,"%s/casper/initrd.gz"%self.isopart,
230 "%s/boot"%self.dospart])
232 def mkpersistent(self):
233 dd("/dev/zero","%s/casper-rw"%self.dospart,bs="1M",count=str(self.persistent_size))
234 mke3fs("%s/casper-rw"%self.dospart)
236 def writeiso(self): dd(self.isoimage,self.device+"2")
238 def mkbootmenu(self):
239 seeds = glob.glob(os.path.join(self.isopart,"preseed","*.seed"))
240 seeds = [ (os.stat(x)[6],x) for x in seeds ]
241 seeds.sort()
242 # pick the last seed which is the largest because of the sort
243 if seeds: seed = "file=/preseed/%s "%os.path.basename(seeds[-1][1])
244 else: seed = ""
245 bootmenu = \
247 default 0
248 timeout 10
250 title Linux (Live)
251 root (hd0,0)
252 kernel /boot/vmlinuz boot=casper %s persistent
253 initrd /boot/initrd.gz
254 """%seed
255 fns = ["grub.conf","menu.lst"]
256 for fn in fns: savefile(os.path.join(mountpoint,"boot","grub",fn),bootmenu)
258 def finalize(self):
259 umount(self.dospart)
260 if hasattr(self,"isopart"): umount(self.isopart)
261 os.rmdir(self.dospart)
262 if hasattr(self,"isopart"): os.rmdir(self.isopart)
263 sync()
265 def cleanup(self):
266 try: umount(self.dospart)
267 except Exception: pass
268 try: umount(self.isopart)
269 except Exception: pass
270 try: os.rmdir(self.dospart)
271 except Exception: pass
272 try: os.rmdir(self.isopart)
273 except Exception: pass
276 def validate_iso(iso):
277 if os.stat(iso)[6] > 700*1024*1024: raise ISOTooLarge
278 mntpnt = tempfile.mkdtemp()
279 try:
280 mountloop(iso,mntpnt,fstype="iso9660")
281 if not os.path.exists(os.path.join(mntpnt,"casper","vmlinuz")): raise UnsupportedISO
282 finally:
283 try: umount(mntpnt)
284 except Exception: pass
285 try: os.rmdir(mntpnt)
286 except Exception: pass
289 class PortableLinux(gtk.glade.XML):
291 def get(self,n): return self.get_widget(n)
293 def __init__ (self):
294 gtk.glade.XML.__init__(self,os.path.join(get_shared_path(),'portablelinux.glade'))
295 self.signal_autoconnect(self)
296 self.get("setup_options").connect("close",gtk.main_quit)
297 self.get("setup_options").connect("destroy",gtk.main_quit)
298 self.get("setup_progress").connect("response",self.setup_progress_response_cb)
299 self.get("setup_progress").connect("delete_event",self.setup_progress_response_cb)
300 self.run_setup()
302 def gtk_main_quit(*args):
303 gtk.main_quit()
305 def run_setup(self):
307 self.valid_iso = False
308 self.valid_device = False
310 def redo_model():
311 model = gtk.ListStore(str,str,int,str)
312 device_list = self.get("device")
313 device_list.set_model(model)
314 return model
316 device_list = self.get("device")
317 redo_model()
318 cell = gtk.CellRendererText()
319 device_list.pack_start(cell, True)
320 device_list.add_attribute(cell, 'text', 1)
321 cell = gtk.CellRendererText()
322 device_list.pack_start(cell, True)
323 device_list.add_attribute(cell, 'text', 3)
325 self._ck = ""
327 def loop(*args):
328 ck = ""
329 lt = []
330 for d in get_devices():
331 lt.append(d)
332 ck = ck + str(d)
333 if ck != self._ck:
334 model = redo_model()
335 for m,n,mb in lt: model.append((m,n,mb,str(mb) + " MiB"))
336 self._ck = ck
337 self.update_readiness()
338 return True
340 gobject.timeout_add(1000,loop)
341 loop()
343 self.get("setup_options").show()
344 self.get("persistent_size").set_text("256")
346 def on_iso_selected(self,*args):
348 filename = self.get("isoimage").get_filename()
349 self.valid_iso = False
350 if filename:
351 try:
352 valid = validate_iso(filename)
353 self.valid_iso = True
354 except UnsupportedISO:
355 self.warning_dialog("The image you chose is not a supported ISO image","The image you selected is a CD image, but is not supported. Only Casper-based Live bootable Linux images like Ubuntu and Knoppix are supported.")
356 except ISOTooLarge:
357 self.warning_dialog("The image you chose is too large","The image you selected is more than 700 MB in size. Please select a proper ISO image.")
358 except CalledProcessError,e:
359 if e.returncode & 1: self.need_root_privs_dialog()
360 if e.returncode & 32: self.warning_dialog("The file you chose is not an ISO image","The operating system has refused to mount that file because it is not an ISO image. Verify that you have chosen a file that is a valid ISO image.","Details from the operating system:\n\n%s"%e.output)
361 else: raise
363 self.update_readiness()
365 def on_device_selected(self,*args):
367 # FIXME verify the USB stick has enough disk space
369 num = self.get("device").get_active()
370 device,name,size,throwaway = self.get("device").get_model()[num]
371 if size != -1:
372 maxsize = min( [ size-700-16 , 4095 ] )
373 adj = gtk.Adjustment(lower=32,upper=maxsize,step_incr=1,page_incr=16,page_size=16)
374 self.get("persistent_size").set_adjustment(adj)
375 self.valid_device = True
376 else:
377 self.valid_device = False
381 self.update_readiness()
383 def justrepairgrub_toggled_cb(self,widget=None):
384 def set_visible(o,d):
385 if d: o.show()
386 else: o.hide()
387 set_visible(self.get("destroydata"),not widget.get_active())
388 set_visible(self.get("onlygrubwillbeinstalled"),widget.get_active())
389 self.get("isoimage").set_sensitive(not widget.get_active())
390 self.get("tablereserve").set_sensitive(not widget.get_active())
391 self.get("verify").set_sensitive(not widget.get_active())
392 self.update_readiness()
394 def update_readiness(self):
395 self.get("ok").set_sensitive( ( self.valid_iso or self.get("destroydata") ) and self.valid_device )
397 def install_portable_linux(self,*args):
398 num = self.get("device").get_active()
399 repair = self.get("justrepairgrub").get_active()
400 device = self.get("device").get_model()[num][0]
401 filename = self.get("isoimage").get_filename()
402 verify = self.get("verify").get_active()
403 persistent_size = int(self.get("persistent_size").get_text())
405 for a in [self.get("arrow_badblocks"),self.get("label_badblocks")]:
406 a.set_sensitive(verify)
407 self.get("setup_options").set_sensitive(False)
408 self.get("setup_progress").set_transient_for(self.get("setup_options"))
409 self.get("cancel_install").show()
410 self.get("dismiss_progress").hide()
411 self.get("setup_progress").show()
413 def idle_reporter(arg):
414 gobject.idle_add(self.handle_progress_report,arg)
416 self.process = MakePortableLinux(idle_reporter,filename,device,verify,persistent_size,repair)
417 self.process.start()
419 def show_activity(*args):
420 if not hasattr(self,"process") or \
421 not self.process.isAlive() or \
422 self.process.error:
423 return False
424 self.get("throbber").pulse()
425 return True
426 gobject.timeout_add(300,show_activity)
429 def handle_progress_report(self,arg):
430 def hidearrows():
431 table = self.get("arrow_container")
432 def h(w,*args):
433 if w.get_name().startswith("arrow_"): w.hide()
434 table.foreach(h)
436 if arg == "done":
437 hidearrows()
438 self.get("progress_primary").set_markup("<b><big><big>Portable Linux has been installed</big></big></b>")
439 self.get("progress_secondary").set_markup("<big>It's safe to remove your portable drive now. Test it by plugging it into a computer, rebooting it, and selecting USB boot from the BIOS setup or BIOS boot menu.</big>")
440 self.get("cancel_install").hide()
441 self.get("dismiss_progress").show()
442 self.setup_progress_response_cb()
443 self.info_dialog("Portable Linux installation is complete","The installation process completed successfully; it is now safe to remove your portable drive from your computer. Your portable drive should now be bootable in any computer.")
444 return
446 if type(arg) is str:
447 # show the right arrow
448 hidearrows()
449 self.get("arrow_%s"%arg).show()
450 return
452 try:
453 raise arg
454 except Exception,e:
456 self.get("progress_primary").set_markup("<b><big><big>Portable Linux could not be installed</big></big></b>")
457 self.get("progress_secondary").set_markup("<big>An error prevented the process from being completed. You can close this dialog and disconnect your portable drive now, but you may need to repartition and reformat your portable drive.</big>")
458 self.get("cancel_install").hide()
459 self.get("dismiss_progress").show()
461 if hasattr(e,"output") and e.output: cmdoutput = "Details from the operating system:\n%s"%e.output
462 else: cmdoutput = None
463 tback = traceback.format_exc()
464 ternary = "\n\n".join( [ a for a in [cmdoutput,tback] if a ] )
465 if self.process.stage == "umountfs":
466 self.error_dialog("Cannot access your portable drive exclusively","One of the partitions in the portable drive cannot be unmounted. Close any applications that have files open on your portable drive, then try again.",ternary)
467 elif self.process.stage == "badblocks":
468 self.error_dialog("This portable drive is malfunctioning","At least one sector from your portable drive is damaged. Thus, Portable Linux cannot be installed in it. Insert another portable drive and try again.",ternary)
469 elif self.process.stage == "umountfs2":
470 self.error_dialog("Cannot access your portable drive exclusively","For some reason, your operating system seems to have mounted one of the newly created partitions automatically, and holds files open there. Please report your operating system's version to the developers.",ternary)
471 elif self.process.stage == "mkfs" and isinstance(e,OSError) and e.errno is 2:
472 self.error_dialog("A required program is missing","Your computer does not have the dosfstools package installed. Use your distribution's package management tools to install it, then try again.",ternary)
473 else:
474 self.error_dialog("An unexpected error took place","An unrecoverable error has stopped the creation of your Portable Linux drive. Please report the details of this error to the Portable Linux developers, so we can fix it right away.",ternary)
476 def setup_progress_response_cb(self,*args):
477 self.process.abort()
478 self.get("setup_progress").hide()
479 self.get("setup_options").set_sensitive(True)
480 return True
482 # info dialogs
484 def info_dialog(self,p,s,t = None): self.dialog(p,s,t)
485 def warning_dialog(self,p,s,t = None): self.dialog(p,s,t,type=gtk.MESSAGE_WARNING)
486 def error_dialog(self,p,s,t = None): self.dialog(p,s,t,type=gtk.MESSAGE_ERROR)
487 def dialog(self,primary,secondary,ternary = None,type=None):
488 kwargs = {
489 "buttons":gtk.BUTTONS_CLOSE,
490 "flags":gtk.DIALOG_MODAL
492 if type: kwargs["type"]=type
493 dialog = gtk.MessageDialog(**kwargs)
494 if self.get("setup_progress").get_property("visible"):
495 dialog.set_transient_for(self.get("setup_progress"))
496 else:
497 dialog.set_transient_for(self.get("setup_options"))
498 dialog.set_markup("<b><big><big>%s</big></big></b>"%primary)
499 dialog.format_secondary_markup("<big>%s</big>"%secondary)
500 if ternary:
501 dialog.format_secondary_markup(
502 "<big>%s</big>\n\n%s"%(secondary,ternary))
504 def destroy(*args): dialog.destroy()
505 dialog.connect("response",destroy)
506 dialog.show()
508 def need_root_privs_dialog(self):
509 self.info_dialog("You need to run Portable Linux as root","Portable Linux requires root privileges to perform several operations on your disks and ISO images. Please restart Portable Linux as root.")
511 def show_about(self,*args):
512 def nothing(*args):
513 self.get("about").hide()
514 return True
515 self.get("about").connect("delete-event",nothing)
516 self.get("about").connect("response",nothing)
517 self.get("about").show()
520 def main():
521 try_to_sudo()
522 gtk.gdk.threads_init()
523 app = PortableLinux()
524 gtk.main()
526 if __name__ == "__main__":
527 main()