1#!/usr/local/bin/python3.8 -OO
2# Copyright 2007-2021 The SABnzbd-Team <team@sabnzbd.org>
3#
4# This program is free software; you can redistribute it and/or
5# modify it under the terms of the GNU General Public License
6# as published by the Free Software Foundation; either version 2
7# of the License, or (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17
18import sys
19
20if sys.hexversion < 0x03060000:
21    print("Sorry, requires Python 3.6 or above")
22    print("You can read more at: https://sabnzbd.org/wiki/installation/install-off-modules")
23    sys.exit(1)
24
25import logging
26import logging.handlers
27import importlib.util
28import traceback
29import getopt
30import signal
31import socket
32import platform
33import subprocess
34import ssl
35import time
36import re
37import gc
38from typing import List, Dict, Any
39
40try:
41    import Cheetah
42    import feedparser
43    import configobj
44    import cherrypy
45    import portend
46    import cryptography
47    import chardet
48except ImportError as e:
49    print("Not all required Python modules are available, please check requirements.txt")
50    print("Missing module:", e.name)
51    print("You can read more at: https://sabnzbd.org/wiki/installation/install-off-modules")
52    print("If you still experience problems, remove all .pyc files in this folder and subfolders")
53    sys.exit(1)
54
55import sabnzbd
56import sabnzbd.lang
57import sabnzbd.interface
58from sabnzbd.constants import *
59import sabnzbd.newsunpack
60from sabnzbd.misc import (
61    check_latest_version,
62    exit_sab,
63    split_host,
64    create_https_certificates,
65    ip_extract,
66    set_serv_parms,
67    get_serv_parms,
68    get_from_url,
69    upload_file_to_sabnzbd,
70    is_localhost,
71    is_lan_addr,
72)
73from sabnzbd.filesystem import get_ext, real_path, long_path, globber_full, remove_file
74from sabnzbd.panic import panic_tmpl, panic_port, panic_host, panic, launch_a_browser
75import sabnzbd.config as config
76import sabnzbd.cfg
77import sabnzbd.downloader
78import sabnzbd.notifier as notifier
79import sabnzbd.zconfig
80from sabnzbd.getipaddress import localipv4, publicipv4, ipv6
81from sabnzbd.utils.getperformance import getpystone, getcpu
82import sabnzbd.utils.ssdp as ssdp
83
84try:
85    import win32api
86    import win32serviceutil
87    import win32evtlogutil
88    import win32event
89    import win32service
90    import win32ts
91    import pywintypes
92    import servicemanager
93    from win32com.shell import shell, shellcon
94
95    from sabnzbd.utils.apireg import get_connection_info, set_connection_info, del_connection_info
96    import sabnzbd.sabtray
97
98    win32api.SetConsoleCtrlHandler(sabnzbd.sig_handler, True)
99except ImportError:
100    if sabnzbd.WIN32:
101        print("Sorry, requires Python module PyWin32.")
102        sys.exit(1)
103
104# Global for this module, signaling loglevel change
105LOG_FLAG = False
106
107
108def guard_loglevel():
109    """Callback function for guarding loglevel"""
110    global LOG_FLAG
111    LOG_FLAG = True
112
113
114def warning_helpful(*args, **kwargs):
115    """Wrapper to ignore helpfull warnings if desired"""
116    if sabnzbd.cfg.helpfull_warnings():
117        return logging.warning(*args, **kwargs)
118    return logging.info(*args, **kwargs)
119
120
121logging.warning_helpful = warning_helpful
122
123
124class GUIHandler(logging.Handler):
125    """Logging handler collects the last warnings/errors/exceptions
126    to be displayed in the web-gui
127    """
128
129    def __init__(self, size):
130        """Initializes the handler"""
131        logging.Handler.__init__(self)
132        self._size: int = size
133        self.store: List[Dict[str, Any]] = []
134
135    def emit(self, record: logging.LogRecord):
136        """Emit a record by adding it to our private queue"""
137        # If % is part of the msg, this could fail
138        try:
139            parsed_msg = record.msg % record.args
140        except TypeError:
141            parsed_msg = record.msg + str(record.args)
142
143        warning = {
144            "type": record.levelname,
145            "text": parsed_msg,
146            "time": int(time.time()),
147            "origin": "%s%d" % (record.filename, record.lineno),
148        }
149
150        # Append traceback, if available
151        if record.exc_info:
152            warning["text"] = "%s\n%s" % (warning["text"], traceback.format_exc())
153
154        # Do not notify the same notification within 1 minute from the same source
155        # This prevents endless looping if the notification service itself throws an error/warning
156        # We don't check based on message content, because if it includes a timestamp it's not unique
157        if not any(
158            stored_warning["origin"] == warning["origin"] and stored_warning["time"] + DEF_TIMEOUT > time.time()
159            for stored_warning in self.store
160        ):
161            if record.levelno == logging.WARNING:
162                sabnzbd.notifier.send_notification(T("Warning"), parsed_msg, "warning")
163            else:
164                sabnzbd.notifier.send_notification(T("Error"), parsed_msg, "error")
165
166        # Loose the oldest record
167        if len(self.store) >= self._size:
168            self.store.pop(0)
169        self.store.append(warning)
170
171    def clear(self):
172        self.store = []
173
174    def count(self):
175        return len(self.store)
176
177    def content(self):
178        """Return an array with last records"""
179        return self.store
180
181
182def print_help():
183    print()
184    print(("Usage: %s [-f <configfile>] <other options>" % sabnzbd.MY_NAME))
185    print()
186    print("Options marked [*] are stored in the config file")
187    print()
188    print("Options:")
189    print("  -f  --config-file <ini>     Location of config file")
190    print("  -s  --server <srv:port>     Listen on server:port [*]")
191    print("  -t  --templates <templ>     Template directory [*]")
192    print()
193    print("  -l  --logging <-1..2>       Set logging level (-1=off, 0=least,2= most) [*]")
194    print("  -w  --weblogging            Enable cherrypy access logging")
195    print()
196    print("  -b  --browser <0..1>        Auto browser launch (0= off, 1= on) [*]")
197    if sabnzbd.WIN32:
198        print("  -d  --daemon                Use when run as a service")
199    else:
200        print("  -d  --daemon                Fork daemon process")
201        print("      --pid <path>            Create a PID file in the given folder (full path)")
202        print("      --pidfile <path>        Create a PID file with the given name (full path)")
203    print()
204    print("  -h  --help                  Print this message")
205    print("  -v  --version               Print version information")
206    print("  -c  --clean                 Remove queue, cache and logs")
207    print("  -p  --pause                 Start in paused mode")
208    print("      --repair                Add orphaned jobs from the incomplete folder to the queue")
209    print("      --repair-all            Try to reconstruct the queue from the incomplete folder")
210    print("                              with full data reconstruction")
211    print("      --https <port>          Port to use for HTTPS server")
212    print("      --ipv6_hosting <0|1>    Listen on IPv6 address [::1] [*]")
213    print("      --inet_exposure <0..5>  Set external internet access [*]")
214    print("      --no-login              Start with username and password reset")
215    print("      --log-all               Log all article handling (for developers)")
216    print("      --disable-file-log      Logging is only written to console")
217    print("      --console               Force logging to console")
218    print("      --new                   Run a new instance of SABnzbd")
219    print()
220    print("NZB (or related) file:")
221    print("  NZB or compressed NZB file, with extension .nzb, .zip, .rar, .7z, .gz, or .bz2")
222    print()
223
224
225def print_version():
226    print(
227        (
228            """
229%s-%s
230
231Copyright (C) 2007-2021 The SABnzbd-Team <team@sabnzbd.org>
232SABnzbd comes with ABSOLUTELY NO WARRANTY.
233This is free software, and you are welcome to redistribute it
234under certain conditions. It is licensed under the
235GNU GENERAL PUBLIC LICENSE Version 2 or (at your option) any later version.
236
237"""
238            % (sabnzbd.MY_NAME, sabnzbd.__version__)
239        )
240    )
241
242
243def daemonize():
244    """Daemonize the process, based on various StackOverflow answers"""
245    try:
246        pid = os.fork()
247        if pid > 0:
248            sys.exit(0)
249    except OSError:
250        print("fork() failed")
251        sys.exit(1)
252
253    os.chdir(sabnzbd.DIR_PROG)
254    os.setsid()
255    # Make sure I can read my own files and shut out others
256    prev = os.umask(0)
257    os.umask(prev and int("077", 8))
258
259    try:
260        pid = os.fork()
261        if pid > 0:
262            sys.exit(0)
263    except OSError:
264        print("fork() failed")
265        sys.exit(1)
266
267    # Flush I/O buffers
268    sys.stdout.flush()
269    sys.stderr.flush()
270
271    # Get log file  path and remove the log file if it got too large
272    log_path = os.path.join(sabnzbd.cfg.log_dir.get_path(), DEF_LOG_ERRFILE)
273    if os.path.exists(log_path) and os.path.getsize(log_path) > sabnzbd.cfg.log_size():
274        remove_file(log_path)
275
276    # Replace file descriptors for stdin, stdout, and stderr
277    with open("/dev/null", "rb", 0) as f:
278        os.dup2(f.fileno(), sys.stdin.fileno())
279    with open(log_path, "ab", 0) as f:
280        os.dup2(f.fileno(), sys.stdout.fileno())
281    with open(log_path, "ab", 0) as f:
282        os.dup2(f.fileno(), sys.stderr.fileno())
283
284
285def abort_and_show_error(browserhost, cherryport, err=""):
286    """Abort program because of CherryPy troubles"""
287    logging.error(T("Failed to start web-interface") + " : " + str(err))
288    if not sabnzbd.DAEMON:
289        if "49" in err:
290            panic_host(browserhost, cherryport)
291        else:
292            panic_port(browserhost, cherryport)
293    sabnzbd.halt()
294    exit_sab(2)
295
296
297def identify_web_template(key, defweb, wdir):
298    """Determine a correct web template set, return full template path"""
299    if wdir is None:
300        try:
301            wdir = fix_webname(key())
302        except:
303            wdir = ""
304    if not wdir:
305        wdir = defweb
306    if key:
307        key.set(wdir)
308    if not wdir:
309        # No default value defined, accept empty path
310        return ""
311
312    full_dir = real_path(sabnzbd.DIR_INTERFACES, wdir)
313    full_main = real_path(full_dir, DEF_MAIN_TMPL)
314
315    if not os.path.exists(full_main):
316        logging.warning_helpful(T("Cannot find web template: %s, trying standard template"), full_main)
317        full_dir = real_path(sabnzbd.DIR_INTERFACES, DEF_STDINTF)
318        full_main = real_path(full_dir, DEF_MAIN_TMPL)
319        if not os.path.exists(full_main):
320            logging.exception("Cannot find standard template: %s", full_dir)
321            panic_tmpl(full_dir)
322            exit_sab(1)
323
324    logging.info("Template location for %s is %s", defweb, full_dir)
325    return real_path(full_dir, "templates")
326
327
328def check_template_scheme(color, web_dir):
329    """Check existence of color-scheme"""
330    if color and os.path.exists(os.path.join(web_dir, "static", "stylesheets", "colorschemes", color + ".css")):
331        return color
332    elif color and os.path.exists(os.path.join(web_dir, "static", "stylesheets", "colorschemes", color)):
333        return color
334    else:
335        return ""
336
337
338def fix_webname(name):
339    if name:
340        xname = name.title()
341    else:
342        xname = ""
343    if xname in ("Default",):
344        return "Glitter"
345    elif xname in ("Glitter", "Plush"):
346        return xname
347    elif xname in ("Wizard",):
348        return name.lower()
349    elif xname in ("Config",):
350        return "Glitter"
351    else:
352        return name
353
354
355def get_user_profile_paths():
356    """Get the default data locations on Windows"""
357    if sabnzbd.DAEMON:
358        # In daemon mode, do not try to access the user profile
359        # just assume that everything defaults to the program dir
360        sabnzbd.DIR_LCLDATA = sabnzbd.DIR_PROG
361        sabnzbd.DIR_HOME = sabnzbd.DIR_PROG
362        if sabnzbd.WIN32:
363            # Ignore Win32 "logoff" signal
364            # This should work, but it doesn't
365            # Instead the signal_handler will ignore the "logoff" signal
366            # signal.signal(5, signal.SIG_IGN)
367            pass
368        return
369    elif sabnzbd.WIN32:
370        try:
371            path = shell.SHGetFolderPath(0, shellcon.CSIDL_LOCAL_APPDATA, None, 0)
372            sabnzbd.DIR_LCLDATA = os.path.join(path, DEF_WORKDIR)
373            sabnzbd.DIR_HOME = os.environ["USERPROFILE"]
374        except:
375            try:
376                root = os.environ["AppData"]
377                user = os.environ["USERPROFILE"]
378                sabnzbd.DIR_LCLDATA = "%s\\%s" % (root.replace("\\Roaming", "\\Local"), DEF_WORKDIR)
379                sabnzbd.DIR_HOME = user
380            except:
381                pass
382
383        # Long-path everything
384        sabnzbd.DIR_LCLDATA = long_path(sabnzbd.DIR_LCLDATA)
385        sabnzbd.DIR_HOME = long_path(sabnzbd.DIR_HOME)
386        return
387
388    elif sabnzbd.DARWIN:
389        home = os.environ.get("HOME")
390        if home:
391            sabnzbd.DIR_LCLDATA = "%s/Library/Application Support/SABnzbd" % home
392            sabnzbd.DIR_HOME = home
393            return
394    else:
395        # Unix/Linux
396        home = os.environ.get("HOME")
397        if home:
398            sabnzbd.DIR_LCLDATA = "%s/.%s" % (home, DEF_WORKDIR)
399            sabnzbd.DIR_HOME = home
400            return
401
402    # Nothing worked
403    panic("Cannot access the user profile.", "Please start with sabnzbd.ini file in another location")
404    exit_sab(2)
405
406
407def print_modules():
408    """Log all detected optional or external modules"""
409    if sabnzbd.decoder.SABYENC_ENABLED:
410        # Yes, we have SABYenc, and it's the correct version, so it's enabled
411        logging.info("SABYenc module (v%s)... found!", sabnzbd.decoder.SABYENC_VERSION)
412    else:
413        # Something wrong with SABYenc, so let's determine and print what:
414        if sabnzbd.decoder.SABYENC_VERSION:
415            # We have a VERSION, thus a SABYenc module, but it's not the correct version
416            logging.error(
417                T("SABYenc disabled: no correct version found! (Found v%s, expecting v%s)"),
418                sabnzbd.decoder.SABYENC_VERSION,
419                sabnzbd.constants.SABYENC_VERSION_REQUIRED,
420            )
421        else:
422            # No SABYenc module at all
423            logging.error(
424                T("SABYenc module... NOT found! Expecting v%s - https://sabnzbd.org/sabyenc"),
425                sabnzbd.constants.SABYENC_VERSION_REQUIRED,
426            )
427        # Do not allow downloading
428        sabnzbd.NO_DOWNLOADING = True
429
430    logging.info("Cryptography module (v%s)... found!", cryptography.__version__)
431
432    if sabnzbd.WIN32 and sabnzbd.newsunpack.MULTIPAR_COMMAND:
433        logging.info("MultiPar binary... found (%s)", sabnzbd.newsunpack.MULTIPAR_COMMAND)
434    elif sabnzbd.newsunpack.PAR2_COMMAND:
435        logging.info("par2 binary... found (%s)", sabnzbd.newsunpack.PAR2_COMMAND)
436    else:
437        logging.error(T("par2 binary... NOT found!"))
438        # Do not allow downloading
439        sabnzbd.NO_DOWNLOADING = True
440
441    if sabnzbd.newsunpack.RAR_COMMAND:
442        logging.info("UNRAR binary... found (%s)", sabnzbd.newsunpack.RAR_COMMAND)
443
444        # Report problematic unrar
445        if sabnzbd.newsunpack.RAR_PROBLEM:
446            have_str = "%.2f" % (float(sabnzbd.newsunpack.RAR_VERSION) / 100)
447            want_str = "%.2f" % (float(sabnzbd.constants.REC_RAR_VERSION) / 100)
448            logging.warning_helpful(
449                T("Your UNRAR version is %s, we recommend version %s or higher.<br />"), have_str, want_str
450            )
451        elif not (sabnzbd.WIN32 or sabnzbd.DARWIN):
452            logging.info("UNRAR binary version %.2f", (float(sabnzbd.newsunpack.RAR_VERSION) / 100))
453    else:
454        logging.error(T("unrar binary... NOT found"))
455        # Do not allow downloading
456        sabnzbd.NO_DOWNLOADING = True
457
458    # If available, we prefer 7zip over unzip
459    if sabnzbd.newsunpack.SEVEN_COMMAND:
460        logging.info("7za binary... found (%s)", sabnzbd.newsunpack.SEVEN_COMMAND)
461    else:
462        logging.info(T("7za binary... NOT found!"))
463
464        if sabnzbd.newsunpack.ZIP_COMMAND:
465            logging.info("unzip binary... found (%s)", sabnzbd.newsunpack.ZIP_COMMAND)
466        else:
467            logging.info(T("unzip binary... NOT found!"))
468
469    if not sabnzbd.WIN32:
470        if sabnzbd.newsunpack.NICE_COMMAND:
471            logging.info("nice binary... found (%s)", sabnzbd.newsunpack.NICE_COMMAND)
472        else:
473            logging.info("nice binary... NOT found!")
474        if sabnzbd.newsunpack.IONICE_COMMAND:
475            logging.info("ionice binary... found (%s)", sabnzbd.newsunpack.IONICE_COMMAND)
476        else:
477            logging.info("ionice binary... NOT found!")
478
479    # Show fatal warning
480    if sabnzbd.NO_DOWNLOADING:
481        logging.error(T("Essential modules are missing, downloading cannot start."))
482
483
484def all_localhosts():
485    """Return all unique values of localhost in order of preference"""
486    ips = ["127.0.0.1"]
487    try:
488        # Check whether IPv6 is available and enabled
489        info = socket.getaddrinfo("::1", None)
490        af, socktype, proto, _canonname, _sa = info[0]
491        s = socket.socket(af, socktype, proto)
492        s.close()
493    except socket.error:
494        return ips
495    try:
496        info = socket.getaddrinfo("localhost", None)
497    except socket.error:
498        # localhost does not resolve
499        return ips
500    ips = []
501    for item in info:
502        item = item[4][0]
503        # Avoid problems on strange Linux settings
504        if not isinstance(item, str):
505            continue
506        # Only return IPv6 when enabled
507        if item not in ips and ("::1" not in item or sabnzbd.cfg.ipv6_hosting()):
508            ips.append(item)
509    return ips
510
511
512def check_resolve(host):
513    """Return True if 'host' resolves"""
514    try:
515        socket.getaddrinfo(host, None)
516    except socket.error:
517        # Does not resolve
518        return False
519    return True
520
521
522def get_webhost(cherryhost, cherryport, https_port):
523    """Determine the webhost address and port,
524    return (host, port, browserhost)
525    """
526    if cherryhost == "0.0.0.0" and not check_resolve("127.0.0.1"):
527        cherryhost = ""
528    elif cherryhost == "::" and not check_resolve("::1"):
529        cherryhost = ""
530
531    if cherryhost is None:
532        cherryhost = sabnzbd.cfg.cherryhost()
533    else:
534        sabnzbd.cfg.cherryhost.set(cherryhost)
535
536    # Get IP address, but discard APIPA/IPV6
537    # If only APIPA's or IPV6 are found, fall back to localhost
538    ipv4 = ipv6 = False
539    localhost = hostip = "localhost"
540    try:
541        info = socket.getaddrinfo(socket.gethostname(), None)
542    except socket.error:
543        # Hostname does not resolve
544        try:
545            # Valid user defined name?
546            info = socket.getaddrinfo(cherryhost, None)
547        except socket.error:
548            if not is_localhost(cherryhost):
549                cherryhost = "0.0.0.0"
550            try:
551                info = socket.getaddrinfo(localhost, None)
552            except socket.error:
553                info = socket.getaddrinfo("127.0.0.1", None)
554                localhost = "127.0.0.1"
555    for item in info:
556        ip = str(item[4][0])
557        if ip.startswith("169.254."):
558            pass  # Automatic Private IP Addressing (APIPA)
559        elif ":" in ip:
560            ipv6 = True
561        elif "." in ip and not ipv4:
562            ipv4 = True
563            hostip = ip
564
565    # A blank host will use the local ip address
566    if cherryhost == "":
567        if ipv6 and ipv4:
568            # To protect Firefox users, use numeric IP
569            cherryhost = hostip
570            browserhost = hostip
571        else:
572            cherryhost = socket.gethostname()
573            browserhost = cherryhost
574
575    # 0.0.0.0 will listen on all ipv4 interfaces (no ipv6 addresses)
576    elif cherryhost == "0.0.0.0":
577        # Just take the gamble for this
578        cherryhost = "0.0.0.0"
579        browserhost = localhost
580
581    # :: will listen on all ipv6 interfaces (no ipv4 addresses)
582    elif cherryhost in ("::", "[::]"):
583        cherryhost = cherryhost.strip("[").strip("]")
584        # Assume '::1' == 'localhost'
585        browserhost = localhost
586
587    # IPV6 address
588    elif "[" in cherryhost or ":" in cherryhost:
589        browserhost = cherryhost
590
591    # IPV6 numeric address
592    elif cherryhost.replace(".", "").isdigit():
593        # IPV4 numerical
594        browserhost = cherryhost
595
596    elif cherryhost == localhost:
597        cherryhost = localhost
598        browserhost = localhost
599
600    else:
601        # If on APIPA, use numerical IP, to help FireFoxers
602        if ipv6 and ipv4:
603            cherryhost = hostip
604        browserhost = cherryhost
605
606        # Some systems don't like brackets in numerical ipv6
607        if sabnzbd.DARWIN:
608            cherryhost = cherryhost.strip("[]")
609        else:
610            try:
611                socket.getaddrinfo(cherryhost, None)
612            except socket.error:
613                cherryhost = cherryhost.strip("[]")
614
615    if ipv6 and ipv4 and not is_localhost(browserhost):
616        sabnzbd.AMBI_LOCALHOST = True
617        logging.info("IPV6 has priority on this system, potential Firefox issue")
618
619    if ipv6 and ipv4 and cherryhost == "" and sabnzbd.WIN32:
620        logging.warning_helpful(T("Please be aware the 0.0.0.0 hostname will need an IPv6 address for external access"))
621
622    if cherryhost == "localhost" and not sabnzbd.WIN32 and not sabnzbd.DARWIN:
623        # On the Ubuntu family, localhost leads to problems for CherryPy
624        ips = ip_extract()
625        if "127.0.0.1" in ips and "::1" in ips:
626            cherryhost = "127.0.0.1"
627            if ips[0] != "127.0.0.1":
628                browserhost = "127.0.0.1"
629
630    # This is to please Chrome on macOS
631    if cherryhost == "localhost" and sabnzbd.DARWIN:
632        cherryhost = "127.0.0.1"
633        browserhost = "localhost"
634
635    if cherryport is None:
636        cherryport = sabnzbd.cfg.cherryport.get_int()
637    else:
638        sabnzbd.cfg.cherryport.set(str(cherryport))
639
640    if https_port is None:
641        https_port = sabnzbd.cfg.https_port.get_int()
642    else:
643        sabnzbd.cfg.https_port.set(str(https_port))
644        # if the https port was specified, assume they want HTTPS enabling also
645        sabnzbd.cfg.enable_https.set(True)
646
647    if cherryport == https_port and sabnzbd.cfg.enable_https():
648        sabnzbd.cfg.enable_https.set(False)
649        # Should have a translated message, but that's not available yet
650        logging.error(T("HTTP and HTTPS ports cannot be the same"))
651
652    return cherryhost, cherryport, browserhost, https_port
653
654
655def attach_server(host, port, cert=None, key=None, chain=None):
656    """Define and attach server, optionally HTTPS"""
657    if sabnzbd.cfg.ipv6_hosting() or "::1" not in host:
658        http_server = cherrypy._cpserver.Server()
659        http_server.bind_addr = (host, port)
660        if cert and key:
661            http_server.ssl_module = "builtin"
662            http_server.ssl_certificate = cert
663            http_server.ssl_private_key = key
664            http_server.ssl_certificate_chain = chain
665        http_server.subscribe()
666
667
668def is_sabnzbd_running(url):
669    """Return True when there's already a SABnzbd instance running."""
670    try:
671        url = "%s&mode=version" % url
672        # Do this without certificate verification, few installations will have that
673        prev = sabnzbd.set_https_verification(False)
674        ver = get_from_url(url)
675        sabnzbd.set_https_verification(prev)
676        return ver and (re.search(r"\d+\.\d+\.", ver) or ver.strip() == sabnzbd.__version__)
677    except:
678        return False
679
680
681def find_free_port(host, currentport):
682    """Return a free port, 0 when nothing is free"""
683    n = 0
684    while n < 10 and currentport <= 49151:
685        try:
686            portend.free(host, currentport, timeout=0.025)
687            return currentport
688        except:
689            currentport += 5
690            n += 1
691    return 0
692
693
694def check_for_sabnzbd(url, upload_nzbs, allow_browser=True):
695    """Check for a running instance of sabnzbd on this port
696    allow_browser==True|None will launch the browser, False will not.
697    """
698    if allow_browser is None:
699        allow_browser = True
700    if is_sabnzbd_running(url):
701        # Upload any specified nzb files to the running instance
702        if upload_nzbs:
703            prev = sabnzbd.set_https_verification(False)
704            for f in upload_nzbs:
705                upload_file_to_sabnzbd(url, f)
706            sabnzbd.set_https_verification(prev)
707        else:
708            # Launch the web browser and quit since sabnzbd is already running
709            # Trim away everything after the final slash in the URL
710            url = url[: url.rfind("/") + 1]
711            launch_a_browser(url, force=allow_browser)
712        exit_sab(0)
713        return True
714    return False
715
716
717def evaluate_inipath(path):
718    """Derive INI file path from a partial path.
719    Full file path: if file does not exist the name must contain a dot
720    but not a leading dot.
721    foldername is enough, the standard name will be appended.
722    """
723    path = os.path.normpath(os.path.abspath(path))
724    inipath = os.path.join(path, DEF_INI_FILE)
725    if os.path.isdir(path):
726        return inipath
727    elif os.path.isfile(path) or os.path.isfile(path + ".bak"):
728        return path
729    else:
730        _dirpart, name = os.path.split(path)
731        if name.find(".") < 1:
732            return inipath
733        else:
734            return path
735
736
737def commandline_handler():
738    """Split win32-service commands are true parameters
739    Returns:
740        service, sab_opts, serv_opts, upload_nzbs
741    """
742    service = ""
743    sab_opts = []
744    serv_opts = [os.path.normpath(os.path.abspath(sys.argv[0]))]
745    upload_nzbs = []
746
747    # macOS binary: get rid of the weird -psn_0_123456 parameter
748    for arg in sys.argv:
749        if arg.startswith("-psn_"):
750            sys.argv.remove(arg)
751            break
752
753    # Ugly hack to remove the extra "SABnzbd*" parameter the Windows binary
754    # gets when it's restarted
755    if len(sys.argv) > 1 and "sabnzbd" in sys.argv[1].lower() and not sys.argv[1].startswith("-"):
756        slice_start = 2
757    else:
758        slice_start = 1
759
760    # Prepend options from env-variable to options
761    info = os.environ.get("SABnzbd", "").split()
762    info.extend(sys.argv[slice_start:])
763
764    try:
765        opts, args = getopt.getopt(
766            info,
767            "phdvncwl:s:f:t:b:2:",
768            [
769                "pause",
770                "help",
771                "daemon",
772                "nobrowser",
773                "clean",
774                "logging=",
775                "weblogging",
776                "server=",
777                "templates",
778                "ipv6_hosting=",
779                "inet_exposure=",
780                "browser=",
781                "config-file=",
782                "disable-file-log",
783                "version",
784                "https=",
785                "autorestarted",
786                "repair",
787                "repair-all",
788                "log-all",
789                "no-login",
790                "pid=",
791                "new",
792                "console",
793                "pidfile=",
794                # Below Win32 Service options
795                "password=",
796                "username=",
797                "startup=",
798                "perfmonini=",
799                "perfmondll=",
800                "interactive",
801                "wait=",
802            ],
803        )
804    except getopt.GetoptError:
805        print_help()
806        exit_sab(2)
807
808    # Check for Win32 service commands
809    if args and args[0] in ("install", "update", "remove", "start", "stop", "restart", "debug"):
810        service = args[0]
811        serv_opts.extend(args)
812
813    if not service:
814        # Get and remove any NZB file names
815        for entry in args:
816            if get_ext(entry) in VALID_NZB_FILES + VALID_ARCHIVES:
817                upload_nzbs.append(os.path.abspath(entry))
818
819    for opt, arg in opts:
820        if opt in ("password", "username", "startup", "perfmonini", "perfmondll", "interactive", "wait"):
821            # Service option, just collect
822            if service:
823                serv_opts.append(opt)
824                if arg:
825                    serv_opts.append(arg)
826        else:
827            if opt == "-f":
828                arg = os.path.normpath(os.path.abspath(arg))
829            sab_opts.append((opt, arg))
830
831    return service, sab_opts, serv_opts, upload_nzbs
832
833
834def get_f_option(opts):
835    """Return value of the -f option"""
836    for opt, arg in opts:
837        if opt == "-f":
838            return arg
839    else:
840        return None
841
842
843def main():
844    global LOG_FLAG
845    import sabnzbd  # Due to ApplePython bug
846
847    autobrowser = None
848    autorestarted = False
849    sabnzbd.MY_FULLNAME = sys.argv[0]
850    sabnzbd.MY_NAME = os.path.basename(sabnzbd.MY_FULLNAME)
851    fork = False
852    pause = False
853    inifile = None
854    cherryhost = None
855    cherryport = None
856    https_port = None
857    cherrypylogging = None
858    clean_up = False
859    logging_level = None
860    console_logging = False
861    no_file_log = False
862    web_dir = None
863    repair = 0
864    no_login = False
865    sabnzbd.RESTART_ARGS = [sys.argv[0]]
866    pid_path = None
867    pid_file = None
868    new_instance = False
869    ipv6_hosting = None
870    inet_exposure = None
871
872    _service, sab_opts, _serv_opts, upload_nzbs = commandline_handler()
873
874    for opt, arg in sab_opts:
875        if opt == "--servicecall":
876            sabnzbd.MY_FULLNAME = arg
877        elif opt in ("-d", "--daemon"):
878            if not sabnzbd.WIN32:
879                fork = True
880            autobrowser = False
881            sabnzbd.DAEMON = True
882            sabnzbd.RESTART_ARGS.append(opt)
883        elif opt in ("-f", "--config-file"):
884            inifile = arg
885            sabnzbd.RESTART_ARGS.append(opt)
886            sabnzbd.RESTART_ARGS.append(arg)
887        elif opt in ("-h", "--help"):
888            print_help()
889            exit_sab(0)
890        elif opt in ("-t", "--templates"):
891            web_dir = arg
892        elif opt in ("-s", "--server"):
893            (cherryhost, cherryport) = split_host(arg)
894        elif opt in ("-n", "--nobrowser"):
895            autobrowser = False
896        elif opt in ("-b", "--browser"):
897            try:
898                autobrowser = bool(int(arg))
899            except ValueError:
900                autobrowser = True
901        elif opt == "--autorestarted":
902            autorestarted = True
903        elif opt in ("-c", "--clean"):
904            clean_up = True
905        elif opt in ("-w", "--weblogging"):
906            cherrypylogging = True
907        elif opt in ("-l", "--logging"):
908            try:
909                logging_level = int(arg)
910            except:
911                logging_level = -2
912            if logging_level < -1 or logging_level > 2:
913                print_help()
914                exit_sab(1)
915        elif opt == "--console":
916            console_logging = True
917        elif opt in ("-v", "--version"):
918            print_version()
919            exit_sab(0)
920        elif opt in ("-p", "--pause"):
921            pause = True
922        elif opt == "--https":
923            https_port = int(arg)
924            sabnzbd.RESTART_ARGS.append(opt)
925            sabnzbd.RESTART_ARGS.append(arg)
926        elif opt == "--repair":
927            repair = 1
928            pause = True
929        elif opt == "--repair-all":
930            repair = 2
931            pause = True
932        elif opt == "--log-all":
933            sabnzbd.LOG_ALL = True
934        elif opt == "--disable-file-log":
935            no_file_log = True
936        elif opt == "--no-login":
937            no_login = True
938        elif opt == "--pid":
939            pid_path = arg
940            sabnzbd.RESTART_ARGS.append(opt)
941            sabnzbd.RESTART_ARGS.append(arg)
942        elif opt == "--pidfile":
943            pid_file = arg
944            sabnzbd.RESTART_ARGS.append(opt)
945            sabnzbd.RESTART_ARGS.append(arg)
946        elif opt == "--new":
947            new_instance = True
948        elif opt == "--ipv6_hosting":
949            ipv6_hosting = arg
950        elif opt == "--inet_exposure":
951            inet_exposure = arg
952
953    sabnzbd.MY_FULLNAME = os.path.normpath(os.path.abspath(sabnzbd.MY_FULLNAME))
954    sabnzbd.MY_NAME = os.path.basename(sabnzbd.MY_FULLNAME)
955    sabnzbd.DIR_PROG = '/usr/local/share/sabnzbdplus'
956    sabnzbd.DIR_INTERFACES = real_path(sabnzbd.DIR_PROG, DEF_INTERFACES)
957    sabnzbd.DIR_LANGUAGE = real_path(sabnzbd.DIR_PROG, DEF_LANGUAGE)
958    org_dir = os.getcwd()
959
960    # Need console logging if requested, for SABnzbd.py and SABnzbd-console.exe
961    console_logging = console_logging or sabnzbd.MY_NAME.lower().find("-console") > 0 or not hasattr(sys, "frozen")
962    console_logging = console_logging and not sabnzbd.DAEMON
963
964    LOGLEVELS = (logging.FATAL, logging.WARNING, logging.INFO, logging.DEBUG)
965
966    # Setup primary logging to prevent default console logging
967    gui_log = GUIHandler(MAX_WARNINGS)
968    gui_log.setLevel(logging.WARNING)
969    format_gui = "%(asctime)s\n%(levelname)s\n%(message)s"
970    gui_log.setFormatter(logging.Formatter(format_gui))
971    sabnzbd.GUIHANDLER = gui_log
972
973    # Create logger
974    logger = logging.getLogger("")
975    logger.setLevel(logging.WARNING)
976    logger.addHandler(gui_log)
977
978    # Detect CPU architecture and Windows variant
979    # Use .machine as .processor is not always filled
980    cpu_architecture = platform.uname().machine
981    if sabnzbd.WIN32:
982        sabnzbd.WIN64 = cpu_architecture == "AMD64"
983
984    if inifile:
985        # INI file given, simplest case
986        inifile = evaluate_inipath(inifile)
987    else:
988        # No ini file given, need profile data
989        get_user_profile_paths()
990        # Find out where INI file is
991        inifile = os.path.abspath(os.path.join(sabnzbd.DIR_LCLDATA, DEF_INI_FILE))
992
993    # Long-path notation on Windows to be sure
994    inifile = long_path(inifile)
995
996    # If INI file at non-std location, then use INI location as $HOME
997    if sabnzbd.DIR_LCLDATA != os.path.dirname(inifile):
998        sabnzbd.DIR_HOME = os.path.dirname(inifile)
999
1000    # All system data dirs are relative to the place we found the INI file
1001    sabnzbd.DIR_LCLDATA = os.path.dirname(inifile)
1002
1003    if not os.path.exists(inifile) and not os.path.exists(inifile + ".bak") and not os.path.exists(sabnzbd.DIR_LCLDATA):
1004        try:
1005            os.makedirs(sabnzbd.DIR_LCLDATA)
1006        except IOError:
1007            panic('Cannot create folder "%s".' % sabnzbd.DIR_LCLDATA, "Check specified INI file location.")
1008            exit_sab(1)
1009
1010    sabnzbd.cfg.set_root_folders(sabnzbd.DIR_HOME, sabnzbd.DIR_LCLDATA)
1011
1012    res, msg = config.read_config(inifile)
1013    if not res:
1014        panic(msg, "Specify a correct file or delete this file.")
1015        exit_sab(1)
1016
1017    # Set root folders for HTTPS server file paths
1018    sabnzbd.cfg.set_root_folders2()
1019
1020    if ipv6_hosting is not None:
1021        sabnzbd.cfg.ipv6_hosting.set(ipv6_hosting)
1022
1023    # Determine web host address
1024    cherryhost, cherryport, browserhost, https_port = get_webhost(cherryhost, cherryport, https_port)
1025    enable_https = sabnzbd.cfg.enable_https()
1026
1027    # When this is a daemon, just check and bail out if port in use
1028    if sabnzbd.DAEMON:
1029        if enable_https and https_port:
1030            try:
1031                portend.free(cherryhost, https_port, timeout=0.05)
1032            except IOError:
1033                abort_and_show_error(browserhost, cherryport)
1034            except:
1035                abort_and_show_error(browserhost, cherryport, "49")
1036        try:
1037            portend.free(cherryhost, cherryport, timeout=0.05)
1038        except IOError:
1039            abort_and_show_error(browserhost, cherryport)
1040        except:
1041            abort_and_show_error(browserhost, cherryport, "49")
1042
1043    # Windows instance is reachable through registry
1044    url = None
1045    if sabnzbd.WIN32 and not new_instance:
1046        url = get_connection_info()
1047        if url and check_for_sabnzbd(url, upload_nzbs, autobrowser):
1048            exit_sab(0)
1049
1050    # SSL
1051    if enable_https:
1052        port = https_port or cherryport
1053        try:
1054            portend.free(browserhost, port, timeout=0.05)
1055        except IOError as error:
1056            if str(error) == "Port not bound.":
1057                pass
1058            else:
1059                if not url:
1060                    url = "https://%s:%s%s/api?" % (browserhost, port, sabnzbd.cfg.url_base())
1061                if new_instance or not check_for_sabnzbd(url, upload_nzbs, autobrowser):
1062                    # Bail out if we have fixed our ports after first start-up
1063                    if sabnzbd.cfg.fixed_ports():
1064                        abort_and_show_error(browserhost, cherryport)
1065                    # Find free port to bind
1066                    newport = find_free_port(browserhost, port)
1067                    if newport > 0:
1068                        # Save the new port
1069                        if https_port:
1070                            https_port = newport
1071                            sabnzbd.cfg.https_port.set(newport)
1072                        else:
1073                            # In case HTTPS == HTTP port
1074                            cherryport = newport
1075                            sabnzbd.cfg.cherryport.set(newport)
1076        except:
1077            # Something else wrong, probably badly specified host
1078            abort_and_show_error(browserhost, cherryport, "49")
1079
1080    # NonSSL check if there's no HTTPS or we only use 1 port
1081    if not (enable_https and not https_port):
1082        try:
1083            portend.free(browserhost, cherryport, timeout=0.05)
1084        except IOError as error:
1085            if str(error) == "Port not bound.":
1086                pass
1087            else:
1088                if not url:
1089                    url = "http://%s:%s%s/api?" % (browserhost, cherryport, sabnzbd.cfg.url_base())
1090                if new_instance or not check_for_sabnzbd(url, upload_nzbs, autobrowser):
1091                    # Bail out if we have fixed our ports after first start-up
1092                    if sabnzbd.cfg.fixed_ports():
1093                        abort_and_show_error(browserhost, cherryport)
1094                    # Find free port to bind
1095                    port = find_free_port(browserhost, cherryport)
1096                    if port > 0:
1097                        sabnzbd.cfg.cherryport.set(port)
1098                        cherryport = port
1099        except:
1100            # Something else wrong, probably badly specified host
1101            abort_and_show_error(browserhost, cherryport, "49")
1102
1103    # We found a port, now we never check again
1104    sabnzbd.cfg.fixed_ports.set(True)
1105
1106    # Logging-checks
1107    logdir = sabnzbd.cfg.log_dir.get_path()
1108    if fork and not logdir:
1109        print("Error: I refuse to fork without a log directory!")
1110        sys.exit(1)
1111
1112    if clean_up:
1113        xlist = globber_full(logdir)
1114        for x in xlist:
1115            if RSS_FILE_NAME not in x:
1116                try:
1117                    os.remove(x)
1118                except:
1119                    pass
1120
1121    # Prevent the logger from raising exceptions
1122    # primarily to reduce the fallout of Python issue 4749
1123    logging.raiseExceptions = 0
1124
1125    # Log-related constants we always need
1126    if logging_level is None:
1127        logging_level = sabnzbd.cfg.log_level()
1128    else:
1129        sabnzbd.cfg.log_level.set(logging_level)
1130    sabnzbd.LOGFILE = os.path.join(logdir, DEF_LOG_FILE)
1131    logformat = "%(asctime)s::%(levelname)s::[%(module)s:%(lineno)d] %(message)s"
1132    logger.setLevel(LOGLEVELS[logging_level + 1])
1133
1134    try:
1135        if not no_file_log:
1136            rollover_log = logging.handlers.RotatingFileHandler(
1137                sabnzbd.LOGFILE, "a+", sabnzbd.cfg.log_size(), sabnzbd.cfg.log_backups()
1138            )
1139            rollover_log.setFormatter(logging.Formatter(logformat))
1140            logger.addHandler(rollover_log)
1141
1142    except IOError:
1143        print("Error:")
1144        print("Can't write to logfile")
1145        exit_sab(2)
1146
1147    # Fork on non-Windows processes
1148    if fork and not sabnzbd.WIN32:
1149        daemonize()
1150    else:
1151        if console_logging:
1152            console = logging.StreamHandler()
1153            console.setLevel(LOGLEVELS[logging_level + 1])
1154            console.setFormatter(logging.Formatter(logformat))
1155            logger.addHandler(console)
1156        if no_file_log:
1157            logging.info("Console logging only")
1158
1159    # Start SABnzbd
1160    logging.info("--------------------------------")
1161    logging.info("%s-%s", sabnzbd.MY_NAME, sabnzbd.__version__)
1162
1163    # See if we can get version from git when running an unknown revision
1164    if sabnzbd.__baseline__ == "unknown":
1165        try:
1166            sabnzbd.__baseline__ = sabnzbd.misc.run_command(
1167                ["git", "rev-parse", "--short", "HEAD"], cwd=sabnzbd.DIR_PROG
1168            ).strip()
1169        except:
1170            pass
1171    logging.info("Commit = %s", sabnzbd.__baseline__)
1172
1173    logging.info("Full executable path = %s", sabnzbd.MY_FULLNAME)
1174    logging.info("Arguments = %s", sabnzbd.CMDLINE)
1175    logging.info("Python-version = %s", sys.version)
1176    logging.info("Dockerized = %s", sabnzbd.DOCKER)
1177    logging.info("CPU architecture = %s", cpu_architecture)
1178
1179    try:
1180        logging.info("Platform = %s - %s", os.name, platform.platform())
1181    except:
1182        # Can fail on special platforms (like Snapcraft or embedded)
1183        pass
1184
1185    # Find encoding; relevant for external processing activities
1186    logging.info("Preferred encoding = %s", sabnzbd.encoding.CODEPAGE)
1187
1188    # On Linux/FreeBSD/Unix "UTF-8" is strongly, strongly adviced:
1189    if not sabnzbd.WIN32 and not sabnzbd.DARWIN and not ("utf-8" in sabnzbd.encoding.CODEPAGE.lower()):
1190        logging.warning_helpful(
1191            T(
1192                "SABnzbd was started with encoding %s, this should be UTF-8. Expect problems with Unicoded file and directory names in downloads."
1193            ),
1194            sabnzbd.encoding.CODEPAGE,
1195        )
1196
1197    # SSL Information
1198    logging.info("SSL version = %s", ssl.OPENSSL_VERSION)
1199
1200    # Load (extra) certificates if supplied by certifi
1201    # This is optional and provided in the binaries
1202    if importlib.util.find_spec("certifi") is not None:
1203        import certifi
1204
1205        try:
1206            os.environ["SSL_CERT_FILE"] = certifi.where()
1207            logging.info("Certifi version = %s", certifi.__version__)
1208            logging.info("Loaded additional certificates from %s", os.environ["SSL_CERT_FILE"])
1209        except:
1210            # Sometimes the certificate file is blocked
1211            logging.warning(T("Could not load additional certificates from certifi package"))
1212            logging.info("Traceback: ", exc_info=True)
1213
1214    # Extra startup info
1215    if sabnzbd.cfg.log_level() > 1:
1216        # List the number of certificates available (can take up to 1.5 seconds)
1217        logging.debug("Available certificates = %s", repr(ssl.create_default_context().cert_store_stats()))
1218
1219        # List networking
1220        logging.debug("Local IPv4 address = %s", localipv4())
1221        logging.debug("Public IPv4 address = %s", publicipv4())
1222        logging.debug("IPv6 address = %s", ipv6())
1223
1224        # Measure and log system performance measured by pystone and - if possible - CPU model
1225        logging.debug("CPU Pystone available performance = %s", getpystone())
1226        logging.debug("CPU model = %s", getcpu())
1227
1228    logging.info("Using INI file %s", inifile)
1229
1230    if autobrowser is not None:
1231        sabnzbd.cfg.autobrowser.set(autobrowser)
1232
1233    sabnzbd.initialize(pause, clean_up, repair=repair)
1234
1235    os.chdir(sabnzbd.DIR_PROG)
1236
1237    sabnzbd.WEB_DIR = identify_web_template(sabnzbd.cfg.web_dir, DEF_STDINTF, fix_webname(web_dir))
1238    sabnzbd.WEB_DIR_CONFIG = identify_web_template(None, DEF_STDCONFIG, "")
1239    sabnzbd.WIZARD_DIR = os.path.join(sabnzbd.DIR_INTERFACES, "wizard")
1240
1241    sabnzbd.WEB_COLOR = check_template_scheme(sabnzbd.cfg.web_color(), sabnzbd.WEB_DIR)
1242    sabnzbd.cfg.web_color.set(sabnzbd.WEB_COLOR)
1243
1244    # Handle the several tray icons
1245    if sabnzbd.cfg.win_menu() and not sabnzbd.DAEMON and not sabnzbd.WIN_SERVICE:
1246        if sabnzbd.WIN32:
1247            sabnzbd.WINTRAY = sabnzbd.sabtray.SABTrayThread()
1248        elif sabnzbd.LINUX_POWER and os.environ.get("DISPLAY"):
1249            try:
1250                import gi
1251
1252                gi.require_version("Gtk", "3.0")
1253                from gi.repository import Gtk
1254                import sabnzbd.sabtraylinux
1255
1256                sabnzbd.LINUXTRAY = sabnzbd.sabtraylinux.StatusIcon()
1257            except:
1258                logging.info("python3-gi not found, no SysTray.")
1259
1260    # Find external programs
1261    sabnzbd.newsunpack.find_programs(sabnzbd.DIR_PROG)
1262    print_modules()
1263
1264    # HTTPS certificate generation
1265    https_cert = sabnzbd.cfg.https_cert.get_path()
1266    https_key = sabnzbd.cfg.https_key.get_path()
1267    https_chain = sabnzbd.cfg.https_chain.get_path()
1268    if not (sabnzbd.cfg.https_chain() and os.path.exists(https_chain)):
1269        https_chain = None
1270
1271    if enable_https:
1272        # If either the HTTPS certificate or key do not exist, make some self-signed ones.
1273        if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
1274            create_https_certificates(https_cert, https_key)
1275
1276        if not (os.path.exists(https_cert) and os.path.exists(https_key)):
1277            logging.warning(T("Disabled HTTPS because of missing CERT and KEY files"))
1278            enable_https = False
1279            sabnzbd.cfg.enable_https.set(False)
1280
1281        # So the cert and key files do exist, now let's check if they are valid:
1282        trialcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
1283        try:
1284            trialcontext.load_cert_chain(https_cert, https_key)
1285            logging.info("HTTPS keys are OK")
1286        except:
1287            logging.warning(T("Disabled HTTPS because of invalid CERT and KEY files"))
1288            logging.info("Traceback: ", exc_info=True)
1289            enable_https = False
1290            sabnzbd.cfg.enable_https.set(False)
1291
1292    # Starting of the webserver
1293    # Determine if this system has multiple definitions for 'localhost'
1294    hosts = all_localhosts()
1295    multilocal = len(hosts) > 1 and cherryhost in ("localhost", "0.0.0.0")
1296
1297    # For 0.0.0.0 CherryPy will always pick IPv4, so make sure the secondary localhost is IPv6
1298    if multilocal and cherryhost == "0.0.0.0" and hosts[1] == "127.0.0.1":
1299        hosts[1] = "::1"
1300
1301    # The Windows binary requires numeric localhost as primary address
1302    if cherryhost == "localhost":
1303        cherryhost = hosts[0]
1304
1305    if enable_https:
1306        if https_port:
1307            # Extra HTTP port for primary localhost
1308            attach_server(cherryhost, cherryport)
1309            if multilocal:
1310                # Extra HTTP port for secondary localhost
1311                attach_server(hosts[1], cherryport)
1312                # Extra HTTPS port for secondary localhost
1313                attach_server(hosts[1], https_port, https_cert, https_key, https_chain)
1314            cherryport = https_port
1315        elif multilocal:
1316            # Extra HTTPS port for secondary localhost
1317            attach_server(hosts[1], cherryport, https_cert, https_key, https_chain)
1318
1319        cherrypy.config.update(
1320            {
1321                "server.ssl_module": "builtin",
1322                "server.ssl_certificate": https_cert,
1323                "server.ssl_private_key": https_key,
1324                "server.ssl_certificate_chain": https_chain,
1325            }
1326        )
1327    elif multilocal:
1328        # Extra HTTP port for secondary localhost
1329        attach_server(hosts[1], cherryport)
1330
1331    if no_login:
1332        sabnzbd.cfg.username.set("")
1333        sabnzbd.cfg.password.set("")
1334
1335    # Overwrite inet_exposure from command-line for VPS-setups
1336    if inet_exposure:
1337        sabnzbd.cfg.inet_exposure.set(inet_exposure)
1338
1339    mime_gzip = (
1340        "text/*",
1341        "application/javascript",
1342        "application/x-javascript",
1343        "application/json",
1344        "application/xml",
1345        "application/vnd.ms-fontobject",
1346        "application/font*",
1347        "image/svg+xml",
1348    )
1349    cherrypy.config.update(
1350        {
1351            "server.environment": "production",
1352            "server.socket_host": cherryhost,
1353            "server.socket_port": cherryport,
1354            "server.shutdown_timeout": 0,
1355            "log.screen": False,
1356            "engine.autoreload.on": False,
1357            "tools.encode.on": True,
1358            "tools.gzip.on": True,
1359            "tools.gzip.mime_types": mime_gzip,
1360            "request.show_tracebacks": True,
1361            "error_page.401": sabnzbd.panic.error_page_401,
1362            "error_page.404": sabnzbd.panic.error_page_404,
1363        }
1364    )
1365
1366    # Do we want CherryPy Logging? Cannot be done via the config
1367    if cherrypylogging:
1368        sabnzbd.WEBLOGFILE = os.path.join(logdir, DEF_LOG_CHERRY)
1369        cherrypy.log.screen = True
1370        cherrypy.log.access_log.propagate = True
1371        cherrypy.log.access_file = str(sabnzbd.WEBLOGFILE)
1372    else:
1373        cherrypy.log.access_log.propagate = False
1374
1375    # Force mimetypes (OS might overwrite them)
1376    forced_mime_types = {"css": "text/css", "js": "application/javascript"}
1377
1378    static = {
1379        "tools.staticdir.on": True,
1380        "tools.staticdir.dir": os.path.join(sabnzbd.WEB_DIR, "static"),
1381        "tools.staticdir.content_types": forced_mime_types,
1382    }
1383    staticcfg = {
1384        "tools.staticdir.on": True,
1385        "tools.staticdir.dir": os.path.join(sabnzbd.WEB_DIR_CONFIG, "staticcfg"),
1386        "tools.staticdir.content_types": forced_mime_types,
1387    }
1388    wizard_static = {
1389        "tools.staticdir.on": True,
1390        "tools.staticdir.dir": os.path.join(sabnzbd.WIZARD_DIR, "static"),
1391        "tools.staticdir.content_types": forced_mime_types,
1392    }
1393
1394    appconfig = {
1395        "/api": {
1396            "tools.auth_basic.on": False,
1397            "tools.response_headers.on": True,
1398            "tools.response_headers.headers": [("Access-Control-Allow-Origin", "*")],
1399        },
1400        "/static": static,
1401        "/wizard/static": wizard_static,
1402        "/favicon.ico": {
1403            "tools.staticfile.on": True,
1404            "tools.staticfile.filename": os.path.join(sabnzbd.WEB_DIR_CONFIG, "staticcfg", "ico", "favicon.ico"),
1405        },
1406        "/staticcfg": staticcfg,
1407    }
1408
1409    # Make available from both URLs
1410    main_page = sabnzbd.interface.MainPage()
1411    cherrypy.Application.relative_urls = "server"
1412    cherrypy.tree.mount(main_page, "/", config=appconfig)
1413    cherrypy.tree.mount(main_page, sabnzbd.cfg.url_base(), config=appconfig)
1414
1415    # Set authentication for CherryPy
1416    sabnzbd.interface.set_auth(cherrypy.config)
1417    logging.info("Starting web-interface on %s:%s", cherryhost, cherryport)
1418
1419    sabnzbd.cfg.log_level.callback(guard_loglevel)
1420
1421    try:
1422        cherrypy.engine.start()
1423    except:
1424        logging.error(T("Failed to start web-interface: "), exc_info=True)
1425        abort_and_show_error(browserhost, cherryport)
1426
1427    # Wait for server to become ready
1428    cherrypy.engine.wait(cherrypy.process.wspbus.states.STARTED)
1429
1430    if sabnzbd.WIN32:
1431        if enable_https:
1432            mode = "s"
1433        else:
1434            mode = ""
1435        api_url = "http%s://%s:%s%s/api?apikey=%s" % (
1436            mode,
1437            browserhost,
1438            cherryport,
1439            sabnzbd.cfg.url_base(),
1440            sabnzbd.cfg.api_key(),
1441        )
1442
1443        # Write URL directly to registry
1444        set_connection_info(api_url)
1445
1446    if pid_path or pid_file:
1447        sabnzbd.pid_file(pid_path, pid_file, cherryport)
1448
1449    # Stop here in case of fatal errors
1450    if sabnzbd.NO_DOWNLOADING:
1451        return
1452
1453    # Start all SABnzbd tasks
1454    logging.info("Starting %s-%s", sabnzbd.MY_NAME, sabnzbd.__version__)
1455    try:
1456        sabnzbd.start()
1457    except:
1458        logging.exception("Failed to start %s-%s", sabnzbd.MY_NAME, sabnzbd.__version__)
1459        sabnzbd.halt()
1460
1461    # Upload any nzb/zip/rar/nzb.gz/nzb.bz2 files from file association
1462    if upload_nzbs:
1463        for upload_nzb in upload_nzbs:
1464            sabnzbd.add_nzbfile(upload_nzb)
1465
1466    # Set URL for browser
1467    if enable_https:
1468        browser_url = "https://%s:%s%s" % (browserhost, cherryport, sabnzbd.cfg.url_base())
1469    else:
1470        browser_url = "http://%s:%s%s" % (browserhost, cherryport, sabnzbd.cfg.url_base())
1471    sabnzbd.BROWSER_URL = browser_url
1472
1473    if not autorestarted:
1474        launch_a_browser(browser_url)
1475        notifier.send_notification("SABnzbd", T("SABnzbd %s started") % sabnzbd.__version__, "startup")
1476        # Now's the time to check for a new version
1477        check_latest_version()
1478    autorestarted = False
1479
1480    # Start SSDP and Bonjour if SABnzbd isn't listening on localhost only
1481    if sabnzbd.cfg.enable_broadcast() and not is_localhost(cherryhost):
1482        # Try to find a LAN IP address for SSDP/Bonjour
1483        if is_lan_addr(cherryhost):
1484            # A specific listening address was configured, use that
1485            external_host = cherryhost
1486        else:
1487            # Fall back to the IPv4 address of the LAN interface
1488            external_host = localipv4()
1489        logging.debug("Using %s as host address for Bonjour and SSDP", external_host)
1490
1491        if is_lan_addr(external_host):
1492            sabnzbd.zconfig.set_bonjour(external_host, cherryport)
1493
1494            # Set URL for browser for external hosts
1495            ssdp_url = "%s://%s:%s%s" % (
1496                ("https" if enable_https else "http"),
1497                external_host,
1498                cherryport,
1499                sabnzbd.cfg.url_base(),
1500            )
1501            ssdp.start_ssdp(
1502                external_host,
1503                "SABnzbd",
1504                ssdp_url,
1505                "SABnzbd %s" % sabnzbd.__version__,
1506                "SABnzbd Team",
1507                "https://sabnzbd.org/",
1508                "SABnzbd %s" % sabnzbd.__version__,
1509                ssdp_broadcast_interval=sabnzbd.cfg.ssdp_broadcast_interval(),
1510            )
1511
1512    # Have to keep this running, otherwise logging will terminate
1513    timer = 0
1514    while not sabnzbd.SABSTOP:
1515        # Wait to be awoken or every 3 seconds
1516        with sabnzbd.SABSTOP_CONDITION:
1517            sabnzbd.SABSTOP_CONDITION.wait(3)
1518        timer += 1
1519
1520        # Check for loglevel changes
1521        if LOG_FLAG:
1522            LOG_FLAG = False
1523            level = LOGLEVELS[sabnzbd.cfg.log_level() + 1]
1524            logger.setLevel(level)
1525            if console_logging:
1526                console.setLevel(level)
1527
1528        # 300 sec polling tasks
1529        if not timer % 100:
1530            if sabnzbd.LOG_ALL:
1531                logging.debug("Triggering Python garbage collection")
1532            gc.collect()
1533            timer = 0
1534
1535        # 30 sec polling tasks
1536        if not timer % 10:
1537            # Keep OS awake (if needed)
1538            sabnzbd.keep_awake()
1539            # Restart scheduler (if needed)
1540            sabnzbd.Scheduler.restart(plan_restart=False)
1541            # Save config (if needed)
1542            config.save_config()
1543            # Check the threads
1544            if not sabnzbd.check_all_tasks():
1545                autorestarted = True
1546                sabnzbd.TRIGGER_RESTART = True
1547
1548        # 3 sec polling tasks
1549        # Check for auto-restart request
1550        # Or special restart cases like Mac and WindowsService
1551        if sabnzbd.TRIGGER_RESTART:
1552            logging.info("Performing triggered restart")
1553            sabnzbd.shutdown_program()
1554
1555            # Add arguments and make sure we are in the right directory
1556            if sabnzbd.Downloader.paused:
1557                sabnzbd.RESTART_ARGS.append("-p")
1558            if autorestarted:
1559                sabnzbd.RESTART_ARGS.append("--autorestarted")
1560            sys.argv = sabnzbd.RESTART_ARGS
1561            os.chdir(org_dir)
1562
1563            # Binaries require special restart
1564            if hasattr(sys, "frozen"):
1565                if sabnzbd.DARWIN:
1566                    # On macOS restart of app instead of embedded python
1567                    my_name = sabnzbd.MY_FULLNAME.replace("/Contents/MacOS/SABnzbd", "")
1568                    my_args = " ".join(sys.argv[1:])
1569                    cmd = 'kill -9 %s && open "%s" --args %s' % (os.getpid(), my_name, my_args)
1570                    logging.info("Launching: %s", cmd)
1571                    os.system(cmd)
1572                elif sabnzbd.WIN_SERVICE:
1573                    # Use external service handler to do the restart
1574                    # Wait 5 seconds to clean up
1575                    subprocess.Popen("timeout 5 & sc start SABnzbd", shell=True)
1576                elif sabnzbd.WIN32:
1577                    # Just a simple restart of the exe
1578                    os.execv(sys.executable, ['"%s"' % arg for arg in sys.argv])
1579            else:
1580                # CherryPy has special logic to include interpreter options such as "-OO"
1581                cherrypy.engine._do_execv()
1582
1583    # Send our final goodbyes!
1584    notifier.send_notification("SABnzbd", T("SABnzbd shutdown finished"), "startup")
1585    logging.info("Leaving SABnzbd")
1586    sys.stderr.flush()
1587    sys.stdout.flush()
1588    sabnzbd.pid_file()
1589
1590    if hasattr(sys, "frozen") and sabnzbd.DARWIN:
1591        try:
1592            AppHelper.stopEventLoop()
1593        except:
1594            # Failing AppHelper libary!
1595            os._exit(0)
1596    elif sabnzbd.WIN_SERVICE:
1597        # Do nothing, let service handle it
1598        pass
1599    else:
1600        os._exit(0)
1601
1602
1603##############################################################################
1604# Windows Service Support
1605##############################################################################
1606
1607
1608if sabnzbd.WIN32:
1609
1610    class SABnzbd(win32serviceutil.ServiceFramework):
1611        """Win32 Service Handler"""
1612
1613        _svc_name_ = "SABnzbd"
1614        _svc_display_name_ = "SABnzbd Binary Newsreader"
1615        _svc_deps_ = ["EventLog", "Tcpip"]
1616        _svc_description_ = (
1617            "Automated downloading from Usenet. "
1618            'Set to "automatic" to start the service at system startup. '
1619            "You may need to login with a real user account when you need "
1620            "access to network shares."
1621        )
1622
1623        # Only SABnzbd-console.exe can print to the console, so the service is installed
1624        # from there. But we run SABnzbd.exe so nothing is logged. Logging can cause the
1625        # Windows Service to stop because the output buffers are full.
1626        if hasattr(sys, "frozen"):
1627            _exe_name_ = "SABnzbd.exe"
1628
1629        def __init__(self, args):
1630            win32serviceutil.ServiceFramework.__init__(self, args)
1631            self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
1632            sabnzbd.WIN_SERVICE = self
1633
1634        def SvcDoRun(self):
1635            msg = "SABnzbd-service %s" % sabnzbd.__version__
1636            self.Logger(servicemanager.PYS_SERVICE_STARTED, msg + " has started")
1637            sys.argv = get_serv_parms(self._svc_name_)
1638            main()
1639            self.Logger(servicemanager.PYS_SERVICE_STOPPED, msg + " has stopped")
1640
1641        def SvcStop(self):
1642            sabnzbd.shutdown_program()
1643            self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
1644            win32event.SetEvent(self.hWaitStop)
1645
1646        def Logger(self, state, msg):
1647            win32evtlogutil.ReportEvent(
1648                self._svc_display_name_, state, 0, servicemanager.EVENTLOG_INFORMATION_TYPE, (self._svc_name_, msg)
1649            )
1650
1651        def ErrLogger(self, msg, text):
1652            win32evtlogutil.ReportEvent(
1653                self._svc_display_name_,
1654                servicemanager.PYS_SERVICE_STOPPED,
1655                0,
1656                servicemanager.EVENTLOG_ERROR_TYPE,
1657                (self._svc_name_, msg),
1658                text,
1659            )
1660
1661
1662SERVICE_MSG = """
1663You may need to set additional Service parameters!
1664Verify the settings in Windows Services (services.msc).
1665
1666https://sabnzbd.org/wiki/advanced/sabnzbd-as-a-windows-service
1667"""
1668
1669
1670def handle_windows_service():
1671    """Handle everything for Windows Service
1672    Returns True when any service commands were detected or
1673    when we have started as a service.
1674    """
1675    # Detect if running as Windows Service
1676    # Adapted from https://stackoverflow.com/a/55248281/5235502
1677    # Only works when run from the exe-files
1678    if hasattr(sys, "frozen") and win32ts.ProcessIdToSessionId(win32api.GetCurrentProcessId()) == 0:
1679        servicemanager.Initialize()
1680        servicemanager.PrepareToHostSingle(SABnzbd)
1681        servicemanager.StartServiceCtrlDispatcher()
1682        return True
1683
1684    # Handle installation and other options
1685    service, sab_opts, serv_opts, _upload_nzbs = commandline_handler()
1686
1687    if service:
1688        if service in ("install", "update"):
1689            # In this case check for required parameters
1690            path = get_f_option(sab_opts)
1691            if not path:
1692                print(("The -f <path> parameter is required.\n" "Use: -f <path> %s" % service))
1693                return True
1694
1695            # First run the service installed, because this will
1696            # set the service key in the Registry
1697            win32serviceutil.HandleCommandLine(SABnzbd, argv=serv_opts)
1698
1699            # Add our own parameter to the Registry
1700            if set_serv_parms(SABnzbd._svc_name_, sab_opts):
1701                print(SERVICE_MSG)
1702            else:
1703                print("ERROR: Cannot set required registry info.")
1704        else:
1705            # Pass the other commands directly
1706            win32serviceutil.HandleCommandLine(SABnzbd)
1707
1708    return bool(service)
1709
1710
1711##############################################################################
1712# Platform specific startup code
1713##############################################################################
1714
1715
1716if __name__ == "__main__":
1717    # We can only register these in the main thread
1718    signal.signal(signal.SIGINT, sabnzbd.sig_handler)
1719    signal.signal(signal.SIGTERM, sabnzbd.sig_handler)
1720
1721    if sabnzbd.WIN32:
1722        if not handle_windows_service():
1723            main()
1724
1725    elif sabnzbd.DARWIN and sabnzbd.FOUNDATION:
1726        # macOS binary runner
1727        from threading import Thread
1728        from PyObjCTools import AppHelper
1729        from AppKit import NSApplication
1730        from sabnzbd.osxmenu import SABnzbdDelegate
1731
1732        # Need to run the main application in separate thread because the eventLoop
1733        # has to be in the main thread. The eventLoop is required for the menu.
1734        # This code is made with trial-and-error, please feel free to improve!
1735        class startApp(Thread):
1736            def run(self):
1737                main()
1738                AppHelper.stopEventLoop()
1739
1740        sabApp = startApp()
1741        sabApp.start()
1742
1743        # Initialize the menu
1744        shared_app = NSApplication.sharedApplication()
1745        sabnzbd_menu = SABnzbdDelegate.alloc().init()
1746        shared_app.setDelegate_(sabnzbd_menu)
1747        # Build the menu
1748        sabnzbd_menu.awakeFromNib()
1749        # Run the main eventloop
1750        AppHelper.runEventLoop()
1751    else:
1752        main()
1753