Refactor unix MTP driver to manage device presence

This commit is contained in:
Kovid Goyal 2012-08-23 11:37:42 +05:30
parent 5cbed9ff4c
commit 5ac5928486
3 changed files with 78 additions and 112 deletions

View File

@ -26,11 +26,6 @@ class MTPDeviceBase(DevicePlugin):
author = 'Kovid Goyal'
version = (1, 0, 0)
# Invalid USB vendor information so the scanner will never match
VENDOR_ID = [0xffff]
PRODUCT_ID = [0xffff]
BCD = [0xffff]
THUMBNAIL_HEIGHT = 128
CAN_SET_METADATA = []
@ -51,4 +46,10 @@ class MTPDeviceBase(DevicePlugin):
def get_gui_name(self):
return self.current_friendly_name or self.name
def is_usb_connected(self, devices_on_system, debug=False,
only_presence=False):
# We manage device presence ourselves, so this method should always
# return False
return False

View File

@ -1,71 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.constants import plugins
class MTPDetect(object):
def __init__(self):
p = plugins['libmtp']
self.libmtp = p[0]
if self.libmtp is None:
print ('Failed to load libmtp, MTP device detection disabled')
print (p[1])
self.cache = {}
def __call__(self, devices):
'''
Given a list of devices as returned by LinuxScanner, return the set of
devices that are likely to be MTP devices. This class maintains a cache
to minimize USB polling. Note that detection is partially based on a
list of known vendor and product ids. This is because polling some
older devices causes problems. Therefore, if this method identifies a
device as MTP, it is not actually guaranteed that it will be a working
MTP device.
'''
# First drop devices that have been disconnected from the cache
connected_devices = {(d.busnum, d.devnum, d.vendor_id, d.product_id,
d.bcd, d.serial) for d in devices}
for d in tuple(self.cache.iterkeys()):
if d not in connected_devices:
del self.cache[d]
# Since is_mtp_device() can cause USB traffic by probing the device, we
# cache its result
mtp_devices = set()
if self.libmtp is None:
return mtp_devices
for d in devices:
ans = self.cache.get((d.busnum, d.devnum, d.vendor_id, d.product_id,
d.bcd, d.serial), None)
if ans is None:
ans = self.libmtp.is_mtp_device(d.busnum, d.devnum,
d.vendor_id, d.product_id)
self.cache[(d.busnum, d.devnum, d.vendor_id, d.product_id,
d.bcd, d.serial)] = ans
if ans:
mtp_devices.add(d)
return mtp_devices
def create_device(self, connected_device):
d = connected_device
return self.libmtp.Device(d.busnum, d.devnum, d.vendor_id,
d.product_id, d.manufacturer, d.product, d.serial)
if __name__ == '__main__':
from calibre.devices.scanner import linux_scanner
mtp_detect = MTPDetect()
devs = mtp_detect(linux_scanner())
print ('Found %d MTP devices:'%len(devs))
for dev in devs:
print (dev, 'at busnum=%d and devnum=%d'%(dev.busnum, dev.devnum))
print()

View File

