diff --git a/.gitignore b/.gitignore index 780795d..353fe20 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ lib/ lib64/ parts/ sdist/ +deb_dist/ var/ *.egg-info/ .installed.cfg @@ -97,6 +98,7 @@ multibootusb*.tar.gz # Temp files data/multibootusb/grub/menus.zip +MANIFEST # Setup file diff --git a/CHANGELOG b/CHANGELOG index 258019b..926fae9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,20 @@ +Version - 9.3.0 +--------------- +* Provide information about free and total space of selected USB device in the main GUI +* Unmount partitions and lock physical drive when dd-ing iso on Windows +* Unmount the USB device partition while using QEMU to boot from USB on Linux +* Fix description of an exception which gets raised of diskpart.exe fails +* Fix repeated calls to update GUI +* Fix syntax of an exception description +* Imager writing on entire disk instead of partitions +* Fix crash when using imager under certain conditions +* Unmount partitions before dding iso image on Linux +* Catch an error generated while copying iso image to the target USB +* Fix uuid generation from NTFS/FAT32 partition on Windows +* Add gptmbr.bin to package data files +* Catch exceptions raised during install_syslinux() and make effort to undo partially completed installation +* Fix a doc string to keep up with the function signature change + Version - 9.2.0 --------------- * Welcome onboard Shiniji Suzuki. The most of the bug fixes and additional features implemented are done by him. A big tanks to him. diff --git a/build_pkg b/build_pkg old mode 100755 new mode 100644 index 732635b..cc23cfe --- a/build_pkg +++ b/build_pkg @@ -126,12 +126,12 @@ class pkg(): Depends3: python3-pyqt5, parted, util-linux, mtools, python3-dbus, python3-pyudev, p7zip-full, python3-six Build-Depends: python3-all Section: system - XS-Python-Version: = 3.5 + XS-Python-Version: = 3 Debian-Version: 1""" with open("stdeb.cfg", "w") as f: f.write(stdcfg) - if subprocess.call('/usr/bin/python3.5 setup.py --command-packages=stdeb.command bdist_deb', shell=True) == 0 and \ + if subprocess.call('/usr/bin/python3 setup.py --command-packages=stdeb.command bdist_deb', shell=True) == 0 and \ os.path.exists(os.path.join("deb_dist", "python3-multibootusb_" + self.version + "-1_all.deb")): try: shutil.copy2(os.path.join("deb_dist", "python3-multibootusb_" + self.version + "-1_all.deb"), @@ -159,7 +159,7 @@ class pkg(): "Requires = " + require) with open("setup.cfg", "w") as f: f.write(setup_cfg) - if subprocess.call('/usr/bin/python3.5 setup.py bdist_rpm', shell=True) == 0 and \ + if subprocess.call('/usr/bin/python3 setup.py bdist_rpm', shell=True) == 0 and \ os.path.exists(os.path.join("dist", "multibootusb-" + self.version + "-1.noarch.rpm")): if self.pkg_name == 'suse': package = "multibootusb-" + self.version + "-1suse.noarch.rpm" diff --git a/data/multibootusb/grub.exe b/data/multibootusb/grub.exe index 57b890d..58b3a63 100644 Binary files a/data/multibootusb/grub.exe and b/data/multibootusb/grub.exe differ diff --git a/data/multibootusb/syslinux.bin b/data/multibootusb/syslinux.bin deleted file mode 100644 index d887d40..0000000 Binary files a/data/multibootusb/syslinux.bin and /dev/null differ diff --git a/data/version.txt b/data/version.txt index deeb3d6..b13d146 100644 --- a/data/version.txt +++ b/data/version.txt @@ -1 +1 @@ -9.2.0 +9.3.0 diff --git a/multibootusb b/multibootusb index ae831e0..0d8f2f1 100644 --- a/multibootusb +++ b/multibootusb @@ -9,6 +9,7 @@ import os import getopt import sys import platform +import shutil # The following line is required for distros based on rpm so as to avoid import errors when running from # installed system @@ -33,13 +34,15 @@ try: from scripts import admin from scripts import gen from scripts import config -except: + from scripts import osdriver +except ImportError: try: from .scripts.mbusb_cli import * from .scripts import admin from .scripts import gen from .scripts import config - except: + from .scripts import osdriver + except ImportError: import scripts gui = True @@ -102,7 +105,7 @@ Example for installing multiple distros without user intervention: Windows: python3 multibootusb -c -y -i ../../favourite.iso,../../other-distro.iso -t G: - + Example for writing ISO image to target USB disk (will destroy data on USB disk): Linux: @@ -160,7 +163,7 @@ if __name__ == '__main__': config.image_path = arg.split(',') elif opt in ('-t', '--target'): # Convert to upper if windows drive name is given - config.usb_disk = len(arg)==2 and arg[0].isalpha() and arg[1]==':'\ + config.usb_disk = len(arg) == 2 and arg[0].isalpha() and arg[1] == ':'\ and arg.upper() or arg elif opt in ('-c', '--command'): gui = False @@ -184,33 +187,46 @@ if __name__ == '__main__': sys.exit() ''' -if config.debug is True: - from scripts.debug import colors - log(colors.HEADER + "=== DEBUG ENABLED ===") +def main(): + if config.debug is True: + from scripts.debug import colors + log(colors.HEADER + "=== DEBUG ENABLED ===") -if gui is False: - check_admin() - if uninstall is True and config.usb_disk is not '': - cli_uninstall_distro() - elif uninstall is True and config.usb_disk is '': - log('\nYou must provide \'-t\' option to point to your USB disk for uninstalling a distro.\n' - 'See the usage example below.') - usage() - elif config.image_path is '' and config.usb_disk is '': - log('\nNo option provided. See the usage below.') - usage() - elif config.cli_syslinux is True and config.usb_disk is not '': - cli_install_syslinux() - elif config.image_path is '' or config.usb_disk is '': - log('\nOptions \'-i\' and \'-t\' must be supplied together. See the usage below.') - usage() - elif config.cli_dd is True: - cli_dd() - else: + + if gui is False: + check_admin() + if uninstall is True and config.usb_disk is not '': + cli_uninstall_distro() + elif uninstall is True and config.usb_disk is '': + log('\nYou must provide \'-t\' option to point to your USB disk for uninstalling a distro.\n' + 'See the usage example below.') + usage() + elif config.image_path is '' and config.usb_disk is '': + log('\nNo option provided. See the usage below.') + usage() + elif config.cli_syslinux is True and config.usb_disk is not '': + cli_install_syslinux() + elif config.image_path is '' or config.usb_disk is '': + log('\nOptions \'-i\' and \'-t\' must be supplied together. See the usage below.') + usage() + elif config.cli_dd is True: + cli_dd() + else: + running_from() + cli_install_distro() + + elif gui is True: running_from() - cli_install_distro() + start_gui() -elif gui is True: - running_from() - start_gui() + +if __name__ == '__main__': + osdriver.initialize() + try: + main() + finally: + from scripts import usb + for p in config.remounted_partitions: + log('Unmouting %s at exit' % p) + usb.unmount(p) diff --git a/multibootusb-pkexec b/multibootusb-pkexec index ba44ef3..e95f159 100644 --- a/multibootusb-pkexec +++ b/multibootusb-pkexec @@ -2,12 +2,19 @@ # Generic wrapper for locating multibootusb file and execute pkexec accordingly # Bin pat of this file and policy file have to match +# Ensure that root is added in to xhost list to access X-Server +# Otherwise script will fail to launch GUI +if type "xhost" > /dev/null; then + xhost local:root > /dev/null +fi + + if [ $(which pkexec) ]; then - if [ -f /usr/bin/multibootusb ]; then + if [ -f /usr/bin/multibootusb ]; then # path to debian based distros pkexec --disable-internal-agent "/usr/bin/multibootusb" "$@" fi - if [ -f /usr/local/bin/multibootusb ]; then + if [ -f /usr/local/bin/multibootusb ]; then # path to fedora based distros pkexec --disable-internal-agent "/usr/local/bin/multibootusb" "$@" fi else diff --git a/org.debian.pkexec.run-multibootusb.policy b/org.debian.pkexec.run-multibootusb.policy index d8e8566..e111748 100644 --- a/org.debian.pkexec.run-multibootusb.policy +++ b/org.debian.pkexec.run-multibootusb.policy @@ -12,7 +12,7 @@ auth_admin auth_admin - /usr/local/bin/multibootusb + /usr/bin/multibootusb true diff --git a/scripts/config.py b/scripts/config.py index 3a9c508..d4662a6 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -7,12 +7,12 @@ # under the terms of GNU General Public License, v.2 or above iso_link = "" -usb_disk = "" +usb_disk = None usb_mount = "" usb_uuid = "" usb_label = "" usb_details = '' -image_path = "" +image_path = None persistence = 0 persistence_available = False persistence_max_size = 0 @@ -35,6 +35,7 @@ imager_usb_disk_selected = "" imager_lock = "" imager_percentage = "" imager_status_text = "" +imager_return = "" install_size = "" @@ -42,5 +43,32 @@ editors_linux = ["xdg-open", "gedit", "kate", "kwrite"] editors_win = ["notepad++.exe", "notepad.exe"] imager_usb_disk = [] +remounted_partitions = [] debug = False + +# protected_drives = ['C:','D:','E:', '/dev/sda', '/dev/sdb', '/dev/sdc'] + +# If turned off, qemu will be sought at a few preset locations +# first before deciding to use the bundled exe. +# Set 'qemu_exe_path' to explicitly specify. +qemu_use_builtin = True # Relevant on Windows only + +# qemu_exe_path = r"C:\pkgs\qemu\qemu-system-x86_64.exe" +# Relevant on Windows only + +# Enable QEMU accelaration by Intel HAXM hypervisor. +# Bundled QEMU does not support this. +# See https://www.qemu.org/2017/11/22/haxm-usage-windows/ for setup. +qemu_use_haxm = not qemu_use_builtin # Relevant on Windows only +# qemu_use_kvm = False +# qemu_bios = 'OVMF.fd' + +def update_usb_mount(new_usb_details): + global usb_mount, usb_details + usb_mount = new_usb_details['mount_point'].replace('\\x20', ' ') + usb_details = new_usb_details + +def add_remounted(usb_disk): + if usb_disk not in remounted_partitions: + remounted_partitions.append(usb_disk) diff --git a/scripts/distro.py b/scripts/distro.py index 9bd8555..1166255 100644 --- a/scripts/distro.py +++ b/scripts/distro.py @@ -6,13 +6,15 @@ # Licence: This file is a part of multibootusb package. You can redistribute it or modify # under the terms of GNU General Public License, v.2 or above +from functools import partial import os import platform import re -from .iso import * -from .isodump3 import ISO9660 -from .gen import * + from . import _7zip +from .gen import * +from . import iso +from .isodump3 import ISO9660 def distro(iso_cfg_ext_dir, iso_link, expose_exception=False): @@ -23,181 +25,235 @@ def distro(iso_cfg_ext_dir, iso_link, expose_exception=False): """ # iso9660fs = ISO9660(iso_link) # iso_file_list = iso9660fs.readDir("/") - iso_file_list = _7zip.list_iso(iso_link, expose_exception=expose_exception) + + distro = None # tenatively set to None + + iso_file_list = _7zip.list_iso( + iso_link, expose_exception=expose_exception) + iso_file_list_lower = [f.lower() for f in iso_file_list] + v_isolinux_bin_exists = iso.isolinux_bin_exist(iso_link) + + # Let's have less costly checks first. + # We'll have to make these checks as strictive as possible + # so that keyword based tests will not be skipped because + # of a false positive. + + if iso_file_list: + distro = perform_strict_detections(iso_cfg_ext_dir, iso_file_list) + if distro: + return distro + distro = detect_iso_from_file_list(iso_file_list) + if distro: + return distro + else: + iso_file_list = [] + + def run_contains(keywords, filename, file_content, iso_flielist, + isolinux_bin_exists): + return any(k in file_content for k in keywords.split('|')) + + def contains(keywords): + return partial(run_contains, keywords.lower()) + + def run_file_exists(filename_sought, filename, file_content, iso_filelist, + isolinux_bin_exists): + return filename_sought in iso_filelist + def file_exists(filename_sought): + return partial(run_file_exists, filename_sought) + + def run_isolinux_bin_exists(exist_or_not, filename, file_content, + iso_filelist, isolinux_bin_exists): + return exist_or_not is isolinux_bin_exists + + def isolinux_bin_exists(exist_or_not): + return partial(run_isolinux_bin_exists, exist_or_not) + + def run_not(predicate, filename, file_content, + iso_filelist, isolinux_bin_exists): + return not predicate(filename, file_content, iso_filelist, + isolinux_bin_exists) + def not_(predicate): + return partial(run_not, predicate) + + # contains(X) predicates that X is contained in an examined text file. + # Multiple keywords can be concatenated by '|'. Predicates gets aaserted + # if anyone of the keywords is found in the text file. + # Sorry you can't include | in a keyword for now. + test_vector = [ + ('ubcd', contains('ubcd')), + ('hbcd', contains('hbcd')), + ('systemrescuecd', contains('systemrescuecd')), + ('parted-magic', [contains('pmagic|partedmagic'), + isolinux_bin_exists(True)]), + # mounting fat filesystem hard coded in to initrd. + # Can be modified only under linux. + ('mageialive', contains('mgalive')), + ('arch', contains('archisolabel|misolabel|parabolaisolabel')), + ('chakra', contains('chakraisolabel')), + ('kaos', contains('kdeosisolabel')), + ('debian', [contains('boot=live'), isolinux_bin_exists(True)]), + ('grml', [contains('grml'), contains('live-media-path')]), + ('debian-install', [contains('debian-installer'), + not_(file_exists('casper'))]), + ('solydx', contains('solydx')), + ('knoppix', contains('knoppix')), + ('centos', contains('root=live:CDLABEL=CentOS')), + ('fedora', contains('root=live:CDLABEL=|root=live:LABEL=')), + ('fedora', contains('redcore')), + ('redhat', contains('redhat')), + ('slitaz', contains('slitaz|dban |ophcrack|tinycore|rescue.cpi' + '|xpud|untangle|4mlinux|partition wizard' + '|android-x86.png|riplinux|lebel dummy' + '|http://pogostick.net/~pnh/ntpasswd/' + '|AVG Rescue CD|AntivirusLiveCD' + '|lkrn|Nanolinux|OSForensics|PING')), + ('slitaz', contains('minimal Slackware|Slackware-HOWTO')), + #('suse', contains('suse')), + ('opensuse-install', contains('class opensuse')), + ('ubuntu', contains('boot=casper')), + ('wifislax', contains('wifislax')), + ('slax', contains('slax')), + ('sms', [contains('sms.jpg|vector |autoexec'), + isolinux_bin_exists(True)]), + ('antix', contains('antix')), + ('porteus', contains('porteus')), + ('pclinuxos', contains('livecd=livecd|PCLinuxOS')), + ('gentoo', contains('looptype=squashfs|http://dee.su/liberte')), + ('finnix', contains('finnix')), + ('wifiway', contains('wifiway')), + ('puppy', contains('puppy|quirky|fatdog|slacko|xenialpup')), + ('ipcop', contains('ipcop')), + ('ipfire', contains('ipfire')), + ('salix-live', [contains('zenwalk|slack|salix'), + contains('live')]), + ('zenwalk', contains('zenwalk|slack|salix')), + ('ubuntu-server', contains('ubuntu server')), + ('centos-install', contains('Install CentOS')), + ('centos', contains('centos')), + ('trinity-rescue', contains('Trinity Rescue Kit')), + ('alpine', contains('alpine')), + ('kaspersky', contains('http://support.kaspersky.com')), + ('alt-linux', contains('ALT Linux')), + ('Windows', contains('Sergei Strelec')), + ('ReactOS', contains('ReactOS')), + ('fsecure', contains('fsecure')), + ('pc-unlocker', contains('default rwp')), + ('pc-tool', contains('/system/stage1')), + ('grub2only', contains('vba32rescue')), + ('rising-av', contains('BOOT_IMAGE=rising')), + ('Avira-RS', contains('Avira Rescue System')), + ('insert', contains('BOOT_IMAGE=insert')), + ('sgrubd2', contains('Super Grub Disk')), + ] + + + # I'm not sure if this platform check is necessary but I will + # avoid removal to not alter the behaviour. if platform.system() == "Linux" or platform.system() == "Windows": for path, subdirs, files in os.walk(iso_cfg_ext_dir): for name in files: - if name.endswith(('.cfg', '.CFG', '.txt', '.TXT', '.lst')): - if name.lower()=='i18n.cfg': - # i18n.cfg in salitaz-rolling cause misdetection - # of centos by the following line. - # MENU LABEL English US (acentos) - continue - try: - # errors='ignore' is required as some files also contain non utf character - string = open(os.path.join(path, name), errors='ignore').read() - except IOError: - return "Read Error." - else: - if any("f4ubcd" in s.lower() for s in iso_file_list): - return "f4ubcd" - if re.search(r'ubcd', string, re.I): - return "ubcd" - elif re.search(r'Super Grub Disk', string, re.I): - return "sgrubd2" - elif re.search(r'hbcd', string, re.I): - return "hbcd" - elif re.search(r'systemrescuecd', string, re.I): - return "systemrescuecd" - elif re.search(r'pmagic|partedmagic', string, re.I) and isolinux_bin_exist(iso_link): - return "parted-magic" - elif re.search(r'mgalive', string, re.I): # mounting fat filesystem hard coded in to initrd. - # Can be modified only under linux. - return "mageialive" - elif re.search(r'archisolabel|misolabel|parabolaisolabel', string, re.I): - return "arch" - elif re.search(r'chakraisolabel', string, re.I): - return "chakra" - elif re.search(r'kdeosisolabel', string, re.I): - return "kaos" - elif re.search(r'boot=live', string, re.I) and isolinux_bin_exist(iso_link): - return "debian" - elif re.search(r'grml', string, re.I) and re.search(r'live-media-path=', string, re.I): - return "grml" - elif re.search(r'debian-installer', string, re.I) and not any("casper" in s.lower() for s in iso_file_list): - return "debian-install" - elif re.search(r'solydx', string, re.I): - return "solydx" - elif re.search(r'knoppix', string, re.I): - return "knoppix" - elif re.search(r'root=live:CDLABEL=', string, re.I) or re.search(r'root=live:LABEL=', string, re.I): - return "fedora" - elif re.search(r'redcore', string, re.I): - return "fedora" - elif re.search(r'redhat', string, re.I): - return "redhat" - elif re.search( - r'slitaz|dban |ophcrack|tinycore|rescue.cpi|xpud|untangle|4mlinux|partition wizard|android-x86.png|' - r'riplinux|lebel dummy|http://pogostick.net/~pnh/ntpasswd/|AVG Rescue CD|AntivirusLiveCD|' - r'lkrn|Nanolinux|OSForensics|acentos', string, re.I): # acentos is a text found in slitaz distro - return "slitaz" - elif re.search(r'minimal Slackware|Slackware-HOWTO', string, re.I): - # for minimal slackware detection - return "slitaz" - # elif re.search(r'suse', string, re.I): - # return "suse" - elif re.search(r'class opensuse', string, re.I): - return "opensuse-install" - elif re.search(r'boot=casper', string, re.I): - return "ubuntu" - elif re.search(r'wifislax', string, re.I): - return "wifislax" - elif re.search(r'slax', string, re.I): - return "slax" - elif re.search(r'sms.jpg|vector |autoexec', string, re.I) and isolinux_bin_exist(iso_link): - return "sms" - elif re.search(r'antix', string, re.I): - return "antix" - elif re.search(r'porteus', string, re.I): - return "porteus" - elif re.search(r'livecd=livecd|PCLinuxOS', string, re.I): - return "pclinuxos" - elif re.search(r'looptype=squashfs|http://dee.su/liberte', string, re.I): - return "gentoo" - elif re.search(r'finnix', string, re.I): - return "finnix" - elif re.search(r'wifiway', string, re.I): - return "wifiway" - elif re.search(r'puppy|quirky|fatdog|slacko|xenialpup', string, re.I): - return "puppy" - elif re.search(r'ipcop', string, re.I): - return "ipcop" - elif re.search(r'ipfire', string, re.I): - return "ipfire" - elif re.search(r'zenwalk|slack|salix', string, re.I) and re.search(r'live', string, re.I): - return "salix-live" - elif re.search(r'zenwalk|slack|salix', string, re.I) and not re.search(r'slacko', string, re.I): - return "zenwalk" - elif re.search(r'ubuntu server', string, re.I): - return "ubuntu-server" - elif re.search(r'root=live:CDLABEL=CentOS', - string, re.I): - return 'centos' # centos-live - elif re.search(r'Install CentOS', string, re.I): - return "centos-install" - elif re.search(r'CentOS', string, re.I): - return "centos" - elif re.search(r'Trinity Rescue Kit', string, re.I): - return "trinity-rescue" - elif re.search(r'alpine', string, re.I): - return "alpine" - elif re.search(r'http://support.kaspersky.com', string, re.I): - return "kaspersky" - elif re.search(r'ALT Linux', string, re.I): - return "alt-linux" - elif re.search(r'Sergei Strelec', string, re.I): - return "Windows" - elif re.search(r'ReactOS', string, re.I): - return "ReactOS" - elif re.search(r'fsecure', string, re.I): - return "fsecure" - elif re.search(r'default rwp', string, re.I): - return "pc-unlocker" - elif re.search(r'/system/stage1', string, re.I): - return 'pc-tool' - elif re.search(r'vba32rescue', string, re.I): - return 'grub2only' - elif re.search(r'BOOT_IMAGE=rising', string, re.I): - return 'rising-av' - elif re.search(r'Avira Rescue System', string, re.I): - return 'Avira-RS' - elif any("alpine-release" in s.lower() for s in iso_file_list): - return 'alpine' - elif re.search(r'BOOT_IMAGE=insert', string, re.I): - return 'insert' - - distro = detect_iso_from_file_list(iso_link, iso_file_list) + name_lower = name.lower() + if not name_lower.endswith(('.cfg', '.txt', '.lst')): + continue + if name_lower=='i18n.cfg': + # i18n.cfg in salitaz-rolling cause misdetection + # of centos by the following line. + # MENU LABEL English US (acentos) + continue + try: + # errors='ignore' is required as some files also + # contain non utf character + string = open(os.path.join(path, name), + errors='ignore').read() + except IOError: + log("Read Error on %s." % name) + continue - if distro: - return distro - # FIXME: See the below comments. -# else: -# # FIXME: The idea of detecting as generic is to work like a unetbootin if other methods fails. -# # This simply extracts distro to root of the USB and install syslinux on isolinux.bin directory. -# # All works fine but unable to boot the distro successfully. Also, see the generic section from -# # syslinux, update_cfg and install_distro modules. -# if self.isolinux_bin_exist(): -# return "generic" - elif str(iso_link).lower().endswith('.iso'): - return 'memdisk_iso' - elif str(iso_link).lower().endswith('.img'): - return 'memdisk_img' - else: - return None - - -def detect_iso_from_file_list(iso_link, iso_file_list): + for distro_, predicate in test_vector: + predicates = [predicate] if callable(predicate) \ + else predicate + if all( p(name_lower, string.lower(), iso_file_list_lower, + v_isolinux_bin_exists) for p in predicates ): + return distro_ + + if distro: + return distro +# FIXME: See the below comments. +# else: +# # FIXME: The idea of detecting as generic is to work like a unetbootin if other methods fails. +# # This simply extracts distro to root of the USB and install syslinux on isolinux.bin directory. +# # All works fine but unable to boot the distro successfully. Also, see the generic section from +# # syslinux, update_cfg and install_distro modules. +# if self.isolinux_bin_exist(): +# return "generic" + elif str(iso_link).lower().endswith('.iso'): + return 'memdisk_iso' + elif str(iso_link).lower().endswith('.img'): + return 'memdisk_img' + else: + return None + + +def detect_iso_from_file_list(iso_file_list): """ Fallback detection script from the content of an ISO. :return: supported distro as string """ - if os.path.exists(iso_link): - if any("sources" in s.lower() for s in iso_file_list) and any("boot.wim" in s.lower() for s in iso_file_list): - return "Windows" - elif any("config.isoclient" in s.lower() for s in iso_file_list): - return "opensuse" - elif any("dban" in s.lower() for s in iso_file_list): - return "slitaz" - elif any("memtest.img" in s.lower() for s in iso_file_list): - return "memtest" - elif any("mt86.png" in s.lower() for s in iso_file_list) and any("isolinux" in s.lower() for s in iso_file_list): - return 'raw_iso' - elif any("menu.lst" in s.lower() for s in iso_file_list): - return "grub4dos" - elif any("bootwiz.cfg" in s.lower() for s in iso_file_list) and any("bootmenu_logo.png" in s.lower() for s in iso_file_list): - return "grub4dos_iso" - else: - log(iso_file_list) + keys_to_distro = [ + (['f4ubcd'], 'f4ubcd'), + (['alpine-release'], 'alpine'), + (['sources', 'boot.wim'], 'Windows'), + (['config.isoclient'], 'opensuse'), + (['dban'], 'slitaz'), + (['memtest.img'], 'memtest'), + (['mt86.png', 'isolinux'], 'raw_iso'), + (['menu.lst'], 'grub4dos'), + (['bootwiz.cfg', 'bootmenu_logo.png'], 'grub4dos_iso') ] + + filenames = [f.lower() for f in iso_file_list] + for keys, distro in keys_to_distro: + if all(k in filenames for k in keys): + return distro + #log("Examined %d %s in the iso but could not determine the distro." + # % (len(filenames), len(filenames)==1 and 'filename' or 'filenames')) + return None + + +def perform_strict_detections(iso_cfg_ext_dir, iso_file_list): + + def run_contains(filepath, keyword, cfg_dir=iso_cfg_ext_dir): + fullpath = os.path.join(cfg_dir, filepath.replace('/', os.sep)) + if not os.path.exists(fullpath): + return False + try: + with open(fullpath, 'rb') as f: + data = f.read().lower() + return keyword in data + except (IOError, OSError): + log("Failed to open %s" % fullpath) + return False + + def contains(relataive_filepath, keyword): + return partial(run_contains, relataive_filepath, + bytes(keyword.lower(),'us-ascii')) + + # contains(P, K) predicates that file P contains the specified + # string K. The predicate get never asserted if P has not been + # extracted into the staging area (iso_cfg_ext_dir). + test_vector = [ + ('wifislax', contains('boot/syslinux/menu/vesamenu.cfg', + 'menu label Wifislax64 Live')), + ('salix-live', contains('boot/menus/mainmenu.cfg', + 'MENU LABEL SALIX LIVE')), + ('grml', contains('boot/isolinux/vesamenu.cfg', + 'menu title Grml - Live Linux')) + ] + for distro, predicate in test_vector: + predicates = [predicate] if callable(predicate) else predicate + if all(p() for p in predicates): + return distro + return None + if __name__ == '__main__': iso_cfg_ext_dir = os.path.join(multibootusb_host_dir(), "iso_cfg_ext_dir") diff --git a/scripts/gen.py b/scripts/gen.py index 03362e6..9da8807 100644 --- a/scripts/gen.py +++ b/scripts/gen.py @@ -13,75 +13,17 @@ import platform import shutil import string import zipfile -import tempfile import re import ctypes +from . import config +from .osdriver import get_physical_disk_number, wmi_get_drive_info, \ + log, resource_path, multibootusb_host_dir def scripts_dir_path(): return os.path.dirname(os.path.realpath(__file__)) -def log(message, info=True, error=False, debug=False, _print=True): - """ - Dirty function to log messages to file and also print on screen. - :param message: - :param info: - :param error: - :param debug: - :return: - """ - # LOG_FILE_PATH = os.path.join(multibootusb_host_dir(), 'multibootusb.log') - LOG_FILE_PATH = mbusb_log_file() - if os.path.exists(LOG_FILE_PATH): - log_file_size = os.path.getsize(LOG_FILE_PATH) / (1024.0 * 1024.0) - if log_file_size > 1: - print('Removing log file as it crosses beyond 1mb') - os.remove(LOG_FILE_PATH) - logging.basicConfig(filename=LOG_FILE_PATH, - filemode='a', - format='%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s', - datefmt='%H:%M:%S', - level=logging.DEBUG) - if _print is True: - print(message) - - # remove ANSI color codes from logs - # message_clean = re.compile(r'\x1b[^m]*m').sub('', message) - - if info is True: - logging.info(message) - elif error is not False: - logging.error(message) - elif debug is not False: - logging.debug(message) - - - -def resource_path(relativePath): - """ - Function to detect the correct path of file when working with sourcecode/install or binary. - :param relativePath: Path to file/data. - :return: Modified path to file/data. - """ - - try: - basePath = sys._MEIPASS # Try if we are running as standalone executable - # log('Running stand alone executable.') - except: - basePath = '/usr/share/multibootusb' # Check if we run in installed environment - #if os.path.exists('/usr/share/multibootusb'): - #log('Running from installed machine.') - if not os.path.exists(basePath): - #basePath = os.path.abspath(".") - basePath = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - - for dir_ in [basePath, os.path.abspath('.'), os.path.abspath('..')]: - fullpath = os.path.join(dir_, relativePath) - if os.path.exists(fullpath): - return fullpath - log("Could not find resource '%s'." % relativePath) - def print_version(): """ Simple log the version number of the multibootusb application @@ -128,37 +70,6 @@ def sys_64bits(): return sys.maxsize > 2**32 -def mbusb_log_file(): - """ - Function to genrate path to log file. - Under linux path is created as /tmp/multibootusb.log - Under Windows the file is created under installation directory - """ - if platform.system() == "Linux": - # home_dir = os.path.expanduser('~') - # log_file = os.path.join(home_dir, "multibootusb.log") - log_file = os.path.join(tempfile.gettempdir(), "multibootusb.log") - elif platform.system() == "Windows": - # log_file = os.path.join(tempfile.gettempdir(), "multibootusb", "multibootusb.log") - log_file = os.path.join("multibootusb.log") - - return log_file - - -def multibootusb_host_dir(): - """ - Cross platform way to detect multibootusb directory on host system. - :return: Path to multibootusb directory of host system. - """ - if platform.system() == "Linux": - home_dir = os.path.expanduser('~') - mbusb_dir = os.path.join(home_dir, ".multibootusb") - elif platform.system() == "Windows": - mbusb_dir = os.path.join(tempfile.gettempdir(), "multibootusb") - - return mbusb_dir - - def iso_cfg_ext_dir(): """ Function to return the path to ISO configuration file extraction directory. @@ -174,12 +85,12 @@ def clean_iso_cfg_ext_dir(iso_cfg_ext_dir): :return: """ if os.path.exists(iso_cfg_ext_dir): - filelist = [f for f in os.listdir(iso_cfg_ext_dir)] - for f in filelist: - if os.path.isdir(os.path.join(iso_cfg_ext_dir, f)): - shutil.rmtree(os.path.join(iso_cfg_ext_dir, f)) + for f in os.listdir(iso_cfg_ext_dir): + fullpath = os.path.join(iso_cfg_ext_dir, f) + if os.path.isdir(fullpath): + shutil.rmtree(fullpath) else: - os.remove(os.path.join(iso_cfg_ext_dir, f)) + os.remove(fullpath) else: log('iso_cfg_ext_dir directory does not exist.') @@ -190,6 +101,7 @@ def copy_mbusb_dir_usb(usb_disk): :param usb_mount_path: Path to USB mount. :return: """ +<<<<<<< HEAD # from .iso import iso_size from .usb import details, PartitionNotMounted @@ -199,6 +111,10 @@ def copy_mbusb_dir_usb(usb_disk): log(str(e)) return False +======= + + usb_details = config.usb_details +>>>>>>> upstream/master usb_mount_path = usb_details['mount_point'] result = '' if not os.path.exists(os.path.join(usb_mount_path, "multibootusb")): @@ -304,6 +220,7 @@ def strings(filename, _min=4): def size_not_enough(iso_link, usb_disk): from .iso import iso_size +<<<<<<< HEAD from .usb import details, PartitionNotMounted isoSize = iso_size(iso_link) try: @@ -311,6 +228,10 @@ def size_not_enough(iso_link, usb_disk): except PartitionNotMounted as e: log(str(e)) return False +======= + isoSize = iso_size(iso_link) + usb_details = config.usb_details +>>>>>>> upstream/master usb_size = usb_details['size_free'] return bool(isoSize > usb_size) @@ -497,6 +418,7 @@ class MemoryCheck(): """ totalMemory = os.popen("free -m").readlines()[1].split()[1] return int(totalMemory) +<<<<<<< HEAD def wmi_get_drive_info(usb_disk): assert platform.system() == 'Windows' @@ -519,6 +441,9 @@ def get_physical_disk_number(usb_disk): partition, logical_disk = wmi_get_drive_info(usb_disk) log("Physical Device Number is %d" % partition.DiskIndex) return partition.DiskIndex +======= + +>>>>>>> upstream/master if __name__ == '__main__': log(quote("""Test-string""")) diff --git a/scripts/grub.py b/scripts/grub.py index ea06a1b..01383d6 100644 --- a/scripts/grub.py +++ b/scripts/grub.py @@ -197,10 +197,13 @@ def write_to_file(file_path, _strings): def locate_kernel_file(subpath, isolinux_dir): subpath_original = subpath - if subpath[0] != '/': - gen.log("Accepting a relative kernel/initrd path '%s' as is." - % subpath) - return subpath + # Looks like relative paths don't work in grub. + #if subpath[0] != '/': + # gen.log("Accepting a relative kernel/initrd path '%s' as is." + # % subpath) + # return subpath + if subpath[:1] != '/': + subpath = '/' + subpath if os.path.exists(os.path.join(config.usb_mount, subpath[1:])): gen.log("Accepting kernel/initrd path '%s' as it exists." % subpath) return subpath @@ -212,7 +215,7 @@ def locate_kernel_file(subpath, isolinux_dir): subpath = subpath[len(drive_relative_prefix):] gen.log("Trying to locate kernel/initrd file '%s'" % subpath) for d in [ - os.path.join('multibootusb', _iso_basename, isolinux_dir), + os.path.join('multibootusb', _iso_basename, isolinux_dir or ''), # Down below are dire attemps to find. os.path.join('multibootusb', _iso_basename), os.path.join('multibootusb', _iso_basename, 'arch'), @@ -292,7 +295,7 @@ def iso2grub2(install_dir, loopback_cfg_path): gen.log('loopback.cfg file is set to ' + loopback_cfg_path) iso_bin_dir = iso.isolinux_bin_dir(config.image_path) - seen_menu_lines = [] + seen_menu_entries = [] # Loop though the distro installed directory for finding config files for dirpath, dirnames, filenames in os.walk(install_dir): for f in filenames: @@ -319,8 +322,8 @@ def iso2grub2(install_dir, loopback_cfg_path): # Append the block after the last matching position matching_blocks.append(data[matching_blocks_re[-1].span()[1]:]) else: - m = re.match('^(label)(.*?)', - data, re.I|re.DOTALL|re.MULTILINE) + m = re.search('^(label)(.*?)', + data, re.I|re.DOTALL|re.MULTILINE) matching_blocks = m and [data[m.start():]] or [] if not matching_blocks: @@ -342,7 +345,7 @@ def iso2grub2(install_dir, loopback_cfg_path): menu_labels = [v for v in matches if v[0].lower()=='menu label'] if 0 == len(labels) + len(menu_labels): gen.log('Warning: found a block without menu-entry.') - menu_line = 'menuentry "Anonymous"' + menu_entry = 'Anonymous' menu_label = 'Unlabeled' else: for vec, name in [ (labels, 'label'), @@ -355,13 +358,14 @@ def iso2grub2(install_dir, loopback_cfg_path): value = menu_labels[0][1].replace('^', '') else: value = labels[0][1] - menu_line = 'menuentry ' + gen.quote(value) - menu_label = value + menu_entry = menu_label = value # Extract lines containing 'kernel','linux','initrd' # or 'append' to convert them into grub2 compatible ones. linux_line = initrd_line = None appends = [] + sought_archs = ['x86_64', 'i686', 'i386'] + arch = [] for keyword, value in re.findall( r'^\s*(kernel|linux|initrd|append)[= ](.*)$', matching_block, re.I|re.MULTILINE): @@ -372,6 +376,8 @@ def iso2grub2(install_dir, loopback_cfg_path): "'kernel/linux' lines in block '%s'." % menu_label) continue + arch = [(value.find(a), a) for a in sought_archs + if 0 <= value.find(a)] linux_line = 'linux ' + \ tweak_bootfile_path(value, iso_bin_dir) elif kw == 'initrd': @@ -393,13 +399,15 @@ def iso2grub2(install_dir, loopback_cfg_path): "specifications in block '%s'." % menu_label) initrd_line = new_initrd_line - - if menu_line in seen_menu_lines: + if arch: # utilize left most arch. + menu_entry += (" (%s)" % sorted(arch)[-1][1]) + menu_line = 'menuentry ' + gen.quote(menu_entry) + if menu_entry in seen_menu_entries: out_lines.append( "# '%s' is superceded by the previous " "definition." % menu_label) else: if linux_line or initrd_line: - seen_menu_lines.append(menu_line) + seen_menu_entries.append(menu_entry) out_lines.append(menu_line + ' {') for starter, value in [ (linux_line, ' '.join(appends)), diff --git a/scripts/gui/about.ui b/scripts/gui/about.ui index be6770e..e2b74a9 100644 --- a/scripts/gui/about.ui +++ b/scripts/gui/about.ui @@ -6,8 +6,8 @@ 0 0 - 498 - 369 + 626 + 393 @@ -25,7 +25,7 @@ - <html><head/><body><p align="center">An advanced bootable usb creator with option to install/uninstall multiple distros.</p><p align="center">This software is written in Python and PyQt. </p><p align="center">Copyright 2010-2017 Sundar</p><p align="center"><span style=" font-weight:600;">Author(s)</span>: Sundar, Ian Bruce, LiQiong Lee and Alin Trăistaru (alindt)</p><p align="center"><span style=" font-weight:600;">Licence</span>: GPL version 2 or later</p><p align="center"><span style=" font-weight:600;">Home page</span>: <a href="http://multibootusb.org"><span style=" text-decoration: underline; color:#0000ff;">http://multibootusb.org</span></a></p><p align="center"><span style=" font-weight:600;">Help/Email</span>: feedback.multibootusb@gmail.com</p><p align="center"><span style=" font-weight:600;">Source Code</span>: <a href="https://github.com/mbusb/multibootusb"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/mbusb/multibootusb</span></a></p><p><br/></p></body></html> + <html><head/><body><p align="center">An advanced bootable usb creator with option to install/uninstall multiple distros.</p><p align="center">This software is written in Python and PyQt. </p><p align="center">Copyright 2010-2018 Sundar</p><p align="center"><span style=" font-weight:600;">Author(s)</span>: Sundar, Ian Bruce, LiQiong Lee, Alin Trăistaru (alindt) and Shinji Suzuki</p><p align="center"><span style=" font-weight:600;">Licence</span>: GPL version 2 or later</p><p align="center"><span style=" font-weight:600;">Home page</span>: <a href="http://multibootusb.org"><span style=" text-decoration: underline; color:#0000ff;">http://multibootusb.org</span></a></p><p align="center"><span style=" font-weight:600;">Help/Email</span>: feedback.multibootusb@gmail.com</p><p align="center"><span style=" font-weight:600;">Source Code</span>: <a href="https://github.com/mbusb/multibootusb"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/mbusb/multibootusb</span></a></p><p><br/></p></body></html> diff --git a/scripts/gui/multibootusb.ui b/scripts/gui/multibootusb.ui index c64063c..56e67f9 100644 --- a/scripts/gui/multibootusb.ui +++ b/scripts/gui/multibootusb.ui @@ -6,8 +6,8 @@ 0 0 - 629 - 605 + 865 + 609 @@ -23,16 +23,7 @@ - - 5 - - - 5 - - - 5 - - + 5 @@ -308,7 +299,7 @@ false - 1 + 4 @@ -318,30 +309,12 @@ MultiBootUSB - - 0 - - - 0 - - - 0 - - + 0 - - 5 - - - 5 - - - 5 - - + 5 @@ -457,33 +430,35 @@ Write Image to disk - - 0 - - - 0 - - - 0 - - + 0 - + 5 - - 5 - - - 5 - - - 5 - - + + + + Write image to USB + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + Qt::Horizontal @@ -496,32 +471,16 @@ - - - - Write image to USB - - - - + - - 5 - - - 5 - - - 5 - - + 5 - + Qt::Horizontal @@ -534,7 +493,7 @@ - + Qt::Vertical @@ -567,20 +526,7 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - + <html><head/><body><p align="justify"><span style=" font-weight:600; color:#ff0000;">WARNING</span> : Any bootable USB made using<span style=" font-weight:600;"> ISO Imager will destroy all data </span>on the selected USB disk. </p><p align="justify">Use it at your own risk. Developers are not responsile for loss of any data.</p></body></html> @@ -945,6 +891,164 @@ + + + true + + + Check Filesystem + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Repair Filesystem + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + true + + + + 0 + 0 + + + + + 0 + 0 + + + + QFrame::Box + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Use this function to check filesystem consistency</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">and repair. Please take a backup before attempting</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">to use tis function. Users will be presented with</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">two message dialogs as repair is currently </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">performed in two steps.</p> +<p align="justify" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Repair Filesystem + + + + + + + + + + Check Filesystem + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + QFrame::Box + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Checking filesystem integrity from time to</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">time is recommended to make sure the integrity</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">is always maintained. Some distros don't </p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">cleanly unmount a usb stick when booting off of</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">it, that will be also detected by this check.</p> +<p align="justify" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Check Filesystem Integrity + + + + + + + + @@ -964,7 +1068,7 @@ 0 0 - 629 + 865 21 diff --git a/scripts/gui/ui_multibootusb.py b/scripts/gui/ui_multibootusb.py index 138c48d..419eeb0 100644 --- a/scripts/gui/ui_multibootusb.py +++ b/scripts/gui/ui_multibootusb.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'gui/multibootusb.ui' +# Form implementation generated from reading ui file 'scripts/gui/multibootusb.ui' # -# Created by: PyQt5 UI code generator 5.5.1 +# Created by: PyQt5 UI code generator 5.6 # # WARNING! All changes made in this file will be lost! @@ -11,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") - MainWindow.resize(629, 605) + MainWindow.resize(865, 609) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -216,33 +216,38 @@ class Ui_MainWindow(object): self.gridLayout_9 = QtWidgets.QGridLayout() self.gridLayout_9.setContentsMargins(5, 5, 5, 5) self.gridLayout_9.setObjectName("gridLayout_9") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.gridLayout_9.addItem(spacerItem, 3, 2, 1, 1) self.button_write_image_to_disk = QtWidgets.QPushButton(self.tab_imager) self.button_write_image_to_disk.setObjectName("button_write_image_to_disk") - self.gridLayout_9.addWidget(self.button_write_image_to_disk, 3, 1, 1, 1) + self.gridLayout_9.addWidget(self.button_write_image_to_disk, 2, 1, 1, 1) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_9.addItem(spacerItem, 4, 1, 1, 1) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_9.addItem(spacerItem1, 5, 2, 1, 1) self.widget_7 = QtWidgets.QWidget(self.tab_imager) self.widget_7.setObjectName("widget_7") self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.widget_7) self.verticalLayout_6.setContentsMargins(5, 5, 5, 5) self.verticalLayout_6.setObjectName("verticalLayout_6") - self.gridLayout_9.addWidget(self.widget_7, 3, 3, 1, 2) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.gridLayout_9.addItem(spacerItem1, 3, 0, 1, 1) - spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout_9.addItem(spacerItem2, 4, 1, 1, 1) + self.gridLayout_9.addWidget(self.widget_7, 5, 3, 1, 2) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_9.addItem(spacerItem2, 5, 0, 1, 1) + spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_9.addItem(spacerItem3, 6, 1, 1, 1) self.label_6 = QtWidgets.QLabel(self.tab_imager) self.label_6.setObjectName("label_6") self.gridLayout_9.addWidget(self.label_6, 1, 1, 1, 1) - spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout_9.addItem(spacerItem3, 0, 1, 1, 1) spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout_9.addItem(spacerItem4, 2, 1, 1, 1) + self.gridLayout_9.addItem(spacerItem4, 0, 1, 1, 1) + self.label_waning = QtWidgets.QLabel(self.tab_imager) + self.label_waning.setWordWrap(False) + self.label_waning.setObjectName("label_waning") + self.gridLayout_9.addWidget(self.label_waning, 3, 1, 1, 1) self.horizontalLayout_7.addLayout(self.gridLayout_9) self.tabWidget.addTab(self.tab_imager, "") self.tab_syslinux = QtWidgets.QWidget() self.tab_syslinux.setObjectName("tab_syslinux") self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.tab_syslinux) + self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_3.setObjectName("horizontalLayout_3") self.gridLayout_2 = QtWidgets.QGridLayout() self.gridLayout_2.setObjectName("gridLayout_2") @@ -291,6 +296,7 @@ class Ui_MainWindow(object): self.tab_testboot = QtWidgets.QWidget() self.tab_testboot.setObjectName("tab_testboot") self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.tab_testboot) + self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) self.verticalLayout_2.setObjectName("verticalLayout_2") self.gridLayout_6 = QtWidgets.QGridLayout() self.gridLayout_6.setContentsMargins(-1, 10, -1, -1) @@ -361,6 +367,58 @@ class Ui_MainWindow(object): spacerItem13 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout_2.addItem(spacerItem13) self.tabWidget.addTab(self.tab_testboot, "") + self.tab_fsck = QtWidgets.QWidget() + self.tab_fsck.setEnabled(True) + self.tab_fsck.setObjectName("tab_fsck") + self.formLayout_5 = QtWidgets.QFormLayout(self.tab_fsck) + self.formLayout_5.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + self.formLayout_5.setContentsMargins(0, 0, 0, 0) + self.formLayout_5.setObjectName("formLayout_5") + self.groupBox_6 = QtWidgets.QGroupBox(self.tab_fsck) + self.groupBox_6.setObjectName("groupBox_6") + self.gridLayout_5 = QtWidgets.QGridLayout(self.groupBox_6) + self.gridLayout_5.setObjectName("gridLayout_5") + spacerItem14 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_5.addItem(spacerItem14, 0, 0, 1, 1) + self.label_8 = QtWidgets.QLabel(self.groupBox_6) + self.label_8.setEnabled(True) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_8.sizePolicy().hasHeightForWidth()) + self.label_8.setSizePolicy(sizePolicy) + self.label_8.setMinimumSize(QtCore.QSize(0, 0)) + self.label_8.setFrameShape(QtWidgets.QFrame.Box) + self.label_8.setObjectName("label_8") + self.gridLayout_5.addWidget(self.label_8, 1, 0, 1, 1) + spacerItem15 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_5.addItem(spacerItem15, 2, 0, 1, 1) + self.run_fsck_repair = QtWidgets.QPushButton(self.groupBox_6) + self.run_fsck_repair.setObjectName("run_fsck_repair") + self.gridLayout_5.addWidget(self.run_fsck_repair, 3, 0, 1, 1) + self.formLayout_5.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.groupBox_6) + self.groupBox = QtWidgets.QGroupBox(self.tab_fsck) + self.groupBox.setObjectName("groupBox") + self.gridLayout_10 = QtWidgets.QGridLayout(self.groupBox) + self.gridLayout_10.setObjectName("gridLayout_10") + spacerItem16 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_10.addItem(spacerItem16, 0, 0, 1, 1) + self.label_7 = QtWidgets.QLabel(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_7.sizePolicy().hasHeightForWidth()) + self.label_7.setSizePolicy(sizePolicy) + self.label_7.setFrameShape(QtWidgets.QFrame.Box) + self.label_7.setObjectName("label_7") + self.gridLayout_10.addWidget(self.label_7, 1, 0, 1, 1) + spacerItem17 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_10.addItem(spacerItem17, 2, 0, 1, 1) + self.run_fsck_check = QtWidgets.QPushButton(self.groupBox) + self.run_fsck_check.setObjectName("run_fsck_check") + self.gridLayout_10.addWidget(self.run_fsck_check, 3, 0, 1, 1) + self.formLayout_5.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.groupBox) + self.tabWidget.addTab(self.tab_fsck, "") self.verticalLayout_7.addWidget(self.tabWidget) self.progressbar = QtWidgets.QProgressBar(self.centralwidget) self.progressbar.setProperty("value", 0) @@ -369,7 +427,7 @@ class Ui_MainWindow(object): self.verticalLayout_7.addWidget(self.progressbar) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 629, 19)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 865, 21)) self.menubar.setNativeMenuBar(True) self.menubar.setObjectName("menubar") self.menuFile = QtWidgets.QMenu(self.menubar) @@ -390,7 +448,7 @@ class Ui_MainWindow(object): self.menubar.addAction(self.menu_Help.menuAction()) self.retranslateUi(MainWindow) - self.tabWidget.setCurrentIndex(1) + self.tabWidget.setCurrentIndex(4) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -421,6 +479,7 @@ class Ui_MainWindow(object): self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_multibootusb), _translate("MainWindow", "MultiBootUSB")) self.button_write_image_to_disk.setText(_translate("MainWindow", "Write image to USB")) self.label_6.setText(_translate("MainWindow", "

