Better handling of smart device disconnects during initialization, and especially password mismatches.

This commit is contained in:
Charles Haley 2012-08-04 11:50:25 +02:00
parent 8e4c24c8a2
commit df2c0a2106
3 changed files with 75 additions and 68 deletions

View File

@ -48,6 +48,11 @@ class OpenFeedback(DeviceError):
''' '''
raise NotImplementedError raise NotImplementedError
class OpenFailed(ProtocolError):
""" Raised when device cannot be opened this time. No retry is to be done.
The device should continue to be polled for future opens. If the
message is empty, no exception trace is produced. """
class DeviceBusy(ProtocolError): class DeviceBusy(ProtocolError):
""" Raised when device is busy """ """ Raised when device is busy """
def __init__(self, uerr=""): def __init__(self, uerr=""):

View File

@ -14,7 +14,7 @@ from functools import wraps
from calibre import prints from calibre import prints
from calibre.constants import numeric_version, DEBUG from calibre.constants import numeric_version, DEBUG
from calibre.devices.errors import OpenFeedback from calibre.devices.errors import OpenFailed, ControlError, TimeoutError
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.devices.usbms.books import Book, BookList from calibre.devices.usbms.books import Book, BookList
from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.deviceconfig import DeviceConfig
@ -353,24 +353,18 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug('protocol error -- empty json string') self._debug('protocol error -- empty json string')
except socket.timeout: except socket.timeout:
self._debug('timeout communicating with device') self._debug('timeout communicating with device')
self.device_socket.close() self._close_device_socket()
self.device_socket = None raise TimeoutError('Device did not respond in reasonable time')
self.is_connected = False
raise IOError(_('Device did not respond in reasonable time'))
except socket.error: except socket.error:
self._debug('device went away') self._debug('device went away')
self.device_socket.close() self._close_device_socket()
self.device_socket = None raise ControlError('Device closed the network connection')
self.is_connected = False
raise IOError(_('Device closed the network connection'))
except: except:
self._debug('other exception') self._debug('other exception')
traceback.print_exc() traceback.print_exc()
self.device_socket.close() self._close_device_socket()
self.device_socket = None
self.is_connected = False
raise raise
raise IOError('Device responded with incorrect information') raise ControlError('Device responded with incorrect information')
# Write a file as a series of base64-encoded strings. # Write a file as a series of base64-encoded strings.
def _put_file(self, infile, lpath, book_metadata, this_book, total_books): def _put_file(self, infile, lpath, book_metadata, this_book, total_books):
@ -449,8 +443,16 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
else: else:
self.known_metadata[lpath] = book.deepcopy() self.known_metadata[lpath] = book.deepcopy()
# The public interface methods. def _close_device_socket(self):
if self.device_socket is not None:
try:
self.device_socket.close()
except:
pass
self.device_socket = None
self.is_connected = False
# The public interface methods.
@synchronous('sync_lock') @synchronous('sync_lock')
def is_usb_connected(self, devices_on_system, debug=False, only_presence=False): def is_usb_connected(self, devices_on_system, debug=False, only_presence=False):
@ -471,11 +473,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
pass pass
try: try:
if self._call_client('NOOP', dict())[0] is None: if self._call_client('NOOP', dict())[0] is None:
self.is_connected = False self._close_device_socket()
except: except:
self.is_connected = False self._close_device_socket()
if not self.is_connected:
self.device_socket.close()
return (self.is_connected, self) return (self.is_connected, self)
if getattr(self, 'listen_socket', None) is not None: if getattr(self, 'listen_socket', None) is not None:
ans = select.select((self.listen_socket,), (), (), 0) ans = select.select((self.listen_socket,), (), (), 0)
@ -491,15 +491,11 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self.device_socket.settimeout(None) self.device_socket.settimeout(None)
self.is_connected = True self.is_connected = True
except socket.timeout: except socket.timeout:
if self.device_socket is not None: self._close_device_socket()
self.device_socket.close()
self.is_connected = False
except socket.error: except socket.error:
x = sys.exc_info()[1] x = sys.exc_info()[1]
self._debug('unexpected socket exception', x.args[0]) self._debug('unexpected socket exception', x.args[0])
if self.device_socket is not None: self._close_device_socket()
self.device_socket.close()
self.is_connected = False
raise raise
return (True, self) return (True, self)
return (False, None) return (False, None)
@ -507,6 +503,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
@synchronous('sync_lock') @synchronous('sync_lock')
def open(self, connected_device, library_uuid): def open(self, connected_device, library_uuid):
self._debug() self._debug()
if not self.is_connected:
# We have been called to retry the connection. Give up immediately
raise ControlError('Attempt to open a closed device')
self.current_library_uuid = library_uuid self.current_library_uuid = library_uuid
self.current_library_name = current_library_name() self.current_library_name = current_library_name()
try: try:
@ -530,28 +529,24 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
# Something wrong with the return. Close the socket # Something wrong with the return. Close the socket
# and continue. # and continue.
self._debug('Protocol error - Opcode not OK') self._debug('Protocol error - Opcode not OK')
self.device_socket.close() self._close_device_socket()
self.is_connected = False
return False return False
if not result.get('versionOK', False): if not result.get('versionOK', False):
# protocol mismatch # protocol mismatch
self._debug('Protocol error - protocol version mismatch') self._debug('Protocol error - protocol version mismatch')
self.device_socket.close() self._close_device_socket()
self.is_connected = False
return False return False
if result.get('maxBookContentPacketLen', 0) <= 0: if result.get('maxBookContentPacketLen', 0) <= 0:
# protocol mismatch # protocol mismatch
self._debug('Protocol error - bogus book packet length') self._debug('Protocol error - bogus book packet length')
self.device_socket.close() self._close_device_socket()
self.is_connected = False
return False return False
self.max_book_packet_len = result.get('maxBookContentPacketLen', self.max_book_packet_len = result.get('maxBookContentPacketLen',
self.BASE_PACKET_LEN) self.BASE_PACKET_LEN)
exts = result.get('acceptedExtensions', None) exts = result.get('acceptedExtensions', None)
if exts is None or not isinstance(exts, list) or len(exts) == 0: if exts is None or not isinstance(exts, list) or len(exts) == 0:
self._debug('Protocol error - bogus accepted extensions') self._debug('Protocol error - bogus accepted extensions')
self.device_socket.close() self._close_device_socket()
self.is_connected = False
return False return False
self.FORMATS = exts self.FORMATS = exts
if password: if password:
@ -559,25 +554,29 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
if result.get('passwordHash', None) is None: if result.get('passwordHash', None) is None:
# protocol mismatch # protocol mismatch
self._debug('Protocol error - missing password hash') self._debug('Protocol error - missing password hash')
self.device_socket.close() self._close_device_socket()
self.is_connected = False
return False return False
if returned_hash != hash_digest: if returned_hash != hash_digest:
# bad password # bad password
self._debug('password mismatch') self._debug('password mismatch')
self._call_client("DISPLAY_MESSAGE", {'messageKind':1}) try:
self.is_connected = False self._call_client("DISPLAY_MESSAGE",
self.device_socket.close() {'messageKind':1,
raise OpenFeedback('Incorrect password supplied') 'currentLibraryName': self.current_library_name,
'currentLibraryUUID': library_uuid})
except:
pass
self._close_device_socket()
# Don't bother with a message. The user will be informed on
# the device.
raise OpenFailed('')
return True return True
except socket.timeout: except socket.timeout:
self.device_socket.close() self._close_device_socket()
self.is_connected = False
except socket.error: except socket.error:
x = sys.exc_info()[1] x = sys.exc_info()[1]
self._debug('unexpected socket exception', x.args[0]) self._debug('unexpected socket exception', x.args[0])
self.device_socket.close() self._close_device_socket()
self.is_connected = False
raise raise
return False return False
@ -656,7 +655,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._set_known_metadata(book) self._set_known_metadata(book)
bl.add_book(book, replace_metadata=True) bl.add_book(book, replace_metadata=True)
else: else:
raise IOError(_('Protocol error -- book metadata not returned')) raise ControlError('book metadata not returned')
return bl return bl
@synchronous('sync_lock') @synchronous('sync_lock')
@ -675,15 +674,12 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
print_debug_info=False) print_debug_info=False)
if opcode != 'OK': if opcode != 'OK':
self._debug('protocol error', opcode, i) self._debug('protocol error', opcode, i)
raise IOError(_('Protocol error -- sync_booklists')) raise ControlError('sync_booklists')
@synchronous('sync_lock') @synchronous('sync_lock')
def eject(self): def eject(self):
self._debug() self._debug()
if self.device_socket: self._close_device_socket()
self.device_socket.close()
self.device_socket = None
self.is_connected = False
@synchronous('sync_lock') @synchronous('sync_lock')
def post_yank_cleanup(self): def post_yank_cleanup(self):
@ -706,7 +702,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
book = Book(self.PREFIX, lpath, other=mdata) book = Book(self.PREFIX, lpath, other=mdata)
length = self._put_file(infile, lpath, book, i, len(files)) length = self._put_file(infile, lpath, book, i, len(files))
if length < 0: if length < 0:
raise IOError(_('Sending book %s to device failed') % lpath) raise ControlError('Sending book %s to device failed' % lpath)
paths.append((lpath, length)) paths.append((lpath, length))
# No need to deal with covers. The client will get the thumbnails # No need to deal with covers. The client will get the thumbnails
# in the mi structure # in the mi structure
@ -747,7 +743,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
if opcode == 'OK': if opcode == 'OK':
self._debug('removed book with UUID', result['uuid']) self._debug('removed book with UUID', result['uuid'])
else: else:
raise IOError(_('Protocol error - delete books')) raise ControlError('Protocol error - delete books')
@synchronous('sync_lock') @synchronous('sync_lock')
def remove_books_from_metadata(self, paths, booklists): def remove_books_from_metadata(self, paths, booklists):
@ -783,7 +779,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
else: else:
eof = True eof = True
else: else:
raise IOError(_('request for book data failed')) raise ControlError('request for book data failed')
@synchronous('sync_lock') @synchronous('sync_lock')
def set_plugboards(self, plugboards, pb_func): def set_plugboards(self, plugboards, pb_func):

