Browse Source

Allow SABnzbd to be run as a Windows Service.

Implemented:
- Full service-enabled, install/update/start/stop
- Command-line parameters are stored in Service's reg-entry

To be done:
- Requires better Win logging
- Restart doesn't work from UI
- Support in installer
- Translated texts

Addresses #12 for trunk.
tags/0.6.0
shypike 16 years ago
parent
commit
c860d9f4cb
  1. 359
      main/SABnzbd.py
  2. 74
      main/package.py
  3. 1
      main/sabnzbd/__init__.py
  4. 42
      main/sabnzbd/misc.py

359
main/SABnzbd.py

@ -67,25 +67,35 @@ import sabnzbd.newsunpack
from sabnzbd.misc import get_user_shellfolders, launch_a_browser, real_path, \
check_latest_version, panic_tmpl, panic_port, panic_fwall, panic, exit_sab, \
panic_xport, notify, split_host, convert_version, get_ext, create_https_certificates, \
windows_variant, ip_extract
windows_variant, ip_extract, set_serv_parms, get_serv_parms
import sabnzbd.scheduler as scheduler
import sabnzbd.config as config
import sabnzbd.cfg
import sabnzbd.downloader as downloader
from sabnzbd.codecs import unicoder
from sabnzbd.lang import T, Ta
from sabnzbd.utils import osx
from threading import Thread
LOG_FLAG = False # Global for this module, signalling loglevel change
LOG_FLAG = False # Global for this module, signalling loglevel change
_first_log = True
def FORCELOG(txt):
global _first_log
if _first_log:
os.remove('d:/temp/debug.txt')
_first_log = False
ff = open('d:/temp/debug.txt', 'a+')
ff.write(txt)
ff.write('\n')
ff.close()
#------------------------------------------------------------------------------
signal.signal(signal.SIGINT, sabnzbd.sig_handler)
signal.signal(signal.SIGTERM, sabnzbd.sig_handler)
#------------------------------------------------------------------------------
try:
import win32api
import win32serviceutil, win32evtlogutil, win32event, win32service, pywintypes
win32api.SetConsoleCtrlHandler(sabnzbd.sig_handler, True)
except ImportError:
if sabnzbd.WIN32:
@ -641,77 +651,86 @@ def cherrypy_logging(log_path):
#------------------------------------------------------------------------------
def main():
global LOG_FLAG
AUTOBROWSER = None
autorestarted = False
def commandline_handler(frozen=True):
""" Split win32-service commands are true parameters
Returns:
service, sab_opts, serv_opts, upload_nzbs
"""
service = ''
sab_opts = []
serv_inst = False
serv_opts = [os.path.normpath(os.path.abspath(sys.argv[0]))]
upload_nzbs = []
sabnzbd.MY_FULLNAME = os.path.normpath(os.path.abspath(sys.argv[0]))
sabnzbd.MY_NAME = os.path.basename(sabnzbd.MY_FULLNAME)
sabnzbd.DIR_PROG = os.path.dirname(sabnzbd.MY_FULLNAME)
sabnzbd.DIR_INTERFACES = real_path(sabnzbd.DIR_PROG, DEF_INTERFACES)
sabnzbd.DIR_LANGUAGE = real_path(sabnzbd.DIR_PROG, DEF_LANGUAGE)
org_dir = os.getcwd()
# Ugly hack to remove the extra "SABnzbd*" parameter the Windows binary
# gets when it's restarted
if len(sys.argv) > 1 and \
'sabnzbd' in sys.argv[1].lower() and \
not sys.argv[1].startswith('-'):
slice = 2
else:
slice = 1
if getattr(sys, 'frozen', None) == 'macosx_app':
# Correct path if frozen with py2app (OSX)
sabnzbd.MY_FULLNAME = sabnzbd.MY_FULLNAME.replace("/Resources/SABnzbd.py","/MacOS/SABnzbd")
# Prepend options from env-variable to options
info = os.environ.get('SABnzbd', '').split()
info.extend(sys.argv[slice:])
# Need console logging for SABnzbd.py and SABnzbd-console.exe
consoleLogging = (not hasattr(sys, "frozen")) or (sabnzbd.MY_NAME.lower().find('-console') > 0)
try:
opts, args = getopt.getopt(info, "phdvncw:l:s:f:t:b:2:",
['pause', 'help', 'daemon', 'nobrowser', 'clean', 'logging=',
'weblogging=', 'server=', 'templates',
'template2', 'browser=', 'config-file=', 'delay=', 'force',
'version', 'https=', 'autorestarted',
# Below Win32 Service options
'password=', 'username=', 'startup=', 'perfmonini=', 'perfmondll=',
'interactive', 'wait=',
])
except getopt.GetoptError:
print_help()
exit_sab(2)
# No console logging needed for OSX app
noConsoleLoggingOSX = (sabnzbd.DIR_PROG.find('.app/Contents/Resources') > 0)
if noConsoleLoggingOSX:
consoleLogging = 1
# Check for Win32 service commands
if args and args[0] in ('install', 'update', 'remove', 'start', 'stop', 'restart', 'debug'):
service = args[0]
serv_opts.extend(args)
LOGLEVELS = (logging.WARNING, logging.INFO, logging.DEBUG)
if not service:
# Get and remove any NZB file names
for entry in args:
if get_ext(entry) in ('.nzb', '.zip','.rar', '.nzb.gz'):
upload_nzbs.append(entry)
# Setup primary logging to prevent default console logging
gui_log = guiHandler(MAX_WARNINGS)
gui_log.setLevel(logging.WARNING)
format_gui = '%(asctime)s\n%(levelname)s\n%(message)s'
gui_log.setFormatter(logging.Formatter(format_gui))
sabnzbd.GUIHANDLER = gui_log
# Create logger
logger = logging.getLogger('')
logger.setLevel(logging.WARNING)
logger.addHandler(gui_log)
for opt, arg in opts:
if opt in ('password','username','startup','perfmonini', 'perfmondll', 'interactive', 'wait'):
# Service option, just collect
if service:
serv_opts.append(opt)
if arg:
serv_opts.append(arg)
else:
if opt == '-f':
arg = os.path.normpath(os.path.abspath(arg))
sab_opts.append((opt, arg))
# Create a list of passed files to load on startup
# or pass to an already running instance of sabnzbd
upload_nzbs = []
for entry in sys.argv:
if get_ext(entry) in ('.nzb','.zip','.rar', '.nzb.gz'):
upload_nzbs.append(entry)
sys.argv.remove(entry)
return service, sab_opts, serv_opts, upload_nzbs
try:
# Ugly hack to remove the extra "SABnzbd*" parameter the Windows binary
# gets when it's restarted
if len(sys.argv) > 1 and \
'sabnzbd' in sys.argv[1].lower() and \
not sys.argv[1].startswith('-'):
slice = 2
else:
slice = 1
def get_f_option(opts):
""" Return value of the -f option """
for opt, arg in opts:
if opt == '-f':
return arg
else:
return None
# Prepend options from env-variable to options
args = os.environ.get('SABnzbd', '').split()
args.extend(sys.argv[slice:])
opts, args = getopt.getopt(args, "phdvncw:l:s:f:t:b:2:",
['pause', 'help', 'daemon', 'nobrowser', 'clean', 'logging=',
'weblogging=', 'server=', 'templates',
'template2', 'browser=', 'config-file=', 'delay=', 'force',
'version', 'https=', 'autorestarted'])
except getopt.GetoptError:
print_help()
exit_sab(2)
#------------------------------------------------------------------------------
def main():
global LOG_FLAG
AUTOBROWSER = None
autorestarted = False
sabnzbd.MY_FULLNAME = sys.argv[0]
fork = False
pause = False
inifile = None
@ -729,21 +748,25 @@ def main():
force_web = False
re_argv = [sys.argv[0]]
for opt, arg in opts:
if (opt in ('-d', '--daemon')):
service, sab_opts, serv_opts, upload_nzbs = commandline_handler()
for opt, arg in sab_opts:
if opt == '--servicecall':
sabnzbd.MY_FULLNAME = arg
elif opt in ('-d', '--daemon'):
if not sabnzbd.WIN32:
fork = True
AUTOBROWSER = False
sabnzbd.DAEMON = True
consoleLogging = False
re_argv.append(opt)
elif opt in ('-h', '--help'):
print_help()
exit_sab(0)
elif opt in ('-f', '--config-file'):
inifile = arg
re_argv.append(opt)
re_argv.append(arg)
elif opt in ('-h', '--help'):
print_help()
exit_sab(0)
elif opt in ('-t', '--templates'):
web_dir = arg
elif opt in ('-2', '--template2'):
@ -760,7 +783,7 @@ def main():
elif opt in ('--autorestarted'):
autorestarted = True
elif opt in ('-c', '--clean'):
clean_up= True
clean_up = True
elif opt in ('-w', '--weblogging'):
try:
cherrypylogging = int(arg)
@ -782,20 +805,53 @@ def main():
exit_sab(0)
elif opt in ('-p', '--pause'):
pause = True
elif opt in ('--delay'):
elif opt in ('--delay',):
# For debugging of memory leak only!!
try:
delay = float(arg)
except:
pass
elif opt in ('--force'):
pass
elif opt in ('--force',):
force_web = True
re_argv.append(opt)
elif opt in ('--https'):
elif opt in ('--https',):
https_port = int(arg)
re_argv.append(opt)
re_argv.append(arg)
sabnzbd.MY_FULLNAME = os.path.normpath(os.path.abspath(sabnzbd.MY_FULLNAME))
sabnzbd.MY_NAME = os.path.basename(sabnzbd.MY_FULLNAME)
sabnzbd.DIR_PROG = os.path.dirname(sabnzbd.MY_FULLNAME)
sabnzbd.DIR_INTERFACES = real_path(sabnzbd.DIR_PROG, DEF_INTERFACES)
sabnzbd.DIR_LANGUAGE = real_path(sabnzbd.DIR_PROG, DEF_LANGUAGE)
org_dir = os.getcwd()
if getattr(sys, 'frozen', None) == 'macosx_app':
# Correct path if frozen with py2app (OSX)
sabnzbd.MY_FULLNAME = sabnzbd.MY_FULLNAME.replace("/Resources/SABnzbd.py","/MacOS/SABnzbd")
# Need console logging for SABnzbd.py and SABnzbd-console.exe
consoleLogging = (not hasattr(sys, "frozen")) or (sabnzbd.MY_NAME.lower().find('-console') > 0)
# No console logging needed for OSX app
noConsoleLoggingOSX = (sabnzbd.DIR_PROG.find('.app/Contents/Resources') > 0)
if noConsoleLoggingOSX:
consoleLogging = 1
LOGLEVELS = (logging.WARNING, logging.INFO, logging.DEBUG)
# Setup primary logging to prevent default console logging
gui_log = guiHandler(MAX_WARNINGS)
gui_log.setLevel(logging.WARNING)
format_gui = '%(asctime)s\n%(levelname)s\n%(message)s'
gui_log.setFormatter(logging.Formatter(format_gui))
sabnzbd.GUIHANDLER = gui_log
# Create logger
logger = logging.getLogger('')
logger.setLevel(logging.WARNING)
logger.addHandler(gui_log)
# Detect Windows variant
if sabnzbd.WIN32:
vista_plus, vista64 = windows_variant()
@ -836,12 +892,24 @@ def main():
# Determine web host address
cherryhost, cherryport, browserhost, https_port = get_webhost(cherryhost, cherryport, https_port)
enable_https = sabnzbd.cfg.ENABLE_HTTPS.get()
# When this is a daemon, just check and bail out if port in use
if sabnzbd.DAEMON:
if enable_https and https_port:
try:
cherrypy.process.servers.check_port(cherryhost, https_port)
except IOError, error:
Bail_Out(browserhost, cherryport)
try:
cherrypy.process.servers.check_port(cherryhost, cherryport)
except IOError, error:
Bail_Out(browserhost, cherryport)
# If an instance of sabnzbd(same version) is already running on this port, launch the browser
# If another program or sabnzbd version is on this port, try 10 other ports going up in a step of 5
# If 'Port is not bound' (firewall) do not do anything (let the script further down deal with that).
## SSL
enable_https = sabnzbd.cfg.ENABLE_HTTPS.get()
if enable_https and https_port:
try:
cherrypy.process.servers.check_port(browserhost, https_port)
@ -979,6 +1047,10 @@ def main():
# Find external programs
sabnzbd.newsunpack.find_programs(sabnzbd.DIR_PROG)
if not sabnzbd.WIN_SERVICE:
signal.signal(signal.SIGINT, sabnzbd.sig_handler)
signal.signal(signal.SIGTERM, sabnzbd.sig_handler)
init_ok = sabnzbd.initialize(pause, clean_up, evalSched=True)
if not init_ok:
@ -1149,7 +1221,16 @@ def main():
# Have to keep this running, otherwise logging will terminate
timer = 0
while not sabnzbd.SABSTOP:
time.sleep(3)
if sabnzbd.WIN_SERVICE:
rc = win32event.WaitForMultipleObjects((sabnzbd.WIN_SERVICE.hWaitStop,
sabnzbd.WIN_SERVICE.overlapped.hEvent), 0, 3000)
if rc == win32event.WAIT_OBJECT_0:
sabnzbd.save_state()
logging.info('Leaving SABnzbd')
sabnzbd.SABSTOP = True
return
else:
time.sleep(3)
# Check for loglevel changes
if LOG_FLAG:
@ -1202,6 +1283,9 @@ def main():
pid = os.fork()
if pid == 0:
os.execv(sys.executable, args)
elif sabnzbd.WIN_SERVICE:
# Hope for the service manager to restart us
sys.exit(1)
else:
cherrypy.engine._do_execv()
@ -1218,22 +1302,126 @@ def main():
os._exit(0)
#####################################################################
#
# Platform specific startup code
# Windows Service Support
#
if sabnzbd.WIN32:
import servicemanager
class SABnzbd(win32serviceutil.ServiceFramework):
""" Win32 Service Handler """
_svc_name_ = 'SABnzbd'
_svc_display_name_ = 'SABnzbd Binary Newsreader'
_svc_deps_ = ["EventLog", "Tcpip"]
_svc_description_ = 'Automated downloading from Usenet. ' \
'Set to "automatic" to start the service at system startup. ' \
'You may need to login with a real user account when you need ' \
'access to network shares.'
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):
msg = 'SABnzbd-service %s' % sabnzbd.__version__
self.Logger(servicemanager.PYS_SERVICE_STARTED, msg + ' has started')
sys.argv = get_serv_parms(self._svc_name_)
main()
self.Logger(servicemanager.PYS_SERVICE_STOPPED, msg + ' has stopped')
if not getattr(sys, 'frozen', None) == 'macosx_app':
# Windows & Unix/Linux
def SvcStop(self):
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
win32event.SetEvent(self.hWaitStop)
if __name__ == '__main__':
main()
def Logger(self, state, msg):
win32evtlogutil.ReportEvent(self._svc_display_name_,
state, 0,
servicemanager.EVENTLOG_INFORMATION_TYPE,
(self._svc_name_, unicoder(msg)))
def ErrLogger(self, msg, text):
win32evtlogutil.ReportEvent(self._svc_display_name_,
servicemanager.PYS_SERVICE_STOPPED, 0,
servicemanager.EVENTLOG_ERROR_TYPE,
(self._svc_name_, unicoder(msg)),
unicoder(text))
def prep_service_parms(args):
""" Prepare parameter list for service """
else:
# 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]))]
# OSX
# 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
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.
"""
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"
return True
elif service:
if service in ('install', 'update'):
# In this case check for required parameters
path = get_f_option(sab_opts)
if not path:
print 'The -f <path> parameter is required.\n' \
'Use: -f <path> %s' % service
return True
# First run the service installed, because this will
# set the service key in the Registry
win32serviceutil.HandleCommandLine(SABnzbd, argv=serv_opts)
# Add our own parameter to the Registry
sab_opts = prep_service_parms(sab_opts)
if set_serv_parms(SABnzbd._svc_name_, sab_opts):
print '\nYou may need to set additional Service parameters.\n' \
'Run services.msc from a command prompt.\n'
else:
print 'Cannot set required Registry info.'
else:
# Other service commands need no manipulation
win32serviceutil.HandleCommandLine(SABnzbd)
return bool(service)
#####################################################################
#
# Platform specific startup code
#
if __name__ == '__main__':
if sabnzbd.WIN32:
if not HandleCommandLine(allow_service=not hasattr(sys, "frozen")):
main()
elif getattr(sys, 'frozen', None) == 'macosx_app':
# OSX binary
if __name__ == '__main__':
try:
from PyObjCTools import AppHelper
from SABnzbdDelegate import SABnzbdDelegate
@ -1260,3 +1448,6 @@ else:
except:
main()
else:
main()

74
main/package.py

@ -201,6 +201,18 @@ def Unix2Dos(name):
exit(1)
def rename_file(folder, old, new):
try:
oldpath = "%s/%s" % (folder, old)
newpath = "%s/%s" % (folder, new)
if os.path.exists(newpath):
os.remove(newpath)
os.rename(oldpath, newpath)
except WindowsError:
print "Cannot create %s" % newpath
exit(1)
print sys.argv[0]
#OSX if svnversion not installed install SCPlugin and execute these commands
@ -228,7 +240,7 @@ if len(sys.argv) < 2:
else:
target = sys.argv[1]
if target not in ('source', 'binary', 'app'):
if target not in ('source', 'binary', 'installer', 'app'):
print 'Usage: package.py binary|source|app'
exit(1)
@ -236,8 +248,10 @@ if target not in ('source', 'binary', 'app'):
base, release = os.path.split(os.getcwd())
prod = 'SABnzbd-' + release
Win32ServiceName = 'SABnzbd-service.exe'
Win32ConsoleName = 'SABnzbd-console.exe'
Win32WindowName = 'SABnzbd.exe'
Win32TempName = 'SABnzbd-windows.exe'
fileIns = prod + '-win32-setup.exe'
fileBin = prod + '-win32-bin.zip'
@ -388,7 +402,7 @@ if target == 'app':
os.system(SvnRevertApp + VERSION_FILE)
os.system(SvnUpdateApp)
elif target == 'binary':
elif target in ('binary', 'installer'):
if not py2exe:
print "Sorry, only works on Windows!"
os.system(SvnRevert)
@ -414,16 +428,13 @@ elif target == 'binary':
}
options['zipfile'] = 'lib/sabnzbd.zip'
############################
# Generate the console-app
options['console'] = program
setup(**options)
try:
if os.path.exists("dist/%s" % Win32ConsoleName):
os.remove("dist/%s" % Win32ConsoleName)
os.rename("dist/%s" % Win32WindowName, "dist/%s" % Win32ConsoleName)
except:
print "Cannot create dist/%s" % Win32ConsoleName
exit(1)
rename_file('dist', Win32WindowName, Win32ConsoleName)
# Make sure that the root files are DOS format
for file in options['data_files'][0][1]:
@ -431,26 +442,45 @@ elif target == 'binary':
DeleteFiles('dist/Sample-PostProc.sh')
DeleteFiles('dist/PKG-INFO')
# Generate the windowed-app (skip datafiles now)
del options['console']
del options['data_files']
DeleteFiles('*.ini')
if sys.version_info < (2,6):
# Copy the proper OpenSSL files into the dist folder
shutil.copy2('win/openssl/libeay32.dll', 'dist/lib')
shutil.copy2('win/openssl/ssleay32.dll', 'dist/lib')
############################
# Generate the windowed-app
options['windows'] = program
del options['data_files']
del options['console']
setup(**options)
rename_file('dist', Win32WindowName, Win32TempName)
############################
# Generate the service-app
options['service'] = [{'modules':["SABnzbd"], 'cmdline_style':'custom'}]
del options['windows']
setup(**options)
rename_file('dist', Win32WindowName, Win32ServiceName)
# Give the Windows app its proper name
rename_file('dist', Win32TempName, Win32WindowName)
DeleteFiles('*.ini')
# Copy the proper OpenSSL files into the dist folder
shutil.copy2('win/openssl/libeay32.dll', 'dist/lib')
shutil.copy2('win/openssl/ssleay32.dll', 'dist/lib')
############################
if target == 'installer':
os.system('makensis.exe /v3 /DSAB_PRODUCT=%s /DSAB_FILE=%s NSIS_Installer.nsi' % \
(release, fileIns))
os.system('makensis.exe /v3 /DSAB_PRODUCT=%s /DSAB_FILE=%s NSIS_Installer.nsi' % \
(release, fileIns))
DeleteFiles(fileBin)
os.rename('dist', prod)
os.system('zip -9 -r -X %s %s' % (fileBin, prod))
os.rename(prod, 'dist')
DeleteFiles(fileBin)
os.rename('dist', prod)
os.system('zip -9 -r -X %s %s' % (fileBin, prod))
os.rename(prod, 'dist')
os.system(SvnRevert)
else:

1
main/sabnzbd/__init__.py

@ -112,6 +112,7 @@ WEBLOGFILE = None
LOGHANDLER = None
GUIHANDLER = None
AMBI_LOCALHOST = False
WIN_SERVICE = None # Instance of our Win32 Service Class
WEB_DIR = None
WEB_DIR2 = None

42
main/sabnzbd/misc.py

@ -309,6 +309,48 @@ def windows_variant():
return vista_plus, x64
#------------------------------------------------------------------------------
_SERVICE_KEY = 'SYSTEM\\CurrentControlSet\\services\\'
_SERVICE_PARM = 'CommandLine'
def get_serv_parms(service):
""" Get the service command line parameters from Registry """
import _winreg
value = []
try:
key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, _SERVICE_KEY + service)
for n in xrange(_winreg.QueryInfoKey(key)[1]):
name, value, val_type = _winreg.EnumValue(key, n)
if name == _SERVICE_PARM:
break
_winreg.CloseKey(key)
except WindowsError:
pass
for n in xrange(len(value)):
value[n] = latin1(value[n])
return value
def set_serv_parms(service, args):
""" Set the service command line parameters in Registry """
import _winreg
uargs = []
for arg in args:
uargs.append(unicoder(arg))
try:
key = _winreg.CreateKey(_winreg.HKEY_LOCAL_MACHINE, _SERVICE_KEY + service)
_winreg.SetValueEx(key, _SERVICE_PARM, None, _winreg.REG_MULTI_SZ, uargs)
_winreg.CloseKey(key)
except WindowsError:
return False
return True
################################################################################
# Launch a browser for various purposes
# including panic messages

Loading…
Cancel
Save