#!/usr/bin/python3 -OO # Copyright 2007-2020 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 # 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.hexversion < 0x03060000: print("Sorry, requires Python 3.6 or above") print("You can read more at: https://sabnzbd.org/python3") sys.exit(1) import logging import logging.handlers import importlib.util import traceback import getopt import signal import socket import platform import subprocess import ssl import time import re try: import Cheetah import feedparser import configobj import cherrypy import portend import cryptography import chardet except ImportError as e: print("Not all required Python modules are available, please check requirements.txt") print("Missing module:", e.name) print("You can read more at: https://sabnzbd.org/python3") print("If you still experience problems, remove all .pyc files in this folder and subfolders") sys.exit(1) import sabnzbd import sabnzbd.lang import sabnzbd.interface from sabnzbd.constants import * import sabnzbd.newsunpack from sabnzbd.misc import ( check_latest_version, exit_sab, split_host, create_https_certificates, windows_variant, ip_extract, set_serv_parms, get_serv_parms, get_from_url, upload_file_to_sabnzbd, ) from sabnzbd.filesystem import get_ext, real_path, long_path, globber_full, remove_file from sabnzbd.panic import panic_tmpl, panic_port, panic_host, panic, launch_a_browser import sabnzbd.scheduler as scheduler import sabnzbd.config as config import sabnzbd.cfg import sabnzbd.downloader import sabnzbd.notifier as notifier import sabnzbd.zconfig try: import win32api import win32serviceutil import win32evtlogutil import win32event import win32service import win32ts import pywintypes win32api.SetConsoleCtrlHandler(sabnzbd.sig_handler, True) from sabnzbd.utils.apireg import get_connection_info, set_connection_info, del_connection_info except ImportError: if sabnzbd.WIN32: print("Sorry, requires Python module PyWin32.") sys.exit(1) # Global for this module, signaling loglevel change LOG_FLAG = False def guard_loglevel(): """ Callback function for guarding loglevel """ global LOG_FLAG LOG_FLAG = True def warning_helpful(*args, **kwargs): """ Wrapper to ignore helpfull warnings if desired """ if sabnzbd.cfg.helpfull_warnings(): return logging.warning(*args, **kwargs) return logging.info(*args, **kwargs) logging.warning_helpful = warning_helpful 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: # Append traceback, if available warning = {"type": record.levelname, "text": record.msg % record.args, "time": int(time.time())} if record.exc_info: warning["text"] = "%s\n%s" % (warning["text"], traceback.format_exc()) self.store.append(warning) except UnicodeDecodeError: # Catch elusive Unicode conversion problems pass def clear(self): self.store = [] def count(self): return len(self.store) 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() print(" -l --logging <-1..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 Create a PID file in the given folder (full path)") print(" --pidfile 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 to use for HTTPS server") print(" --ipv6_hosting <0|1> Listen on IPv6 address [::1] [*]") print(" --no-login Start with username and password reset") print(" --log-all Log all article handling (for developers)") print(" --disable-file-log Logging is only written to console") print(" --new Run a new instance of SABnzbd") print() print("NZB (or related) file:") print(" NZB or compressed NZB file, with extension .nzb, .zip, .rar, .7z, .gz, or .bz2") print() def print_version(): print( ( """ %s-%s Copyright (C) 2007-2020 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 or (at your option) any later version. """ % (sabnzbd.MY_NAME, sabnzbd.__version__) ) ) def daemonize(): """ Daemonize the process, based on various StackOverflow answers """ 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) # Flush I/O buffers sys.stdout.flush() sys.stderr.flush() # Get log file path and remove the log file if it got too large log_path = os.path.join(sabnzbd.cfg.log_dir.get_path(), DEF_LOG_ERRFILE) if os.path.exists(log_path) and os.path.getsize(log_path) > sabnzbd.cfg.log_size.get_int(): remove_file(log_path) # Replace file descriptors for stdin, stdout, and stderr with open("/dev/null", "rb", 0) as f: os.dup2(f.fileno(), sys.stdin.fileno()) with open(log_path, "ab", 0) as f: os.dup2(f.fileno(), sys.stdout.fileno()) with open(log_path, "ab", 0) as f: os.dup2(f.fileno(), sys.stderr.fileno()) def abort_and_show_error(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 identify_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) if not os.path.exists(full_main): logging.warning_helpful(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) logging.info("Template location for %s is %s", defweb, full_dir) return real_path(full_dir, "templates") def check_template_scheme(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: xname = name.title() else: xname = "" if xname in ("Default",): return "Glitter" elif xname in ("Glitter", "Plush"): return xname elif xname in ("Wizard",): return name.lower() elif xname in ("Config",): return "Glitter" else: return name def get_user_profile_paths(vista_plus): """ Get the default data locations on Windows""" 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 return 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"] 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 sabnzbd.DIR_LCLDATA = sabnzbd.DIR_APPDATA except: pass # Long-path everything sabnzbd.DIR_APPDATA = long_path(sabnzbd.DIR_APPDATA) sabnzbd.DIR_LCLDATA = long_path(sabnzbd.DIR_LCLDATA) sabnzbd.DIR_HOME = long_path(sabnzbd.DIR_HOME) return 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 return 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 return # Nothing worked 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.decoder.SABYENC_VERSION) 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.error( 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.error( T("SABYenc module... NOT found! Expecting v%s - https://sabnzbd.org/sabyenc"), sabnzbd.constants.SABYENC_VERSION_REQUIRED, ) # Do not allow downloading sabnzbd.NO_DOWNLOADING = True logging.info("Cryptography module (v%s)... found!", cryptography.__version__) if sabnzbd.WIN32 and sabnzbd.newsunpack.MULTIPAR_COMMAND: logging.info("MultiPar binary... found (%s)", sabnzbd.newsunpack.MULTIPAR_COMMAND) elif sabnzbd.newsunpack.PAR2_COMMAND: logging.info("par2 binary... found (%s)", sabnzbd.newsunpack.PAR2_COMMAND) else: logging.error(T("par2 binary... NOT found!")) # Do not allow downloading sabnzbd.NO_DOWNLOADING = True if sabnzbd.newsunpack.RAR_COMMAND: logging.info("UNRAR binary... found (%s)", sabnzbd.newsunpack.RAR_COMMAND) # Report problematic unrar if sabnzbd.newsunpack.RAR_PROBLEM: have_str = "%.2f" % (float(sabnzbd.newsunpack.RAR_VERSION) / 100) want_str = "%.2f" % (float(sabnzbd.constants.REC_RAR_VERSION) / 100) logging.warning_helpful( T("Your UNRAR version is %s, we recommend version %s or higher.
"), 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(T("unrar binary... NOT found")) # Do not allow downloading sabnzbd.NO_DOWNLOADING = True # If available, we prefer 7zip over unzip if sabnzbd.newsunpack.SEVEN_COMMAND: logging.info("7za binary... found (%s)", sabnzbd.newsunpack.SEVEN_COMMAND) else: logging.info(T("7za binary... NOT found!")) if sabnzbd.newsunpack.ZIP_COMMAND: logging.info("unzip binary... found (%s)", sabnzbd.newsunpack.ZIP_COMMAND) else: logging.info(T("unzip 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!") # Show fatal warning if sabnzbd.NO_DOWNLOADING: logging.error(T("Essential modules are missing, downloading cannot start.")) 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 socket.error: # 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, str): 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: socket.getaddrinfo(host, None) except socket.error: # 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 = 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 socket.error: # Hostname does not resolve try: # Valid user defined name? info = socket.getaddrinfo(cherryhost, None) except socket.error: if cherryhost not in ("localhost", "127.0.0.1", "::1"): cherryhost = "0.0.0.0" try: info = socket.getaddrinfo(localhost, None) except socket.error: 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: socket.getaddrinfo(cherryhost, None) except socket.error: 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_helpful(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 macOS 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 = 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: portend.free(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: prev = sabnzbd.set_https_verification(False) for f in upload_nzbs: upload_file_to_sabnzbd(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. """ 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(): """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 = [] # macOS 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_start = 2 else: slice_start = 1 # Prepend options from env-variable to options info = os.environ.get("SABnzbd", "").split() info.extend(sys.argv[slice_start:]) 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", "disable-file-log", "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 VALID_NZB_FILES + VALID_ARCHIVES: 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 no_file_log = False web_dir = None vista_plus = False win64 = False repair = 0 no_login = False sabnzbd.RESTART_ARGS = [sys.argv[0]] pid_path = None pid_file = None new_instance = 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 ValueError: autobrowser = True elif opt == "--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 == "--https": https_port = int(arg) sabnzbd.RESTART_ARGS.append(opt) sabnzbd.RESTART_ARGS.append(arg) elif opt == "--repair": repair = 1 pause = True elif opt == "--repair-all": repair = 2 pause = True elif opt == "--log-all": sabnzbd.LOG_ALL = True elif opt == "--disable-file-log": no_file_log = True elif opt == "--no-login": no_login = True elif opt == "--pid": pid_path = arg sabnzbd.RESTART_ARGS.append(opt) sabnzbd.RESTART_ARGS.append(arg) elif opt == "--pidfile": pid_file = arg sabnzbd.RESTART_ARGS.append(opt) sabnzbd.RESTART_ARGS.append(arg) elif opt == "--new": new_instance = True elif opt == "--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() # Need console logging for SABnzbd.py and SABnzbd-console.exe console_logging = (not hasattr(sys, "frozen")) or (sabnzbd.MY_NAME.lower().find("-console") > 0) console_logging = console_logging and not sabnzbd.DAEMON 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, win64 = windows_variant() sabnzbd.WIN64 = win64 if inifile: # INI file given, simplest case inifile = evaluate_inipath(inifile) else: # No ini file given, need profile data get_user_profile_paths(vista_plus) # Find out where INI file is inifile = os.path.abspath(os.path.join(sabnzbd.DIR_LCLDATA, DEF_INI_FILE)) # Long-path notation on Windows to be sure inifile = long_path(inifile) # 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: portend.free(cherryhost, https_port, timeout=0.05) except IOError: abort_and_show_error(browserhost, cherryport) except: abort_and_show_error(browserhost, cherryport, "49") try: portend.free(cherryhost, cherryport, timeout=0.05) except IOError: abort_and_show_error(browserhost, cherryport) except: abort_and_show_error(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: portend.free(browserhost, port, timeout=0.05) except IOError as error: if str(error) == "Port not bound.": pass else: if not url: url = "https://%s:%s%s/api?" % (browserhost, port, sabnzbd.cfg.url_base()) 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(): abort_and_show_error(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.cherryport.set(newport) except: # Something else wrong, probably badly specified host abort_and_show_error(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: portend.free(browserhost, cherryport, timeout=0.05) except IOError as error: if str(error) == "Port not bound.": pass else: if not url: url = "http://%s:%s%s/api?" % (browserhost, cherryport, sabnzbd.cfg.url_base()) 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(): abort_and_show_error(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 abort_and_show_error(browserhost, cherryport, "49") # We found a port, now we never check again sabnzbd.cfg.fixed_ports.set(True) # Logging-checks logdir = sabnzbd.cfg.log_dir.get_path() if fork and not logdir: print("Error: 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 # Log-related constants we always need if logging_level is None: logging_level = sabnzbd.cfg.log_level() else: sabnzbd.cfg.log_level.set(logging_level) sabnzbd.LOGFILE = os.path.join(logdir, DEF_LOG_FILE) logformat = "%(asctime)s::%(levelname)s::[%(module)s:%(lineno)d] %(message)s" logger.setLevel(LOGLEVELS[logging_level + 1]) try: if not no_file_log: rollover_log = logging.handlers.RotatingFileHandler( sabnzbd.LOGFILE, "a+", sabnzbd.cfg.log_size.get_int(), sabnzbd.cfg.log_backups() ) rollover_log.setFormatter(logging.Formatter(logformat)) logger.addHandler(rollover_log) except IOError: print("Error:") print("Can't write to logfile") exit_sab(2) # Fork on non-Windows processes if fork and not sabnzbd.WIN32: daemonize() else: if console_logging: console = logging.StreamHandler() console.setLevel(LOGLEVELS[logging_level + 1]) console.setFormatter(logging.Formatter(logformat)) logger.addHandler(console) if no_file_log: logging.info("Console logging only") 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 win64: suffix = "(win64)" try: logging.info("Platform = %s %s", platform.platform(), suffix) except: logging.info("Platform = %s ", suffix) else: logging.info("Platform = %s", os.name) logging.info("Python-version = %s", sys.version) logging.info("Arguments = %s", sabnzbd.CMDLINE) if sabnzbd.DOCKER: logging.info("Running inside a docker container") else: logging.info("Not inside a docker container") # Find encoding; relevant for external processing activities logging.info("Preferred encoding = %s", sabnzbd.encoding.CODEPAGE) # On Linux/FreeBSD/Unix "UTF-8" is strongly, strongly adviced: if not sabnzbd.WIN32 and not sabnzbd.DARWIN and not ("utf-8" in sabnzbd.encoding.CODEPAGE.lower()): logging.warning_helpful( T( "SABnzbd was started with encoding %s, this should be UTF-8. Expect problems with Unicoded file and directory names in downloads." ), sabnzbd.encoding.CODEPAGE, ) # SSL Information logging.info("SSL version = %s", ssl.OPENSSL_VERSION) # Load (extra) certificates if supplied by certifi # This is optional and provided in the binaries if importlib.util.find_spec("certifi") is not None: import certifi try: os.environ["SSL_CERT_FILE"] = certifi.where() logging.info("Certifi version: %s", certifi.__version__) logging.info("Loaded additional certificates from: %s", os.environ["SSL_CERT_FILE"]) except: # Sometimes the certificate file is blocked logging.warning(T("Could not load additional certificates from certifi package")) logging.info("Traceback: ", exc_info=True) # 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 = %s", pystoneperf) else: logging.debug("CPU Pystone available performance could not be calculated") cpumodel = getcpu() # Linux only if cpumodel: logging.debug("CPU model = %s", cpumodel) logging.info("Using INI file %s", inifile) if autobrowser is not None: sabnzbd.cfg.autobrowser.set(autobrowser) sabnzbd.initialize(pause, clean_up, evalSched=True, repair=repair) os.chdir(sabnzbd.DIR_PROG) sabnzbd.WEB_DIR = identify_web_template(sabnzbd.cfg.web_dir, DEF_STDINTF, fix_webname(web_dir)) sabnzbd.WEB_DIR_CONFIG = identify_web_template(None, DEF_STDCONFIG, "") sabnzbd.WIZARD_DIR = os.path.join(sabnzbd.DIR_INTERFACES, "wizard") sabnzbd.WEB_COLOR = check_template_scheme(sabnzbd.cfg.web_color(), sabnzbd.WEB_DIR) sabnzbd.cfg.web_color.set(sabnzbd.WEB_COLOR) # Handle the several tray icons 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 gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk import sabnzbd.sabtraylinux sabnzbd.LINUXTRAY = sabnzbd.sabtraylinux.StatusIcon() except: logging.info("python3-gi 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 sabnzbd.cfg.enable_https.set(False) # So the cert and key files do exist, now let's check if they are valid: trialcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) try: trialcontext.load_cert_chain(https_cert, https_key) logging.info("HTTPS keys are OK") except: logging.warning(T("Disabled HTTPS because of invalid CERT and KEY files")) logging.info("Traceback: ", exc_info=True) enable_https = False sabnzbd.cfg.enable_https.set(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.auth_basic.on": False, "tools.response_headers.on": True, "tools.response_headers.headers": [("Access-Control-Allow-Origin", "*")], }, "/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.Application.relative_urls = "server" cherrypy.tree.mount(main_page, "/", config=appconfig) cherrypy.tree.mount(main_page, sabnzbd.cfg.url_base(), 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) abort_and_show_error(browserhost, cherryport) # Wait for server to become ready cherrypy.engine.wait(cherrypy.process.wspbus.states.STARTED) if sabnzbd.WIN32: if enable_https: mode = "s" else: mode = "" api_url = "http%s://%s:%s%s/api?apikey=%s" % ( mode, browserhost, cherryport, sabnzbd.cfg.url_base(), sabnzbd.cfg.api_key(), ) # Write URL directly to registry set_connection_info(api_url) if pid_path or pid_file: sabnzbd.pid_file(pid_path, pid_file, cherryport) # Stop here in case of fatal errors if sabnzbd.NO_DOWNLOADING: return # 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: for upload_nzb in upload_nzbs: sabnzbd.add_nzbfile(upload_nzb) # Set URL for browser if enable_https: browser_url = "https://%s:%s%s" % (browserhost, cherryport, sabnzbd.cfg.url_base()) else: browser_url = "http://%s:%s%s" % (browserhost, cherryport, sabnzbd.cfg.url_base()) sabnzbd.BROWSER_URL = browser_url if not autorestarted: launch_a_browser(browser_url) notifier.send_notification("SABnzbd", 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") time.sleep(3) # Check for loglevel changes if LOG_FLAG: LOG_FLAG = False level = LOGLEVELS[sabnzbd.cfg.log_level() + 1] logger.setLevel(level) if console_logging: 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 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 sabnzbd.shutdown_program() if sabnzbd.Downloader.paused: sabnzbd.RESTART_ARGS.append("-p") if autorestarted: sabnzbd.RESTART_ARGS.append("--autorestarted") sys.argv = sabnzbd.RESTART_ARGS os.chdir(org_dir) # If macOS frozen restart of app instead of embedded python if hasattr(sys, "frozen") and sabnzbd.DARWIN: # [[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: # Use external service handler to do the restart # Wait 5 seconds to clean up subprocess.Popen("timeout 5 & sc start SABnzbd", shell=True) else: cherrypy.engine._do_execv() config.save_config() if sabnzbd.WINTRAY: sabnzbd.WINTRAY.terminate = True if sabnzbd.WIN32: del_connection_info() # Send our final goodbyes! notifier.send_notification("SABnzbd", T("SABnzbd shutdown finished"), "startup") logging.info("Leaving SABnzbd") sys.stderr.flush() sys.stdout.flush() sabnzbd.pid_file() if hasattr(sys, "frozen") and sabnzbd.DARWIN: try: AppHelper.stopEventLoop() except: # Failing AppHelper libary! os._exit(0) elif sabnzbd.WIN_SERVICE: # Do nothing, let service handle it pass else: 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"] _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." ) # Only SABnzbd-console.exe can print to the console, so the service is installed # from there. But we run SABnzbd.exe so nothing is logged. Logging can cause the # Windows Service to stop because the output buffers are full. if hasattr(sys, "frozen"): _exe_name_ = "SABnzbd.exe" def __init__(self, args): win32serviceutil.ServiceFramework.__init__(self, args) self.hWaitStop = 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): sabnzbd.shutdown_program() 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_, msg) ) def ErrLogger(self, msg, text): win32evtlogutil.ReportEvent( self._svc_display_name_, servicemanager.PYS_SERVICE_STOPPED, 0, servicemanager.EVENTLOG_ERROR_TYPE, (self._svc_name_, msg), text, ) SERVICE_MSG = """ You may need to set additional Service parameters! Verify the settings in Windows Services (services.msc). https://sabnzbd.org/wiki/advanced/sabnzbd-as-a-windows-service """ def handle_windows_service(): """Handle everything for Windows Service Returns True when any service commands were detected or when we have started as a service. """ # Detect if running as Windows Service (only Vista and above!) # Adapted from https://stackoverflow.com/a/55248281/5235502 if win32ts.ProcessIdToSessionId(win32api.GetCurrentProcessId()) == 0: servicemanager.Initialize() servicemanager.PrepareToHostSingle(SABnzbd) servicemanager.StartServiceCtrlDispatcher() return True # Handle installation and other options service, sab_opts, serv_opts, _upload_nzbs = commandline_handler() if 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 parameter is required.\n" "Use: -f %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 if set_serv_parms(SABnzbd._svc_name_, sab_opts): print(SERVICE_MSG) else: print("ERROR: Cannot set required registry info.") else: # Pass the other commands directly win32serviceutil.HandleCommandLine(SABnzbd) return bool(service) ############################################################################## # Platform specific startup code ############################################################################## if __name__ == "__main__": # We can only register these in the main thread signal.signal(signal.SIGINT, sabnzbd.sig_handler) signal.signal(signal.SIGTERM, sabnzbd.sig_handler) if sabnzbd.WIN32: if not handle_windows_service(): main() elif sabnzbd.DARWIN and sabnzbd.FOUNDATION: # macOS binary runner from threading import Thread from PyObjCTools import AppHelper from AppKit import NSApplication from sabnzbd.osxmenu import SABnzbdDelegate # Need to run the main application in separate thread because the eventLoop # has to be in the main thread. The eventLoop is required for the menu. # This code is made with trial-and-error, please improve! class startApp(Thread): def run(self): main() AppHelper.stopEventLoop() sabApp = startApp() sabApp.start() # Initialize the menu shared_app = NSApplication.sharedApplication() sabnzbd_menu = SABnzbdDelegate.alloc().init() shared_app.setDelegate_(sabnzbd_menu) # Build the menu sabnzbd_menu.awakeFromNib() # Run the main eventloop AppHelper.runEventLoop() else: main()