diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py index f4a00c0932..16f2eb7316 100644 --- a/src/calibre/gui2/preferences/server.py +++ b/src/calibre/gui2/preferences/server.py @@ -63,16 +63,21 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): def start_server(self): ConfigWidgetBase.commit(self) - self.gui.start_content_server(check_started=False) - while not self.gui.content_server.is_running and self.gui.content_server.exception is None: - time.sleep(1) - if self.gui.content_server.exception is not None: - error_dialog(self, _('Failed to start content server'), - as_unicode(self.gui.content_server.exception)).exec_() - return - self.start_button.setEnabled(False) - self.test_button.setEnabled(True) - self.stop_button.setEnabled(True) + self.setCursor(Qt.BusyCursor) + try: + self.gui.start_content_server(check_started=False) + while (not self.gui.content_server.is_running and + self.gui.content_server.exception is None): + time.sleep(0.1) + if self.gui.content_server.exception is not None: + error_dialog(self, _('Failed to start content server'), + as_unicode(self.gui.content_server.exception)).exec_() + return + self.start_button.setEnabled(False) + self.test_button.setEnabled(True) + self.stop_button.setEnabled(True) + finally: + self.unsetCursor() def stop_server(self): self.gui.content_server.threaded_exit() diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 39bf80da07..800eeacdc8 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -368,9 +368,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.library_view.model().db, server_config().parse()) self.content_server.state_callback = Dispatcher( self.iactions['Connect Share'].content_server_state_changed) - self.content_server.state_callback(True) 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): MainWindow.resizeEvent(self, ev) diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index fc18e25ff9..b5cf2e5ef3 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -26,7 +26,7 @@ from calibre.library.server.cache import Cache from calibre.library.server.browse import BrowseServer from calibre.library.server.ajax import AjaxServer from calibre.utils.search_query_parser import saved_searches -from calibre import prints +from calibre import prints, as_unicode class DispatchController(object): # {{{ @@ -112,6 +112,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache, self.opts = opts self.embedded = embedded self.state_callback = None + self.start_failure_callback = None try: self.max_cover_width, self.max_cover_height = \ map(int, self.opts.max_cover.split('x')) @@ -225,41 +226,57 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache, h.setFormatter(cherrypy._cplogging.logfmt) 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): self.is_running = False + self.exception = None cherrypy.tree.mount(root=None, config=self.config) try: - 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() - - self.is_running = True - #if hasattr(cherrypy.engine, 'signal_handler'): - # cherrypy.engine.signal_handler.subscribe() - - cherrypy.engine.block() + self.start_cherrypy() except Exception as e: self.exception = e import traceback 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: self.is_running = False - try: - if callable(self.state_callback): - self.state_callback(self.is_running) - except: - pass + self.notify_listener() + + def notify_listener(self): + try: + if callable(self.state_callback): + self.state_callback(self.is_running) + except: + pass def exit(self): try: @@ -267,11 +284,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache, finally: cherrypy.server.httpserver = None self.is_running = False - try: - if callable(self.state_callback): - self.state_callback(self.is_running) - except: - pass + self.notify_listener() def threaded_exit(self): from threading import Thread diff --git a/src/cherrypy/process/wspbus.py b/src/cherrypy/process/wspbus.py index 6ef768dcbb..0eacf03d20 100644 --- a/src/cherrypy/process/wspbus.py +++ b/src/cherrypy/process/wspbus.py @@ -81,21 +81,21 @@ _startup_cwd = os.getcwd() class ChannelFailures(Exception): """Exception raised when errors occur in a listener during Bus.publish().""" delimiter = '\n' - + def __init__(self, *args, **kwargs): # Don't use 'super' here; Exceptions are old-style in Py2.4 # See http://www.cherrypy.org/ticket/959 Exception.__init__(self, *args, **kwargs) self._exceptions = list() - + def handle_exception(self): """Append the current exception to self.""" self._exceptions.append(sys.exc_info()[1]) - + def get_instances(self): """Return a list of seen exception instances.""" return self._exceptions[:] - + def __str__(self): exception_strings = map(repr, self.get_instances()) return self.delimiter.join(exception_strings) @@ -112,7 +112,7 @@ class _StateEnum(object): name = None def __repr__(self): return "states.%s" % self.name - + def __setattr__(self, key, value): if isinstance(value, self.State): value.name = key @@ -138,19 +138,19 @@ else: class Bus(object): """Process state-machine and messenger for HTTP site deployment. - + All listeners for a given channel are guaranteed to be called even 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 processing from inside a listener is to raise SystemExit and stop the whole server. """ - + states = states state = states.STOPPED execv = False max_cloexec_files = max_files - + def __init__(self): self.execv = False self.state = states.STOPPED @@ -158,32 +158,32 @@ class Bus(object): [(channel, set()) for channel in ('start', 'stop', 'exit', 'graceful', 'log', 'main')]) self._priorities = {} - + def subscribe(self, channel, callback, priority=None): """Add the given callback at the given channel (if not present).""" if channel not in self.listeners: self.listeners[channel] = set() self.listeners[channel].add(callback) - + if priority is None: priority = getattr(callback, 'priority', 50) self._priorities[(channel, callback)] = priority - + def unsubscribe(self, channel, callback): """Discard the given callback (if present).""" listeners = self.listeners.get(channel) if listeners and callback in listeners: listeners.discard(callback) del self._priorities[(channel, callback)] - + def publish(self, channel, *args, **kwargs): """Return output of all subscribers for the given channel.""" if channel not in self.listeners: return [] - + exc = ChannelFailures() output = [] - + items = [(self._priorities[(channel, listener)], listener) for listener in self.listeners[channel]] try: @@ -214,7 +214,7 @@ class Bus(object): if exc: raise exc return output - + def _clean_exit(self): """An atexit handler which asserts the Bus is not running.""" if self.state != states.EXITING: @@ -224,11 +224,11 @@ class Bus(object): "bus.block() after start(), or call bus.exit() before the " "main thread exits." % self.state, RuntimeWarning) self.exit() - + def start(self): """Start all services.""" atexit.register(self._clean_exit) - + self.state = states.STARTING self.log('Bus STARTING') try: @@ -248,13 +248,13 @@ class Bus(object): pass # Re-raise the original error raise e_info - + def exit(self): """Stop all services and prepare to exit the process.""" exitstate = self.state try: self.stop() - + self.state = states.EXITING self.log('Bus EXITING') self.publish('exit') @@ -267,31 +267,35 @@ class Bus(object): # can't just let exceptions propagate out unhandled. # Assume it's been logged and just die. 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 # Ctrl-C because a start listener got stuck. In this case, # we could get stuck in a loop where Ctrl-C never exits the # process, so we just call os.exit here. os._exit(70) # EX_SOFTWARE - + def restart(self): """Restart the process (may close connections). - + This method does not restart the process from the calling thread; instead, it stops the bus and asks the main thread to call execv. """ self.execv = True self.exit() - + def graceful(self): """Advise all services to reload.""" self.log('Bus graceful') self.publish('graceful') - + def block(self, interval=0.1): """Wait for the EXITING state, KeyboardInterrupt or SystemExit. - + This function is intended to be called only by the main thread. 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 @@ -309,7 +313,7 @@ class Bus(object): self.log('SystemExit raised: shutting down bus') self.exit() raise - + # Waiting for ALL child threads to finish is necessary on OS X. # See http://www.cherrypy.org/ticket/581. # It's also good to let them all shut down before allowing @@ -327,22 +331,22 @@ class Bus(object): if not d: self.log("Waiting for thread %s." % t.getName()) t.join() - + if self.execv: self._do_execv() - + def wait(self, state, interval=0.1, channel=None): """Poll for the given state(s) at intervals; publish to channel.""" if isinstance(state, (tuple, list)): states = state else: states = [state] - + def _wait(): while self.state not in states: time.sleep(interval) self.publish(channel) - + # From http://psyco.sourceforge.net/psycoguide/bugs.html: # "The compiled machine code does not include the regular polling # done by Python, meaning that a KeyboardInterrupt will not be @@ -353,18 +357,18 @@ class Bus(object): sys.modules['psyco'].cannotcompile(_wait) except (KeyError, AttributeError): pass - + _wait() - + def _do_execv(self): """Re-execute the current process. - + 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. """ args = sys.argv[:] self.log('Re-spawning %s' % ' '.join(args)) - + if sys.platform[:4] == 'java': from _systemrestart import SystemRestart raise SystemRestart @@ -377,16 +381,16 @@ class Bus(object): if self.max_cloexec_files: self._set_cloexec() os.execv(sys.executable, args) - + def _set_cloexec(self): """Set the CLOEXEC flag on all open files (except stdin/out/err). - + If self.max_cloexec_files is an integer (the default), then on platforms which support it, it represents the max open files setting for the operating system. This function will be called just before the process is restarted via os.execv() to prevent open files from persisting into the new process. - + Set self.max_cloexec_files to 0 to disable this behavior. """ for fd in range(3, self.max_cloexec_files): # skip stdin/out/err @@ -395,7 +399,7 @@ class Bus(object): except IOError: continue fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) - + def stop(self): """Stop all services.""" self.state = states.STOPPING @@ -403,7 +407,7 @@ class Bus(object): self.publish('stop') self.state = states.STOPPED self.log('Bus STOPPED') - + def start_with_callback(self, func, args=None, kwargs=None): """Start 'func' in a new thread T, then start self (and return T).""" if args is None: @@ -411,18 +415,18 @@ class Bus(object): if kwargs is None: kwargs = {} args = (func,) + args - + def _callback(func, *a, **kw): self.wait(states.STARTED) func(*a, **kw) t = threading.Thread(target=_callback, args=args, kwargs=kwargs) t.setName('Bus Callback ' + t.getName()) t.start() - + self.start() - + return t - + def log(self, msg="", level=20, traceback=False): """Log the given message. Append the last traceback if requested.""" if traceback: