More smart device plumbing

This commit is contained in:
Kovid Goyal 2012-07-25 13:15:43 +05:30
commit 859d8c6f50
9 changed files with 390 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -514,6 +514,67 @@ class DevicePlugin(Plugin):
'''
pass
# Dynamic control interface
# All of these methods are called on the device_manager thread
def is_dynamically_controllable(self):
'''
Called by the device manager when starting plugins. If this method returns
a string, then a) it supports the device manager's dynamic control
interface, and b) that name is to be used when talking to the plugin.
This method must be called from the device_manager thread.
'''
return None
def start_plugin(self):
'''
This method is called to start the plugin. The plugin should begin
to accept device connections however it does that. If the plugin is
already accepting connections, then do nothing.
This method must be called from the device_manager thread.
'''
pass
def stop_plugin(self):
'''
This method is called to stop the plugin. The plugin should no longer
accept connections, and should cleanup behind itself. It is likely that
this method should call shutdown. If the plugin is already not accepting
connections, then do nothing.
This method must be called from the device_manager thread.
'''
pass
def get_option(self, opt_string, default=None):
'''
Return the value of the option indicated by opt_string. This method can
be called when the plugin is not started. Return None if the option does
not exist.
This method must be called from the device_manager thread.
'''
return default
def set_option(self, opt_string, opt_value):
'''
Set the value of the option indicated by opt_string. This method can
be called when the plugin is not started.
This method must be called from the device_manager thread.
'''
pass
def is_running(self):
'''
Return True if the plugin is started, otherwise false
This method must be called from the device_manager thread.
'''
return False
class BookList(list):
'''
A list of books. Each Book object must have the fields

View File

