Fix regression in 0.8.33 that caused calibre to crash when starting the Content Server, if the port the content server is trying to listen on is blocked/busy. Fixes #910512 (calibre crashs after start without message)

This commit is contained in:
Kovid Goyal 2012-01-02 10:59:33 +05:30
parent 87c2406cd8
commit b117148241
4 changed files with 114 additions and 87 deletions

View File

@ -63,16 +63,21 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def start_server(self): def start_server(self):
ConfigWidgetBase.commit(self) ConfigWidgetBase.commit(self)
self.gui.start_content_server(check_started=False) self.setCursor(Qt.BusyCursor)
while not self.gui.content_server.is_running and self.gui.content_server.exception is None: try:
time.sleep(1) self.gui.start_content_server(check_started=False)
if self.gui.content_server.exception is not None: while (not self.gui.content_server.is_running and
error_dialog(self, _('Failed to start content server'), self.gui.content_server.exception is None):
as_unicode(self.gui.content_server.exception)).exec_() time.sleep(0.1)
return if self.gui.content_server.exception is not None:
self.start_button.setEnabled(False) error_dialog(self, _('Failed to start content server'),
self.test_button.setEnabled(True) as_unicode(self.gui.content_server.exception)).exec_()
self.stop_button.setEnabled(True) return
self.start_button.setEnabled(False)
self.test_button.setEnabled(True)
self.stop_button.setEnabled(True)
finally:
self.unsetCursor()
def stop_server(self): def stop_server(self):
self.gui.content_server.threaded_exit() self.gui.content_server.threaded_exit()

View File

@ -368,9 +368,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.library_view.model().db, server_config().parse()) self.library_view.model().db, server_config().parse())
self.content_server.state_callback = Dispatcher( self.content_server.state_callback = Dispatcher(
self.iactions['Connect Share'].content_server_state_changed) self.iactions['Connect Share'].content_server_state_changed)
self.content_server.state_callback(True)
if check_started: if check_started:
QTimer.singleShot(10000, self.test_server) self.content_server.start_failure_callback = \
Dispatcher(self.content_server_start_failed)
def content_server_start_failed(self, msg):
error_dialog(self, _('Failed to start Content Server'),
_('Could not start the content server. Error:\n\n%s')%msg,
show=True)
def resizeEvent(self, ev): def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev) MainWindow.resizeEvent(self, ev)

View File

@ -26,7 +26,7 @@ from calibre.library.server.cache import Cache
from calibre.library.server.browse import BrowseServer from calibre.library.server.browse import BrowseServer
from calibre.library.server.ajax import AjaxServer from calibre.library.server.ajax import AjaxServer
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre import prints from calibre import prints, as_unicode
class DispatchController(object): # {{{ class DispatchController(object): # {{{
@ -112,6 +112,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
self.opts = opts self.opts = opts
self.embedded = embedded self.embedded = embedded
self.state_callback = None self.state_callback = None
self.start_failure_callback = None
try: try:
self.max_cover_width, self.max_cover_height = \ self.max_cover_width, self.max_cover_height = \
map(int, self.opts.max_cover.split('x')) map(int, self.opts.max_cover.split('x'))
@ -225,41 +226,57 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
h.setFormatter(cherrypy._cplogging.logfmt) h.setFormatter(cherrypy._cplogging.logfmt)
log.access_log.addHandler(h) log.access_log.addHandler(h)
def start_cherrypy(self):
try:
cherrypy.engine.start()
except:
ip = get_external_ip()
if not ip or ip.startswith('127.'):
raise
cherrypy.log('Trying to bind to single interface: '+ip)
# Change the host we listen on
cherrypy.config.update({'server.socket_host' : ip})
# This ensures that the change is actually applied
cherrypy.server.socket_host = ip
cherrypy.server.httpserver = cherrypy.server.instance = None
cherrypy.engine.start()
def start(self): def start(self):
self.is_running = False self.is_running = False
self.exception = None
cherrypy.tree.mount(root=None, config=self.config) cherrypy.tree.mount(root=None, config=self.config)
try: try:
try: self.start_cherrypy()
cherrypy.engine.start()
except:
ip = get_external_ip()
if not ip or ip.startswith('127.'):
raise
cherrypy.log('Trying to bind to single interface: '+ip)
# Change the host we listen on
cherrypy.config.update({'server.socket_host' : ip})
# This ensures that the change is actually applied
cherrypy.server.socket_host = ip
cherrypy.server.httpserver = cherrypy.server.instance = None
cherrypy.engine.start()
self.is_running = True
#if hasattr(cherrypy.engine, 'signal_handler'):
# cherrypy.engine.signal_handler.subscribe()
cherrypy.engine.block()
except Exception as e: except Exception as e:
self.exception = e self.exception = e
import traceback import traceback
traceback.print_exc() traceback.print_exc()
if callable(self.start_failure_callback):
try:
self.start_failure_callback(as_unicode(e))
except:
pass
return
try:
self.is_running = True
self.notify_listener()
cherrypy.engine.block()
except Exception as e:
import traceback
traceback.print_exc()
self.exception = e
finally: finally:
self.is_running = False self.is_running = False
try: self.notify_listener()
if callable(self.state_callback):
self.state_callback(self.is_running) def notify_listener(self):
except: try:
pass if callable(self.state_callback):
self.state_callback(self.is_running)
except:
pass
def exit(self): def exit(self):
try: try:
@ -267,11 +284,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
finally: finally:
cherrypy.server.httpserver = None cherrypy.server.httpserver = None
self.is_running = False self.is_running = False
try: self.notify_listener()
if callable(self.state_callback):
self.state_callback(self.is_running)
except:
pass
def threaded_exit(self): def threaded_exit(self):
from threading import Thread from threading import Thread

