Ensure only on instance of dosage is running to prevent accedental DoS on sites with multiple comics.
This commit is contained in:
parent
c80372a5e0
commit
1f38895681
8 changed files with 141 additions and 27 deletions
|
@ -1,3 +1,10 @@
|
|||
Dosage 2.10 (released xx.xx.2014)
|
||||
|
||||
Changes:
|
||||
- cmdline: Ensure only one instance of dosage is running to prevent
|
||||
accidental DoS when fetching multiple comics of one site.
|
||||
|
||||
|
||||
Dosage 2.9 (released 22.12.2013)
|
||||
|
||||
Features:
|
||||
|
|
|
@ -148,11 +148,6 @@ the beginning.
|
|||
.B dosage \-a calvinandhobbes:2012/07/22
|
||||
.RE
|
||||
.PP
|
||||
On Unix, \fBxargs(1)\fP can download several comic strips in parallel,
|
||||
for example using up to 4 processes:
|
||||
.RS
|
||||
.B cd Comics && find . -type d | xargs -n1 -P4 dosage -b . -v
|
||||
.RE
|
||||
.SH ENVIRONMENT
|
||||
.IP HTTP_PROXY
|
||||
.B dosage
|
||||
|
|
|
@ -202,13 +202,6 @@ the beginning.
|
|||
|
||||
<P>
|
||||
|
||||
On Unix, <B>xargs(1)</B> can download several comic strips in parallel,
|
||||
for example using up to 4 processes:
|
||||
<DL COMPACT><DT><DD>
|
||||
<B>cd Comics && find . -type d | xargs -n1 -P4 dosage -b . -v</B>
|
||||
|
||||
</DL>
|
||||
|
||||
<A NAME="lbAI"> </A>
|
||||
<H2>ENVIRONMENT</H2>
|
||||
|
||||
|
|
|
@ -9,15 +9,6 @@
|
|||
|
||||
<A NAME="lbAB"> </A>
|
||||
<H2>NAME</H2>
|
||||
@@ -190,7 +190,7 @@
|
||||
|
||||
<P>
|
||||
|
||||
-On Unix, <B><A HREF="../man1/xargs.1.html">xargs</A>(1)</B> can download several comic strips in parallel,
|
||||
+On Unix, <B>xargs(1)</B> can download several comic strips in parallel,
|
||||
for example using up to 4 processes:
|
||||
<DL COMPACT><DT><DD>
|
||||
<B>cd Comics && find . -type d | xargs -n1 -P4 dosage -b . -v</B>
|
||||
@@ -282,7 +282,7 @@
|
||||
</DL>
|
||||
<HR>
|
||||
|
|
|
@ -131,11 +131,6 @@ EXAMPLES
|
|||
backwards to the beginning.
|
||||
dosage -a calvinandhobbes:2012/07/22
|
||||
|
||||
On Unix, xargs(1) can download several comic strips in paral‐
|
||||
lel, for example using up to 4 processes:
|
||||
cd Comics && find . -type d | xargs -n1 -P4 dosage -b .
|
||||
-v
|
||||
|
||||
ENVIRONMENT
|
||||
HTTP_PROXY
|
||||
dosage will use the specified HTTP proxy when download‐
|
||||
|
|
4
dosage
4
dosage
|
@ -16,7 +16,7 @@ import argparse
|
|||
import pydoc
|
||||
from io import StringIO
|
||||
|
||||
from dosagelib import events, scraper, configuration
|
||||
from dosagelib import events, scraper, configuration, singleton
|
||||
from dosagelib.output import out
|
||||
from dosagelib.util import internal_error, getDirname, strlimit, getLangName
|
||||
from dosagelib.ansicolor import get_columns
|
||||
|
@ -271,6 +271,8 @@ def getStrips(scraperobj, options):
|
|||
def run(options):
|
||||
"""Execute comic commands."""
|
||||
setOutputInfo(options)
|
||||
# ensure only one instance of dosage is running
|
||||
me = singleton.SingleInstance()
|
||||
if options.version:
|
||||
return displayVersion(options.verbose)
|
||||
if options.list:
|
||||
|
|
86
dosagelib/singleton.py
Normal file
86
dosagelib/singleton.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
# -*- coding: iso-8859-1 -*-
|
||||
# Copied from: https://github.com/pycontribs/tendo
|
||||
# License: PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
# Author: Sorin Sbarnea
|
||||
# Changes: changed logging and formatting
|
||||
|
||||
import sys
|
||||
import os
|
||||
import errno
|
||||
import tempfile
|
||||
from .output import out
|
||||
|
||||
|
||||
class SingleInstance(object):
|
||||
"""
|
||||
To prevent a script from running in parallel instantiate the
|
||||
SingleInstance() class. If is there another instance
|
||||
already running it will exit the application with the message
|
||||
"Another instance is already running, quitting.",
|
||||
returning an error code of -1.
|
||||
|
||||
>>> me = SingleInstance()
|
||||
|
||||
This is very useful to execute scripts by crontab that should only run
|
||||
one at a time.
|
||||
|
||||
Note that this works by creating a lock file with a filename based
|
||||
on the full path to the script file.
|
||||
"""
|
||||
|
||||
def __init__(self, flavor_id="", exit_code=-1):
|
||||
"""Create an exclusive lockfile or exit with an error and the given
|
||||
exit code."""
|
||||
self.initialized = False
|
||||
scriptname = os.path.splitext(os.path.abspath(sys.argv[0]))[0]
|
||||
lockname = scriptname.replace("/", "-").replace(":", "").replace("\\", "-")
|
||||
if flavor_id:
|
||||
lockname += "-%s" % flavor_id
|
||||
lockname += '.lock'
|
||||
tempdir = tempfile.gettempdir()
|
||||
self.lockfile = os.path.normpath(os.path.join(tempdir, lockname))
|
||||
out.debug("SingleInstance lockfile: " + self.lockfile)
|
||||
print(self.lockfile)
|
||||
if sys.platform == 'win32':
|
||||
try:
|
||||
# file already exists, try to remove it in case the previous
|
||||
# execution was interrupted
|
||||
if os.path.exists(self.lockfile):
|
||||
os.unlink(self.lockfile)
|
||||
self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
||||
except OSError:
|
||||
type, e, tb = sys.exc_info()
|
||||
if e.errno == errno.EACCES: # EACCES == 13
|
||||
self.exit(exit_code)
|
||||
raise
|
||||
else: # non Windows
|
||||
import fcntl
|
||||
self.fp = open(self.lockfile, 'w')
|
||||
try:
|
||||
fcntl.lockf(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
# raises IOError on Python << 3.3, else OSError
|
||||
except (IOError, OSError):
|
||||
self.exit(exit_code)
|
||||
self.initialized = True
|
||||
|
||||
def exit(self, exit_code):
|
||||
"""Exit with an error message and the given exit code."""
|
||||
out.error("Another instance is already running, quitting.")
|
||||
sys.exit(exit_code)
|
||||
|
||||
def __del__(self):
|
||||
"""Remove the lock file."""
|
||||
if not self.initialized:
|
||||
return
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
if hasattr(self, 'fd'):
|
||||
os.close(self.fd)
|
||||
os.unlink(self.lockfile)
|
||||
else:
|
||||
import fcntl
|
||||
fcntl.lockf(self.fp, fcntl.LOCK_UN)
|
||||
if os.path.isfile(self.lockfile):
|
||||
os.unlink(self.lockfile)
|
||||
except StandardError as e:
|
||||
out.exception("could not remove lockfile: %s" % e)
|
45
tests/test_singletoninstance.py
Normal file
45
tests/test_singletoninstance.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# -*- coding: iso-8859-1 -*-
|
||||
# Copied from: https://github.com/pycontribs/tendo
|
||||
# License: PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
# Author: Sorin Sbarnea
|
||||
# Changes: changed logging and formatting
|
||||
from unittest import TestCase
|
||||
from dosagelib import singleton
|
||||
from multiprocessing import Process
|
||||
|
||||
|
||||
def f(flavor_id):
|
||||
return singleton.SingleInstance(flavor_id=flavor_id, exit_code=1)
|
||||
|
||||
|
||||
class TestSingleton(TestCase):
|
||||
|
||||
def test_1(self):
|
||||
# test in current process
|
||||
me = singleton.SingleInstance(flavor_id="test-1")
|
||||
del me # now the lock should be removed
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_2(self):
|
||||
# test in current subprocess
|
||||
p = Process(target=f, args=("test-2",))
|
||||
p.start()
|
||||
p.join()
|
||||
# the called function should succeed
|
||||
self.assertEqual(p.exitcode, 0)
|
||||
|
||||
def test_3(self):
|
||||
# test in current process and subprocess with failure
|
||||
# start first instance
|
||||
me = f("test-3")
|
||||
# second instance
|
||||
p = Process(target=f, args=("test-3",))
|
||||
p.start()
|
||||
p.join()
|
||||
self.assertEqual(p.exitcode, 1)
|
||||
# third instance
|
||||
p = Process(target=f, args=("test-3",))
|
||||
p.start()
|
||||
p.join()
|
||||
self.assertEqual(p.exitcode, 1)
|
||||
del me # now the lock should be removed
|
Loading…
Reference in a new issue