Fixed start/restart/shutdown issues including any issues with daemonizing.

This commit is contained in:
echel0n 2014-07-01 06:08:10 -07:00
parent 130daf7d0a
commit 1fc909299d
6 changed files with 359 additions and 165 deletions

View file

@ -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...")
logger.log("Goodbye ...")

View file

@ -1,28 +1,53 @@
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.
// 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.
@ -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();
});

216
lib/daemon.py Normal file
View file

@ -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 <david@boxedice.com>)
- 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 <david@boxedice.com>)
- Fixed problem with daemon exiting on Python 2.4
(before SystemExit was part of the Exception base)
13th Aug 2010 (David Mytton <david@boxedice.com>
- 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().
"""

View file

@ -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():

View file

@ -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):

View file

@ -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)
logger.log('Failed shutting down tornado IO loop: %s' % traceback.format_exc(), logger.ERROR)