From 1fc909299d80d8dbfb54cbcaeffd5d1df240ec8f Mon Sep 17 00:00:00 2001 From: echel0n Date: Tue, 1 Jul 2014 06:08:10 -0700 Subject: [PATCH] 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