diff --git a/src/calibre/constants.py b/src/calibre/constants.py
index 7d2c0af091..a9041ad93b 100644
--- a/src/calibre/constants.py
+++ b/src/calibre/constants.py
@@ -38,6 +38,11 @@ fcntl = None if iswindows else __import__('fcntl')
filesystem_encoding = sys.getfilesystemencoding()
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
+DEBUG = False
+
+def debug():
+ global DEBUG
+ DEBUG = True
################################################################################
plugins = None
diff --git a/src/calibre/debug.py b/src/calibre/debug.py
index 0b09442eac..b7fb55f4aa 100644
--- a/src/calibre/debug.py
+++ b/src/calibre/debug.py
@@ -164,6 +164,8 @@ def add_simple_plugin(path_to_plugin):
def main(args=sys.argv):
+ from calibre.constants import debug
+ debug()
opts, args = option_parser().parse_args(args)
if opts.gui:
from calibre.gui2.main import main
diff --git a/src/calibre/devices/blackberry/driver.py b/src/calibre/devices/blackberry/driver.py
index 24642cf980..1d96d4118f 100644
--- a/src/calibre/devices/blackberry/driver.py
+++ b/src/calibre/devices/blackberry/driver.py
@@ -17,15 +17,15 @@ class BLACKBERRY(USBMS):
FORMATS = ['mobi', 'prc']
VENDOR_ID = [0x0fca]
- PRODUCT_ID = [0x8004]
- BCD = [0x0200]
+ PRODUCT_ID = [0x8004, 0x0004]
+ BCD = [0x0200, 0x0107]
VENDOR_NAME = 'RIM'
WINDOWS_MAIN_MEM = 'BLACKBERRY_SD'
#OSX_MAIN_MEM = 'Kindle Internal Storage Media'
- MAIN_MEMORY_VOLUME_LABEL = 'Blackberry Main Memory'
+ MAIN_MEMORY_VOLUME_LABEL = 'Blackberry SD Card'
EBOOK_DIR_MAIN = 'ebooks'
SUPPORTS_SUB_DIRS = True
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index fe69bd1579..4cde8dbe57 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -17,7 +17,10 @@ class Book(object):
self.authors = authors
self.mime = mime
self.size = os.path.getsize(path)
- self.datetime = time.gmtime(os.path.getctime(path))
+ try:
+ self.datetime = time.gmtime(os.path.getctime(path))
+ except ValueError:
+ self.datetime = time.gmtime()
self.path = path
self.thumbnail = None
self.tags = []
diff --git a/src/calibre/ebooks/metadata/worker.py b/src/calibre/ebooks/metadata/worker.py
index 071ec77c23..684ebd6b79 100644
--- a/src/calibre/ebooks/metadata/worker.py
+++ b/src/calibre/ebooks/metadata/worker.py
@@ -8,12 +8,13 @@ __docformat__ = 'restructuredtext en'
from threading import Thread
from Queue import Empty
-import os, time, sys
+import os, time, sys, shutil
from calibre.utils.ipc.job import ParallelJob
from calibre.utils.ipc.server import Server
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre import prints
+from calibre.constants import filesystem_encoding
def debug(*args):
@@ -23,6 +24,7 @@ def debug(*args):
def read_metadata_(task, tdir, notification=lambda x,y:x):
from calibre.ebooks.metadata.meta import metadata_from_formats
from calibre.ebooks.metadata.opf2 import metadata_to_opf
+ from calibre.customize.ui import run_plugins_on_import
for x in task:
try:
id, formats = x
@@ -38,6 +40,24 @@ def read_metadata_(task, tdir, notification=lambda x,y:x):
if cdata:
with open(os.path.join(tdir, str(id)), 'wb') as f:
f.write(cdata)
+ import_map = {}
+ for format in formats:
+ nfp = run_plugins_on_import(format)
+ nfp = os.path.abspath(nfp)
+ if isinstance(nfp, unicode):
+ nfp.encode(filesystem_encoding)
+ x = lambda j : os.path.abspath(os.path.normpath(os.path.normcase(j)))
+ if x(nfp) != x(format) and os.access(nfp, os.R_OK|os.W_OK):
+ fmt = os.path.splitext(format)[1].replace('.', '').lower()
+ nfmt = os.path.splitext(nfp)[1].replace('.', '').lower()
+ dest = os.path.join(tdir, '%s.%s'%(id, nfmt))
+ shutil.copyfile(nfp, dest)
+ import_map[fmt] = dest
+ os.remove(nfp)
+ if import_map:
+ with open(os.path.join(tdir, str(id)+'.import'), 'wb') as f:
+ for fmt, nfp in import_map.items():
+ f.write(fmt+':'+nfp+'\n')
notification(0.5, id)
except:
import traceback
@@ -66,6 +86,7 @@ class ReadMetadata(Thread):
self.canceled = False
Thread.__init__(self)
self.daemon = True
+ self.failure_details = {}
self.tdir = PersistentTemporaryDirectory('_rm_worker')
@@ -76,33 +97,34 @@ class ReadMetadata(Thread):
ids.add(b[0])
progress = Progress(self.result_queue, self.tdir)
server = Server() if self.spare_server is None else self.spare_server
- for i, task in enumerate(self.tasks):
- job = ParallelJob('read_metadata',
- 'Read metadata (%d of %d)'%(i, len(self.tasks)),
- lambda x,y:x, args=[task, self.tdir])
- jobs.add(job)
- server.add_job(job)
+ try:
+ for i, task in enumerate(self.tasks):
+ job = ParallelJob('read_metadata',
+ 'Read metadata (%d of %d)'%(i, len(self.tasks)),
+ lambda x,y:x, args=[task, self.tdir])
+ jobs.add(job)
+ server.add_job(job)
- while not self.canceled:
- time.sleep(0.2)
- running = False
- for job in jobs:
- while True:
- try:
- id = job.notifications.get_nowait()[-1]
- if id in ids:
- progress(id)
- ids.remove(id)
- except Empty:
- break
- job.update(consume_notifications=False)
- if not job.is_finished:
- running = True
+ while not self.canceled:
+ time.sleep(0.2)
+ running = False
+ for job in jobs:
+ while True:
+ try:
+ id = job.notifications.get_nowait()[-1]
+ if id in ids:
+ progress(id)
+ ids.remove(id)
+ except Empty:
+ break
+ job.update(consume_notifications=False)
+ if not job.is_finished:
+ running = True
- if not running:
- break
-
- server.close()
+ if not running:
+ break
+ finally:
+ server.close()
time.sleep(1)
if self.canceled:
diff --git a/src/calibre/ebooks/oeb/transforms/rasterize.py b/src/calibre/ebooks/oeb/transforms/rasterize.py
index 1549838bfe..a4ebb0bb23 100644
--- a/src/calibre/ebooks/oeb/transforms/rasterize.py
+++ b/src/calibre/ebooks/oeb/transforms/rasterize.py
@@ -187,6 +187,10 @@ class SVGRasterizer(object):
covers = self.oeb.metadata.cover
if not covers:
return
+ if unicode(covers[0]) not in self.oeb.manifest.ids:
+ self.oeb.logger.warn('Cover not in manifest, skipping.')
+ self.oeb.metadata.clear('cover')
+ return
cover = self.oeb.manifest.ids[unicode(covers[0])]
if not cover.media_type == SVG_MIME:
return
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index cca9680207..63d7e5407f 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -103,6 +103,33 @@ def available_width():
def extension(path):
return os.path.splitext(path)[1][1:].lower()
+class CopyButton(QPushButton):
+
+ ACTION_KEYS = [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Space]
+
+ def copied(self):
+ self.emit(SIGNAL('copy()'))
+ self.setDisabled(True)
+ self.setText(_('Copied to clipboard'))
+
+
+ def keyPressEvent(self, ev):
+ if ev.key() in self.ACTION_KEYS:
+ self.copied()
+ else:
+ QPushButton.event(self, ev)
+
+
+ def keyReleaseEvent(self, ev):
+ if ev.key() in self.ACTION_KEYS:
+ pass
+ else:
+ QPushButton.event(self, ev)
+
+ def mouseReleaseEvent(self, ev):
+ ev.accept()
+ self.copied()
+
class MessageBox(QMessageBox):
def __init__(self, type_, title, msg, buttons, parent, det_msg=''):
@@ -111,9 +138,16 @@ class MessageBox(QMessageBox):
self.msg = msg
self.det_msg = det_msg
self.setDetailedText(det_msg)
- self.cb = QPushButton(_('Copy to Clipboard'))
- self.layout().addWidget(self.cb)
- self.connect(self.cb, SIGNAL('clicked()'), self.copy_to_clipboard)
+ # Cannot set keyboard shortcut as the event is not easy to filter
+ self.cb = CopyButton(_('Copy to Clipboard'))
+ self.connect(self.cb, SIGNAL('copy()'), self.copy_to_clipboard)
+ self.addButton(self.cb, QMessageBox.ActionRole)
+ default_button = self.button(self.Ok)
+ if default_button is None:
+ default_button = self.button(self.Yes)
+ if default_button is not None:
+ self.setDefaultButton(default_button)
+
def copy_to_clipboard(self):
QApplication.clipboard().setText('%s: %s\n\n%s' %
diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py
index f976d72fee..c7c3a9e62e 100644
--- a/src/calibre/gui2/add.py
+++ b/src/calibre/gui2/add.py
@@ -1,13 +1,14 @@
'''
UI for adding books to the database and saving books to disk
'''
-import os, shutil
+import os, shutil, time
from Queue import Queue, Empty
+from threading import Thread
from PyQt4.Qt import QThread, SIGNAL, QObject, QTimer, Qt
from calibre.gui2.dialogs.progress import ProgressDialog
-from calibre.gui2 import question_dialog
+from calibre.gui2 import question_dialog, error_dialog
from calibre.ebooks.metadata.opf2 import OPF
from calibre.ebooks.metadata import MetaInformation
from calibre.constants import preferred_encoding
@@ -36,6 +37,96 @@ class RecursiveFind(QThread):
if not self.canceled:
self.emit(SIGNAL('found(PyQt_PyObject)'), self.books)
+class DBAdder(Thread):
+
+ def __init__(self, db, ids, nmap):
+ self.db, self.ids, self.nmap = db, dict(**ids), dict(**nmap)
+ self.end = False
+ self.critical = {}
+ self.number_of_books_added = 0
+ self.duplicates = []
+ self.names, self.path, self.infos = [], [], []
+ Thread.__init__(self)
+ self.daemon = True
+ self.input_queue = Queue()
+ self.output_queue = Queue()
+
+ def run(self):
+ while not self.end:
+ try:
+ id, opf, cover = self.input_queue.get(True, 0.2)
+ except Empty:
+ continue
+ name = self.nmap.pop(id)
+ title = None
+ try:
+ title = self.add(id, opf, cover, name)
+ except:
+ import traceback
+ self.critical[name] = traceback.format_exc()
+ title = name
+ self.output_queue.put(title)
+
+ def process_formats(self, opf, formats):
+ imp = opf[:-4]+'.import'
+ if not os.access(imp, os.R_OK):
+ return formats
+ fmt_map = {}
+ for line in open(imp, 'rb').readlines():
+ if ':' not in line:
+ continue
+ f, _, p = line.partition(':')
+ fmt_map[f] = p.rstrip()
+ fmts = []
+ for fmt in formats:
+ e = os.path.splitext(fmt)[1].replace('.', '').lower()
+ fmts.append(fmt_map.get(e, fmt))
+ if not os.access(fmts[-1], os.R_OK):
+ fmts[-1] = fmt
+ return fmts
+
+ def add(self, id, opf, cover, name):
+ formats = self.ids.pop(id)
+ if opf.endswith('.error'):
+ mi = MetaInformation('', [_('Unknown')])
+ self.critical[name] = open(opf, 'rb').read().decode('utf-8', 'replace')
+ else:
+ try:
+ mi = MetaInformation(OPF(opf))
+ except:
+ import traceback
+ mi = MetaInformation('', [_('Unknown')])
+ self.critical[name] = traceback.format_exc()
+ formats = self.process_formats(opf, formats)
+ if not mi.title:
+ mi.title = os.path.splitext(name)[0]
+ mi.title = mi.title if isinstance(mi.title, unicode) else \
+ mi.title.decode(preferred_encoding, 'replace')
+ if self.db is not None:
+ if cover:
+ cover = open(cover, 'rb').read()
+ id = self.db.create_book_entry(mi, cover=cover, add_duplicates=False)
+ self.number_of_books_added += 1
+ if id is None:
+ self.duplicates.append((mi, cover, formats))
+ else:
+ self.add_formats(id, formats)
+ else:
+ self.names.append(name)
+ self.paths.append(formats[0])
+ self.infos.append({'title':mi.title,
+ 'authors':', '.join(mi.authors),
+ 'cover':None,
+ 'tags':mi.tags if mi.tags else []})
+ return mi.title
+
+ def add_formats(self, id, formats):
+ for path in formats:
+ fmt = os.path.splitext(path)[-1].replace('.', '').upper()
+ with open(path, 'rb') as f:
+ self.db.add_format(id, fmt, f, index_is_id=True,
+ notify=False)
+
class Adder(QObject):
@@ -44,15 +135,12 @@ class Adder(QObject):
self.pd = ProgressDialog(_('Adding...'), parent=parent)
self.spare_server = spare_server
self.db = db
- self.critical = {}
self.pd.setModal(True)
self.pd.show()
self._parent = parent
- self.number_of_books_added = 0
self.rfind = self.worker = self.timer = None
self.callback = callback
self.callback_called = False
- self.infos, self.paths, self.names = [], [], []
self.connect(self.pd, SIGNAL('canceled()'), self.canceled)
def add_recursive(self, root, single=True):
@@ -87,32 +175,33 @@ class Adder(QObject):
self.pd.set_max(len(self.ids))
self.pd.value = 0
self.timer = QTimer(self)
+ self.db_adder = DBAdder(self.db, self.ids, self.nmap)
+ self.db_adder.start()
self.connect(self.timer, SIGNAL('timeout()'), self.update)
+ self.last_added_at = time.time()
+ self.entry_count = len(self.ids)
self.timer.start(200)
- def add_formats(self, id, formats):
- for path in formats:
- fmt = os.path.splitext(path)[-1].replace('.', '').upper()
- self.db.add_format_with_hooks(id, fmt, path, index_is_id=True,
- notify=False)
-
def canceled(self):
if self.rfind is not None:
- self.rfind.cenceled = True
+ self.rfind.canceled = True
if self.timer is not None:
self.timer.stop()
if self.worker is not None:
self.worker.canceled = True
+ if hasattr(self, 'db_adder'):
+ self.db_adder.end = True
self.pd.hide()
if not self.callback_called:
self.callback(self.paths, self.names, self.infos)
self.callback_called = True
def update(self):
- if not self.ids:
+ if self.entry_count <= 0:
self.timer.stop()
self.process_duplicates()
self.pd.hide()
+ self.db_adder.end = True
if not self.callback_called:
self.callback(self.paths, self.names, self.infos)
self.callback_called = True
@@ -120,61 +209,53 @@ class Adder(QObject):
try:
id, opf, cover = self.rq.get_nowait()
+ self.db_adder.input_queue.put((id, opf, cover))
+ self.last_added_at = time.time()
except Empty:
- return
- self.pd.value += 1
- formats = self.ids.pop(id)
- name = self.nmap.pop(id)
- if opf.endswith('.error'):
- mi = MetaInformation('', [_('Unknown')])
- self.critical[name] = open(opf, 'rb').read().decode('utf-8', 'replace')
- else:
- try:
- mi = MetaInformation(OPF(opf))
- except:
- import traceback
- mi = MetaInformation('', [_('Unknown')])
- self.critical[name] = traceback.format_exc()
- if not mi.title:
- mi.title = os.path.splitext(name)[0]
- mi.title = mi.title if isinstance(mi.title, unicode) else \
- mi.title.decode(preferred_encoding, 'replace')
+ pass
- if self.db is not None:
- if cover:
- cover = open(cover, 'rb').read()
- id = self.db.create_book_entry(mi, cover=cover, add_duplicates=False)
- self.number_of_books_added += 1
- if id is None:
- self.duplicates.append((mi, cover, formats))
- else:
- self.add_formats(id, formats)
- else:
- self.names.append(name)
- self.paths.append(formats[0])
- self.infos.append({'title':mi.title,
- 'authors':', '.join(mi.authors),
- 'cover':None,
- 'tags':mi.tags if mi.tags else []})
+ try:
+ title = self.db_adder.output_queue.get_nowait()
+ self.pd.value += 1
+ self.pd.set_msg(_('Added')+' '+title)
+ self.last_added_at = time.time()
+ self.entry_count -= 1
+ except Empty:
+ pass
- self.pd.set_msg(_('Added')+' '+mi.title)
+ if (time.time() - self.last_added_at) > 300:
+ self.timer.stop()
+ self.pd.hide()
+ self.db_adder.end = True
+ if not self.callback_called:
+ self.callback([], [], [])
+ self.callback_called = True
+ error_dialog(self._parent, _('Adding failed'),
+ _('The add books process seems to have hung.'
+ ' Try restarting calibre and adding the '
+ 'books in smaller increments, until you '
+ 'find the problem book.'), show=True)
def process_duplicates(self):
- if not self.duplicates:
+ duplicates = self.db_adder.duplicates
+ if not duplicates:
return
- files = [x[0].title for x in self.duplicates]
+ self.pd.hide()
+ files = [x[0].title for x in duplicates]
if question_dialog(self._parent, _('Duplicates found!'),
_('Books with the same title as the following already '
'exist in the database. Add them anyway?'),
'\n'.join(files)):
- for mi, cover, formats in self.duplicates:
+ for mi, cover, formats in duplicates:
id = self.db.create_book_entry(mi, cover=cover,
add_duplicates=True)
- self.add_formats(id, formats)
- self.number_of_books_added += 1
+ self.db_adder.add_formats(id, formats)
+ self.db_adder.number_of_books_added += 1
def cleanup(self):
+ if hasattr(self, 'pd'):
+ self.pd.hide()
if hasattr(self, 'worker') and hasattr(self.worker, 'tdir') and \
self.worker.tdir is not None:
if os.path.exists(self.worker.tdir):
@@ -183,6 +264,35 @@ class Adder(QObject):
except:
pass
+ @property
+ def number_of_books_added(self):
+ return getattr(getattr(self, 'db_adder', None), 'number_of_books_added',
+ 0)
+
+ @property
+ def critical(self):
+ return getattr(getattr(self, 'db_adder', None), 'critical',
+ {})
+ @property
+ def paths(self):
+ return getattr(getattr(self, 'db_adder', None), 'paths',
+ [])
+
+ @property
+ def names(self):
+ return getattr(getattr(self, 'db_adder', None), 'names',
+ [])
+
+ @property
+ def infos(self):
+ return getattr(getattr(self, 'db_adder', None), 'infos',
+ [])
+
+
+###############################################################################
+############################## END ADDER ######################################
+###############################################################################
+
class Saver(QObject):
def __init__(self, parent, db, callback, rows, path,
diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py
index 58eeb01a99..7ffaf90b2e 100644
--- a/src/calibre/gui2/dialogs/config.py
+++ b/src/calibre/gui2/dialogs/config.py
@@ -5,14 +5,15 @@ import os, re, time, textwrap
from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
QStringListModel, QAbstractItemModel, QFont, \
- SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \
+ SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
QModelIndex, QInputDialog, QAbstractTableModel, \
QDialogButtonBox, QTabWidget, QBrush, QLineEdit
from calibre.constants import islinux, iswindows
from calibre.gui2.dialogs.config_ui import Ui_Dialog
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
- ALL_COLUMNS, NONE, info_dialog, choose_files
+ ALL_COLUMNS, NONE, info_dialog, choose_files, \
+ warning_dialog
from calibre.utils.config import prefs
from calibre.gui2.widgets import FilenamePattern
from calibre.gui2.library import BooksModel
@@ -736,19 +737,46 @@ class ConfigDialog(QDialog, Ui_Dialog):
config['frequently_used_directories'] = self.directories
QDialog.accept(self)
+class VacThread(QThread):
+
+ def __init__(self, parent, db):
+ QThread.__init__(self, parent)
+ self.db = db
+ self._parent = parent
+
+ def run(self):
+ bad = self.db.check_integrity()
+ self.emit(SIGNAL('check_done(PyQt_PyObject)'), bad)
+
class Vacuum(QMessageBox):
def __init__(self, parent, db):
self.db = db
- QMessageBox.__init__(self, QMessageBox.Information, _('Compacting...'),
- _('Compacting database. This may take a while.'),
+ QMessageBox.__init__(self, QMessageBox.Information, _('Checking...'),
+ _('Checking database integrity. This may take a while.'),
QMessageBox.NoButton, parent)
- QTimer.singleShot(200, self.vacuum)
+ self.vthread = VacThread(self, db)
+ self.connect(self.vthread, SIGNAL('check_done(PyQt_PyObject)'),
+ self.check_done,
+ Qt.QueuedConnection)
+ self.vthread.start()
- def vacuum(self):
- self.db.vacuum()
+
+ def check_done(self, bad):
+ if bad:
+ titles = [self.db.title(x, index_is_id=True) for x in bad]
+ det_msg = '\n'.join(titles)
+ warning_dialog(self, _('Some inconsistencies found'),
+ _('The following books had formats listed in the '
+ 'database that are not actually available. '
+ 'The entries for the formats have been removed. '
+ 'You should check them manually. This can '
+ 'happen if you manipulate the files in the '
+ 'library folder directly.'), det_msg=det_msg, show=True)
self.accept()
+
+
if __name__ == '__main__':
from calibre.library.database2 import LibraryDatabase2
from PyQt4.Qt import QApplication
diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui
index f76f25f374..be213ba7bc 100644
--- a/src/calibre/gui2/dialogs/config.ui
+++ b/src/calibre/gui2/dialogs/config.ui
@@ -710,7 +710,7 @@
Free unused diskspace from the database
- &Compact database
+ &Check database integrity
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 95c1731fcd..194933bb3a 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -1787,4 +1787,38 @@ books_series_link feeds
return duplicates
+ def check_integrity(self):
+ bad = {}
+ for id in self.data.universal_set():
+ formats = self.data.get(id, FIELD_MAP['formats'], row_is_id=True)
+ if not formats:
+ formats = []
+ else:
+ formats = [x.lower() for x in formats.split(',')]
+ actual_formats = self.formats(id, index_is_id=True)
+ if not actual_formats:
+ actual_formats = []
+ else:
+ actual_formats = [x.lower() for x in actual_formats.split(',')]
+
+ mismatch = False
+ for fmt in formats:
+ if fmt in actual_formats:
+ continue
+ mismatch = True
+ if id not in bad:
+ bad[id] = []
+ bad[id].append(fmt)
+
+ for id in bad:
+ for fmt in bad[id]:
+ self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, fmt.upper()))
+ self.conn.commit()
+ self.refresh_ids(list(bad.keys()))
+
+ self.vacuum()
+
+ return bad
+
+
diff --git a/src/calibre/utils/ipc/job.py b/src/calibre/utils/ipc/job.py
index 8ddbdf998f..ec33c54231 100644
--- a/src/calibre/utils/ipc/job.py
+++ b/src/calibre/utils/ipc/job.py
@@ -11,6 +11,9 @@ _count = 0
import time, cStringIO
from Queue import Queue, Empty
+from calibre import prints
+from calibre.constants import DEBUG
+
class BaseJob(object):
WAITING = 0
@@ -47,6 +50,9 @@ class BaseJob(object):
self._status_text = _('Stopped')
else:
self._status_text = _('Error') if self.failed else _('Finished')
+ if DEBUG:
+ prints('Job:', self.id, self.description, 'finished')
+ prints('\t'.join(self.details.splitlines(True)))
if not self._done_called:
self._done_called = True
try:
diff --git a/src/calibre/utils/ipc/server.py b/src/calibre/utils/ipc/server.py
index 55da9c60ca..2613702084 100644
--- a/src/calibre/utils/ipc/server.py
+++ b/src/calibre/utils/ipc/server.py
@@ -119,13 +119,33 @@ class Server(Thread):
'CALIBRE_WORKER_KEY' : hexlify(self.auth_key),
'CALIBRE_WORKER_RESULT' : hexlify(rfile),
}
+ for i in range(2):
+ # Try launch twice as occasionally on OS X
+ # Listener.accept fails with EINTR
+ cw = self.do_launch(env, gui, redirect_output, rfile)
+ if isinstance(cw, ConnectedWorker):
+ break
+ if isinstance(cw, basestring):
+ raise Exception('Failed to launch worker process:\n'+cw)
+ return cw
+
+ def do_launch(self, env, gui, redirect_output, rfile):
w = Worker(env, gui=gui)
+
if redirect_output is None:
redirect_output = not gui
- w(redirect_output=redirect_output)
- conn = self.listener.accept()
- if conn is None:
- raise Exception('Failed to launch worker process')
+ try:
+ w(redirect_output=redirect_output)
+ conn = self.listener.accept()
+ if conn is None:
+ raise Exception('Failed to launch worker process')
+ except BaseException:
+ try:
+ w.kill()
+ except:
+ pass
+ import traceback
+ return traceback.format_exc()
return ConnectedWorker(w, conn, rfile)
def add_job(self, job):