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)
|
Dosage 2.9 (released 22.12.2013)
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
|
|
@ -148,11 +148,6 @@ the beginning.
|
||||||
.B dosage \-a calvinandhobbes:2012/07/22
|
.B dosage \-a calvinandhobbes:2012/07/22
|
||||||
.RE
|
.RE
|
||||||
.PP
|
.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
|
.SH ENVIRONMENT
|
||||||
.IP HTTP_PROXY
|
.IP HTTP_PROXY
|
||||||
.B dosage
|
.B dosage
|
||||||
|
|
|
@ -202,13 +202,6 @@ the beginning.
|
||||||
|
|
||||||
<P>
|
<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>
|
<A NAME="lbAI"> </A>
|
||||||
<H2>ENVIRONMENT</H2>
|
<H2>ENVIRONMENT</H2>
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,6 @@
|
||||||
|
|
||||||
<A NAME="lbAB"> </A>
|
<A NAME="lbAB"> </A>
|
||||||
<H2>NAME</H2>
|
<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 @@
|
@@ -282,7 +282,7 @@
|
||||||
</DL>
|
</DL>
|
||||||
<HR>
|
<HR>
|
||||||
|
|
|
@ -131,11 +131,6 @@ EXAMPLES
|
||||||
backwards to the beginning.
|
backwards to the beginning.
|
||||||
dosage -a calvinandhobbes:2012/07/22
|
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
|
ENVIRONMENT
|
||||||
HTTP_PROXY
|
HTTP_PROXY
|
||||||
dosage will use the specified HTTP proxy when download‐
|
dosage will use the specified HTTP proxy when download‐
|
||||||
|
|
4
dosage
4
dosage
|
@ -16,7 +16,7 @@ import argparse
|
||||||
import pydoc
|
import pydoc
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from dosagelib import events, scraper, configuration
|
from dosagelib import events, scraper, configuration, singleton
|
||||||
from dosagelib.output import out
|
from dosagelib.output import out
|
||||||
from dosagelib.util import internal_error, getDirname, strlimit, getLangName
|
from dosagelib.util import internal_error, getDirname, strlimit, getLangName
|
||||||
from dosagelib.ansicolor import get_columns
|
from dosagelib.ansicolor import get_columns
|
||||||
|
@ -271,6 +271,8 @@ def getStrips(scraperobj, options):
|
||||||
def run(options):
|
def run(options):
|
||||||
"""Execute comic commands."""
|
"""Execute comic commands."""
|
||||||
setOutputInfo(options)
|
setOutputInfo(options)
|
||||||
|
# ensure only one instance of dosage is running
|
||||||
|
me = singleton.SingleInstance()
|
||||||
if options.version:
|
if options.version:
|
||||||
return displayVersion(options.verbose)
|
return displayVersion(options.verbose)
|
||||||
if options.list:
|
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