View File

@ -13,7 +13,7 @@ from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL,
from calibre.customize.ui import (available_input_formats, available_output_formats, from calibre.customize.ui import (available_input_formats, available_output_formats,
device_plugins) device_plugins)
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.devices.errors import UserFeedback, OpenFeedback from calibre.devices.errors import UserFeedback, OpenFeedback, OpenFailed
from calibre.gui2.dialogs.choose_format_device import ChooseFormatDeviceDialog from calibre.gui2.dialogs.choose_format_device import ChooseFormatDeviceDialog
from calibre.utils.ipc.job import BaseJob from calibre.utils.ipc.job import BaseJob
from calibre.devices.scanner import DeviceScanner from calibre.devices.scanner import DeviceScanner
@ -172,6 +172,8 @@ class DeviceManager(Thread): # {{{
self.open_feedback_msg(dev.get_gui_name(), e) self.open_feedback_msg(dev.get_gui_name(), e)
self.ejected_devices.add(dev) self.ejected_devices.add(dev)
continue continue
except OpenFailed:
raise
except: except:
tb = traceback.format_exc() tb = traceback.format_exc()
if DEBUG or tb not in self.reported_errors: if DEBUG or tb not in self.reported_errors:
@ -225,6 +227,7 @@ class DeviceManager(Thread): # {{{
only_presence=True, debug=True) only_presence=True, debug=True)
self.connected_device_removed() self.connected_device_removed()
else: else:
try:
possibly_connected_devices = [] possibly_connected_devices = []
for device in self.devices: for device in self.devices:
if device in self.ejected_devices: if device in self.ejected_devices:
@ -243,6 +246,9 @@ class DeviceManager(Thread): # {{{
device_kind='usb'): device_kind='usb'):
if DEBUG: if DEBUG:
prints('Device connect failed again, giving up') prints('Device connect failed again, giving up')
except OpenFailed as e:
if str(e):
traceback.print_exc()
# Mount devices that don't use USB, such as the folder device and iTunes # Mount devices that don't use USB, such as the folder device and iTunes
# This will be called on the GUI thread. Because of this, we must store # This will be called on the GUI thread. Because of this, we must store