diff --git a/SABHelper.py b/SABHelper.py deleted file mode 100644 index 58865b0..0000000 --- a/SABHelper.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/python3 -OO -# Copyright 2007-2020 The SABnzbd-Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -import sys -if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): - print("Sorry, requires Python 2.6 or 2.7.") - sys.exit(1) - -import time -import subprocess - - -try: - import win32api - import win32file - import win32serviceutil - import win32evtlogutil - import win32event - import win32service - import pywintypes -except ImportError: - print("Sorry, requires Python module PyWin32.") - sys.exit(1) - -from util.mailslot import MailSlot -from util.apireg import del_connection_info, set_connection_info - - -WIN_SERVICE = None - - -def HandleCommandLine(allow_service=True): - """ Handle command line for a Windows Service - Prescribed name that will be called by Py2Exe. - You MUST set 'cmdline_style':'custom' in the package.py! - """ - win32serviceutil.HandleCommandLine(SABHelper) - - -def start_sab(): - return subprocess.Popen('net start SABnzbd', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) - - -def main(): - - mail = MailSlot() - if not mail.create(10): - return '- Cannot create Mailslot' - - active = False # SABnzbd should be running - counter = 0 # Time allowed for SABnzbd to be silent - while True: - msg = mail.receive() - if msg: - if msg == 'restart': - time.sleep(1.0) - counter = 0 - del_connection_info(user=False) - start_sab() - elif msg == 'stop': - active = False - del_connection_info(user=False) - elif msg == 'active': - active = True - counter = 0 - elif msg.startswith('api '): - active = True - counter = 0 - _cmd, url = msg.split() - if url: - set_connection_info(url.strip(), user=False) - - if active: - counter += 1 - if counter > 120: # 120 seconds - counter = 0 - start_sab() - - rc = win32event.WaitForMultipleObjects((WIN_SERVICE.hWaitStop, - WIN_SERVICE.overlapped.hEvent), 0, 1000) - if rc == win32event.WAIT_OBJECT_0: - del_connection_info(user=False) - mail.disconnect() - return '' - - -############################################################################## -# Windows Service Support -############################################################################## -import servicemanager - - -class SABHelper(win32serviceutil.ServiceFramework): - """ Win32 Service Handler """ - - _svc_name_ = 'SABHelper' - _svc_display_name_ = 'SABnzbd Helper' - _svc_deps_ = ["EventLog", "Tcpip"] - _svc_description_ = 'Automated downloading from Usenet. ' \ - 'This service helps SABnzbd to restart itself.' - - def __init__(self, args): - global WIN_SERVICE - win32serviceutil.ServiceFramework.__init__(self, args) - - self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) - self.overlapped = pywintypes.OVERLAPPED() # @UndefinedVariable - self.overlapped.hEvent = win32event.CreateEvent(None, 0, 0, None) - WIN_SERVICE = self - - def SvcDoRun(self): - msg = 'SABHelper-service' - self.Logger(servicemanager.PYS_SERVICE_STARTED, msg + ' has started') - res = main() - self.Logger(servicemanager.PYS_SERVICE_STOPPED, msg + ' has stopped' + res) - - def SvcStop(self): - self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) - win32event.SetEvent(self.hWaitStop) - - def Logger(self, state, msg): - win32evtlogutil.ReportEvent(self._svc_display_name_, - state, 0, - servicemanager.EVENTLOG_INFORMATION_TYPE, - (self._svc_name_, str(msg))) - - def ErrLogger(self, msg, text): - win32evtlogutil.ReportEvent(self._svc_display_name_, - servicemanager.PYS_SERVICE_STOPPED, 0, - servicemanager.EVENTLOG_ERROR_TYPE, - (self._svc_name_, str(msg)), - str(text)) - - -############################################################################## -# Platform specific startup code -############################################################################## -if __name__ == '__main__': - - win32serviceutil.HandleCommandLine(SABHelper, argv=sys.argv) diff --git a/SABnzbd.py b/SABnzbd.py index f38ee75..9e4b054 100755 --- a/SABnzbd.py +++ b/SABnzbd.py @@ -28,6 +28,7 @@ import getopt import signal import socket import platform +import subprocess import ssl import time import re @@ -75,13 +76,11 @@ try: import win32evtlogutil import win32event import win32service + import win32ts import pywintypes win32api.SetConsoleCtrlHandler(sabnzbd.sig_handler, True) - from util.mailslot import MailSlot from util.apireg import get_connection_info, set_connection_info, del_connection_info except ImportError: - class MailSlot: - pass if sabnzbd.WIN32: print("Sorry, requires Python module PyWin32.") sys.exit(1) @@ -1336,8 +1335,7 @@ def main(): # Wait for server to become ready cherrypy.engine.wait(cherrypy.process.wspbus.states.STARTED) - # Window Service support - mail = None + if sabnzbd.WIN32: if enable_https: mode = 's' @@ -1345,17 +1343,8 @@ def main(): mode = '' api_url = 'http%s://%s:%s%s/api?apikey=%s' % (mode, browserhost, cherryport, sabnzbd.cfg.url_base(), sabnzbd.cfg.api_key()) - if sabnzbd.WIN_SERVICE: - mail = MailSlot() - if mail.connect(): - logging.info('Connected to the SABHelper service') - mail.send('api %s' % api_url) - else: - logging.error(T('Cannot reach the SABHelper service')) - mail = None - else: - # Write URL directly to registry - set_connection_info(api_url) + # Write URL directly to registry + set_connection_info(api_url) if pid_path or pid_file: sabnzbd.pid_file(pid_path, pid_file, cherryport) @@ -1411,18 +1400,7 @@ def main(): sabnzbd.LAST_ERROR = None sabnzbd.notifier.send_notification(T('Error'), msg, 'error') - if sabnzbd.WIN_SERVICE: - rc = win32event.WaitForMultipleObjects((sabnzbd.WIN_SERVICE.hWaitStop, - sabnzbd.WIN_SERVICE.overlapped.hEvent), 0, 3000) - if rc == win32event.WAIT_OBJECT_0: - if mail: - mail.send('stop') - sabnzbd.save_state() - logging.info('Leaving SABnzbd') - sabnzbd.SABSTOP = True - return - else: - time.sleep(3) + time.sleep(3) # Check for loglevel changes if LOG_FLAG: @@ -1445,9 +1423,6 @@ def main(): if not sabnzbd.check_all_tasks(): autorestarted = True sabnzbd.TRIGGER_RESTART = True - # Notify guardian - if sabnzbd.WIN_SERVICE and mail: - mail.send('active') else: timer += 1 @@ -1475,12 +1450,10 @@ def main(): cmd = 'kill -9 %s && open "%s" --args %s' % (my_pid, my_name, my_args) logging.info('Launching: ', cmd) os.system(cmd) - - elif sabnzbd.WIN_SERVICE and mail: - logging.info('Asking the SABHelper service for a restart') - mail.send('restart') - mail.disconnect() - return + elif sabnzbd.WIN_SERVICE: + # Use external service handler to do the restart + # Wait 5 seconds to clean up + subprocess.Popen('timeout 5 & sc start SABnzbd', shell=True) else: cherrypy.engine._do_execv() @@ -1488,9 +1461,6 @@ def main(): if sabnzbd.WINTRAY: sabnzbd.WINTRAY.terminate = True - - if sabnzbd.WIN_SERVICE and mail: - mail.send('stop') if sabnzbd.WIN32: del_connection_info() @@ -1507,6 +1477,9 @@ def main(): except: # Failing AppHelper libary! os._exit(0) + elif sabnzbd.WIN_SERVICE: + # Do nothing, let service handle it + pass else: os._exit(0) @@ -1521,21 +1494,23 @@ if sabnzbd.WIN32: class SABnzbd(win32serviceutil.ServiceFramework): """ Win32 Service Handler """ - _svc_name_ = 'SABnzbd' _svc_display_name_ = 'SABnzbd Binary Newsreader' - _svc_deps_ = ["EventLog", "Tcpip", "SABHelper"] + _svc_deps_ = ["EventLog", "Tcpip"] _svc_description_ = 'Automated downloading from Usenet. ' \ 'Set to "automatic" to start the service at system startup. ' \ 'You may need to login with a real user account when you need ' \ 'access to network shares.' + # Only SABnzbd-console.exe can print to the console, so the service is installed + # from there. But we run SABnzbd.exe so nothing is logged. Logging can cause the + # Windows Service to stop because the output buffers are full. + if hasattr(sys, "frozen"): + _exe_name_ = "SABnzbd.exe" + def __init__(self, args): win32serviceutil.ServiceFramework.__init__(self, args) - self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) - self.overlapped = pywintypes.OVERLAPPED() - self.overlapped.hEvent = win32event.CreateEvent(None, 0, 0, None) sabnzbd.WIN_SERVICE = self def SvcDoRun(self): @@ -1546,6 +1521,7 @@ if sabnzbd.WIN32: self.Logger(servicemanager.PYS_SERVICE_STOPPED, msg + ' has stopped') def SvcStop(self): + sabnzbd.shutdown_program() self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) win32event.SetEvent(self.hWaitStop) @@ -1562,44 +1538,31 @@ if sabnzbd.WIN32: (self._svc_name_, msg), text) -def prep_service_parms(args): - """ Prepare parameter list for service """ - - # Must store our original path, because the Python Service launcher - # won't give it to us. - serv = [os.path.normpath(os.path.abspath(sys.argv[0]))] - - # Convert the tuples to list - for arg in args: - serv.append(arg[0]) - if arg[1]: - serv.append(arg[1]) - - # Make sure we run in daemon mode - serv.append('-d') - return serv - - SERVICE_MSG = """ -You may need to set additional Service parameters. -Run services.msc from a command prompt. +You may need to set additional Service parameters! +Verify the settings in Windows Services (services.msc). -Don't forget to install the Service SABnzbd-helper.exe too! +https://sabnzbd.org/wiki/advanced/sabnzbd-as-a-windows-service """ -def HandleCommandLine(allow_service=True): - """ Handle command line for a Windows Service - Prescribed name that will be called by Py2Exe. - You MUST set 'cmdline_style':'custom' in the package.py! - Returns True when any service commands were detected. +def handle_windows_service(): + """ Handle everything for Windows Service + Returns True when any service commands were detected or + when we have started as a service. """ - service, sab_opts, serv_opts, _upload_nzbs = commandline_handler() - if service and not allow_service: - # The other frozen apps don't support Services - print("For service support, use SABnzbd-service.exe") + # Detect if running as Windows Service (only Vista and above!) + # Adapted from https://stackoverflow.com/a/55248281/5235502 + if win32ts.ProcessIdToSessionId(win32api.GetCurrentProcessId()) == 0: + servicemanager.Initialize() + servicemanager.PrepareToHostSingle(SABnzbd) + servicemanager.StartServiceCtrlDispatcher() return True - elif service: + + # Handle installation and other options + service, sab_opts, serv_opts, _upload_nzbs = commandline_handler() + + if service: if service in ('install', 'update'): # In this case check for required parameters path = get_f_option(sab_opts) @@ -1613,14 +1576,14 @@ def HandleCommandLine(allow_service=True): win32serviceutil.HandleCommandLine(SABnzbd, argv=serv_opts) # Add our own parameter to the Registry - sab_opts = prep_service_parms(sab_opts) if set_serv_parms(SABnzbd._svc_name_, sab_opts): print(SERVICE_MSG) else: - print('Cannot set required Registry info.') + print('ERROR: Cannot set required registry info.') else: - # Other service commands need no manipulation + # Pass the other commands directly win32serviceutil.HandleCommandLine(SABnzbd) + return bool(service) @@ -1635,7 +1598,7 @@ if __name__ == '__main__': signal.signal(signal.SIGTERM, sabnzbd.sig_handler) if sabnzbd.WIN32: - if not HandleCommandLine(allow_service=not hasattr(sys, "frozen")): + if not handle_windows_service(): main() elif sabnzbd.DARWIN and sabnzbd.FOUNDATION: diff --git a/sabnzbd/__init__.py b/sabnzbd/__init__.py index b82aa06..2e7b06d 100644 --- a/sabnzbd/__init__.py +++ b/sabnzbd/__init__.py @@ -754,10 +754,11 @@ def system_standby(): def shutdown_program(): """ Stop program after halting and saving """ - logging.info("[%s] Performing SABnzbd shutdown", misc.caller_name()) - sabnzbd.halt() - cherrypy.engine.exit() - sabnzbd.SABSTOP = True + if not sabnzbd.SABSTOP: + logging.info("[%s] Performing SABnzbd shutdown", misc.caller_name()) + sabnzbd.halt() + cherrypy.engine.exit() + sabnzbd.SABSTOP = True def restart_program(): diff --git a/sabnzbd/misc.py b/sabnzbd/misc.py index 7379134..97c3504 100644 --- a/sabnzbd/misc.py +++ b/sabnzbd/misc.py @@ -281,28 +281,36 @@ def get_serv_parms(service): """ Get the service command line parameters from Registry """ import winreg - value = [] + service_parms = [] try: key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, _SERVICE_KEY + service) for n in range(winreg.QueryInfoKey(key)[1]): - name, value, _val_type = winreg.EnumValue(key, n) + name, service_parms, _val_type = winreg.EnumValue(key, n) if name == _SERVICE_PARM: break winreg.CloseKey(key) except WindowsError: pass - for n in range(len(value)): - value[n] = value[n] - return value + + # Always add the base program + service_parms.insert(0, os.path.normpath(os.path.abspath(sys.argv[0]))) + + return service_parms def set_serv_parms(service, args): """ Set the service command line parameters in Registry """ import winreg + serv = [] + for arg in args: + serv.append(arg[0]) + if arg[1]: + serv.append(arg[1]) + try: key = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, _SERVICE_KEY + service) - winreg.SetValueEx(key, _SERVICE_PARM, None, winreg.REG_MULTI_SZ, args) + winreg.SetValueEx(key, _SERVICE_PARM, None, winreg.REG_MULTI_SZ, serv) winreg.CloseKey(key) except WindowsError: return False diff --git a/tests/test_win_utils.py b/tests/test_win_utils.py index 97f7d17..b16e3da 100644 --- a/tests/test_win_utils.py +++ b/tests/test_win_utils.py @@ -16,13 +16,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ -tests.test_win_utils - Testing mailslot communiction on Windows +tests.test_win_utils - Testing Windows utils """ -import subprocess import sys -import time - import pytest if not sys.platform.startswith("win"): @@ -31,29 +28,6 @@ if not sys.platform.startswith("win"): import util.apireg as ar -class TestMailslot: - def test_mailslot_basic(self): - """ Do the basic testing provided by the module """ - # Start async both processes - server_p = subprocess.Popen([sys.executable, "util/mailslot.py", "server"], stdout=subprocess.PIPE) - # Need to pause to give server time to listen - time.sleep(0.5) - client_p = subprocess.Popen([sys.executable, "util/mailslot.py", "client"], stdout=subprocess.PIPE) - - # Server outputs basic response - assert server_p.stdout.readlines() == [ - b"restart\r\n", - b"restart\r\n", - b"restart\r\n", - b"restart\r\n", - b"restart\r\n", - b"stop\r\n", - ] - - # Client outputs nothing - assert not client_p.stdout.readlines() - - class TestAPIReg: def test_set_get_connection_info_user(self): """ Test the saving of the URL in USER-registery diff --git a/tools/extract_pot.py b/tools/extract_pot.py index e9b1589..1bc1e84 100755 --- a/tools/extract_pot.py +++ b/tools/extract_pot.py @@ -55,7 +55,7 @@ DOMAIN = "SABnzbd" DOMAIN_EMAIL = "SABemail" DOMAIN_NSIS = "SABnsis" PARMS = "-d %s -p %s -w500 -k T -k TT -o %s.pot.tmp" % (DOMAIN, PO_DIR, DOMAIN) -FILES = "SABnzbd.py SABHelper.py SABnzbdDelegate.py sabnzbd/*.py sabnzbd/utils/*.py" +FILES = "SABnzbd.py sabnzbd/*.py sabnzbd/utils/*.py" FILE_CACHE = {} diff --git a/util/mailslot.py b/util/mailslot.py deleted file mode 100644 index 37e90ce..0000000 --- a/util/mailslot.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/python3 -OO -# Copyright 2007-2020 The SABnzbd-Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -""" -sabnzbd.mailslot - Mailslot communication -""" - -import sys -from time import sleep -from win32file import GENERIC_WRITE, FILE_SHARE_READ, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL -from ctypes import c_uint, c_buffer, byref, sizeof, windll - -# Win32API Shortcuts -CreateFile = windll.kernel32.CreateFileW -ReadFile = windll.kernel32.ReadFile -WriteFile = windll.kernel32.WriteFile -CloseHandle = windll.kernel32.CloseHandle -CreateMailslot = windll.kernel32.CreateMailslotW - - -class MailSlot: - """ Simple Windows Mailslot communication """ - - slotname = r"mailslot\SABnzbd\ServiceSlot" - - def __init__(self): - self.handle = -1 - - def create(self, timeout): - """ Create the Mailslot, after this only receiving is possible - timeout is the read timeout used for receive calls. - """ - slot = r"\\.\%s" % MailSlot.slotname - self.handle = CreateMailslot(slot, 0, timeout, None) - - return self.handle != -1 - - def connect(self): - """ Connect to existing Mailslot so that writing is possible """ - slot = r"\\.\%s" % MailSlot.slotname - self.handle = CreateFile(slot, GENERIC_WRITE, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) - return self.handle != -1 - - def disconnect(self): - """ Disconnect from Mailslot """ - if self.handle != -1: - CloseHandle(self.handle) - self.handle = -1 - return True - - def send(self, command): - """ Send one message in bytes to Mailslot """ - if self.handle == -1: - return False - w = c_uint() - command_bytes = command.encode("utf-8") - return bool(WriteFile(self.handle, command_bytes, len(command_bytes), byref(w), 0)) - - def receive(self): - """ Receive one message from Mailslot, convert back to unicode """ - r = c_uint() - buf = c_buffer(1024) - if ReadFile(self.handle, buf, sizeof(buf), byref(r), 0): - return buf.value.decode("utf-8") - else: - return None - - -############################################################################## -# Simple test -# -# First start "mailslot.py server" in one process, -# Then start "mailslot.py client" in another. -# Five "restart" and one "stop" will be send from client to server. -# The server will stop after receiving "stop" -############################################################################## - -if __name__ == "__main__": - if not __debug__: - print("Run this test in non-optimized mode") - exit(1) - - if len(sys.argv) > 1 and "server" in sys.argv[1]: - - recv = MailSlot() - ret = recv.create(2) - assert ret, "Failed to create" - while True: - data = recv.receive() - if data is not None: - print(data) - if data.startswith("stop"): - break - sleep(1.0) - recv.disconnect() - - elif len(sys.argv) > 1 and "client" in sys.argv[1]: - - send = MailSlot() - ret = send.connect() - assert ret, "Failed to connect" - for n in range(5): - ret = send.send("restart") - assert ret, "Failed to send" - sleep(1.0) - send.send("stop") - assert ret, "Failed to send" - send.disconnect() - - else: - print("Usage: mailslot.py server|client")