WARNING!

This operation destroys ALL data on the selected disk.

Please select the destination disk carefully.

")) + self.label_waning.setText(_translate("MainWindow", "

WARNING : Any bootable USB made using ISO Imager will destroy all data on the selected USB disk.

Use it at your own risk. Developers are not responsile for loss of any data.

")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_imager), _translate("MainWindow", "Write Image to disk")) self.groupBox_2.setTitle(_translate("MainWindow", "Install Syslinux")) self.check_install_sys_only.setText(_translate("MainWindow", "Install only syslinu&x (existing configurations will not be altered).")) @@ -451,6 +510,33 @@ class Ui_MainWindow(object): self.combo_iso_boot_ram.setItemText(5, _translate("MainWindow", "2048")) self.label.setText(_translate("MainWindow", "MB")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_testboot), _translate("MainWindow", "Boot ISO/USB")) + self.groupBox_6.setTitle(_translate("MainWindow", "Repair Filesystem")) + self.label_8.setText(_translate("MainWindow", "\n" +"\n" +"


\n" +"

Use this function to check filesystem consistency

\n" +"

and repair. Please take a backup before attempting

\n" +"

to use tis function. Users will be presented with

\n" +"

two message dialogs as repair is currently

\n" +"

performed in two steps.

\n" +"


