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