From 1f388956814303bf597a9adc8e09c611b6472625 Mon Sep 17 00:00:00 2001 From: Bastian Kleineidam Date: Sun, 5 Jan 2014 10:36:22 +0100 Subject: [PATCH] Ensure only on instance of dosage is running to prevent accedental DoS on sites with multiple comics. --- doc/changelog.txt | 7 +++ doc/dosage.1 | 5 -- doc/dosage.1.html | 7 --- doc/dosage.1.html.diff | 9 ---- doc/dosage.txt | 5 -- dosage | 4 +- dosagelib/singleton.py | 86 +++++++++++++++++++++++++++++++++ tests/test_singletoninstance.py | 45 +++++++++++++++++ 8 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 dosagelib/singleton.py create mode 100644 tests/test_singletoninstance.py diff --git a/doc/changelog.txt b/doc/changelog.txt index bc3d439eb..5ee9613bd 100644 --- a/doc/changelog.txt +++ b/doc/changelog.txt @@ -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: diff --git a/doc/dosage.1 b/doc/dosage.1 index ce8f30955..c57423088 100644 --- a/doc/dosage.1 +++ b/doc/dosage.1 @@ -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 diff --git a/doc/dosage.1.html b/doc/dosage.1.html index cf715147b..177dcada2 100644 --- a/doc/dosage.1.html +++ b/doc/dosage.1.html @@ -202,13 +202,6 @@ the beginning.

-On Unix, xargs(1) can download several comic strips in parallel, -for example using up to 4 processes: -

-cd Comics && find . -type d | xargs -n1 -P4 dosage -b . -v - -
-  

ENVIRONMENT

diff --git a/doc/dosage.1.html.diff b/doc/dosage.1.html.diff index be9d2bf10..5fac56419 100644 --- a/doc/dosage.1.html.diff +++ b/doc/dosage.1.html.diff @@ -9,15 +9,6 @@  

NAME

-@@ -190,7 +190,7 @@ - -

- --On Unix, xargs(1) can download several comic strips in parallel, -+On Unix, xargs(1) can download several comic strips in parallel, - for example using up to 4 processes: -

- cd Comics && find . -type d | xargs -n1 -P4 dosage -b . -v @@ -282,7 +282,7 @@

diff --git a/doc/dosage.txt b/doc/dosage.txt index 1092578f7..40c77f484 100644 --- a/doc/dosage.txt +++ b/doc/dosage.txt @@ -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‐ diff --git a/dosage b/dosage index fe2af1624..18ac99b03 100755 --- a/dosage +++ b/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: diff --git a/dosagelib/singleton.py b/dosagelib/singleton.py new file mode 100644 index 000000000..dee4967bb --- /dev/null +++ b/dosagelib/singleton.py @@ -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) diff --git a/tests/test_singletoninstance.py b/tests/test_singletoninstance.py new file mode 100644 index 000000000..937b56838 --- /dev/null +++ b/tests/test_singletoninstance.py @@ -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