You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1735 lines
62 KiB

#!/usr/bin/python -OO
# Copyright 2008-2017 The SABnzbd-Team <team@sabnzbd.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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.version_info[:2] < (2, 7) or sys.version_info[:2] >= (3, 0):
print "Sorry, requires Python 2.7."
sys.exit(1)
# Make sure UTF-8 is default 8bit encoding
if not hasattr(sys, "setdefaultencoding"):
reload(sys)
try:
sys.setdefaultencoding('utf-8')
except:
print 'Sorry, you MUST add the SABnzbd folder to the PYTHONPATH environment variable'
print 'or find another way to force Python to use UTF-8 for text encoding.'
sys.exit(1)
import logging
import logging.handlers
import os
import getopt
import signal
import socket
import platform
import ssl
import time
import re
try:
import Cheetah
if Cheetah.Version[0] != '2':
raise ValueError
except ValueError:
print "Sorry, requires Python module Cheetah 2.0rc7 or higher."
sys.exit(1)
except:
print "The Python module Cheetah is required"
sys.exit(1)
import cherrypy
if [int(n) for n in cherrypy.__version__.split('.')] < [8, 1, 2]:
print 'Sorry, requires Python module Cherrypy 8.1.2+ (use the included version)'
sys.exit(1)
SQLITE_DLL = True
try:
from sqlite3 import version as sqlite3_version
except:
try:
from pysqlite2.dbapi2 import version as sqlite3_version
except:
if os.name != 'nt':
print "Sorry, requires Python module sqlite3"
print "Try: apt-get install python-pysqlite2"
sys.exit(1)
else:
SQLITE_DLL = False
import locale
import __builtin__
try:
locale.setlocale(locale.LC_ALL, "")
__builtin__.__dict__['codepage'] = locale.getlocale()[1] or 'cp1252'
except:
# Work-around for Python-ports with bad "locale" support
__builtin__.__dict__['codepage'] = 'cp1252'
import sabnzbd
import sabnzbd.lang
import sabnzbd.interface
from sabnzbd.constants import *
import sabnzbd.newsunpack
from sabnzbd.misc import real_path, \
check_latest_version, exit_sab, \
split_host, get_ext, create_https_certificates, \
windows_variant, ip_extract, set_serv_parms, get_serv_parms, globber_full
from sabnzbd.panic import panic_tmpl, panic_port, panic_host, \
panic_sqlite, panic, launch_a_browser
import sabnzbd.scheduler as scheduler
import sabnzbd.config as config
import sabnzbd.cfg
import sabnzbd.downloader
from sabnzbd.encoding import unicoder, deunicode
import sabnzbd.notifier as notifier
import sabnzbd.zconfig
from threading import Thread
LOG_FLAG = False # Global for this module, signaling loglevel change
try:
import win32api
import win32serviceutil
import win32evtlogutil
import win32event
import win32service
import pywintypes
win32api.SetConsoleCtrlHandler(sabnzbd.sig_handler, True)
from util.mailslot import MailSlot
from util.apireg import get_connection_info, set_connection_info, del_connection_info
except ImportError:
class MailSlot:
pass
if sabnzbd.WIN32:
print "Sorry, requires Python module PyWin32."
sys.exit(1)
def guard_loglevel():
""" Callback function for guarding loglevel """
global LOG_FLAG
LOG_FLAG = True
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 record.levelname == 'WARNING':
sabnzbd.LAST_WARNING = record.msg % record.args
else:
sabnzbd.LAST_ERROR = record.msg % record.args
if len(self.store) >= self.size:
# Loose the oldest record
self.store.pop(0)
try:
self.store.append(self.format(record))
except UnicodeDecodeError:
# Catch elusive Unicode conversion problems
pass
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 <configfile>] <other options>" % sabnzbd.MY_NAME
print
print "Options marked [*] are stored in the config file"
print
print "Options:"
print " -f --config-file <ini> Location of config file"
print " -s --server <srv:port> Listen on server:port [*]"
print " -t --templates <templ> Template directory [*]"
print
print " -l --logging <0..2> Set logging level (-1=off, 0= least, 2= most) [*]"
print " -w --weblogging Enable cherrypy access logging"
print
print " -b --browser <0..1> Auto browser launch (0= off, 1= on) [*]"
if sabnzbd.WIN32:
print " -d --daemon Use when run as a service"
else:
print " -d --daemon Fork daemon process"
print " --pid <path> Create a PID file in the given folder (full path)"
print " --pidfile <path> Create a PID file with the given name (full path)"
print
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"
print " --repair Add orphaned jobs from the incomplete folder to the queue"
print " --repair-all Try to reconstruct the queue from the incomplete folder"
print " with full data reconstruction"
print " --https <port> Port to use for HTTPS server"
print " --no-login Start with username and password reset"
print " --log-all Log all article handling (for developers)"
print " --console Force console logging for OSX app"
print " --new Run a new instance of SABnzbd"
print " --ipv6_hosting <0|1> Listen on IPv6 address [::1]"
def print_version():
print """
%s-%s
Copyright (C) 2008-2017, The SABnzbd-Team <team@sabnzbd.org>
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 or (at your option) any later version.
""" % (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, err=''):
""" Abort program because of CherryPy troubles """
logging.error(T('Failed to start web-interface') + ' : ' + str(err))
if not sabnzbd.DAEMON:
if '49' in err:
panic_host(browserhost, cherryport)
else:
panic_port(browserhost, cherryport)
sabnzbd.halt()
exit_sab(2)
def Web_Template(key, defweb, wdir):
""" Determine a correct web template set, return full template path """
if wdir is None:
try:
wdir = fix_webname(key())
except:
wdir = ''
if not wdir:
wdir = defweb
if key:
key.set(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):
# end temp fix
logging.warning(T('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_tmpl(full_dir)
exit_sab(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
elif color and os.path.exists(os.path.join(web_dir, 'static/stylesheets/colorschemes/' + color)):
return color
else:
return ''
def fix_webname(name):
if name:
name = deunicode(name)
xname = name.title()
else:
xname = ''
if xname in ('Default', ):
return 'Glitter'
elif xname in ('Glitter', 'Plush'):
return xname
elif xname in ('Smpl', 'Wizard'):
return name.lower()
elif xname in ('Config',):
return 'Glitter'
else:
return name
def GetProfileInfo(vista_plus):
""" 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 sabnzbd.WIN32:
# Ignore Win32 "logoff" signal
# This should work, but it doesn't
# Instead the signal_handler will ignore the "logoff" signal
# signal.signal(5, signal.SIG_IGN)
pass
ok = True
elif sabnzbd.WIN32:
try:
from win32com.shell import shell, shellcon
path = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, None, 0)
sabnzbd.DIR_APPDATA = os.path.join(path, DEF_WORKDIR)
path = shell.SHGetFolderPath(0, shellcon.CSIDL_LOCAL_APPDATA, None, 0)
sabnzbd.DIR_LCLDATA = os.path.join(path, DEF_WORKDIR)
sabnzbd.DIR_HOME = os.environ['USERPROFILE']
ok = True
except:
try:
if vista_plus:
root = os.environ['AppData']
user = os.environ['USERPROFILE']
sabnzbd.DIR_APPDATA = '%s\\%s' % (root.replace('\\Roaming', '\\Local'), DEF_WORKDIR)
sabnzbd.DIR_HOME = 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(codepage)
sabnzbd.DIR_HOME = sabnzbd.DIR_HOME.encode(codepage)
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
elif sabnzbd.DARWIN:
home = os.environ.get('HOME')
if home:
sabnzbd.DIR_APPDATA = '%s/Library/Application Support/SABnzbd' % home
sabnzbd.DIR_LCLDATA = sabnzbd.DIR_APPDATA
sabnzbd.DIR_HOME = home
ok = True
else:
# Unix/Linux
home = os.environ.get('HOME')
if home:
sabnzbd.DIR_APPDATA = '%s/.%s' % (home, DEF_WORKDIR)
sabnzbd.DIR_LCLDATA = sabnzbd.DIR_APPDATA
sabnzbd.DIR_HOME = home
ok = True
if not ok:
panic("Cannot access the user profile.",
"Please start with sabnzbd.ini file in another location")
exit_sab(2)
def print_modules():
""" Log all detected optional or external modules """
if sabnzbd.decoder.SABYENC_ENABLED:
# Yes, we have SABYenc, and it's the correct version, so it's enabled
logging.info("SABYenc module (v%s)... found!", sabnzbd.constants.SABYENC_VERSION_REQUIRED)
else:
# Something wrong with SABYenc, so let's determine and print what:
if sabnzbd.decoder.SABYENC_VERSION:
# We have a VERSION, thus a SABYenc module, but it's not the correct version
logging.warning(T("SABYenc disabled: no correct version found! (Found v%s, expecting v%s)") % (sabnzbd.decoder.SABYENC_VERSION, sabnzbd.constants.SABYENC_VERSION_REQUIRED))
else:
# No SABYenc module at all
logging.warning(T("SABYenc module... NOT found! Expecting v%s - https://sabnzbd.org/sabyenc") % sabnzbd.constants.SABYENC_VERSION_REQUIRED)
# No correct SABYenc version or no SABYenc at all, so now we care about old-yEnc
if sabnzbd.decoder.HAVE_YENC:
logging.info("_yenc module... found!")
else:
logging.error(T('_yenc module... NOT found!'))
if sabnzbd.HAVE_CRYPTOGRAPHY:
logging.info('Cryptography module (v%s)... found!', sabnzbd.HAVE_CRYPTOGRAPHY)
else:
logging.info('Cryptography module... NOT found!')
if sabnzbd.newsunpack.PAR2_COMMAND:
logging.info("par2 binary... found (%s)", sabnzbd.newsunpack.PAR2_COMMAND)
else:
logging.error('%s %s' % (T('par2 binary... NOT found!'), T('Verification and repair will not be possible.')))
if sabnzbd.newsunpack.MULTIPAR_COMMAND:
logging.info("MultiPar binary... found (%s)", sabnzbd.newsunpack.MULTIPAR_COMMAND)
elif sabnzbd.WIN32:
logging.error('%s %s' % (T('MultiPar binary... NOT found!'), T('Verification and repair will not be possible.')))
if sabnzbd.newsunpack.RAR_COMMAND:
logging.info("UNRAR binary... found (%s)", sabnzbd.newsunpack.RAR_COMMAND)
# Report problematic unrar
if sabnzbd.newsunpack.RAR_PROBLEM and not sabnzbd.cfg.ignore_wrong_unrar():
have_str = '%.2f' % (float(sabnzbd.newsunpack.RAR_VERSION) / 100)
want_str = '%.2f' % (float(sabnzbd.constants.REC_RAR_VERSION) / 100)
logging.warning(T('Your UNRAR version is %s, we recommend version %s or higher.<br />') % (have_str, want_str))
elif not (sabnzbd.WIN32 or sabnzbd.DARWIN):
logging.info('UNRAR binary version %.2f', (float(sabnzbd.newsunpack.RAR_VERSION) / 100))
else:
logging.error('%s %s' % (T('unrar binary... NOT found'), T('Downloads will not unpacked.')))
if sabnzbd.newsunpack.ZIP_COMMAND:
logging.info("unzip binary... found (%s)", sabnzbd.newsunpack.ZIP_COMMAND)
else:
logging.info(T('unzip binary... NOT found!'))
if sabnzbd.newsunpack.SEVEN_COMMAND:
logging.info("7za binary... found (%s)", sabnzbd.newsunpack.SEVEN_COMMAND)
else:
logging.info(T('7za binary... NOT found!'))
if not sabnzbd.WIN32:
if sabnzbd.newsunpack.NICE_COMMAND:
logging.info("nice binary... found (%s)", sabnzbd.newsunpack.NICE_COMMAND)
else:
logging.info("nice binary... NOT found!")
if sabnzbd.newsunpack.IONICE_COMMAND:
logging.info("ionice binary... found (%s)", sabnzbd.newsunpack.IONICE_COMMAND)
else:
logging.info("ionice binary... NOT found!")
def all_localhosts():
""" Return all unique values of localhost in order of preference """
ips = ['127.0.0.1']
try:
# Check whether IPv6 is available and enabled
info = socket.getaddrinfo('::1', None)
af, socktype, proto, _canonname, _sa = info[0]
s = socket.socket(af, socktype, proto)
s.close()
except socket.error:
return ips
try:
info = socket.getaddrinfo('localhost', None)
except:
# localhost does not resolve
return ips
ips = []
for item in info:
item = item[4][0]
# Avoid problems on strange Linux settings
if not isinstance(item, basestring):
continue
# Only return IPv6 when enabled
if item not in ips and ('::1' not in item or sabnzbd.cfg.ipv6_hosting()):
ips.append(item)
return ips
def check_resolve(host):
""" Return True if 'host' resolves """
try:
dummy = socket.getaddrinfo(host, None)
except:
# Does not resolve
return False
return True
def get_webhost(cherryhost, cherryport, https_port):
""" Determine the webhost address and port,
return (host, port, browserhost)
"""
if cherryhost == '0.0.0.0' and not check_resolve('127.0.0.1'):
cherryhost = ''
elif cherryhost == '::' and not check_resolve('::1'):
cherryhost = ''
if cherryhost is None:
cherryhost = deunicode(sabnzbd.cfg.cherryhost())
else:
sabnzbd.cfg.cherryhost.set(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
try:
# Valid user defined name?
info = socket.getaddrinfo(cherryhost, None)
except:
if cherryhost not in ('localhost', '127.0.0.1', '::1'):
cherryhost = '0.0.0.0'
try:
info = socket.getaddrinfo(localhost, None)
except:
info = socket.getaddrinfo('127.0.0.1', None)
localhost = '127.0.0.1'
for item in info:
ip = str(item[4][0])
if ip.startswith('169.254.'):
pass # Automatic Private IP Addressing (APIPA)
elif ':' in ip:
ipv6 = True
elif '.' in ip and not ipv4:
ipv4 = True
hostip = ip
# A blank host will use the local ip address
if cherryhost == '':
if ipv6 and ipv4:
# To protect Firefox users, use numeric IP
cherryhost = hostip
browserhost = hostip
else:
cherryhost = socket.gethostname()
browserhost = cherryhost
# 0.0.0.0 will listen on all ipv4 interfaces (no ipv6 addresses)
elif cherryhost == '0.0.0.0':
# Just take the gamble for this
cherryhost = '0.0.0.0'
browserhost = localhost
# :: will listen on all ipv6 interfaces (no ipv4 addresses)
elif cherryhost in ('::', '[::]'):
cherryhost = cherryhost.strip('[').strip(']')
# Assume '::1' == 'localhost'
browserhost = localhost
# IPV6 address
elif '[' in cherryhost or ':' in cherryhost:
browserhost = cherryhost
# IPV6 numeric address
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 sabnzbd.DARWIN:
cherryhost = cherryhost.strip('[]')
else:
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 sabnzbd.WIN32:
logging.warning(T('Please be aware the 0.0.0.0 hostname will need an IPv6 address for external access'))
if cherryhost == 'localhost' and not sabnzbd.WIN32 and not sabnzbd.DARWIN:
# On the Ubuntu family, localhost leads to problems for CherryPy
ips = ip_extract()
if '127.0.0.1' in ips and '::1' in ips:
cherryhost = '127.0.0.1'
if ips[0] != '127.0.0.1':
browserhost = '127.0.0.1'
# This is to please Chrome on OSX
if cherryhost == 'localhost' and sabnzbd.DARWIN:
cherryhost = '127.0.0.1'
browserhost = 'localhost'
if cherryport is None:
cherryport = sabnzbd.cfg.cherryport.get_int()
else:
sabnzbd.cfg.cherryport.set(str(cherryport))
if https_port is None:
https_port = sabnzbd.cfg.https_port.get_int()
else:
sabnzbd.cfg.https_port.set(str(https_port))
# if the https port was specified, assume they want HTTPS enabling also
sabnzbd.cfg.enable_https.set(True)
if cherryport == https_port and sabnzbd.cfg.enable_https():
sabnzbd.cfg.enable_https.set(False)
# Should have a translated message, but that's not available yet
logging.error(T('HTTP and HTTPS ports cannot be the same'))
return cherryhost, cherryport, browserhost, https_port
def attach_server(host, port, cert=None, key=None, chain=None):
""" Define and attach server, optionally HTTPS """
if sabnzbd.cfg.ipv6_hosting() or '::1' not in host:
http_server = cherrypy._cpserver.Server()
http_server.bind_addr = (host, port)
if cert and key:
http_server.ssl_module = 'builtin'
http_server.ssl_certificate = cert
http_server.ssl_private_key = key
http_server.ssl_certificate_chain = chain
http_server.subscribe()
def is_sabnzbd_running(url):
""" Return True when there's already a SABnzbd instance running. """
try:
url = '%s&mode=version' % (url)
# Do this without certificate verification, few installations will have that
prev = sabnzbd.set_https_verification(False)
ver = sabnzbd.newsunpack.get_from_url(url)
sabnzbd.set_https_verification(prev)
return (ver and (re.search(r'\d+\.\d+\.', ver) or ver.strip() == sabnzbd.__version__))
except:
return False
def find_free_port(host, currentport):
""" Return a free port, 0 when nothing is free """
n = 0
while n < 10 and currentport <= 49151:
try:
cherrypy.process.servers.check_port(host, currentport, timeout=0.025)
return currentport
except:
currentport += 5
n += 1
return 0
def check_for_sabnzbd(url, upload_nzbs, allow_browser=True):
""" Check for a running instance of sabnzbd on this port
allow_browser==True|None will launch the browser, False will not.
"""
if allow_browser is None:
allow_browser = True
if is_sabnzbd_running(url):
# Upload any specified nzb files to the running instance
if upload_nzbs:
from sabnzbd.utils.upload import upload_file
prev = sabnzbd.set_https_verification(0)
for f in upload_nzbs:
upload_file(url, f)
sabnzbd.set_https_verification(prev)
else:
# Launch the web browser and quit since sabnzbd is already running
# Trim away everything after the final slash in the URL
url = url[:url.rfind('/') + 1]
launch_a_browser(url, force=allow_browser)
exit_sab(0)
return True
return False
def evaluate_inipath(path):
""" Derive INI file path from a partial path.
Full file path: if file does not exist the name must contain a dot
but not a leading dot.
foldername is enough, the standard name will be appended.
"""
if sabnzbd.WIN32:
path = unicoder(path)
path = os.path.normpath(os.path.abspath(path))
inipath = os.path.join(path, DEF_INI_FILE)
if os.path.isdir(path):
return inipath
elif os.path.isfile(path) or os.path.isfile(path + '.bak'):
return path
else:
_dirpart, name = os.path.split(path)
if name.find('.') < 1:
return inipath
else:
return path
def commandline_handler(frozen=True):
""" Split win32-service commands are true parameters
Returns:
service, sab_opts, serv_opts, upload_nzbs
"""
service = ''
sab_opts = []
serv_opts = [os.path.normpath(os.path.abspath(sys.argv[0]))]
upload_nzbs = []
# OSX binary: get rid of the weird -psn_0_123456 parameter
for arg in sys.argv:
if arg.startswith('-psn_'):
sys.argv.remove(arg)
break
# Ugly hack to remove the extra "SABnzbd*" parameter the Windows binary
# gets when it's restarted
if len(sys.argv) > 1 and \
'sabnzbd' in sys.argv[1].lower() and \
not sys.argv[1].startswith('-'):
slice = 2
else:
slice = 1
# Prepend options from env-variable to options
info = os.environ.get('SABnzbd', '').split()
info.extend(sys.argv[slice:])
try:
opts, args = getopt.getopt(info, "phdvncwl:s:f:t:b:2:",
['pause', 'help', 'daemon', 'nobrowser', 'clean', 'logging=',
'weblogging', 'server=', 'templates', 'ipv6_hosting=',
'template2', 'browser=', 'config-file=', 'force',
'version', 'https=', 'autorestarted', 'repair', 'repair-all',
'log-all', 'no-login', 'pid=', 'new', 'console', 'pidfile=',
# Below Win32 Service options
'password=', 'username=', 'startup=', 'perfmonini=', 'perfmondll=',
'interactive', 'wait=',
])
except getopt.GetoptError:
print_help()
exit_sab(2)
# Check for Win32 service commands
if args and args[0] in ('install', 'update', 'remove', 'start', 'stop', 'restart', 'debug'):
service = args[0]
serv_opts.extend(args)
if not service:
# Get and remove any NZB file names
for entry in args:
if get_ext(entry) in ('.nzb', '.zip', '.rar', '.gz', '.bz2'):
upload_nzbs.append(os.path.abspath(entry))
for opt, arg in opts:
if opt in ('password', 'username', 'startup', 'perfmonini', 'perfmondll', 'interactive', 'wait'):
# Service option, just collect
if service:
serv_opts.append(opt)
if arg:
serv_opts.append(arg)
else:
if opt == '-f':
arg = os.path.normpath(os.path.abspath(arg))
sab_opts.append((opt, arg))
return service, sab_opts, serv_opts, upload_nzbs
def get_f_option(opts):
""" Return value of the -f option """
for opt, arg in opts:
if opt == '-f':
return arg
else:
return None
def main():
global LOG_FLAG
import sabnzbd # Due to ApplePython bug
autobrowser = None
autorestarted = False
sabnzbd.MY_FULLNAME = sys.argv[0]
sabnzbd.MY_NAME = os.path.basename(sabnzbd.MY_FULLNAME)
fork = False
pause = False
inifile = None
cherryhost = None
cherryport = None
https_port = None
cherrypylogging = None
clean_up = False
logging_level = None
web_dir = None
vista_plus = False
vista64 = False
repair = 0
api_url = None
no_login = False
sabnzbd.RESTART_ARGS = [sys.argv[0]]
pid_path = None
pid_file = None
new_instance = False
osx_console = False
ipv6_hosting = None
_service, sab_opts, _serv_opts, upload_nzbs = commandline_handler()
for opt, arg in sab_opts:
if opt == '--servicecall':
sabnzbd.MY_FULLNAME = arg
elif opt in ('-d', '--daemon'):
if not sabnzbd.WIN32:
fork = True
autobrowser = False
sabnzbd.DAEMON = True
sabnzbd.RESTART_ARGS.append(opt)
elif opt in ('-f', '--config-file'):
inifile = arg
sabnzbd.RESTART_ARGS.append(opt)
sabnzbd.RESTART_ARGS.append(arg)
elif opt in ('-h', '--help'):
print_help()
exit_sab(0)
elif opt in ('-t', '--templates'):
web_dir = arg
elif opt in ('-s', '--server'):
(cherryhost, cherryport) = split_host(arg)
elif opt in ('-n', '--nobrowser'):
autobrowser = False
elif opt in ('-b', '--browser'):
try:
autobrowser = bool(int(arg))
except:
autobrowser = True
elif opt in ('--autorestarted', ):
autorestarted = True
elif opt in ('-c', '--clean'):
clean_up = True
elif opt in ('-w', '--weblogging'):
cherrypylogging = True
elif opt in ('-l', '--logging'):
try:
logging_level = int(arg)
except:
logging_level = -2
if logging_level < -1 or logging_level > 2:
print_help()
exit_sab(1)
elif opt in ('-v', '--version'):
print_version()
exit_sab(0)
elif opt in ('-p', '--pause'):
pause = True
elif opt in ('--https',):
https_port = int(arg)
sabnzbd.RESTART_ARGS.append(opt)
sabnzbd.RESTART_ARGS.append(arg)
elif opt in ('--repair',):
repair = 1
pause = True
elif opt in ('--repair-all',):
repair = 2
pause = True
elif opt in ('--log-all',):
sabnzbd.LOG_ALL = True
elif opt in ('--no-login',):
no_login = True
elif opt in ('--pid',):
pid_path = arg
sabnzbd.RESTART_ARGS.append(opt)
sabnzbd.RESTART_ARGS.append(arg)
elif opt in ('--pidfile',):
pid_file = arg
sabnzbd.RESTART_ARGS.append(opt)
sabnzbd.RESTART_ARGS.append(arg)
elif opt in ('--new',):
new_instance = True
elif opt in ('--console',):
sabnzbd.RESTART_ARGS.append(opt)
osx_console = True
elif opt in ('--ipv6_hosting',):
ipv6_hosting = arg
sabnzbd.MY_FULLNAME = os.path.normpath(os.path.abspath(sabnzbd.MY_FULLNAME))
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)
sabnzbd.DIR_LANGUAGE = real_path(sabnzbd.DIR_PROG, DEF_LANGUAGE)
org_dir = os.getcwd()
if getattr(sys, 'frozen', None) == 'macosx_app':
# Correct path if frozen with py2app (OSX)
sabnzbd.MY_FULLNAME = sabnzbd.MY_FULLNAME.replace("/Resources/SABnzbd.py", "/MacOS/SABnzbd")
# Need console logging for SABnzbd.py and SABnzbd-console.exe
consoleLogging = (not hasattr(sys, "frozen")) or (sabnzbd.MY_NAME.lower().find('-console') > 0)
consoleLogging = consoleLogging and not sabnzbd.DAEMON
# No console logging needed for OSX app
noConsoleLoggingOSX = (not osx_console) and (sabnzbd.DIR_PROG.find('.app/Contents/Resources') > 0)
if noConsoleLoggingOSX:
consoleLogging = 1
LOGLEVELS = (logging.FATAL, 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)
# Detect Windows variant
if sabnzbd.WIN32:
vista_plus, vista64 = windows_variant()
sabnzbd.WIN64 = vista64
if not SQLITE_DLL:
panic_sqlite(sabnzbd.MY_FULLNAME)
exit_sab(2)
if inifile:
# INI file given, simplest case
inifile = evaluate_inipath(inifile)
else:
# No ini file given, need profile data
GetProfileInfo(vista_plus)
# Find out where INI file is
inifile = os.path.abspath(sabnzbd.DIR_LCLDATA + '/' + DEF_INI_FILE)
# If INI file at non-std location, then use INI location as $HOME
if sabnzbd.DIR_LCLDATA != os.path.dirname(inifile):
sabnzbd.DIR_HOME = os.path.dirname(inifile)
# All system data dirs are relative to the place we found the INI file
sabnzbd.DIR_LCLDATA = os.path.dirname(inifile)
if not os.path.exists(inifile) and not os.path.exists(inifile + '.bak') and not os.path.exists(sabnzbd.DIR_LCLDATA):
try:
os.makedirs(sabnzbd.DIR_LCLDATA)
except IOError:
panic('Cannot create folder "%s".' % sabnzbd.DIR_LCLDATA, 'Check specified INI file location.')
exit_sab(1)
sabnzbd.cfg.set_root_folders(sabnzbd.DIR_HOME, sabnzbd.DIR_LCLDATA)
res, msg = config.read_config(inifile)
if not res:
panic(msg, 'Specify a correct file or delete this file.')
exit_sab(1)
# Set root folders for HTTPS server file paths
sabnzbd.cfg.set_root_folders2()
if ipv6_hosting is not None:
sabnzbd.cfg.ipv6_hosting.set(ipv6_hosting)
# Determine web host address
cherryhost, cherryport, browserhost, https_port = get_webhost(cherryhost, cherryport, https_port)
enable_https = sabnzbd.cfg.enable_https()
# When this is a daemon, just check and bail out if port in use
if sabnzbd.DAEMON:
if enable_https and https_port:
try:
cherrypy.process.servers.check_port(cherryhost, https_port, timeout=0.05)
except IOError, error:
Bail_Out(browserhost, cherryport)
except:
Bail_Out(browserhost, cherryport, '49')
try:
cherrypy.process.servers.check_port(cherryhost, cherryport, timeout=0.05)
except IOError, error:
Bail_Out(browserhost, cherryport)
except:
Bail_Out(browserhost, cherryport, '49')
# Windows instance is reachable through registry
url = None
if sabnzbd.WIN32 and not new_instance:
url = get_connection_info()
if url and check_for_sabnzbd(url, upload_nzbs, autobrowser):
exit_sab(0)
# SSL
if enable_https:
port = https_port or cherryport
try:
cherrypy.process.servers.check_port(browserhost, port, timeout=0.05)
except IOError, error:
if str(error) == 'Port not bound.':
pass
else:
if not url:
url = 'https://%s:%s/sabnzbd/api?' % (browserhost, port)
if new_instance or not check_for_sabnzbd(url, upload_nzbs, autobrowser):
# Bail out if we have fixed our ports after first start-up
if sabnzbd.cfg.fixed_ports():
Bail_Out(browserhost, cherryport)
# Find free port to bind
newport = find_free_port(browserhost, port)
if newport > 0:
# Save the new port
if https_port:
https_port = newport
sabnzbd.cfg.https_port.set(newport)
else:
# In case HTTPS == HTTP port
cherryport = newport
sabnzbd.cfg.port.set(newport)
except:
# Something else wrong, probably badly specified host
Bail_Out(browserhost, cherryport, '49')
# NonSSL check if there's no HTTPS or we only use 1 port
if not (enable_https and not https_port):
try:
cherrypy.process.servers.check_port(browserhost, cherryport, timeout=0.05)
except IOError, error:
if str(error) == 'Port not bound.':
pass
else:
if not url:
url = 'http://%s:%s/sabnzbd/api?' % (browserhost, cherryport)
if new_instance or not check_for_sabnzbd(url, upload_nzbs, autobrowser):
# Bail out if we have fixed our ports after first start-up
if sabnzbd.cfg.fixed_ports():
Bail_Out(browserhost, cherryport)
# Find free port to bind
port = find_free_port(browserhost, cherryport)
if port > 0:
sabnzbd.cfg.cherryport.set(port)
cherryport = port
except:
# Something else wrong, probably badly specified host
Bail_Out(browserhost, cherryport, '49')
# We found a port, now we never check again
sabnzbd.cfg.fixed_ports.set(True)
if logging_level is None:
logging_level = sabnzbd.cfg.log_level()
else:
sabnzbd.cfg.log_level.set(logging_level)
logdir = sabnzbd.cfg.log_dir.get_path()
if fork and not logdir:
print "Error:"
print "I refuse to fork without a log directory!"
sys.exit(1)
if clean_up:
xlist = globber_full(logdir)
for x in xlist:
if RSS_FILE_NAME not in x:
try:
os.remove(x)
except:
pass
# Prevent the logger from raising exceptions
# primarily to reduce the fallout of Python issue 4749
logging.raiseExceptions = 0
sabnzbd.LOGFILE = os.path.join(logdir, DEF_LOG_FILE)
try:
rollover_log = logging.handlers.RotatingFileHandler(
sabnzbd.LOGFILE, 'a+',
sabnzbd.cfg.log_size.get_int(),
sabnzbd.cfg.log_backups())
logformat = '%(asctime)s::%(levelname)s::[%(module)s:%(lineno)d] %(message)s'
rollover_log.setFormatter(logging.Formatter(logformat))
sabnzbd.LOGHANDLER = rollover_log
logger.addHandler(rollover_log)
logger.setLevel(LOGLEVELS[logging_level + 1])
except IOError:
print "Error:"
print "Can't write to logfile"
exit_sab(2)
if fork:
try:
x = sys.stderr.fileno
x = sys.stdout.fileno
ol_path = os.path.join(logdir, 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:
x = sys.stderr.fileno
x = sys.stdout.fileno
if consoleLogging:
console = logging.StreamHandler()
console.setLevel(LOGLEVELS[logging_level + 1])
console.setFormatter(logging.Formatter(logformat))
logger.addHandler(console)
if noConsoleLoggingOSX:
logging.info('Console logging for OSX App disabled')
so = file('/dev/null', 'a+')
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(so.fileno(), sys.stderr.fileno())
except AttributeError:
pass
logging.info('--------------------------------')
logging.info('%s-%s (rev=%s)', sabnzbd.MY_NAME, sabnzbd.__version__, sabnzbd.__baseline__)
logging.info('Full executable path = %s', sabnzbd.MY_FULLNAME)
if sabnzbd.WIN32:
suffix = ''
if vista_plus:
suffix = ' (=Vista+)'
if vista64:
suffix = ' (=Vista+ x64)'
try:
logging.info('Platform=%s%s Class=%s', platform.platform(), suffix, os.name)
except:
logging.info('Platform=%s <unknown> Class=%s', suffix, os.name)
else:
logging.info('Platform = %s', os.name)
logging.info('Python-version = %s', sys.version)
logging.info('Arguments = %s', sabnzbd.CMDLINE)
# Find encoding; relevant for unrar activities
try:
preferredencoding = locale.getpreferredencoding()
logging.info('Preferred encoding = %s', preferredencoding)
except:
logging.info('Preferred encoding = ERROR')
preferredencoding = ''
# On Linux/FreeBSD/Unix "UTF-8" is strongly, strongly adviced:
if not sabnzbd.WIN32 and not sabnzbd.DARWIN and not ('utf' in preferredencoding.lower() and '8' in preferredencoding.lower()):
logging.warning(T("SABnzbd was started with encoding %s, this should be UTF-8. Expect problems with Unicoded file and directory names in downloads.") % preferredencoding)
# SSL Information
logging.info("SSL version %s", ssl.OPENSSL_VERSION)
# Load (extra) certificates in the binary distributions
if hasattr(sys, "frozen") and (sabnzbd.WIN32 or sabnzbd.DARWIN):
# The certifi package brings the latest certificates on build
# This will cause the create_default_context to load it automatically
os.environ["SSL_CERT_FILE"] = os.path.join(sabnzbd.DIR_PROG, 'cacert.pem')
logging.info('Loaded additional certificates from %s', os.environ["SSL_CERT_FILE"])
# Extra startup info
if sabnzbd.cfg.log_level() > 1:
# List the number of certificates available (can take up to 1.5 seconds)
ctx = ssl.create_default_context()
logging.debug('Available certificates: %s', repr(ctx.cert_store_stats()))
# Show IPv4/IPv6 address
from sabnzbd.getipaddress import localipv4, publicipv4, ipv6
mylocalipv4 = localipv4()
if mylocalipv4:
logging.debug('My local IPv4 address = %s', mylocalipv4)
else:
logging.debug('Could not determine my local IPv4 address')
mypublicipv4 = publicipv4()
if mypublicipv4:
logging.debug('My public IPv4 address = %s', mypublicipv4)
else:
logging.debug('Could not determine my public IPv4 address')
myipv6 = ipv6()
if myipv6:
logging.debug('My IPv6 address = %s', myipv6)
else:
logging.debug('Could not determine my IPv6 address')
# Measure and log system performance measured by pystone and - if possible - CPU model
from sabnzbd.utils.getperformance import getpystone, getcpu
pystoneperf = getpystone()
if pystoneperf:
logging.debug('CPU Pystone available performance is %s', pystoneperf)
else:
logging.debug('CPU Pystone available performance could not be calculated')
cpumodel = getcpu() # Linux only
if cpumodel:
logging.debug('CPU model name is %s', cpumodel)
logging.info('Read INI file %s', inifile)
if autobrowser is not None:
sabnzbd.cfg.autobrowser.set(autobrowser)
else:
autobrowser = sabnzbd.cfg.autobrowser()
if not sabnzbd.WIN_SERVICE and not getattr(sys, 'frozen', None) == 'macosx_app':
signal.signal(signal.SIGINT, sabnzbd.sig_handler)
signal.signal(signal.SIGTERM, sabnzbd.sig_handler)
sabnzbd.initialize(pause, clean_up, evalSched=True, repair=repair)
os.chdir(sabnzbd.DIR_PROG)
sabnzbd.WEB_DIR = Web_Template(sabnzbd.cfg.web_dir, DEF_STDINTF, fix_webname(web_dir))
sabnzbd.WEB_DIR_CONFIG = Web_Template(None, DEF_STDCONFIG, '')
sabnzbd.WIZARD_DIR = os.path.join(sabnzbd.DIR_INTERFACES, 'wizard')
sabnzbd.WEB_COLOR = CheckColor(sabnzbd.cfg.web_color(), sabnzbd.WEB_DIR)
sabnzbd.cfg.web_color.set(sabnzbd.WEB_COLOR)
if fork and not sabnzbd.WIN32:
daemonize()
# Save the INI file
config.save_config(force=True)
if sabnzbd.cfg.win_menu() and not sabnzbd.DAEMON:
if sabnzbd.WIN32:
import sabnzbd.sabtray
sabnzbd.WINTRAY = sabnzbd.sabtray.SABTrayThread()
elif sabnzbd.LINUX_POWER and os.environ.get('DISPLAY'):
try:
import gtk
import sabnzbd.sabtraylinux
sabnzbd.LINUXTRAY = sabnzbd.sabtraylinux.StatusIcon()
except:
logging.info("pygtk2 not found. No SysTray.")
# Find external programs
sabnzbd.newsunpack.find_programs(sabnzbd.DIR_PROG)
print_modules()
# HTTPS certificate generation
https_cert = sabnzbd.cfg.https_cert.get_path()
https_key = sabnzbd.cfg.https_key.get_path()
https_chain = sabnzbd.cfg.https_chain.get_path()
if not (sabnzbd.cfg.https_chain() and os.path.exists(https_chain)):
https_chain = None
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)):
create_https_certificates(https_cert, https_key)
if not (os.path.exists(https_cert) and os.path.exists(https_key)):
logging.warning(T('Disabled HTTPS because of missing CERT and KEY files'))
enable_https = False
# Starting of the webserver
# Determine if this system has multiple definitions for 'localhost'
hosts = all_localhosts()
multilocal = len(hosts) > 1 and cherryhost in ('localhost', '0.0.0.0')
# For 0.0.0.0 CherryPy will always pick IPv4, so make sure the secondary localhost is IPv6
if multilocal and cherryhost == '0.0.0.0' and hosts[1] == '127.0.0.1':
hosts[1] = '::1'
# The Windows binary requires numeric localhost as primary address
if cherryhost == 'localhost':
cherryhost = hosts[0]
if enable_https:
if https_port:
# Extra HTTP port for primary localhost
attach_server(cherryhost, cherryport)
if multilocal:
# Extra HTTP port for secondary localhost
attach_server(hosts[1], cherryport)
# Extra HTTPS port for secondary localhost
attach_server(hosts[1], https_port, https_cert, https_key, https_chain)
cherryport = https_port
elif multilocal:
# Extra HTTPS port for secondary localhost
attach_server(hosts[1], cherryport, https_cert, https_key, https_chain)
cherrypy.config.update({'server.ssl_module': 'builtin',
'server.ssl_certificate': https_cert,
'server.ssl_private_key': https_key,
'server.ssl_certificate_chain': https_chain})
elif multilocal:
# Extra HTTP port for secondary localhost
attach_server(hosts[1], cherryport)
if no_login:
sabnzbd.cfg.username.set('')
sabnzbd.cfg.password.set('')
mime_gzip = ('text/*',
'application/javascript',
'application/x-javascript',
'application/json',
'application/xml',
'application/vnd.ms-fontobject',
'application/font*',
'image/svg+xml'
)
cherrypy.config.update({'server.environment': 'production',
'server.socket_host': cherryhost,
'server.socket_port': cherryport,
'server.shutdown_timeout': 0,
'log.screen': False,
'engine.autoreload.on': False,
'tools.encode.on': True,
'tools.gzip.on': True,
'tools.gzip.mime_types': mime_gzip,
'request.show_tracebacks': True,
'error_page.401': sabnzbd.panic.error_page_401,
'error_page.404': sabnzbd.panic.error_page_404
})
# Do we want CherryPy Logging? Cannot be done via the config
if cherrypylogging:
sabnzbd.WEBLOGFILE = os.path.join(logdir, DEF_LOG_CHERRY)
cherrypy.log.screen = True
cherrypy.log.access_log.propagate = True
cherrypy.log.access_file = str(sabnzbd.WEBLOGFILE)
else:
cherrypy.log.access_log.propagate = False
# Force mimetypes (OS might overwrite them)
forced_mime_types = {'css': 'text/css', 'js': 'application/javascript'}
static = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(sabnzbd.WEB_DIR, 'static'), 'tools.staticdir.content_types': forced_mime_types}
staticcfg = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(sabnzbd.WEB_DIR_CONFIG, 'staticcfg'), 'tools.staticdir.content_types': forced_mime_types}
wizard_static = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(sabnzbd.WIZARD_DIR, 'static'), 'tools.staticdir.content_types': forced_mime_types}
appconfig = {'/api': {'tools.basic_auth.on': False},
'/rss': {'tools.basic_auth.on': False},
'/static': static,
'/wizard/static': wizard_static,
'/favicon.ico': {'tools.staticfile.on': True, 'tools.staticfile.filename': os.path.join(sabnzbd.WEB_DIR_CONFIG, 'staticcfg', 'ico', 'favicon.ico')},
'/staticcfg': staticcfg
}
# Make available from both URLs
main_page = sabnzbd.interface.MainPage()
cherrypy.tree.mount(main_page, '/', config=appconfig)
cherrypy.tree.mount(main_page, '/sabnzbd/', config=appconfig)
# Set authentication for CherryPy
sabnzbd.interface.set_auth(cherrypy.config)
logging.info('Starting web-interface on %s:%s', cherryhost, cherryport)
sabnzbd.cfg.log_level.callback(guard_loglevel)
try:
cherrypy.engine.start()
except:
logging.error(T('Failed to start web-interface: '), exc_info=True)
Bail_Out(browserhost, cherryport)
# Wait for server to become ready
cherrypy.engine.wait(cherrypy.process.wspbus.states.STARTED)
# Window Service support
mail = None
if sabnzbd.WIN32:
if enable_https:
mode = 's'
else:
mode = ''
api_url = 'http%s://%s:%s/sabnzbd/api?apikey=%s' % (mode, browserhost, cherryport, sabnzbd.cfg.api_key())
if sabnzbd.WIN_SERVICE:
mail = MailSlot()
if mail.connect():
logging.info('Connected to the SABHelper service')
mail.send('api %s' % api_url)
else:
logging.error(T('Cannot reach the SABHelper service'))
mail = None
else:
# Write URL directly to registry
set_connection_info(api_url)
if pid_path or pid_file:
sabnzbd.pid_file(pid_path, pid_file, cherryport)
# Start all SABnzbd tasks
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()
# Upload any nzb/zip/rar/nzb.gz/nzb.bz2 files from file association
if upload_nzbs:
from sabnzbd.utils.upload import add_local
for f in upload_nzbs:
add_local(f)
# Set URL for browser
if enable_https:
browser_url = "https://%s:%s/sabnzbd" % (browserhost, cherryport)
else:
browser_url = "http://%s:%s/sabnzbd" % (browserhost, cherryport)
sabnzbd.BROWSER_URL = browser_url
if not autorestarted:
launch_a_browser(browser_url)
if sabnzbd.FOUNDATION:
import sabnzbd.osxmenu
sabnzbd.osxmenu.notify("SAB_Launched", None)
notifier.send_notification('SABnzbd%s' % notifier.hostname(),
T('SABnzbd %s started') % sabnzbd.__version__, 'startup')
# Now's the time to check for a new version
check_latest_version()
autorestarted = False
# ZeroConfig/Bonjour needs a ip. Lets try to find it.
try:
z_host = socket.gethostbyname(socket.gethostname())
except socket.gaierror:
z_host = cherryhost
sabnzbd.zconfig.set_bonjour(z_host, cherryport)
# Have to keep this running, otherwise logging will terminate
timer = 0
while not sabnzbd.SABSTOP:
if sabnzbd.LAST_WARNING:
msg = sabnzbd.LAST_WARNING
sabnzbd.LAST_WARNING = None
sabnzbd.notifier.send_notification(T('Warning'), msg, 'warning')
if sabnzbd.LAST_ERROR:
msg = sabnzbd.LAST_ERROR
sabnzbd.LAST_ERROR = None
sabnzbd.notifier.send_notification(T('Error'), msg, 'error')
if sabnzbd.WIN_SERVICE:
rc = win32event.WaitForMultipleObjects((sabnzbd.WIN_SERVICE.hWaitStop,
sabnzbd.WIN_SERVICE.overlapped.hEvent), 0, 3000)
if rc == win32event.WAIT_OBJECT_0:
if mail:
mail.send('stop')
sabnzbd.save_state()
logging.info('Leaving SABnzbd')
sabnzbd.SABSTOP = True
return
else:
time.sleep(3)
# Check for loglevel changes
if LOG_FLAG:
LOG_FLAG = False
level = LOGLEVELS[sabnzbd.cfg.log_level() + 1]
logger.setLevel(level)
if consoleLogging:
console.setLevel(level)
# 30 sec polling tasks
if timer > 9:
timer = 0
# Keep OS awake (if needed)
sabnzbd.keep_awake()
# Restart scheduler (if needed)
scheduler.restart()
# Save config (if needed)
config.save_config()
# Check the threads
if not sabnzbd.check_all_tasks():
autorestarted = True
sabnzbd.TRIGGER_RESTART = True
# Notify guardian
if sabnzbd.WIN_SERVICE and mail:
mail.send('active')
else:
timer += 1
# 3 sec polling tasks
# Check for auto-restart request
# Or special restart cases like Mac and WindowsService
if sabnzbd.TRIGGER_RESTART:
# Shutdown
cherrypy.engine.exit()
sabnzbd.halt()
sabnzbd.SABSTOP = True
if sabnzbd.downloader.Downloader.do.paused:
sabnzbd.RESTART_ARGS.append('-p')
if autorestarted:
sabnzbd.RESTART_ARGS.append('--autorestarted')
sys.argv = sabnzbd.RESTART_ARGS
os.chdir(org_dir)
# If OSX frozen restart of app instead of embedded python
if getattr(sys, 'frozen', None) == 'macosx_app':
# [[NSProcessInfo processInfo] processIdentifier]]
# logging.info("%s" % (NSProcessInfo.processInfo().processIdentifier()))
my_pid = os.getpid()
my_name = sabnzbd.MY_FULLNAME.replace('/Contents/MacOS/SABnzbd', '')
my_args = ' '.join(sys.argv[1:])
cmd = 'kill -9 %s && open "%s" --args %s' % (my_pid, my_name, my_args)
logging.info('Launching: ', cmd)
os.system(cmd)
elif sabnzbd.WIN_SERVICE and mail:
logging.info('Asking the SABHelper service for a restart')
mail.send('restart')
mail.disconnect()
return
else:
cherrypy.engine._do_execv()
config.save_config()
if sabnzbd.WINTRAY:
sabnzbd.WINTRAY.terminate = True
if sabnzbd.WIN_SERVICE and mail:
mail.send('stop')
if sabnzbd.WIN32:
del_connection_info()
if sabnzbd.FOUNDATION:
sabnzbd.osxmenu.notify("SAB_Shutdown", None)
logging.info('Leaving SABnzbd')
sys.stderr.flush()
sys.stdout.flush()
sabnzbd.pid_file()
if getattr(sys, 'frozen', None) == 'macosx_app':
try:
AppHelper.stopEventLoop()
except:
# Failing AppHelper libary!
os._exit(0)
else:
notifier.send_notification('SABnzbd', T('SABnzbd shutdown finished'), 'startup')
os._exit(0)
##############################################################################
# Windows Service Support
##############################################################################
if sabnzbd.WIN32:
import servicemanager
class SABnzbd(win32serviceutil.ServiceFramework):
""" Win32 Service Handler """
_svc_name_ = 'SABnzbd'
_svc_display_name_ = 'SABnzbd Binary Newsreader'
_svc_deps_ = ["EventLog", "Tcpip", "SABHelper"]
_svc_description_ = 'Automated downloading from Usenet. ' \
'Set to "automatic" to start the service at system startup. ' \
'You may need to login with a real user account when you need ' \
'access to network shares.'
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
self.overlapped = pywintypes.OVERLAPPED() # @UndefinedVariable
self.overlapped.hEvent = win32event.CreateEvent(None, 0, 0, None)
sabnzbd.WIN_SERVICE = self
def SvcDoRun(self):
msg = 'SABnzbd-service %s' % sabnzbd.__version__
self.Logger(servicemanager.PYS_SERVICE_STARTED, msg + ' has started')
sys.argv = get_serv_parms(self._svc_name_)
main()
self.Logger(servicemanager.PYS_SERVICE_STOPPED, msg + ' has stopped')
def SvcStop(self):
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
win32event.SetEvent(self.hWaitStop)
def Logger(self, state, msg):
win32evtlogutil.ReportEvent(self._svc_display_name_,
state, 0,
servicemanager.EVENTLOG_INFORMATION_TYPE,
(self._svc_name_, unicoder(msg)))
def ErrLogger(self, msg, text):
win32evtlogutil.ReportEvent(self._svc_display_name_,
servicemanager.PYS_SERVICE_STOPPED, 0,
servicemanager.EVENTLOG_ERROR_TYPE,
(self._svc_name_, unicoder(msg)),
unicoder(text))
def prep_service_parms(args):
""" Prepare parameter list for service """
# Must store our original path, because the Python Service launcher
# won't give it to us.
serv = [os.path.normpath(os.path.abspath(sys.argv[0]))]
# Convert the tuples to list
for arg in args:
serv.append(arg[0])
if arg[1]:
serv.append(arg[1])
# Make sure we run in daemon mode
serv.append('-d')
return serv
SERVICE_MSG = """
You may need to set additional Service parameters.
Run services.msc from a command prompt.
Don't forget to install the Service SABnzbd-helper.exe too!
"""
def HandleCommandLine(allow_service=True):
""" Handle command line for a Windows Service
Prescribed name that will be called by Py2Exe.
You MUST set 'cmdline_style':'custom' in the package.py!
Returns True when any service commands were detected.
"""
service, sab_opts, serv_opts, _upload_nzbs = commandline_handler()
if service and not allow_service:
# The other frozen apps don't support Services
print "For service support, use SABnzbd-service.exe"
return True
elif service:
if service in ('install', 'update'):
# In this case check for required parameters
path = get_f_option(sab_opts)
if not path:
print 'The -f <path> parameter is required.\n' \
'Use: -f <path> %s' % service
return True
# First run the service installed, because this will
# set the service key in the Registry
win32serviceutil.HandleCommandLine(SABnzbd, argv=serv_opts)
# Add our own parameter to the Registry
sab_opts = prep_service_parms(sab_opts)
if set_serv_parms(SABnzbd._svc_name_, sab_opts):
print SERVICE_MSG
else:
print 'Cannot set required Registry info.'
else:
# Other service commands need no manipulation
win32serviceutil.HandleCommandLine(SABnzbd)
return bool(service)
##############################################################################
# Platform specific startup code
##############################################################################
if __name__ == '__main__':
args = []
for txt in sys.argv:
if ' ' in txt:
txt = '"%s"' % unicoder(txt)
else:
txt = unicoder(txt)
args.append(txt)
sabnzbd.CMDLINE = ' '.join(args)
if sabnzbd.WIN32:
if not HandleCommandLine(allow_service=not hasattr(sys, "frozen")):
main()
elif getattr(sys, 'frozen', None) == 'macosx_app':
try:
# OSX binary runner
from PyObjCTools import AppHelper
from sabnzbd.osxmenu import SABnzbdDelegate
class startApp(Thread):
def __init__(self):
logging.info('[osx] sabApp Starting - starting main thread')
Thread.__init__(self)
def run(self):
main()
logging.info('[osx] sabApp Stopping - main thread quit ')
AppHelper.stopEventLoop()
def stop(self):
logging.info('[osx] sabApp Quit - stopping main thread ')
sabnzbd.halt()
cherrypy.engine.exit()
sabnzbd.SABSTOP = True
logging.info('[osx] sabApp Quit - main thread stopped')
sabApp = startApp()
sabApp.start()
AppHelper.runEventLoop()
except:
main()
else:
main()