@ -14,6 +14,7 @@ from calibre.utils.smtp import config as email_config
from calibre.constants import iswindows, isosx
from calibre.customize.ui import is_disabled
from calibre.devices.bambook.driver import BAMBOOK
from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog
from calibre.gui2 import info_dialog
class ShareConnMenu(QMenu): # {{{
@ -24,6 +25,7 @@ class ShareConnMenu(QMenu): # {{{
config_email = pyqtSignal()
toggle_server = pyqtSignal()
control_smartdevice = pyqtSignal()
dont_add_to = frozenset(['context-menu-device'])
def __init__(self, parent=None):
@ -56,6 +58,11 @@ class ShareConnMenu(QMenu): # {{{
_('Start Content Server'))
self.toggle_server_action.triggered.connect(lambda x:
self.toggle_server.emit())
self.control_smartdevice_action = \
self.addAction(QIcon(I('dot_green.png')),
_('Control Smart Device Connections'))
self.control_smartdevice_action.triggered.connect(lambda x:
self.control_smartdevice.emit())
self.addSeparator()
self.email_actions = []
@ -80,6 +87,9 @@ class ShareConnMenu(QMenu): # {{{
text = _('Stop Content Server') + ' [%s]'%get_external_ip()
self.toggle_server_action.setText(text)
def hide_smartdevice_menus(self):
self.control_smartdevice_action.setVisible(False)
def build_email_entries(self, sync_menu):
from calibre.gui2.device import DeviceAction
for ac in self.email_actions:
@ -158,6 +168,7 @@ class ConnectShareAction(InterfaceAction):
def genesis(self):
self.share_conn_menu = ShareConnMenu(self.gui)
self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
self.share_conn_menu.control_smartdevice.connect(self.control_smartdevice)
self.share_conn_menu.config_email.connect(partial(
self.gui.iactions['Preferences'].do_config,
initial_plugin=('Sharing', 'Email')))
@ -200,8 +211,21 @@ class ConnectShareAction(InterfaceAction):
if not self.stopping_msg.isVisible():
self.stopping_msg.exec_()
return
self.gui.content_server = None
self.stopping_msg.accept()
def control_smartdevice(self):
sd_dialog = SmartdeviceDialog(self.gui)
sd_dialog.exec_()
self.set_smartdevice_icon()
def check_smartdevice_menus(self):
if not self.gui.device_manager.is_enabled('smartdevice'):
self.share_conn_menu.hide_smartdevice_menus()
def set_smartdevice_icon(self):
running = self.gui.device_manager.is_running('smartdevice')
if running:
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_green.png')))
else:
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_red.png')))

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
# Imports {{{
import os, traceback, Queue, time, cStringIO, re, sys
from threading import Thread
from threading import Thread, Event
from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL,
Qt, pyqtSignal, QDialog, QObject, QVBoxLayout,
@ -30,6 +30,7 @@ from calibre.constants import DEBUG
from calibre.utils.config import prefs, tweaks
from calibre.utils.magick.draw import thumbnail
from calibre.library.save_to_disk import find_plugboard
from calibre.gui2 import is_gui_thread
# }}}
class DeviceJob(BaseJob): # {{{
@ -145,6 +146,10 @@ class DeviceManager(Thread): # {{{
self._device_information = None
self.current_library_uuid = None
self.call_shutdown_on_disconnect = False
self.devices_initialized = Event()
self.dynamic_plugins = {}
self.dynamic_plugin_requests = Queue.Queue(0)
self.dynamic_plugin_responses = Queue.Queue(0)
def report_progress(self, *args):
pass
@ -286,6 +291,10 @@ class DeviceManager(Thread): # {{{
# Do any device-specific startup processing.
for d in self.devices:
self.run_startup(d)
n = d.is_dynamically_controllable()
if n:
self.dynamic_plugins[n] = d
self.devices_initialized.set()
while self.keep_going:
kls = None
@ -309,6 +318,7 @@ class DeviceManager(Thread): # {{{
traceback.print_exc()
else:
self.detect_device()
while True:
job = self.next()
if job is not None:
@ -319,8 +329,15 @@ class DeviceManager(Thread): # {{{
self.current_job = None
else:
break
time.sleep(self.sleep_time)
while True:
dynamic_method = None
try:
(dynamic_method, args, kwargs) = \
self.dynamic_plugin_requests.get(timeout=self.sleep_time)
res = dynamic_method(*args, **kwargs)
self.dynamic_plugin_responses.put(res)
except Queue.Empty:
break
# We are exiting. Call the shutdown method for each plugin
for p in self.devices:
try:
@ -508,6 +525,51 @@ class DeviceManager(Thread): # {{{
if self.connected_device:
self.connected_device.set_driveinfo_name(location_code, name)
# dynamic plugin interface
# This is a helper function that handles queueing with the device manager
def _queue_request(self, name, method, *args, **kwargs):
if not is_gui_thread():
raise ValueError(
'The device_manager dynamic plugin methods must be called from the GUI thread')
try:
d = self.dynamic_plugins.get(name, None)
if d:
self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs))
return self.dynamic_plugin_responses.get()
except:
traceback.print_exc()
return kwargs.get('default', None)
# The dynamic plugin methods below must be called on the GUI thread. They
# will switch to the device thread before calling the plugin.
def start_plugin(self, name):
self._queue_request(name, 'start_plugin')
def stop_plugin(self, name):
self._queue_request(name, 'stop_plugin')
def get_option(self, name, opt_string, default=None):
return self._queue_request(name, 'get_option', opt_string, default=default)
def set_option(self, name, opt_string, opt_value):
self._queue_request(name, 'set_option', opt_string, opt_value)
def is_running(self, name):
if self._queue_request(name, 'is_running'):
return True
return False
def is_enabled(self, name):
try:
d = self.dynamic_plugins.get(name, None)
if d:
return True
except:
pass
return False
# }}}
class DeviceAction(QAction): # {{{
@ -708,6 +770,7 @@ class DeviceMixin(object): # {{{
self.job_manager, Dispatcher(self.status_bar.show_message),
Dispatcher(self.show_open_feedback))
self.device_manager.start()
self.device_manager.devices_initialized.wait()
if tweaks['auto_connect_to_folder']:
self.connect_to_folder_named(tweaks['auto_connect_to_folder'])

View File