")) + self.run_fsck_repair.setText(_translate("MainWindow", "Repair Filesystem")) + self.groupBox.setTitle(_translate("MainWindow", "Check Filesystem")) + self.label_7.setText(_translate("MainWindow", "\n" +"\n" +"


\n" +"

Checking filesystem integrity from time to

\n" +"

time is recommended to make sure the integrity

\n" +"

is always maintained. Some distros don\'t

\n" +"

cleanly unmount a usb stick when booting off of

\n" +"

it, that will be also detected by this check.

\n" +"


")) + self.run_fsck_check.setText(_translate("MainWindow", "Check Filesystem Integrity")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_fsck), _translate("MainWindow", "Check Filesystem")) self.menuFile.setTitle(_translate("MainWindow", "&File")) self.menu_Help.setTitle(_translate("MainWindow", "&Help")) self.action_Quit.setText(_translate("MainWindow", "&Quit")) diff --git a/scripts/imager.py b/scripts/imager.py index 5862502..a136a77 100644 --- a/scripts/imager.py +++ b/scripts/imager.py @@ -7,32 +7,41 @@ # under the terms of GNU General Public License, v.2 or above # WARNING : Any boot-able USB made using this module will destroy data stored on USB disk. -import os -import subprocess import collections +import io +import os import platform import signal +import time +import subprocess +import traceback + from PyQt5 import QtWidgets from .gui.ui_multibootusb import Ui_MainWindow from .gen import * -from . import iso from . import config +from . import iso +from . import osdriver from . import progressbar +from . import osdriver +from . import usb + if platform.system() == "Windows": import win32com.client - -def dd_linux(): - import time - _input = "if=" + config.image_path - in_file_size = float(os.path.getsize(config.image_path)) - _output = "of=" + config.usb_disk - os.system("umount " + config.usb_disk + "1") - command = ['dd', _input, _output, "bs=1M", "oflag=sync"] - log("Executing ==> " + " ".join(command)) - dd_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - +def dd_iso_image(dd_progress_thread): + try: + dd_progress_thread.set_error(None) + _dd_iso_image(dd_progress_thread) + except: + # config.imager_return = False + o = io.StringIO() + traceback.print_exc(None, o) + log(o.getvalue()) + dd_progress_thread.set_error(o.getvalue()) + +def _dd_iso_image(dd_progress_thread): pbar = progressbar.ProgressBar( maxval=100, widgets=[ @@ -43,52 +52,34 @@ def dd_linux(): ] ).start() - while dd_process.poll() is None: - time.sleep(0.1) # If this time delay is not given, the Popen does not execute the actual command - dd_process.send_signal(signal.SIGUSR1) - dd_process.stderr.flush() - while True: - time.sleep(0.1) - out_error = dd_process.stderr.readline().decode() - if out_error: - if 'bytes' in out_error: - copied = int(out_error.split(' ', 1)[0]) - config.imager_percentage = round((float(copied) / float(in_file_size) * 100)) - pbar.update(config.imager_percentage) - break - - if dd_process.poll() is not None: - log("\nExecuting ==> sync") - os.sync() - log("ISO has been written to USB disk...") - return - - -def dd_win(): - - windd = resource_path(os.path.join("data", "tools", "dd", "dd.exe")) - if os.path.exists(resource_path(os.path.join("data", "tools", "dd", "dd.exe"))): - log("dd exist") - _input = "if=" + config.image_path - in_file_size = float(os.path.getsize(config.image_path) / 1024 / 1024) - _output = "of=\\\.\\" + config.usb_disk - command = [windd, _input, _output, "bs=1M", "--progress"] - log("Executing ==> " + " ".join(command)) - dd_process = subprocess.Popen(command, universal_newlines=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, - shell=False) - while dd_process.poll() is None: - for line in iter(dd_process.stderr.readline, ''): - line = line.strip() - if 'error' in line.lower() or 'invalid' in line.lower(): - log("Error writing to disk...") - break - if line and line[-1] == 'M': - copied = float(line.strip('M').replace(',', '')) - config.imager_percentage = round((copied / float(in_file_size) * 100)) - - log("ISO has been written to USB disk...") - - return + + def gui_update(percentage): + config.imager_percentage = percentage + pbar.update(percentage) + + def status_update(text): + config.status_text = text + + mounted_partitions = osdriver.find_mounted_partitions_on(config.usb_disk) + unmounted = [] + try: + for x in mounted_partitions: + partition_dev, mount_point = x[:2] + c = usb.UnmountedContext(partition_dev, config.update_usb_mount) + c.__enter__() + unmounted.append((c, partition_dev)) + error = osdriver.dd_iso_image( + config.image_path, config.usb_disk, gui_update, status_update) + if error: + dd_progress_thread.set_error(error) + log('Error writing iso image...') + # config.imager_return = False + else: + log('ISO has been written to USB disk...') + # config.imager_return = True + finally: + for c, partition_dev in unmounted: + c.__exit__(None, None, None) class Imager(QtWidgets.QMainWindow, Ui_MainWindow): @@ -152,42 +143,34 @@ class Imager(QtWidgets.QMainWindow, Ui_MainWindow): return disk @staticmethod - def imager_usb_detail(usb_disk, partition=1): + def imager_usb_detail(physical_disk): """ Function to detect details of USB disk using lsblk - :param usb_disk: path to usb disk - :param partition: by default partition is set (but yet to code for it) + :param physical_disk: /dev/sd? (linux) or integer disk number (win) :return: details of size, type and model as tuples """ - _ntuple_diskusage = collections.namedtuple('usage', 'total_size usb_type model') + _ntuple_diskusage = collections.namedtuple( + 'usage', 'total_size usb_type model') if platform.system() == "Linux": - output = subprocess.check_output("lsblk -ib " + usb_disk, shell=True) + output = subprocess.check_output("lsblk -ib " + physical_disk, + shell=True) for line in output.splitlines(): line = line.split() - if partition != 1: - if line[2].strip() == b'1' and line[5].strip() == b'disk': - total_size = line[3] - if not total_size: - total_size = "Unknown" - usb_type = "Removable" - model = subprocess.check_output("lsblk -in -f -o MODEL " + usb_disk, shell=True).decode().strip() - if not model: - model = "Unknown" + if line[2].strip() == b'1' and line[5].strip() == b'disk': + total_size = line[3] + if not total_size: + total_size = "Unknown" + usb_type = "Removable" + model = subprocess.check_output( + "lsblk -in -f -o MODEL " + physical_disk, + shell=True).decode().strip() + if not model: + model = "Unknown" else: - try: - selected_usb_part = str(usb_disk) - oFS = win32com.client.Dispatch("Scripting.FileSystemObject") - d = oFS.GetDrive(oFS.GetDriveName(oFS.GetAbsolutePathName(selected_usb_part))) -# selected_usb_device = d.DriveLetter - label = (d.VolumeName).strip() - if not label.strip(): - label = "No label." - total_size = d.TotalSize - usb_type = "Removable" - model = label - except: - log("Error detecting USB details.") + dinfo = osdriver.wmi_get_physicaldrive_info_ex(physical_disk) + return _ntuple_diskusage(*[dinfo[a] for a in [ + 'size_total', 'mediatype', 'model']]) return _ntuple_diskusage(total_size, usb_type, model) diff --git a/scripts/install.py b/scripts/install.py index 7b50d4c..a399290 100644 --- a/scripts/install.py +++ b/scripts/install.py @@ -6,11 +6,12 @@ # Licence: This file is a part of multibootusb package. You can redistribute it or modify # under the terms of GNU General Public License, v.2 or above +import lzma import os -import shutil import platform -import threading +import shutil import subprocess +import threading import time from .usb import * from .gen import * @@ -76,10 +77,10 @@ def install_distro(): config.status_text = "Copying ISO..." iso.iso_extract_file(config.image_path, install_dir, "kernel") copy_iso(config.image_path, install_dir) - elif config.distro == "salix-live": + elif config.distro in ["salix-live", 'wifislax']: # iso.iso_extract_file(config.image_path, install_dir, "boot") iso.iso_extract_file(config.image_path, install_dir, - ['*syslinux', '*menus', '*vmlinuz', '*initrd*', + ['*syslinux', '*isolinux', '*system_tools', '*menus', '*vmlinuz', '*initrd*', 'EFI']) iso.iso_extract_file(config.image_path, usb_mount, ['*modules', '*packages', '*optional', @@ -125,9 +126,6 @@ def install_distro(): else: iso.iso_extract_full(config.image_path, install_dir) - if platform.system() == 'Linux': - log('ISO extracted successfully. Sync is in progress...') - os.sync() if config.persistence != 0: log('Creating persistence...') @@ -167,6 +165,10 @@ def install_progress(): log(str(e)) return +<<<<<<< HEAD +======= + usb_details = config.usb_details +>>>>>>> upstream/master config.usb_mount = usb_details['mount_point'] usb_size_used = usb_details['size_used'] thrd = threading.Thread(target=install_distro, name="install_progress") @@ -186,48 +188,74 @@ def install_progress(): time.sleep(0.1) +def replace_syslinux_modules(syslinux_version, under_this_dir): + # Replace modules files extracted from iso with corresponding + # version provided by multibootusb. + modules_src_dir = os.path.join( + multibootusb_host_dir(), "syslinux", "modules", syslinux_version) + + for dirpath, dirnames, filenames in os.walk(under_this_dir): + for fname in filenames: + if not fname.lower().endswith('.c32'): + continue + dst_path = os.path.join(under_this_dir, dirpath, fname) + src_path = os.path.join(modules_src_dir, fname) + if not os.path.exists(src_path): + log("Suitable replacement of '%s' is not bundled. " + "Trying to unlzma." % fname) + try: + with lzma.open(dst_path) as f: + expanded = f.read() + except lzma.LZMAError: + continue + except (OSError, IOError) as e: + log("%s while accessing %s." % (e, dst_path)) + continue + with open(dst_path, 'wb') as f: + f.write(expanded) + log("Successfully decompressed %s." % fname) + continue + try: + os.remove(dst_path) + shutil.copy(src_path, dst_path) + log("Replaced %s module" % fname) + except (OSError, IOError) as err: + log(err) + log("Could not update " + fname) + def install_patch(): """ Function to certain distros which uses makeboot.sh script for making bootable usb disk. This is required to make sure that same version (32/64 bit) of modules present is the isolinux directory :return: """ - if config.distro == 'debian': - if platform.system() == 'Linux': # Need to syn under Linux. Otherwise, USB disk becomes random read only. - os.sync() - iso_cfg_ext_dir = os.path.join(multibootusb_host_dir(), "iso_cfg_ext_dir") - isolinux_path = os.path.join(iso_cfg_ext_dir, isolinux_bin_path(config.image_path)) -# iso_linux_bin_dir = isolinux_bin_dir(config.image_path) - config.syslinux_version = isolinux_version(isolinux_path) - iso_file_list = iso.iso_file_list(config.image_path) + isobin_path = isolinux_bin_path(config.image_path) + if not isobin_path: + return + iso_cfg_ext_dir = os.path.join(multibootusb_host_dir(), + "iso_cfg_ext_dir") + isolinux_path = os.path.join(iso_cfg_ext_dir, isobin_path) +# iso_linux_bin_dir = isolinux_bin_dir(config.image_path) + distro_install_dir = os.path.join( + config.usb_mount, "multibootusb", iso_basename(config.image_path)) + config.syslinux_version = isolinux_version(isolinux_path) + + if config.distro in ['slitaz', 'ubunu']: + replace_syslinux_modules(config.syslinux_version, distro_install_dir) + elif config.distro == 'gentoo': + replace_syslinux_modules(config.syslinux_version, distro_install_dir) + elif config.distro == 'debian': + iso_file_list = iso.iso_file_list(config.image_path) if not any(s.strip().lower().endswith("makeboot.sh") for s in iso_file_list): log('Patch not required...') return - # Replace modules files extracted from iso with corresponding - # version provided by multibootusb. - distro_install_dir = os.path.join(config.usb_mount, "multibootusb", - iso_basename(config.image_path)) isolinux_bin_dir_ = os.path.join( distro_install_dir, isolinux_bin_dir(config.image_path)) - modules_src_dir = os.path.join( - multibootusb_host_dir(), "syslinux", "modules", - config.syslinux_version) - for module in os.listdir(isolinux_bin_dir_): - if not module.endswith(".c32"): - continue - fpath = os.path.join(isolinux_bin_dir_, module) - try: - os.remove(fpath) - src_module_path = os.path.join(modules_src_dir, module) - log("Copying " + module) - log((src_module_path, fpath)) - shutil.copy(src_module_path, fpath) - except Exception as err: - log(err) - log("Could not copy " + module) + replace_syslinux_modules(config.syslinux_version, isolinux_bin_dir_) + class DirectoryRelocator: def __init__(self, src_dir, dst_dir): diff --git a/scripts/mbusb_cli.py b/scripts/mbusb_cli.py index 5f93c6b..4748456 100644 --- a/scripts/mbusb_cli.py +++ b/scripts/mbusb_cli.py @@ -131,7 +131,7 @@ def cli_uninstall_distro(): for index, _distro_dir in enumerate(distro_list): if index == user_input: config.uninstall_distro_dir_name = _distro_dir - unin_distro() + do_uninstall_distro(_distro_dir, _distro_dir) else: log('No distro installed on ' + config.usb_disk) diff --git a/scripts/mbusb_gui.py b/scripts/mbusb_gui.py index 9f94292..ad629f4 100644 --- a/scripts/mbusb_gui.py +++ b/scripts/mbusb_gui.py @@ -5,7 +5,8 @@ # Authors: Sundar # Licence: This file is a part of multibootusb package. You can redistribute it or modify # under the terms of GNU General Public License, v.2 or above - +from functools import partial +import io import os import platform import sys @@ -13,23 +14,29 @@ import signal from PyQt5 import QtCore, QtGui, QtWidgets import subprocess import time +import traceback import webbrowser + +if platform.system() == 'Linux': + import dbus + from scripts.gui.ui_multibootusb import Ui_MainWindow from scripts.gui.ui_about import Ui_About from . import usb from .gen import * from .install import * -from .uninstall_distro import * +from . import uninstall_distro from .syslinux import * from .distro import * from .qemu import * from .iso import * # from .imager import * -from .imager import Imager, dd_linux, dd_win +from .imager import Imager, dd_iso_image from . import persistence from . import config from . import admin from . import qemu +from . import osdriver from .update_cfg_file import update_distro_cfg_files import scripts.gui.resources @@ -51,9 +58,6 @@ class AppGui(qemu.Qemu, Imager, QtWidgets.QMainWindow, Ui_MainWindow): self.ui.label_persistence.setVisible(False) self.ui.slider_persistence.setVisible(False) - config.usb_disk = None - config.image_path = None - # Main Tab self.ui.checkbox_all_drives.clicked.connect(self.onAllDrivesClicked) self.ui.button_detect_drives.clicked.connect(self.onRefreshClick) @@ -80,6 +84,12 @@ class AppGui(qemu.Qemu, Imager, QtWidgets.QMainWindow, Ui_MainWindow): # self.ui.combo_iso_boot_ram.activated[str].connect(self.qemu_iso_ram) # self.ui.combo_usb_boot_ram.activated[str].connect(self.qemu_usb_ram) # self.ui.boot_usb_qemu.clicked.connect(lambda: self.on_Qemu_Boot_usb_Click(str(self.ui.combo_drives.currentText()))) + + self.ui.run_fsck_repair.clicked.connect( + partial(self.onFsckClick, usb.repair_vfat_filesystem)) + self.ui.run_fsck_check.clicked.connect( + partial(self.onFsckClick, usb.check_vfat_filesystem)) + # Update progressbar and status (Main ISO install) self.progress_thread_install = GuiInstallProgress() self.progress_thread_install.finished.connect(self.install_syslinux) @@ -98,6 +108,11 @@ class AppGui(qemu.Qemu, Imager, QtWidgets.QMainWindow, Ui_MainWindow): self.progress_thread_dd.finished.connect(self.dd_finished) self.progress_thread_dd.status.connect(self.ui.statusbar.showMessage) + if platform.system() == 'Windows' or os.system('which fsck.vfat') != 0: + i = self.ui.tabWidget.indexOf(self.ui.tab_fsck) + if 0<=i: + self.ui.tabWidget.removeTab(i) + prepare_mbusb_host_dir() self.onRefreshClick() @@ -110,12 +125,16 @@ class AppGui(qemu.Qemu, Imager, QtWidgets.QMainWindow, Ui_MainWindow): self.onRefreshClick() return - reply = QtWidgets.QMessageBox.warning(self, "WARNING!", - "This option enables working with fixed drives\n\ -and is potentially VERY DANGEROUS\n\n\ -Are you SURE you want to enable it?", - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) + if getattr(config, 'protected_drives', []): + reply = QtWidgets.QMessageBox.Yes + else: + reply = QtWidgets.QMessageBox.warning( + self, "WARNING!", + "This option enables working with fixed drives\n" + "and is potentially VERY DANGEROUS\n\n" + "Are you SURE you want to enable it?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.No: self.ui.checkbox_all_drives.setChecked(False) @@ -140,31 +159,29 @@ Are you SURE you want to enable it?", :return: """ self.ui.installed_distros.clear() - config.usb_disk = str(self.ui.combo_drives.currentText()) - config.imager_usb_disk = str(self.ui.combo_drives.currentText()) - - if config.usb_disk: - log("Selected device " + config.usb_disk) + config.usb_disk = osdriver.listbox_entry_to_device( + self.ui.combo_drives.currentText()) + if config.usb_disk == 0 or config.usb_disk: + # Get the GPT status of the disk and store it on a variable try: - config.usb_details = usb.details(config.usb_disk) - except usb.PartitionNotMounted as e: - log(str(e)) - QtWidgets.QMessageBox.error("Error: partition not mounted", str(e)) + usb.gpt_device(config.usb_disk) + config.imager_usb_disk \ + = self.ui.combo_drives.currentText() + config.usb_details \ + = usb.details(config.usb_disk) + except Exception as e: + o = io.StringIO() + traceback.print_exc(None, o) + log(o.getvalue()) + QtWidgets.QMessageBox.critical( + self, "The disk/partition is not usable.", + str(e)) + self.ui.combo_drives.setCurrentIndex(0) + # Above statement triggers call to this method. return - config.persistence_max_size = persistence.max_disk_persistence(config.usb_disk) - config.usb_mount = config.usb_details.get('mount_point', "") - self.ui.usb_dev.setText(config.usb_disk) - - self.ui.usb_vendor.setText(config.usb_details.get('vendor', "")) - self.ui.usb_model.setText(config.usb_details.get('model', "")) - self.ui.usb_size.setText(str(usb.bytes2human(config.usb_details.get('size_total', "")))) - self.ui.usb_mount.setText(config.usb_details.get('mount_point', "")) - self.ui.usb_type.setText(config.usb_details.get('devtype', "")) - self.ui.usb_fs.setText(config.usb_details.get('file_system', "")) - - # Get the GPT status of the disk and store it on a variable - usb.gpt_device(config.usb_disk) - + log("Selected device " + + osdriver.usb_disk_desc(config.usb_disk)) + self.update_target_info() self.update_list_box(config.usb_disk) self.ui_update_persistence() else: @@ -175,7 +192,6 @@ Are you SURE you want to enable it?", self.ui.usb_mount.clear() self.ui.usb_type.clear() self.ui.usb_fs.clear() - log("No USB disk found...") def onRefreshClick(self): @@ -184,15 +200,15 @@ Are you SURE you want to enable it?", :return: """ self.ui.combo_drives.clear() - if self.ui.checkbox_all_drives.isChecked(): - detected_devices = usb.list_devices(fixed=True) - else: - detected_devices = usb.list_devices() - - if detected_devices: - for device in detected_devices: + detected_devices = usb.list_devices( + fixed=self.ui.checkbox_all_drives.isChecked()) + if not detected_devices: + return + protected_drives = getattr(config, 'protected_drives', []) + for device in detected_devices: + if all(not device.startswith(d) for d in protected_drives): self.ui.combo_drives.addItem(str(device)) - self.ui.combo_drives.setCurrentIndex(0) + self.ui.combo_drives.setCurrentIndex(0) def update_list_box(self, usb_disk): """ @@ -200,7 +216,7 @@ Are you SURE you want to enable it?", :param usb_mount: Selected USB disk from combobox. :return: """ - distro_list = install_distro_list() + distro_list = uninstall_distro.install_distro_list() if distro_list is not None: self.ui.installed_distros.clear() for name in distro_list: @@ -246,7 +262,7 @@ Are you SURE you want to enable it?", # Detect supported distro try: - clean_iso_cfg_ext_dir( # Need to be cleaned. + clean_iso_cfg_ext_dir( # Need to be cleaned. os.path.join(multibootusb_host_dir(), "iso_cfg_ext_dir")) extract_cfg_file(config.image_path) config.distro = distro(iso_cfg_ext_dir(), config.image_path, @@ -322,21 +338,23 @@ Are you SURE you want to enable it?", self.ui.label_persistence.setVisible(False) self.ui.slider_persistence.setVisible(False) + def get_controls(self): + return [ + self.ui.combo_drives, + self.ui.checkbox_all_drives, + self.ui.button_detect_drives, + self.ui.button_browse_image, + self.ui.image_path, + self.ui.tabWidget, + self.ui.button_install_distro, + self.ui.button_uninstall_distro, + ] + def ui_disable_controls(self): - self.ui.combo_drives.setEnabled(False) - self.ui.checkbox_all_drives.setEnabled(False) - self.ui.button_detect_drives.setEnabled(False) - self.ui.button_browse_image.setEnabled(False) - self.ui.image_path.setEnabled(False) - self.ui.tabWidget.setEnabled(False) + [c.setEnabled(False) for c in self.get_controls()] def ui_enable_controls(self): - self.ui.combo_drives.setEnabled(True) - self.ui.checkbox_all_drives.setEnabled(True) - self.ui.button_detect_drives.setEnabled(True) - self.ui.button_browse_image.setEnabled(True) - self.ui.image_path.setEnabled(True) - self.ui.tabWidget.setEnabled(True) + [c.setEnabled(True) for c in self.get_controls()] def update_slider_text(self): slide_value = self.ui.slider_persistence.value() * 1024 * 1024 @@ -344,6 +362,26 @@ Are you SURE you want to enable it?", config.persistence = slide_value def install_syslinux(self): + try: + try: + self.install_syslinux_impl() + finally: + config.process_exist = None + self.ui_enable_controls() + except (KeyboardInterrupt, SystemExit): + raise + except: + uninstall_distro.do_uninstall_distro( + config.distro, iso_basename(config.image_path)) + o = io.StringIO() + traceback.print_exc(None, o) + QtWidgets.QMessageBox.information( + self, 'install_syslinux() failed', + o.getvalue()) + log("install_syslinux() failed.") + log(o.getvalue()) + + def install_syslinux_impl(self): """ Function to install syslinux on distro directory and on selected USB disks. :return: @@ -353,18 +391,13 @@ Are you SURE you want to enable it?", syslinux_default(config.usb_disk) replace_grub_binary() update_distro_cfg_files(config.image_path, config.usb_disk, - config.distro, config.persistence) + config.distro, config.persistence) self.update_list_box(config.usb_disk) - if sys.platform.startswith("linux"): - self.ui.statusbar.showMessage("Status: Sync is in progress...") - os.sync() self.ui.statusbar.showMessage("Status: Idle") self.ui_disable_persistence() log(iso_name(config.image_path) + ' has been successfully installed.') QtWidgets.QMessageBox.information(self, 'Finished...', iso_name(config.image_path) + ' has been successfully installed.') - config.process_exist = None - self.ui_enable_controls() def onInstall_syslinuxClick(self): """ @@ -415,9 +448,6 @@ Are you SURE you want to enable it?", dest_fp) QtWidgets.QMessageBox.information(self, 'Install Success...', 'Syslinux installed successfully on ' + config.usb_disk) - elif ret is True and self.ui.check_install_sys_only.isChecked(): - QtWidgets.QMessageBox.information(self, 'Install Success...', - 'Syslinux installed successfully on ' + config.usb_disk) elif ret is False: QtWidgets.QMessageBox.information(self, 'Install error...', 'Sorry. Syslinux failed to install on ' + config.usb_disk) @@ -427,6 +457,38 @@ Are you SURE you want to enable it?", self.ui_enable_controls() + + def onFsckClick(self, fsck_func): + try: + self.onFsckClick_impl(fsck_func) + except (KeyboardInterrupt, SystemExit): + raise + except: + o = io.StringIO() + traceback.print_exc(None, o) + QtWidgets.QMessageBox.information( + self, 'Failed to run fsck', + o.getvalue()) + + def onFsckClick_impl(self, fsck_func): + if not config.usb_disk: + QtWidgets.QMessageBox.information( + self, 'No partition is selected', + 'Please select the partition to check.') + return + if not config.usb_disk[-1:].isdigit(): + QtWidgets.QMessageBox.information( + self, 'Selected device is not partition', + 'Please select a partition not a disk.') + return + output = [] + with usb.UnmountedContext(config.usb_disk, self.update_usb_mount): + fsck_func(config.usb_disk, output) + for resultcode, msgout, cmd in output: + QtWidgets.QMessageBox.information( + self, 'Integrity Check', + cmd + ' said:\n' + str(msgout[0], 'utf-8')) + def onedit_syslinux(self): """ Function to edit main syslinux.cfg file. @@ -493,8 +555,8 @@ Are you SURE you want to enable it?", config.uninstall_distro_dir_name)): log("Distro install directory not found. " "Just updating syslinux.cfg and grub.cfg.") - update_sys_cfg_file(config.uninstall_distro_dir_name) - update_grub_cfg_file(config.uninstall_distro_dir_name) + uninstall_distro.update_sys_cfg_file(config.uninstall_distro_dir_name) + uninstall_distro.update_grub_cfg_file(config.uninstall_distro_dir_name) self.uninstall_sys_file_update() # self.uninstall.update_sys_cfg_file() self.ui_enable_controls() @@ -513,134 +575,159 @@ Are you SURE you want to enable it?", # update_sys_cfg_file(config.uninstall_distro_dir_name) self.update_list_box(config.usb_mount) - if sys.platform.startswith("linux"): - self.ui.statusbar.showMessage("Status: Sync in progress...") - os.sync() self.ui.statusbar.showMessage("Status: Idle") QtWidgets.QMessageBox.information(self, 'Uninstall Complete...', config.uninstall_distro_dir_name + ' has been successfully removed.') self.ui_enable_controls() def onCreateClick(self): + installing = False + self.ui_disable_controls() + try: + installing = self.onCreateClick_impl() + finally: + if not installing: + self.ui_enable_controls() + + def onCreateClick_impl(self): """ Main function to create bootable USB disk. :param usb_disk: ComboBox text as detected USB disk. :param iso_link: LineEdit text as selected ISO link. :return: """ - - self.ui_disable_controls() - try: - usb_details = usb.details(config.usb_disk) - except usb.PartitionNotMounted as e: - log(str(e)) - QtWidgets.QMessageBox.error("Error: partition not mounted", str(e)) - return - - if not config.usb_disk: - log("ERROR: No USB device found.") - QtWidgets.QMessageBox.information(self, "No Device...", - "No USB device found.\n\nInsert USB and use Refresh USB button to detect USB.") - self.ui_enable_controls() - elif not config.image_path: - log("No ISO selected.") - QtWidgets.QMessageBox.information(self, "No ISO...", "No ISO found.\n\nPlease select an ISO.") - self.ui_enable_controls() - elif usb_details['mount_point'] == 'No_Mount': + for cond, log_msg, dialog_title, dialog_msg in [ + (lambda: config.usb_disk is None, + 'ERROR: No USB device found.', + 'No Device...', + 'No USB device found.\n\nInsert USB and ' + 'use Refresh USB button to detect USB.'), + (lambda: not config.image_path, + 'No ISO selected.', + 'No ISO...', + 'No ISO found.\n\nPlease select an ISO.'), + (lambda: ' ' in + os.path.basename(config.image_path), + 'Spaces in iso-file name is not allowed.', + 'Bad ISO filename...', + 'Filename that contains space(s) is not ' + 'supported.')]: + if cond(): + QtWidgets.QMessageBox.information( + self, dialog_title, dialog_msg) + return False + + usb_details = config.usb_details + if usb_details['mount_point'] == 'No_Mount': log("ERROR: USB disk is not mounted.") - QtWidgets.QMessageBox.information(self, "No Mount...", "USB disk is not mounted.\n" - "Please mount USB disk and press refresh USB button.") - self.ui_enable_controls() - elif platform.system() == 'Linux' and config.usb_disk[-1].isdigit() is False: - gen.log('Selected USB is a disk. Please select a disk partition from the drop down list') - QtWidgets.QMessageBox.information(self, 'No Partition...!', - 'USB disk selected doesn\'t contain a partition.\n' - 'Please select the partition (ending ' - 'with a digit eg. /dev/sdb1)\nfrom the drop down list.') - self.ui_enable_controls() - elif 0 < config.persistence and \ + QtWidgets.QMessageBox.information( + self, "No Mount...", + "USB disk is not mounted.\n" + "Please mount USB disk and press refresh " + "USB button.") + return False + if config.usb_details['devtype'] == 'disk': + gen.log('Selected USB is a physical disk. ' + 'Please select ' + 'a partition or volume from the drop down list') + QtWidgets.QMessageBox.information( + self, 'No Partition...!', + 'Selected USB is a physical disk. ' + 'Please select a partition (e.g. /dev/sdc1) ' + 'or a volume (e.g. G:) ' + 'from the drop down list.') + return False + if 0 < config.persistence and \ persistence.detect_missing_tools(config.distro): QtWidgets.QMessageBox.information( self, 'Missing tools...!', - persistence.detect_missing_tools(config.distro)) - self.ui_enable_controls() - else: - # clean_iso_cfg_ext_dir(os.path.join(multibootusb_host_dir(), "iso_cfg_ext_dir")) # Need to be cleaned. - # extract_cfg_file(config.image_path) # Extract files from ISO - # config.distro = distro(iso_cfg_ext_dir(), config.image_path) # Detect supported distro - try: - usb_details = usb.details(config.usb_disk) - except usb.PartitionNotMounted as e: - log(str(e)) - QtWidgets.QMessageBox.error("Error: partition not mounted", str(e)) - return - log("MultiBoot Install: USB Disk: " + config.usb_disk) - log("MultiBoot Install: USB Label: " + config.usb_label) - log("MultiBoot Install: USB UUID: " + config.usb_uuid) - log("MultiBoot Install: USB mount path: " + config.usb_mount) - log("MultiBoot Install: Disk total size: " + str(usb.bytes2human(usb_details['size_total']))) - log("MultiBoot Install: Disk used size: " + str(usb.bytes2human(usb_details['size_used']))) - log("MultiBoot Install: Disk free size: " + str(usb.bytes2human(usb_details['size_free']))) - log("MultiBoot Install: Filesystem: " + usb_details['file_system']) - log("MultiBoot Install: Disk vendor: " + usb_details['vendor']) - log("MultiBoot Install: Disk model: " + usb_details['model']) - log("MultiBoot Install: ISO file: " + iso_name(config.image_path)) + persistence.detect_missing_tools( + config.distro)) + return False + if not self.check_remount(): + self.update_target_info() + return False + + # clean_iso_cfg_ext_dir(os.path.join(multibootusb_host_dir(), "iso_cfg_ext_dir")) # Need to be cleaned. + # extract_cfg_file(config.image_path) # Extract files from ISO + # config.distro = distro(iso_cfg_ext_dir(), config.image_path) # Detect supported distro + log("MultiBoot Install: USB Disk: " + config.usb_disk) + log("MultiBoot Install: USB Label: " + config.usb_label) + log("MultiBoot Install: USB UUID: " + config.usb_uuid) + log("MultiBoot Install: USB mount path: " + config.usb_mount) + log("MultiBoot Install: Disk total size: " + str(usb.bytes2human(usb_details['size_total']))) + log("MultiBoot Install: Disk used size: " + str(usb.bytes2human(usb_details['size_used']))) + log("MultiBoot Install: Disk free size: " + str(usb.bytes2human(usb_details['size_free']))) + log("MultiBoot Install: Filesystem: " + usb_details['file_system']) + log("MultiBoot Install: Disk vendor: " + usb_details['vendor']) + log("MultiBoot Install: Disk model: " + usb_details['model']) + log("MultiBoot Install: ISO file: " + iso_name(config.image_path)) + + if not os.path.exists(config.image_path): + return False + + # self.ui.image_path.clear() + if not config.distro: + QtWidgets.QMessageBox.information( + self, 'No support...', + 'Sorry.\n' + + os.path.basename(config.image_path) + + ' is not supported at the moment.\n' + 'Please email this issue to ' + 'feedback.multibootusb@gmail.com') + return False + + log("MultiBoot Install: Distro type detected: " + config.distro) + full_image_path = os.path.join( + config.usb_mount, "multibootusb", + iso_basename(config.image_path)) + if os.path.exists(full_image_path): + QtWidgets.QMessageBox.information( + self, 'Already exists...', + os.path.basename(config.image_path) + + ' is already installed.') + return False + + config.persistence = self.ui.slider_persistence.value() \ + * 1024 * 1024 + log("Persistence chosen is " + + str(bytes2human(config.persistence))) + install_size = iso_size(config.image_path) + config.persistence + if install_size >= disk_usage(config.usb_mount).free: + log("ERROR: Not enough space available on " + + config.usb_disk) + QtWidgets.QMessageBox.information( + self, "No Space.", + "No space available on " + config.usb_disk) + return False + + msg = ''' +The ISO sleceted is not supported at the moment. +You can try booting ISO using memdisk. +Distro can be uninstalled anytime from main menu. + +Proceed with installation?'''.lstrip() if config.distro == 'memdisk_iso' else \ + ''' +Selected USB disk: %s +USB mount point: %s +Selected distro: %s + +Log location: %s + +Proceed with installation?'''.lstrip() % \ + (config.usb_disk, config.usb_mount, iso_name(config.image_path), + osdriver.mbusb_log_file()) + reply = QtWidgets.QMessageBox.question( + self, 'Review selection...', msg) + if reply == QtWidgets.QMessageBox.Yes: + self.ui.slider_persistence.setEnabled(False) + copy_mbusb_dir_usb(config.usb_disk) + config.process_exist = True + self.progress_thread_install.start() + return True - if os.path.exists(config.image_path): - # self.ui.image_path.clear() - if config.distro: - log("MultiBoot Install: Distro type detected: " + config.distro) - if not os.path.exists( - os.path.join(config.usb_mount, "multibootusb", iso_basename(config.image_path))): - config.persistence = self.ui.slider_persistence.value() * 1024 * 1024 - log("Persistence chosen is " + str(bytes2human(config.persistence))) - install_size = iso_size(config.image_path) + config.persistence - if install_size >= disk_usage(config.usb_mount).free: - log("ERROR: Not enough space available on " + config.usb_disk) - QtWidgets.QMessageBox.information(self, "No Space.", - "No space available on " + config.usb_disk) - self.ui_enable_controls() - else: - if config.distro == 'memdisk_iso': - reply = QtWidgets.QMessageBox.question(self, 'Review selection...', - 'The ISO sleceted is not supported at the moment.\n' - 'You can try booting ISO using memdisk.\n' - 'Distro can be uninstalled anytime from main menu.\n\n' - 'Proceed with installation?', - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) - else: - reply = QtWidgets.QMessageBox.question(self, 'Review selection...', - 'Selected USB disk: %s\n' % config.usb_disk + - 'USB mount point: %s\n' % config.usb_mount + - 'Selected distro: %s\n\n' % iso_name( - config.image_path) + - 'Proceed with installation?', - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) - - if reply == QtWidgets.QMessageBox.Yes: - self.ui.slider_persistence.setEnabled(False) - copy_mbusb_dir_usb(config.usb_disk) - config.process_exist = True - self.progress_thread_install.start() - elif reply == QtWidgets.QMessageBox.No: - self.ui_enable_controls() + return False - else: - QtWidgets.QMessageBox.information(self, 'Already exists...', - os.path.basename( - config.image_path) + ' is already installed.') - self.ui_enable_controls() - else: - QtWidgets.QMessageBox.information(self, 'No support...', - 'Sorry.\n' + os.path.basename(config.image_path) + - ' is not supported at the moment.\n' - 'Please email this issue to feedback.multibootusb@gmail.com') - self.ui_enable_controls() - - # Added to refresh usb disk remaining size after distro installation - # self.update_gui_usb_info() def dd_finished(self): """ @@ -652,8 +739,15 @@ Are you SURE you want to enable it?", config.process_exist = None msgBox = QtWidgets.QMessageBox() - msgBox.setText("Image succesfully written to USB disk.") - msgBox.setInformativeText("Reboot to boot from USB or test it from Boot ISO/USB tab."); + if self.progress_thread_dd.error: + title = "Failed to write the iso image to the USB disk." + msg = self.progress_thread_dd.error + else: + title = "Image succesfully written to USB disk." + msg = "Reboot to boot from USB or test it from " \ + "Boot ISO/USB tab." + msgBox.setText(title) + msgBox.setInformativeText(msg); msgBox.setStandardButtons(QtWidgets.QMessageBox.Ok) msgBox.setIcon(QtWidgets.QMessageBox.Information) msgBox.exec_() @@ -671,7 +765,9 @@ Are you SURE you want to enable it?", self.ui.button_browse_image.setEnabled(False) self.ui.combo_drives.setEnabled(False) # FIXME self.ui.pushbtn_imager_refreshusb.setEnabled(False) - status_text = ("Status: Writing " + os.path.basename(config.image_path) + " to " + config.usb_disk) + status_text = ("Status: Writing " + + os.path.basename(config.image_path) + " to " + + osdriver.usb_disk_desc(config.usb_disk)) self.ui.statusbar.showMessage(status_text) def dd_quit(self): @@ -693,19 +789,19 @@ Are you SURE you want to enable it?", self.ui_enable_controls() else: imager = Imager() - if platform.system() == 'Linux' and config.usb_details['devtype'] == "partition": + if config.usb_details['devtype'] == "partition": gen.log('Selected device is a partition. Please select a disk from the drop down list') QtWidgets.QMessageBox.information(self, 'Incompatible device', 'Selected device (%s) is a partition!\n' 'ISO must be written to a whole disk.' '\n\nPlease select a disk from the drop down list.' % config.usb_disk) self.ui_enable_controls() else: - usb_disk_size = int(imager.imager_usb_detail(config.usb_disk, partition=0).total_size) + usb_disk_size = int(imager.imager_usb_detail(config.usb_disk).total_size) self.iso_size = os.path.getsize(config.image_path) if self.iso_size >= usb_disk_size: QtWidgets.QMessageBox.information(self, "No enough space on disk.", os.path.basename(config.image_path) + - " size is larger than the size of " + config.usb_disk) + " size is larger than the size of " + osdriver.usb_disk_desc(config.usb_disk)) self.ui_enable_controls() # elif gen.process_exist('explorer.exe') is not False: # # Check if windows explorer is running and inform user to close it. @@ -715,7 +811,7 @@ Are you SURE you want to enable it?", else: reply = QtWidgets.QMessageBox.question \ (self, 'Review selection', - 'Selected disk: %s\n' % config.usb_disk + + 'Selected disk: %s\n' % osdriver.usb_disk_desc(config.usb_disk) + 'Selected image: %s\n\n' % os.path.basename(config.image_path) + 'Proceed with writing image to disk?', QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) @@ -734,6 +830,42 @@ Are you SURE you want to enable it?", """ self.close() + def update_usb_mount(self, new_usb_details): + config.update_usb_mount(new_usb_details) + self.ui.usb_mount.setText(config.usb_mount) + + def check_remount(self): + if config.usb_details['file_system'] != 'vfat': + return True + try: + with UnmountedContext(config.usb_disk, + self.update_usb_mount) as m: + pass + return True + except usb.RemountError: + QtWidgets.QMessageBox.critical( + self,"Remount failed.", + "Could not remount '{0}'. " + "Please make sure no process has open " + "handle(s) to previously mounted filesystem." + .format(config.usb_disk)) + return False + + def update_target_info(self): + + usb_total_size= str(usb.bytes2human(config.usb_details.get('size_total', ""))) + usb_free_size= str(usb.bytes2human(config.usb_details.get('size_free', ""))) + config.persistence_max_size = persistence.max_disk_persistence(config.usb_disk) + config.usb_mount = config.usb_details.get('mount_point', "") + self.ui.usb_dev.setText(osdriver.usb_disk_desc(config.usb_disk)) + + self.ui.usb_vendor.setText(config.usb_details.get('vendor', "")) + self.ui.usb_model.setText(config.usb_details.get('model', "")) + self.ui.usb_size.setText('Free :: ' + usb_free_size + ' / Total :: ' + usb_total_size) + self.ui.usb_mount.setText(config.usb_details.get('mount_point', "")) + self.ui.usb_type.setText(config.usb_details.get('devtype', "")) + self.ui.usb_fs.setText(config.usb_details.get('file_system', "")) + def closeEvent(self, event): """ To capture the main close event. @@ -756,7 +888,6 @@ Are you SURE you want to enable it?", log("Close event cancelled.") event.ignore() - class GuiInstallProgress(QtCore.QThread): """ Update GUI thread during install. @@ -810,7 +941,7 @@ class GuiUninstallProgress(QtCore.QThread): def __init__(self): QtCore.QThread.__init__(self) - self.thread = GenericThread(uninstall_progress) + self.thread = GenericThread(uninstall_distro.uninstall_progress) def __del__(self): self.wait() @@ -849,21 +980,23 @@ class DD_Progress(QtCore.QThread): def __init__(self): QtCore.QThread.__init__(self) - - if platform.system() == 'Linux': - self.thread = GenericThread(dd_linux) - elif platform.system() == 'Windows': - self.thread = GenericThread(dd_win) + self.error = None + self.thread = GenericThread(partial(dd_iso_image, self)) def __del__(self): self.wait() + def set_error(self, error): + self.error = error + def run(self): + config.imager_percentage = 0 self.thread.start() while self.thread.isRunning(): if config.imager_percentage: self.update.emit(config.imager_percentage) - if not self.thread.isFinished() and config.percentage == 100: + if not self.thread.isFinished() and \ + config.percentage == 100: config.imager_status_text = "" self.status.emit("Please wait...") time.sleep(0.1) diff --git a/scripts/osdriver.py b/scripts/osdriver.py new file mode 100644 index 0000000..737e538 --- /dev/null +++ b/scripts/osdriver.py @@ -0,0 +1,463 @@ +import collections +import logging +import logging.handlers +import os +import platform +import queue +import shutil +import signal +import subprocess +import sys +import tempfile +import time + +if platform.system() == 'Windows': + import wmi + from scripts import win32 +else: + try: + from . import udisks + except ImportError: + import udisks + +def log(message, info=True, error=False, debug=False, _print=True): + """ + Dirty function to log messages to file and also print on screen. + :param message: + :param info: + :param error: + :param debug: + :return: + """ + if _print is True: + print(message) + + # remove ANSI color codes from logs + # message_clean = re.compile(r'\x1b[^m]*m').sub('', message) + + if info is True: + logging.info(message) + elif error is not False: + logging.error(message) + elif debug is not False: + logging.debug(message) + +def resource_path(relativePath): + """ + Function to detect the correct path of file when working with sourcecode/install or binary. + :param relativePath: Path to file/data. + :return: Modified path to file/data. + """ + # This is not strictly needed because Windows recognize '/' + # as a path separator but we follow the discipline here. + relativePath = relativePath.replace('/', os.sep) + for dir_ in [ + os.path.abspath('.'), + os.path.abspath('..'), + getattr(sys, '_MEIPASS', None), + os.path.dirname(os.path.dirname( # go up two levels + os.path.realpath(__file__))), + '/usr/share/multibootusb'.replace('/', os.sep), + ]: + if dir_ is None: + continue + fullpath = os.path.join(dir_, relativePath) + if os.path.exists(fullpath): + return fullpath + log("Could not find resource '%s'." % relativePath) + + +def get_physical_disk_number(usb_disk): + """ + Get the physical disk number as detected ny Windows. + :param usb_disk: USB disk (Like F:) + :return: Disk number. + """ + partition, logical_disk = wmi_get_drive_info(usb_disk) + log("Physical Device Number is %d" % partition.DiskIndex) + return partition.DiskIndex + + +def wmi_get_drive_info(usb_disk): + assert platform.system() == 'Windows' + for partition in wmi.WMI().Win32_DiskPartition(): + logical_disks = partition.associators("Win32_LogicalDiskToPartition") + # Here, 'disk' is a windows logical drive rather than a physical drive + for disk in logical_disks: + if disk.Caption == usb_disk: + return (partition, disk) + raise RuntimeError('Failed to obtain drive information ' + usb_disk) + + +def collect_relevant_info(obj, tuple_name, attributes, named_tuple): + if len(named_tuple)==0: + names = [x[0] for x in attributes] + named_tuple.append(collections.namedtuple(tuple_name, names)) + L = [] + for (attr, convfunc) in attributes: + v = getattr(obj, attr) + L.append(None if v is None else convfunc(v)) + return named_tuple[0](*L) + + +def collect_relevant_physicaldrive_info(d, physicaldrive_info_tuple=[]): + attributes = [ + ('BytesPerSector', int), + ('DeviceID', str), + ('Index', int), + ('Manufacturer', str), + ('MediaType', str), + ('Model', str), + ('Partitions', int), + ('SerialNumber', str), + ('Size', int), + ('TotalSectors', int), + ] + return collect_relevant_info(d, 'PhysicalDrive', attributes, + physicaldrive_info_tuple) + + +def collect_relevant_volume_info(v, volume_info_tuple=[]): + attributes = [ + ('DeviceID', str), + ('DriveType', int), + ('FreeSpace', int), + ('FileSystem', str), + ('Size', int), + ] + return collect_relevant_info(v, 'Volume', attributes, volume_info_tuple) + + +def wmi_get_physicaldrive_info(usb_disk): + "Return information about the drive that contains 'usb_disk'." + partition, disk = wmi_get_drive_info(usb_disk) + import wmi + c = wmi.WMI() + drv_list = [d for d in c.Win32_DiskDrive() + if d.Index == partition.DiskIndex] + assert len(drv_list)==1 + return collect_relevant_physicaldrive_info(drv_list[0]) + + +def wmi_get_physicaldrive_info_all(): + c = wmi.WMI() + L = [collect_relevant_physicaldrive_info(d) for d in c.Win32_DiskDrive()] + L.sort(key=lambda x: x.Index) + return L + + +def wmi_get_volume_info_on(diskIndex): + L = [volumes for (dindex, volumes) in wmi_get_volume_info_all().items() + if dindex==diskIndex] + return [] if len(L)==0 else L[0] + + +def wmi_get_volume_info_all(): + r = {} + for dindex, volumes in [ + (p.DiskIndex, map(lambda d: collect_relevant_volume_info(d), + p.associators("Win32_LogicalDiskToPartition"))) + for p in wmi.WMI().Win32_DiskPartition()]: + r.setdefault(dindex, []).extend(volumes) + for dindex, volumes in r.items(): + volumes.sort(key=lambda x: x.DeviceID) + return r + + +def wmi_get_volume_info_ex(usb_disk): + assert platform.system() == 'Windows' + partition, disk = wmi_get_drive_info(usb_disk) + #print (disk.Caption, partition.StartingOffset, partition.DiskIndex, + # disk.FileSystem, disk.VolumeName) + + # Extract Volume serial number off of the boot sector because + # retrieval via COM object 'Scripting.FileSystemObject' or wmi interface + # truncates NTFS serial number to 32 bits. + with open('//./Physicaldrive%d'%partition.DiskIndex, 'rb') as f: + f.seek(int(partition.StartingOffset)) + bs_ = f.read(512) + serial_extractor = { + 'NTFS' : lambda bs: \ + ''.join('%02X' % c for c in reversed(bs[0x48:0x48+8])), + 'FAT32' : lambda bs: \ + '%02X%02X-%02X%02X' % tuple( + map(int,reversed(bs[67:71]))) + }.get(disk.FileSystem, lambda bs: None) + uuid = serial_extractor(bs_) + mount_point = usb_disk + '\\' + size_total, size_used, size_free \ + = shutil.disk_usage(mount_point)[:3] + r = { + 'uuid' : uuid, + 'file_system' : disk.FileSystem, + 'label' : disk.VolumeName.strip() or 'No_label', + 'mount_point' : mount_point, + 'size_total' : size_total, + 'size_used' : size_used, + 'size_free' : size_free, + 'vendor' : 'Not_Found', + 'model' : 'Not_Found', + 'devtype' : 'partition', + 'mediatype' : { + 0 : 'Unknown', + 1 : 'Fixed Disk', + 2 : 'Removable Disk', + 3 : 'Local Disk', + 4 : 'Network Drive', + 5 : 'Compact Disc', + 6 : 'RAM Disk', + }.get(disk.DriveType, 'DiskType(%d)' % disk.DriveType), + 'disk_index' : partition.DiskIndex, + } + # print (r) + return r + +def wmi_get_physicaldrive_info_ex(diskIndex): + drv_list = [d for d in wmi.WMI().Win32_DiskDrive() + if d.Index == diskIndex] + assert len(drv_list)==1 + d = collect_relevant_physicaldrive_info(drv_list[0]) + r = {} + for src, dst in [ + ('Size', 'size_total'), + ('Model', 'model'), + ('Manufacturer', 'vendor'), + ('MediaType', 'mediatype'), + ('SerialNumber', 'uuid'), + ('DeviceID', 'label'), + ]: + r[dst] = getattr(d, src) + r['devtype'] = 'disk' + r['size_free'] = 0 + r['file_system'] = 'N/A' + r['mount_point'] = 'N/A' + return r + + +def win_physicaldrive_to_listbox_entry(pdrive): + return '%d:%s' % (pdrive.Index,pdrive.Model) + + +def win_volume_to_listbox_entry(v): + return v.DeviceID + +class Base: + + def run_dd(self, input, output, bs, count): + cmd = [self.dd_exe, 'if='+input, 'of='+output, + 'bs=%d' % bs, 'count=%d'%count] + self.dd_add_args(cmd, input, output, bs, count) + if subprocess.call(cmd) != 0: + log('Failed to execute [%s]' % str(cmd)) + else: + log("%s succeeded." % str(cmd)) + + + def dd_iso_image(self, input_, output, gui_update, status_update): + ''' Implementation for OS that use dd to write the iso image. + ''' + in_file_size = os.path.getsize(input_) + cmd = [self.dd_exe, 'if=' + input_, + 'of=' + self.physical_disk(output), 'bs=1M'] + self.dd_iso_image_add_args(cmd, input_, output) + kw_args = { + 'stdout' : subprocess.PIPE, + 'stderr' : subprocess.PIPE, + 'shell' : False, + } + self.add_dd_iso_image_popen_args(kw_args) + self.dd_iso_image_prepare(input, output, status_update) + log('Executing => ' + str(cmd)) + dd_process = subprocess.Popen(cmd, **kw_args) + output_q = queue.Queue() + while dd_process.poll() is None: + self.dd_iso_image_readoutput(dd_process, gui_update, in_file_size, + output_q) + output_lines = [output_q.get() for i in range(output_q.qsize())] + for l in output_lines: + log('dd: ' + l) + return self.dd_iso_image_interpret_result( + dd_process.returncode, output_lines) + +class Windows(Base): + + def __init__(self): + self.dd_exe = resource_path('data/tools/dd/dd.exe') + + def dd_add_args(self, cmd_vec, input, output, bs, count): + pass + + def dd_iso_image(self, input_, output, gui_update, status_update): + assert type(output) is int + status_update('Zapping PhyiscalDisk%d' % output) + win32.ZapPhysicalDrive(output, wmi_get_volume_info_on, log) + # Ouch. Needs sometime for the zapping to take effect... + # Better way than sleeping constant time? + time.sleep(3) + status_update('Writing to PhysicalDisk%d' % output) + in_file_size = os.path.getsize(input_) + with win32.openHandle('\\\\.\\PhysicalDrive%d' % output, + True, False, log) as hDrive: + hDrive.LockPhysicalDrive() + hDrive.CopyFrom(input_, lambda bytes_copied: + gui_update(float(bytes_copied)/in_file_size*100.)) + + def physical_disk(self, usb_disk): + if type(usb_disk) is str: + usb_disk = get_physical_disk_number(usb_disk) + return r'\\.\physicaldrive%d' % usb_disk + + def mbusb_log_file(self): + return os.path.join(os.getcwd(), 'multibootusb.log') + + def find_mounted_partitions_on(self, usb_disk): + return [] # No-op until UnmountedContext() get implemented for Windows + + def multibootusb_host_dir(self): + return os.path.join(tempfile.gettempdir(), "multibootusb") + + def gpt_device(self, dev_name): + if type(dev_name) is int: + diskIndex = dev_name + for p in wmi.WMI().Win32_DiskPartition(): + if p.DiskIndex == diskIndex: + return p.Type.startswith('GPT:') + log(usb_disk_desc(dev_name) + ' seems not partitioned. ' + + 'assuming msdos.') + return False + else: + partition, disk = wmi_get_drive_info(dev_name) + return partition.Type.startswith('GPT:') + + def usb_disk_desc(self, dev_name): + if type(dev_name) is int: + return 'PhysicalDrive%d' % dev_name + return dev_name + + def listbox_entry_to_device(self, lb_entry): + left = lb_entry.split(':', 1)[0] + if left.isdigit(): + return int(left) # see win_physicaldrive_to_listbox_entry() + else: + return lb_entry # see win_volume_to_listbox_entry() + + def qemu_more_params(self, disk): + return ['-L', '.', '-boot', 'c', '-hda', self.physical_disk(disk)] + +class Linux(Base): + + def __init__(self): + self.dd_exe = 'dd' + + def dd_iso_image_prepare(self, input, output, status_update): + pass + + def dd_add_args(self, cmd_vec, input, output, bs, count): + cmd_vec.append('conv=notrunc') + + def dd_iso_image_add_args(self, cmd_vec, input_, output): + cmd_vec.append('oflag=sync') + + def add_dd_iso_image_popen_args(self, dd_iso_image_popen_args): + pass + + def dd_iso_image_readoutput(self, dd_process, gui_update, in_file_size, + output_q): + # If this time delay is not given, the Popen does not execute + # the actual command + time.sleep(0.1) + dd_process.send_signal(signal.SIGUSR1) + dd_process.stderr.flush() + while True: + time.sleep(0.1) + out_error = dd_process.stderr.readline().decode() + if out_error: + if 'bytes' in out_error: + bytes_copied = float(out_error.split(' ', 1)[0]) + gui_update( bytes_copied / in_file_size * 100. ) + break + if 15 < output_q.qsize(): + output_q.get() + output_q.put(out_error.rstrip()) + else: + # stderr is closed + break + + def dd_iso_image_interpret_result(self, returncode, output_list): + return None if returncode==0 else '\n'.join(output_list) + + def physical_disk(self, usb_disk): + return usb_disk.rstrip('0123456789') + + def mbusb_log_file(self): + return '/var/log/multibootusb.log' + + def find_mounted_partitions_on(self, usb_disk): + return udisks.find_mounted_partitions_on(usb_disk) + + def multibootusb_host_dir(self): + return os.path.join(os.path.expanduser('~'), ".multibootusb") + + def gpt_device(self, dev_name): + disk_dev = self.physical_disk(dev_name) + cmd = ['parted', disk_dev, '-s', 'print'] + with open(os.devnull) as devnull: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, stdin=devnull) + _cmd_out, _err_out = p.communicate() + p.wait() + if p.returncode != 0: + lang = os.getenv('LANG') + encoding = lang.rsplit('.')[-1] if lang else 'utf-8' + raise RuntimeError(str(_err_out, encoding)) + subprocess.check_call(['partprobe', disk_dev]) + if b'msdos' in _cmd_out: + return False + if b'gpt' in _cmd_out: + return True + raise RuntimeError("Disk '%s' is uninitialized and not usable." % + disk_dev) + + def usb_disk_desc(self, dev_name): + return dev_name + + def listbox_entry_to_device(self, lb_entry): + return lb_entry + + def qemu_more_params(self, disk): + return ['-hda', self.physical_disk(disk), '-vga', 'std'] + +driverClass = { + 'Windows' : Windows, + 'Linux' : Linux, +}.get(platform.system(), None) +if driverClass is None: + raise Exception('Platform [%s] is not supported.' % platform.system()) +osdriver = driverClass() + +for func_name in [ + 'run_dd', + 'physical_disk', + 'mbusb_log_file', + 'dd_iso_image', + 'find_mounted_partitions_on', + 'multibootusb_host_dir', + 'gpt_device', + 'listbox_entry_to_device', + 'usb_disk_desc', + 'qemu_more_params', + ]: + globals()[func_name] = getattr(osdriver, func_name) + +def initialize(): + logging.root.setLevel(logging.DEBUG) + fmt = '%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s' + datefmt = '%H:%M:%S' + the_handler = logging.handlers.RotatingFileHandler( + osdriver.mbusb_log_file(), 'a', 1024*1024, 5) + the_handler.setFormatter(logging.Formatter(fmt, datefmt)) + logging.root.addHandler(the_handler) + + if platform.system() == 'Windows': + import pythoncom + pythoncom.CoInitialize() diff --git a/scripts/persistence.py b/scripts/persistence.py index 4d5dc1a..8117c99 100644 --- a/scripts/persistence.py +++ b/scripts/persistence.py @@ -30,10 +30,11 @@ def max_disk_persistence(usb_disk): config.usb_uuid = usb_details['uuid'] config.usb_label = usb_details['label'] - if usb_details['file_system'] in ['vfat', 'FAT32'] and usb_details['size_free'] > fat_max_size: - _max_size = fat_max_size + size_free = usb_details['size_free'] + if usb_details['file_system'] in ['vfat', 'FAT32']: + _max_size = min(fat_max_size, size_free) else: - _max_size = usb_details['size_free'] + _max_size = size_free return _max_size @@ -189,7 +190,8 @@ def detect_missing_tools(distro): try: with open(os.devnull) as devnull: for tool in [e2fsck_exe, resize2fs_exe]: - subprocess.Popen([tool], stdout=devnull, stderr=devnull) + p = subprocess.Popen([tool], stdout=devnull, stderr=devnull) + p.communicate() except FileNotFoundError: # Windows return "'%s.exe' is not installed or not available for use." % tool except OSError: # Linux diff --git a/scripts/qemu.py b/scripts/qemu.py index 56d9553..82c9205 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -9,13 +9,15 @@ # under the terms of GNU General Public License, v.2 or above import os -import subprocess import platform +import subprocess +import traceback from PyQt5 import QtWidgets from .gui.ui_multibootusb import Ui_MainWindow from .gen import * from . import config - +from . import osdriver +from . import usb class Qemu(QtWidgets.QMainWindow, Ui_MainWindow): """ @@ -27,35 +29,66 @@ class Qemu(QtWidgets.QMainWindow, Ui_MainWindow): self.ui = Ui_MainWindow() self.ui.setupUi(self) + def run_qemu(self, ram_size, qemu_more_params, + qemu_not_found_log_msg, + exec_error_title, exec_error_msg): + + qemu = self.find_qemu() + if not qemu: + log(qemu_not_found_log_msg) + QtWidgets.QMessageBox.information( + self, 'No QEMU...', + 'Please install qemu to use this feature.') + return + options = [] # '-bios', 'OVMF.fd'] + if ram_size: + options.extend(['-m', ram_size]) + if getattr(config, 'qemu_use_haxm', False): + options.extend(['-accel', 'hax']) + bios = getattr(config, 'qemu_bios', None) + if bios: + options.extend(['-bios', bios]) + if platform.system()=='Linux' and getattr(config,'qemu_use_kvm', True): + options.append('-enable-kvm') + + cmd = [qemu] + options + qemu_more_params + try: + new_wd = os.path.split(qemu)[0] + if new_wd: + old_wd = os.getcwd() + os.chdir(new_wd) + try: + with usb.UnmountedContext(config.usb_disk, + self.update_usb_mount): + log("Executing ==> %s" % cmd) + out = subprocess.check_output(cmd) + if out: + log('%s => %s' % (cmd, out)) + finally: + if new_wd: + os.chdir(old_wd) + except (KeyboardInterrupt, SystemExit): + raise + except: + traceback.print_exc() + QtWidgets.QMessageBox.information( + self, exec_error_title, exec_error_msg) + + def on_Qemu_Boot_iso_Click(self): """ Main function to boot a selected ISO. :return: """ -# if not self.ui.lineEdit_2.text(): if not config.image_path: - QtWidgets.QMessageBox.information(self, 'No ISO...', 'No ISO selected.\n\nPlease choose an ISO first.') - else: - qemu = self.check_qemu_exist() - qemu_iso_link = config.image_path - if not qemu: - log("ERROR: ISO Boot: qemu not found!") - QtWidgets.QMessageBox.information(self, 'No QEMU...', 'Please install qemu to use this feature.') - else: - ram = self.qemu_iso_ram() - if ram: - ram = " -m " + ram - else: - ram = "" - - cmd = qemu + ram + ' -enable-kvm -boot d' + ' -cdrom "' + str(qemu_iso_link) + '"' - try: - log("Executing ==> " + cmd) - subprocess.Popen(cmd, shell=True) - except: - QtWidgets.QMessageBox.information(self, 'Error...', 'Error booting ISO\n' - 'Unable to start QEMU.') - + QtWidgets.QMessageBox.information( + self, 'No ISO...', + 'No ISO selected.\n\nPlease choose an ISO first.') + return + self.run_qemu( + self.qemu_iso_ram(), ['-boot', 'd','-cdrom', config.image_path], + "ERROR: ISO Boot: qemu not found!", + 'Error...', 'Error booting ISO\nUnable to start QEMU.') def on_Qemu_Boot_usb_Click(self): """ @@ -63,79 +96,40 @@ class Qemu(QtWidgets.QMainWindow, Ui_MainWindow): :param usb_disk: Path to usb disk. :return: """ - qemu = self.check_qemu_exist() - if not config.usb_disk: - QtWidgets.QMessageBox.information(self, 'No disk...', 'No USB disk selected.\n\nPlease choose a disk first.') - else: - qemu = self.check_qemu_exist() - if platform.system() == 'Linux' and config.usb_disk[-1].isdigit() is True: - qemu_usb_disk = config.usb_disk[:-1] - else: - qemu_usb_disk = config.usb_disk - - if qemu is None: - log("ERROR: USB Boot: qemu not found!") - QtWidgets.QMessageBox.information(self, 'No QEMU...', 'Please install qemu to use this feature.') - else: - ram = self.qemu_usb_ram() - if ram: - ram = " -m " + ram - else: - ram = "" - - if platform.system() == "Windows": - disk_number = get_physical_disk_number(qemu_usb_disk) - parent_dir = os.getcwd() - os.chdir(resource_path(os.path.join("data", "tools", "qemu"))) - cmd = quote(qemu) + ' -L . -boot c' + ram \ - + ' -hda //./PhysicalDrive' + str(disk_number) - - try: - log("Executing ==> " + cmd) - subprocess.Popen(cmd, shell=True) - except: - QtWidgets.QMessageBox.information(self, 'Error...', 'Error booting USB\nUnable to start QEMU.') - os.chdir(parent_dir) - - elif platform.system() == "Linux": - cmd = qemu + ' -enable-kvm -hda "' + qemu_usb_disk + '"' + ram + ' -vga std' - try: - log('Executing ==> ' + cmd) - subprocess.Popen(cmd, shell=True) - except: - QtWidgets.QMessageBox.information(self, 'Error...', 'Error booting USB\n\nUnable to start QEMU.') + QtWidgets.QMessageBox.information( + self, 'No disk...', + 'No USB disk selected.\n\nPlease choose a disk first.') + return + more_params = osdriver.qemu_more_params(config.usb_disk) + self.run_qemu(self.qemu_usb_ram(), more_params, + "ERROR: USB Boot: qemu not found!", + 'Error...', 'Error booting USB\nUnable to start QEMU.') + + def qemu_ram_size(self, combo, log_msg): + selected_ram = combo.currentText() + log(log_msg % selected_ram) + return selected_ram != 'Default' and selected_ram or None def qemu_iso_ram(self): """ Choose a ram size for ISO booting. :return: Ram size as string. """ - selected_ram = self.ui.combo_iso_boot_ram.currentText() - log("QEMU: ISO RAM = " + selected_ram) - - if selected_ram == "Default": - return None - else: - return selected_ram - + return self.qemu_ram_size(self.ui.combo_iso_boot_ram, + "QEMU: ISO RAM = %s") def qemu_usb_ram(self): """ Choose a ram size for USB booting. :return: Ram size as string. """ - selected_ram = self.ui.combo_usb_boot_ram.currentText() - log("QEMU: USB RAM = " + selected_ram) - - if selected_ram == "Default": - return None - else: - return selected_ram + return self.qemu_ram_size(self.ui.combo_usb_boot_ram, + "QEMU: USB RAM = %s") @staticmethod - def check_qemu_exist(): + def find_qemu(): """ - Check if QEMU is available on host system. + Check if QEMU is available on host system and return path of the binary :return: path to QEMU program or None otherwise. """ if platform.system() == "Linux": @@ -147,8 +141,7 @@ class Qemu(QtWidgets.QMainWindow, Ui_MainWindow): qemu = "" elif platform.system() == "Windows": - qemu = resource_path(os.path.join("data", "tools", "qemu", "qemu-system-x86_64.exe")) - log(qemu) + qemu = find_qemu_exe() if qemu: log("QEMU: using " + qemu) @@ -157,3 +150,17 @@ class Qemu(QtWidgets.QMainWindow, Ui_MainWindow): return qemu +def find_qemu_exe(): + exe_name = 'qemu-system-x86_64.exe' + if hasattr(config, 'qemu_exe_path'): + return config.qemu_exe_path + if not getattr(config, 'qemu_use_builtin', True): + for wellknown_path in [ + r'c:\Program Files\qemu', + r'd:\Program Files\qemu', + r'e:\Program Files\qemu', + ]: + exe_path = os.path.join(wellknown_path, exe_name) + if os.path.exists(exe_path): + return exe_path + return resource_path(os.path.join("data", "tools", "qemu", exe_name)) diff --git a/scripts/syslinux.py b/scripts/syslinux.py index 94f729c..f65ca75 100644 --- a/scripts/syslinux.py +++ b/scripts/syslinux.py @@ -13,11 +13,13 @@ from .gen import * from . import usb from .iso import * from . import config +from . import osdriver extlinux_path = os.path.join(multibootusb_host_dir(), "syslinux", "bin", "extlinux4") syslinux_path = os.path.join(multibootusb_host_dir(), "syslinux", "bin", "syslinux4") +syslinux_path = os.path.join(multibootusb_host_dir(), "syslinux", "bin", "syslinux4") mbr_bin = resource_path(os.path.join("data", "tools", "mbr.bin")) -win_gdisk = resource_path(os.path.join('data', 'tools', 'gdisk', 'gdisk.exe')) + # Force Linux to install extlinux on NTFS if platform.system() == 'Linux': extlinux_fs = ["ext2", "ext3", "ext4", "Btrfs", "NTFS", "ntfs"] @@ -25,6 +27,8 @@ if platform.system() == 'Linux': else: extlinux_fs = ["ext2", "ext3", "ext4", "Btrfs"] syslinux_fs = ["vfat", "ntfs", "FAT32", "NTFS"] + win_gdisk = resource_path(os.path.join( + 'data', 'tools', 'gdisk', 'gdisk.exe')) def gpt_part_table(usb_disk): @@ -90,6 +94,39 @@ def set_boot_flag(usb_disk): log("\nUnable to set legacy_boot flag on " + usb_disk[:-1], '\n') return False +def linux_install_default_bootsector(usb_disk, mbr_install_cmd): + + with usb.UnmountedContext(usb_disk, config.update_usb_mount): + syslinux_cmd = [syslinux_path, '-i', '-d', 'multibootusb', usb_disk] + if os.access(syslinux_path, os.X_OK) is False: + subprocess.call('chmod +x ' + syslinux_path, shell=True) + log("\nExecuting ==> %s\n" % syslinux_cmd) + config.status_text = 'Installing default syslinux version 4...' + if subprocess.call(syslinux_cmd) == 0: + + # On my system, it takes hours long to complete a single check + # So not included as of now + # usb.repair_vfat_filesystem(usb_disk) + + log("\nDefault syslinux install is success...\n") + config.status_text = 'Default syslinux successfully installed...' + log('\nExecuting ==> ' + mbr_install_cmd) + if subprocess.call(mbr_install_cmd, shell=True) == 0: + config.status_text = 'mbr install is success...' + log("\nmbr install is success...\n") + if set_boot_flag(usb_disk) is True: + return True + else: + log("\nFailed to install default syslinux...\n") + config.status_text = 'Failed to install default syslinux...' + return False + return None + + +def create_syslinux_bs(usb_disk, usb_mount): + osdriver.run_dd(osdriver.physical_disk(usb_disk), + os.path.join(usb_mount, 'multibootusb', 'syslinux.bin'), + 512, 1) def syslinux_default(usb_disk): """ @@ -107,18 +144,20 @@ def syslinux_default(usb_disk): usb_fs = usb_details['file_system'] usb_mount = usb_details['mount_point'] mbr_bin = get_mbr_bin_path(usb_disk) - if platform.system() == 'Linux': - mbr_install_cmd = 'dd bs=440 count=1 conv=notrunc if=' + mbr_bin + ' of=' + usb_disk[:-1] + mbr_install_cmd = 'dd bs=440 count=1 conv=notrunc if=' + mbr_bin \ + + ' of=' + usb_disk[:-1] else: - win_usb_disk_no = get_physical_disk_number(config.usb_disk) + win_usb_disk_no = get_physical_disk_number(usb_disk) _windd = resource_path(os.path.join("data", "tools", "dd", "dd.exe")) _input = "if=" + mbr_bin _output = 'of=\\\.\\physicaldrive' + str(win_usb_disk_no) mbr_install_cmd = _windd + ' ' + _input + ' ' + _output + ' count=1' + if usb_fs in extlinux_fs: - extlinu_cmd = extlinux_path + ' --install ' + os.path.join(usb_mount, 'multibootusb') + extlinu_cmd = extlinux_path + ' --install ' + \ + os.path.join(usb_mount, 'multibootusb') if os.access(extlinux_path, os.X_OK) is False: subprocess.call('chmod +x ' + extlinux_path, shell=True) log("\nExecuting ==> " + extlinu_cmd) @@ -132,30 +171,16 @@ def syslinux_default(usb_disk): config.status_text = 'mbr install is successful...' log("\nmbr install is success...\n") if set_boot_flag(usb_disk) is True: + create_syslinux_bs(usb_disk, usb_mount) return True elif usb_fs in syslinux_fs: if platform.system() == "Linux": - syslinux_cmd = syslinux_path + ' -i -d multibootusb ' + usb_disk - if os.access(syslinux_path, os.X_OK) is False: - subprocess.call('chmod +x ' + syslinux_path, shell=True) - log("\nExecuting ==> " + syslinux_cmd + "\n") - config.status_text = 'Installing default syslinux version 4...' - if subprocess.call(syslinux_cmd, shell=True) == 0: - log("\nDefault syslinux install is success...\n") - config.status_text = 'Default syslinux successfully installed...' - log('\nExecuting ==> ' + mbr_install_cmd) - if subprocess.call(mbr_install_cmd, shell=True) == 0: - config.status_text = 'mbr install is success...' - log("\nmbr install is success...\n") - if set_boot_flag(usb_disk) is True: - return True - else: - log("\nFailed to install default syslinux...\n") - config.status_text = 'Failed to install default syslinux...' - return False - + r = linux_install_default_bootsector(usb_disk, mbr_install_cmd) + if r: + create_syslinux_bs(usb_disk, usb_mount) + return r elif platform.system() == "Windows": syslinux = resource_path(os.path.join(multibootusb_host_dir(), "syslinux", "bin", "syslinux4.exe")) config.status_text = 'Installing default syslinux version 4...' @@ -180,12 +205,19 @@ def syslinux_default(usb_disk): ''' if config.usb_gpt is False: log('\nExecuting ==> ' + mbr_install_cmd) + # + # Updating mbr using dd results in catastrophy. + # Windows will lose track of the filesystem and + # the target volume will go away. + # Never enable this code without proper work around! + # if subprocess.call(mbr_install_cmd, shell=True) == 0: log("\nmbr install is success...\n") return True else: log('Disk uses GPT and mbr install is not required...') ''' + create_syslinux_bs(usb_disk, usb_mount) return True else: @@ -194,6 +226,54 @@ def syslinux_default(usb_disk): return False +def build_distro_bootsector(usb_disk, options, + distro_syslinux_install_dir, + distro_sys_install_bs): + with usb.UnmountedContext(config.usb_disk, config.update_usb_mount): + tmp_bs = build_distro_bootsector_impl( + usb_disk, options, distro_syslinux_install_dir) + if tmp_bs: + shutil.copy(tmp_bs, distro_sys_install_bs) + + +def build_distro_bootsector_impl(usb_disk, options, + distro_syslinux_install_dir): + syslinux_path = os.path.join( + multibootusb_host_dir(), "syslinux", "bin", "syslinux") \ + + config.syslinux_version + + if os.access(syslinux_path, os.X_OK) is False: + subprocess.call('chmod +x ' + syslinux_path, shell=True) + sys_cmd = [syslinux_path] + options + [ + distro_syslinux_install_dir, usb_disk] + log("Executing ==> %s" % sys_cmd) + if subprocess.call(sys_cmd) == 0: + config.status_text = \ + 'Syslinux install on distro directory is successful...' + log("\nSyslinux install on distro directory is successful...\n") + + # On my system, it takes hours long to complete a single check + # So not included as of now + # usb.repair_vfat_filesystem(usb_disk) + + tmp_bs_file = '/tmp/mbusb_temp.bs' + dd_cmd = ['dd', 'if=' + usb_disk, 'of=' + tmp_bs_file, 'count=1'] + log('Executing ==> %s' % dd_cmd + '\n') + config.status_text = 'Copying boot sector...' + config.status_text = 'Installing distro specific syslinux...' + if subprocess.call(dd_cmd) == 0: + config.status_text = 'Bootsector copy is successful...' + log("\nBootsector copy is successful...\n") + else: + config.status_text = 'Failed to copy boot sector...' + log("\nFailed to copy boot sector...\n") + return tmp_bs_file + else: + config.status_text = 'Failed to install syslinux on distro directory...' + log("\nFailed to install syslinux on distro directory...\n") + return None + + def syslinux_distro_dir(usb_disk, iso_link, distro): """ Install syslinux/extlinux on distro specific isolinux directory. @@ -224,60 +304,43 @@ def syslinux_distro_dir(usb_disk, iso_link, distro): if distro in ["generic"]: install_dir = usb_mount - distro_syslinux_install_dir = os.path.join(usb_mount, iso_linux_bin_dir.strip("/")).replace(usb_mount, "") - distro_sys_install_bs = os.path.join(install_dir, iso_linux_bin_dir.strip("/"), distro + '.bs') + distro_syslinux_install_dir = os.path.join( + usb_mount, iso_linux_bin_dir.strip("/")).replace(usb_mount, "") + distro_sys_install_bs = os.path.join( + install_dir, iso_linux_bin_dir.strip("/"), distro + '.bs') else: - install_dir = os.path.join(usb_mount, "multibootusb", iso_basename(iso_link)) - distro_syslinux_install_dir = os.path.join(install_dir, iso_linux_bin_dir.strip("/")).replace(usb_mount, "") - distro_sys_install_bs = os.path.join(install_dir, iso_linux_bin_dir.strip("/"), distro + '.bs') + install_dir = os.path.join(usb_mount, "multibootusb", + iso_basename(iso_link)) + distro_syslinux_install_dir = os.path.join( + install_dir, iso_linux_bin_dir.strip("/") + ).replace(usb_mount, "") + distro_sys_install_bs = os.path.join( + install_dir, iso_linux_bin_dir.strip("/"), distro + '.bs') + # log(distro_sys_install_bs) # log(distro_syslinux_install_dir) if usb_fs in syslinux_fs: - if config.syslinux_version == str(3): - if distro == "generic" and iso_linux_bin_dir == "/": - option = "" - else: - option = " -d " - else: - if distro == "generic" and iso_linux_bin_dir == "/": - option = " -i " - else: - option = " -i -d " - + options = ['-f'] + if config.syslinux_version != '3': + options.append('-i') + if not (distro == "generic" and iso_linux_bin_dir == "/"): + options.append('-d') if platform.system() == "Linux": - syslinux_path = os.path.join(multibootusb_host_dir(), "syslinux", "bin", "syslinux") + config.syslinux_version - if os.access(syslinux_path, os.X_OK) is False: - subprocess.call('chmod +x ' + syslinux_path, shell=True) - sys_cmd = syslinux_path + option + quote(distro_syslinux_install_dir) + ' ' + usb_disk - dd_cmd = 'dd if=' + usb_disk + ' ' + 'of=' + quote(distro_sys_install_bs) + ' count=1' - log("Executing ==> " + sys_cmd) - config.status_text = 'Installing distro specific syslinux...' - if subprocess.call(sys_cmd, shell=True) == 0: - config.status_text = 'Syslinux install on distro directory is successful...' - log("\nSyslinux install on distro directory is successful...\n") - log('Executing ==> ' + dd_cmd + '\n') - config.status_text = 'Copying boot sector...' - if subprocess.call(dd_cmd, shell=True) == 0: - config.status_text = 'Bootsector copy is successful...' - log("\nBootsector copy is successful...\n") - else: - config.status_text = 'Failed to copy boot sector...' - log("\nFailed to copy boot sector...\n") - else: - config.status_text = 'Failed to install syslinux on distro directory...' - log("\nFailed to install syslinux on distro directory...\n") - + build_distro_bootsector(usb_disk, options, + distro_syslinux_install_dir, + distro_sys_install_bs) elif platform.system() == "Windows": syslinux_path = resource_path(os.path.join(multibootusb_host_dir(), "syslinux", "bin")) + \ "\syslinux" + config.syslinux_version + ".exe" distro_syslinux_install_dir = "/" + distro_syslinux_install_dir.replace("\\", "/") distro_sys_install_bs = distro_sys_install_bs.replace("/", "\\") - sys_cmd = syslinux_path + option + distro_syslinux_install_dir + ' ' + usb_disk + ' ' + \ - distro_sys_install_bs - log("\nExecuting ==> " + sys_cmd + '\n') + sys_cmd = [syslinux_path] + options + \ + [distro_syslinux_install_dir, usb_disk, + distro_sys_install_bs] + log("\nExecuting ==> %s" % sys_cmd ) config.status_text = 'Installing distro specific syslinux...' - if subprocess.call(sys_cmd, shell=True) == 0: + if subprocess.call(sys_cmd) == 0: config.status_text = 'Syslinux install on distro directory is successful...' log("\nSyslinux install was successful on distro directory...\n") else: @@ -301,6 +364,8 @@ def syslinux_distro_dir(usb_disk, iso_link, distro): log("\nFailed to install syslinux on distro directory...\n") + + def replace_grub_binary(): """ This function checks if correct binary is installed on grub and EFI directory. diff --git a/scripts/udisks.py b/scripts/udisks.py index d1538df..8460d88 100644 --- a/scripts/udisks.py +++ b/scripts/udisks.py @@ -15,18 +15,25 @@ import os import re -def node_mountpoint(node): +def de_mangle_mountpoint(raw): + return raw.replace('\\040', ' ').replace('\\011', '\t') \ + .replace('\\012', '\n').replace('\\0134', '\\') - def de_mangle(raw): - return raw.replace('\\040', ' ').replace('\\011', '\t').replace('\\012', - '\n').replace('\\0134', '\\') +def node_mountpoint(node): for line in open('/proc/mounts').readlines(): line = line.split() if line[0] == node: - return de_mangle(line[1]) + return de_mangle_mountpoint(line[1]) return None +def find_mounted_partitions_on(disk): + assert not disk[-1:].isdigit() + with open('/proc/mounts') as f: + relevant_lines = [l.split(' ') for l in f.readlines() + if l.startswith(disk)] + return [ [v[0], de_mangle_mountpoint(v[1])] + v[2:] for v + in relevant_lines ] class NoUDisks1(Exception): pass @@ -51,27 +58,24 @@ class UDisks(object): return dbus.Interface(self.bus.get_object('org.freedesktop.UDisks', devpath), 'org.freedesktop.UDisks.Device') - def mount(self, device_node_path): + def mount(self, device_node_path, remounted=None): + mp = node_mountpoint(str(device_node_path)) + if mp: + return mp d = self.device(device_node_path) - try: - return str(d.FilesystemMount('', - ['auth_no_user_interaction', 'rw', 'noexec', 'nosuid', + r = str(d.FilesystemMount( + '', ['auth_no_user_interaction', 'rw', 'noexec', 'nosuid', 'nodev', 'uid=%d'%os.geteuid(), 'gid=%d'%os.getegid()])) - except: - # May be already mounted, check - mp = node_mountpoint(str(device_node_path)) - if mp is None: - raise - return mp + if remounted is not None: + remounted.append(True) + return r def unmount(self, device_node_path): d = self.device(device_node_path) d.FilesystemUnmount(['force']) def eject(self, device_node_path): - parent = device_node_path - while parent[-1] in '0123456789': - parent = parent[:-1] + parent = device_node_path.rstrip('0123456789') d = self.device(parent) d.DriveEject([]) @@ -132,24 +136,20 @@ class UDisks2(object): raise ValueError('%r not known to UDisks2'%device_node_path) - def mount(self, device_node_path): + def mount(self, device_node_path, remounted=None): + mp = node_mountpoint(str(device_node_path)) + if mp: + return mp d = self.device(device_node_path) mount_options = ['rw', 'noexec', 'nosuid', 'nodev'] - - try: - mp = str(d.Mount( - { - 'auth.no_user_interaction':True, - 'options':','.join(mount_options) + mp = str(d.Mount( + { + 'auth.no_user_interaction':True, + 'options':','.join(mount_options) }, dbus_interface=self.FILESYSTEM)) - print(mp) - except: - # May be already mounted, check - mp = node_mountpoint(str(device_node_path)) - if mp is None: - raise - + if remounted is not None: + remounted.append(True) return mp def unmount(self, device_node_path): diff --git a/scripts/uninstall_distro.py b/scripts/uninstall_distro.py index 39d495c..5f4f99f 100644 --- a/scripts/uninstall_distro.py +++ b/scripts/uninstall_distro.py @@ -99,9 +99,6 @@ def delete_frm_file_list(iso_file_list, uninstall_distro_dir_name): if os.path.exists(os.path.join(usb_mount, generic.strip("/"))): os.remove(os.path.join(usb_mount, generic.strip("/"))) gen.log('Removed files from ' + uninstall_distro_dir_name) - if platform.system() == 'Linux': - gen.log('Syncing....') - os.sync() @@ -109,7 +106,6 @@ def delete_frm_file_list(iso_file_list, uninstall_distro_dir_name): def do_uninstall_distro(target_distro, uninstall_distro_dir_name): """ Uninstall selected distro from selected USB disk. - :param config.usb_disk: Path of the USB disk :param target_distro: Generic name applied to distro to be uninstalled :param uninstall_distro_dir_name: Directory where the distro is installed :return: @@ -122,7 +118,6 @@ def do_uninstall_distro(target_distro, uninstall_distro_dir_name): usb_mount = usb_details['mount_point'] if platform.system() == 'Linux': - os.sync() # remove 'immutable' from files on ext2/3/4 fs if usb_mount: subprocess.call("chattr -i -R %s/* 2>/dev/null" % usb_mount, shell=True) @@ -163,8 +158,6 @@ def do_uninstall_distro(target_distro, uninstall_distro_dir_name): shutil.rmtree(os.path.join(usb_mount, "trk3")) if os.path.exists(uninstall_distro_dir_name_fullpath): - if platform.system() == 'Linux': - os.sync() shutil.rmtree(uninstall_distro_dir_name_fullpath) delete_frm_file_list(iso_file_list, uninstall_distro_dir_name) @@ -192,8 +185,6 @@ def update_sys_cfg_file(uninstall_distro_dir_name): Main function to remove uninstall distro specific operations. :return: """ - if platform.system() == 'Linux': - os.sync() sys_cfg_file = os.path.join(config.usb_mount, "multibootusb", "syslinux.cfg") if not os.path.exists(sys_cfg_file): @@ -215,8 +206,6 @@ def update_grub_cfg_file(uninstall_distro_dir_name): Main function to remove uninstall distro name from the grub.cfg file. :return: """ - if platform.system() == 'Linux': - os.sync() grub_cfg_file = os.path.join(config.usb_mount, "multibootusb", "grub", "grub.cfg") @@ -250,8 +239,6 @@ def uninstall_progress(): return usb_mount = usb_details['mount_point'] - if platform.system() == 'Linux': - os.sync() uninstall_distro_dir_name = config.uninstall_distro_dir_name \ .replace('\n', '') diff --git a/scripts/update_cfg_file.py b/scripts/update_cfg_file.py index 324f42f..8aae75f 100644 --- a/scripts/update_cfg_file.py +++ b/scripts/update_cfg_file.py @@ -135,6 +135,8 @@ def update_distro_cfg_files(iso_link, usb_disk, distro, persistence=0): usb_mount = usb_details['mount_point'] usb_uuid = usb_details['uuid'] usb_label = usb_details['label'] + usb_fs_type = usb_details['file_system'] + # iso_cfg_ext_dir = os.path.join(multibootusb_host_dir(), "iso_cfg_ext_dir") config.status_text = "Updating config files..." _iso_name = iso_basename(iso_link) @@ -143,17 +145,19 @@ def update_distro_cfg_files(iso_link, usb_disk, distro, persistence=0): log('Updating distro specific config files...') tweaker_params = ConfigTweakerParam( - _iso_name, install_dir_for_grub, - persistence, usb_uuid, usb_mount, usb_disk) + iso_link, install_dir_for_grub, + persistence, usb_uuid, usb_mount, usb_disk, usb_fs_type) tweaker_class_dict = { 'ubuntu' : UbuntuConfigTweaker, 'debian' : DebianConfigTweaker, 'debian-install' : DebianConfigTweaker, 'gentoo' : GentooConfigTweaker, - 'centos' : CentosConfigTweaker, - 'centos-install' : CentosConfigTweaker, + 'centos' : FedoraConfigTweaker, + 'centos-install' : FedoraConfigTweaker, + 'fedora' : FedoraConfigTweaker, 'antix' : AntixConfigTweaker, 'salix-live' : SalixConfigTweaker, + 'wifislax' : WifislaxConfigTweaker, } tweaker_class = tweaker_class_dict.get(distro) @@ -181,24 +185,6 @@ def update_distro_cfg_files(iso_link, usb_disk, distro, persistence=0): string = re.sub(r'file', 'cdrom-detect/try-usb=true floppy.allowed_drive_mask=0 ignore_uuid ignore_bootid root=UUID=' + usb_uuid + ' file', string) - elif distro == "fedora": - string = re.sub(r'root=\S*', 'root=live:UUID=' + usb_uuid, string) - if re.search(r'liveimg', string, re.I): - string = re.sub(r'liveimg', 'liveimg live_dir=/multibootusb/' + - iso_basename(iso_link) + '/LiveOS', string) - elif re.search(r'rd.live.image', string, re.I): - string = re.sub(r'rd.live.image', 'rd.live.image rd.live.dir=/multibootusb/' + - iso_basename(iso_link) + '/LiveOS', string) - elif re.search(r'Solus', string, re.I): - string = re.sub(r'initrd=', 'rd.live.dir=/multibootusb/' + iso_basename(iso_link) + - '/LiveOS initrd=', string) - - if persistence != 0: - if re.search(r'liveimg', string, re.I): - string = re.sub(r'liveimg', 'liveimg overlay=UUID=' + usb_uuid, string) - elif re.search(r'rd.live.image', string, re.I): - string = re.sub(r'rd.live.image', 'rd.live.image rw rd.live.overlay=UUID=' + usb_uuid, string) - string = re.sub(r' ro ', '', string) elif distro == 'kaspersky': if not os.path.exists(os.path.join(usb_mount, 'multibootusb', iso_basename(iso_link), 'kaspersky.cfg')): shutil.copyfile(resource_path(os.path.join('data', 'multibootusb', 'syslinux.cfg')), @@ -247,7 +233,7 @@ def update_distro_cfg_files(iso_link, usb_disk, distro, persistence=0): string) elif distro == "slax": string = re.sub(r'initrd=', - r'from=/multibootusb/' + iso_basename(iso_link) + '/slax fromusb initrd=', string) + r'from=/multibootusb/' + iso_basename(iso_link) + '/slax changes=/multibootusb/' + iso_basename(iso_link) + '/slax fromusb initrd=', string) elif distro == "finnix": string = re.sub(r'initrd=', r'finnixdir=/multibootusb/' + iso_basename(iso_link) + '/finnix initrd=', string) @@ -277,7 +263,7 @@ def update_distro_cfg_files(iso_link, usb_disk, distro, persistence=0): 'isodevice=/dev/disk/by-uuid/' + usb_uuid, string, flags=re.I) string = re.sub(r'isobasedir=', 'isobasedir=/multibootusb/' + iso_basename(iso_link) + '/', string, flags=re.I) - string = re.sub(r'ui gfxboot', '# ui gfxboot', string) # Bug in the isolinux package + string = commentout_gfxboot(string) string = string.replace('%INSTALL_DIR%', 'arch') if 'manjaro' in string: if not os.path.exists(os.path.join(usb_mount, '.miso')): @@ -288,7 +274,7 @@ def update_distro_cfg_files(iso_link, usb_disk, distro, persistence=0): 'kdeosisodevice=/dev/disk/by-uuid/' + usb_uuid, string, flags=re.I) string = re.sub(r'append', 'append kdeosisobasedir=/multibootusb/' + iso_basename(iso_link) + '/kdeos/', string, flags=re.I) - string = re.sub(r'ui gfxboot', '# ui gfxboot', string) # Bug in the isolinux package + string = commentout_gfxboot(string) elif distro in ["suse", "opensuse"]: if re.search(r'opensuse_12', string, re.I): string = re.sub(r'append', @@ -307,7 +293,7 @@ def update_distro_cfg_files(iso_link, usb_disk, distro, persistence=0): 'fromusb livecd=' + '/multibootusb/' + iso_basename(iso_link) + '/', string) string = re.sub(r'prompt', '#prompt', string) - string = re.sub(r'ui gfxboot.com', '#ui gfxboot.com', string) + string = commentout_gfxboot(string) string = re.sub(r'timeout', '#timeout', string) elif distro == "wifislax": string = re.sub(r'vmlinuz', @@ -432,58 +418,66 @@ def update_distro_cfg_files(iso_link, usb_disk, distro, persistence=0): gen.log('multibootusb EFI image already exist. Not copying...') +# Bug in the isolinux package +def commentout_gfxboot(input_text): + return re.sub(r'(ui\s+.*?gfxboot\.c32.*)$', r'# \1', input_text, + flags=re.I | re.MULTILINE) + def update_mbusb_cfg_file(iso_link, usb_uuid, usb_mount, distro): """ - Update main multibootusb suslinux.cfg file after distro is installed. + Update main multibootusb syslinux.cfg file after distro is installed. :return: """ - if platform.system() == 'Linux': - os.sync() log('Updating multibootusb config file...') + name_from_iso = iso_basename(iso_link) + name_of_iso = iso_name(iso_link) + _isolinux_bin_exists = isolinux_bin_exist(config.image_path) + _isolinux_bin_dir = isolinux_bin_dir(iso_link) sys_cfg_file = os.path.join(usb_mount, "multibootusb", "syslinux.cfg") - install_dir = os.path.join(usb_mount, "multibootusb", iso_basename(iso_link)) - if os.path.exists(sys_cfg_file): + install_dir = os.path.join(usb_mount, "multibootusb", name_from_iso) + label = name_from_iso + ('' if _isolinux_bin_exists else ' via GRUB') + if os.path.exists(sys_cfg_file): if distro == "hbcd": if os.path.exists(os.path.join(usb_mount, "multibootusb", "menu.lst")): _config_file = os.path.join(usb_mount, "multibootusb", "menu.lst") config_file = open(_config_file, "w") - string = re.sub(r'/HBCD', '/multibootusb/' + iso_basename(iso_link) + '/HBCD', _config_file) + string = re.sub(r'/HBCD', '/multibootusb/' + name_from_iso + '/HBCD', _config_file) config_file.write(string) config_file.close() with open(sys_cfg_file, "a") as f: f.write("#start " + iso_basename(config.image_path) + "\n") - f.write("LABEL " + iso_basename(config.image_path) + "\n") - f.write("MENU LABEL " + iso_basename(config.image_path) + "\n") - f.write("BOOT " + '/multibootusb/' + iso_basename(iso_link) + '/' + isolinux_bin_dir(iso_link).replace("\\", "/") + '/' + distro + '.bs' + "\n") + f.write("LABEL " + label + "\n") + f.write("MENU LABEL " + label + "\n") + f.write("BOOT " + '/multibootusb/' + name_from_iso + '/' + _isolinux_bin_dir.replace("\\", "/") + '/' + distro + '.bs' + "\n") f.write("#end " + iso_basename(config.image_path) + "\n") elif distro == "Windows": if os.path.exists(sys_cfg_file): config_file = open(sys_cfg_file, "a") - config_file.write("#start " + iso_basename(iso_link) + "\n") - config_file.write("LABEL " + iso_basename(iso_link) + "\n") - config_file.write("MENU LABEL " + iso_basename(iso_link) + "\n") + config_file.write("#start " + name_from_iso + "\n") + config_file.write("LABEL " + label + "\n") + config_file.write("MENU LABEL " + label + "\n") config_file.write("KERNEL chain.c32 hd0 1 ntldr=/bootmgr" + "\n") - config_file.write("#end " + iso_basename(iso_link) + "\n") + config_file.write("#end " + name_from_iso + "\n") config_file.close() elif distro == 'f4ubcd': if os.path.exists(sys_cfg_file): config_file = open(sys_cfg_file, "a") - config_file.write("#start " + iso_basename(iso_link) + "\n") - config_file.write("LABEL " + iso_basename(iso_link) + "\n") - config_file.write("MENU LABEL " + iso_basename(iso_link) + "\n") + config_file.write("#start " + name_from_iso + "\n") + config_file.write("LABEL " + label + "\n") + config_file.write("MENU LABEL " + label + "\n") config_file.write("KERNEL grub.exe" + "\n") config_file.write('APPEND --config-file=/multibootusb/' + iso_basename(config.image_path) + '/menu.lst' + "\n") - config_file.write("#end " + iso_basename(iso_link) + "\n") + config_file.write("#end " + name_from_iso + "\n") config_file.close() elif distro == 'kaspersky': if os.path.exists(sys_cfg_file): config_file = open(sys_cfg_file, "a") - config_file.write("#start " + iso_basename(iso_link) + "\n") - config_file.write("LABEL " + iso_basename(iso_link) + "\n") - config_file.write("MENU LABEL " + iso_basename(iso_link) + "\n") + config_file.write("#start " + name_from_iso + "\n") + config_file.write("LABEL " + label + "\n") + config_file.write("MENU LABEL " + label + "\n") config_file.write("CONFIG " + '/multibootusb/' + iso_basename(config.image_path) + '/kaspersky.cfg' + "\n") - config_file.write("#end " + iso_basename(iso_link) + "\n") + config_file.write("#end " + name_from_iso + "\n") config_file.close() elif distro == 'grub4dos': update_menu_lst() @@ -491,27 +485,30 @@ def update_mbusb_cfg_file(iso_link, usb_uuid, usb_mount, distro): update_grub4dos_iso_menu() else: config_file = open(sys_cfg_file, "a") - config_file.write("#start " + iso_basename(iso_link) + "\n") - config_file.write("LABEL " + iso_basename(iso_link) + "\n") - config_file.write("MENU LABEL " + iso_basename(iso_link) + "\n") + config_file.write("#start " + name_from_iso + "\n") + config_file.write("LABEL " + label + "\n") + config_file.write("MENU LABEL " + label + "\n") if distro == "salix-live": - if os.path.exists(os.path.join(config.usb_mount, 'multibootusb', iso_basename(iso_link), 'boot', 'grub2-linux.img')): + if os.path.exists( + os.path.join(install_dir, 'boot', 'grub2-linux.img')): config_file.write( - "LINUX " + '/multibootusb/' + iso_basename(iso_link) + '/boot/grub2-linux.img' + "\n") + "LINUX " + '/multibootusb/' + name_from_iso + + '/boot/grub2-linux.img' + "\n") else: - config_file.write("BOOT " + '/multibootusb/' + iso_basename(iso_link) + '/' + isolinux_bin_dir(iso_link).replace("\\", "/") + '/' + distro + '.bs' + "\n") + config_file.write("BOOT " + '/multibootusb/' + name_from_iso + '/' + _isolinux_bin_dir.replace("\\", "/") + '/' + distro + '.bs' + "\n") elif distro == "pclinuxos": - config_file.write("kernel " + '/multibootusb/' + iso_basename(iso_link) + '/isolinux/vmlinuz' + "\n") + config_file.write("kernel " + '/multibootusb/' + name_from_iso + + '/isolinux/vmlinuz' + "\n") config_file.write("append livecd=livecd root=/dev/rd/3 acpi=on vga=788 keyb=us vmalloc=256M nokmsboot " "fromusb root=UUID=" + usb_uuid + " bootfromiso=/multibootusb/" + - iso_basename(iso_link) + "/" + iso_name(iso_link) + " initrd=/multibootusb/" - + iso_basename(iso_link) + '/isolinux/initrd.gz' + "\n") + name_from_iso + "/" + name_of_iso + " initrd=/multibootusb/" + + name_from_iso + '/isolinux/initrd.gz' + "\n") elif distro == "memtest": - config_file.write("kernel " + '/multibootusb/' + iso_basename(iso_link) + '/BOOT/MEMTEST.IMG\n') + config_file.write("kernel " + '/multibootusb/' + name_from_iso + '/BOOT/MEMTEST.IMG\n') elif distro == "sgrubd2" or config.distro == 'raw_iso': config_file.write("LINUX memdisk\n") - config_file.write("INITRD " + "/multibootusb/" + iso_basename(iso_link) + '/' + iso_name(iso_link) + '\n') + config_file.write("INITRD " + "/multibootusb/" + name_from_iso + '/' + name_of_iso + '\n') config_file.write("APPEND iso\n") elif distro == 'ReactOS': @@ -529,30 +526,37 @@ def update_mbusb_cfg_file(iso_link, usb_uuid, usb_mount, distro): elif distro == 'memdisk_img': config_file.write(menus.memdisk_img_cfg(syslinux=True, grub=False)) else: - if isolinux_bin_exist(config.image_path) is True: + if _isolinux_bin_exists is True: if distro == "generic": - distro_syslinux_install_dir = isolinux_bin_dir(iso_link) - if isolinux_bin_dir(iso_link) != "/": - distro_sys_install_bs = os.path.join(usb_mount, isolinux_bin_dir(iso_link)) + '/' + distro + '.bs' + distro_syslinux_install_dir = _isolinux_bin_dir + if _isolinux_bin_dir != "/": + distro_sys_install_bs = os.path.join(usb_mount, _isolinux_bin_dir) + '/' + distro + '.bs' else: distro_sys_install_bs = '/' + distro + '.bs' else: distro_syslinux_install_dir = install_dir distro_syslinux_install_dir = distro_syslinux_install_dir.replace(usb_mount, '') - distro_sys_install_bs = distro_syslinux_install_dir + '/' + isolinux_bin_dir(iso_link) + '/' + distro + '.bs' + distro_sys_install_bs = distro_syslinux_install_dir + '/' + _isolinux_bin_dir + '/' + distro + '.bs' distro_sys_install_bs = "/" + distro_sys_install_bs.replace("\\", "/") # Windows path issue. if config.syslinux_version == '3': - config_file.write("CONFIG /multibootusb/" + iso_basename(iso_link) + '/' + isolinux_bin_dir(iso_link).replace("\\", "/") + '/isolinux.cfg\n') - config_file.write("APPEND /multibootusb/" + iso_basename(iso_link) + '/' + isolinux_bin_dir(iso_link).replace("\\", "/") + '\n') + config_file.write("CONFIG /multibootusb/" + name_from_iso + '/' + _isolinux_bin_dir.replace("\\", "/") + '/isolinux.cfg\n') + config_file.write("APPEND /multibootusb/" + name_from_iso + '/' + _isolinux_bin_dir.replace("\\", "/") + '\n') config_file.write("# Delete or comment above two lines using # and remove # from below line if " "you get not a COM module error.\n") config_file.write("#BOOT " + distro_sys_install_bs.replace("//", "/") + "\n") else: config_file.write("BOOT " + distro_sys_install_bs.replace("//", "/") + "\n") - - config_file.write("#end " + iso_basename(iso_link) + "\n") + else: + # isolinux_bin does not exist. + config_file.write('Linux /multibootusb/grub/lnxboot.img\n') + config_file.write('INITRD /multibootusb/grub/core.img\n') + config_file.write('TEXT HELP\n') + config_file.write('Booting via syslinux is not supported. ' + 'Please boot via GRUB\n') + config_file.write('ENDTEXT\n') + config_file.write("#end " + name_from_iso + "\n") config_file.close() # Update extlinux.cfg file by copying updated syslinux.cfg shutil.copy(os.path.join(usb_mount, 'multibootusb', 'syslinux.cfg'), @@ -612,15 +616,18 @@ def update_grub4dos_iso_menu(): f.write("#end " + iso_basename(config.image_path) + "\n") class ConfigTweakerParam: - def __init__(self, distro_name, distro_path, persistence_size, - usb_uuid, usb_mount, usb_disk): - self.distro_name = distro_name + # 'iso_link' is also known as 'image_path' + def __init__(self, iso_link, distro_path, persistence_size, + usb_uuid, usb_mount, usb_disk, usb_fs_type): + self.iso_fname = os.path.split(iso_link)[1] + self.distro_name = os.path.splitext(self.iso_fname)[0] assert distro_path[0] == '/' self.distro_path = distro_path # drive relative self.persistence_size = persistence_size self.usb_uuid = usb_uuid self.usb_mount = usb_mount self.usb_disk = usb_disk + self.usb_fs_type = usb_fs_type class ConfigTweaker: @@ -721,7 +728,8 @@ class ConfigTweaker: def fullpath(self, subpath): p = self.setup_params - return os.path.join(p.usb_mount, p.distro_path[1:], subpath) + return os.path.join(p.usb_mount, p.distro_path[1:], + subpath).replace('/', os.sep) def file_is_installed(self, subpath): return os.path.exists(self.fullpath(subpath)) @@ -733,6 +741,26 @@ class ConfigTweaker: with open(fp, errors='ignore') as f: return f.read() + def extract_distroinfo_from_file(self, subpath, regex, distro_group, + version_group): + content = self.file_content(subpath) + if not content: + return None + m = re.compile(regex, re.I).search(content) + if not m: + return None + return (m.group(distro_group), + [int(x) for x in m.group(version_group).split('.')]) + + def extract_distroinfo_from_fname(self, which_dir, regex, distro_group, + version_group): + p = re.compile(regex, re.I) + for fname in os.listdir(self.fullpath(which_dir)): + m = p.match(fname) + if m: + return (m.group(distro_group), + [int(x) for x in m.group(version_group).split('.')]) + return None class PersistenceConfigTweaker(ConfigTweaker): def __init__(self, pac_re, *args, **kw): @@ -818,8 +846,9 @@ class NoPersistenceTweaker(ConfigTweaker): class GentooConfigTweaker(NoPersistenceTweaker): def param_operations(self): + uuid_spec = 'UUID=%s' % self.setup_params.usb_uuid ops = [ - ([add_or_replace_kv('real_root=', self.setup_params.usb_disk), + ([add_or_replace_kv('real_root=', uuid_spec), add_tokens('slowusb'), add_or_replace_kv('subdir=', self.setup_params.distro_path), remove_keys('cdroot_hash='), @@ -833,6 +862,10 @@ class GentooConfigTweaker(NoPersistenceTweaker): ], starter_is_either('append', 'linux')), ] + fs_type = self.setup_params.usb_fs_type + if fs_type == 'vfat': + ops.append( (add_or_replace_kv('cdroot_type=', fs_type), + always) ) self.add_op_if_file_exists( ops, add_or_replace_kv, 'loop=', ['liberte/boot/root-x86.sfs', 'image.squashfs'], @@ -840,13 +873,13 @@ class GentooConfigTweaker(NoPersistenceTweaker): return ops -class CentosConfigTweaker(PersistenceConfigTweaker): +class FedoraConfigTweaker(PersistenceConfigTweaker): def __init__(self, *args, **kw): persistence_awareness_checking_re = re.compile( r'^\s*(%s).*?\s(rd.live.overlay|overlay)=.+?' % self.BOOT_PARAMS_STARTER, flags=re.I|re.MULTILINE) - super(CentosConfigTweaker, self).__init__( + super(FedoraConfigTweaker, self).__init__( persistence_awareness_checking_re, *args, **kw) def has_persistency_param(self, params): @@ -877,9 +910,9 @@ class CentosConfigTweaker(PersistenceConfigTweaker): (add_or_replace_kv( 'inst.repo=', 'hd:UUID=%s:%s' % ( - self.setup_params.usb_uuid, - self.setup_params.distro_path + '/' + - self.setup_params.distro_name + '.iso')), + self.setup_params.usb_uuid, + self.setup_params.distro_path + '/' + + self.setup_params.iso_fname)), starter_is_either('append', 'linux'))) return ops @@ -896,8 +929,15 @@ class CentosConfigTweaker(PersistenceConfigTweaker): class AntixConfigTweaker(NoPersistenceTweaker): def param_operations(self): - content = self.file_content('version') - if content and 0 <= content.find('antiX-17'): + dinfo = self.extract_distroinfo_from_file( + 'version', r'(antiX|MX)-(\d+\.\d+)', 1, 2) + if not dinfo: + dinfo = self.extract_distroinfo_from_file( + 'boot/isolinux/isolinux.cfg', r'(antiX|MX)-(\d+\.\d+)', 1, 2) + if not dinfo: + dinfo = self.extract_distroinfo_from_fname( + '', r'(MX)-(\d+\.\d+).*', 1, 2) + if dinfo and 17<=dinfo[1][0]: ops = [ add_or_replace_kv('buuid=', self.setup_params.usb_uuid), add_or_replace_kv('bdir=', @@ -907,6 +947,11 @@ class AntixConfigTweaker(NoPersistenceTweaker): self.setup_params.distro_path) return [(ops, starter_is_either('append', 'APPEND', 'linux'))] + def post_process(self, s): + s = re.sub(r'^(\s*UI\s+(.*?gfxboot(\.c32|)))\s+(.*?)\s+(.*)$', + r'# \1 \4.renamed-to-avoid-lockup \5', s, + flags=re.I + re.MULTILINE) + return s class SalixConfigTweaker(NoPersistenceTweaker): @@ -915,18 +960,26 @@ class SalixConfigTweaker(NoPersistenceTweaker): return None p = self.setup_params for replacee, replacer in [ - ('iso_path', "%s/%s.iso" % (p.distro_path, p.distro_name)), - ('initrd=', 'fromiso=%s/%s.iso initrd=' % ( - p.distro_path, p.distro_name)), + ('iso_path', "%s/%s" % (p.distro_path, p.iso_fname)), + ('initrd=', 'fromiso=%s/%s initrd=' % ( + p.distro_path, p.iso_fname)), ]: content = content.replace(replacee, replacer) return content + # salixlive-xfce-14.2.1 assumes that the installation media is + # labeled "LIVE" and the file tree is exploded at the root. + # (See /init for details.) Supporing it in harmony with installation + # of other distros is very hard to impossible. Do nothing here. + def param_operations(self): + return [] + +class WifislaxConfigTweaker(NoPersistenceTweaker): def param_operations(self): ops = [ - (add_or_replace_kv('livemedia=','%s:%s/%s.iso' % ( + (add_or_replace_kv('livemedia=','%s:%s/%s' % ( self.setup_params.usb_uuid, self.setup_params.distro_path, - self.setup_params.distro_name)), + self.setup_params.iso_fname)), starter_is_either('append', 'linux'))] return ops @@ -957,7 +1010,7 @@ def _test_tweak_objects(): '{usb-uuid}', usb_mount, usb_disk) debian_tweaker = DebianConfigTweaker('debian', setup_params_no_persistence) ubuntu_tweaker = UbuntuConfigTweaker('ubuntu', setup_params_no_persistence) - centos_tweaker = CentosConfigTweaker('centos', setup_params_no_persistence) + centos_tweaker = FedoraConfigTweaker('centos', setup_params_no_persistence) salix_tweaker = SalixConfigTweaker('centos', setup_params_no_persistence) # Test awareness on 'persistent' @@ -1038,7 +1091,7 @@ append foo""" 'debian', setup_params_persistent) ubuntu_persistence_tweaker = UbuntuConfigTweaker( 'ubuntu', setup_params_persistent) - centos_persistence_tweaker = CentosConfigTweaker( + centos_persistence_tweaker = FedoraConfigTweaker( 'centos', setup_params_persistent) print ("Testing if debian tweaker appends persistence parameters.") diff --git a/scripts/usb.py b/scripts/usb.py index 244894e..6121f0e 100644 --- a/scripts/usb.py +++ b/scripts/usb.py @@ -6,23 +6,28 @@ # Licence: This file is a part of multibootusb package. You can redistribute it or modify # under the terms of GNU General Public License, v.2 or above -import sys -import platform -import os -import shutil import collections import ctypes +import os +import platform +import shutil import subprocess +import sys +import time + +if platform.system()=='Linux': + import dbus + from . import config from . import gen +from . import osdriver + if platform.system() == 'Linux': from . import udisks UDISKS = udisks.get_udisks(ver=None) + if platform.system() == 'Windows': - import psutil import win32com.client -# import wmi - import pythoncom class PartitionNotMounted(Exception): @@ -120,7 +125,6 @@ def list_devices(fixed=False): except Exception as e: gen.log(e) - import dbus bus = dbus.SystemBus() try: # You should come here only if your system does'nt have udev installed. @@ -174,29 +178,14 @@ def list_devices(fixed=False): devices.sort() elif platform.system() == "Windows": - if fixed is True: - for drive in psutil.disk_partitions(): - if 'cdrom' in drive.opts or drive.fstype == '': - # Skip cdrom drives or the disk with no filesystem - continue - devices.append(drive[0][:-1]) - else: - try: - # Try new method using psutil. It should also detect USB 3.0 (but not tested by me) - for drive in psutil.disk_partitions(): - if 'cdrom' in drive.opts or drive.fstype == '': - # Skip cdrom drives or the disk with no filesystem - continue - if 'removable' in drive.opts: - devices.append(drive[0][:-1]) - except: - # Revert back to old method if psutil fails (which is unlikely) - oFS = win32com.client.Dispatch("Scripting.FileSystemObject") - oDrives = oFS.Drives - for drive in oDrives: - if drive.DriveType == 1 and drive.IsReady: - devices.append(drive) - + volumes = osdriver.wmi_get_volume_info_all() + devices = [] + for pdrive in osdriver.wmi_get_physicaldrive_info_all(): + if (not fixed) and pdrive.MediaType != 'Removable Media': + continue + devices.append(osdriver.win_physicaldrive_to_listbox_entry(pdrive)) + devices.extend([osdriver.win_volume_to_listbox_entry(d) + for d in volumes.get(pdrive.Index, [])]) if devices: return devices else: @@ -252,27 +241,29 @@ def details_udev(usb_disk_part): str(usb_disk_part)) return None - if b'Extended' in fdisk_cmd_out: - mount_point = '' - uuid = '' - file_system = '' - vendor = '' - model = '' - label = '' - devtype = "extended partition" - elif b'swap' in fdisk_cmd_out: + detected_type = None + for keyword, ptype in [(b'Extended', 'extended partition'), + (b'swap', 'swap partition'), + (b'Linux LVM', 'lvm partition'),]: + if keyword in fdisk_cmd_out: + detected_type = ptype + break + if detected_type: mount_point = '' uuid = '' file_system = '' vendor = '' model = '' label = '' - devtype = "swap partition" + devtype = detected_type elif device.get('DEVTYPE') == "partition": uuid = device.get('ID_FS_UUID') or "" file_system = device.get('ID_FS_TYPE') or "" label = device.get('ID_FS_LABEL') or "" - mount_point = UDISKS.mount(usb_disk_part) or "" + remounted = [] + mount_point = UDISKS.mount(usb_disk_part, remounted) or "" + if remounted and remounted[0]: + config.add_remounted(usb_disk_part) mount_point = mount_point.replace('\\x20', ' ') vendor = device.get('ID_VENDOR') or "" model = device.get('ID_MODEL') or "" @@ -338,6 +329,7 @@ def details_udisks2(usb_disk_part): else: try: mount_point = UDISKS.mount(usb_disk_part) + config.add_remounted(usb_disk_part) except: mount_point = "No_Mount" try: @@ -355,9 +347,8 @@ def details_udisks2(usb_disk_part): except: model = str('No_Model') if not mount_point == "No_Mount": - size_total = shutil.disk_usage(mount_point)[0] - size_used = shutil.disk_usage(mount_point)[1] - size_free = shutil.disk_usage(mount_point)[2] + size_total, size_used, size_free = \ + shutil.disk_usage(mount_point)[:3] else: raise PartitionNotMounted(usb_disk_part) @@ -393,77 +384,114 @@ def gpt_device(dev_name): :param dev_name: :return: True if GPT else False """ - if platform.system() == 'Windows': - partition, disk = gen.wmi_get_drive_info(dev_name) - is_gpt = partition.Type.startswith('GPT:') - gen.log('Device %s is a %s disk...' % - (dev_name, is_gpt and 'GPT' or 'MBR')) - config.usb_gpt = is_gpt - return is_gpt - if platform.system() == "Linux": - if gen.has_digit(dev_name): - _cmd_out = subprocess.check_output("parted " + dev_name[:-1] + " print", shell=True) - else: - _cmd_out = subprocess.check_output("parted " + dev_name + " print", shell=True) - if b'msdos' in _cmd_out: - config.usb_gpt = False - gen.log('Device ' + dev_name + ' is a MBR disk...') - return False - elif b'gpt' in _cmd_out: - config.usb_gpt = True - gen.log('Device ' + dev_name + ' is a GPT disk...') - return True + is_gpt = osdriver.gpt_device(dev_name) + config.usb_gpt = is_gpt + gen.log('Device %s is a %s disk.' % (dev_name, is_gpt and 'GPT' or 'MBR')) -def win_disk_details(disk_drive): - """ - Populate and get details of an USB disk under windows. Minimum required windows version is Vista. - :param disk_drive: USB disk like 'G:' - :return: See the details(usb_disk_part) function for return values. - """ - pythoncom.CoInitialize() - vendor = 'Not_Found' - model = 'Not_Found' - devtype = 'Not_Found' - selected_usb_part = str(disk_drive) - oFS = win32com.client.Dispatch("Scripting.FileSystemObject") - d = oFS.GetDrive(oFS.GetDriveName(oFS.GetAbsolutePathName(selected_usb_part))) - selected_usb_device = d.DriveLetter - if d.DriveType == 1: - devtype = "Removable Disk" - elif d.DriveType == 2: - devtype = "Fixed Disk" - label = (d.VolumeName).strip() - if not label.strip(): - label = "No_label" - mount_point = selected_usb_device + ":\\" - serno = "%X" % (int(d.SerialNumber) & 0xFFFFFFFF) - uuid = serno[:4] + '-' + serno[4:] - file_system = (d.FileSystem).strip() - size_total = shutil.disk_usage(mount_point)[0] - size_used = shutil.disk_usage(mount_point)[1] - size_free = shutil.disk_usage(mount_point)[2] - - # The below code works only from vista and above. I have removed it as many people reported that the software - # was not working under windows xp. Even then, it is significantly slow if 'All Drives' option is checked. - # Removing the code doesn't affect the functionality as it is only used to find vendor id and model of the drive. -# c = wmi.WMI() -# for physical_disk in c.Win32_DiskDrive(InterfaceType="USB"): -# for partition in physical_disk.associators("Win32_DiskDriveToDiskPartition"): -# for logical_disk in partition.associators("Win32_LogicalDiskToPartition"): -# if logical_disk.Caption == disk_drive: -# vendor = (physical_disk.PNPDeviceID.split('&VEN_'))[1].split('&PROD_')[0] -# model = (physical_disk.PNPDeviceID.split('&PROD_'))[1].split('&REV_')[0] +def unmount(usb_disk): + UDISKS.unmount(usb_disk) - return {'uuid': uuid, 'file_system': file_system, 'label': label, 'mount_point': mount_point, - 'size_total': size_total, 'size_used': size_used, 'size_free': size_free, - 'vendor': vendor, 'model': model, 'devtype': devtype} + +class RemountError(Exception): + def __init__(self, caught_exception, *args, **kw): + super(RemountError, self).__init__(*args, **kw) + self.caught_exception = caught_exception + + def __str__(self): + return "%s due to '%s'" % ( + self.__class__.__name__, self.caught_exception) -def details(usb_disk_part): +class UnmountError(RemountError): + def __init__(self, *args, **kw): + super(UnmountError, self).__init__(*args, **kw) + + +class MountError(RemountError): + def __init__(self, *args, **kw): + super(MountError, self).__init__(*args, **kw) + + +class UnmountedContext: + def __init__(self, usb_disk, exit_callback): + self.usb_disk = usb_disk + self.exit_callback = exit_callback + self.is_relevant = platform.system() != 'Windows' and \ + self.usb_disk[-1:].isdigit() + + def assert_no_access(self): + p = subprocess.Popen(['lsof', self.usb_disk], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + stdin=subprocess.PIPE) + output = p.communicate() + if len(output[0].strip()) != 0: + gen.log("Open handle exists.") + gen.log(output[0]) + raise UnmountError(Exception('open handle exists.')) + + def __enter__(self): + if not self.is_relevant: + return + self.assert_no_access() + try: + gen.log("Unmounting %s" % self.usb_disk) + os.sync() # This is needed because UDISK.unmount() can timeout. + UDISKS.unmount(self.usb_disk) + except dbus.exceptions.DBusException as e: + gen.log("Unmount of %s has failed." % self.usb_disk) + # This may get the partition mounted. Don't call! + # self.exit_callback(details(self.usb_disk)) + raise UnmountError(e) + gen.log("Unmounted %s" % self.usb_disk) + return self + + def __exit__(self, type_, value, traceback_): + if not self.is_relevant: + return + os.sync() # This should not be strictly necessary + time.sleep(1) # Yikes, mount always fails without this sleep(). + try: + mount_point = UDISKS.mount(self.usb_disk) + config.add_remounted(self.usb_disk) + self.exit_callback(details(self.usb_disk)) + except dbus.exceptions.DBusException as e: + raise MountError(e) + gen.log("Mounted %s" % (self.usb_disk)) + + +def check_vfat_filesystem(usb_disk, result=None): + p = subprocess.Popen(['fsck.vfat', '-n', usb_disk], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + stdin=subprocess.PIPE) + output = p.communicate() + gen.log("fsck.vfat -n returned %d" % p.returncode) + gen.log(b"fsck.vfat -n said:" + b'\n---\n'.join(f for f in output if f)) + if result is not None: + result.append((p.returncode, output, 'fsck.vfat -n')) + return len(output[0].split(b'\n'))==3 and output[1]==b'' \ + and p.returncode==0 + + +def repair_vfat_filesystem(usb_disk, result=None): + for args, input_ in [ + (['-a', usb_disk], None, ), + (['-r', usb_disk], b'1\ny\n', ), + ]: + cmd = ['fsck.vfat'] + args + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, stdin=subprocess.PIPE) + output = p.communicate(input=input_) + gen.log("%s returned %d" % (' '.join(cmd), p.returncode)) + gen.log(b"It said:" + b'\n---\n'.join(f for f in output if f)) + if result is not None: + result.append((p.returncode, output, ' '.join(cmd))) + return None + +def details(disk_or_partition): """ Populate and get details of an USB disk. - :param usb_disk_part: USB disk. Example.. "/dev/sdb1" on Linux and "D:\" on Windows. + :param disk_or_partition: USB disk. Example.. "/dev/sdb1" on Linux and "D:\" on Windows. :return: label == > returns name/label of an inserted USB device. mount_point == > returns mount path of an inserted USB device. uuid == > returns uuid of an inserted USB device. @@ -476,18 +504,20 @@ def details(usb_disk_part): model == > returns the model name of the USB. """ - assert usb_disk_part is not None + assert disk_or_partition is not None details = {} if platform.system() == 'Linux': try: - details = details_udev(usb_disk_part) + details = details_udev(disk_or_partition) except: - details = details_udisks2(usb_disk_part) + details = details_udisks2(disk_or_partition) elif platform.system() == 'Windows': - details = win_disk_details(usb_disk_part) - + if type(disk_or_partition) == int: + details = osdriver.wmi_get_physicaldrive_info_ex(disk_or_partition) + else: + details = osdriver.wmi_get_volume_info_ex(disk_or_partition) return details diff --git a/scripts/win32.py b/scripts/win32.py new file mode 100644 index 0000000..9d0693a --- /dev/null +++ b/scripts/win32.py @@ -0,0 +1,258 @@ +import collections +import ctypes +import io +import pywintypes +import struct +import sys +import time +import win32api +import win32con +import win32file +import winerror +import winioctlcon +import wmi + +from ctypes import wintypes +from functools import reduce + +kernel32 = ctypes.WinDLL('kernel32') # , use_last_error=True) + +kernel32.FindFirstVolumeW.restype = wintypes.HANDLE +kernel32.FindNextVolumeW.argtypes = (wintypes.HANDLE, + wintypes.LPWSTR, + wintypes.DWORD) +kernel32.FindVolumeClose.argtypes = (wintypes.HANDLE,) + +def FindFirstVolume(): + volume_name = ctypes.create_unicode_buffer(" " * 255) + h = kernel32.FindFirstVolumeW(volume_name, 255) + if h == win32file.INVALID_HANDLE_VALUE: + raise RuntimeError("FindFirstVolume() returned an invalid handle.") + return h, volume_name.value + +def FindNextVolume(hSearch): + volume_name = ctypes.create_unicode_buffer(" " * 255) + if kernel32.FindNextVolumeW(hSearch, volume_name, 255) != 0: + return volume_name.value + else: + errno = ctypes.GetLastError() + if errno == winerror.ERROR_NO_MORE_FILES: + FindVolumeClose(hSearch) + return None + raise RuntimeError("FindNextVolume failed (%s)" % errno) + +def FindVolumeClose(hSearch): + """Close a search handle opened by FindFirstVolume, typically + after the last volume has been returned. + """ + if kernel32.FindVolumeClose(hSearch) == 0: + raise RuntimeError("FindVolumeClose() failed.") + +def findAvailableDrives(): + return [(d, win32file.GetDriveType(d)) for d in + win32api.GetLogicalDriveStrings().rstrip('\0').split('\0')] + +def findNewDriveLetter(used_letters): + all_letters = set([chr(i) for i in range(ord('C'), ord('Z')+1)]) + return min(list(all_letters - set([s[0] for s in used_letters]))) + +def _openHandle(path, bWriteAccess, bWriteShare, + logfunc = lambda s: None): + TIMEOUT, NUM_RETRIES = 10, 20 + for retry_count in range(6): + try: + access_flag = win32con.GENERIC_READ | \ + (bWriteAccess and win32con.GENERIC_WRITE or 0) + share_flag = win32con.FILE_SHARE_READ | \ + (bWriteShare and win32con.FILE_SHARE_WRITE or 0) + handle = win32file.CreateFile( + path, access_flag, share_flag, None, + win32con.OPEN_EXISTING, win32con.FILE_ATTRIBUTE_NORMAL, None) + nth = { 0: 'first', 1:'second', 2:'third'} + logfunc("Opening [%s]: success at the %s iteration" % + (path, nth.get(retry_count, '%sth' % (retry_count+1)))) + return handle + except pywintypes.error as e: + logfunc('Exception=>'+str(e)) + if NUM_RETRIES/3 < retry_count: + bWriteShare = True + time.sleep(TIMEOUT / float(NUM_RETRIES)) + else: + raise RuntimeError("Couldn't open handle for %s." % path) + +def _closeHandle(h): + x = win32file.CloseHandle(h) + assert x != win32file.INVALID_HANDLE_VALUE + return x + +class openHandle: + def __init__(self, path, bWriteAccess, bWriteShare, + logfunc = lambda s: None): + self.path = path + self.bWriteAccess = bWriteAccess + self.bWriteShare = bWriteShare + self.logfunc = logfunc + self.h = None + + def __enter__(self): + self.h = _openHandle(self.path, self.bWriteAccess, self.bWriteShare, + self.logfunc) + return self + + def __exit__(self, type_, value, traceback_): + _closeHandle(self.h) + + def assert_physical_drive(self): + if self.path.lower().find('physicaldrive')<0: + raise RuntimeError("Handle is not one of a physical drive.") + + def LockPhysicalDrive(self): + self.assert_physical_drive() + lockPhysicalDrive(self.h, self.logfunc) + self.logfunc("Successfully locked '%s'" % self.path) + + + def ReadFile(self, size): + return win32file.ReadFile(self.h, size, None) + + def WriteFile(self, b): + return win32file.WriteFile(self.h, b, None) + + geometory_tuple = collections.namedtuple( + 'DiskGeometory', + ['number_of_cylinders', 'media_type', 'tracks_per_cylinder', + 'sectors_per_track', 'bytes_per_sector', 'disk_size']) + def DiskGeometory(self): + self.assert_physical_drive() + o = win32file.DeviceIoControl( + self.h, winioctlcon.IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, + None, 256, None) + return self.geometory_tuple(*struct.unpack('%s' % x) + x = win32file.DeleteVolumeMountPoint(volume_path+'\\') + log_func('DeleteVolumeMountPoint=>%s' % x) + else: + log_func('No volumes on %s' % target_drive) + + add1MB = False + hDrive.ZapMBRGPT(geom.disk_size, geom.bytes_per_sector, add1MB) + + +if __name__ == '__main__': + + # used_letters = [d for d in + # win32api.GetLogicalDriveStrings().rstrip('\0').split('\0')] + # print (used_letters) + # print (findNewDriveLetter(used_letters)) + # TargetDrive = 2 + # vinfo_list = [x for x in findVolumeGuids() + # if x.Extents and x.Extents[0].DiskNumber==TargetDrive] + + TargetDrive = 5 + with openHandle('\\\\.\\PhysicalDrive%d' % TargetDrive, True, False, + lambda s:sys.stdout.write(s+'\n')) as hDrive: + hDrive.CopyFrom('c:/Users/shinj/Downloads/salitaz-rolling_iso', + lambda b: None) diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 diff --git a/tests/test-distro.py b/tests/test-distro.py new file mode 100644 index 0000000..8283263 --- /dev/null +++ b/tests/test-distro.py @@ -0,0 +1,146 @@ +import sys +import unittest +from unittest.mock import MagicMock as MM, patch, mock_open + +sys.path = ['..'] + sys.path +from scripts import distro +from scripts import gen + +class DistoDetection(unittest.TestCase): + + def distro(self, isobin_exists, filelist_in_iso, input_text): + mock_isobin_exists = MM(return_value=isobin_exists) + mock_iso_list = MM(return_value=filelist_in_iso) + mock_os_walk = MM(return_value=[('/', [], ['grub.cfg'])]) + @patch('scripts.iso.isolinux_bin_exist', mock_isobin_exists) + @patch('os.walk', mock_os_walk) + @patch('scripts._7zip.list_iso', mock_iso_list) + @patch('builtins.open', mock_open(read_data=input_text)) + def test_when_isolinux_bin_is_available(): + return (distro.distro('{iso-cfg-dir}', 'ProDOS2.iso')) + return test_when_isolinux_bin_is_available() + + + def test_filebased_detection(self): + test_inputs = [ + ('f4ubcd', '', ['f4ubcd']), + ('memdisk_iso', '', []), + ('memdisk_iso', 'debian-installer', ['casper']), + ('debian-install', 'debian-installer', []), + ('alpine', '', ['alpine-release']), + ('memdisk_iso', '', ['']), + ] + for expected_distro, input_texts, file_list in test_inputs: + for input_text in input_texts.split('|'): + distro = self.distro(True, file_list, input_text) + assert distro==expected_distro, ( + "From \"%s&%s\", '%s' is expected but got '%s'" % + (input_text, file_list, expected_distro, distro)) + + + def test_detection_with_isobin(self): + test_inputs = [ + ('parted-magic', 'pmagic|partedmagic', True), + ('memdisk_iso', 'pmagic|partedmagic', False), + ('debian', 'boot=live', True), + ('memdisk_iso', 'boot=live', False), + ('sms', 'sms.jpg|vector |autoexec', True), + ('memdisk_iso', 'sms.jpg|vector |autoexec', False), + ] + for expected_distro, input_texts, isobin_exists in test_inputs: + for input_text in input_texts.split('|'): + distro = self.distro(isobin_exists, [], input_text) + assert distro==expected_distro, ( + "From \"%s&isobin=%s\", '%s' is expected but got '%s'" % + (input_text, isobin_exists, expected_distro, distro)) + + + def test_detection_isobin_agnostic(self): + test_inputs = [ + ('ubcd', 'ubcd'), + ('sgrubd2', 'Super Grub Disk'), + ('hbcd', 'hbcd'), + ('systemrescuecd', 'systemrescuecd'), + ('mageialive', 'mgalive'), + ('arch', 'archisolabel|misolabel|parabolaisolabel'), + ('chakra', 'chakraisolabel'), + ('kaos', 'kdeosisolabel'), + ('memdisk_iso', 'grml'), + ('grml', 'grml live-media-path=/dev/sda1'), + ('solydx', 'solydx'), + ('knoppix', 'knoppix'), + ('fedora', 'root=live:CDLABEL=|redcore'), + ('redhat', 'redhat'), + ('slitaz', 'slitaz|dban |ophcrack|tinycore' + '|rescue.cpi|xpud|untangle|4mlinux|partition wizard' + '|android-x86.png|riplinux|lebel dummy' + '|http://pogostick.net/~pnh/ntpasswd/|AVG Rescue CD' + '|AntivirusLiveCD|lkrn|Nanolinux|OSForensics' + '|minimal Slackware|Slackware-HOWTO'), + ('opensuse-install', 'class opensuse'), + ('ubuntu', 'boot=casper'), + ('wifislax', 'wifislax'), + ('slax', 'slax'), + ('antix', 'antix'), + ('porteus', 'porteus'), + ('pclinuxos', 'livecd=livecd|PCLinuxOS'), + ('gentoo', 'looptype=squashfs|http://dee.su/liberte'), + ('finnix', 'finnix'), + ('wifiway', 'wifiway'), + ('puppy', 'puppy|quirky|fatdog|slacko|xenialpup'), + ('ipcop', 'ipcop'), + ('ipfire', 'ipfire'), + ('zenwalk', 'zenwalk|slack|salix'), + ('salix-live', 'zenwalk live|live slack|live salix'), + ('zenwalk', 'zenwalk|slack|salix'), + ('puppy', 'zenwalk slacko|slacko slack'), + ('ubuntu-server', 'ubuntu server'), + ('centos', 'root=live:CDLABEL=CentOS'), + ('centos-install', 'Install CentOS'), + ('centos', 'CentOS'), + ('trinity-rescue', 'Trinity Rescue Kit'), + ('kaspersky', 'http://support.kaspersky.com'), + ('alt-linux', 'ALT Linux'), + ('Windows', 'Sergei Strelec'), + ('ReactOS', 'ReactOS'), + ('fsecure', 'fsecure'), + ('pc-unlocker', 'default rwp'), + ('pc-tool', '/system/stage1'), + ('grub2only', 'vba32rescue'), + ('rising-av', 'BOOT_IMAGE=rising'), + ('Avira-RS', 'Avira Rescue System'), + ('insert', 'BOOT_IMAGE=insert'), + ] + + for expected_distro, input_texts in test_inputs: + for input_text in input_texts.lower().split('|'): + distro = self.distro(False, [], input_text) + assert distro==expected_distro, ( + "From \"%s\", '%s' is expected but got '%s'" % + (input_text, expected_distro, distro)) + + + def test_distro_detection(self): + def os_path_exists(f): + if f.endswith('multibootusb.log'): + return False + return True + os_path_exists_mock = MM() + log_mock = MM() + @patch('os.path.exists', os_path_exists) + @patch('scripts.distro.log', log_mock) + def _(): + fn = distro.detect_iso_from_file_list + assert fn(['BOOT.wim', 'Sources']) == 'Windows' + assert fn(['BOOT.wim', 'Sause']) is None + assert fn(['config.isoclient', 'foo']) == 'opensuse' + assert fn(['bar', 'dban', 'foo']) == 'slitaz' + assert fn(['memtest.img']) == 'memtest' + assert fn(['mt86.png','isolinux']) == 'raw_iso' + assert fn(['menu.lst']) == 'grub4dos' + assert fn(['bootwiz.cfg', 'bootmenu_logo.png']) == \ + 'grub4dos_iso' + _() + +if __name__ == '__main__': + unittest.main()