From 364915c890336499796d0ac2d43b5a2f906eb2b9 Mon Sep 17 00:00:00 2001 From: moebius/ALUG <fs11linux.20.masuefke@spamgourmet.com> Date: Thu, 26 Mar 2015 15:25:38 +0100 Subject: [PATCH] Mass Storage Cloner for writing many USB sticks - 1st version --- MassStorageCloner/.gitignore | 1 + MassStorageCloner/README | 16 ++ MassStorageCloner/StorageObjects.py | 343 ++++++++++++++++++++++++ MassStorageCloner/StoreObjFrames.py | 291 +++++++++++++++++++++ MassStorageCloner/blkwrite.sh | 69 +++++ MassStorageCloner/dbusdetect.py | 207 +++++++++++++++ MassStorageCloner/gui.py | 391 ++++++++++++++++++++++++++++ MassStorageCloner/vtk.py | 223 ++++++++++++++++ MassStorageCloner/write.sh | 15 ++ 9 files changed, 1556 insertions(+) create mode 100644 MassStorageCloner/.gitignore create mode 100644 MassStorageCloner/README create mode 100644 MassStorageCloner/StorageObjects.py create mode 100644 MassStorageCloner/StoreObjFrames.py create mode 100755 MassStorageCloner/blkwrite.sh create mode 100644 MassStorageCloner/dbusdetect.py create mode 100755 MassStorageCloner/gui.py create mode 100644 MassStorageCloner/vtk.py create mode 100755 MassStorageCloner/write.sh diff --git a/MassStorageCloner/.gitignore b/MassStorageCloner/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/MassStorageCloner/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/MassStorageCloner/README b/MassStorageCloner/README new file mode 100644 index 0000000..1536920 --- /dev/null +++ b/MassStorageCloner/README @@ -0,0 +1,16 @@ +Mass Storage Cloner +(C)2014-2015 Martin Suefke +License: GPLv3 or newer, the GNU Public Licence, see http://www.gnu.org/licenses/gpl.html + +Use this tool on a linux machine to write any image file to a target device selected in a GUI. + +Prerequisites: +python 2.6 + Tk +xterm +pv +dd + +Select the source file by editing 'write.sh' +Start the program with 'python gui.py' + +First release 2015-03-26, version history in 'gui.py' diff --git a/MassStorageCloner/StorageObjects.py b/MassStorageCloner/StorageObjects.py new file mode 100644 index 0000000..7b163f4 --- /dev/null +++ b/MassStorageCloner/StorageObjects.py @@ -0,0 +1,343 @@ +# -*- coding: utf-8 -*- + + +""" + Storage Abstraction in Python Classes + (C) 2014 Martin Süfke + License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html +""" +# Proper logging +from pprint import pformat +import logging + +if __name__ == "__main__": + #logging.basicConfig(level=logging.WARN) + raise Exception("{0} cannot run as the main python program".format(__file__)) + +if len(logging.root.handlers) == 0: +# SPE compatible logging format + logging.basicConfig(level=logging.DEBUG,format='%(levelno)2d:%(funcName)s:%(message)s:File "%(filename)s", line %(lineno)d') +logger = logging.getLogger(__name__) + +import inspect + +from dbus import String as _dbus_String +from dbus import Array as _dbus_Array +from dbus import DBusException + +def Implement(msg=""): + logger.warn('Implement File "%s", line %s, %s'+msg,*inspect.stack()[1][1:4]) + +def DBusStringArrayToString(Array_Or_String): + if isinstance(Array_Or_String, _dbus_Array): + return ''.join(chr(c) for c in Array_Or_String if c <> 0).strip() + elif isinstance(Array_Or_String, str): + return Array_Or_String.rstrip('\x00').strip() + +class Storage(object): + """ Represents system data storage + may have multiple StorageDrives + """ + def __init__(self): + self._ChildClass = StorageDrive + self.StorageDrives = {} + + def AddDrive(self, UDisks2Path, UDisks2Dict, EjectFunc, PowerOffFunc): + if UDisks2Dict is None: + logger.error("AddDrive() Empty dictionary for %s",UDisks2Path) + return + self.StorageDrives[UDisks2Path] = self._ChildClass(self, UDisks2Path, UDisks2Dict, EjectFunc, PowerOffFunc) + + def RemoveDrive(self, UDisks2Path): + Drive = self.StorageDrives.pop(UDisks2Path, None) + if Drive is not None: + Drive._close() + else: + logger.warn("No Drive by Path %s", UDisks2Path) + + def AddBlockDevice(self, UDisks2Path, UDisks2Dict): + logger.debug('Storage U2d.k: %s', UDisks2Dict.keys()) + drive = UDisks2Dict[_dbus_String(u'Drive')] + self.StorageDrives[drive].AddBlockDevice(UDisks2Path, UDisks2Dict) + + def RemoveBlockDevice(self, UDisks2Path): + BlockDev = self.GetStorageBlockDeviceByPath(UDisks2Path) + if BlockDev is not None: + Drive = BlockDev.Parent + Drive.RemoveBlockDevice(UDisks2Path) + else: + logger.warn("No BlockDevice by Path %s", UDisks2Path) + + def AddFileSystem(self, UDisks2Path, UDisks2Dict, MountFunc, UnmountFunc): + if UDisks2Dict is None: + logger.info("AddFileSystem() Empty dictionary for %s",UDisks2Path) + return + #logger.debug("UDisks2Path = %s, Dict = %s)",UDisks2Path, UDisks2Dict) + BlockDev = self.GetStorageBlockDeviceByPath(UDisks2Path) + if BlockDev is None: + logger.warn("AddFileSystem( UDisks2Path = %s, Dict = %s) No BlockDevice",UDisks2Path, UDisks2Dict) + else: + BlockDev.AddFileSystem(UDisks2Path, UDisks2Dict, MountFunc, UnmountFunc) + + def RemoveFileSystem(self, UDisks2Path): + BlockDev = self.GetStorageBlockDeviceByPath(UDisks2Path) + if BlockDev is not None: + BlockDev.RemoveFileSystem(UDisks2Path) + else: + logger.warn("No BlockDevice for FileSytem Path %s", UDisks2Path) + + def UpdateMountPoints(self, UDisks2Path, UDisk2Mountpoints, UnmountFunc): + """Update all Filesystems matched by UDisks2Path to reflect (new) UDisk2Mountpoints""" + BlockDev = self.GetStorageBlockDeviceByPath(UDisks2Path) + if BlockDev is None: + logger.warn("UDisks2Path %s: No such BlockDevice",UDisks2Path) + else: + BlockDev.UpdateMountPoints(UDisks2Path, UDisk2Mountpoints, UnmountFunc) + + def GetStorageBlockDeviceByPath(self, aUDisks2Path): + """ Walk through all StorageDrives and look for a StorageBlockDevice with given UDisks2Path + Returns the BlockDevice + """ + for drive in self.StorageDrives.itervalues(): + blkdev = drive.GetStorageBlockDeviceByPath(aUDisks2Path) + if blkdev is not None: + return blkdev + return None + +class StorageDrive(object): + """ Represents a physical drive/device/medium + parent is Storage + may have multiple StorageBlockDevices + can Eject() -> will not detect drive any more until physical (?) reconnect + """ + def __init__(self, Parent, UDisks2Path, UDisks2Dict, EjectFunc, PowerOffFunc): + self._ChildClass = StorageBlockDevice + self.Parent = Parent + self.UDisks2Path = UDisks2Path + self.UDisks2Dict = UDisks2Dict + self.EjectFunc = EjectFunc + self.PowerOffFunc = PowerOffFunc + for name in "Id Size".split(): + setattr(self, name, UDisks2Dict.get(_dbus_String(unicode(name)))) + self.BlockDevices = {} + + def _close(self): + """ a manual destructor """ + pass + + def AddBlockDevice(self, UDisks2Path, UDisks2Dict): + if UDisks2Dict is None: + logger.error("AddBlockDevice() Empty dictionary for %s",UDisks2Path) + return + #logger.debug("AddBlockDevice( Path = %s, Dict.Keys = %s)", UDisks2Path, UDisks2Dict.keys()) + blockdev = self._ChildClass(self, UDisks2Path, UDisks2Dict) + self.BlockDevices[UDisks2Path] = blockdev + + def RemoveBlockDevice(self, UDisks2Path): + BlkDev = self.BlockDevices.pop(UDisks2Path, None) + if BlkDev is not None: + BlkDev._close() + else: + logger.warn("No BlockDevice by Path %s", UDisks2Path) + + def GetStorageBlockDeviceByPath(self, aUDisks2Path): + """ Get StorageBlockDevice by UDisks2Path """ + return self.BlockDevices.get(aUDisks2Path) + + def Eject(self): + logger.debug("StorageDrive.Eject() on %s",self.UDisks2Path) + if callable(self.EjectFunc): + self.EjectFunc(self.UDisks2Path) + else: + logger.error("StorageDrive.Eject has uncallable EjectFunc") + + def PowerOff(self): + logger.debug("StorageDrive.Poweroff() on %s",self.UDisks2Path) + if callable(self.PowerOffFunc): + self.PowerOffFunc(self.UDisks2Path) + else: + logger.error("StorageDrive.PowerOff has uncallable PowerOffFunc") + + + def __str__(self): + return '{0:s}( UDisks2Path = "{1:s}")'.format(self.__class__.__name__, self.UDisks2Path) + +class StorageBlockDevice(object): + """ Represents a block device '/dev/sdXYZ' + parent is StorageDrive + may have a single StorageFileSystem + can ReScan() -> does nothing (?) + """ + def __init__(self, Parent, UDisks2Path, UDisks2Dict=None, ChildClass=None): + assert UDisks2Dict is not None, "StorageBlockDevice( UDisks2Path = {0:s} ), UDisks2Dict is None. ".format(UDisks2Path) + #Implement(pformat([UDisks2Path, UDisks2Dict.keys()])) + #logger.debug("StorageBlockDevice.__init__(): ChildClass %s", ChildClass.__name__) + self._ChildClass = ChildClass or StorageFileSystem + self.Parent = Parent + self.UDisks2Path = UDisks2Path + self.UDisks2Dict = UDisks2Dict + self.Size = UDisks2Dict.get(_dbus_String(u'Size')) + logger.debug('Getting PreferredDevice') + self.PreferredDevice = DBusStringArrayToString(UDisks2Dict.get(_dbus_String(u'PreferredDevice'),'')) + + # Filesystem properties: IdUUID + IdLabel , IdUsage + IdType + IdVersion + # Crude hack: if IdUsage == Filesystem > "" then create a FS object + + # Even worse Hack for ease of use: + # Leave Filesytem alone for now, add it when AddFileSystem() is called + self.FileSystem = None + self.FsParms = dict() # keep these + for name in "IdUUID IdLabel IdUsage IdType IdVersion".split(): + self.FsParms[name] = str(UDisks2Dict.get(_dbus_String(unicode(name))).strip('\x00').strip()) + +# if len(self.FsParms['IdUsage'])>0: +# logger.info("got Filesystem info, not adding yet: %s using Class %s", +# pformat(self.FsParms), +# self._ChildClass.__name__ +# ) + + def _close(self): + """ a manual destructor """ + pass + + def AddFileSystem(self, UDisks2Path, UDisks2Dict, MountFunc, UnmountFunc): + """ Add the info in the "Filesystem" dictionary or UDisks2. + That means the mount points, basically + """ + if self.FileSystem is None: + self.FileSystem = self._ChildClass(self, MountFunc, **self.FsParms) + #logger.warn("Trying to AddFileSystemPoint() to StorageBlockDevice %s which has no (known) Filesystem",self.UDisks2Path) + #else: + self.FileSystem.AddMountPoints(UDisks2Path, UDisks2Dict, UnmountFunc) + + def RemoveFileSystem(self, UDisks2Path): + if UDisks2Path == self.UDisks2Path: + if self.FileSystem is not None: + fs=self.FileSystem + self.FileSystem = None + fs._close() + else: + logger.warn("Trying to remove non-existent filesystem by Path %s", UDisks2Path) + else: + logger.warn("No FileSystem by Path %s", UDisks2Path) + + def UpdateMountPoints(self, UDisks2Path, UDisk2Mountpoints, UnmountFunc): + """Update the BlockDevice's FileSystem to reflect (new) UDisk2Mountpoints""" + assert self.UDisks2Path == UDisks2Path, "UpdateMountPoints() with wrong UDisks2Path, expected {0!s}, got {1!s}".format(self.UDisks2Path, UDisks2Path) + assert self.FileSystem is not None, "UpdateMountPoints() FileSystem is None" + self.FileSystem.UpdateMountPoints(UDisks2Path, UDisk2Mountpoints, UnmountFunc) + + def ReScan(self): + Implement() + + def __str__(self): + return '{0:s}( UDisks2Path = "{1:s}")'.format(self.__class__.__name__, self.UDisks2Path) + +class StorageFileSystem(object): + """ Represents a Filesystem (vfat, ext, ...) + parent is BlockDevice + may have multiply StorageMountpoint + can Mount() + """ + def __init__(self, Parent, MountFunc, IdUUID, IdLabel, IdUsage, IdType, IdVersion): + self._ChildClass = StorageMountPoint + self.Parent = Parent + self.MountFunc = MountFunc # Call this to mount the filesystem + self.IdUUID = IdUUID + self.IdLabel = IdLabel + self.IdUsage = IdUsage + self.IdType = IdType + self.IdVersion = IdVersion + self.MountPoints = {} + + def _close(self): + """ a manual destructor """ + pass + + def AddMountPoints(self, UDisks2Path, UDisks2Dict, UnmountFunc): + """ Add all MountPoints based on the UDisks2 Filesystem dictionary """ + if UDisks2Path <> self.Parent.UDisks2Path: + logger.error("AddMountPoint() UDisks2Path given >%s< differs from Parent.Path >%s<.", UDisks2Path, self.Parent.UDisks2Path) + return + U2MountPoints = UDisks2Dict.get(_dbus_String(u'MountPoints'), {}) + for U2MPoint in U2MountPoints: + MountPoint = ''.join(chr(c) for c in U2MPoint if c <> 0).strip() + #logger.debug("got Mountpoints %s", MountPoint) + self.AddMountPoint(UDisks2Path, MountPoint, UnmountFunc) + + def AddMountPoint(self, UDisks2Path, MountPoint, UnmountFunc): + """ Add single MountPoint based on the UDisks2Path and the MountPoint directory """ + MountPointObj = self._ChildClass(self, UDisks2Path, MountPoint, UnmountFunc) + self.MountPoints[MountPoint]=MountPointObj + + def UpdateMountPoints(self, UDisks2Path, UDisk2Mountpoints, UnmountFunc): + """Update this FileSystem to reflect (new) UDisk2Mountpoints""" + if UDisks2Path <> self.Parent.UDisks2Path: + logger.error("UpdateMountPoints() UDisks2Path given >%s< differs from Parent.Path >%s<.", UDisks2Path, self.Parent.UDisks2Path) + return + NewMountpoints=list(s.strip('\x00') for s in UDisk2Mountpoints) + DelMountpoints=list() + + # First, kill all stale mountpoints + for oldmp in self.MountPoints: # keys are mount point paths + if oldmp in NewMountpoints: + logger.debug("known MP >%s<", oldmp) + NewMountpoints.remove(oldmp) + else: + logger.debug("stale MP to remove >%s<", oldmp) + DelMountpoints.append(oldmp) + + for delmp in DelMountpoints: + logger.debug("delete stale MP >%s<", delmp) + self.MountPoints.pop(delmp)._close() + # Now create new mountpoints + for newmp in NewMountpoints: + logger.debug("create new MP >%s<", newmp) + self.AddMountPoint(UDisks2Path, newmp, UnmountFunc) + + def __str__(self): + return '{0:s}( UDisks2Path = "{1:s}")'.format(self.__class__.__name__, self.Parent.UDisks2Path) + + def Mount(self, *args, **kwargs): + logger.debug("StorageMountPoint.Mount args: %s %s",args, kwargs) + if callable(self.MountFunc): + try: + self.MountFunc(self.Parent.UDisks2Path) + except DBusException as dbe: + logger.warn(dbe.message) + else: + logger.error("StorageMountPoint.Mount has uncallable MountFunc") + +class StorageMountPoint(object): + """ Represents a Filesystem (vfat, ext, ...) + parent is StorageFileSystem + can UnMount() + """ + def __init__(self, Parent, UDisks2Path, MountPoint, UnmountFunc): + self.Parent = Parent + self.UDisks2Path = UDisks2Path + self.MountPoint = MountPoint + self.UnmountFunc = UnmountFunc + #Implement() + +# def __del__(self): +# logger.debug("%s.__del__()",self.__class__.__name__) + + def _close(self): + """ A 'manual' destructor""" + logger.debug("%s _close for %s",self.__class__.__name__,str(self)) + + def __str__(self): + return '{0:s}( UDisks2Path = "{1:s}")'.format(self.__class__.__name__, self.UDisks2Path) + + def Unmount(self, *args, **kwargs): + logger.debug("StorageMountPoint.Unmount args: %s %s", args, kwargs) + if callable(self.UnmountFunc): + try: + self.UnmountFunc(self.UDisks2Path) + except DBusException as dbe: + logger.warn(dbe.message) + else: + logger.error("StorageMountPoint.Unmount has uncallable UnmountFunc") + +#end; diff --git a/MassStorageCloner/StoreObjFrames.py b/MassStorageCloner/StoreObjFrames.py new file mode 100644 index 0000000..e216120 --- /dev/null +++ b/MassStorageCloner/StoreObjFrames.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- + +""" + TK Frames for Representation of Storage Abstraction + (C) 2014 Martin Süfke + License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html +""" +# Proper logging +from pprint import pprint, pformat +import logging +#import os # A dirty hack here +import subprocess +import shlex +import signal # future + + +if __name__ == "__main__": + #logging.basicConfig(level=logging.WARN) + raise Exception("{0} cannot run as the main python program".format(__file__)) + +if len(logging.root.handlers) == 0: +# SPE compatible logging format + logging.basicConfig(level=logging.DEBUG,format='%(levelno)2d:%(funcName)s:%(message)s:File "%(filename)s", line %(lineno)d') +logger = logging.getLogger(__name__) + +import inspect + +import StorageObjects + +import Tkinter as Tk +#from Tkinter import BooleanVar, IntVar, DoubleVar, StringVar +#import tkMessageBox as dialog +#import tkFileDialog as filedialog +from Tkconstants import N, E, S, W, HORIZONTAL, LEFT, RIGHT, TOP, BOTTOM, \ + BOTH, NORMAL, DISABLED, X, Y + +def Implement(msg=""): + logger.warn('Implement File "%s", line %s, %s'+msg,*inspect.stack()[1][1:4]) + +def SizeStr(size): + """ Print size with thousands separators """ + return "{0:,d} bytes".format(size) + +class TkFStorage(StorageObjects.Storage): + """ Tk Frame for Storage """ + def __init__(self, ParentFrame): + #logger.debug(self.__class__.__name__) + super(self.__class__,self).__init__() + self._ChildClass = TkFDrive + self.TKFrame = Tk.Frame(ParentFrame, borderwidth=2, relief='groove') + self.TKFrame.pack(side=TOP, fill=X) + #Tk.Label(self.TKFrame,text=self.__class__.__name__).pack(side=LEFT, fill=X) + + def FilterViewDriveAdd(self, MoreFiltersAsDict): + """ Adds view filters on drives + example: FilterViewDriveAdd(self, {'Removable': True}) + """ + #logger.debug("Num Drives %d", len(self.StorageDrives)) + for k,v in self.StorageDrives.iteritems(): +# logger.debug("\nDrive %s %s", k, str(v)) + v.FilterViewAdd(MoreFiltersAsDict) +# TODO: Idea here is to retain the order of hidden/visible drives. +# That may not be possible in all circumstances +# Also, is may not be desirable. When showing devices, add them at the TOP +# f = v.TKFrame +# x = f.tk.call('pack', 'info', str(f)) +## f._saved_w = f._w +## f._w = str(f) +## logger.debug("repr(f) %s",repr(f)) +## logger.debug(" str(f) %s", str(f)) +## x = f.pack_info() +## f._saved_w = f._w +## v.TKFrame.pack_forget() +# logger.debug("Pack_info: %s", x) +# +# logger.debug("Slave Frames %s", self.TKFrame.pack_slaves()) + + def FilterViewDriveRemove(self, FilterKeysAsList): + """ Removes view filters from drives + example: FilterViewDriveRemove(self, ['Removable', 'Ejectable']}) + """ + for k,v in self.StorageDrives.iteritems(): + v.FilterViewRemove(FilterKeysAsList) + +class TkFDrive(StorageObjects.StorageDrive): + """ Tk Frame for Drive """ + def __init__(self, *args, **kwargs): + """ + Pass argment to StorageObjects.StoragedDrive + + FilterView = {} : + Dictionary of Key = Value pairs that make this entry invisible if found inside UDisks2Dict + """ + #logger.debug(self.__class__.__name__) + self.ViewFilter = kwargs.pop('FilterView', {}) + super(self.__class__,self).__init__(*args, **kwargs) + self._ChildClass = TkFBlockdevice + self.TKFrame = Tk.Frame(self.Parent.TKFrame, borderwidth=2, relief='groove', background='light blue') + self.visible = False # Not pack()'ed yet + self.FilterView() # leads to self.TKFrame.Pack() if visible + Tk.Label(self.TKFrame,text="Drive", background='light blue').pack(side=LEFT, anchor="nw") + InfoFrame = Tk.Frame(self.TKFrame, borderwidth=2) + Tk.Button(InfoFrame,text="ignore", command = self.OnBtnIgnore, background='light blue').pack(side=RIGHT, anchor="e") + if self.UDisks2Dict.get('Ejectable',False): + Tk.Button(InfoFrame,text="Eject", command = self.OnBtnEject).pack(side=RIGHT, anchor="e") + if self.UDisks2Dict.get('CanPowerOff',False): + Tk.Button(InfoFrame,text="PowerOff", command = self.OnBtnPowerOff).pack(side=RIGHT, anchor="e") + Tk.Label( InfoFrame,text=self.Id).pack(anchor="nw") + # Size as a str ?!? + try: + Tk.Label( InfoFrame,text=SizeStr(self.Size)).pack(anchor="nw") + except ValueError: + logger.debug("self.Size: s:%s r:%s",str(self.Size), repr(self.Size)) + InfoFrame.pack(side=TOP, fill=X, padx=5, pady=5) + + def _close(self): + """ A manual destructor""" + self.TKFrame.pack_forget() # Remove visible component + self.TKFrame.destroy() # Kill visible component + self.TKFrame = None + super(self.__class__,self)._close() + + def OnBtnEject(self): + self.Eject() + + def OnBtnPowerOff(self): + self.PowerOff() + + def OnBtnIgnore(self): + #self.Ignore = True # Make a Filter that always makes thisone ingnore itself. + self.FilterViewAdd({'Id':self.UDisks2Dict['Id']}) + #logger.debug("UDisk2Dict: %s", pformat(self.UDisks2Dict)) + #self.FilterMakeInvisible() + + def FilterViewAdd(self, MoreFiltersAsDict): + """ Adds view filters on this drive + Filtered drives will not show themselves in the main gui. + example: FilterViewDriveAdd(self, {'Removable': True}) + see also: FilterViewRemove, FilterView + """ + self.ViewFilter.update(MoreFiltersAsDict) + self.FilterView() + + def FilterViewRemove(self, FilterKeysAsList): + """ Removes view filters from this drive + example: FilterViewDriveRemove(self, ['Removable', 'Ejectable']}) + see also: FilterViewAdd, FilterView + """ + for k in FilterKeysAsList: + if k in self.ViewFilter: + del self.ViewFilter[k] + self.FilterView() + + def FilterView(self): + """ Apply Filter against this drive + see also: FilterViewAdd, FilterViewRemove + """ + visible = True + for key, test in self.ViewFilter.iteritems(): + # A little suprising, but self.UDisks2Dict[dbus.String(u'Foo')] is as + # good as self.UDisks2Dict['Foo'] + #logger.debug("Testing key %s",key) + if self.UDisks2Dict.has_key(key): + #logger.debug("key found, value is %s", self.UDisks2Dict[key]) + #logger.debug("Testing %s == %s", test, self.UDisks2Dict[key] == test) + if self.UDisks2Dict[key] == test: + visible = False + if visible: + self.FilterMakeVisible() + else: + self.FilterMakeInvisible() + + def FilterMakeVisible(self): + if not self.visible: + #logger.debug("") + self.TKFrame.pack(side=BOTTOM, fill=X) # Pack on Bottom, so that new entries end up on top. + self.visible = True + + def FilterMakeInvisible(self): + if self.visible: + #logger.debug("") + self.TKFrame.pack_forget() + self.visible = False + +class TkFBlockdevice(StorageObjects.StorageBlockDevice): + """ Tk Frame for Blockdevice """ + def __init__(self, *args, **kwargs): + #logger.debug(self.__class__.__name__) + #logger.debug("args %s kwargs %s", args, kwargs) + kwargs['ChildClass'] = TkFFilesystem + super(self.__class__,self).__init__(*args, **kwargs) + self.TKFrame = Tk.Frame(self.Parent.TKFrame, borderwidth=2, relief='groove', background='light green') + self.TKFrame.pack(side=BOTTOM, fill=X) + Tk.Label(self.TKFrame,text="BlkDev", background='light green').pack(side=LEFT, anchor="nw") + InfoFrame = Tk.Frame(self.TKFrame, borderwidth=2, relief='flat') + #Tk.Label(self.TKFrame,text=self.__class__.__name__).pack(side=TOP, fill=X) + Tk.Button(InfoFrame, text="Write", command=self.TkCmdWrite).pack(side=RIGHT) + Tk.Label( InfoFrame, text="{0:s}".format(self.PreferredDevice)).pack(anchor="nw") + Tk.Label( InfoFrame, text="Size "+SizeStr(self.Size)).pack(anchor="nw") + InfoFrame.pack(side=TOP, fill=X, padx=5, pady=5) + + def _close(self): + """ A manual destructor""" + self.TKFrame.pack_forget() # Remove visible component + self.TKFrame.destroy() # Kill visible component + self.TKFrame = None + super(self.__class__,self)._close() + + def TkCmdWrite(self): + logger.debug("Write to %s -> execute write.sh %s", self.PreferredDevice, self.PreferredDevice) + # TODO: This would need a nice subprocess.popen() ... + #os.system("./write.sh %s"%(self.PreferredDevice)) + + if True: #not self.proc: + cmdarray=" ".join( [ + "./write.sh %s"%(self.PreferredDevice), + ] ) + + args = shlex.split(cmdarray) + + self.proc = subprocess.Popen(args, + #bufsize = 1, # line buffered + bufsize = 4096, # byte buffered + stdin = None, + stdout = None, #subprocess.PIPE, + stderr = None, #subprocess.STDOUT, + # preexec_fn unneeded + close_fds = True, # Do not leak file descriptors + #cwd = Current Working Directory + #env Environment mapping + ) + + logger.debug("write.sh executed") + +class TkFFilesystem(StorageObjects.StorageFileSystem): + """ Tk Frame for Filesystem """ + def __init__(self, *args, **kwargs): + #logger.debug(self.__class__.__name__) + super(self.__class__,self).__init__(*args, **kwargs) + self._ChildClass = TkFMountpoint + self.TKFrame = Tk.Frame(self.Parent.TKFrame, borderwidth=2, relief='groove') + self.TKFrame.pack(side=TOP, fill=X) + InfoFrame = Tk.Frame(self.TKFrame, borderwidth=2, relief='flat') + #Tk.Label(self.TKFrame,text="Filesystem").pack(anchor="nw") + Tk.Button(InfoFrame,text="Mount", command=self.TkCmdMount).pack(side=RIGHT) + Tk.Label( InfoFrame,text=self.IdUsage + " " + self.IdType + " " + self.IdVersion).pack(anchor="nw") + Tk.Label( InfoFrame,text=self.IdUUID + " " + self.IdLabel).pack(anchor="nw") + InfoFrame.pack(side=TOP, fill=X) + + def _close(self): + """ A manual destructor""" + self.TKFrame.pack_forget() # Remove visible component + self.TKFrame.destroy() # Kill visible component + self.TKFrame = None + super(self.__class__,self)._close() + + def TkCmdMount(self, *args, **kwargs): + #logger.debug("TkFFilesystem args:%s %s",args,kwargs) + self.Mount() + + #logger.debug("TkFFilesystem args:%s %s",args,kwargs) + +class TkFMountpoint(StorageObjects.StorageMountPoint): + """ Tk Frame for Mountpoint """ + def __init__(self, *args, **kwargs): + #logger.debug("args %s kwargs %s", args, kwargs) + #logger.debug(self.__class__.__name__) + super(self.__class__,self).__init__(*args, **kwargs) + self.TKFrame = Tk.Frame(self.Parent.TKFrame, borderwidth=2, relief='groove', background='#FFC4C4') + self.TKFrame.pack(side=TOP, fill=X, ipadx=5, ipady=2) + Tk.Button(self.TKFrame,text="Unmount", command=self.TkCmdUnmount).pack(side=RIGHT) + Tk.Label(self.TKFrame,text="mounted on " + self.MountPoint).pack(side=LEFT) + +# def __del__(self): +# logger.debug("%s.__del__()",self.__class__.__name__) +# super(self.__class__,self).__del__() + + def _close(self): + """ A 'manual' destructor""" + #logger.debug("%s _close for %s",self.__class__.__name__,str(self)) + self.TKFrame.pack_forget() # Remove visible component + self.TKFrame.destroy() # Kill visible component + self.TKFrame = None + super(self.__class__,self)._close() + + def TkCmdUnmount(self, *args, **kwargs): + #logger.debug("TkFMountpoint args:%s %s",args,kwargs) + self.Unmount() + + +#end; \ No newline at end of file diff --git a/MassStorageCloner/blkwrite.sh b/MassStorageCloner/blkwrite.sh new file mode 100755 index 0000000..4003234 --- /dev/null +++ b/MassStorageCloner/blkwrite.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# +# Mass Storage Cloner - blkwrite.sh +# (C)2015 Martin Suefke +# License: GPLv3 or newer +# +# writes source file $1 to block device $2 +# +# DO NOT Customize this file. Try to customize write.sh first +# + +set -e # break if error + +function onerror() { + echo + echo "Trapped error while copying >$SRC< to >$TGT< Result code is \"$res\"". + echo -n "Press Enter to Exit this script ..." + read -n1 + trap EXIT + exit 1 +} + +trap onerror ERR EXIT + +echo "Writing >$1< to >$2<" +if [ $# -lt 2 ] +then + echo "Need 2 parameters: source target" + exit 1 +fi + +SRC="$1" +TGT="$2" + +if [ ! -b "$TGT" ] +then + echo "need a blockdevice as parameter 2" + exit 2 +fi + +if [ "x${TGT%%[0-9]*}" == "x$TGT" ] +then + # have a "master" block device + echo "removing partitions from $TGT with partx" + partx -d "$TGT" || true +fi + +echo "Start copy operation" +# live version +pv -trabe -B8m $SRC | dd bs=8M iflag=fullblock of="$TGT" oflag=sync +# debug version +#pv -trabe -B8m $SRC | dd bs=8M iflag=fullblock of=/dev/null oflag=sync +res="$?" +echo "Result code $res" + +if [ "x${TGT%%[0-9]*}" == "x$TGT" ] +then + # have a "master" block device + echo "adding partitions for $TGT with partx" + partx -a "$TGT" +fi + +if [ x$res == x0 ] +then + trap EXIT +else + onerror +fi +#end; diff --git a/MassStorageCloner/dbusdetect.py b/MassStorageCloner/dbusdetect.py new file mode 100644 index 0000000..d4a2cbb --- /dev/null +++ b/MassStorageCloner/dbusdetect.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- + +""" + DBus Removable Mass Storage Detection + (C) 2014 Martin Süfke + License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html + partly based on http://stackoverflow.com/questions/23244245/listing-details-of-usb-drives-using-python-and-udisk2 +""" + +import logging + +if len(logging.root.handlers) == 0: +# SPE compatible logging format + logging.basicConfig(level=logging.DEBUG,format='%(levelno)2d:%(funcName)s:%(message)s:File "%(filename)s", line %(lineno)d') +logger = logging.getLogger(__name__) + +from pprint import pformat#, pprint +#from StorageObjects import Storage + +import dbus +from dbus.mainloop.glib import DBusGMainLoop +dbus.mainloop.glib.threads_init() +DBusGMainLoop(set_as_default=True) +#dbus.set_default_main_loop() +bus = dbus.SystemBus() +ud_manager_obj = bus.get_object('org.freedesktop.UDisks2', '/org/freedesktop/UDisks2') +om = dbus.Interface(ud_manager_obj, 'org.freedesktop.DBus.ObjectManager') + +def GetDriveId(DriveDict): + """ Input a Drive dict (from key = value) returned from a GetManagedObjects() call + Output: UDisks2.Drive.Id + """ + try: + return DriveDict[ + dbus.String(u'org.freedesktop.UDisks2.Drive')][ + dbus.String(u'Id') + ] + except: + return "no [org.freedesktop.UDisks2.Drive][Id] in "+pformat(DriveDict) + + +def GetDrive(BlockDict): + """ Input a BlockDev dict (from key = value) returned from a GetManagedObjects() call + Output: UDisks2.Drive dict + """ + try: + return BlockDict[dbus.String(u'org.freedesktop.UDisks2.Block')][dbus.String(u'Drive')] + except: + return "no [org.freedesktop.UDisks2.Block][Drive] in "+pformat(BlockDict) + +def ScanDevices(AllStorage = None, RequireRemovable = False): + global om + udisks_path_drives={} + udisks_path_drives_removable={} + udisks_path_blockdev={} + + ManagedObjects=om.GetManagedObjects() + for k,v in ManagedObjects.iteritems(): + #print "" + #print "Key:",k + d=dict(v) + #print "dict:", isinstance(v, dict) + udisks_is_blockdev = v.has_key(dbus.String(u'org.freedesktop.UDisks2.Block')) + udisks_is_drive = v.has_key(dbus.String(u'org.freedesktop.UDisks2.Drive')) + #print "Block?", udisks_is_blockdev + if udisks_is_blockdev: + udisks_path_blockdev[k]=v + #pprint(d) + elif udisks_is_drive: + udisks_path_drives[k]=v + if RequireRemovable == False or v.\ + get(dbus.String(u'org.freedesktop.UDisks2.Drive'), False).\ + get(dbus.String(u'Removable'),False): + udisks_path_drives_removable[k]=v + #pprint(d) + + # Removing all blockdevs that have no "link" to any of the drives selected + udisks_path_blockdev_removable={} + for b,d in udisks_path_blockdev.iteritems(): + blockdrive = d[dbus.String(u'org.freedesktop.UDisks2.Block')][dbus.String(u'Drive')] + d['U2.Block.Drive'] = blockdrive + if blockdrive in udisks_path_drives_removable: + # See if there is filesystem(s) on it + filesystem = d.get(dbus.String(u'org.freedesktop.UDisks2.Filesystem')) + d['U2.Filesystem'] = filesystem + blockdevices = udisks_path_drives_removable[blockdrive].get('U2.BlockDevices', dict()) + blockdevices[b] = d + udisks_path_drives_removable[blockdrive]['U2.BlockDevices'] = blockdevices + udisks_path_blockdev_removable[b] = d + + if AllStorage is not None: + for k,d in udisks_path_drives_removable.iteritems(): + AllStorage.AddDrive(k,d.get(dbus.String(u'org.freedesktop.UDisks2.Drive')), EjectByUDisks, PowerOffByUDisks) + + for k,d in udisks_path_blockdev_removable.iteritems(): + AllStorage.AddBlockDevice(k,d.get(dbus.String(u'org.freedesktop.UDisks2.Block'))) + FsDict = d.get(dbus.String(u'org.freedesktop.UDisks2.Filesystem')) + if FsDict is not None: + AllStorage.AddFileSystem(k,FsDict,MountByUDisks,UnmountByUDisks) + + # Output the scan results for the log + # TODO: (re)move this section; output from storage AllStore + logger.debug(""" + - ScanRemovableDevices() Results - + """ + ) + for k,d in udisks_path_drives_removable.iteritems(): + logger.debug("Drive: %s", GetDriveId(d)) + #, "removable:", udisks_path_drives[d].\ + # get(dbus.String(u'org.freedesktop.UDisks2.Drive'), False).\ + # get(dbus.String(u'Removable'),False) + # print + + for k,d in udisks_path_blockdev_removable.iteritems(): + logger.debug("Blockdev: %s %s", k.rsplit('/',1)[1], GetDriveId(udisks_path_drives_removable[GetDrive(d)])) + + logger.debug(""" + - ScanRemovableDevices() Results End- + """ + ) + +def EjectByUDisks(UDisk2PathToDrive): + """ Call with a UDisks2 path to a drive """ + global bus + logger.debug("Eject %s", UDisk2PathToDrive) + dbus_obj = bus.get_object('org.freedesktop.UDisks2', UDisk2PathToDrive) + fs_intf = dbus.Interface(dbus_obj, 'org.freedesktop.UDisks2.Drive') + logger.info(fs_intf.Eject({})) + +def PowerOffByUDisks(UDisk2PathToDrive): + """ Call with a UDisks2 path to a drive """ + global bus + logger.debug("PowerOff %s", UDisk2PathToDrive) + dbus_obj = bus.get_object('org.freedesktop.UDisks2', UDisk2PathToDrive) + fs_intf = dbus.Interface(dbus_obj, 'org.freedesktop.UDisks2.Drive') + logger.info(fs_intf.PowerOff({})) + +def MountByUDisks(UDisk2PathToBlockDevice): + """ Call with a UDisks2 path to a block device """ + global bus + logger.debug("Mount %s", UDisk2PathToBlockDevice) + dbus_obj = bus.get_object('org.freedesktop.UDisks2', UDisk2PathToBlockDevice) + fs_intf = dbus.Interface(dbus_obj, 'org.freedesktop.UDisks2.Filesystem') + logger.info(fs_intf.Mount({})) + +def UnmountByUDisks(UDisk2PathToBlockDevice): + """ Call with a UDisks2 path to a block device """ + global bus + logger.debug("Unmount %s", UDisk2PathToBlockDevice) + dbus_obj = bus.get_object('org.freedesktop.UDisks2', UDisk2PathToBlockDevice) + fs_intf = dbus.Interface(dbus_obj, 'org.freedesktop.UDisks2.Filesystem') + logger.info(fs_intf.Unmount({})) + +#def UDisks2Signal_SignalReceived(*args, **kwargs): +# print "UDisks2 Signal Received: {0!s} {1!s}\n".format(args,kwargs) + +def InstallChangeReceiver( + PropertiesChangedHandler=None, + InterfaceAddedHander=None, + InterfaceRemovedHandler=None, + ): + + logger.debug( + """Object Manager + Path: %s + Intf: %s """, + om.object_path, + om.dbus_interface + ) +# bus.add_signal_receiver(handler_function=UDisks2Signal_SignalReceived, +# interface_keyword='interface', +# member_keyword='member', +# sender_keyword='sender', +# destination_keyword='destination', +# path_keyword='path', +# message_keyword='message', +# ) + if callable(PropertiesChangedHandler): + bus.add_signal_receiver(handler_function=PropertiesChangedHandler, + dbus_interface=dbus.String(u'org.freedesktop.DBus.Properties'), + path_keyword='path', + byte_arrays=True, + ) + + if callable(InterfaceAddedHander): + om.connect_to_signal(signal_name="InterfacesAdded", + handler_function=InterfaceAddedHander, +# path_keyword='path', + byte_arrays=True, + ) + + if callable(InterfaceRemovedHandler): + om.connect_to_signal(signal_name="InterfacesRemoved", + handler_function=InterfaceRemovedHandler, + # path_keyword='path', + byte_arrays=True, + ) + + logger.debug("succcess") + +if __name__ == "__main__": +# logging.basicConfig(level=logging.WARN) +# raise Exception("{0} cannot run as the main python program".format(__file__)) + logger.warn("Running {0} as a program. Will scan for devices and exit.") + ScanDevices() + +#end; \ No newline at end of file diff --git a/MassStorageCloner/gui.py b/MassStorageCloner/gui.py new file mode 100755 index 0000000..132e4e2 --- /dev/null +++ b/MassStorageCloner/gui.py @@ -0,0 +1,391 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- + +""" + Mass Storage Cloner + writes an image file to many mass storage devices with a gui + (C) 2014 Martin Süfke + License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html +""" + +""" + History: + +2014-10-14 started +2015-03-26 added write.sh quick hack for LIP SS2015, released on redmine.fsmpi.rwth-aachen.de +""" + +# Proper logging +from pprint import pprint, pformat +import logging +# SPE compatible logging format +logging.basicConfig(level=logging.DEBUG,format='%(levelno)2d:%(funcName)s:%(message)s:File "%(filename)s", line %(lineno)d') +logger = logging.getLogger(__name__) + +#import stacktrace +import inspect + +#import Tkinter #as Tk +try: + import ttk as Tk +# Tk.Tk = Tkinter.Tk # need a Tk.Tk to make the root Window. Faking one. +except: + logger.info("Tiling Toolkit (ttk) not found. Falling back to classic Tk") + import Tkinter as Tk + +#from Tkinter import BooleanVar, IntVar, DoubleVar, StringVar +#import tkMessageBox as dialog +import tkFileDialog as filedialog +from Tkconstants import N,E,S,W,HORIZONTAL,TOP,BOTTOM,BOTH, NORMAL, DISABLED +from vtk import VCheckbutton, VSLabel, VIScale, Vset, VGridFrame#, Vget +import dbus + +import dbusdetect +import gobject + +import StoreObjFrames + +import os.path +import sys + +import threading +import Queue + +LastSavedFile=None + +def DoLoadFile(): + filename = filedialog.askopenfilename( + filetypes=[ ("Image files", "*.img"), +# TODO: zipped/bzipped/xzipped images ? +# ("All files", "*.*"), + ("All files", "*"), + ], + title="Select image file to write" + ) + logger.debug( "Open dialog returns >>>%s<<< ",str(filename)) + if (len(filename)==0) or (str(filename).strip("\0\t\r\n ")==""): + return + +def Implement(msg=""): + logger.warn('Implement File "%s", line %s, %s'+msg,*inspect.stack()[1][1:4]) + +class ThreadEventedQueue(Queue.Queue): + def __init__(self, EventName, TkRoot=None, maxsize=0): + # Cannot get super() to work ?!? + #super(ThreadEventedQueue, self ).__init__(maxsize) + Queue.Queue.__init__(self, maxsize) + self.EventName = '<<{0:s}>>'.format(EventName) + self.TkRoot = TkRoot + + def AddEvent(self, EventObj): + self.put(EventObj) + if self.TkRoot is not None: + self.TkRoot.event_generate(self.EventName, when='tail') + + def SetRoot(self, TkRoot, Callback): + self.TkRoot = TkRoot + self.TkRoot.bind(self.EventName, Callback) + +class MSClonerGui(threading.Thread): + """ + Separate Thread according to + http://stackoverflow.com/questions/459083/how-do-you-run-your-own-code-alongside-tkinters-event-loop + """ + def __init__(self, autostart=True, NotifierThread = None): + threading.Thread.__init__(self) + self.NotifierThread = NotifierThread + self.ThreadNotifyQueue = ThreadEventedQueue('MyUDisk2Notify', None) + #self.DoQuit = False + if autostart: + self.start() + + def BuildGui(self): + try: + self.root = Tk.Tk() + except AttributeError: + self.root = Tk.Tkinter.Tk() + + #build the GUI on 'root' + self.root.wm_title("MassStorageCloner - GPLv3 or newer (C) 2014 Martin Süfke") + FrameHead = VGridFrame(self.root, relief='raised') + self.CbRemovableOnly = VCheckbutton(master=FrameHead, text="Removable only", command=self.OnCbRemovableOnly) + FrameContent = Tk.Frame(self.root) + FrameContDevice = Tk.Frame(FrameContent) + + self.root.columnconfigure(0,weight=1) + self.root.rowconfigure(0,weight=1) + FrameHead.grid(column=0, row=0, sticky=(N,E,W)) + FrameHead.grid_next([self.CbRemovableOnly, + Tk.Button(master=FrameHead, text="Unhide ignored", command=self.OnUnhideIgnored), + ]) + FrameContent.grid(column=0, row=1, sticky=(N,S,E,W)) + FrameContent.columnconfigure(0,weight=1) + FrameContent.rowconfigure(0,weight=1) + FrameContDevice.grid(sticky=(N,E,S,W)) + + self.TKStorageFrame = StoreObjFrames.TkFStorage(FrameContDevice) + dbusdetect.ScanDevices(self.TKStorageFrame) + + def run(self): + logger.debug("GUI thread run()") + #self.root.update() + self.BuildGui() + logger.debug("GUI thread built GUI") + # Activate Event system + self.ThreadNotifyQueue.SetRoot(self.root, self.ThreadNotifyCallback) + #if not self.DoQuit: + self.root.mainloop() + logger.debug("Tk mainloop() ended") + if self.NotifierThread is not None: + if self.NotifierThread.is_alive(): + self.NotifierThread.quit() + else: + logger.debug('NotifierThread already dead') + + def quit(self): + #self.DoQuit = False + #try: + self.root.quit() + #except: + # pass + + def OnCbRemovableOnly(self): + """ CbRemovableOnly has been checked or unchecked """ + #logger.debug("CbRemovableOnly(): %s", self.CbRemovableOnly.value ) + if self.CbRemovableOnly.value: + self.TKStorageFrame.FilterViewDriveAdd({'Removable':False}) + else: + self.TKStorageFrame.FilterViewDriveRemove(['Removable', ]) + + def OnUnhideIgnored(self): + """ Unhide Ignore has been clicked""" + self.TKStorageFrame.FilterViewDriveRemove(['Id', ]) + + def SignalDBusEvent(self, EventObj): + """ This can be called by the DBus notifier thread """ + assert self.ThreadNotifyQueue is not None + self.ThreadNotifyQueue.AddEvent(EventObj) + + def ThreadNotifyCallback(self, event): + try: + while True: + ev = self.ThreadNotifyQueue.get_nowait() + logger.debug("GUI Thread %s %s", ev, event) + self.ProcessNotifyEvent(ev) + self.ThreadNotifyQueue.task_done() + except Queue.Empty: + pass + + def ProcessNotifyEvent(self, event): + """ event is a tuple containing at least an Identifier in event[0] + called by ThreadNotifyCallback() + """ + assert len(event)>0, "Event tuple length >0 required, got {0!s}".format(event) + if event[0] == 'MountPointsChanged': + #assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event) + assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event) + self.TKStorageFrame.UpdateMountPoints(event[1], event[2], dbusdetect.UnmountByUDisks) + elif event[0] == 'DriveAdded': + assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event) + self.TKStorageFrame.AddDrive(event[1], event[2], dbusdetect.EjectByUDisks, dbusdetect.PowerOffByUDisks) + elif event[0] == 'BlockDeviceAdded': + assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event) + self.TKStorageFrame.AddBlockDevice(event[1], event[2]) + elif event[0] == 'FileSystemAdded': + assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event) + self.TKStorageFrame.AddFileSystem(event[1],event[2], dbusdetect.MountByUDisks, dbusdetect.UnmountByUDisks) + elif event[0] == 'DriveRemoved': + #self.TKStorageFrame.AddDrive(event[1], event[2], dbusdetect.EjectByUDisks, dbusdetect.PowerOffByUDisks) + assert len(event)>1, "Event tuple length >1 required, got {0!s}".format(event) + self.TKStorageFrame.RemoveDrive(event[1]) + elif event[0] == 'BlockDeviceRemoved': + #self.TKStorageFrame.AddBlockDevice(event[1], event[2]) + assert len(event)>1, "Event tuple length >1 required, got {0!s}".format(event) + self.TKStorageFrame.RemoveBlockDevice(event[1]) + elif event[0] == 'FileSystemRemoved': + assert len(event)>1, "Event tuple length >1 required, got {0!s}".format(event) + self.TKStorageFrame.RemoveFileSystem(event[1]) + #self.TKStorageFrame.AddFileSystem(event[1],event[2], dbusdetect.MountByUDisks, dbusdetect.UnmountByUDisks) + else: + logger.warn("Unknown Event %s",event[0]) + +class MSGObjectNotifier(threading.Thread): + """ Another thread to contain the GObject mainloop """ + def __init__(self, GuiThread = None): + threading.Thread.__init__(self) + self.GuiThread = GuiThread + + def run(self): + logger.debug("GObject thread started") + try: + gobject.threads_init() # the key to make gobject threads run in python + self.loop = gobject.MainLoop() + self.context = self.loop.get_context() + self.InstallDBusListener() + logger.debug("running GObject mainloop()") + self.loop.run() + finally: + logger.debug("GObject mainloop() ended") + if self.GuiThread is not None: + if self.GuiThread.is_alive(): + self.GuiThread.quit() + else: + logger.debug('GuiThread already dead') + + def quit(self): + self.loop.quit() + + def InstallDBusListener(self): + dbusdetect.InstallChangeReceiver( + PropertiesChangedHandler=self.UDisks2Signal_PropertiesChanged, + InterfaceAddedHander=self.UDisks2Signal_InterfaceAdded, + InterfaceRemovedHandler=self.UDisks2Signal_InterfaceRemoved, + ) + + def UDisks2Signal_PropertiesChanged(self, *args, **kwargs): + """ Callback for any DBus.Properties -> PropertiesChanged """ + # print "UDisks2 Properties Changed: {0!s} {1!s}\n".format(args,pformat(kwargs)) + if (len(args) >= 3) and (len(kwargs) >= 1): + # May have args: subject, message, signature (TODO: How are these defined in DBus ?) + if 'org.freedesktop.UDisks2.Filesystem' == args[0]: + if isinstance(args[1],dict) and ('MountPoints' in args[1]): + # don't care about args[2] / Signature + U2MountPoints=args[1].get('MountPoints',[]) + #logger.debug('U2Path: %s %s',type(kwargs), pformat(kwargs)) + U2Path=kwargs.get('path','') + #logger.debug('U2Path: %s',U2Path) + if U2Path.startswith('/org/freedesktop/UDisks2/block_devices/'): + # Have a change in Block Devices + self.SignalGuiMountPointChanged(U2Path, U2MountPoints) + return + + assert logger is not None + logger.info("Unhandled Message %s %s\n", args, pformat(kwargs)) + + def UDisks2Signal_InterfaceAdded(self, *args, **kwargs): + """ Callback for any UDisks2.ObjectManager -> InterfaceAdded + Through this call go drives and blockdevices that are added """ + if (len(args) >= 2): + U2Path = args[0] + if isinstance(args[1], dict): + U2Dict = args[1] + #logger.debug("U2Dict: %s\n", pformat(U2Dict)) + else: + U2Dict = {} + if U2Path.startswith('/org/freedesktop/UDisks2/drives/'): + #logger.debug("Drive added %s\n", args[0].rsplit('/')[-1]) + DriveDict = U2Dict.get('org.freedesktop.UDisks2.Drive') + if DriveDict is not None: + self.SignalGuiDriveAdded(U2Path, DriveDict) + return + elif U2Path.startswith('/org/freedesktop/UDisks2/block_devices/'): + #logger.debug("Blockdevice added %s\n", args[0].rsplit('/')[-1]) + BlockDict = U2Dict.get('org.freedesktop.UDisks2.Block') + if BlockDict is not None: + self.SignalGuiBlockDeviceAdded(U2Path, BlockDict) + FsDict = U2Dict.get('org.freedesktop.UDisks2.Filesystem') + if FsDict is not None: + self.SignalGuiFileSystemAdded(U2Path, FsDict) + if (BlockDict is not None) or (FsDict is not None): + return + + assert logger is not None + logger.info("Unknown %s %s\n", args, pformat(kwargs)) + + def UDisks2Signal_InterfaceRemoved(self, *args, **kwargs): + """ Callback for any UDisks2.ObjectManager -> InterfaceRemoved """ + if (len(args) >= 2): + U2Path = args[0] + if isinstance(args[1], list): + U2List = args[1] + else: + U2List = [] + Unknown = True + if U2Path.startswith('/org/freedesktop/UDisks2/block_devices/'): + if 'org.freedesktop.UDisks2.Filesystem' in U2List: + self.SignalGuiFileSystemRemoved(U2Path) + Unknown = False + if 'org.freedesktop.UDisks2.Block' in U2List: + self.SignalGuiBlockDeviceRemoved(U2Path) + Unknown = False + if U2Path.startswith('/org/freedesktop/UDisks2/drives/'): + #logger.debug("Drive removed %s\n", args[0].rsplit('/')[-1]) + if 'org.freedesktop.UDisks2.Drive' in U2List: + self.SignalGuiDriveRemoved(U2Path) + Unknown = False + + if not Unknown: + return + + assert logger is not None + logger.info("Unknown %s %s\n", args, pformat(kwargs)) + + def SignalGuiMountPointChanged(self, U2Path, U2MountPoints): + logger.debug("GObj() path %s mp %s\n", U2Path, U2MountPoints) + EvObj=('MountPointsChanged', U2Path, U2MountPoints) + self.GuiThread.SignalDBusEvent(EvObj) + + def SignalGuiDriveAdded(self, U2Path, U2Dict): + logger.debug("GObj() path %s u2d.k %s\n", U2Path, U2Dict.keys()) + EvObj=('DriveAdded', U2Path, U2Dict) + self.GuiThread.SignalDBusEvent(EvObj) + + def SignalGuiBlockDeviceAdded(self, U2Path, U2Dict): + logger.debug("GObj() path %s u2d.k %s\n", U2Path, U2Dict.keys()) + EvObj=('BlockDeviceAdded', U2Path, U2Dict) + self.GuiThread.SignalDBusEvent(EvObj) + + def SignalGuiFileSystemAdded(self, U2Path, U2Dict): + logger.debug("GObj() path %s u2d.k %s\n", U2Path, U2Dict.keys()) + EvObj=('FileSystemAdded', U2Path, U2Dict) + self.GuiThread.SignalDBusEvent(EvObj) + + def SignalGuiEventRemoved(self, name, param1): + EvObj=(name, param1) + self.GuiThread.SignalDBusEvent(EvObj) + + def SignalGuiDriveRemoved(self, U2Path): + self.SignalGuiEventRemoved('DriveRemoved', U2Path) + + def SignalGuiBlockDeviceRemoved(self, U2Path): + self.SignalGuiEventRemoved('BlockDeviceRemoved', U2Path) + + def SignalGuiFileSystemRemoved(self, U2Path): + self.SignalGuiEventRemoved('FileSystemRemoved', U2Path) + +gnotifier = MSGObjectNotifier() +gui = MSClonerGui(NotifierThread=gnotifier) +gnotifier.GuiThread = gui +gnotifier.start() + +#dbusdetect.InstallChangeReceiver() + +#logger.debug('Time to do more in the main thread') +# Do not know what to do here (sensibly) to wait ... trying join() +#import time +#while gui.is_alive() and gnotifier.is_alive(): +# time.sleep(1) +# logger.debug('tick ') + +# Waiting for a key press is not smart either +#logger.info("Waiting for key press on stdin") +#sys.stdin.read(1) +#gnotifier.quit() +#gui.quit() + +logger.debug('Waiting for thread joins') +gnotifier.join() +gui.join() # don't know: join() again or not ? +logger.debug('End, threads joined') + +"""make use of figure.ginput() for mouse input or waitforbuttonpress() for keyboard-stuff +""" + +def _quit(self): + logger.debug("called python _quit()") + if gui is not None: + gui.quit() # stops mainloop + gui.root.destroy() # this is necessary on Windows to prevent + # Fatal Python Error: PyEval_RestoreThread: NULL tstate + +#end; diff --git a/MassStorageCloner/vtk.py b/MassStorageCloner/vtk.py new file mode 100644 index 0000000..2a365b3 --- /dev/null +++ b/MassStorageCloner/vtk.py @@ -0,0 +1,223 @@ +#! /usr/bin/python +# -*- coding: UTF-8 -*- +""" + Visual with Variable TK components. + These elements extend the widgets in Tkinter to include their variables + in one pythonic object. + + (C) 2012-2014 Martin Süfke + License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html +""" +try: + from ttk import BooleanVar, IntVar, DoubleVar, StringVar + from ttk import Checkbutton, Label, Scale, Entry, OptionMenu, Text, Frame +except: + from Tkinter import BooleanVar, IntVar, DoubleVar, StringVar + from Tkinter import Checkbutton, Label, Scale, Entry, OptionMenu, Text, Frame +#from Tkconstants import * +import types +import io + +def _setifunset(d,n,v): + if not n in d: + d[n]=v + +class Vget(): + @property + def value(self): + return self.variable.get() + def get(self): + return self.variable.get() + +class Vset(): + #TODO: make a setter property on ""value" ? + def set(self,val): + self.variable.set(val) + +class Vvalue(Vget): + @property + def value(self): + return self.variable.get() + @value.setter + def value_setter(self,newvalue): + return self.variable.set(newvalue) + + +class VsetFmt(Vset): + def set(self,msg,*arg): + #msg = msg + #if len(arg)>0: + # self.variable.set(msg % arg) + #else: + Vset.set(self,msg%arg) + +class VCheckbutton(Checkbutton,Vget,Vset,Vvalue,Vvalue): + """Checkbutton with Boolean value""" + def __init__(self, *arg, **kw): + # this is an inherited __init__ using "classic" style without super() + self.variable=BooleanVar(value=kw.pop("value",False)) + _setifunset(kw,"offvalue",False) + _setifunset(kw,"onvalue",True) + _setifunset(kw,"variable",self.variable) + Checkbutton.__init__(self, *arg, **kw) + +class VDLabel(Label,Vget,Vset,Vvalue): + """Label with DoubleVar() Value""" + def __init__(self, *arg, **kw): + # this is an inherited __init__ using "classic" style without super() + self.variable=DoubleVar(value=kw.pop("value",0.0)) + _setifunset(kw,"textvariable",self.variable) + Label.__init__(self, *arg, **kw) + +class VSLabel(Label,Vget,VsetFmt): + """Label with StringVar() Value""" + def __init__(self, *arg, **kw): + # this is an inherited __init__ using "classic" style without super() + self.variable=StringVar(value=kw.pop("value","")) + _setifunset(kw,"textvariable",self.variable) + Label.__init__(self, *arg, **kw) + +class VIScale(Scale,Vget,Vset,Vvalue): + """Scale with IntVar() Value""" + def __init__(self, *arg, **kw): + # this is an inherited __init__ using "classic" style without super() + self.variable=IntVar(value=kw.pop("value",0)) + _setifunset(kw,"variable",self.variable) + Scale.__init__(self, *arg, **kw) + +class VSEntry(Entry,Vget,VsetFmt): + """Entry with StringVar() Value""" + def __init__(self, *arg, **kw): + # this is an inherited __init__ using "classic" style without super() + self.variable=StringVar(value=kw.pop("value",0)) + _setifunset(kw,"textvariable",self.variable) + Entry.__init__(self, *arg, **kw) + +class VIOptionMenu(OptionMenu,Vget,Vset,Vvalue): + """OptionMenu with IntVar() Value""" + def __init__(self, master, *arg, **kw): + # this is an inherited __init__ using "classic" style without super() + value=kw.pop("value",0) + self.variable=IntVar(value=value) + var=kw.pop("variable",self.variable) + OptionMenu.__init__(self, master, var, value, *arg, **kw) # this is a mess in TKinter + +class VSOptionMenu(OptionMenu,Vget,Vset,Vvalue): + """OptionMenu with StringVar() Value""" + def __init__(self, master, *arg, **kw): + # this is an inherited __init__ using "classic" style without super() + value=kw.pop("value",0) + self.variable=StringVar(value=value) + var=kw.pop("variable",self.variable) + OptionMenu.__init__(self, master, var, value, *arg, **kw) # this is a mess in TKinter + +class VText(Text): + """A Text widget with a more spohisticated interface""" + def __init__(self, *arg, **kw): + Text.__init__(self, *arg, **kw) + + def FromList(self,list): + """Put all text from list into Text Widget""" + self.delete('1.0','end') + for line in list: + self.insert('end',line+"\n") + + def FromString(self,input): + """convert input string with line breaks (cr, lf, cr/lf) into Text Widget""" + sio=io.StringIO(unicode(input)) + self.delete('1.0','end') + line=sio.readline() + while ""<line: + self.insert('end',line) + line=sio.readline() + sio.close() + +class VGridFrame(Frame): + """A Frame widget with a more spohisticated grid layout interface""" + + nextCol="col" + nextRow="row" + + def __init__(self, *arg, **kw): + self.set_next(kw.pop('next', self.nextCol)) # put in colum or row + Frame.__init__(self, *arg, **kw) + self._col=0 # next position to grid into + self._row=0 + self._sticky=None # no stickyness + + def colconfig(self,**kw): + """Apply columnconfigure() to the current column. + Usually used before grid_next()""" + self.columnconfigure(self._col,**kw) + + def rowconfig(self,**kw): + """Apply rowconfigure() to the current row. + Usually used before grid_next()""" + self.rowconfigure(self._row,**kw) + + def set_next(self,next): + """set next step direction to 'col' or 'row'""" + if next: + if next == self.nextCol: + self._next=self.nextCol + elif next == self.nextRow: + self._next=self.nextRow + else: + assert False, "Need next='%s' or next='%s' but got %s"%(self.nextCol,self.nextRow,next) + + def grid_next(self,widget_or_list,**kw): + """apply widget.grid() with increasing colum= or row= values + grid_next( (A, B, ..), opt) is a shortcurt for + grid_next( A, opt) + grid_next( B, opt) + grid_next( .., opt) + """ + + def _grid(widget): + widget.grid(column=self._col, row=self._row, **kw) + if self._next == self.nextCol: + self._col+=colinc + elif self._next == self.nextRow: + self._row+=rowinc + + self._col=kw.pop('column', self._col) + self._row=kw.pop('row', self._row) + colinc=kw.get('columnspan',1) + rowinc=kw.get('rowspan',1) + + self.set_next(kw.pop('next',None)) + try: + sticky=kw.pop('sticky') + self._sticky=sticky # save stickyness for next time + except KeyError: + sticky=self._sticky # get last value (may be None if sticky unset) + if sticky: + kw['sticky']=sticky + + if isinstance( widget_or_list, (types.TupleType,types.ListType) ): + for w in widget_or_list: + _grid(w) + else: + _grid(widget_or_list) # is a widget + + def step(self,**kw): + """ Step a column or a row forward, resetting column or row + usage: + step(column=0) resets to column 0, advancing row + step(row=2) resets to row 2, advancing column + step(column=3, row=4) sets to coulmn 3 and row 4""" + col=kw.get("column",None) + row=kw.get("row",None) + if col is not None: + self._col=col + self._row=row or self._row+1 + elif row is not None: + self._row=row + self._col=col or self._col+1 + else: + assert False, "You need either 'column=...' or 'row=...' in step() %r"%(kw) + +## check not running as main +assert '__main__' != __name__ , "You should not run this, but [import] it" + +#end; diff --git a/MassStorageCloner/write.sh b/MassStorageCloner/write.sh new file mode 100755 index 0000000..5d078d4 --- /dev/null +++ b/MassStorageCloner/write.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# +# Mass Storage Cloner - write.sh +# (C)2015 Martin Suefke +# License: GPLv3 or newer +# +# This is exected whenever the user clicks "write" on any blockdev +# The blockdev is passed as $1 +# +# Customize this file as needed. +# + +xterm -e "sudo bash ./blkwrite.sh ~/LIP-Image-WS2014-10-09-01.img $1" + +#end; -- GitLab