From abff43f5680865470fd7f0c38354df595e774bae Mon Sep 17 00:00:00 2001 From: echel0n Date: Sun, 15 Jun 2014 00:16:55 -0700 Subject: [PATCH] Improved startup/shutdown of tornado. Fixed issues with notifications related to tornado. --- SickBeard.py | 11 ++-- sickbeard/__init__.py | 37 +++-------- sickbeard/config.py | 2 +- sickbeard/webserve.py | 58 +++++++++-------- sickbeard/webserveInit.py | 128 +++++++++++++++++++++++++++----------- 5 files changed, 135 insertions(+), 101 deletions(-) diff --git a/SickBeard.py b/SickBeard.py index 8863d8c6..55cc115c 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -361,12 +361,8 @@ def main(): 'https_key': sickbeard.HTTPS_KEY, } - # init tornado web server - sickbeard.webserveInitScheduler = sickbeard.scheduler.Scheduler(webserverInit(options), - cycleTime=datetime.timedelta(seconds=3), - threadName="TORNADO", - silent=True, - runImmediately=True) + # init tornado server + sickbeard.WEBSERVER = webserverInit(options) # Build from the DB to start with logger.log(u"Loading initial show list") @@ -375,6 +371,9 @@ def main(): # Fire up all our threads sickbeard.start() + # start tornado thread + sickbeard.WEBSERVER.thread.start() + # Launch browser if we're supposed to if sickbeard.LAUNCH_BROWSER and not noLaunch and not sickbeard.DAEMON: sickbeard.launchBrowser(startPort) diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 8cd7ae2f..43813f66 100644 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -29,7 +29,7 @@ from urllib2 import getproxies from threading import Lock # apparently py2exe won't build these unless they're imported somewhere -from sickbeard import providers, metadata, config +from sickbeard import providers, metadata, config, webserveInit from sickbeard.providers.generic import GenericProvider from providers import ezrss, tvtorrents, btn, newznab, womble, thepiratebay, torrentleech, kat, iptorrents, \ omgwtfnzbs, scc, hdtorrents, torrentday, hdbits, nextgen, speedcd, nyaatorrents, fanzub @@ -77,6 +77,7 @@ PIDFILE = '' DAEMON = None NO_RESIZE = False +WEBSERVER = None maintenanceScheduler = None dailySearchScheduler = None @@ -89,7 +90,6 @@ properFinderScheduler = None autoPostProcesserScheduler = None subtitlesFinderScheduler = None traktWatchListCheckerScheduler = None -webserveInitScheduler = None showList = None loadingShowList = None @@ -479,7 +479,7 @@ def initialize(consoleLogging=True): USE_FAILED_DOWNLOADS, DELETE_FAILED, ANON_REDIRECT, LOCALHOST_IP, REMOTE_IP, TMDB_API_KEY, DEBUG, PROXY_SETTING, \ AUTOPOSTPROCESSER_FREQUENCY, DEFAULT_AUTOPOSTPROCESSER_FREQUENCY, MIN_AUTOPOSTPROCESSER_FREQUENCY, \ ANIME_DEFAULT, NAMING_ANIME, ANIMESUPPORT, USE_ANIDB, ANIDB_USERNAME, ANIDB_PASSWORD, ANIDB_USE_MYLIST, \ - ANIME_SPLIT_HOME, maintenanceScheduler, SCENE_DEFAULT, WEB_DATA_ROOT, webserveInitScheduler + ANIME_SPLIT_HOME, maintenanceScheduler, SCENE_DEFAULT, WEB_DATA_ROOT, WEBSERVER if __INITIALIZED__: return False @@ -1119,15 +1119,12 @@ def start(): showUpdateScheduler, versionCheckScheduler, showQueueScheduler, \ properFinderScheduler, autoPostProcesserScheduler, searchQueueScheduler, \ subtitlesFinderScheduler, USE_SUBTITLES,traktWatchListCheckerScheduler, \ - dailySearchScheduler, webserveInitScheduler, started + dailySearchScheduler, started with INIT_LOCK: if __INITIALIZED__: - # start tornado web server - webserveInitScheduler.thread.start() - # start the maintenance scheduler maintenanceScheduler.thread.start() logger.log(u"Performing initial maintenance tasks, please wait ...") @@ -1173,7 +1170,7 @@ def halt(): showUpdateScheduler, versionCheckScheduler, showQueueScheduler, \ properFinderScheduler, autoPostProcesserScheduler, searchQueueScheduler, \ subtitlesFinderScheduler, traktWatchListCheckerScheduler, \ - dailySearchScheduler, webserveInitScheduler, started + dailySearchScheduler, started with INIT_LOCK: @@ -1183,13 +1180,6 @@ def halt(): # abort all the threads - webserveInitScheduler.about = True - logger.log(u"Waiting for the TORNADO thread to exit") - try: - webserveInitScheduler.thread.join(10) - except: - pass - maintenanceScheduler.abort = True logger.log(u"Waiting for the MAINTENANCE scheduler thread to exit") try: @@ -1197,13 +1187,6 @@ def halt(): except: pass - dailySearchScheduler.abort = True - logger.log(u"Waiting for the DAILYSEARCHER thread to exit") - try: - dailySearchScheduler.thread.join(10) - except: - pass - backlogSearchScheduler.abort = True logger.log(u"Waiting for the BACKLOG thread to exit") try: @@ -1310,16 +1293,12 @@ def saveAll(): def saveAndShutdown(restart=False): + global WEBSERVER + halt() saveAll() - logger.log(u"Killing tornado") - try: - IOLoop.current().stop() - except RuntimeError: - pass - except: - logger.log('Failed shutting down the server: %s' % traceback.format_exc(), logger.ERROR) + IOLoop.instance().add_callback(WEBSERVER.shutdown) if CREATEPID: logger.log(u"Removing pidfile " + str(PIDFILE)) diff --git a/sickbeard/config.py b/sickbeard/config.py index 6969503f..e40207ba 100644 --- a/sickbeard/config.py +++ b/sickbeard/config.py @@ -163,7 +163,7 @@ def change_AUTOPOSTPROCESSER_FREQUENCY(freq): if sickbeard.AUTOPOSTPROCESSER_FREQUENCY < sickbeard.MIN_AUTOPOSTPROCESSER_FREQUENCY: sickbeard.AUTOPOSTPROCESSER_FREQUENCY = sickbeard.MIN_AUTOPOSTPROCESSER_FREQUENCY - sickbeard.autoPostProcessorScheduler.cycleTime = datetime.timedelta(minutes=sickbeard.AUTOPOSTPROCESSER_FREQUENCY) + sickbeard.autoPostProcesserScheduler.cycleTime = datetime.timedelta(minutes=sickbeard.AUTOPOSTPROCESSER_FREQUENCY) def change_DAILYSEARCH_FREQUENCY(freq): sickbeard.DAILYSEARCH_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_DAILYSEARCH_FREQUENCY) diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 2845f71c..7e372ab8 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -190,6 +190,7 @@ class IndexHandler(RedirectHandler): args[arg] = value[0] return args + @asynchronous def _dispatch(self): """ Load up the requested URL if it matches one of our own methods. @@ -235,12 +236,10 @@ class IndexHandler(RedirectHandler): def get_current_user(self): return self.get_secure_cookie("user") - @asynchronous @authenticated def get(self, *args, **kwargs): return self._dispatch() - @asynchronous def post(self, *args, **kwargs): return self._dispatch() @@ -1065,7 +1064,7 @@ class Manage(IndexHandler): exceptions_list = [] - curErrors += Home().editShow(curShow, new_show_dir, anyQualities, bestQualities, exceptions_list, + curErrors += self.editShow(curShow, new_show_dir, anyQualities, bestQualities, exceptions_list, new_flatten_folders, new_paused, subtitles=new_subtitles, anime=new_anime, scene=new_scene, directCall=True) @@ -1602,9 +1601,9 @@ class ConfigPostProcessing(IndexHandler): config.change_AUTOPOSTPROCESSER_FREQUENCY(autopostprocesser_frequency) if sickbeard.PROCESS_AUTOMATICALLY: - sickbeard.autoPostProcessorScheduler.silent = False + sickbeard.autoPostProcesserScheduler.silent = False else: - sickbeard.autoPostProcessorScheduler.silent = True + sickbeard.autoPostProcesserScheduler.silent = True if unpack: if self.isRarSupported() != 'not supported': @@ -3281,8 +3280,7 @@ class Home(IndexHandler): title = "Shutting down" message = "SickRage is shutting down..." - return _genericMessage(title, message) - + return self.finish(_genericMessage(title, message)) def restart(self, pid=None): @@ -3311,8 +3309,8 @@ class Home(IndexHandler): t = PageTemplate(file="restart_bare.tmpl") return self.finish(_munge(t)) else: - return _genericMessage("Update Failed", - "Update wasn't successful, not restarting. Check your log for more information.") + return self.finish(_genericMessage("Update Failed", + "Update wasn't successful, not restarting. Check your log for more information.")) def displayShow(self, show=None): @@ -3477,7 +3475,7 @@ class Home(IndexHandler): if directCall: return [errString] else: - return _genericMessage("Error", errString) + return self.finish(_genericMessage("Error", errString)) showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) @@ -3486,7 +3484,7 @@ class Home(IndexHandler): if directCall: return [errString] else: - return _genericMessage("Error", errString) + return self.finish(_genericMessage("Error", errString)) showObj.exceptions = scene_exceptions.get_scene_exceptions(showObj.indexerid) @@ -3727,16 +3725,16 @@ class Home(IndexHandler): def deleteShow(self, show=None): if show is None: - return _genericMessage("Error", "Invalid show ID") + return self.finish(_genericMessage("Error", "Invalid show ID")) showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) if showObj is None: - return _genericMessage("Error", "Unable to find the specified show") + return self.finish(_genericMessage("Error", "Unable to find the specified show")) if sickbeard.showQueueScheduler.action.isBeingAdded( showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): # @UndefinedVariable - return _genericMessage("Error", "Shows can't be deleted while they're being added or updated.") + return self.finish(_genericMessage("Error", "Shows can't be deleted while they're being added or updated.")) showObj.deleteShow() @@ -3747,12 +3745,12 @@ class Home(IndexHandler): def refreshShow(self, show=None): if show is None: - return _genericMessage("Error", "Invalid show ID") + return self.finish(_genericMessage("Error", "Invalid show ID")) showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) if showObj is None: - return _genericMessage("Error", "Unable to find the specified show") + return self.finish(_genericMessage("Error", "Unable to find the specified show")) # force the update from the DB try: @@ -3769,12 +3767,12 @@ class Home(IndexHandler): def updateShow(self, show=None, force=0): if show is None: - return _genericMessage("Error", "Invalid show ID") + return self.finish(_genericMessage("Error", "Invalid show ID")) showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) if showObj is None: - return _genericMessage("Error", "Unable to find the specified show") + return self.finish(_genericMessage("Error", "Unable to find the specified show")) # force the update try: @@ -3792,12 +3790,12 @@ class Home(IndexHandler): def subtitleShow(self, show=None, force=0): if show is None: - return _genericMessage("Error", "Invalid show ID") + return self.finish(_genericMessage("Error", "Invalid show ID")) showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) if showObj is None: - return _genericMessage("Error", "Unable to find the specified show") + return self.finish(_genericMessage("Error", "Unable to find the specified show")) # search and download subtitles sickbeard.showQueueScheduler.action.downloadSubtitles(showObj, bool(force)) # @UndefinedVariable @@ -3840,7 +3838,7 @@ class Home(IndexHandler): ui.notifications.error('Error', errMsg) return json.dumps({'result': 'error'}) else: - return _genericMessage("Error", errMsg) + return self.finish(_genericMessage("Error", errMsg)) if not statusStrings.has_key(int(status)): errMsg = "Invalid status" @@ -3848,7 +3846,7 @@ class Home(IndexHandler): ui.notifications.error('Error', errMsg) return json.dumps({'result': 'error'}) else: - return _genericMessage("Error", errMsg) + return self.finish(_genericMessage("Error", errMsg)) showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) @@ -3858,7 +3856,7 @@ class Home(IndexHandler): ui.notifications.error('Error', errMsg) return json.dumps({'result': 'error'}) else: - return _genericMessage("Error", errMsg) + return self.finish(_genericMessage("Error", errMsg)) segment = {} if eps is not None: @@ -3873,7 +3871,7 @@ class Home(IndexHandler): epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) if epObj is None: - return _genericMessage("Error", "Episode couldn't be retrieved") + return self.finish(_genericMessage("Error", "Episode couldn't be retrieved")) if int(status) in [WANTED, FAILED]: # figure out what episodes are wanted so we can backlog them @@ -3949,17 +3947,17 @@ class Home(IndexHandler): def testRename(self, show=None): if show is None: - return _genericMessage("Error", "You must specify a show") + return self.finish(_genericMessage("Error", "You must specify a show")) showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) if showObj is None: - return _genericMessage("Error", "Show not in show list") + return self.finish(_genericMessage("Error", "Show not in show list")) try: show_loc = showObj.location # @UnusedVariable except exceptions.ShowDirNotFoundException: - return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") + return self.finish(_genericMessage("Error", "Can't rename episodes when the show dir is missing.")) ep_obj_rename_list = [] @@ -3996,18 +3994,18 @@ class Home(IndexHandler): if show is None or eps is None: errMsg = "You must specify a show and at least one episode" - return _genericMessage("Error", errMsg) + return self.finish(_genericMessage("Error", errMsg)) show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) if show_obj is None: errMsg = "Error", "Show not in show list" - return _genericMessage("Error", errMsg) + return self.finish(_genericMessage("Error", errMsg)) try: show_loc = show_obj.location # @UnusedVariable except exceptions.ShowDirNotFoundException: - return _genericMessage("Error", "Can't rename episodes when the show dir is missing.") + return self.finish(_genericMessage("Error", "Can't rename episodes when the show dir is missing.")) if eps is None: self.redirect("/home/displayShow?show=" + show) diff --git a/sickbeard/webserveInit.py b/sickbeard/webserveInit.py index 5ea21a4f..775822b3 100644 --- a/sickbeard/webserveInit.py +++ b/sickbeard/webserveInit.py @@ -1,11 +1,17 @@ import os +import threading +import time +import traceback +import datetime import sickbeard import webserve -import tornado.httpserver -import tornado.ioloop +from sickbeard.exceptions import ex from sickbeard import logger from sickbeard.helpers import create_https_certificates -from tornado.web import Application, StaticFileHandler, HTTPError, RedirectHandler +from tornado.web import Application, StaticFileHandler, RedirectHandler, HTTPError +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop + class MultiStaticFileHandler(StaticFileHandler): def initialize(self, paths, default_filename=None): @@ -27,17 +33,27 @@ class MultiStaticFileHandler(StaticFileHandler): # Oops file not found anywhere! raise HTTPError(404) + class webserverInit(): - def __init__(self, options): + def __init__(self, options, cycleTime=datetime.timedelta(seconds=3)): + self.amActive = False - options.setdefault('port', 8081) - options.setdefault('host', '0.0.0.0') - options.setdefault('log_dir', None) - options.setdefault('username', '') - options.setdefault('password', '') - options.setdefault('web_root', '/') - assert isinstance(options['port'], int) - assert 'data_root' in options + self.lastRun = datetime.datetime.fromordinal(1) + self.cycleTime = cycleTime + self.abort = False + + self.server = None + self.thread = None + + self.options = options + self.options.setdefault('port', 8081) + self.options.setdefault('host', '0.0.0.0') + self.options.setdefault('log_dir', None) + self.options.setdefault('username', '') + self.options.setdefault('password', '') + self.options.setdefault('web_root', '/') + assert isinstance(self.options['port'], int) + assert 'data_root' in self.options def http_error_401_hander(status, message, traceback, version): """ Custom handler for 401 error """ @@ -72,12 +88,12 @@ class webserverInit():
- ''' % options['web_root'] + ''' % self.options['web_root'] # tornado setup - enable_https = options['enable_https'] - https_cert = options['https_cert'] - https_key = options['https_key'] + enable_https = self.options['enable_https'] + https_cert = self.options['https_cert'] + https_key = self.options['https_key'] if enable_https: # If either the HTTPS certificate or key do not exist, make some self-signed ones. @@ -103,42 +119,84 @@ class webserverInit(): # Index Handler app.add_handlers(".*$", [ - (r"/", tornado.web.RedirectHandler, {'url': '/home/'}), + (r"/", RedirectHandler, {'url': '/home/'}), (r'/login', webserve.LoginHandler), - (r'%s(.*)(/?)' % options['web_root'], webserve.IndexHandler) + (r'%s(.*)(/?)' % self.options['web_root'], webserve.IndexHandler) ]) # Static Path Handler app.add_handlers(".*$", [ - ('%s/%s/(.*)([^/]*)' % (options['web_root'], 'images'), MultiStaticFileHandler, - {'paths': [os.path.join(options['data_root'], 'images'), + ('%s/%s/(.*)([^/]*)' % (self.options['web_root'], 'images'), MultiStaticFileHandler, + {'paths': [os.path.join(self.options['data_root'], 'images'), os.path.join(sickbeard.CACHE_DIR, 'images'), os.path.join(sickbeard.CACHE_DIR, 'images', 'thumbnails')]}), - ('%s/%s/(.*)([^/]*)' % (options['web_root'], 'css'), MultiStaticFileHandler, - {'paths': [os.path.join(options['data_root'], 'css')]}), - ('%s/%s/(.*)([^/]*)' % (options['web_root'], 'js'), MultiStaticFileHandler, - {'paths': [os.path.join(options['data_root'], 'js')]}) + ('%s/%s/(.*)([^/]*)' % (self.options['web_root'], 'css'), MultiStaticFileHandler, + {'paths': [os.path.join(self.options['data_root'], 'css')]}), + ('%s/%s/(.*)([^/]*)' % (self.options['web_root'], 'js'), MultiStaticFileHandler, + {'paths': [os.path.join(self.options['data_root'], 'js')]}) ]) if enable_https: protocol = "https" - server = tornado.httpserver.HTTPServer(app, no_keep_alive=True, - ssl_options={"certfile": https_cert, "keyfile": https_key}) + self.server = HTTPServer(app, no_keep_alive=True, + ssl_options={"certfile": https_cert, "keyfile": https_key}) else: protocol = "http" - server = tornado.httpserver.HTTPServer(app, no_keep_alive=True) + self.server = HTTPServer(app, no_keep_alive=True) - logger.log(u"Starting SickRage on " + protocol + "://" + str(options['host']) + ":" + str( - options['port']) + "/") + logger.log(u"Starting SickRage on " + protocol + "://" + str(self.options['host']) + ":" + str( + self.options['port']) + "/") - server.listen(options['port'], options['host']) + self.server.listen(self.options['port'], self.options['host']) + if self.thread == None or not self.thread.isAlive(): + self.thread = threading.Thread(None, self.monitor, 'TORNADO') + + def monitor(self): + + while True: + + currentTime = datetime.datetime.now() + + if currentTime - self.lastRun > self.cycleTime: + self.lastRun = currentTime + try: + logger.log(u"Starting tornado", logger.DEBUG) + + IOLoop.instance().start() + except Exception, e: + logger.log(u"Exception generated in tornado: " + ex(e), logger.ERROR) + logger.log(repr(traceback.format_exc()), logger.DEBUG) + + if self.abort: + self.abort = False + self.thread = None + return + + time.sleep(1) + + def shutdown(self): + + logger.logging.info('Shutting down tornado') - def run(self, force=False): try: - self.amActive = True - tornado.ioloop.IOLoop.instance().start() + self.abort = True + self.server.stop() + + deadline = time.time() + 10 + + io_loop = IOLoop.instance() + def stop_loop(): + now = time.time() + + if now < deadline: + if io_loop._callbacks: + io_loop.add_timeout(now + 1, stop_loop) + return + stop_loop() + self.thread.join(10) except: - self.amActive = False - raise \ No newline at end of file + pass + + logger.logging.info('Tornado is now shutdown') \ No newline at end of file