Wireless device driver: Fix errors on some OS X machines caused by network resource exhaustion.

This commit is contained in:
Kovid Goyal 2012-08-13 22:03:02 +05:30
commit b71829038e
2 changed files with 70 additions and 20 deletions

View File

@ -92,6 +92,7 @@ class ControlError(ProtocolError):
def __init__(self, query=None, response=None, desc=None): def __init__(self, query=None, response=None, desc=None):
self.query = query self.query = query
self.response = response self.response = response
self.desc = desc
ProtocolError.__init__(self, desc) ProtocolError.__init__(self, desc)
def __str__(self): def __str__(self):

View File

@ -11,11 +11,12 @@ import socket, select, json, inspect, os, traceback, time, sys, random
import hashlib, threading import hashlib, threading
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from functools import wraps from functools import wraps
from errno import EAGAIN, EINTR
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 (OpenFailed, ControlError, TimeoutError, from calibre.devices.errors import (OpenFailed, ControlError, TimeoutError,
InitialConnectionError) InitialConnectionError, PacketError)
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.devices.usbms.books import Book, CollectionsBookList from calibre.devices.usbms.books import Book, CollectionsBookList
from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.deviceconfig import DeviceConfig
@ -85,6 +86,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
MAX_CLIENT_COMM_TIMEOUT = 60.0 # Wait at most N seconds for an answer MAX_CLIENT_COMM_TIMEOUT = 60.0 # Wait at most N seconds for an answer
MAX_UNSUCCESSFUL_CONNECTS = 5 MAX_UNSUCCESSFUL_CONNECTS = 5
SEND_NOOP_EVERY_NTH_PROBE = 5
DISCONNECT_AFTER_N_SECONDS = 30*60 # 30 minutes
opcodes = { opcodes = {
'NOOP' : 12, 'NOOP' : 12,
'OK' : 0, 'OK' : 0,
@ -120,7 +124,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
_('Use fixed network port') + ':::<p>' + _('Use fixed network port') + ':::<p>' +
_('If checked, use the port number in the "Port" box, otherwise ' _('If checked, use the port number in the "Port" box, otherwise '
'the driver will pick a random port') + '</p>', 'the driver will pick a random port') + '</p>',
_('Port') + ':::<p>' + _('Port number: ') + ':::<p>' +
_('Enter the port number the driver is to use if the "fixed port" box is checked') + '</p>', _('Enter the port number the driver is to use if the "fixed port" box is checked') + '</p>',
_('Print extra debug information') + ':::<p>' + _('Print extra debug information') + ':::<p>' +
_('Check this box if requested when reporting problems') + '</p>', _('Check this box if requested when reporting problems') + '</p>',
@ -131,7 +135,13 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
_('. Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add ' _('. Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add '
'these values to the list to enable them. The collections will be ' 'these values to the list to enable them. The collections will be '
'given the name provided after the ":" character.')%dict( 'given the name provided after the ":" character.')%dict(
abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR) abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR),
'',
_('Enable the no-activity timeout') + ':::<p>' +
_('If this box is checked, calibre will automatically disconnect if '
'a connected device does nothing for %d minutes. Unchecking this '
' box disables this timeout, so calibre will never automatically '
'disconnect.')%(DISCONNECT_AFTER_N_SECONDS/60,) + '</p>',
] ]
EXTRA_CUSTOMIZATION_DEFAULT = [ EXTRA_CUSTOMIZATION_DEFAULT = [
False, False,
@ -141,7 +151,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
False, '9090', False, '9090',
False, False,
'', '',
'' '',
'',
True,
] ]
OPT_AUTOSTART = 0 OPT_AUTOSTART = 0
OPT_PASSWORD = 2 OPT_PASSWORD = 2
@ -149,6 +161,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
OPT_PORT_NUMBER = 5 OPT_PORT_NUMBER = 5
OPT_EXTRA_DEBUG = 6 OPT_EXTRA_DEBUG = 6
OPT_COLLECTIONS = 8 OPT_COLLECTIONS = 8
OPT_AUTODISCONNECT = 10
def __init__(self, path): def __init__(self, path):
self.sync_lock = threading.RLock() self.sync_lock = threading.RLock()
@ -165,7 +178,16 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
inspect.stack()[1][3]), end='') inspect.stack()[1][3]), end='')
for a in args: for a in args:
try: try:
prints('', a, end='') if isinstance(a, dict):
printable = {}
for k,v in a.iteritems():
if isinstance(v, (str, unicode)) and len(v) > 50:
printable[k] = 'too long'
else:
printable[k] = v
prints('', printable, end='');
else:
prints('', a, end='')
except: except:
prints('', 'value too long', end='') prints('', 'value too long', end='')
print() print()
@ -339,6 +361,26 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
pos += len(v) pos += len(v)
return data return data
def _send_byte_string(self, s):
if not isinstance(s, bytes):
self._debug('given a non-byte string!')
raise PacketError("Internal error: found a string that isn't bytes")
sent_len = 0;
total_len = len(s)
while sent_len < total_len:
try:
if sent_len == 0:
amt_sent = self.device_socket.send(s)
else:
amt_sent = self.device_socket.send(s[sent_len:])
if amt_sent <= 0:
raise IOError('Bad write on device socket');
sent_len += amt_sent
except socket.error as e:
self._debug('socket error', e, e.errno)
if e.args[0] != EAGAIN and e.args[0] != EINTR:
raise
def _call_client(self, op, arg, print_debug_info=True): def _call_client(self, op, arg, print_debug_info=True):
if op != 'NOOP': if op != 'NOOP':
self.noop_counter = 0 self.noop_counter = 0
@ -355,9 +397,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
if print_debug_info and extra_debug: if print_debug_info and extra_debug:
self._debug('send string', s) self._debug('send string', s)
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT)
self.device_socket.sendall(('%d' % len(s))+s) self._send_byte_string((b'%d' % len(s))+s)
self.device_socket.settimeout(None)
v = self._read_string_from_net() v = self._read_string_from_net()
self.device_socket.settimeout(None)
if print_debug_info and extra_debug: if print_debug_info and extra_debug:
self._debug('received string', v) self._debug('received string', v)
if v: if v:
@ -373,13 +415,13 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
except socket.error: except socket.error:
self._debug('device went away') self._debug('device went away')
self._close_device_socket() self._close_device_socket()
raise ControlError('Device closed the network connection') raise ControlError(desc='Device closed the network connection')
except: except:
self._debug('other exception') self._debug('other exception')
traceback.print_exc() traceback.print_exc()
self._close_device_socket() self._close_device_socket()
raise raise
raise ControlError('Device responded with incorrect information') raise ControlError(desc='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):
@ -475,7 +517,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self.is_connected = False self.is_connected = False
if self.is_connected: if self.is_connected:
self.noop_counter += 1 self.noop_counter += 1
if only_presence and (self.noop_counter % 5) != 1: if only_presence and (
self.noop_counter % self.SEND_NOOP_EVERY_NTH_PROBE) != 1:
try: try:
ans = select.select((self.device_socket,), (), (), 0) ans = select.select((self.device_socket,), (), (), 0)
if len(ans[0]) == 0: if len(ans[0]) == 0:
@ -486,11 +529,16 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
# This will usually toss an exception if the socket is gone. # This will usually toss an exception if the socket is gone.
except: except:
pass pass
try: if (self.settings().extra_customization[self.OPT_AUTODISCONNECT] and
if self._call_client('NOOP', dict())[0] is None: self.noop_counter > self.DISCONNECT_AFTER_N_SECONDS):
self._close_device_socket()
except:
self._close_device_socket() self._close_device_socket()
self._debug('timeout -- disconnected')
else:
try:
if self._call_client('NOOP', dict())[0] is None:
self._close_device_socket()
except:
self._close_device_socket()
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)
@ -533,7 +581,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug() self._debug()
if not self.is_connected: if not self.is_connected:
# We have been called to retry the connection. Give up immediately # We have been called to retry the connection. Give up immediately
raise ControlError('Attempt to open a closed device') raise ControlError(desc='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:
@ -569,6 +617,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug('Protocol error - bogus book packet length') self._debug('Protocol error - bogus book packet length')
self._close_device_socket() self._close_device_socket()
return False return False
self._debug('CC version #:', result.get('ccVersionNumber', 'unknown'))
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)
@ -689,7 +738,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 ControlError('book metadata not returned') raise ControlError(desc='book metadata not returned')
return bl return bl
@synchronous('sync_lock') @synchronous('sync_lock')
@ -720,7 +769,7 @@ 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 ControlError('sync_booklists') raise ControlError(desc='sync_booklists')
@synchronous('sync_lock') @synchronous('sync_lock')
def eject(self): def eject(self):
@ -748,7 +797,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 ControlError('Sending book %s to device failed' % lpath) raise ControlError(desc='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
@ -789,7 +838,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 ControlError('Protocol error - delete books') raise ControlError(desc='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):
@ -825,7 +874,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
else: else:
eof = True eof = True
else: else:
raise ControlError('request for book data failed') raise ControlError(desc='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):