View | Details | Raw Unified | Return to bug 234537 | Differences between
and this patch

Collapse All | Expand All

(-)GIDs (-1 / +1 lines)
Lines 295-301 Link Here
295
radarr:*:352:
295
radarr:*:352:
296
_iodined:*:353:
296
_iodined:*:353:
297
jackett:*:354:
297
jackett:*:354:
298
# free: 355
298
nzbhydra2:*:355:
299
# free: 356
299
# free: 356
300
# free: 357
300
# free: 357
301
# free: 358
301
# free: 358
(-)UIDs (-1 / +1 lines)
Lines 301-307 Link Here
301
_iodined:*:353:353::0:0:Iodine Daemon:/nonexistent:/usr/sbin/nologin
301
_iodined:*:353:353::0:0:Iodine Daemon:/nonexistent:/usr/sbin/nologin
302
jackett:*:354:354::0:0:Jackett Torznab Proxy Daemon:/nonexistent:/usr/sbin/nologin
302
jackett:*:354:354::0:0:Jackett Torznab Proxy Daemon:/nonexistent:/usr/sbin/nologin
303
# free: 354
303
# free: 354
304
# free: 355
304
nzbhydra2:*:355:355::0:0:NZBHydra 2 Daemon:/nonexistent:/usr/sbin/nologin
305
# free: 356
305
# free: 356
306
# free: 357
306
# free: 357
307
# free: 358
307
# free: 358
(-)news/Makefile (+1 lines)
Lines 46-51 Link Here
46
    SUBDIR += nntpcache
46
    SUBDIR += nntpcache
47
    SUBDIR += noffle
47
    SUBDIR += noffle
48
    SUBDIR += nzbget
48
    SUBDIR += nzbget
49
    SUBDIR += nzbhydra2
49
    SUBDIR += nzbperl
50
    SUBDIR += nzbperl
50
    SUBDIR += p5-NNTPClient
51
    SUBDIR += p5-NNTPClient
51
    SUBDIR += p5-News-Article
52
    SUBDIR += p5-News-Article
(-)news/nzbhydra2/Makefile (+52 lines)
Added Link Here
1
# $FreeBSD$
2
3
PORTNAME=	nzbhydra2
4
DISTVERSION=	2.2.0
5
CATEGORIES=	news java
6
MASTER_SITES=	https://github.com/theotherp/${PORTNAME}/releases/download/v${DISTVERSION}/
7
DISTNAME=	${PORTNAME}-${DISTVERSION}-linux
8
9
MAINTAINER=	daniel@shafer.cc
10
COMMENT=	Usenet meta search
11
12
LICENSE=	APACHE20
13
LICENSE_FILE=	${WRKSRC}/LICENSE
14
15
ONLY_FOR_ARCHS= amd64
16
17
USES=		python shebangfix zip
18
USE_JAVA=	yes
19
USE_RC_SUBR=	nzbhydra2
20
21
NO_BUILD=	yes
22
NO_WRKSUBDIR=	yes
23
24
SUB_FILES=	nzbhydra2 nzbhydra2wrapper.py
25
SUB_LIST=	PYTHON_CMD=${PYTHON_CMD} \
26
		JAVA=${JAVA}
27
28
JAVA_VERSION=	1.8 1.9 1.10
29
JAVA_VENDOR=	openjdk
30
JAVA_RUN=	yes
31
32
USERS=		nzbhydra2
33
GROUPS=		nzbhydra2
34
35
PLIST_FILES=	${DATADIR}/lib/core-2.2.0-exec.jar \
36
		${DATADIR}/nzbhydra2 \
37
		${DATADIR}/nzbhydra2wrapper.py \
38
		${DATADIR}/changelog.md \
39
		${DATADIR}/readme.md \
40
		${DATADIR}/LICENSE
41
42
post-extract:
43
	# Cleanup unnecessary files
44
	@${RM} -r ${WRKSRC}/systemd ${WRKSRC}/sysv ${WRKSRC}/rc.d ${WRKSRC}/upstart
45
46
do-install:
47
	@${MKDIR} ${STAGEDIR}/${DATADIR}
48
	${INSTALL} -d -m 755 ${STAGEDIR}/${DATADIR}
49
	cd ${WRKSRC} && ${COPYTREE_SHARE} \* ${STAGEDIR}/${DATADIR}
50
	${INSTALL_DATA} ${WRKDIR}/nzbhydra2wrapper.py ${STAGEDIR}/${DATADIR}
