From 2815d05ccc6159bdea23d4d114cd5b0a9c9039b3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 3 Jun 2009 13:18:29 -0700 Subject: [PATCH] Switch to using sysfs+pmount for device mounting in linux. Should allow for the device to be disconnected without unmounting and subsequently re-connected and still detected in calibre. --- src/calibre/devices/prs500/cli/main.py | 86 +++++++-------- src/calibre/devices/prs505/driver.py | 5 +- src/calibre/devices/usbms/device.py | 143 +++++++++++++++++-------- src/calibre/linux.py | 47 +------- src/calibre/trac/plugins/download.py | 1 + 5 files changed, 148 insertions(+), 134 deletions(-) diff --git a/src/calibre/devices/prs500/cli/main.py b/src/calibre/devices/prs500/cli/main.py index 9211fcff41..53431f69af 100755 --- a/src/calibre/devices/prs500/cli/main.py +++ b/src/calibre/devices/prs500/cli/main.py @@ -3,14 +3,14 @@ __copyright__ = '2008, Kovid Goyal ' """ Provides a command-line and optional graphical interface to the SONY Reader PRS-500. -For usage information run the script. +For usage information run the script. """ import StringIO, sys, time, os from optparse import OptionParser from calibre import __version__, iswindows, __appname__ -from calibre.devices.errors import PathError +from calibre.devices.errors import PathError from calibre.utils.terminfo import TerminalController from calibre.devices.errors import ArgumentError, DeviceError, DeviceLocked from calibre.customize.ui import device_plugins @@ -29,7 +29,7 @@ def human_readable(size): return size + suffix class FileFormatter(object): - def __init__(self, file, term): + def __init__(self, file, term): self.term = term self.is_dir = file.is_dir self.is_readonly = file.is_readonly @@ -38,18 +38,18 @@ class FileFormatter(object): self.wtime = file.wtime self.name = file.name self.path = file.path - + @dynamic_property def mode_string(self): doc=""" The mode string for this file. There are only two modes read-only and read-write """ def fget(self): - mode, x = "-", "-" + mode, x = "-", "-" if self.is_dir: mode, x = "d", "x" if self.is_readonly: mode += "r-"+x+"r-"+x+"r-"+x else: mode += "rw"+x+"rw"+x+"rw"+x return mode return property(doc=doc, fget=fget) - + @dynamic_property def isdir_name(self): doc='''Return self.name + '/' if self is a directory''' @@ -59,8 +59,8 @@ class FileFormatter(object): name += '/' return name return property(doc=doc, fget=fget) - - + + @dynamic_property def name_in_color(self): doc=""" The name in ANSI text. Directories are blue, ebooks are green """ @@ -71,24 +71,24 @@ class FileFormatter(object): if self.is_dir: cname = blue + self.name + normal else: ext = self.name[self.name.rfind("."):] - if ext in (".pdf", ".rtf", ".lrf", ".lrx", ".txt"): cname = green + self.name + normal + if ext in (".pdf", ".rtf", ".lrf", ".lrx", ".txt"): cname = green + self.name + normal return cname return property(doc=doc, fget=fget) - + @dynamic_property def human_readable_size(self): doc=""" File size in human readable form """ def fget(self): return human_readable(self.size) return property(doc=doc, fget=fget) - + @dynamic_property def modification_time(self): doc=""" Last modified time in the Linux ls -l format """ def fget(self): return time.strftime("%Y-%m-%d %H:%M", time.localtime(self.wtime)) return property(doc=doc, fget=fget) - + @dynamic_property def creation_time(self): doc=""" Last modified time in the Linux ls -l format """ @@ -104,7 +104,7 @@ def info(dev): print "Mime type: ", info[3] def ls(dev, path, term, recurse=False, color=False, human_readable_size=False, ll=False, cols=0): - def col_split(l, cols): # split list l into columns + def col_split(l, cols): # split list l into columns rows = len(l) / cols if len(l) % cols: rows += 1 @@ -112,8 +112,8 @@ def ls(dev, path, term, recurse=False, color=False, human_readable_size=False, l for i in range(rows): m.append(l[i::rows]) return m - - def row_widths(table): # Calculate widths for each column in the row-wise table + + def row_widths(table): # Calculate widths for each column in the row-wise table tcols = len(table[0]) rowwidths = [ 0 for i in range(tcols) ] for row in table: @@ -122,19 +122,19 @@ def ls(dev, path, term, recurse=False, color=False, human_readable_size=False, l rowwidths[c] = len(item) if len(item) > rowwidths[c] else rowwidths[c] c += 1 return rowwidths - - output = StringIO.StringIO() + + output = StringIO.StringIO() if path.endswith("/") and len(path) > 1: path = path[:-1] dirs = dev.list(path, recurse) for dir in dirs: - if recurse: print >>output, dir[0] + ":" + if recurse: print >>output, dir[0] + ":" lsoutput, lscoloutput = [], [] files = dir[1] maxlen = 0 if ll: # Calculate column width for size column for file in files: size = len(str(file.size)) - if human_readable_size: + if human_readable_size: file = FileFormatter(file, term) size = len(file.human_readable_size) if size > maxlen: maxlen = size @@ -148,29 +148,29 @@ def ls(dev, path, term, recurse=False, color=False, human_readable_size=False, l size = str(file.size) if human_readable_size: size = file.human_readable_size print >>output, file.mode_string, ("%"+str(maxlen)+"s")%size, file.modification_time, name - if not ll and len(lsoutput) > 0: + if not ll and len(lsoutput) > 0: trytable = [] for colwidth in range(MINIMUM_COL_WIDTH, cols): trycols = int(cols/colwidth) - trytable = col_split(lsoutput, trycols) + trytable = col_split(lsoutput, trycols) works = True for row in trytable: row_break = False for item in row: - if len(item) > colwidth - 1: + if len(item) > colwidth - 1: works, row_break = False, True break if row_break: break if works: break rowwidths = row_widths(trytable) trytablecol = col_split(lscoloutput, len(trytable[0])) - for r in range(len(trytable)): + for r in range(len(trytable)): for c in range(len(trytable[r])): padding = rowwidths[c] - len(trytable[r][c]) print >>output, trytablecol[r][c], "".ljust(padding), - print >>output + print >>output print >>output - listing = output.getvalue().rstrip()+ "\n" + listing = output.getvalue().rstrip()+ "\n" output.close() return listing @@ -179,20 +179,20 @@ def main(): cols = term.COLS if not cols: # On windows terminal width is unknown cols = 80 - + parser = OptionParser(usage="usage: %prog [options] command args\n\ncommand is one of: info, books, df, ls, cp, mkdir, touch, cat, rm\n\n"+ "For help on a particular command: %prog command", version=__appname__+" version: " + __version__) parser.add_option("--log-packets", help="print out packet stream to stdout. "+\ - "The numbers in the left column are byte offsets that allow the packet size to be read off easily.", + "The numbers in the left column are byte offsets that allow the packet size to be read off easily.", dest="log_packets", action="store_true", default=False) parser.remove_option("-h") parser.disable_interspersed_args() # Allow unrecognized options options, args = parser.parse_args() - + if len(args) < 1: parser.print_help() return 1 - + command = args[0] args = args[1:] dev = None @@ -207,27 +207,27 @@ def main(): if scanner.is_device_connected(d): dev = d dev.reset(log_packets=options.log_packets) - + if dev is None: print >>sys.stderr, 'Unable to find a connected ebook reader.' return 1 - + try: dev.open() if command == "df": total = dev.total_space(end_session=False) free = dev.free_space() where = ("Memory", "Stick", "Card") - print "Filesystem\tSize \tUsed \tAvail \tUse%" + print "Filesystem\tSize \tUsed \tAvail \tUse%" for i in range(3): print "%-10s\t%s\t%s\t%s\t%s"%(where[i], human_readable(total[i]), human_readable(total[i]-free[i]), human_readable(free[i]),\ str(0 if total[i]==0 else int(100*(total[i]-free[i])/(total[i]*1.)))+"%") elif command == "books": print "Books in main memory:" - for book in dev.books(): + for book in dev.books(): print book print "\nBooks on storage card:" - for book in dev.books(oncard=True): print book + for book in dev.books(oncard=True): print book elif command == "mkdir": parser = OptionParser(usage="usage: %prog mkdir [options] path\nCreate a directory on the device\n\npath must begin with / or card:/") if len(args) != 1: @@ -245,7 +245,7 @@ def main(): if len(args) != 1: parser.print_help() return 1 - print ls(dev, args[0], term, color=options.color, recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols), + print ls(dev, args[0], term, color=options.color, recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols), elif command == "info": info(dev) elif command == "cp": @@ -259,22 +259,22 @@ def main(): parser.add_option('-f', '--force', dest='force', action='store_true', default=False, help='Overwrite the destination file if it exists already.') options, args = parser.parse_args(args) - if len(args) != 2: + if len(args) != 2: parser.print_help() return 1 if args[0].startswith("prs500:"): outfile = args[1] path = args[0][7:] - if path.endswith("/"): path = path[:-1] + if path.endswith("/"): path = path[:-1] if os.path.isdir(outfile): - outfile = os.path.join(outfile, path[path.rfind("/")+1:]) + outfile = os.path.join(outfile, path[path.rfind("/")+1:]) try: outfile = open(outfile, "wb") except IOError, e: print >> sys.stderr, e parser.print_help() return 1 - dev.get_file(path, outfile) + dev.get_file(path, outfile) outfile.close() elif args[1].startswith("prs500:"): try: @@ -299,7 +299,7 @@ def main(): outfile = sys.stdout parser = OptionParser(usage="usage: %prog cat path\nShow file on the device\n\npath should point to a file on the device and must begin with /,a:/ or b:/") options, args = parser.parse_args(args) - if len(args) != 1: + if len(args) != 1: parser.print_help() return 1 if args[0].endswith("/"): path = args[0][:-1] @@ -311,15 +311,15 @@ def main(): "and must begin with / or card:/\n\n"+\ "rm will DELETE the file. Be very CAREFUL") options, args = parser.parse_args(args) - if len(args) != 1: + if len(args) != 1: parser.print_help() return 1 - dev.rm(args[0]) + dev.rm(args[0]) elif command == "touch": parser = OptionParser(usage="usage: %prog touch path\nCreate an empty file on the device\n\npath should point to a file on the device and must begin with /,a:/ or b:/\n\n"+ "Unfortunately, I cant figure out how to update file times on the device, so if path already exists, touch does nothing" ) options, args = parser.parse_args(args) - if len(args) != 1: + if len(args) != 1: parser.print_help() return 1 dev.touch(args[0]) diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index e75f67223a..378f15cab1 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -60,9 +60,8 @@ class PRS505(CLI, Device): '''.encode('utf8')) - return True + return True except: - self._card_prefix = None import traceback traceback.print_exc() return False @@ -226,5 +225,5 @@ class PRS505(CLI, Device): f.close() write_card_prefix(self._card_a_prefix, 1) write_card_prefix(self._card_b_prefix, 2) - + self.report_progress(1.0, _('Sending metadata to device...')) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 4b9a3f5adf..4a52b1035b 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -6,7 +6,7 @@ intended to be subclassed with the relevant parts implemented for a particular device. This class handles device detection. ''' -import os, subprocess, time, re +import os, subprocess, time, re, sys, glob from itertools import repeat from calibre.devices.interface import DevicePlugin @@ -36,6 +36,7 @@ class Device(DeviceConfig, DevicePlugin): MAIN_MEMORY_VOLUME_LABEL = '' STORAGE_CARD_VOLUME_LABEL = '' + STORAGE_CARD2_VOLUME_LABEL = None FDI_TEMPLATE = \ ''' @@ -345,63 +346,119 @@ class Device(DeviceConfig, DevicePlugin): raise DeviceError(_('Unable to detect the %s disk drive.') %self.__class__.__name__) - devnodes = [] + devnodes, ok = [], {} for x, isfile in walk(usb_dir): if not isfile and '/block/' in x: parts = x.split('/') idx = parts.index('block') if idx == len(parts)-2: - devnodes.append(parts[idx+1]) + sz = j(x, 'size') + node = parts[idx+1] + try: + exists = int(open(sz).read()) > 0 + if exists: + node = self.find_largest_partition(x) + ok[node] = True + else: + ok[node] = False + except: + ok[node] = False + devnodes.append(node) devnodes.sort() - devnodes += list(repeat(None, 2)) - return devnodes[:3] + devnodes += list(repeat(None, 3)) + return tuple(['/dev/'+x if ok.get(x, False) else None for x in devnodes[:3]]) + + def node_mountpoint(self, node): + for line in open('/proc/mounts').readlines(): + line = line.split() + if line[0] == node: + return line[1] + return None + + def find_largest_partition(self, path): + node = path.split('/')[-1] + nodes = [] + for x in glob.glob(path+'/'+node+'*'): + sz = x + '/size' + + if not os.access(sz, os.R_OK): + continue + try: + sz = int(open(sz).read()) + except: + continue + if sz > 0: + nodes.append((x.split('/')[-1], sz)) + + nodes.sort(cmp=lambda x, y: cmp(x[1], y[1])) + if not nodes: + return node + return nodes[-1][0] def open_linux(self): - import dbus - bus = dbus.SystemBus() - hm = dbus.Interface(bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager"), "org.freedesktop.Hal.Manager") - def conditional_mount(dev): - mmo = bus.get_object("org.freedesktop.Hal", dev) - label = mmo.GetPropertyString('volume.label', dbus_interface='org.freedesktop.Hal.Device') - is_mounted = mmo.GetPropertyString('volume.is_mounted', dbus_interface='org.freedesktop.Hal.Device') - mount_point = mmo.GetPropertyString('volume.mount_point', dbus_interface='org.freedesktop.Hal.Device') - fstype = mmo.GetPropertyString('volume.fstype', dbus_interface='org.freedesktop.Hal.Device') - if is_mounted: - return str(mount_point) - mmo.Mount(label, fstype, ['umask=077', 'uid='+str(os.getuid()), 'sync'], - dbus_interface='org.freedesktop.Hal.Device.Volume') - return os.path.normpath('/media/'+label)+'/' + def mount(node, type): + mp = self.node_mountpoint(node) + if mp is not None: + return mp, 0 - mm = hm.FindDeviceStringMatch(__appname__+'.mainvolume', self.__class__.__name__) - if not mm: - raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%(self.__class__.__name__,)) - self._main_prefix = None - for dev in mm: - try: - self._main_prefix = conditional_mount(dev)+os.sep - break - except dbus.exceptions.DBusException: - continue + if type == 'main': + label = self.MAIN_MEMORY_VOLUME_LABEL + if type == 'carda': + label = self.STORAGE_CARD_VOLUME_LABEL + if type == 'cardb': + label = self.STORAGE_CARD2_VOLUME_LABEL + if label is None: + label = self.STORAGE_CARD_VOLUME_LABEL + ' 2' + extra = 0 + while True: + q = ' (%d)'%extra if extra else '' + if not os.path.exists('/media/'+label+q): + break + extra += 1 + if extra: + label += ' (%d)'%extra - if not self._main_prefix: - raise DeviceError('Could not open device for reading. Try a reboot.') + def do_mount(node, label): + cmd = ['pmount', '-w', '-s'] + label = label.replace(' ', '_') + try: + p = subprocess.Popen(cmd + [node, label]) + except OSError: + raise DeviceError(_('You must install the pmount package.')) + while p.poll() is None: + time.sleep(0.1) + return p.returncode - self._card_a_prefix = self._card_b_prefix = None - cards = hm.FindDeviceStringMatch(__appname__+'.cardvolume', self.__class__.__name__) + ret = do_mount(node, label) + if ret != 0: + return None, ret + return self.node_mountpoint(node)+'/', 0 - def mount_card(dev): - try: - return conditional_mount(dev)+os.sep - except: - import traceback - print traceback - if len(cards) >= 1: - self._card_a_prefix = mount_card(cards[0]) - if len(cards) >=2: - self._card_b_prefix = mount_card(cards[1]) + main, carda, cardb = self.find_device_nodes() + if main is None: + raise DeviceError(_('Unable to detect the %s disk drive.') + %self.__class__.__name__) + + mp, ret = mount(main, 'main') + if mp is None: + raise DeviceError( + _('Unable to mount main memory (Error code: %d)')%ret) + if not mp.endswith('/'): mp += '/' + self._main_prefix = mp + cards = [x for x in (carda, cardb) if x is not None] + prefix, typ = '_card_a_prefix', 'carda' + for card in cards: + mp, ret = mount(card, typ) + if mp is None: + print >>sys.stderr, 'Unable to mount card (Error code: %d)'%ret + else: + if not mp.endswith('/'): mp += '/' + setattr(self, prefix, mp) + prefix, typ = '_card_b_prefix', 'cardb' + def open(self): time.sleep(5) diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 183ba73e04..a9a5556afa 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' ''' Post installation script for linux ''' import sys, os, shutil -from subprocess import check_call, call +from subprocess import check_call from calibre import __version__, __appname__ from calibre.customize.ui import device_plugins @@ -263,49 +263,6 @@ def setup_udev_rules(group_file, reload, fatal_errors): '''BUS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="%s"\n'''%(group,) ) udev.close() - fdi = open_file('/usr/share/hal/fdi/policy/20thirdparty/10-calibre.fdi') - manifest.append(fdi.name) - fdi.write('\n\n\n') - for cls in DEVICES: - fdi.write(\ -''' - - - - - %(cls)s - - - - -'''%dict(cls=cls.__class__.__name__, vendor_id=cls.VENDOR_ID, product_id=cls.PRODUCT_ID, - prog=__appname__, bcd=cls.BCD)) - fdi.write('\n'+cls.get_fdi()) - fdi.write('\n\n') - fdi.close() - if reload: - called = False - for hal in ('hald', 'hal', 'haldaemon'): - hal = os.path.join('/etc/init.d', hal) - if os.access(hal, os.X_OK): - call((hal, 'restart')) - called = True - break - if not called and os.access('/etc/rc.d/rc.hald', os.X_OK): - call(('/etc/rc.d/rc.hald', 'restart')) - - try: - check_call('udevadm control --reload_rules', shell=True) - except: - try: - check_call('udevcontrol reload_rules', shell=True) - except: - try: - check_call('/etc/init.d/udev reload', shell=True) - except: - if fatal_errors: - raise Exception("Couldn't reload udev, you may have to reboot") - print >>sys.stderr, "Couldn't reload udev, you may have to reboot" return manifest def option_parser(): @@ -314,7 +271,7 @@ def option_parser(): parser.add_option('--use-destdir', action='store_true', default=False, dest='destdir', help='If set, respect the environment variable DESTDIR when installing files') parser.add_option('--do-not-reload-udev-hal', action='store_true', dest='dont_reload', default=False, - help='If set, do not try to reload udev rules and HAL FDI files') + help='Does nothing. Present for legacy reasons.') parser.add_option('--group-file', default='/etc/group', dest='group_file', help='File from which to read group information. Default: %default') parser.add_option('--dont-check-root', action='store_true', default=False, dest='no_root', diff --git a/src/calibre/trac/plugins/download.py b/src/calibre/trac/plugins/download.py index 72105cd68f..2372f14f86 100644 --- a/src/calibre/trac/plugins/download.py +++ b/src/calibre/trac/plugins/download.py @@ -21,6 +21,7 @@ DEPENDENCIES = [ ('dnspython', '1.6.0', 'dnspython', 'dnspython', 'dnspython', 'dnspython'), ('poppler', '0.10.5', 'poppler', 'poppler', 'poppler', 'poppler'), ('podofo', '0.7', 'podofo', 'podofo', 'podofo', 'podofo'), + ('pmount', '0.9.19', 'pmount', 'pmount', 'pmount', 'pmount'), ]