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