51
52
.include <bsd.port.mk>
(-)news/nzbhydra2/distinfo (+3 lines)
Added Link Here
1
TIMESTAMP = 1546676442
2
SHA256 (nzbhydra2-2.2.0-linux.zip) = 4c1d8645dae530f136a34bfaafd6f778ee1a25b7ee8554db62058df61929b5df
3
SIZE (nzbhydra2-2.2.0-linux.zip) = 57881316
(-)news/nzbhydra2/files/nzbhydra2.in (+58 lines)
Added Link Here
1
#!/bin/sh
2
#
3
# $FreeBSD$
4
#
5
# PROVIDE: nzbhydra2
6
# REQUIRE: LOGIN
7
# KEYWORD: shutdown
8
#
9
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
10
# to enable this service:
11
#
12
# nzbhydra2_enable (bool):	Set to NO by default.
13
#			Set it to YES to enable it.
14
# nzbhydra2_user:	The user account nzbhydra daemon runs as what
15
#			you want it to be. It uses '_sabnzbd' user by
16
#			default. Do not sets it as empty or it will run
17
#			as root.
18
# nzbhydra2_group:	The group account nzbhydra daemon runs as what
19
#			you want it to be. It uses 'nzbhydra2' group by
20
#			default. Do not sets it as empty or it will run
21
#			as wheel.
22
# nzbhydra2_dir:	Directory where nzbhydra lives.
23
#			Default: %%PREFIX%%/share/nzbhydra2
24
# nzbhydra2_datafolder:	Data directory for nzbhydra (DB, Logs, config)
25
#			Default: %%PREFIX%%/nzbhydra2
26
27
. /etc/rc.subr
28
29
name="nzbhydra2"
30
rcvar=${name}_enable
31
32
load_rc_config ${name}
33
34
: ${nzbhydra2_enable:="NO"}
35
: ${nzbhydra2_user:="nzbhydra2"}
36
: ${nzbhydra2_group:="nzbhydra2"}
37
: ${nzbhydra2_dir:="%%PREFIX%%/share/nzbhydra2"}
38
: ${nzbhydra2_datafolder:="%%PREFIX%%/nzbhydra2"}
39
40
pidfile="/var/run/nzbhydra2/nzbhydra2.pid"
41
command="%%PYTHON_CMD%%"
42
command_args="${nzbhydra2_dir}/nzbhydra2wrapper.py --datafolder ${nzbhydra2_datafolder} --pidfile ${pidfile} --daemon --nobrowser --java %%JAVA%%"
43
44
start_precmd="prestart"
45
prestart() {
46
	if [ -f ${pidfile} ]; then
47
		rm -f ${pidfile}
48
		echo "Removing stale pidfile."
49
	elif [ ! -d ${pidfile%/*} ]; then
50
		install -d -o ${nzbhydra2_user} -g ${nzbhydra2_group} ${pidfile%/*}
51
	fi
52
53
	if [ ! -d ${nzbhydra2_datadir} ]; then
54
		install -d -o ${nzbhydra2_user} -g ${nzbhydra2_group} ${nzbhydra2_datadir}
55
	fi
56
}
57
58
run_rc_command "$1"
(-)news/nzbhydra2/files/nzbhydra2wrapper.py.in (+625 lines)
Added Link Here
1
#!%%PYTHON_CMD%%
2
from __future__ import print_function
3
import sys
4
5
CURRENT_PYTHON = sys.version_info[:2]
6
REQUIRED_PYTHON = (2, 7)
7
8
# This check and everything above must remain compatible with Python 2.7 and above.
9
if CURRENT_PYTHON > REQUIRED_PYTHON:
10
    sys.stderr.write("This script requires Python {}.{}, but you're trying to run it on Python {}.{}.".format(*(REQUIRED_PYTHON + CURRENT_PYTHON)))
11
    sys.exit(1)
12
13
import argparse
14
import datetime
15
import logging
16
import os
17
import platform
18
import re
19
import shutil
20
import subprocess
21
import zipfile
22
from __builtin__ import file
23
from logging.handlers import RotatingFileHandler
24
25
jarFile = None
26
basepath = None
27
args = []
28
unknownArgs = []
29
terminatedByWrapper = False
30
31
LOGGER_DEFAULT_FORMAT = u'%(asctime)s  %(levelname)s - %(message)s'
32
LOGGER_DEFAULT_LEVEL = 'INFO'
33
logger = logging.getLogger('root')
34
console_logger = logging.StreamHandler(sys.stdout)
35
console_logger.setFormatter(logging.Formatter(LOGGER_DEFAULT_FORMAT))
36
console_logger.setLevel(LOGGER_DEFAULT_LEVEL)
37
logger.addHandler(console_logger)
38
file_logger = None
39
logger.setLevel(LOGGER_DEFAULT_LEVEL)
40
consoleLines = []
41
42
def getBasePath():
43
    global basepath
44
    if basepath is not None:
45
        return basepath
46
    if "HYDRAWORKINGFOLDER" in os.environ.keys():
47
        return os.environ["HYDRAWORKINGFOLDER"]
48
    import sys
49
    if sys.executable:
50
        basepath = os.path.dirname(sys.executable)
51
        if os.path.exists(os.path.join(basepath, "readme.md")) and os.path.exists(os.path.join(basepath, "changelog.md")):
52
            return basepath
53
    basepath = os.path.dirname(os.path.abspath(sys.argv[0]))
54
    if os.path.exists(os.path.join(basepath, "readme.md")) and os.path.exists(os.path.join(basepath, "changelog.md")):
55
        return basepath
56
    try:
57
        basepath = os.path.dirname(os.path.abspath(__file__))
58
    except NameError:  # We are the main py2exe script, not a module
59
        import sys
60
        basepath = os.path.dirname(os.path.abspath(sys.argv[0]))
61
    return basepath
62
63
64
class GracefulKiller:
65
    def __init__(self):
66
        import signal
67
        signal.signal(signal.SIGINT, terminated)
68
        signal.signal(signal.SIGTERM, terminated)
69
70
71
def terminated(signum, frame):
72
    logger.info("Terminated by signal %d" % signum)
73
    killProcess()
74
75
76
def killProcess():
77
    if process is not None and process.poll() is None:
78
        global terminatedByWrapper
79
        logger.info("NZBHydra2 wrapper shutdown request. Terminating main process gracefully")
80
        terminatedByWrapper = True
81
        process.terminate()
82
83
84
def daemonize(pidfile, nopidfile):
85
    # Make a non-session-leader child process
86
    try:
87
        pid = os.fork()  # @UndefinedVariable - only available in UNIX
88
        if pid != 0:
89
            os._exit(0)
90
    except OSError as e:
91
        sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
92
        sys.exit(1)
93
94
    os.setsid()  # @UndefinedVariable - only available in UNIX
95
96
    # Make sure I can read my own files and shut out others
97
    prev = os.umask(0)
98
    os.umask(prev and int('077', 8))
99
100
    # Make the child a session-leader by detaching from the terminal
101
    try:
102
        pid = os.fork()  # @UndefinedVariable - only available in UNIX
103
        if pid != 0:
104
            os._exit(0)
105
    except OSError as e:
106
        sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
107
        sys.exit(1)
108
109
    # Write pid
110
    if not nopidfile:
111
        pid = str(os.getpid())
112
        try:
113
            file(pidfile, 'w').write("%s\n" % pid)
114
        except IOError as e:
115
            sys.stderr.write(u"Unable to write PID file: " + pidfile + ". Error: " + str(e.strerror) + " [" + str(e.errno) + "]")
116
            sys.exit(1)
117
    else:
118
        print("no pid file")
119
120
    # Redirect all output
121
    sys.stdout.flush()
122
    sys.stderr.flush()
123
124
    devnull = getattr(os, 'devnull', '/dev/null')
125
    stdin = file(devnull, 'r')
126
    stdout = file(devnull, 'a+')
127
    stderr = file(devnull, 'a+')
128
    os.dup2(stdin.fileno(), sys.stdin.fileno())
129
    os.dup2(stdout.fileno(), sys.stdout.fileno())
130
    os.dup2(stderr.fileno(), sys.stderr.fileno())
131
132
133
def setupLogger():
134
    logsFolder = os.path.join(args.datafolder, "logs")
135
    if not os.path.exists(logsFolder):
136
        os.makedirs(logsFolder)
137
    logfilename = os.path.join(logsFolder, "wrapper.log")
138
    if not args.quiet:
139
        print("Logging wrapper output to " + logfilename)
140
    if not args.quiet:
141
        console_logger.setLevel("INFO")
142
    else:
143
        console_logger.setLevel("CRITICAL")
144
    global file_logger
145
    file_logger = RotatingFileHandler(filename=logfilename, maxBytes=100000, backupCount=1)
146
    file_logger.setFormatter(logging.Formatter(LOGGER_DEFAULT_FORMAT))
147
    file_logger.setLevel("INFO")
148
    logger.addHandler(file_logger)
149
    logger.setLevel("INFO")
150
151
152
def update():
153
    global jarFile
154
    basePath = getBasePath()
155
    updateFolder = os.path.join(args.datafolder, "update")
156
    libFolder = os.path.join(basePath, "lib")
157
    isWindows = any([x for x in os.listdir(basePath) if x.lower().endswith(".exe")])
158
    logger.debug("Is Windows installation: %r", isWindows)
159
    if not os.path.exists(updateFolder):
160
        logger.critical("Error: Update folder %s does not exist", updateFolder)
161
        sys.exit(-2)
162
    onlyfiles = [f for f in os.listdir(updateFolder) if os.path.isfile(os.path.join(updateFolder, f))]
163
    if len(onlyfiles) != 1 or not onlyfiles[0].lower().endswith("zip"):
164
        logger.critical("Error: Unable to identify update ZIP")
165
        sys.exit(-2)
166
    updateZip = os.path.join(updateFolder, onlyfiles[0])
167
168
    try:
169
        with zipfile.ZipFile(updateZip, "r") as zf:
170
            logger.info("Extracting updated files to %s", basePath)
171
            for member in zf.namelist():
172
                if not member.lower() == "nzbhydra2" and not member.lower().endswith(".exe"):
173
                    logger.debug("Extracting %s to %s", member, basePath)
174
                    try:
175
                        zf.extract(member, basePath)
176
                    except IOError as ex:
177
                        logger.critical("Unable to extract file %s to path %s: %s", member, basePath, ex)
178
                        sys.exit(-2)
179
        logger.info("Removing update ZIP %s", updateZip)
180
        os.remove(updateZip)
181
        filesInLibFolder = [f for f in os.listdir(libFolder) if os.path.isfile(os.path.join(libFolder, f)) and f.endswith(".jar")]
182
        logger.info("Found %d JAR files in lib folder", len(filesInLibFolder))
183
        for file in filesInLibFolder:
184
            logger.info("Found file: %s", file)
185
        if len(filesInLibFolder) == 2:
186
            logger.info("Deleting old JAR %s", jarFile)
187
            os.remove(jarFile)
188
        elif len(filesInLibFolder) == 1:
189
            if filesInLibFolder[0] == os.path.basename(jarFile):
190
                logger.warning("New JAR file in lib folder is the same as the old one. The update may not have found a newer version or failed for some reason")
191
        else:
192
            logger.warning("Expected the number of JAR files in folder %s to be 2 but it's %d. This will be fixed with the next start", libFolder, len(filesInLibFolder))
193
194
    except zipfile.BadZipfile:
195
        logger.critical("File is not a ZIP")
196
        sys.exit(-2)
197
    logger.info("Deleting folder " + updateFolder)
198
    shutil.rmtree(updateFolder)
199
    logger.info("Update successful, restarting Hydra main process")
200
201
202
def restore():
203
    global args
204
    dataFolder = args.datafolder
205
    restoreFolder = os.path.join(args.datafolder, "restore")
206
    if not os.path.exists(dataFolder):
207
        logger.critical("Data folder %s does not exist", dataFolder)
208
        sys.exit(-1)
209
    if not os.path.exists(restoreFolder):
210
        logger.critical("Restore folder %s does not exist", restoreFolder)
211
        sys.exit(-1)
212
    try:
213
        oldSettingsFile = os.path.join(dataFolder, "nzbhydra.yml")
214
        logger.info("Deleting old settings file " + oldSettingsFile)
215
        os.remove(oldSettingsFile)
216
        oldDatabaseFile = os.path.join(dataFolder, "database", "nzbhydra.mv.db")
217
        logger.info("Deleting old database file " + oldDatabaseFile)
218
        os.remove(oldDatabaseFile)
219
    except Exception as e:
220
        logger.critical("Error while deleting old data folder: %r", e)
221
        sys.exit(-1)
222
    for f in os.listdir(restoreFolder):
223
        source = os.path.join(restoreFolder, f)
224
        if source.endswith("db"):
225
            dest = os.path.join(dataFolder, "database", f)
226
        else:
227
            dest = os.path.join(dataFolder, f)
228
        logger.info("Moving " + source + " to " + dest)
229
        shutil.move(source, dest)
230
    logger.info("Deleting folder " + restoreFolder)
231
    os.rmdir(restoreFolder)
232
    logger.info("Moved all files from restore folder to data folder")
233
    return True
234
235
236
# From https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess
237
def subprocess_args(include_stdout=True):
238
    # The following is true only on Windows.
239
    if hasattr(subprocess, 'STARTUPINFO'):
240
        # On Windows, subprocess calls will pop up a command window by default
241
        # when run from Pyinstaller with the ``--noconsole`` option. Avoid this
242
        # distraction.
243
        si = subprocess.STARTUPINFO()
244
        try:
245
            import _subprocess
246
            si.dwFlags |= _subprocess.STARTF_USESHOWWINDOW
247
        except:
248
            si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
249
        # Windows doesn't search the path by default. Pass it an environment so
250
        # it will.
251
        env = os.environ.copy()
252
    else:
253
        si = None
254
        env = None
255
256
    # ``subprocess.check_output`` doesn't allow specifying ``stdout``::
257
    #
258
    #   Traceback (most recent call last):
259
    #     File "test_subprocess.py", line 58, in <module>
260
    #       **subprocess_args(stdout=None))
261
    #     File "C:\Python27\lib\subprocess.py", line 567, in check_output
262
    #       raise ValueError('stdout argument not allowed, it will be overridden.')
263
    #   ValueError: stdout argument not allowed, it will be overridden.
264
    #
265
    # So, add it only if it's needed.
266
    if include_stdout:
267
        ret = {'stdout': subprocess.PIPE}
268
    else:
269
        ret = {}
270
271
    # On Windows, running this from the binary produced by Pyinstaller
272
    # with the ``--noconsole`` option requires redirecting everything
273
    # (stdin, stdout, stderr) to avoid an OSError exception
274
    # "[Error 6] the handle is invalid."
275
    ret.update({'stdin': subprocess.PIPE,
276
                'stderr': subprocess.STDOUT,
277
                'startupinfo': si,
278
                'env': env})
279
    return ret
280
281
282
def startup():
283
    global jarFile, process, args, unknownArgs, consoleLines
284
    basePath = getBasePath()
285
286
    readme = os.path.join(basePath, "readme.md")
287
    if not os.path.exists(readme):
288
        logger.critical("Unable to determine base path correctly. Please make sure to run NZBHydra in the folder where its binary is located. Current base path: " + basePath)
289
        sys.exit(-1)
290
291
    debugSwitchFile = os.path.join(args.datafolder, "DEBUG")
292
    if os.path.exists(debugSwitchFile):
293
        logger.setLevel("DEBUG")
294
        global file_logger, console_logger
295
        file_logger.setLevel("DEBUG")
296
        console_logger.setLevel("DEBUG")
297
        logger.info("Setting wrapper log level to DEBUG")
298
299
    isWindows = platform.system().lower() == "windows"
300
    libFolder = os.path.join(basePath, "lib")
301
    if not os.path.exists(libFolder):
302
        logger.critical("Error: Lib folder %s not found. An update might've failed or the installation folder is corrupt", libFolder)
303
        sys.exit(-1)
304
305
    jarFiles = [os.path.join(libFolder, f) for f in os.listdir(libFolder) if os.path.isfile(os.path.join(libFolder, f)) and f.endswith(".jar")]
306
    if len(jarFiles) == 0:
307
        logger.critical("Error: No JAR files found in folder %s. An update might've failed or the installation folder is corrupt", libFolder)
308
        sys.exit(-1)
309
    if len(jarFiles) == 1:
310
        jarFile = jarFiles[0]
311
    else:
312
        latestFile = max(jarFiles, key=os.path.getmtime)
313
        logger.warning("Expected the number of JAR files in folder %s to be 1 but it's %d. Will remove all JARs except the one last changed: %s", libFolder, len(jarFiles), latestFile)
314
        for file in jarFiles:
315
            if file is not latestFile:
316
                logger.info("Deleting file %s", file)
317
                os.remove(file)
318
        jarFile = latestFile
319
    logger.debug("Using JAR file " + jarFile)
320
321
    if args.repairdb:
322
        arguments = ["--repairdb", args.repairdb]
323
    elif args.version:
324
        arguments = ["--version"]
325
    else:
326
        arguments = unknownArgs  # Those arguments not "caught" by this parser
327
328
        # We need to set the ones which we "pass through" separately
329
        if args.restarted and "restarted" not in arguments:
330
            arguments.append("restarted")
331
        if (args.daemon in arguments or args.nobrowser) and "--nobrowser" not in arguments:
332
            arguments.append("--nobrowser")
333
        if args.datafolder and "--datafolder" not in arguments:
334
            arguments.append("--datafolder")
335
            arguments.append(escape_parameter(isWindows, args.datafolder))
336
        if args.host and "--host" not in arguments:
337
            arguments.append("--host")
338
            arguments.append(args.host)
339
        if args.port and "--port" not in arguments:
340
            arguments.append("--port")
341
            arguments.append(args.port)
342
        if args.baseurl and "--baseurl" not in arguments:
343
            arguments.append("--baseurl")
344
            arguments.append(args.baseurl)
345
    yamlPath = os.path.join(args.datafolder, "nzbhydra.yml")
346
    if args.xmx:
347
        xmx = args.xmx
348
    elif os.path.exists(yamlPath):
349
        with open(yamlPath, "r") as f:
350
            for line in f.readlines():
351
                index = line.find("xmx:")
352
                if index > -1:
353
                    xmx = line[index + 5:].rstrip("\n\r ")
354
                    break
355
            else:
356
                logger.warn("Didn't find XMX in YAML file, using default of 256")
357
                xmx = 256
358
    else:
359
        logger.info("No file nzbhydra.yml found. Using 256M XMX")
360
        xmx = 256
361
    xmx = str(xmx)
362
    if xmx.lower().endswith("m"):
363
        logger.info("Removing superfluous M from XMX value " + xmx)
364
        xmx = xmx[:-1]
365
366
    javaVersion = getJavaVersion(args.java)
367
368
    gcLogFilename = (os.path.join(args.datafolder, "logs") + "/gclog-" + datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".log").replace("\\", "/")
369
    gcLogFilename = os.path.relpath(gcLogFilename, basePath)
370
371
    gcArguments = []
372
    if javaVersion < 9:
373
        gcArguments = ["-Xloggc:" + gcLogFilename,
374
                       "-XX:+PrintGCDetails",
375
                       "-XX:+PrintGCTimeStamps",
376
                       "-XX:+PrintTenuringDistribution",
377
                       "-XX:+PrintGCCause",
378
                       "-XX:+UseGCLogFileRotation",
379
                       "-XX:NumberOfGCLogFiles=10",
380
                       "-XX:GCLogFileSize=5M",
381
                       ]
382
    else:
383
        gcArguments = [
384
            "-Xlog:gc*:file=" + gcLogFilename + "::filecount=10,filesize=5000"]
385
386
    java_arguments = ["-Xmx" + xmx + "M",
387
                      "-DfromWrapper",
388
                      "-XX:TieredStopAtLevel=1",
389
                      "-noverify",
390
                      "-XX:+HeapDumpOnOutOfMemoryError",
391
                      "-XX:HeapDumpPath=" + os.path.join(args.datafolder, "logs")
392
                      ]
393
    java_arguments.extend(gcArguments)
394
    if args.debugport:
395
        java_arguments.append("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + args.debugport)
396
    if not args.nocolors and not isWindows:
397
        java_arguments.append("-Dspring.output.ansi.enabled=ALWAYS")
398
    if args.debug:
399
        java_arguments.append("-Ddebug=true")
400
    arguments = [args.java] + java_arguments + ["-jar", escape_parameter(isWindows, jarFile)] + arguments
401
    commandLine = " ".join(arguments)
402
    logger.info("Starting NZBHydra main process with command line: %s in folder %s", commandLine, basePath)
403
    if hasattr(subprocess, 'STARTUPINFO'):
404
        si = subprocess.STARTUPINFO()
405
        try:
406
            import _subprocess
407
            si.dwFlags |= _subprocess.STARTF_USESHOWWINDOW
408
        except:
409
            si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
410
    else:
411
        si = None
412
    # todo check shell=True/False for linux and windows
413
    # shell=true: pass string, shell=false: pass arguments
414
    try:
415
        process = subprocess.Popen(arguments, shell=False, cwd=basePath, bufsize=-1, **subprocess_args())
416
417
        # atexit.register(killProcess)
418
        while True:
419
            # Handle error first in case startup of main process returned only an error (on stderror)
420
            nextline = process.stdout.readline()
421
            if nextline == '' and process.poll() is not None:
422
                break
423
            if nextline != "":
424
                consoleLines.append(nextline)
425
426
            if len(consoleLines) > 100:
427
                consoleLines = consoleLines[-100:]
428
            if not args.quiet:
429
                sys.stdout.write(nextline)
430
                sys.stdout.flush()
431
        process.wait()
432
433
        return process
434
    except Exception as e:
435
        logger.error("Unable to start process; make sure Java is installed and callable. Error message: " + str(e))
436
437
438
def escape_parameter(is_windows, parameter):
439
    return parameter  # TODO FInd out when to actually escape with windows, I think when shell=True is used
440
    # return '"' + parameter + '"' if is_windows else parameter
441
442
443
def list_files(startpath):
444
    for root, dirs, files in os.walk(startpath):
445
        level = root.replace(startpath, '').count(os.sep)
446
        indent = ' ' * 4 * (level)
447
        logger.info('{}{}/'.format(indent, os.path.basename(root)))
448
        subindent = ' ' * 4 * (level + 1)
449
        for f in files:
450
            logger.info('{}{}'.format(subindent, f))
451
452
453
def handleUnexpectedExit():
454
    global consoleLines
455
    message = "Main process shut down unexpectedly. If the wrapper was started in daemon mode you might not see the error output. Start Hydra manually with the same parameters in the same environment to see it"
456
    for x in consoleLines:
457
        if "Unrecognized option: -Xlog" in x:
458
            message = "You seem to be trying to run NZBHydra with a wrong Java version. Please make sure to use at least Java 9"
459
        elif "java.lang.OutOfMemoryError" in x:
460
            message = "The main process has exited because it didn't have enough memory. Please increase the XMX value in the main config"
461
    logger.error(message)
462
    sys.exit(-1)
463
464
465
def getJavaVersion(javaExecutable):
466
    if hasattr(subprocess, 'STARTUPINFO'):
467
        si = subprocess.STARTUPINFO()
468
        try:
469
            import _subprocess
470
            si.dwFlags |= _subprocess.STARTF_USESHOWWINDOW
471
        except:
472
            si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
473
    else:
474
        si = None
475
    # todo check shell=True/False for linux and windows
476
    # shell=true: pass string, shell=false: pass arguments
477
    try:
478
        lines = []
479
        process = subprocess.Popen([javaExecutable, "-version"], shell=False, bufsize=-1, **subprocess_args())
480
481
        # atexit.register(killProcess)
482
        while True:
483
            # Handle error first in case startup of main process returned only an error (on stderror)
484
            nextline = process.stdout.readline()
485
            if nextline == '' and process.poll() is not None:
486
                break
487
            if nextline != "":
488
                lines.append(nextline)
489
        process.wait()
490
        if len(lines) == 0:
491
            raise Exception("Unable to get output from call to java -version")
492
        versionLine = lines[0].replace("\n", "").replace("\r", "")
493
        match = re.match('(java|openjdk) version "(?P<major>\d+)(\.(?P<minor>\d+)\.(?P<patch>\d)+[\-_\w]*)?".*', versionLine)
494
        if match is None:
495
            raise Exception("Unable to determine java version from string " + lines[0])
496
        javaMajor = int(match.group("major"))
497
        javaMinor = int(match.group("minor")) if match.group("minor") is not None else 0
498
        javaVersion = 0
499
        if (javaMajor == 1 and javaMinor < 8) or (javaMajor > 1 and javaMajor < 8):
500
            logger.error("Found incompatible java version '" + versionLine + "'")
501
            sys.exit(-1)
502
        if javaMajor == 1 and javaMinor == 8:
503
            javaVersion = 8
504
        else:
505
            javaVersion = javaMajor
506
        logger.info("Determined java version as '%d' from version string '%s'", javaVersion, versionLine)
507
        return javaVersion
508
    except Exception as e:
509
        logger.error("Unable to determine java version; make sure Java is installed and callable. Error message: " + str(e))
510
        sys.exit(-1)
511
512
513
if __name__ == '__main__':
514
    GracefulKiller()
515
    parser = argparse.ArgumentParser(description='NZBHydra 2')
516
    parser.add_argument('--java', action='store', help='Full path to java executable', default="java")
517
    parser.add_argument('--debugport', action='store', help='Set debug port to enable remote debugging', default=None)
518
    parser.add_argument('--daemon', '-D', action='store_true', help='Run as daemon. *nix only', default=False)
519
    parser.add_argument('--pidfile', action='store', help='Path to PID file. Only relevant with daemon argument', default="nzbhydra2.pid")
520
    parser.add_argument('--nopidfile', action='store_true', help='Disable writing of PID file. Only relevant with daemon argument', default=False)
521
    parser.add_argument('--nocolors', action='store_true', help='Disable color coded console output (disabled on Windows by default)', default=False)
522
    parser.add_argument('--listfiles', action='store', help='Lists all files in given folder and quits. For debugging docker', default=None)
523
524
    # Pass to main process
525
    parser.add_argument('--datafolder', action='store', help='Set the main data folder containing config, database, etc using an absolute path', default=os.path.join(getBasePath(), "data"))
526
    parser.add_argument('--xmx', action='store', help='Java Xmx setting in MB (e.g. 256)', default=None)
527
    parser.add_argument('--quiet', action='store_true', help='Set to disable all console output', default=False)
528
    parser.add_argument('--host', action='store', help='Set the host')
529
    parser.add_argument('--port', action='store', help='Set the port')
530
    parser.add_argument('--baseurl', action='store', help='Set the base URL (e.g. /nzbhydra)')
531
    parser.add_argument('--nobrowser', action='store_true', help='Set to disable opening of browser at startup', default=False)
532
    parser.add_argument('--debug', action='store_true', help='Start with more debugging output', default=False)
533
    # Main process actions
534
    parser.add_argument('--repairdb', action='store', help='Attempt to repair the database. Provide path to database file as parameter')
535
    parser.add_argument('--version', action='store_true', help='Print version')
536
537
    # Internal logic
538
    parser.add_argument('--restarted', action='store_true', default=False, help=argparse.SUPPRESS)
539
540
    args, unknownArgs = parser.parse_known_args()
541
    setupLogger()
542
543
    # Delete old files from last backup
544
    oldFiles = [f for f in os.listdir(getBasePath()) if os.path.isfile(os.path.join(getBasePath(), f)) and f.endswith(".old")]
545
    if len(oldFiles) > 0:
546
        logger.info("Deleting .old files from last update")
547
        for f in oldFiles:
548
            logger.debug("Deleting file %s", f)
549
            os.remove(f)
550
551
    if not (os.path.isabs(args.datafolder)):
552
        args.datafolder = os.path.join(os.getcwd(), args.datafolder)
553
        logger.info("Data folder path is not absolute. Will assume " + args.datafolder + " was meant")
554
555
    # Delete old control id file if it exists. Shouldn't ever exist or if it does it should be overwritten by main process, but who knows
556
    controlIdFilePath = os.path.join(args.datafolder, "control.id")
557
    if os.path.exists(controlIdFilePath):
558
        os.remove(controlIdFilePath)
559
    doStart = True
560
561
    if "--version" in unknownArgs or "--help" in unknownArgs:
562
        # no fancy shit, just start the file
563
        startup()
564
    elif args.listfiles is not None:
565
        path = args.listfiles
566
        curpath = os.path.dirname(os.path.realpath(__file__))
567
        if not os.path.isabs(path):
568
            path = os.path.join(curpath, path)
569
        logger.info("Listing files in %s", path)
570
        list_files(os.path.dirname(path))
571
    else:
572
        if args.daemon:
573
            logger.info("Daemonizing...")
574
            daemonize(args.pidfile, args.nopidfile)
575
576
        while doStart:
577
            process = startup()
578
579
            if process is None:
580
                logger.debug("No process found, exiting")
581
                sys.exit(-1)
582
583
            if terminatedByWrapper:
584
                logger.debug("Shutting down because child process was terminated by us after getting signal")
585
                sys.exit(0)
586
587
            if process.returncode == 1:
588
                handleUnexpectedExit()
589
            args.restarted = True
590
591
            # Try to read control code from file because under linux when started from the wrapper the return code is always 0
592
            controlCode = 0
593
            try:
594
                with open(controlIdFilePath, "r") as f:
595
                    controlCode = int(f.readline())
596
                    logger.debug("Control code read from file %s: %d", controlIdFilePath, controlCode)
597
            except Exception as e:
598
                controlCode = process.returncode
599
                if not (args.version or args.repairdb):
600
                    logger.warn("Unable to read control ID from %s: %s. Falling back to process return code %d", controlIdFilePath, e, controlCode)
601
            if os.path.exists(controlIdFilePath):
602
                try:
603
                    logger.debug("Deleting old control ID file %s", controlIdFilePath)
604
                    os.remove(controlIdFilePath)
605
                except Exception as e:
606
                    logger.error("Unable to delete control ID file %s: %s", controlIdFilePath, e)
607
608
            if controlCode == 11:
609
                logger.info("NZBHydra main process has terminated for updating")
610
                update()
611
                doStart = True
612
            elif controlCode == 22:
613
                logger.info("NZBHydra main process has terminated for restart")
614
                doStart = True
615
            elif controlCode == 33:
616
                logger.info("NZBHydra main process has terminated for restoration")
617
                doStart = restore()
618
                logger.info("Restoration successful")
619
                doStart = True
620
            elif args.version or args.repairdb:
621
                # Just quit without further ado, help was printed by main process
622
                doStart = False
623
            else:
624
                logger.info("NZBHydra main process has terminated for shutdown")
625
                doStart = False
(-)news/nzbhydra2/pkg-descr (+6 lines)
Added Link Here
1
NZBHydra 2 is a meta search for NZB indexers. It provides easy access to a 
2
number of raw and newznab based indexers. You can search all your indexers 
3
from one place and use it as an indexer source for tools like Sonarr, 
4
Radarr or CouchPotato.
5
6
WWW: https://github.com/theotherp/nzbhydra2

Return to bug 234537