View File

@ -81,21 +81,21 @@ _startup_cwd = os.getcwd()
class ChannelFailures(Exception): class ChannelFailures(Exception):
"""Exception raised when errors occur in a listener during Bus.publish().""" """Exception raised when errors occur in a listener during Bus.publish()."""
delimiter = '\n' delimiter = '\n'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't use 'super' here; Exceptions are old-style in Py2.4 # Don't use 'super' here; Exceptions are old-style in Py2.4
# See http://www.cherrypy.org/ticket/959 # See http://www.cherrypy.org/ticket/959
Exception.__init__(self, *args, **kwargs) Exception.__init__(self, *args, **kwargs)
self._exceptions = list() self._exceptions = list()
def handle_exception(self): def handle_exception(self):
"""Append the current exception to self.""" """Append the current exception to self."""
self._exceptions.append(sys.exc_info()[1]) self._exceptions.append(sys.exc_info()[1])
def get_instances(self): def get_instances(self):
"""Return a list of seen exception instances.""" """Return a list of seen exception instances."""
return self._exceptions[:] return self._exceptions[:]
def __str__(self): def __str__(self):
exception_strings = map(repr, self.get_instances()) exception_strings = map(repr, self.get_instances())
return self.delimiter.join(exception_strings) return self.delimiter.join(exception_strings)
@ -112,7 +112,7 @@ class _StateEnum(object):
name = None name = None
def __repr__(self): def __repr__(self):
return "states.%s" % self.name return "states.%s" % self.name
def __setattr__(self, key, value): def __setattr__(self, key, value):
if isinstance(value, self.State): if isinstance(value, self.State):
value.name = key value.name = key
@ -138,19 +138,19 @@ else:
class Bus(object): class Bus(object):
"""Process state-machine and messenger for HTTP site deployment. """Process state-machine and messenger for HTTP site deployment.
All listeners for a given channel are guaranteed to be called even All listeners for a given channel are guaranteed to be called even
if others at the same channel fail. Each failure is logged, but if others at the same channel fail. Each failure is logged, but
execution proceeds on to the next listener. The only way to stop all execution proceeds on to the next listener. The only way to stop all
processing from inside a listener is to raise SystemExit and stop the processing from inside a listener is to raise SystemExit and stop the
whole server. whole server.
""" """
states = states states = states
state = states.STOPPED state = states.STOPPED
execv = False execv = False
max_cloexec_files = max_files max_cloexec_files = max_files
def __init__(self): def __init__(self):
self.execv = False self.execv = False
self.state = states.STOPPED self.state = states.STOPPED
@ -158,32 +158,32 @@ class Bus(object):
[(channel, set()) for channel [(channel, set()) for channel
in ('start', 'stop', 'exit', 'graceful', 'log', 'main')]) in ('start', 'stop', 'exit', 'graceful', 'log', 'main')])
self._priorities = {} self._priorities = {}
def subscribe(self, channel, callback, priority=None): def subscribe(self, channel, callback, priority=None):
"""Add the given callback at the given channel (if not present).""" """Add the given callback at the given channel (if not present)."""
if channel not in self.listeners: if channel not in self.listeners:
self.listeners[channel] = set() self.listeners[channel] = set()
self.listeners[channel].add(callback) self.listeners[channel].add(callback)
if priority is None: if priority is None:
priority = getattr(callback, 'priority', 50) priority = getattr(callback, 'priority', 50)
self._priorities[(channel, callback)] = priority self._priorities[(channel, callback)] = priority
def unsubscribe(self, channel, callback): def unsubscribe(self, channel, callback):
"""Discard the given callback (if present).""" """Discard the given callback (if present)."""
listeners = self.listeners.get(channel) listeners = self.listeners.get(channel)
if listeners and callback in listeners: if listeners and callback in listeners:
listeners.discard(callback) listeners.discard(callback)
del self._priorities[(channel, callback)] del self._priorities[(channel, callback)]
def publish(self, channel, *args, **kwargs): def publish(self, channel, *args, **kwargs):
"""Return output of all subscribers for the given channel.""" """Return output of all subscribers for the given channel."""
if channel not in self.listeners: if channel not in self.listeners:
return [] return []
exc = ChannelFailures() exc = ChannelFailures()
output = [] output = []
items = [(self._priorities[(channel, listener)], listener) items = [(self._priorities[(channel, listener)], listener)
for listener in self.listeners[channel]] for listener in self.listeners[channel]]
try: try:
@ -214,7 +214,7 @@ class Bus(object):
if exc: if exc:
raise exc raise exc
return output return output
def _clean_exit(self): def _clean_exit(self):
"""An atexit handler which asserts the Bus is not running.""" """An atexit handler which asserts the Bus is not running."""
if self.state != states.EXITING: if self.state != states.EXITING:
@ -224,11 +224,11 @@ class Bus(object):
"bus.block() after start(), or call bus.exit() before the " "bus.block() after start(), or call bus.exit() before the "
"main thread exits." % self.state, RuntimeWarning) "main thread exits." % self.state, RuntimeWarning)
self.exit() self.exit()
def start(self): def start(self):
"""Start all services.""" """Start all services."""
atexit.register(self._clean_exit) atexit.register(self._clean_exit)
self.state = states.STARTING self.state = states.STARTING
self.log('Bus STARTING') self.log('Bus STARTING')
try: try:
@ -248,13 +248,13 @@ class Bus(object):
pass pass
# Re-raise the original error # Re-raise the original error
raise e_info raise e_info
def exit(self): def exit(self):
"""Stop all services and prepare to exit the process.""" """Stop all services and prepare to exit the process."""
exitstate = self.state exitstate = self.state
try: try:
self.stop() self.stop()
self.state = states.EXITING self.state = states.EXITING
self.log('Bus EXITING') self.log('Bus EXITING')
self.publish('exit') self.publish('exit')
@ -267,31 +267,35 @@ class Bus(object):
# can't just let exceptions propagate out unhandled. # can't just let exceptions propagate out unhandled.
# Assume it's been logged and just die. # Assume it's been logged and just die.
os._exit(70) # EX_SOFTWARE os._exit(70) # EX_SOFTWARE
if exitstate == states.STARTING: # Changed by Kovid, we cannot have all of calibre being quit
# Also we want to catch the port blocked/busy error and try listening only on
# the external ip
# See https://bitbucket.org/cherrypy/cherrypy/issue/1017/exit-behavior-is-not-good-when-running-in
if False and exitstate == states.STARTING:
# exit() was called before start() finished, possibly due to # exit() was called before start() finished, possibly due to
# Ctrl-C because a start listener got stuck. In this case, # Ctrl-C because a start listener got stuck. In this case,
# we could get stuck in a loop where Ctrl-C never exits the # we could get stuck in a loop where Ctrl-C never exits the
# process, so we just call os.exit here. # process, so we just call os.exit here.
os._exit(70) # EX_SOFTWARE os._exit(70) # EX_SOFTWARE
def restart(self): def restart(self):
"""Restart the process (may close connections). """Restart the process (may close connections).
This method does not restart the process from the calling thread; This method does not restart the process from the calling thread;
instead, it stops the bus and asks the main thread to call execv. instead, it stops the bus and asks the main thread to call execv.
""" """
self.execv = True self.execv = True
self.exit() self.exit()
def graceful(self): def graceful(self):
"""Advise all services to reload.""" """Advise all services to reload."""
self.log('Bus graceful') self.log('Bus graceful')
self.publish('graceful') self.publish('graceful')
def block(self, interval=0.1): def block(self, interval=0.1):
"""Wait for the EXITING state, KeyboardInterrupt or SystemExit. """Wait for the EXITING state, KeyboardInterrupt or SystemExit.
This function is intended to be called only by the main thread. This function is intended to be called only by the main thread.
After waiting for the EXITING state, it also waits for all threads After waiting for the EXITING state, it also waits for all threads
to terminate, and then calls os.execv if self.execv is True. This to terminate, and then calls os.execv if self.execv is True. This
@ -309,7 +313,7 @@ class Bus(object):
self.log('SystemExit raised: shutting down bus') self.log('SystemExit raised: shutting down bus')
self.exit() self.exit()
raise raise
# Waiting for ALL child threads to finish is necessary on OS X. # Waiting for ALL child threads to finish is necessary on OS X.
# See http://www.cherrypy.org/ticket/581. # See http://www.cherrypy.org/ticket/581.
# It's also good to let them all shut down before allowing # It's also good to let them all shut down before allowing
@ -327,22 +331,22 @@ class Bus(object):
if not d: if not d:
self.log("Waiting for thread %s." % t.getName()) self.log("Waiting for thread %s." % t.getName())
t.join() t.join()
if self.execv: if self.execv:
self._do_execv() self._do_execv()
def wait(self, state, interval=0.1, channel=None): def wait(self, state, interval=0.1, channel=None):
"""Poll for the given state(s) at intervals; publish to channel.""" """Poll for the given state(s) at intervals; publish to channel."""
if isinstance(state, (tuple, list)): if isinstance(state, (tuple, list)):
states = state states = state
else: else:
states = [state] states = [state]
def _wait(): def _wait():
while self.state not in states: while self.state not in states:
time.sleep(interval) time.sleep(interval)
self.publish(channel) self.publish(channel)
# From http://psyco.sourceforge.net/psycoguide/bugs.html: # From http://psyco.sourceforge.net/psycoguide/bugs.html:
# "The compiled machine code does not include the regular polling # "The compiled machine code does not include the regular polling
# done by Python, meaning that a KeyboardInterrupt will not be # done by Python, meaning that a KeyboardInterrupt will not be
@ -353,18 +357,18 @@ class Bus(object):
sys.modules['psyco'].cannotcompile(_wait) sys.modules['psyco'].cannotcompile(_wait)
except (KeyError, AttributeError): except (KeyError, AttributeError):
pass pass
_wait() _wait()
def _do_execv(self): def _do_execv(self):
"""Re-execute the current process. """Re-execute the current process.
This must be called from the main thread, because certain platforms This must be called from the main thread, because certain platforms
(OS X) don't allow execv to be called in a child thread very well. (OS X) don't allow execv to be called in a child thread very well.
""" """
args = sys.argv[:] args = sys.argv[:]
self.log('Re-spawning %s' % ' '.join(args)) self.log('Re-spawning %s' % ' '.join(args))
if sys.platform[:4] == 'java': if sys.platform[:4] == 'java':
from _systemrestart import SystemRestart from _systemrestart import SystemRestart
raise SystemRestart raise SystemRestart
@ -377,16 +381,16 @@ class Bus(object):
if self.max_cloexec_files: if self.max_cloexec_files:
self._set_cloexec() self._set_cloexec()
os.execv(sys.executable, args) os.execv(sys.executable, args)
def _set_cloexec(self): def _set_cloexec(self):
"""Set the CLOEXEC flag on all open files (except stdin/out/err). """Set the CLOEXEC flag on all open files (except stdin/out/err).
If self.max_cloexec_files is an integer (the default), then on If self.max_cloexec_files is an integer (the default), then on
platforms which support it, it represents the max open files setting platforms which support it, it represents the max open files setting
for the operating system. This function will be called just before for the operating system. This function will be called just before
the process is restarted via os.execv() to prevent open files the process is restarted via os.execv() to prevent open files
from persisting into the new process. from persisting into the new process.
Set self.max_cloexec_files to 0 to disable this behavior. Set self.max_cloexec_files to 0 to disable this behavior.
""" """
for fd in range(3, self.max_cloexec_files): # skip stdin/out/err for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
@ -395,7 +399,7 @@ class Bus(object):
except IOError: except IOError:
continue continue
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
def stop(self): def stop(self):
"""Stop all services.""" """Stop all services."""
self.state = states.STOPPING self.state = states.STOPPING
@ -403,7 +407,7 @@ class Bus(object):
self.publish('stop') self.publish('stop')
self.state = states.STOPPED self.state = states.STOPPED
self.log('Bus STOPPED') self.log('Bus STOPPED')
def start_with_callback(self, func, args=None, kwargs=None): def start_with_callback(self, func, args=None, kwargs=None):
"""Start 'func' in a new thread T, then start self (and return T).""" """Start 'func' in a new thread T, then start self (and return T)."""
if args is None: if args is None:
@ -411,18 +415,18 @@ class Bus(object):
if kwargs is None: if kwargs is None:
kwargs = {} kwargs = {}
args = (func,) + args args = (func,) + args
def _callback(func, *a, **kw): def _callback(func, *a, **kw):
self.wait(states.STARTED) self.wait(states.STARTED)
func(*a, **kw) func(*a, **kw)
t = threading.Thread(target=_callback, args=args, kwargs=kwargs) t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
t.setName('Bus Callback ' + t.getName()) t.setName('Bus Callback ' + t.getName())
t.start() t.start()
self.start() self.start()
return t return t
def log(self, msg="", level=20, traceback=False): def log(self, msg="", level=20, traceback=False):
"""Log the given message. Append the last traceback if requested.""" """Log the given message. Append the last traceback if requested."""
if traceback: if traceback: