Ensure only on instance of dosage is running to prevent accedental DoS on sites with multiple comics.

This commit is contained in:
Bastian Kleineidam 2014-01-05 10:36:22 +01:00
parent c80372a5e0
commit 1f38895681
8 changed files with 141 additions and 27 deletions

View file

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

View file

@ -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

View file

@ -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 &amp;&amp; find . -type d | xargs -n1 -P4 dosage -b . -v</B>
</DL>
<A NAME="lbAI">&nbsp;</A>
<H2>ENVIRONMENT</H2>

View file

@ -9,15 +9,6 @@
<A NAME="lbAB">&nbsp;</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 &amp;&amp; find . -type d | xargs -n1 -P4 dosage -b . -v</B>
@@ -282,7 +282,7 @@
</DL>
<HR>

View file

@ -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
View file

@ -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
View 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)

View 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