From 130daf7d0a1bf77dedb1ac712b5a659984d15ab9 Mon Sep 17 00:00:00 2001 From: echel0n Date: Tue, 1 Jul 2014 01:49:12 -0700 Subject: [PATCH 1/3] Added trending shows feature, this allows you to choose from a wide selection of popular/trending shows with rating stats and easily add the show into SR by simply clicking the plus sign. --- gui/slick/css/trakt.css | 92 +++++++++++++++++++ .../interfaces/default/home_addShows.tmpl | 14 ++- .../default/home_trendingShows.tmpl | 58 ++++++++++++ lib/trakt/__init__.py | 15 ++- sickbeard/__init__.py | 2 +- sickbeard/traktChecker.py | 5 +- sickbeard/webserve.py | 78 ++++++++++++---- 7 files changed, 232 insertions(+), 32 deletions(-) create mode 100644 gui/slick/css/trakt.css create mode 100644 gui/slick/interfaces/default/home_trendingShows.tmpl diff --git a/gui/slick/css/trakt.css b/gui/slick/css/trakt.css new file mode 100644 index 00000000..f2de517f --- /dev/null +++ b/gui/slick/css/trakt.css @@ -0,0 +1,92 @@ +.traktShowDiv { + + clear: both; + border-left: 1px solid #CCCCCC; + border-right: 1px solid #CCCCCC; + border-bottom: 1px solid #CCCCCC; + margin: auto; + padding: 0px; + text-align: left; + width: 750px; +} + +.traktShowDiv a, .traktShowDiv a:link, .traktShowDiv a:visited, .traktShowDiv a:hover { + text-decoration: none; + background: none; +} + +.traktShowTitle a { + color: #000000; + float: left; + padding-top: 3px; + line-height: 1.2em; + font-size: 1.1em; + text-shadow: -1px -1px 0 #FFF); +} + +.traktShowTitleIcons { + float: right; + padding: 3px 5px; +} + +.traktShowDiv .title { + font-weight: 900; + color: #333; +} +.imgWrapper { + background: url("../images/loading.gif") no-repeat scroll center center #FFFFFF; + border: 3px solid #FFFFFF; + box-shadow: 1px 1px 2px 0 #555555; + float: left; + height: 50px; + overflow: hidden; + text-indent: -3000px; + width: 50px; +} +.imgWrapper .traktPosterThumb { + float: left; + min-height: 100%; + min-width: 100%; + width: 50px; + height: auto; + position: relative; + border: none; + vertical-align: middle; +} +.traktPosterThumb { + -ms-interpolation-mode: bicubic; /* make scaling look nicer for ie */ + vertical-align: top; + height: auto; + width: 160px; + border-top: 1px solid #ccc; + border-right: 1px solid #ccc; +} + +.traktShowDiv th { + color: #000; + letter-spacing: 1px; + text-align: left; + background-color: #333333; +} + +.traktShowDiv th.nobg { + background: #efefef; + border-top: 1px solid #666; + text-align: center; +} + +.traktShowDiv td { + border-top: 1px solid #d2ebe8; + background: #fff; + padding: 5px 10px 5px 10px; + color: #000; +} + +.traktShowDiv td.trakts_show { + width: 100%; + height: 90%; + border-top: 1px solid #ccc; + vertical-align: top; + background: #F5FAFA; + color: #000; +} diff --git a/gui/slick/interfaces/default/home_addShows.tmpl b/gui/slick/interfaces/default/home_addShows.tmpl index 4d28ec81..953ccf96 100644 --- a/gui/slick/interfaces/default/home_addShows.tmpl +++ b/gui/slick/interfaces/default/home_addShows.tmpl @@ -26,15 +26,23 @@

For shows that you haven't downloaded yet, this option finds a show on theTVDB.com and TVRage.com, creates a directory for its episodes, and adds it to SickRage.

- +

+ + +
Add Trending Shows
+
+

Add Trending Show

+

For shows that you haven't downloaded yet, this option lets you choose from a list of current trending shows with ratings to add, creates a directory for its episodes, and adds it to SickRage.

+
+


#if $sickbeard.TRAKT_USE_RECOMMENDED:
Add Recommended Shows
-

Add Recommended Shows

-

For shows that you haven't downloaded yet, this option recommends shows to add based on your Trakt.tv watch list, creates a directory for its episodes, and adds it to SickRage. *** Trakt.tv must be enabled ***

+

Add Recommended Show

+

For shows that you haven't downloaded yet, this option recommends shows to add based on your Trakt.tv show library, creates a directory for its episodes, and adds it to SickRage. *** Trakt.tv must be enabled ***



diff --git a/gui/slick/interfaces/default/home_trendingShows.tmpl b/gui/slick/interfaces/default/home_trendingShows.tmpl new file mode 100644 index 00000000..7bbdeb19 --- /dev/null +++ b/gui/slick/interfaces/default/home_trendingShows.tmpl @@ -0,0 +1,58 @@ +#import sickbeard +#import datetime +#import re +#from sickbeard.common import * +#from sickbeard import sbdatetime + +#set global $title="Trending Shows" +#set global $header="Trending Shows" + +#set global $sbPath=".." + +#set global $topmenu="comingEpisodes" +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_top.tmpl") + + + + +#if $varExists('header') +

$header

+#else +

$title

+#end if + +
+ +#for $i, $cur_show in $enumerate($trending_shows): +
+ +#if not $i%4 +
+#end if + + + + +#end for +
+ + +
+

$cur_show["ratings"]["percentage"]%

+ $cur_show["ratings"]["votes"] votes + +
+ [add show] +
+
+
+
+ + + +#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_bottom.tmpl") \ No newline at end of file diff --git a/lib/trakt/__init__.py b/lib/trakt/__init__.py index f4cc7f48..c9565a3b 100644 --- a/lib/trakt/__init__.py +++ b/lib/trakt/__init__.py @@ -7,7 +7,7 @@ try: except ImportError: from lib import simplejson as json -def TraktCall(method, api, username, password, data = {}): +def TraktCall(method, api, username=None, password=None, data={}): """ A generic method for communicating with trakt. Uses the method and data provided along with the auth info to send the command. @@ -26,19 +26,16 @@ def TraktCall(method, api, username, password, data = {}): return None # if the username isn't given then it failed - if not username: - return None - - password = sha1(password).hexdigest() + if username and password: + password = sha1(password).hexdigest() + data["username"] = username + data["password"] = password # replace the API string with what we found method = method.replace("%API%", api) - data["username"] = username - data["password"] = password - # take the URL params and make a json object out of them - encoded_data = json.dumps(data); + encoded_data = json.dumps(data) # request the URL from trakt and parse the result as json try: diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 8da92f76..0b774322 100644 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -432,7 +432,7 @@ IGNORE_WORDS = "german,french,core2hd,dutch,swedish,reenc,MrLss" CALENDAR_UNPROTECTED = False TMDB_API_KEY = 'edc5f123313769de83a71e157758030b' - +TRAKT_API_KEY = 'abd806c54516240c76e4ebc9c5ccf394' __INITIALIZED__ = False def initialize(consoleLogging=True): diff --git a/sickbeard/traktChecker.py b/sickbeard/traktChecker.py index 51d2b7ef..eec11e15 100644 --- a/sickbeard/traktChecker.py +++ b/sickbeard/traktChecker.py @@ -147,9 +147,9 @@ class TraktChecker(): """ Adds a new show with the default settings """ - showObj = helpers.findCertainShow(sickbeard.showList, int(indexerid)) - if showObj != None: + if helpers.findCertainShow(sickbeard.showList, int(indexerid)): return + logger.log(u"Adding show " + str(indexerid)) root_dirs = sickbeard.ROOT_DIRS.split('|') location = root_dirs[int(root_dirs[0]) + 1] @@ -161,6 +161,7 @@ class TraktChecker(): return else: helpers.chmodAsParent(showPath) + sickbeard.showQueueScheduler.action.addShow(1, int(indexerid), showPath, status, int(sickbeard.QUALITY_DEFAULT), int(sickbeard.FLATTEN_FOLDERS_DEFAULT)) diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index c55dbdc5..8f8d0ee2 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -48,7 +48,7 @@ from sickbeard import subtitles from sickbeard import network_timezones from sickbeard.providers import newznab, rsstorrent -from sickbeard.common import Quality, Overview, statusStrings, qualityPresetStrings, cpu_presets +from sickbeard.common import Quality, Overview, statusStrings, qualityPresetStrings, cpu_presets, SKIPPED from sickbeard.common import SNATCHED, UNAIRED, IGNORED, ARCHIVED, WANTED, FAILED from sickbeard.common import SD, HD720p, HD1080p from sickbeard.exceptions import ex @@ -82,6 +82,7 @@ from lib import adba from Cheetah.Template import Template from tornado.web import RequestHandler, HTTPError + def authenticated(handler_class): def wrap_execute(handler_execute): def basicauth(handler, transforms, *args, **kwargs): @@ -125,6 +126,7 @@ def authenticated(handler_class): handler_class._execute = wrap_execute(handler_class._execute) return handler_class + class HTTPRedirect(Exception): """Exception raised when the request should be redirected.""" @@ -138,9 +140,11 @@ class HTTPRedirect(Exception): """Use this exception as a request.handler (raise self).""" raise self + def redirect(url, permanent=False, status=None): raise HTTPRedirect(url, permanent, status) + @authenticated class MainHandler(RequestHandler): def http_error_401_handler(self): @@ -216,7 +220,7 @@ class MainHandler(RequestHandler): def get(self, *args, **kwargs): try: self.finish(self._dispatch()) - except HTTPRedirect,inst: + except HTTPRedirect, inst: self.redirect(inst.url, inst.permanent, inst.status) def post(self, *args, **kwargs): @@ -462,6 +466,7 @@ class MainHandler(RequestHandler): browser = WebFileBrowser + class PageTemplate(Template): def __init__(self, headers, *args, **KWs): KWs['file'] = os.path.join(sickbeard.PROG_DIR, "gui/" + sickbeard.GUI_NAME + "/interfaces/default/", @@ -499,7 +504,7 @@ class PageTemplate(Template): {'title': 'Manage', 'key': 'manage'}, {'title': 'Config', 'key': 'config'}, {'title': logPageTitle, 'key': 'errorlogs'}, - ] + ] class IndexerWebUI(MainHandler): @@ -518,6 +523,7 @@ class IndexerWebUI(MainHandler): def _munge(string): return unicode(string).encode('utf-8', 'xmlcharrefreplace') + def _getEpisode(show, season=None, episode=None, absolute=None): if show is None: return "Invalid show parameters" @@ -2643,7 +2649,7 @@ class NewHomeAddShows(MainHandler): 'display_dir': '' + ek.ek(os.path.dirname, cur_path) + os.sep + '' + ek.ek( os.path.basename, cur_path), - } + } # see if the folder is in XBMC already dirResults = myDB.select("SELECT * FROM tv_shows WHERE location = ?", [cur_path]) @@ -2745,10 +2751,10 @@ class NewHomeAddShows(MainHandler): logger.log(u"Could not connect to trakt service, aborting recommended list update", logger.ERROR) return - map(final_results.append, ([int(show['tvdb_id']), show['url'], show['title'], show['overview'], - datetime.date.fromtimestamp(show['first_aired']).strftime('%Y%m%d')] for show in - recommendedlist if - not helpers.findCertainShow(sickbeard.showList, indexerid=int(show['tvdb_id'])))) + map(final_results.append, + ([int(show['tvdb_id']), show['url'], show['title'], show['overview'], + datetime.date.fromtimestamp(int(show['first_aired']) / 1000.0).strftime('%Y%m%d')] for show in + recommendedlist if not helpers.findCertainShow(sickbeard.showList, indexerid=int(show['tvdb_id'])))) return json.dumps({'results': final_results}) @@ -2764,10 +2770,22 @@ class NewHomeAddShows(MainHandler): show_name = whichSeries.split('|')[2] return self.addNewShow('|'.join([indexer_name, str(indexer), show_url, indexer_id, show_name, ""]), - indexerLang, rootDir, - defaultStatus, - anyQualities, bestQualities, flatten_folders, subtitles, fullShowPath, other_shows, - skipShow, providedIndexer, anime, scene) + indexerLang, rootDir, + defaultStatus, + anyQualities, bestQualities, flatten_folders, subtitles, fullShowPath, other_shows, + skipShow, providedIndexer, anime, scene) + + def trendingShows(self, *args, **kwargs): + """ + Display the new show page which collects a tvdb id, folder, and extra options and + posts them to addNewShow + """ + t = PageTemplate(headers=self.request.headers, file="home_trendingShows.tmpl") + t.submenu = HomeMenu() + + t.trending_shows = TraktCall("shows/trending.json/%API%/", sickbeard.TRAKT_API_KEY) + + return _munge(t) def existingShows(self, *args, **kwargs): """ @@ -2778,6 +2796,33 @@ class NewHomeAddShows(MainHandler): return _munge(t) + def addTraktShow(self, indexer_id, showName): + if helpers.findCertainShow(sickbeard.showList, int(indexer_id)): + return + + root_dirs = sickbeard.ROOT_DIRS.split('|') + location = root_dirs[int(root_dirs[0]) + 1] + + show_dir = ek.ek(os.path.join, location, helpers.sanitizeFileName(showName)) + dir_exists = helpers.makeDir(show_dir) + if not dir_exists: + logger.log(u"Unable to create the folder " + show_dir + ", can't add the show", logger.ERROR) + return + else: + helpers.chmodAsParent(show_dir) + + sickbeard.showQueueScheduler.action.addShow(1, int(indexer_id), show_dir, + default_status=sickbeard.STATUS_DEFAULT, + quality=sickbeard.QUALITY_DEFAULT, + flatten_folders=sickbeard.FLATTEN_FOLDERS_DEFAULT, + subtitles=sickbeard.SUBTITLES_DEFAULT, + anime=sickbeard.ANIME_DEFAULT, + scene=sickbeard.SCENE_DEFAULT) + + ui.notifications.message('Show added', 'Adding the specified show into ' + show_dir) + + # done adding show + redirect('/home/') def addNewShow(self, whichSeries=None, indexerLang="en", rootDir=None, defaultStatus=None, anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, @@ -3394,7 +3439,7 @@ class Home(MainHandler): return _munge(t) else: return self._genericMessage("Update Failed", - "Update wasn't successful, not restarting. Check your log for more information.") + "Update wasn't successful, not restarting. Check your log for more information.") def displayShow(self, show=None): @@ -3999,7 +4044,6 @@ class Home(MainHandler): myDB = db.DBConnection() myDB.mass_action(sql_l) - if int(status) == WANTED: msg = "Backlog was automatically started for the following seasons of " + showObj.name + ":
" for season in segment: @@ -4294,9 +4338,9 @@ class Home(MainHandler): return json.dumps({'result': 'failure'}) + class UI(MainHandler): def add_message(self): - ui.notifications.message('Test 1', 'This is test number 1') ui.notifications.error('Test 2', 'This is test number 2') @@ -4307,8 +4351,8 @@ class UI(MainHandler): cur_notification_num = 1 for cur_notification in ui.notifications.get_notifications(self.request.remote_ip): messages['notification-' + str(cur_notification_num)] = {'title': cur_notification.title, - 'message': cur_notification.message, - 'type': cur_notification.type} + 'message': cur_notification.message, + 'type': cur_notification.type} cur_notification_num += 1 return json.dumps(messages) \ No newline at end of file From 1fc909299d80d8dbfb54cbcaeffd5d1df240ec8f Mon Sep 17 00:00:00 2001 From: echel0n Date: Tue, 1 Jul 2014 06:08:10 -0700 Subject: [PATCH 2/3] Fixed start/restart/shutdown issues including any issues with daemonizing. --- SickBeard.py | 160 ++++++++++++---------------- gui/slick/js/restart.js | 100 ++++++++---------- lib/daemon.py | 216 ++++++++++++++++++++++++++++++++++++++ sickbeard/__init__.py | 35 ++++-- sickbeard/webserve.py | 5 +- sickbeard/webserveInit.py | 8 +- 6 files changed, 359 insertions(+), 165 deletions(-) create mode 100644 lib/daemon.py diff --git a/SickBeard.py b/SickBeard.py index a5c476fd..ad2aa536 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -19,7 +19,6 @@ # Check needed software dependencies to nudge users to fix their setup from __future__ import with_statement -import functools import sys import shutil @@ -66,14 +65,21 @@ from sickbeard.databases.mainDB import MIN_DB_VERSION from sickbeard.databases.mainDB import MAX_DB_VERSION from lib.configobj import ConfigObj - from tornado.ioloop import IOLoop +from daemon import Daemon signal.signal(signal.SIGINT, sickbeard.sig_handler) signal.signal(signal.SIGTERM, sickbeard.sig_handler) throwaway = datetime.datetime.strptime('20110101', '%Y%m%d') +restart = False +daemon = None +startPort = None +forceUpdate = None +noLaunch = None +web_options = None + def loadShowsFromDB(): """ Populates the showList with shows from the database @@ -91,48 +97,11 @@ def loadShowsFromDB(): sickbeard.showList.append(curShow) except Exception, e: logger.log( - u"There was an error creating the show in " + sqlShow["location"] + ": " + str(e).decode('utf-8'), + u"There was an error creating the show in " + sqlShow["location"] + ": " + str(e).decode('utf-8'), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) - # TODO: update the existing shows if the showlist has something in it - -def daemonize(): - try: - pid = os.fork() - if pid > 0: - sys.exit(0) - except OSError: - print "fork() failed" - sys.exit(1) - - os.chdir(sickbeard.PROG_DIR) - os.setsid() - # Make sure I can read my own files and shut out others - prev= os.umask(0) - os.umask(prev and int('077',8)) - - try: - pid = os.fork() - if pid > 0: - sys.exit(0) - except OSError: - print "fork() failed" - sys.exit(1) - - # Write pid - if sickbeard.CREATEPID: - pid = str(os.getpid()) - logger.log(u"Writing PID: " + pid + " to " + str(sickbeard.PIDFILE)) - try: - file(sickbeard.PIDFILE, 'w').write("%s\n" % pid) - except IOError, e: - logger.log_error_and_exit( - u"Unable to write PID file: " + sickbeard.PIDFILE + " Error: " + str(e.strerror) + " [" + str( - e.errno) + "]") - - dev_null = file('/dev/null', 'r') - os.dup2(dev_null.fileno(), sys.stdin.fileno()) + # TODO: update the existing shows if the showlist has something in it def restore(srcDir, dstDir): try: @@ -153,6 +122,8 @@ def main(): TV for me """ + global daemon, startPort, forceUpdate, noLaunch, web_options + # do some preliminary stuff sickbeard.MY_FULLNAME = os.path.normpath(os.path.abspath(__file__)) sickbeard.MY_NAME = os.path.basename(sickbeard.MY_FULLNAME) @@ -255,7 +226,7 @@ def main(): sys.exit("PID file: " + sickbeard.PIDFILE + " already exists. Exiting.") # The pidfile is only useful in daemon mode, make sure we can write the file properly - if sickbeard.CREATEPID and not sickbeard.restarted: + if sickbeard.CREATEPID: if sickbeard.DAEMON: pid_dir = os.path.dirname(sickbeard.PIDFILE) if not os.access(pid_dir, os.F_OK): @@ -289,7 +260,8 @@ def main(): if os.path.isfile(sickbeard.CONFIG_FILE): raise SystemExit("Config file '" + sickbeard.CONFIG_FILE + "' must be writeable.") elif not os.access(os.path.dirname(sickbeard.CONFIG_FILE), os.W_OK): - raise SystemExit("Config file root dir '" + os.path.dirname(sickbeard.CONFIG_FILE) + "' must be writeable.") + raise SystemExit( + "Config file root dir '" + os.path.dirname(sickbeard.CONFIG_FILE) + "' must be writeable.") # Check if we need to perform a restore first restoreDir = os.path.join(sickbeard.DATA_DIR, 'restore') @@ -327,12 +299,6 @@ def main(): # Initialize the config and our threads sickbeard.initialize(consoleLogging=consoleLogging) - if sickbeard.DAEMON: - daemonize() - - # Use this PID for everything - sickbeard.PID = os.getpid() - if forcedPort: logger.log(u"Forcing web server to port " + str(forcedPort)) startPort = forcedPort @@ -354,7 +320,8 @@ def main(): else: webhost = '0.0.0.0' - options = { + # web server options + web_options = { 'port': int(startPort), 'host': webhost, 'data_root': os.path.join(sickbeard.PROG_DIR, 'gui', sickbeard.GUI_NAME), @@ -366,62 +333,71 @@ def main(): 'handle_reverse_proxy': sickbeard.HANDLE_REVERSE_PROXY, 'https_cert': sickbeard.HTTPS_CERT, 'https_key': sickbeard.HTTPS_KEY, - } + } - # init tornado - try: - webserveInit.initWebServer(options) - except IOError: - logger.log(u"Unable to start web server, is something else running on port %d?" % startPort, logger.ERROR) - if sickbeard.LAUNCH_BROWSER and not sickbeard.DAEMON: - logger.log(u"Launching browser and exiting", logger.ERROR) + # Start SickRage + if daemon and daemon.is_running(): + daemon.restart(daemonize=sickbeard.DAEMON) + else: + daemon = SickRage(sickbeard.PIDFILE) + daemon.start(daemonize=sickbeard.DAEMON) + +class SickRage(Daemon): + def run(self): + global restart, startPort, forceUpdate, noLaunch, web_options + + # Use this PID for everything + sickbeard.PID = os.getpid() + + try: + webserveInit.initWebServer(web_options) + except IOError: + logger.log(u"Unable to start web server, is something else running on port %d?" % startPort, logger.ERROR) + if sickbeard.LAUNCH_BROWSER and not sickbeard.DAEMON: + logger.log(u"Launching browser and exiting", logger.ERROR) + sickbeard.launchBrowser(startPort) + sys.exit() + + # Build from the DB to start with + loadShowsFromDB() + + # Fire up all our threads + sickbeard.start() + + # Launch browser if we're supposed to + if sickbeard.LAUNCH_BROWSER and not noLaunch: sickbeard.launchBrowser(startPort) - sys.exit() - # Build from the DB to start with - loadShowsFromDB() + # Start an update if we're supposed to + if forceUpdate or sickbeard.UPDATE_SHOWS_ON_START: + sickbeard.showUpdateScheduler.action.run(force=True) # @UndefinedVariable - # Fire up all our threads - sickbeard.start() + if sickbeard.LAUNCH_BROWSER and not (noLaunch or sickbeard.DAEMON or restart): + sickbeard.launchBrowser(startPort) - # Launch browser if we're supposed to - if sickbeard.LAUNCH_BROWSER and not noLaunch: - sickbeard.launchBrowser(startPort) + # reset this if sickrage was restarted + restart = False - # Start an update if we're supposed to - if forceUpdate or sickbeard.UPDATE_SHOWS_ON_START: - sickbeard.showUpdateScheduler.action.run(force=True) # @UndefinedVariable + # start IO loop + IOLoop.current().start() - # If we restarted then unset the restarted flag - if sickbeard.restarted: - sickbeard.restarted = False + # close IO loop + IOLoop.current().close(True) - # IOLoop - io_loop = IOLoop.current() + # stop all tasks + sickbeard.halt() - # Open browser window - if sickbeard.LAUNCH_BROWSER and not (noLaunch or sickbeard.DAEMON or sickbeard.restarted): - io_loop.add_timeout(datetime.timedelta(seconds=5), functools.partial(sickbeard.launchBrowser, startPort)) - - # Start web server - io_loop.start() - - # Save and restart/shutdown - sickbeard.saveAndShutdown() + # save all shows to DB + sickbeard.saveAll() if __name__ == "__main__": if sys.hexversion >= 0x020600F0: freeze_support() - while(True): + while(not sickbeard.shutdown): main() - # check if restart was requested - if not sickbeard.restarted: - if sickbeard.CREATEPID: - logger.log(u"Removing pidfile " + str(sickbeard.PIDFILE)) - sickbeard.remove_pid_file(sickbeard.PIDFILE) - break + logger.log("SickRage is restarting, please stand by ...") + restart = True - # restart - logger.log("Restarting SickRage, please stand by...") \ No newline at end of file + logger.log("Goodbye ...") \ No newline at end of file diff --git a/gui/slick/js/restart.js b/gui/slick/js/restart.js index 293ca3b3..017beff8 100644 --- a/gui/slick/js/restart.js +++ b/gui/slick/js/restart.js @@ -1,30 +1,55 @@ -if (sbHandleReverseProxy != "False" && sbHandleReverseProxy != 0) - // Don't add the port to the url if using reverse proxy - if (sbHttpsEnabled != "False" && sbHttpsEnabled != 0) - var sb_base_url = 'https://'+sbHost+sbRoot; - else - var sb_base_url = 'http://'+sbHost+sbRoot; -else - if (sbHttpsEnabled != "False" && sbHttpsEnabled != 0) - var sb_base_url = 'https://'+sbHost+':'+sbHttpPort+sbRoot; - else - var sb_base_url = 'http://'+sbHost+':'+sbHttpPort+sbRoot; +if (sbHttpsEnabled != "False" && sbHttpsEnabled != 0) { + var sb_base_url = 'https://' + sbHost + ':' + sbHttpPort + sbRoot; +} else { + var sb_base_url = 'http://' + sbHost + ':' + sbHttpPort + sbRoot; +} -var base_url = window.location.protocol+'//'+window.location.host+sbRoot; -var is_alive_url = sbRoot+'/home/is_alive'; +var base_url = window.location.protocol + '//' + window.location.host + sbRoot; +var is_alive_url = sbRoot + '/home/is_alive/'; var timeout_id; -var restarted = ''; +var current_pid = ''; var num_restart_waits = 0; -function restartHandler() { +function is_alive() { + timeout_id = 0; + $.get(is_alive_url, function(data) { + + // if it's still initalizing then just wait and try again + if (data.msg == 'nope') { + $('#shut_down_loading').hide(); + $('#shut_down_success').show(); + $('#restart_message').show(); + setTimeout('is_alive()', 1000); + } else { + // if this is before we've even shut down then just try again later + if (current_pid == '' || data.msg == current_pid) { + current_pid = data.msg; + setTimeout(is_alive, 1000); + + // if we're ready to go then redirect to new url + } else { + $('#restart_loading').hide(); + $('#restart_success').show(); + $('#refresh_message').show(); + window.location = sb_base_url + '/home/'; + } + } + }, 'jsonp'); +} + +$(document).ready(function() { + + is_alive(); + + $('#shut_down_message').ajaxError(function(e, jqxhr, settings, exception) { num_restart_waits += 1; $('#shut_down_loading').hide(); $('#shut_down_success').show(); $('#restart_message').show(); - is_alive_url = sb_base_url+'/home/is_alive'; + is_alive_url = sb_base_url + '/home/is_alive/'; - // if https is enabled or you are currently on https and the port or protocol changed just wait 5 seconds then redirect. + // if https is enabled or you are currently on https and the port or protocol changed just wait 5 seconds then redirect. // This is because the ajax will fail if the cert is untrusted or the the http ajax requst from https will fail because of mixed content error. if ((sbHttpsEnabled != "False" && sbHttpsEnabled != 0) || window.location.protocol == "https:") { if (base_url != sb_base_url) { @@ -34,16 +59,8 @@ function restartHandler() { $('#restart_success').show(); $('#refresh_message').show(); }, 3000); - setTimeout("window.location = sb_base_url+'/home/'", 5000); + setTimeout("window.location = sb_base_url + '/home/'", 5000); } - } else { - timeout_id = 1; - setTimeout(function(){ - $('#restart_loading').hide(); - $('#restart_success').show(); - $('#refresh_message').show(); - }, 3000); - setTimeout("window.location = sb_base_url+'/home/'", 5000); } // if it is taking forever just give up @@ -54,34 +71,9 @@ function restartHandler() { return; } - if (timeout_id == 0) + if (timeout_id == 0) { timeout_id = setTimeout('is_alive()', 1000); -} - -function is_alive() { - timeout_id = 0; - - $.get(is_alive_url, function(data) { - - // if it's still initalizing then just wait and try again - if (data.msg == 'nope') { - $('#shut_down_loading').hide(); - $('#shut_down_success').show(); - $('#restart_message').show(); - setTimeout('is_alive()', 1000); - } else if (data.restarted == 'True') { - restartHandler(); - } else { - // if this is before we've even shut down then just try again later - if (restarted == '' || data.restarted == restarted) { - restarted = data.restarted; - setTimeout(is_alive, 1000); - } } - }, 'jsonp'); -} + }); -$(document).ready(function() -{ - is_alive(); -}); +}); \ No newline at end of file diff --git a/lib/daemon.py b/lib/daemon.py new file mode 100644 index 00000000..228e52c4 --- /dev/null +++ b/lib/daemon.py @@ -0,0 +1,216 @@ +''' +*** +Modified generic daemon class +*** + +Author: http://www.jejik.com/articles/2007/02/ + a_simple_unix_linux_daemon_in_python/www.boxedice.com + +License: http://creativecommons.org/licenses/by-sa/3.0/ + +Changes: 23rd Jan 2009 (David Mytton ) + - Replaced hard coded '/dev/null in __init__ with os.devnull + - Added OS check to conditionally remove code that doesn't + work on OS X + - Added output to console on completion + - Tidied up formatting + 11th Mar 2009 (David Mytton ) + - Fixed problem with daemon exiting on Python 2.4 + (before SystemExit was part of the Exception base) + 13th Aug 2010 (David Mytton + - Fixed unhandled exception if PID file is empty +''' + +# Core modules +import atexit +import os +import sys +import time +import signal + + +class Daemon(object): + """ + A generic daemon class. + + Usage: subclass the Daemon class and override the run() method + """ + def __init__(self, pidfile, stdin=os.devnull, + stdout=os.devnull, stderr=os.devnull, + home_dir='.', umask=022, verbose=1): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + self.home_dir = home_dir + self.verbose = verbose + self.umask = umask + self.daemon_alive = True + + def daemonize(self): + """ + Do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # Exit first parent + sys.exit(0) + except OSError, e: + sys.stderr.write( + "fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # Decouple from parent environment + os.chdir(self.home_dir) + os.setsid() + os.umask(self.umask) + + # Do second fork + try: + pid = os.fork() + if pid > 0: + # Exit from second parent + sys.exit(0) + except OSError, e: + sys.stderr.write( + "fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + if sys.platform != 'darwin': # This block breaks on OS X + # Redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = file(self.stdin, 'r') + so = file(self.stdout, 'a+') + if self.stderr: + se = file(self.stderr, 'a+', 0) + else: + se = so + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + def sigtermhandler(signum, frame): + self.daemon_alive = False + signal.signal(signal.SIGTERM, sigtermhandler) + signal.signal(signal.SIGINT, sigtermhandler) + + if self.verbose >= 1: + print "Started" + + # Write pidfile + atexit.register( + self.delpid) # Make sure pid file is removed if we quit + pid = str(os.getpid()) + file(self.pidfile, 'w+').write("%s\n" % pid) + + def delpid(self): + os.remove(self.pidfile) + + def start(self, daemonize=True, *args, **kwargs): + """ + Start the daemon + """ + + if daemonize: + if self.verbose >= 1: + print "Starting..." + + # Check for a pidfile to see if the daemon already runs + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + except SystemExit: + pid = None + + if pid: + message = "pidfile %s already exists. Is it already running?\n" + sys.stderr.write(message % self.pidfile) + sys.exit(1) + + # Start the daemon + self.daemonize() + + self.run(*args, **kwargs) + + def stop(self, daemonize=True): + """ + Stop the daemon + """ + + if not daemonize: + return + + if self.verbose >= 1: + print "Stopping..." + + # Get the pid from the pidfile + pid = self.get_pid() + + if not pid: + message = "pidfile %s does not exist. Not running?\n" + sys.stderr.write(message % self.pidfile) + + # Just to be sure. A ValueError might occur if the PID file is + # empty but does actually exist + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + + return # Not an error in a restart + + # Try killing the daemon process + try: + i = 0 + while 1: + os.kill(pid, signal.SIGTERM) + time.sleep(0.1) + i = i + 1 + if i % 10 == 0: + os.kill(pid, signal.SIGHUP) + except OSError, err: + err = str(err) + if err.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print str(err) + sys.exit(1) + + if self.verbose >= 1: + print "Stopped" + + def restart(self, daemonize=True): + """ + Restart the daemon + """ + self.stop(daemonize=daemonize) + self.start(daemonize=daemonize) + + def get_pid(self): + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + except SystemExit: + pid = None + return pid + + def is_running(self): + pid = self.get_pid() + print(pid) + return pid and os.path.exists('/proc/%d' % pid) + + def run(self): + """ + You should override this method when you subclass Daemon. + It will be called after the process has been + daemonized by start() or restart(). + """ \ No newline at end of file diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 0b774322..389b49e4 100644 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -19,7 +19,6 @@ from __future__ import with_statement import webbrowser -import time import datetime import socket import os @@ -104,7 +103,7 @@ CUR_COMMIT_HASH = None INIT_LOCK = Lock() started = False -restarted = False +shutdown = False ACTUAL_LOG_DIR = None LOG_DIR = None @@ -1284,9 +1283,12 @@ def remove_pid_file(PIDFILE): def sig_handler(signum=None, frame=None): + global shutdown + if type(signum) != type(None): logger.log(u"Signal %i caught, saving and exiting..." % int(signum)) - webserveInit.shutdown() + shutdown = True + IOLoop.current().stop() def saveAll(): global showList @@ -1300,10 +1302,24 @@ def saveAll(): logger.log(u"Saving config file to disk") save_config() -def saveAndShutdown(): +def saveAndShutdown(restart=False): + global shutdown + + if not restart: + shutdown = True + + # stop tornado web server + webserveInit.server.stop() + + # stop all tasks halt() + + # save all shows to db saveAll() + #stop tornado io loop + IOLoop.current().stop() + def invoke_command(to_call, *args, **kwargs): def delegate(): @@ -1319,21 +1335,18 @@ def invoke_restart(soft=True): def invoke_shutdown(): - invoke_command(webserveInit.shutdown) - + global shutdown + shutdown = True + invoke_command(IOLoop.current().stop) def restart(soft=True): - global restarted - if soft: halt() saveAll() logger.log(u"Re-initializing all data") initialize() else: - restarted=True - time.sleep(5) - webserveInit.shutdown() + IOLoop.current().stop() def save_config(): diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 8f8d0ee2..d76199fd 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -3099,9 +3099,9 @@ class Home(MainHandler): if sickbeard.started: return callback + '(' + json.dumps( - {"msg": str(sickbeard.PID), "restarted": str(sickbeard.restarted)}) + ');' + {"msg": str(sickbeard.PID)}) + ');' else: - return callback + '(' + json.dumps({"msg": "nope", "restarted": str(sickbeard.restarted)}) + ');' + return callback + '(' + json.dumps({"msg": "nope"}) + ');' def index(self, *args, **kwargs): @@ -3424,7 +3424,6 @@ class Home(MainHandler): return _munge(t) - def update(self, pid=None): if str(pid) != str(sickbeard.PID): diff --git a/sickbeard/webserveInit.py b/sickbeard/webserveInit.py index a6416e00..570b2e1f 100644 --- a/sickbeard/webserveInit.py +++ b/sickbeard/webserveInit.py @@ -105,16 +105,14 @@ def initWebServer(options={}): logger.log(u"Starting SickRage on " + protocol + "://" + str(options['host']) + ":" + str( options['port']) + "/") - if not sickbeard.restarted: - server.listen(options['port'], options['host']) + server.listen(options['port'], options['host']) def shutdown(): - global server - logger.log('Shutting down tornado io loop') + logger.log('Shutting down tornado IO loop') try: IOLoop.current().stop() except RuntimeError: pass except: - logger.log('Failed shutting down tornado io loop: %s' % traceback.format_exc(), logger.ERROR) \ No newline at end of file + logger.log('Failed shutting down tornado IO loop: %s' % traceback.format_exc(), logger.ERROR) \ No newline at end of file From 12ac388dc283af4d55497221ff8d3a8ce6906455 Mon Sep 17 00:00:00 2001 From: echel0n Date: Wed, 2 Jul 2014 11:51:14 -0700 Subject: [PATCH 3/3] Fixed startup/restart/shutdown issues on Windows, Linux, FreeBSD platforms tested. Fixed for updating issues. Fixed high cpu and memory usage. --- SickBeard.py | 617 ++++++++++++++-------------- lib/daemon.py | 81 ++-- lib/lockfile/__init__.py | 15 +- sickbeard/__init__.py | 31 +- sickbeard/providers/btn.py | 2 + sickbeard/providers/hdbits.py | 2 + sickbeard/providers/hdtorrents.py | 2 + sickbeard/providers/iptorrents.py | 2 + sickbeard/providers/kat.py | 2 + sickbeard/providers/newznab.py | 2 + sickbeard/providers/nextgen.py | 2 + sickbeard/providers/publichd.py | 2 + sickbeard/providers/scc.py | 2 + sickbeard/providers/speedcd.py | 2 + sickbeard/providers/thepiratebay.py | 2 + sickbeard/providers/torrentday.py | 2 + sickbeard/providers/torrentleech.py | 2 + sickbeard/providers/womble.py | 3 + sickbeard/tvcache.py | 2 + sickbeard/webserveInit.py | 161 ++++---- 20 files changed, 482 insertions(+), 454 deletions(-) diff --git a/SickBeard.py b/SickBeard.py index ad2aa536..64876036 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -20,8 +20,10 @@ # Check needed software dependencies to nudge users to fix their setup from __future__ import with_statement +import time import sys import shutil +import subprocess if sys.version_info < (2, 6): print "Sorry, requires Python 2.6 or 2.7." @@ -59,13 +61,12 @@ import sickbeard from sickbeard import db from sickbeard.tv import TVShow from sickbeard import logger -from sickbeard import webserveInit +from sickbeard.webserveInit import SRWebServer from sickbeard.version import SICKBEARD_VERSION from sickbeard.databases.mainDB import MIN_DB_VERSION from sickbeard.databases.mainDB import MAX_DB_VERSION from lib.configobj import ConfigObj -from tornado.ioloop import IOLoop from daemon import Daemon signal.signal(signal.SIGINT, sickbeard.sig_handler) @@ -73,331 +74,343 @@ signal.signal(signal.SIGTERM, sickbeard.sig_handler) throwaway = datetime.datetime.strptime('20110101', '%Y%m%d') -restart = False -daemon = None -startPort = None -forceUpdate = None -noLaunch = None -web_options = None +class SickRage(object): -def loadShowsFromDB(): - """ - Populates the showList with shows from the database - """ + def loadShowsFromDB(self): + """ + Populates the showList with shows from the database + """ - logger.log(u"Loading initial show list") + logger.log(u"Loading initial show list") - myDB = db.DBConnection() - sqlResults = myDB.select("SELECT * FROM tv_shows") + myDB = db.DBConnection() + sqlResults = myDB.select("SELECT * FROM tv_shows") - sickbeard.showList = [] - for sqlShow in sqlResults: + sickbeard.showList = [] + for sqlShow in sqlResults: + try: + curShow = TVShow(int(sqlShow["indexer"]), int(sqlShow["indexer_id"])) + sickbeard.showList.append(curShow) + except Exception, e: + logger.log( + u"There was an error creating the show in " + sqlShow["location"] + ": " + str(e).decode('utf-8'), + logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + def restore(self, srcDir, dstDir): try: - curShow = TVShow(int(sqlShow["indexer"]), int(sqlShow["indexer_id"])) - sickbeard.showList.append(curShow) - except Exception, e: - logger.log( - u"There was an error creating the show in " + sqlShow["location"] + ": " + str(e).decode('utf-8'), - logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) + for file in os.listdir(srcDir): + srcFile = os.path.join(srcDir, file) + dstFile = os.path.join(dstDir, file) + bakFile = os.path.join(dstDir, file + '.bak') + shutil.move(dstFile, bakFile) + shutil.move(srcFile, dstFile) - # TODO: update the existing shows if the showlist has something in it + os.rmdir(srcDir) + return True + except: + return False -def restore(srcDir, dstDir): - try: - for file in os.listdir(srcDir): - srcFile = os.path.join(srcDir, file) - dstFile = os.path.join(dstDir, file) - bakFile = os.path.join(dstDir, file + '.bak') - shutil.move(dstFile, bakFile) - shutil.move(srcFile, dstFile) + def __init__(self): + self.daemon = None + self.webserver = None + self.runAsDaemon = False + self.CREATEPID = False + self.PIDFILE = None + self.forceUpdate = False + self.forcedPort = None + self.noLaunch = False - os.rmdir(srcDir) - return True - except: - return False - -def main(): - """ - TV for me - """ - - global daemon, startPort, forceUpdate, noLaunch, web_options - - # do some preliminary stuff - sickbeard.MY_FULLNAME = os.path.normpath(os.path.abspath(__file__)) - sickbeard.MY_NAME = os.path.basename(sickbeard.MY_FULLNAME) - sickbeard.PROG_DIR = os.path.dirname(sickbeard.MY_FULLNAME) - sickbeard.DATA_DIR = sickbeard.PROG_DIR - sickbeard.MY_ARGS = sys.argv[1:] - sickbeard.DAEMON = False - sickbeard.CREATEPID = False - - sickbeard.SYS_ENCODING = None - - try: - locale.setlocale(locale.LC_ALL, "") - sickbeard.SYS_ENCODING = locale.getpreferredencoding() - except (locale.Error, IOError): - pass - - # For OSes that are poorly configured I'll just randomly force UTF-8 - if not sickbeard.SYS_ENCODING or sickbeard.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'): - sickbeard.SYS_ENCODING = 'UTF-8' - - if not hasattr(sys, "setdefaultencoding"): - reload(sys) - - try: - # pylint: disable=E1101 - # On non-unicode builds this will raise an AttributeError, if encoding type is not valid it throws a LookupError - sys.setdefaultencoding(sickbeard.SYS_ENCODING) - except: - print 'Sorry, you MUST add the SickRage folder to the PYTHONPATH environment variable' - print 'or find another way to force Python to use ' + sickbeard.SYS_ENCODING + ' for string encoding.' - sys.exit(1) - - # Need console logging for SickBeard.py and SickBeard-console.exe - consoleLogging = (not hasattr(sys, "frozen")) or (sickbeard.MY_NAME.lower().find('-console') > 0) - - # Rename the main thread - threading.currentThread().name = "MAIN" - - try: - opts, args = getopt.getopt(sys.argv[1:], "qfdp::", - ['quiet', 'forceupdate', 'daemon', 'port=', 'pidfile=', 'nolaunch', 'config=', - 'datadir=']) # @UnusedVariable - except getopt.GetoptError: - print "Available Options: --quiet, --forceupdate, --port, --daemon, --pidfile, --config, --datadir" - sys.exit() - - forceUpdate = False - forcedPort = None - noLaunch = False - - for o, a in opts: - # For now we'll just silence the logging - if o in ('-q', '--quiet'): - consoleLogging = False - - # Should we update (from indexer) all shows in the DB right away? - if o in ('-f', '--forceupdate'): - forceUpdate = True - - # Suppress launching web browser - # Needed for OSes without default browser assigned - # Prevent duplicate browser window when restarting in the app - if o in ('--nolaunch',): - noLaunch = True - - # Override default/configured port - if o in ('-p', '--port'): - forcedPort = int(a) - - # Run as a double forked daemon - if o in ('-d', '--daemon'): - sickbeard.DAEMON = True - # When running as daemon disable consoleLogging and don't start browser - consoleLogging = False - noLaunch = True - - if sys.platform == 'win32': - sickbeard.DAEMON = False - - # Specify folder to load the config file from - if o in ('--config',): - sickbeard.CONFIG_FILE = os.path.abspath(a) - - # Specify folder to use as the data dir - if o in ('--datadir',): - sickbeard.DATA_DIR = os.path.abspath(a) - - # Prevent resizing of the banner/posters even if PIL is installed - if o in ('--noresize',): - sickbeard.NO_RESIZE = True - - # Write a pidfile if requested - if o in ('--pidfile',): - sickbeard.CREATEPID = True - sickbeard.PIDFILE = str(a) - - # If the pidfile already exists, sickbeard may still be running, so exit - if os.path.exists(sickbeard.PIDFILE): - sys.exit("PID file: " + sickbeard.PIDFILE + " already exists. Exiting.") - - # The pidfile is only useful in daemon mode, make sure we can write the file properly - if sickbeard.CREATEPID: - if sickbeard.DAEMON: - pid_dir = os.path.dirname(sickbeard.PIDFILE) - if not os.access(pid_dir, os.F_OK): - sys.exit("PID dir: " + pid_dir + " doesn't exist. Exiting.") - if not os.access(pid_dir, os.W_OK): - sys.exit("PID dir: " + pid_dir + " must be writable (write permissions). Exiting.") - - else: - if consoleLogging: - sys.stdout.write("Not running in daemon mode. PID file creation disabled.\n") - - sickbeard.CREATEPID = False - - # If they don't specify a config file then put it in the data dir - if not sickbeard.CONFIG_FILE: - sickbeard.CONFIG_FILE = os.path.join(sickbeard.DATA_DIR, "config.ini") - - # Make sure that we can create the data dir - if not os.access(sickbeard.DATA_DIR, os.F_OK): - try: - os.makedirs(sickbeard.DATA_DIR, 0744) - except os.error, e: - raise SystemExit("Unable to create datadir '" + sickbeard.DATA_DIR + "'") - - # Make sure we can write to the data dir - if not os.access(sickbeard.DATA_DIR, os.W_OK): - raise SystemExit("Datadir must be writeable '" + sickbeard.DATA_DIR + "'") - - # Make sure we can write to the config file - if not os.access(sickbeard.CONFIG_FILE, os.W_OK): - if os.path.isfile(sickbeard.CONFIG_FILE): - raise SystemExit("Config file '" + sickbeard.CONFIG_FILE + "' must be writeable.") - elif not os.access(os.path.dirname(sickbeard.CONFIG_FILE), os.W_OK): - raise SystemExit( - "Config file root dir '" + os.path.dirname(sickbeard.CONFIG_FILE) + "' must be writeable.") - - # Check if we need to perform a restore first - restoreDir = os.path.join(sickbeard.DATA_DIR, 'restore') - if os.path.exists(restoreDir): - if restore(restoreDir, sickbeard.DATA_DIR): - logger.log(u"Restore successful...") - else: - logger.log(u"Restore FAILED!", logger.ERROR) - - os.chdir(sickbeard.DATA_DIR) - - if consoleLogging: - print "Starting up SickRage " + SICKBEARD_VERSION + " from " + sickbeard.CONFIG_FILE - - # Load the config and publish it to the sickbeard package - if not os.path.isfile(sickbeard.CONFIG_FILE): - logger.log(u"Unable to find '" + sickbeard.CONFIG_FILE + "' , all settings will be default!", logger.ERROR) - - sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE) - - CUR_DB_VERSION = db.DBConnection().checkDBVersion() - - if CUR_DB_VERSION > 0: - if CUR_DB_VERSION < MIN_DB_VERSION: - raise SystemExit("Your database version (" + str( - CUR_DB_VERSION) + ") is too old to migrate from with this version of SickRage (" + str( - MIN_DB_VERSION) + ").\n" + \ - "Upgrade using a previous version of SB first, or start with no database file to begin fresh.") - if CUR_DB_VERSION > MAX_DB_VERSION: - raise SystemExit("Your database version (" + str( - CUR_DB_VERSION) + ") has been incremented past what this version of SickRage supports (" + str( - MAX_DB_VERSION) + ").\n" + \ - "If you have used other forks of SB, your database may be unusable due to their modifications.") - - # Initialize the config and our threads - sickbeard.initialize(consoleLogging=consoleLogging) - - if forcedPort: - logger.log(u"Forcing web server to port " + str(forcedPort)) - startPort = forcedPort - else: - startPort = sickbeard.WEB_PORT - - if sickbeard.WEB_LOG: - log_dir = sickbeard.LOG_DIR - else: - log_dir = None - - # sickbeard.WEB_HOST is available as a configuration value in various - # places but is not configurable. It is supported here for historic reasons. - if sickbeard.WEB_HOST and sickbeard.WEB_HOST != '0.0.0.0': - webhost = sickbeard.WEB_HOST - else: - if sickbeard.WEB_IPV6: - webhost = '::' - else: - webhost = '0.0.0.0' - - # web server options - web_options = { - 'port': int(startPort), - 'host': webhost, - 'data_root': os.path.join(sickbeard.PROG_DIR, 'gui', sickbeard.GUI_NAME), - 'web_root': sickbeard.WEB_ROOT, - 'log_dir': log_dir, - 'username': sickbeard.WEB_USERNAME, - 'password': sickbeard.WEB_PASSWORD, - 'enable_https': sickbeard.ENABLE_HTTPS, - 'handle_reverse_proxy': sickbeard.HANDLE_REVERSE_PROXY, - 'https_cert': sickbeard.HTTPS_CERT, - 'https_key': sickbeard.HTTPS_KEY, - } - - # Start SickRage - if daemon and daemon.is_running(): - daemon.restart(daemonize=sickbeard.DAEMON) - else: - daemon = SickRage(sickbeard.PIDFILE) - daemon.start(daemonize=sickbeard.DAEMON) - -class SickRage(Daemon): - def run(self): - global restart, startPort, forceUpdate, noLaunch, web_options - - # Use this PID for everything - sickbeard.PID = os.getpid() + def start(self): + # do some preliminary stuff + sickbeard.MY_FULLNAME = os.path.normpath(os.path.abspath(__file__)) + sickbeard.MY_NAME = os.path.basename(sickbeard.MY_FULLNAME) + sickbeard.PROG_DIR = os.path.dirname(sickbeard.MY_FULLNAME) + sickbeard.DATA_DIR = sickbeard.PROG_DIR + sickbeard.MY_ARGS = sys.argv[1:] + sickbeard.SYS_ENCODING = None try: - webserveInit.initWebServer(web_options) - except IOError: - logger.log(u"Unable to start web server, is something else running on port %d?" % startPort, logger.ERROR) - if sickbeard.LAUNCH_BROWSER and not sickbeard.DAEMON: - logger.log(u"Launching browser and exiting", logger.ERROR) - sickbeard.launchBrowser(startPort) + locale.setlocale(locale.LC_ALL, "") + sickbeard.SYS_ENCODING = locale.getpreferredencoding() + except (locale.Error, IOError): + pass + + # For OSes that are poorly configured I'll just randomly force UTF-8 + if not sickbeard.SYS_ENCODING or sickbeard.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'): + sickbeard.SYS_ENCODING = 'UTF-8' + + if not hasattr(sys, "setdefaultencoding"): + reload(sys) + + try: + # pylint: disable=E1101 + # On non-unicode builds this will raise an AttributeError, if encoding type is not valid it throws a LookupError + sys.setdefaultencoding(sickbeard.SYS_ENCODING) + except: + print 'Sorry, you MUST add the SickRage folder to the PYTHONPATH environment variable' + print 'or find another way to force Python to use ' + sickbeard.SYS_ENCODING + ' for string encoding.' + sys.exit(1) + + # Need console logging for SickBeard.py and SickBeard-console.exe + self.consoleLogging = (not hasattr(sys, "frozen")) or (sickbeard.MY_NAME.lower().find('-console') > 0) + + # Rename the main thread + threading.currentThread().name = "MAIN" + + try: + opts, args = getopt.getopt(sys.argv[1:], "qfdp::", + ['quiet', 'forceupdate', 'daemon', 'port=', 'pidfile=', 'nolaunch', 'config=', + 'datadir=']) # @UnusedVariable + except getopt.GetoptError: + print "Available Options: --quiet, --forceupdate, --port, --daemon, --pidfile, --config, --datadir" sys.exit() + for o, a in opts: + # For now we'll just silence the logging + if o in ('-q', '--quiet'): + self.consoleLogging = False + + # Should we update (from indexer) all shows in the DB right away? + if o in ('-f', '--forceupdate'): + self.forceUpdate = True + + # Suppress launching web browser + # Needed for OSes without default browser assigned + # Prevent duplicate browser window when restarting in the app + if o in ('--nolaunch',): + self.noLaunch = True + + # Override default/configured port + if o in ('-p', '--port'): + self.forcedPort = int(a) + + # Run as a double forked daemon + if o in ('-d', '--daemon'): + self.runAsDaemon = True + # When running as daemon disable consoleLogging and don't start browser + self.consoleLogging = False + self.noLaunch = True + + if sys.platform == 'win32': + self.runAsDaemon = False + + # Specify folder to load the config file from + if o in ('--config',): + sickbeard.CONFIG_FILE = os.path.abspath(a) + + # Specify folder to use as the data dir + if o in ('--datadir',): + sickbeard.DATA_DIR = os.path.abspath(a) + + # Prevent resizing of the banner/posters even if PIL is installed + if o in ('--noresize',): + sickbeard.NO_RESIZE = True + + # Write a pidfile if requested + if o in ('--pidfile',): + self.CREATEPID = True + self.PIDFILE = str(a) + + # If the pidfile already exists, sickbeard may still be running, so exit + if os.path.exists(self.PIDFILE): + sys.exit("PID file: " + self.PIDFILE + " already exists. Exiting.") + + # The pidfile is only useful in daemon mode, make sure we can write the file properly + if self.CREATEPID: + if self.runAsDaemon: + pid_dir = os.path.dirname(self.PIDFILE) + if not os.access(pid_dir, os.F_OK): + sys.exit("PID dir: " + pid_dir + " doesn't exist. Exiting.") + if not os.access(pid_dir, os.W_OK): + sys.exit("PID dir: " + pid_dir + " must be writable (write permissions). Exiting.") + + else: + if self.consoleLogging: + sys.stdout.write("Not running in daemon mode. PID file creation disabled.\n") + + self.CREATEPID = False + + # If they don't specify a config file then put it in the data dir + if not sickbeard.CONFIG_FILE: + sickbeard.CONFIG_FILE = os.path.join(sickbeard.DATA_DIR, "config.ini") + + # Make sure that we can create the data dir + if not os.access(sickbeard.DATA_DIR, os.F_OK): + try: + os.makedirs(sickbeard.DATA_DIR, 0744) + except os.error, e: + raise SystemExit("Unable to create datadir '" + sickbeard.DATA_DIR + "'") + + # Make sure we can write to the data dir + if not os.access(sickbeard.DATA_DIR, os.W_OK): + raise SystemExit("Datadir must be writeable '" + sickbeard.DATA_DIR + "'") + + # Make sure we can write to the config file + if not os.access(sickbeard.CONFIG_FILE, os.W_OK): + if os.path.isfile(sickbeard.CONFIG_FILE): + raise SystemExit("Config file '" + sickbeard.CONFIG_FILE + "' must be writeable.") + elif not os.access(os.path.dirname(sickbeard.CONFIG_FILE), os.W_OK): + raise SystemExit( + "Config file root dir '" + os.path.dirname(sickbeard.CONFIG_FILE) + "' must be writeable.") + + # Check if we need to perform a restore first + restoreDir = os.path.join(sickbeard.DATA_DIR, 'restore') + if os.path.exists(restoreDir): + if self.restore(restoreDir, sickbeard.DATA_DIR): + logger.log(u"Restore successful...") + else: + logger.log(u"Restore FAILED!", logger.ERROR) + + os.chdir(sickbeard.DATA_DIR) + + # Load the config and publish it to the sickbeard package + if not os.path.isfile(sickbeard.CONFIG_FILE): + logger.log(u"Unable to find '" + sickbeard.CONFIG_FILE + "' , all settings will be default!", logger.ERROR) + + sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE) + + CUR_DB_VERSION = db.DBConnection().checkDBVersion() + + if CUR_DB_VERSION > 0: + if CUR_DB_VERSION < MIN_DB_VERSION: + raise SystemExit("Your database version (" + str( + CUR_DB_VERSION) + ") is too old to migrate from with this version of SickRage (" + str( + MIN_DB_VERSION) + ").\n" + \ + "Upgrade using a previous version of SB first, or start with no database file to begin fresh.") + if CUR_DB_VERSION > MAX_DB_VERSION: + raise SystemExit("Your database version (" + str( + CUR_DB_VERSION) + ") has been incremented past what this version of SickRage supports (" + str( + MAX_DB_VERSION) + ").\n" + \ + "If you have used other forks of SB, your database may be unusable due to their modifications.") + + # Initialize the config and our threads + sickbeard.initialize(consoleLogging=self.consoleLogging) + + if self.runAsDaemon: + self.daemon = Daemon(self.PIDFILE or os.path.join(sickbeard.DATA_DIR, 'sickbeard.pid')) + self.daemon.daemonize() + + # Get PID + sickbeard.PID = os.getpid() + + if self.forcedPort: + logger.log(u"Forcing web server to port " + str(self.forcedPort)) + self.startPort = self.forcedPort + else: + self.startPort = sickbeard.WEB_PORT + + if sickbeard.WEB_LOG: + self.log_dir = sickbeard.LOG_DIR + else: + self.log_dir = None + + # sickbeard.WEB_HOST is available as a configuration value in various + # places but is not configurable. It is supported here for historic reasons. + if sickbeard.WEB_HOST and sickbeard.WEB_HOST != '0.0.0.0': + self.webhost = sickbeard.WEB_HOST + else: + if sickbeard.WEB_IPV6: + self.webhost = '::' + else: + self.webhost = '0.0.0.0' + + # web server options + self.web_options = { + 'port': int(self.startPort), + 'host': self.webhost, + 'data_root': os.path.join(sickbeard.PROG_DIR, 'gui', sickbeard.GUI_NAME), + 'web_root': sickbeard.WEB_ROOT, + 'log_dir': self.log_dir, + 'username': sickbeard.WEB_USERNAME, + 'password': sickbeard.WEB_PASSWORD, + 'enable_https': sickbeard.ENABLE_HTTPS, + 'handle_reverse_proxy': sickbeard.HANDLE_REVERSE_PROXY, + 'https_cert': sickbeard.HTTPS_CERT, + 'https_key': sickbeard.HTTPS_KEY, + } + + # start web server + try: + self.webserver = SRWebServer(self.web_options) + self.webserver.start() + except IOError: + logger.log(u"Unable to start web server, is something else running on port %d?" % self.startPort, + logger.ERROR) + if sickbeard.LAUNCH_BROWSER and not self.runAsDaemon: + logger.log(u"Launching browser and exiting", logger.ERROR) + sickbeard.launchBrowser(self.startPort) + os._exit(1) + + if self.consoleLogging: + print "Starting up SickRage " + SICKBEARD_VERSION + " from " + sickbeard.CONFIG_FILE + # Build from the DB to start with - loadShowsFromDB() + self.loadShowsFromDB() # Fire up all our threads sickbeard.start() - # Launch browser if we're supposed to - if sickbeard.LAUNCH_BROWSER and not noLaunch: - sickbeard.launchBrowser(startPort) - # Start an update if we're supposed to - if forceUpdate or sickbeard.UPDATE_SHOWS_ON_START: + if self.forceUpdate or sickbeard.UPDATE_SHOWS_ON_START: sickbeard.showUpdateScheduler.action.run(force=True) # @UndefinedVariable - if sickbeard.LAUNCH_BROWSER and not (noLaunch or sickbeard.DAEMON or restart): - sickbeard.launchBrowser(startPort) + if sickbeard.LAUNCH_BROWSER and not (self.noLaunch or self.runAsDaemon): + sickbeard.launchBrowser(self.startPort) - # reset this if sickrage was restarted - restart = False - - # start IO loop - IOLoop.current().start() - - # close IO loop - IOLoop.current().close(True) - - # stop all tasks - sickbeard.halt() - - # save all shows to DB - sickbeard.saveAll() + while(sickbeard.started): + time.sleep(1) if __name__ == "__main__": if sys.hexversion >= 0x020600F0: freeze_support() - while(not sickbeard.shutdown): - main() + sr = None + try: + # init sickrage + sr = SickRage() - logger.log("SickRage is restarting, please stand by ...") - restart = True + # start sickrage + sr.start() - logger.log("Goodbye ...") \ No newline at end of file + # shutdown web server + sr.webserver.shutDown() + sr.webserver.join() + sr.webserver = None + + # if run as daemon delete the pidfile + if sr.runAsDaemon: + sr.daemon.delpid() + + if not sickbeard.shutdown: + install_type = sickbeard.versionCheckScheduler.action.install_type + + popen_list = [] + + if install_type in ('git', 'source'): + popen_list = [sys.executable, sickbeard.MY_FULLNAME] + elif install_type == 'win': + if hasattr(sys, 'frozen'): + # c:\dir\to\updater.exe 12345 c:\dir\to\sickbeard.exe + popen_list = [os.path.join(sickbeard.PROG_DIR, 'updater.exe'), str(sickbeard.PID), sys.executable] + else: + logger.log(u"Unknown SB launch method, please file a bug report about this", logger.ERROR) + popen_list = [sys.executable, os.path.join(sickbeard.PROG_DIR, 'updater.py'), str(sickbeard.PID), sys.executable, + sickbeard.MY_FULLNAME] + + if popen_list: + popen_list += sickbeard.MY_ARGS + if '--nolaunch' not in popen_list: + popen_list += ['--nolaunch'] + logger.log(u"Restarting SickRage with " + str(popen_list)) + logger.close() + subprocess.Popen(popen_list, cwd=os.getcwd()) + + # exit process + os._exit(0) + except: + if sr: + logger.log(traceback.format_exc(), logger.ERROR) + else: + print(traceback.format_exc()) + sys.exit(1) \ No newline at end of file diff --git a/lib/daemon.py b/lib/daemon.py index 228e52c4..77c435b1 100644 --- a/lib/daemon.py +++ b/lib/daemon.py @@ -1,26 +1,3 @@ -''' -*** -Modified generic daemon class -*** - -Author: http://www.jejik.com/articles/2007/02/ - a_simple_unix_linux_daemon_in_python/www.boxedice.com - -License: http://creativecommons.org/licenses/by-sa/3.0/ - -Changes: 23rd Jan 2009 (David Mytton ) - - Replaced hard coded '/dev/null in __init__ with os.devnull - - Added OS check to conditionally remove code that doesn't - work on OS X - - Added output to console on completion - - Tidied up formatting - 11th Mar 2009 (David Mytton ) - - Fixed problem with daemon exiting on Python 2.4 - (before SystemExit was part of the Exception base) - 13th Aug 2010 (David Mytton - - Fixed unhandled exception if PID file is empty -''' - # Core modules import atexit import os @@ -57,7 +34,7 @@ class Daemon(object): pid = os.fork() if pid > 0: # Exit first parent - sys.exit(0) + os._exit(0) except OSError, e: sys.stderr.write( "fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) @@ -73,7 +50,7 @@ class Daemon(object): pid = os.fork() if pid > 0: # Exit from second parent - sys.exit(0) + os._exit(0) except OSError, e: sys.stderr.write( "fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) @@ -93,11 +70,6 @@ class Daemon(object): os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) - def sigtermhandler(signum, frame): - self.daemon_alive = False - signal.signal(signal.SIGTERM, sigtermhandler) - signal.signal(signal.SIGINT, sigtermhandler) - if self.verbose >= 1: print "Started" @@ -110,43 +82,38 @@ class Daemon(object): def delpid(self): os.remove(self.pidfile) - def start(self, daemonize=True, *args, **kwargs): + def start(self, *args, **kwargs): """ Start the daemon """ - if daemonize: - if self.verbose >= 1: - print "Starting..." + if self.verbose >= 1: + print "Starting..." - # Check for a pidfile to see if the daemon already runs - try: - pf = file(self.pidfile, 'r') - pid = int(pf.read().strip()) - pf.close() - except IOError: - pid = None - except SystemExit: - pid = None + # Check for a pidfile to see if the daemon already runs + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + except SystemExit: + pid = None - if pid: - message = "pidfile %s already exists. Is it already running?\n" - sys.stderr.write(message % self.pidfile) - sys.exit(1) - - # Start the daemon - self.daemonize() + if pid: + message = "pidfile %s already exists. Is it already running?\n" + sys.stderr.write(message % self.pidfile) + sys.exit(1) + # Start the daemon + self.daemonize() self.run(*args, **kwargs) - def stop(self, daemonize=True): + def stop(self): """ Stop the daemon """ - if not daemonize: - return - if self.verbose >= 1: print "Stopping..." @@ -185,12 +152,12 @@ class Daemon(object): if self.verbose >= 1: print "Stopped" - def restart(self, daemonize=True): + def restart(self): """ Restart the daemon """ - self.stop(daemonize=daemonize) - self.start(daemonize=daemonize) + self.stop() + self.start() def get_pid(self): try: diff --git a/lib/lockfile/__init__.py b/lib/lockfile/__init__.py index 668b426f..d905af96 100644 --- a/lib/lockfile/__init__.py +++ b/lib/lockfile/__init__.py @@ -174,10 +174,19 @@ class LockBase: else: self.tname = "" dirname = os.path.dirname(self.lock_file) + + # unique name is mostly about the current process, but must + # also contain the path -- otherwise, two adjacent locked + # files conflict (one file gets locked, creating lock-file and + # unique file, the other one gets locked, creating lock-file + # and overwriting the already existing lock-file, then one + # gets unlocked, deleting both lock-file and unique file, + # finally the last lock errors out upon releasing. self.unique_name = os.path.join(dirname, - "%s%s.%s" % (self.hostname, - self.tname, - self.pid)) + "%s%s.%s%s" % (self.hostname, + self.tname, + self.pid, + hash(self.path))) self.timeout = timeout def acquire(self, timeout=None): diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 389b49e4..70ccffab 100644 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -28,6 +28,7 @@ from urllib2 import getproxies from threading import Lock # apparently py2exe won't build these unless they're imported somewhere +import sys 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, \ @@ -1269,7 +1270,7 @@ def halt(): pass __INITIALIZED__ = False - + started = False def remove_pid_file(PIDFILE): try: @@ -1283,12 +1284,9 @@ def remove_pid_file(PIDFILE): def sig_handler(signum=None, frame=None): - global shutdown - if type(signum) != type(None): logger.log(u"Signal %i caught, saving and exiting..." % int(signum)) - shutdown = True - IOLoop.current().stop() + saveAndShutdown() def saveAll(): global showList @@ -1303,22 +1301,14 @@ def saveAll(): save_config() def saveAndShutdown(restart=False): - global shutdown + global shutdown, started + # flag restart/shutdown if not restart: shutdown = True - # stop tornado web server - webserveInit.server.stop() - - # stop all tasks - halt() - - # save all shows to db - saveAll() - - #stop tornado io loop - IOLoop.current().stop() + # proceed with shutdown + started = False def invoke_command(to_call, *args, **kwargs): @@ -1333,11 +1323,8 @@ def invoke_command(to_call, *args, **kwargs): def invoke_restart(soft=True): invoke_command(restart, soft=soft) - def invoke_shutdown(): - global shutdown - shutdown = True - invoke_command(IOLoop.current().stop) + invoke_command(saveAndShutdown, False) def restart(soft=True): if soft: @@ -1346,7 +1333,7 @@ def restart(soft=True): logger.log(u"Re-initializing all data") initialize() else: - IOLoop.current().stop() + saveAndShutdown(True) def save_config(): diff --git a/sickbeard/providers/btn.py b/sickbeard/providers/btn.py index 70038bbd..1fd17210 100644 --- a/sickbeard/providers/btn.py +++ b/sickbeard/providers/btn.py @@ -349,6 +349,8 @@ class BTNCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/providers/hdbits.py b/sickbeard/providers/hdbits.py index 6e91e472..8821f0e2 100644 --- a/sickbeard/providers/hdbits.py +++ b/sickbeard/providers/hdbits.py @@ -260,6 +260,8 @@ class HDBitsCache(tvcache.TVCache): if ci is not None: ql.append(ci) + time.sleep(.2) + if ql: myDB = self._getDB() myDB.mass_action(ql) diff --git a/sickbeard/providers/hdtorrents.py b/sickbeard/providers/hdtorrents.py index 3c43e203..5f27d351 100644 --- a/sickbeard/providers/hdtorrents.py +++ b/sickbeard/providers/hdtorrents.py @@ -382,6 +382,8 @@ class HDTorrentsCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/providers/iptorrents.py b/sickbeard/providers/iptorrents.py index e72972d8..39ec76d0 100644 --- a/sickbeard/providers/iptorrents.py +++ b/sickbeard/providers/iptorrents.py @@ -323,6 +323,8 @@ class IPTorrentsCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/providers/kat.py b/sickbeard/providers/kat.py index c3a2d426..69080ca9 100644 --- a/sickbeard/providers/kat.py +++ b/sickbeard/providers/kat.py @@ -460,6 +460,8 @@ class KATCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/providers/newznab.py b/sickbeard/providers/newznab.py index 6328f67d..b40fab34 100755 --- a/sickbeard/providers/newznab.py +++ b/sickbeard/providers/newznab.py @@ -347,6 +347,8 @@ class NewznabCache(tvcache.TVCache): if ci is not None: ql.append(ci) + time.sleep(.2) + if ql: myDB = self._getDB() myDB.mass_action(ql) diff --git a/sickbeard/providers/nextgen.py b/sickbeard/providers/nextgen.py index db25d5d5..79430263 100644 --- a/sickbeard/providers/nextgen.py +++ b/sickbeard/providers/nextgen.py @@ -372,6 +372,8 @@ class NextGenCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/providers/publichd.py b/sickbeard/providers/publichd.py index d370f2c2..c280e43e 100644 --- a/sickbeard/providers/publichd.py +++ b/sickbeard/providers/publichd.py @@ -345,6 +345,8 @@ class PublicHDCache(tvcache.TVCache): if ci is not None: ql.append(ci) + time.sleep(.2) + if ql: myDB = self._getDB() myDB.mass_action(ql) diff --git a/sickbeard/providers/scc.py b/sickbeard/providers/scc.py index 564b3daf..b056e3e4 100644 --- a/sickbeard/providers/scc.py +++ b/sickbeard/providers/scc.py @@ -367,6 +367,8 @@ class SCCCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/providers/speedcd.py b/sickbeard/providers/speedcd.py index 16d611e3..0809620d 100644 --- a/sickbeard/providers/speedcd.py +++ b/sickbeard/providers/speedcd.py @@ -307,6 +307,8 @@ class SpeedCDCache(tvcache.TVCache): if ci is not None: ql.append(ci) + time.sleep(.2) + if ql: myDB = self._getDB() myDB.mass_action(ql) diff --git a/sickbeard/providers/thepiratebay.py b/sickbeard/providers/thepiratebay.py index e7818827..4ddc2a60 100644 --- a/sickbeard/providers/thepiratebay.py +++ b/sickbeard/providers/thepiratebay.py @@ -440,6 +440,8 @@ class ThePirateBayCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/providers/torrentday.py b/sickbeard/providers/torrentday.py index a7e0275c..23b98364 100644 --- a/sickbeard/providers/torrentday.py +++ b/sickbeard/providers/torrentday.py @@ -331,6 +331,8 @@ class TorrentDayCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/providers/torrentleech.py b/sickbeard/providers/torrentleech.py index 08384528..56e33227 100644 --- a/sickbeard/providers/torrentleech.py +++ b/sickbeard/providers/torrentleech.py @@ -326,6 +326,8 @@ class TorrentLeechCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/providers/womble.py b/sickbeard/providers/womble.py index df2fe780..10f38e45 100644 --- a/sickbeard/providers/womble.py +++ b/sickbeard/providers/womble.py @@ -15,6 +15,7 @@ # # You should have received a copy of the GNU General Public License # along with SickRage. If not, see . +import time import sickbeard import generic @@ -73,6 +74,8 @@ class WombleCache(tvcache.TVCache): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/tvcache.py b/sickbeard/tvcache.py index d3905fbc..da350e0b 100644 --- a/sickbeard/tvcache.py +++ b/sickbeard/tvcache.py @@ -128,6 +128,8 @@ class TVCache(): if ci is not None: cl.append(ci) + time.sleep(.2) + if cl: myDB = self._getDB() myDB.mass_action(cl) diff --git a/sickbeard/webserveInit.py b/sickbeard/webserveInit.py index 570b2e1f..8e38aa11 100644 --- a/sickbeard/webserveInit.py +++ b/sickbeard/webserveInit.py @@ -1,5 +1,8 @@ import os -import traceback +import socket +import time +import threading +import sys import sickbeard import webserve import webapi @@ -10,9 +13,6 @@ from tornado.web import Application, StaticFileHandler, RedirectHandler, HTTPErr from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop -server = None - - class MultiStaticFileHandler(StaticFileHandler): def initialize(self, paths, default_filename=None): self.paths = paths @@ -34,85 +34,106 @@ class MultiStaticFileHandler(StaticFileHandler): # Oops file not found anywhere! raise HTTPError(404) +class SRWebServer(threading.Thread): + def __init__(self, options=[], io_loop=None): + threading.Thread.__init__(self) + self.daemon = True + self.alive = True + self.name = "TORNADO" + self.io_loop = io_loop or IOLoop.current() -def initWebServer(options={}): - 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.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 - # tornado setup - enable_https = options['enable_https'] - https_cert = options['https_cert'] - https_key = options['https_key'] + # tornado setup + self.enable_https = self.options['enable_https'] + self.https_cert = self.options['https_cert'] + self.https_key = self.options['https_key'] - if enable_https: - # If either the HTTPS certificate or key do not exist, make some self-signed ones. - if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)): - if not create_https_certificates(https_cert, https_key): - logger.log(u"Unable to create CERT/KEY files, disabling HTTPS") + if self.enable_https: + # If either the HTTPS certificate or key do not exist, make some self-signed ones. + if not (self.https_cert and os.path.exists(self.https_cert)) or not (self.https_key and os.path.exists(self.https_key)): + if not create_https_certificates(self.https_cert, self.https_key): + logger.log(u"Unable to create CERT/KEY files, disabling HTTPS") + sickbeard.ENABLE_HTTPS = False + enable_https = False + + if not (os.path.exists(self.https_cert) and os.path.exists(self.https_key)): + logger.log(u"Disabled HTTPS because of missing CERT and KEY files", logger.WARNING) sickbeard.ENABLE_HTTPS = False enable_https = False - if not (os.path.exists(https_cert) and os.path.exists(https_key)): - logger.log(u"Disabled HTTPS because of missing CERT and KEY files", logger.WARNING) - sickbeard.ENABLE_HTTPS = False - enable_https = False + # Load the app + self.app = Application([], + debug=False, + gzip=True, + xheaders=sickbeard.HANDLE_REVERSE_PROXY, + cookie_secret='61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=' + ) - # Load the app - app = Application([], - debug=False, - gzip=True, - xheaders=sickbeard.HANDLE_REVERSE_PROXY, - cookie_secret='61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=' - ) + # Main Handler + self.app.add_handlers(".*$", [ + (r"%s" % self.options['web_root'], RedirectHandler, {'url': '%s/home/' % self.options['web_root']}), + (r'%s/api/(.*)(/?)' % self.options['web_root'], webapi.Api), + (r'%s/(.*)(/?)' % self.options['web_root'], webserve.MainHandler) + ]) - # Main Handler - app.add_handlers(".*$", [ - (r"%s" % options['web_root'], RedirectHandler, {'url': '%s/home/' % options['web_root']}), - (r'%s/api/(.*)(/?)' % options['web_root'], webapi.Api), - (r'%s/(.*)(/?)' % options['web_root'], webserve.MainHandler) - ]) + # Static Path Handler + self.app.add_handlers(".*$", [ + (r'%s/(favicon\.ico)' % self.options['web_root'], MultiStaticFileHandler, + {'paths': [os.path.join(self.options['data_root'], 'images/ico/favicon.ico')]}), + (r'%s/%s/(.*)(/?)' % (self.options['web_root'], 'images'), MultiStaticFileHandler, + {'paths': [os.path.join(self.options['data_root'], 'images'), + os.path.join(sickbeard.CACHE_DIR, 'images')]}), + (r'%s/%s/(.*)(/?)' % (self.options['web_root'], 'css'), MultiStaticFileHandler, + {'paths': [os.path.join(self.options['data_root'], 'css')]}), + (r'%s/%s/(.*)(/?)' % (self.options['web_root'], 'js'), MultiStaticFileHandler, + {'paths': [os.path.join(self.options['data_root'], 'js')]}) - # Static Path Handler - app.add_handlers(".*$", [ - (r'%s/(favicon\.ico)' % options['web_root'], MultiStaticFileHandler, - {'paths': [os.path.join(options['data_root'], 'images/ico/favicon.ico')]}), - (r'%s/%s/(.*)(/?)' % (options['web_root'], 'images'), MultiStaticFileHandler, - {'paths': [os.path.join(options['data_root'], 'images'), - os.path.join(sickbeard.CACHE_DIR, 'images')]}), - (r'%s/%s/(.*)(/?)' % (options['web_root'], 'css'), MultiStaticFileHandler, - {'paths': [os.path.join(options['data_root'], 'css')]}), - (r'%s/%s/(.*)(/?)' % (options['web_root'], 'js'), MultiStaticFileHandler, - {'paths': [os.path.join(options['data_root'], 'js')]}) + ]) - ]) + def run(self): + if self.enable_https: + protocol = "https" + self.server = HTTPServer(self.app, no_keep_alive=True, + ssl_options={"certfile": self.https_cert, "keyfile": self.https_key}) + else: + protocol = "http" + self.server = HTTPServer(self.app, no_keep_alive=True) - global server + logger.log(u"Starting SickRage on " + protocol + "://" + str(self.options['host']) + ":" + str( + self.options['port']) + "/") - if enable_https: - protocol = "https" - server = HTTPServer(app, no_keep_alive=True, - ssl_options={"certfile": https_cert, "keyfile": https_key}) - else: - protocol = "http" - server = HTTPServer(app, no_keep_alive=True) + try: + self.server.listen(self.options['port'], self.options['host']) + except: + etype, evalue, etb = sys.exc_info() + logger.log("Could not start webserver on %s. Excpeption: %s, Error: %s" % (self.options['port'], etype, evalue), logger.ERROR) + return - logger.log(u"Starting SickRage on " + protocol + "://" + str(options['host']) + ":" + str( - options['port']) + "/") + try: + self.io_loop.start() + self.io_loop.close(True) - server.listen(options['port'], options['host']) + # stop all tasks + sickbeard.halt() -def shutdown(): + # save all shows to DB + sickbeard.saveAll() - logger.log('Shutting down tornado IO loop') - try: - IOLoop.current().stop() - except RuntimeError: - pass - except: - logger.log('Failed shutting down tornado IO loop: %s' % traceback.format_exc(), logger.ERROR) \ No newline at end of file + except ValueError: + # Ignore errors like "ValueError: I/O operation on closed kqueue fd". These might be thrown during a reload. + pass + + def shutDown(self): + self.alive = False + if self.server: + self.server.stop() + self.io_loop.stop() \ No newline at end of file