@ -10,11 +10,19 @@ __docformat__ = 'restructuredtext en'
import time, operator
from threading import RLock
from io import BytesIO
from collections import namedtuple
from calibre.constants import plugins
from calibre.devices.errors import OpenFailed, DeviceError
from calibre.devices.mtp.base import MTPDeviceBase, synchronous
from calibre.devices.mtp.filesystem_cache import FilesystemCache
from calibre.devices.mtp.unix.detect import MTPDetect
MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id '
'bcd serial manufacturer product')
def fingerprint(d):
return MTPDevice(d.busnum, d.devnum, d.vendor_id, d.product_id, d.bcd,
d.serial, d.manufacturer, d.product)
class MTP_DEVICE(MTPDeviceBase):
@ -22,13 +30,18 @@ class MTP_DEVICE(MTPDeviceBase):
def __init__(self, *args, **kwargs):
MTPDeviceBase.__init__(self, *args, **kwargs)
self.libmtp = None
self.detect_cache = {}
self.dev = None
self._filesystem_cache = None
self.lock = RLock()
self.blacklisted_devices = set()
self.ejected_devices = set()
self.currently_connected_dev = None
def set_debug_level(self, lvl):
self.detect.libmtp.set_debug_level(lvl)
self.libmtp.set_debug_level(lvl)
def report_progress(self, sent, total):
try:
@ -39,40 +52,67 @@ class MTP_DEVICE(MTPDeviceBase):
self.progress_reporter(p)
@synchronous
def is_usb_connected(self, devices_on_system, debug=False,
only_presence=False):
def detect_managed_devices(self, devices_on_system):
if self.libmtp is None: return None
# First remove blacklisted devices.
devs = []
devs = set()
for d in devices_on_system:
if (d.busnum, d.devnum, d.vendor_id,
d.product_id, d.bcd, d.serial) not in self.blacklisted_devices:
devs.append(d)
fp = fingerprint(d)
if fp not in self.blacklisted_devices:
devs.add(fp)
devs = self.detect(devs)
if self.dev is not None:
# Check if the currently opened device is still connected
ids = self.dev.ids
found = False
for d in devs:
if ( (d.busnum, d.devnum, d.vendor_id, d.product_id, d.serial)
== ids ):
found = True
break
return found
# Check if any MTP capable device is present
return len(devs) > 0
# Clean up ejected devices
self.ejected_devices = devs.intersection(self.ejected_devices)
# Check if the currently connected device is still present
if self.currently_connected_dev is not None:
return (self.currently_connected_dev if
self.currently_connected_dev in devs else None)
# Remove ejected devices
devs = devs - self.ejected_devices
# Now check for MTP devices
cache = self.detect_cache
for d in devs:
ans = cache.get(d, None)
if ans is None:
ans = self.libmtp.is_mtp_device(d.busnum, d.devnum,
d.vendor_id, d.product_id)
cache[d] = ans
if ans:
return d
return None
@synchronous
def create_device(self, connected_device):
d = connected_device
return self.libmtp.Device(d.busnum, d.devnum, d.vendor_id,
d.product_id, d.manufacturer, d.product, d.serial)
@synchronous
def eject(self):
if self.currently_connected_dev is None: return
self.ejected_devices.add(self.currently_connected_dev)
self.post_yank_cleanup()
@synchronous
def post_yank_cleanup(self):
self.dev = self._filesystem_cache = self.current_friendly_name = None
self.currently_connected_dev = None
@synchronous
def startup(self):
self.detect = MTPDetect()
for x in vars(self.detect.libmtp):
p = plugins['libmtp']
self.libmtp = p[0]
if self.libmtp is None:
print ('Failed to load libmtp, MTP device detection disabled')
print (p[1])
for x in vars(self.libmtp):
if x.startswith('LIBMTP'):
setattr(self, x, getattr(self.detect.libmtp, x))
setattr(self, x, getattr(self.libmtp, x))
@synchronous
def shutdown(self):
@ -85,29 +125,25 @@ class MTP_DEVICE(MTPDeviceBase):
@synchronous
def open(self, connected_device, library_uuid):
self.dev = self._filesystem_cache = None
def blacklist_device():
d = connected_device
self.blacklisted_devices.add((d.busnum, d.devnum, d.vendor_id,
d.product_id, d.bcd, d.serial))
try:
self.dev = self.detect.create_device(connected_device)
except ValueError:
self.dev = self.create_device(connected_device)
except self.libmtp.MTPError:
# Give the device some time to settle
time.sleep(2)
try:
self.dev = self.detect.create_device(connected_device)
except ValueError:
self.dev = self.create_device(connected_device)
except self.libmtp.MTPError:
# Black list this device so that it is ignored for the
# remainder of this session.
blacklist_device()
self.blacklisted_devices.add(connected_device)
raise OpenFailed('%s is not a MTP device'%(connected_device,))
except TypeError:
blacklist_device()
self.blacklisted_devices.add(connected_device)
raise OpenFailed('')
storage = sorted(self.dev.storage_info, key=operator.itemgetter('id'))
if not storage:
blacklist_device()
self.blacklisted_devices.add(connected_device)
raise OpenFailed('No storage found for device %s'%(connected_device,))
self._main_id = storage[0]['id']
self._carda_id = self._cardb_id = None