@ -0,0 +1,81 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re
from PyQt4.QtGui import QDialog, QLineEdit
from PyQt4.QtCore import SIGNAL, Qt
from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog
from calibre.gui2 import dynamic
class SmartdeviceDialog(QDialog, Ui_Dialog):
def __init__(self, parent):
QDialog.__init__(self, parent)
Ui_Dialog.__init__(self)
self.setupUi(self)
self.msg.setText(
_('This dialog starts and stops the smart device app interface. '
'When you start the interface, you might see some messages from '
'your computer\'s firewall or anti-virus manager asking you '
'if it is OK for calibre to connect to the network. <B>Please '
'answer yes</b>. If you do not, the app will not work. It will '
'be unable to connect to calibre.'))
self.password_box.setToolTip('<p>' +
_('Use a password if calibre is running on a network that '
'is not secure. For example, if you run calibre on a laptop, '
'use that laptop in an airport, and want to connect your '
'smart device to calibre, you should use a password.') + '</p>')
self.run_box.setToolTip('<p>' +
_('Check this box to allow calibre to accept connections from the '
'smart device. Uncheck the box to prevent connections.') + '</p>')
self.autostart_box.setToolTip('<p>' +
_('Check this box if you want calibre to automatically start the '
'smart device interface when calibre starts. You should not do '
'this if you are using a network that is not secure and you '
'are not setting a password.') + '</p>')
self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.toggle_password)
self.autostart_box.stateChanged.connect(self.autostart_changed)
self.device_manager = parent.device_manager
if self.device_manager.is_running('smartdevice'):
self.run_box.setChecked(True)
else:
self.run_box.setChecked(False)
if self.device_manager.get_option('smartdevice', 'autostart'):
self.autostart_box.setChecked(True)
self.run_box.setChecked(True)
self.run_box.setEnabled(False)
pw = self.device_manager.get_option('smartdevice', 'password')
if pw:
self.password_box.setText(pw)
def autostart_changed(self):
if self.autostart_box.isChecked():
self.run_box.setChecked(True)
self.run_box.setEnabled(False)
else:
self.run_box.setEnabled(True)
def toggle_password(self, state):
if state == Qt.Unchecked:
self.password_box.setEchoMode(QLineEdit.Password)
else:
self.password_box.setEchoMode(QLineEdit.Normal)
def accept(self):
self.device_manager.set_option('smartdevice', 'password',
unicode(self.password_box.text()))
self.device_manager.set_option('smartdevice', 'autostart',
self.autostart_box.isChecked())
if self.run_box.isChecked():
self.device_manager.start_plugin('smartdevice')
else:
self.device_manager.stop_plugin('smartdevice')
QDialog.accept(self)

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>209</height>
</rect>
</property>
<property name="windowTitle">
<string>Smart device control</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/mimetypes/unknown.png</normaloff>:/images/mimetypes/unknown.png</iconset>
</property>
<layout class="QGridLayout">
<item row="4" column="1">
<widget class="QCheckBox" name="autostart_box">
<property name="text">
<string>&amp;Automatically allow connections at startup</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="label_43">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Optional password for security</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="run_box">
<property name="text">
<string>&amp;Allow connections</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="msg">
<property name="text">
<string>TextLabel</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Password:</string>
</property>
<property name="buddy">
<cstring>password_box</cstring>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="show_password">
<property name="text">
<string>&amp;Show password</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -337,6 +337,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if config['autolaunch_server']:
self.start_content_server()
smartdevice_actions = self.iactions['Connect Share']
smartdevice_actions.check_smartdevice_menus()
if self.device_manager.get_option('smartdevice', 'autostart'):
try:
self.device_manager.start_plugin('smartdevice')
except:
pass
smartdevice_actions.set_smartdevice_icon()
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
self.read_settings()

View File

@ -955,11 +955,16 @@ class Reaper(threading.Thread):
return
if globals()['_GLOBAL_DONE']:
return
try:
# can get here in a race condition with shutdown. Swallow the
# exception and run around the loop again.
now = currentTimeMillis()
for record in self.zeroconf.cache.entries():
if record.isExpired(now):
self.zeroconf.updateRecord(now, record)
self.zeroconf.cache.remove(record)
except:
pass
class ServiceBrowser(threading.Thread):