Browse Source

Restore Windows Service functionality

pull/1426/head
Safihre 5 years ago
parent
commit
233ab2ee6e
  1. 154
      SABHelper.py
  2. 123
      SABnzbd.py
  3. 9
      sabnzbd/__init__.py
  4. 20
      sabnzbd/misc.py
  5. 28
      tests/test_win_utils.py
  6. 2
      tools/extract_pot.py
  7. 125
      util/mailslot.py

154
SABHelper.py

@ -1,154 +0,0 @@
#!/usr/bin/python3 -OO
# Copyright 2007-2020 The SABnzbd-Team <team@sabnzbd.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import sys
if sys.version_info[:2] < (2, 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)

123
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:

9
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():

20
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

28
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

2
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 = {}

125
util/mailslot.py

@ -1,125 +0,0 @@
#!/usr/bin/python3 -OO
# Copyright 2007-2020 The SABnzbd-Team <team@sabnzbd.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
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")
Loading…
Cancel
Save