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/>.
31 from subprocess
import CalledProcessError
32 from threading
import Thread
38 def get_shared_path():
39 testfile
= 'portablelinux.glade'
40 sharedirs
= [".",os
.path
.join(os
.path
.dirname(sys
.argv
[0]),"../share/portablelinux")]
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
)
49 raise Exception, "Portable Linux shared files " + testfile
+ " cannot be found in any of " + str(sharedirs
) + " default paths"
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
68 p
= subprocess
.Popen(*args
,**kwargs
)
69 last_check_call_pid
= p
.pid
74 e
= CalledProcessError(ret
,args
[0])
80 # this snippet was inspired from usb-creator
81 try: collect_check_call(["which","gksu"])
82 except CalledProcessError
: return #fail silently if no gksu
85 args
= ['gksu', 'gksu', '--desktop',
86 os
.path
.join(get_shared_path(),'portablelinux.desktop'), '--']
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
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"
127 dirs
= ( os
.path
.join(mountpoint
,"boot"), os
.path
.join(mountpoint
,"boot","grub") )
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
):
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
152 Thread
.__init
__(self
,name
="MakePortableLinux")
154 self
.reporter
= reporter
155 self
.isoimage
= isoimage
158 self
.persistent_size
= persistent_size
162 self
.justrepair
= repair
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
175 func
= getattr(self
,stage
)
178 if self
.cancel
is not True: self
.reporter(e
)
182 self
.reporter("done")
185 mounted
= [ a
.split()[0] for a
in file("/etc/mtab").readlines() if a
.startswith(self
.device
) ]
186 for mntpnt
in mounted
: umount(mntpnt
)
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
204 subprocess
.Popen(["sfdisk",self
.device
],
205 stdin
=subprocess
.PIPE
).communicate(text
)
212 if self
.verify
: badblocks(self
.device
+"2")
215 mkdosfs(self
.device
+"1",verify
=self
.verify
)
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")
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
]
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])
252 kernel /boot/vmlinuz boot=casper %s persistent
253 initrd /boot/initrd.gz
255 fns
= ["grub.conf","menu.lst"]
256 for fn
in fns
: savefile(os
.path
.join(mountpoint
,"boot","grub",fn
),bootmenu
)
260 if hasattr(self
,"isopart"): umount(self
.isopart
)
261 os
.rmdir(self
.dospart
)
262 if hasattr(self
,"isopart"): os
.rmdir(self
.isopart
)
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()
280 mountloop(iso
,mntpnt
,fstype
="iso9660")
281 if not os
.path
.exists(os
.path
.join(mntpnt
,"casper","vmlinuz")): raise UnsupportedISO
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
)
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
)
302 def gtk_main_quit(*args
):
307 self
.valid_iso
= False
308 self
.valid_device
= False
311 model
= gtk
.ListStore(str,str,int,str)
312 device_list
= self
.get("device")
313 device_list
.set_model(model
)
316 device_list
= self
.get("device")
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)
330 for d
in get_devices():
335 for m
,n
,mb
in lt
: model
.append((m
,n
,mb
,str(mb
) + " MiB"))
337 self
.update_readiness()
340 gobject
.timeout_add(1000,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
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.")
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
)
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
]
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
377 self
.valid_device
= False
381 self
.update_readiness()
383 def justrepairgrub_toggled_cb(self
,widget
=None):
384 def set_visible(o
,d
):
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
)
419 def show_activity(*args
):
420 if not hasattr(self
,"process") or \
421 not self
.process
.isAlive() or \
424 self
.get("throbber").pulse()
426 gobject
.timeout_add(300,show_activity
)
429 def handle_progress_report(self
,arg
):
431 table
= self
.get("arrow_container")
433 if w
.get_name().startswith("arrow_"): w
.hide()
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.")
447 # show the right arrow
449 self
.get("arrow_%s"%arg
).show()
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
)
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
):
478 self
.get("setup_progress").hide()
479 self
.get("setup_options").set_sensitive(True)
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):
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"))
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
)
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
)
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
):
513 self
.get("about").hide()
515 self
.get("about").connect("delete-event",nothing
)
516 self
.get("about").connect("response",nothing
)
517 self
.get("about").show()
522 gtk
.gdk
.threads_init()
523 app
= PortableLinux()
526 if __name__
== "__main__":