#!/usr/bin/python -OO # Copyright 2008 The SABnzbd-Team # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License V2 # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import sys if sys.hexversion < 0x020403F0: print "Sorry, requires Python 2.4.3 or higher." exit(1) import logging import logging.handlers import os import getopt import signal import re import glob import socket if os.name=='nt': import platform try: import Cheetah if Cheetah.Version[0] != '2': raise ValueError except: print "Sorry, requires Python module Cheetah 2.0rc7 or higher." exit(1) import cherrypy import sabnzbd from sabnzbd.utils.configobj import ConfigObj, ConfigObjError from sabnzbd.__init__ import check_setting_str, check_setting_int, dir_setup from sabnzbd.interface import * from sabnzbd.constants import * from sabnzbd.newsunpack import find_programs from sabnzbd.misc import Get_User_ShellFolders, save_configfile, launch_a_browser, from_units, \ check_latest_version, Panic_Templ, Panic_Port, Panic_FWall, Panic, ExitSab, \ decodePassword, Notify, SplitHost, ConvertVersion from threading import Thread cfg = {} #------------------------------------------------------------------------------ signal.signal(signal.SIGINT, sabnzbd.sig_handler) signal.signal(signal.SIGTERM, sabnzbd.sig_handler) try: import win32api win32api.SetConsoleCtrlHandler(sabnzbd.sig_handler, True) except ImportError: if os.name == 'nt': print "Sorry, requires Python module PyWin32." exit(1) #------------------------------------------------------------------------------ class guiHandler(logging.Handler): """ Logging handler collects the last warnings/errors/exceptions to be displayed in the web-gui """ def __init__(self, size): """ Initializes the handler """ logging.Handler.__init__(self) self.size = size self.store = [] def emit(self, record): """ Emit a record by adding it to our private queue """ if len(self.store) >= self.size: # Loose the oldest record self.store.pop(0) self.store.append(self.format(record)) def clear(self): self.store = [] def count(self): return len(self.store) def last(self): if self.store: return self.store[len(self.store)-1] else: return "" def content(self): """ Return an array with last records """ return self.store #------------------------------------------------------------------------------ def print_help(): print print "Usage: %s [-f ] " % sabnzbd.MY_NAME print print "Options marked [*] are stored in the config file" print print "Options:" print " -f --config-file Location of config file" print " -s --server Listen on server:port [*]" print " -t --templates Template directory [*]" print " -2 --template2 Secondary template dir [*]" print print " -l --logging <0..2> Set logging level (0= least, 2= most) [*]" print " -w --weblogging <0..1> Set cherrypy logging (0= off, 1= on) [*]" print print " -b --browser <0..1> Auto browser launch (0= off, 1= on) [*]" if os.name != 'nt': print " -d --daemon Fork daemon process" print " --permissions Set the chmod mode (e.g. o=rwx,g=rwx) [*]" else: print " -d --daemon Use when run as a service" print print " --force Discard web-port timeout (see Wiki!)" print " -h --help Print this message" print " -v --version Print version information" print " -c --clean Remove queue, cache and logs" print " -p --pause Start in paused mode" def print_version(): print """ %s-%s Copyright (C) 2008, The SABnzbd-Team SABnzbd comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. It is licensed under the GNU GENERAL PUBLIC LICENSE Version 2. """ % (sabnzbd.MY_NAME, sabnzbd.__version__) def daemonize(): try: pid = os.fork() if pid > 0: sys.exit(0) except OSError: print "fork() failed" sys.exit(1) os.chdir(sabnzbd.DIR_PROG) 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) dev_null = file('/dev/null', 'r') os.dup2(dev_null.fileno(), sys.stdin.fileno()) def Bail_Out(browserhost, cherryport): """Abort program because of CherryPy troubles """ logging.error("Failed to start web-interface") Panic_Port(browserhost, cherryport) sabnzbd.halt() ExitSab(2) def Web_Template(key, defweb, wdir): """ Determine a correct web template set, return full template path """ if wdir == None: try: wdir = cfg['misc'][key] except: wdir = '' if not wdir: wdir = defweb cfg['misc'][key] = wdir if not wdir: # No default value defined, accept empty path return '' full_dir = real_path(sabnzbd.DIR_INTERFACES, wdir) full_main = real_path(full_dir, DEF_MAIN_TMPL) logging.info("Web dir is %s", full_dir) if not os.path.exists(full_main): logging.warning('Cannot find web template: %s, trying standard template', full_main) full_dir = real_path(sabnzbd.DIR_INTERFACES, DEF_STDINTF) full_main = real_path(full_dir, DEF_MAIN_TMPL) if not os.path.exists(full_main): logging.exception('Cannot find standard template: %s', full_dir) Panic_Templ(full_dir) ExitSab(1) return real_path(full_dir, "templates") def CheckColor(color, web_dir): """ Check existence of color-scheme """ if color and os.path.exists(os.path.join(web_dir,'static/stylesheets/colorschemes/'+color+'.css')): return color else: return '' def GetProfileInfo(vista): """ Get the default data locations """ ok = False if sabnzbd.DAEMON: # In daemon mode, do not try to access the user profile # just assume that everything defaults to the program dir sabnzbd.DIR_APPDATA = sabnzbd.DIR_PROG sabnzbd.DIR_LCLDATA = sabnzbd.DIR_PROG sabnzbd.DIR_HOME = sabnzbd.DIR_PROG if os.name == 'nt': # Ignore Win23 "logoff" signal # This should work, but it doesn't # Instead the signal_handler will ignore the "logoff" signal signal.signal(5, signal.SIG_IGN) ok = True elif os.name == 'nt': specials = Get_User_ShellFolders() try: sabnzbd.DIR_APPDATA = '%s\\%s' % (specials['AppData'], DEF_WORKDIR) sabnzbd.DIR_LCLDATA = '%s\\%s' % (specials['Local AppData'], DEF_WORKDIR) sabnzbd.DIR_HOME = specials['Personal'] ok = True except: try: if vista: root = os.environ['AppData'] user = os.environ['USERPROFILE'] sabnzbd.DIR_APPDATA = '%s\\%s' % (root.replace('\\Roaming', '\\Local'), DEF_WORKDIR) sabnzbd.DIR_HOME = '%s\\Documents' % user else: root = os.environ['USERPROFILE'] sabnzbd.DIR_APPDATA = '%s\\%s' % (root, DEF_WORKDIR) sabnzbd.DIR_HOME = root try: # Conversion to 8bit ASCII required for CherryPy sabnzbd.DIR_APPDATA = sabnzbd.DIR_APPDATA.encode('latin-1') sabnzbd.DIR_HOME = sabnzbd.DIR_HOME.encode('latin-1') ok = True except: # If unconvertible characters exist, use MSDOS name try: sabnzbd.DIR_APPDATA = win32api.GetShortPathName(sabnzbd.DIR_APPDATA) sabnzbd.DIR_HOME = win32api.GetShortPathName(sabnzbd.DIR_HOME) ok = True except: pass sabnzbd.DIR_LCLDATA = sabnzbd.DIR_APPDATA except: pass else: # Unix/Linux/OSX sabnzbd.DIR_APPDATA = '%s/.%s' % (os.environ['HOME'], DEF_WORKDIR) sabnzbd.DIR_LCLDATA = sabnzbd.DIR_APPDATA sabnzbd.DIR_HOME = os.environ['HOME'] ok = True if not ok: Panic("Cannot access the user profile.", "Please start with sabnzbd.ini file in another location") ExitSab(2) def main(): global cfg sabnzbd.MY_FULLNAME = os.path.normpath(os.path.abspath(sys.argv[0])) sabnzbd.MY_NAME = os.path.basename(sabnzbd.MY_FULLNAME) sabnzbd.DIR_PROG = os.path.dirname(sabnzbd.MY_FULLNAME) sabnzbd.DIR_INTERFACES = real_path(sabnzbd.DIR_PROG, DEF_INTERFACES) # Need console logging for SABnzbd.py and SABnzbd-console.exe consoleLogging = (not hasattr(sys, "frozen")) or (sabnzbd.MY_NAME.lower().find('-console') > 0) LOGLEVELS = [ logging.WARNING, logging.INFO, logging.DEBUG ] # Setup primary logging to prevent default console logging gui_log = guiHandler(MAX_WARNINGS) gui_log.setLevel(logging.WARNING) format_gui = '%(asctime)s\n%(levelname)s\n%(message)s' gui_log.setFormatter(logging.Formatter(format_gui)) sabnzbd.GUIHANDLER = gui_log # Create logger logger = logging.getLogger('') logger.setLevel(logging.WARNING) logger.addHandler(gui_log) try: opts, args = getopt.getopt(sys.argv[1:], "phdvncu:w:l:s:f:t:b:2:", ['pause', 'help', 'daemon', 'nobrowser', 'clean', 'logging=', \ 'weblogging=', 'umask=', 'server=', 'templates', 'permissions=', \ 'template2', 'browser=', 'config-file=', 'delay=', 'force', 'version']) except getopt.GetoptError: print_help() ExitSab(2) fork = False pause = False f = None cherryhost = None cherryport = None cherrypylogging = None sabnzbd.AUTOBROWSER = None clean_up = False logging_level = None umask = None web_dir = None web_dir2 = None delay = 0.0 vista = False vista64 = False force_web = False for o, a in opts: if (o in ('-d', '--daemon')): if os.name != 'nt': fork = True sabnzbd.AUTOBROWSER = 0 sabnzbd.DAEMON = True consoleLogging = False if o in ('-h', '--help'): print_help() ExitSab(0) if o in ('-f', '--config-file'): f = a if o in ('-t', '--templates'): web_dir = a if o in ('-2', '--template2'): web_dir2 = a if o in ('-s', '--server'): (cherryhost, cherryport) = SplitHost(a) if o in ('-n', '--nobrowser'): sabnzbd.AUTOBROWSER = 0 if o in ('-b', '--browser'): try: sabnzbd.AUTOBROWSER = int(a) except: sabnzbd.AUTOBROWSER = 1 if o in ('-c', '--clean'): clean_up= True if o in ('-w', '--weblogging'): try: cherrypylogging = int(a) except: cherrypylogging = -1 if cherrypylogging < 0 or cherrypylogging > 1: print_help() ExitSab(1) if o in ('-l', '--logging'): try: logging_level = int(a) except: logging_level = -1 if logging_level < 0 or logging_level > 2: print_help() ExitSab(1) if o in ('--permissions'): umask = a if o in ('-v', '--version'): print_version() ExitSab(0) if o in ('-p', '--pause'): pause = True #if o in ('--delay'): # For debugging of memory leak only!! #try: # delay = float(a) #except: # pass if o in ('--force'): force_web = True # Detect Vista or higher if os.name == 'nt': if platform.platform().find('Windows-32bit') >= 0: vista = True vista64 = 'ProgramFiles(x86)' in os.environ if f: # INI file given, simplest case f = os.path.normpath(os.path.abspath(f)) else: # No ini file given, need profile data GetProfileInfo(vista) # Find out where INI file is f = os.path.abspath(sabnzbd.DIR_PROG + '/' + DEF_INI_FILE) if not os.path.exists(f): f = os.path.abspath(sabnzbd.DIR_LCLDATA + '/' + DEF_INI_FILE) # If INI file at non-std location, then use program dir as $HOME if sabnzbd.DIR_LCLDATA != os.path.dirname(f): sabnzbd.DIR_HOME = os.path.dirname(f) # All system data dirs are relative to the place we found the INI file sabnzbd.DIR_LCLDATA = os.path.dirname(f) if not os.path.exists(f): # No file found, create default INI file try: if not os.path.exists(sabnzbd.DIR_LCLDATA): os.makedirs(sabnzbd.DIR_LCLDATA) fp = open(f, "w") fp.write("__version__=%s\n[misc]\n[logging]\n" % CONFIG_VERSION) fp.close() except: Panic('Cannot create file "%s".' % f, 'Check specified INI file location.') ExitSab(1) try: cfg = ConfigObj(f) try: my_version = cfg['__version__'] except: my_version = CONFIG_VERSION cfg['__version__'] = my_version except ConfigObjError, strerror: Panic('"%s" is not a valid configuration file.' % f, \ 'Specify a correct file or delete this file.') ExitSab(1) if cherrypylogging == None: cherrypylogging = bool(check_setting_int(cfg, 'logging', 'enable_cherrypy_logging', 1)) else: cfg['logging']['enable_cherrypy_logging'] = cherrypylogging if logging_level == None: logging_level = check_setting_int(cfg, 'logging', 'log_level', DEF_LOGLEVEL) if logging_level > 2: logging_level = 2 else: cfg['logging']['log_level'] = logging_level ver, testRelease = ConvertVersion(sabnzbd.__version__) if testRelease: logging_level = 2 cherrypylogging = True my_logdir = dir_setup(cfg, 'log_dir', sabnzbd.DIR_LCLDATA, DEF_LOG_DIR) if fork and not my_logdir: print "Error:" print "I refuse to fork without a log directory!" sys.exit() logdir = "" logdir = dir_setup(cfg, 'log_dir', sabnzbd.DIR_LCLDATA, DEF_LOG_DIR) if clean_up: xlist= glob.glob(logdir + '/*') for x in xlist: if x.find(RSS_FILE_NAME) < 0: os.remove(x) try: sabnzbd.LOGFILE = os.path.join(logdir, DEF_LOG_FILE) logsize = check_setting_str(cfg, 'logging', 'max_log_size', '5M') logsize = int(from_units(logsize)) rollover_log = logging.handlers.RotatingFileHandler(\ sabnzbd.LOGFILE, 'a+', logsize, check_setting_int(cfg, 'logging', 'log_backups', 5)) rollover_log.setLevel(LOGLEVELS[logging_level]) format = '%(asctime)s::%(levelname)s::%(message)s' rollover_log.setFormatter(logging.Formatter(format)) sabnzbd.LOGHANDLER = rollover_log logger.addHandler(rollover_log) logger.setLevel(LOGLEVELS[logging_level]) except IOError: print "Error:" print "Can't write to logfile" ExitSab(2) if fork: try: sys.stderr.fileno sys.stdout.fileno my_logpath = dir_setup(cfg, 'log_dir', sabnzbd.DIR_LCLDATA, DEF_LOG_DIR) ol_path = os.path.join(my_logpath, DEF_LOG_ERRFILE) out_log = file(ol_path, 'a+', 0) sys.stderr.flush() sys.stdout.flush() os.dup2(out_log.fileno(), sys.stderr.fileno()) os.dup2(out_log.fileno(), sys.stdout.fileno()) except AttributeError: pass else: try: sys.stderr.fileno sys.stdout.fileno if consoleLogging: console = logging.StreamHandler() console.setLevel(LOGLEVELS[logging_level]) console.setFormatter(logging.Formatter(format)) logger.addHandler(console) except AttributeError: pass logging.info('--------------------------------') logging.info('%s-%s (rev=%s)', sabnzbd.MY_NAME, sabnzbd.__version__, sabnzbd.__baseline__) if os.name == 'nt': suffix = '' if vista: suffix = ' (=Vista)' if vista64: suffix = ' (=Vista64)' logging.info('Platform=%s%s Class=%s', platform.platform(), suffix, os.name) else: logging.info('Platform = %s', os.name) logging.info('Python-version = %s', sys.version) if testRelease: logging.info('Test release, setting maximum logging levels') if sabnzbd.AUTOBROWSER == None: sabnzbd.AUTOBROWSER = bool(check_setting_int(cfg, 'misc', 'auto_browser', 1)) else: cfg['misc']['auto_browser'] = sabnzbd.AUTOBROWSER if umask == None: umask = check_setting_str(cfg, 'misc', 'permissions', '') if umask: cfg['misc']['permissions'] = umask sabnzbd.DEBUG_DELAY = delay sabnzbd.CFG = cfg init_ok = sabnzbd.initialize(pause, clean_up, evalSched=True) if not init_ok: logging.error('Initializing %s-%s failed, aborting', sabnzbd.MY_NAME, sabnzbd.__version__) ExitSab(2) find_programs(sabnzbd.DIR_PROG) if sabnzbd.decoder.HAVE_YENC: logging.info("_yenc module... found!") else: if hasattr(sys, "frozen"): logging.warning("_yenc module... NOT found!") else: logging.info("_yenc module... NOT found!") if sabnzbd.nzbstuff.HAVE_CELEMENTTREE: logging.info("celementtree module... found!") else: logging.info("celementtree module... NOT found!") if sabnzbd.newsunpack.PAR2_COMMAND: logging.info("par2 binary... found (%s)", sabnzbd.newsunpack.PAR2_COMMAND) else: logging.error("par2 binary... NOT found!") if sabnzbd.newsunpack.RAR_COMMAND: logging.info("rar binary... found (%s)", sabnzbd.newsunpack.RAR_COMMAND) else: logging.error("rar binary... NOT found") if sabnzbd.newsunpack.ZIP_COMMAND: logging.info("unzip binary... found (%s)", sabnzbd.newsunpack.ZIP_COMMAND) else: logging.error("unzip binary... NOT found!") if sabnzbd.newswrapper.HAVE_SSL: logging.info("pyOpenSSL... found (%s)", sabnzbd.newswrapper.HAVE_SSL) else: logging.info("pyOpenSSL... NOT found - try apt-get install python-pyopenssl (SSL is optional)") if cherryhost == None: cherryhost = check_setting_str(cfg, 'misc','host', DEF_HOST) else: cfg['misc']['host'] = cherryhost # Get IP address, but discard APIPA/IPV6 # If only APIPA's or IPV6 are found, fall back to localhost ipv4 = ipv6 = False localhost = hostip = 'localhost' try: info = socket.getaddrinfo(socket.gethostname(), None) except: # Hostname does not resolve, use 0.0.0.0 cherryhost = '0.0.0.0' info = socket.getaddrinfo(localhost, None) for item in info: ip = item[4][0] if ip.find('169.254.') == 0: pass # Is an APIPA elif ip.find(':') >= 0: ipv6 = True elif ip.find('.') >= 0: ipv4 = True hostip = ip break if cherryhost == '': if ipv6 and ipv4: # To protect Firefox users, use numeric IP cherryhost = hostip browserhost = hostip else: cherryhost = socket.gethostname() browserhost = cherryhost elif cherryhost == '0.0.0.0': # Just take the gamble for this cherryhost = '' browserhost = localhost elif cherryhost.find('[') >= 0 or cherryhost.find(':') >= 0: # IPV6 browserhost = cherryhost elif cherryhost.replace('.', '').isdigit(): # IPV4 numerical browserhost = cherryhost elif cherryhost == localhost: cherryhost = localhost browserhost = localhost else: # If on Vista and/or APIPA, use numerical IP, to help FireFoxers if ipv6 and ipv4: cherryhost = hostip browserhost = cherryhost # Some systems don't like brackets in numerical ipv6 if cherryhost.find('[') >= 0: try: info = socket.getaddrinfo(cherryhost, None) except: cherryhost = cherryhost.strip('[]') if ipv6 and ipv4 and \ (browserhost not in ('localhost', '127.0.0.1', '[::1]', '::1')): sabnzbd.AMBI_LOCALHOST = True logging.info("IPV6 has priority on this system, potential Firefox issue") if ipv6 and ipv4 and cherryhost == '' and os.name == 'nt': logging.warning("Please be aware the 0.0.0.0 hostname will need an IPv6 address for external access") if cherryport == None: if os.name == 'nt': defport = DEF_PORT_WIN else: defport = DEF_PORT_UNIX cherryport= check_setting_int(cfg, 'misc', 'port', defport) else: cfg['misc']['port'] = cherryport log_dir = dir_setup(cfg, 'log_dir', sabnzbd.DIR_LCLDATA, DEF_LOG_DIR) os.chdir(sabnzbd.DIR_PROG) web_dir = Web_Template('web_dir', DEF_STDINTF, web_dir) web_dir2 = Web_Template('web_dir2', '', web_dir2) sabnzbd.WEB_DIR = web_dir sabnzbd.WEB_DIR2 = web_dir2 sabnzbd.WEB_COLOR = CheckColor(sabnzbd.WEB_COLOR, web_dir) sabnzbd.WEB_COLOR2 = CheckColor(sabnzbd.WEB_COLOR2, web_dir2) cfg['misc']['web_color'] = sabnzbd.WEB_COLOR cfg['misc']['web_color2'] = sabnzbd.WEB_COLOR2 sabnzbd.interface.USERNAME = check_setting_str(cfg, 'misc', 'username', '') sabnzbd.interface.PASSWORD = decodePassword(check_setting_str(cfg, 'misc', 'password', '', False), 'web') if fork and os.name != 'nt': daemonize() # Save the INI file save_configfile(cfg) logging.info('Starting %s-%s', sabnzbd.MY_NAME, sabnzbd.__version__) try: sabnzbd.start() except: logging.exception("Failed to start %s-%s", sabnzbd.MY_NAME, sabnzbd.__version__) sabnzbd.halt() cherrylogtoscreen = False sabnzbd.WEBLOGFILE = None if cherrypylogging: if log_dir: sabnzbd.WEBLOGFILE = os.path.join(log_dir, DEF_LOG_CHERRY); if not fork: try: sys.stderr.fileno sys.stdout.fileno cherrylogtoscreen = True except: cherrylogtoscreen = False cherrypy.tree.mount(LoginPage(web_dir, '/sabnzbd/', web_dir2, '/sabnzbd/m/'), '/') cherrypy.config.update(updateMap={'server.environment': 'production', 'server.socketHost': cherryhost, 'server.socketPort': cherryport, 'server.logToScreen': cherrylogtoscreen, 'server.logFile': sabnzbd.WEBLOGFILE, 'sessionFilter.on': True, 'server.show_tracebacks': True, '/sabnzbd': {'streamResponse': True}, '/sabnzbd/static': {'staticFilter.on': True, 'staticFilter.dir': os.path.join(web_dir, 'static')}, '/sabnzbd/m': {'streamResponse': True}, '/sabnzbd/m/static': {'staticFilter.on': True, 'staticFilter.dir': os.path.join(web_dir2, 'static')} }) logging.info('Starting web-interface on %s:%s', cherryhost, cherryport) sabnzbd.LOGLEVEL = logging_level try: cherrypy.server.start(init_only=True) cherrypy.server.wait() except cherrypy.NotReady, error: if str(error) == 'Port not bound.': if not force_web: Panic_FWall(vista) sabnzbd.halt() ExitSab(2) else: Bail_Out(browserhost, cherryport) except: Bail_Out(browserhost, cherryport) launch_a_browser("http://%s:%s/sabnzbd" % (browserhost, cherryport)) Notify("SAB_Launched", None) # Now's the time to check for a new version if sabnzbd.VERSION_CHECK: check_latest_version() # Have to keep this running, otherwise logging will terminate while cherrypy.server.ready: if (not testRelease) and sabnzbd.LOGLEVEL != logging_level: logging_level = sabnzbd.LOGLEVEL logger.setLevel(LOGLEVELS[logging_level]) time.sleep(3) Notify("SAB_Shutdown", None) if __name__ == '__main__': main()