From 12ac388dc283af4d55497221ff8d3a8ce6906455 Mon Sep 17 00:00:00 2001 From: echel0n Date: Wed, 2 Jul 2014 11:51:14 -0700 Subject: [PATCH] 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