From 48fece41e76a1cc254211845bc505e345312ce1f Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 26 Aug 2009 17:35:38 -0600
Subject: [PATCH 01/21] New recipe for RGA Online by Werner Gerard
---
src/calibre/web/feeds/recipes/__init__.py | 2 +-
src/calibre/web/feeds/recipes/recipe_rga.py | 61 +++++++++++++++++++++
2 files changed, 62 insertions(+), 1 deletion(-)
create mode 100644 src/calibre/web/feeds/recipes/recipe_rga.py
diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py
index 3a2b7a4e13..8ce81047b9 100644
--- a/src/calibre/web/feeds/recipes/__init__.py
+++ b/src/calibre/web/feeds/recipes/__init__.py
@@ -55,7 +55,7 @@ recipe_modules = ['recipe_' + r for r in (
'eltiempo_hn', 'slate', 'tnxm', 'bbcvietnamese', 'vnexpress',
'volksrant', 'theeconomictimes_india', 'ourdailybread',
'monitor', 'republika', 'beta', 'beta_en', 'glasjavnosti',
- 'esquire', 'livemint', 'thedgesingapore', 'darknet',
+ 'esquire', 'livemint', 'thedgesingapore', 'darknet', 'rga',
)]
diff --git a/src/calibre/web/feeds/recipes/recipe_rga.py b/src/calibre/web/feeds/recipes/recipe_rga.py
new file mode 100644
index 0000000000..47ae2f4614
--- /dev/null
+++ b/src/calibre/web/feeds/recipes/recipe_rga.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+
+__license__ = 'GPL v3'
+__copyright__ = '2009, W. Gerard '
+'''
+rga-online.de
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class rga_onliner(BasicNewsRecipe):
+ title = 'RGA Online - German'
+ __author__ = 'Werner Gerard'
+ description = "E-Zeitung aus RSS-Artikeln zusammengestellt."
+ publisher = 'RGA-Online'
+ category = 'Nachrichten, RGA'
+ oldest_article = 3
+ max_articles_per_feed = 100
+ language = _('German')
+ lang = 'de-DE'
+ no_stylesheets = True
+ use_embedded_content = False
+ encoding = 'cp1252'
+
+ remove_tags_before = dict(name='span', attrs={'class':'headgross'})
+ remove_tags_after = dict(name='br', attrs={'clear':'all'})
+
+# remove_tags_after = dict(name='br', attrs={'clear':'clear'})
+
+ feeds = [
+ ('RGA-Online Remscheid', 'http://www.rga-online.de/rss/rs_news.php'),
+ ('RGA-Online Wermelskirchen', 'http://www.rga-online.de/rss/wk_news.php'),
+ ('RGA-Online Hueckeswagen', 'http://www.rga-online.de/rss/hk_news.php'),
+ ('RGA-Online Radevormwald', 'http://www.rga-online.de/rss/rz_news.php'),
+ ('RGA-Online Tagesthemen', 'http://www.rga-online.de/rss/tt_news.php'),
+ ('RGA-Online Brennpunkte', 'http://www.rga-online.de/rss/br_news.php'),
+ ('RGA-Online Sport', 'http://www.rga-online.de/rss/spo_news.php'),
+ ('RGA-Online Lokalsport', 'http://www.rga-online.de/rss/sp_news.php'),
+ ('RGA-Online Bergisches Land', 'http://www.rga-online.de/rss/bg_news.php'),
+ ('RGA-Online Bergische Wirtschaft', 'http://www.rga-online.de/rss/bw_news.php')
+ ]
+#"print based version"
+# def print_version(self, url):
+# main, separatior, sub = url.rpartition('?')
+# sub1, sep1, artikel = sub.rpartition('&')
+# sub2, sep2, publikation = sub1.rpartition('&')
+
+
+# return 'http://www.pipeline.de/cgi-bin/pipeline.fcg?userid=1&publikation=2&template=druck.html&'+ publikation + '&' + artikel
+# return 'http://www.pipeline.de/cgi-bin/pipeline.fcg?userid=1&publikation=2&template=druck.html&redaktion=2&artikel=109208787'
+# http://www.pipeline.de/cgi-bin/pipeline.fcg?userid=1&publikation=2&template=druck.html&redaktion=1&artikel=109209772
+# http://www.rga-online.de/lokales/h6ckeswagen.php?publikation=2&template=phparttext&ausgabe=49740&redaktion=2&artikel=109208787
+
+
+ def get_cover_url(self):
+ return 'http://rga.werner-gerard.de/rga.jpg'
+
+ def postprocess_html(self, soup, first):
+ for tag in soup.findAll(name=['table', 'tr', 'td']):
+ tag.name = 'span'
+ return soup
From b99bec90d3726452116684b40be5b65f7a6eea4e Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 27 Aug 2009 09:40:05 -0600
Subject: [PATCH 02/21] IGN:64-bit linux binary. Also initial windows only
PRS600 driver
---
src/calibre/customize/builtins.py | 3 ++-
src/calibre/devices/prs700/driver.py | 9 +++++++++
src/calibre/gui2/wizard/__init__.py | 1 +
src/calibre/trac/plugins/templates/linux.html | 11 +----------
4 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 069d13b55d..3493ace26a 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -364,7 +364,7 @@ from calibre.devices.jetbook.driver import JETBOOK
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
from calibre.devices.prs500.driver import PRS500
from calibre.devices.prs505.driver import PRS505
-from calibre.devices.prs700.driver import PRS700
+from calibre.devices.prs700.driver import PRS700, PRS600
from calibre.devices.android.driver import ANDROID
plugins = [HTML2ZIP]
@@ -413,6 +413,7 @@ plugins += [
PRS500,
PRS505,
PRS700,
+ PRS600,
ANDROID,
CYBOOK_OPUS,
COOL_ER
diff --git a/src/calibre/devices/prs700/driver.py b/src/calibre/devices/prs700/driver.py
index 7856ef0b34..aa6e2cf83e 100644
--- a/src/calibre/devices/prs700/driver.py
+++ b/src/calibre/devices/prs700/driver.py
@@ -28,3 +28,12 @@ class PRS700(PRS505):
OSX_CARD_A_MEM = re.compile(r'Sony PRS-700/[^:]+:MS Media')
OSX_CARD_B_MEM = re.compile(r'Sony PRS-700/[^:]+:SD Media')
+class PRS600(PRS700):
+
+ name = 'PRS-600 Device Interface'
+ description = PRS700.description.replace('700', '600')
+
+ WINDOWS_MAIN_MEM = 'PRS-600'
+ WINDOWS_CARD_A_MEM = re.compile(r'PRS-700/\S+:MS')
+ WINDOWS_CARD_B_MEM = re.compile(r'PRS-700/\S+:SD')
+
diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py
index 7045f6334d..d00417205d 100644
--- a/src/calibre/gui2/wizard/__init__.py
+++ b/src/calibre/gui2/wizard/__init__.py
@@ -486,6 +486,7 @@ class LibraryPage(QWizardPage, LibraryUI):
try:
os.makedirs(lp)
except:
+ traceback.print_exc()
lp = os.path.expanduser('~')
self.location.setText(lp)
diff --git a/src/calibre/trac/plugins/templates/linux.html b/src/calibre/trac/plugins/templates/linux.html
index 7dafa6a6d8..1a2d803841 100644
--- a/src/calibre/trac/plugins/templates/linux.html
+++ b/src/calibre/trac/plugins/templates/linux.html
@@ -78,7 +78,7 @@
and press Enter:
-sudo python -c "import urllib2; exec urllib2.urlopen('http://calibre.kovidgoyal.net/download_linux_binary_installer').read(); main()"
+sudo python -c "import urllib2; exec urllib2.urlopen('http://status.calibre-ebook.com/linux_installer').read(); main()"
Note
@@ -91,15 +91,6 @@ sudo python -c "import urllib2; exec urllib2.urlopen('http://calibre.kovidgoyal.
You must have xdg-utils installed
on your system before running the installer.
- -
- For device automounting to work, you must have the pmount
- package installed on your system.
-
- -
- On a 64bit machine, you must have 32-bit versions
- of common libraries like X11, freetype, fontconfig,
- expat and their various dependencies installed.
-
Source install
From fbde0cdb089d28800b3e1759f5c0fa59f2032eda Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 27 Aug 2009 10:01:21 -0600
Subject: [PATCH 03/21] IGN:Upgrade cherrypy to 3.1.2 and double the timeout
when waiting for a free port
---
src/cherrypy/__init__.py | 2 +-
src/cherrypy/_cpchecker.py | 4 +-
src/cherrypy/_cperror.py | 6 +-
src/cherrypy/_cprequest.py | 6 +-
src/cherrypy/lib/caching.py | 6 +-
src/cherrypy/lib/cptools.py | 10 +-
src/cherrypy/lib/encoding.py | 2 +-
src/cherrypy/lib/httpauth.py | 14 +-
src/cherrypy/lib/sessions-r2062.py | 698 ----------------------------
src/cherrypy/lib/sessions.py | 10 +-
src/cherrypy/lib/static.py | 6 +
src/cherrypy/process/servers.py | 64 +--
src/cherrypy/process/wspbus.py | 43 +-
src/cherrypy/wsgiserver/__init__.py | 35 +-
14 files changed, 136 insertions(+), 770 deletions(-)
delete mode 100644 src/cherrypy/lib/sessions-r2062.py
diff --git a/src/cherrypy/__init__.py b/src/cherrypy/__init__.py
index 274f00694b..a9a8a93752 100644
--- a/src/cherrypy/__init__.py
+++ b/src/cherrypy/__init__.py
@@ -57,7 +57,7 @@ These API's are described in the CherryPy specification:
http://www.cherrypy.org/wiki/CherryPySpec
"""
-__version__ = "3.1.1"
+__version__ = "3.1.2"
from urlparse import urljoin as _urljoin
diff --git a/src/cherrypy/_cpchecker.py b/src/cherrypy/_cpchecker.py
index 445a1f3201..ddf3438ce3 100644
--- a/src/cherrypy/_cpchecker.py
+++ b/src/cherrypy/_cpchecker.py
@@ -39,7 +39,7 @@ class Checker(object):
finally:
warnings.formatwarning = oldformatwarning
- def formatwarning(self, message, category, filename, lineno):
+ def formatwarning(self, message, category, filename, lineno, line=None):
"""Function to format a warning."""
return "CherryPy Checker:\n%s\n\n" % message
@@ -58,7 +58,7 @@ class Checker(object):
"specific sections. You must explicitly pass "
"application config via "
"cherrypy.tree.mount(..., config=app_config)")
- warnings.warn(msg[:5])
+ warnings.warn(msg)
return
def check_static_paths(self):
diff --git a/src/cherrypy/_cperror.py b/src/cherrypy/_cperror.py
index 506a6652d9..d3d0f229c3 100644
--- a/src/cherrypy/_cperror.py
+++ b/src/cherrypy/_cperror.py
@@ -187,7 +187,9 @@ class HTTPError(CherryPyException):
self.status = status = int(status)
if status < 400 or status > 599:
raise ValueError("status must be between 400 and 599.")
- self.message = message
+ # See http://www.python.org/dev/peps/pep-0352/
+ # self.message = message
+ self._message = message
CherryPyException.__init__(self, status, message)
def set_response(self):
@@ -211,7 +213,7 @@ class HTTPError(CherryPyException):
response.headers['Content-Type'] = "text/html"
content = self.get_error_page(self.status, traceback=tb,
- message=self.message)
+ message=self._message)
response.body = content
response.headers['Content-Length'] = len(content)
diff --git a/src/cherrypy/_cprequest.py b/src/cherrypy/_cprequest.py
index 3b245519cb..23bdad23b6 100644
--- a/src/cherrypy/_cprequest.py
+++ b/src/cherrypy/_cprequest.py
@@ -646,7 +646,11 @@ class Request(object):
# Handle cookies differently because on Konqueror, multiple
# cookies come on different lines with the same key
if name == 'Cookie':
- self.cookie.load(value)
+ try:
+ self.cookie.load(value)
+ except Cookie.CookieError:
+ msg = "Illegal cookie name %s" % value.split('=')[0]
+ raise cherrypy.HTTPError(400, msg)
if not dict.__contains__(headers, 'Host'):
# All Internet-based HTTP/1.1 servers MUST respond with a 400
diff --git a/src/cherrypy/lib/caching.py b/src/cherrypy/lib/caching.py
index d43a593279..cc6b79c605 100644
--- a/src/cherrypy/lib/caching.py
+++ b/src/cherrypy/lib/caching.py
@@ -17,7 +17,11 @@ class MemoryCache:
self.clear()
t = threading.Thread(target=self.expire_cache, name='expire_cache')
self.expiration_thread = t
- t.setDaemon(True)
+ if hasattr(threading.Thread, "daemon"):
+ # Python 2.6+
+ t.daemon = True
+ else:
+ t.setDaemon(True)
t.start()
def clear(self):
diff --git a/src/cherrypy/lib/cptools.py b/src/cherrypy/lib/cptools.py
index eefd1ae73a..b54019cb81 100644
--- a/src/cherrypy/lib/cptools.py
+++ b/src/cherrypy/lib/cptools.py
@@ -1,7 +1,13 @@
"""Functions for builtin CherryPy tools."""
import logging
-import md5
+
+try:
+ # Python 2.5+
+ from hashlib import md5
+except ImportError:
+ from md5 import new as md5
+
import re
import cherrypy
@@ -40,7 +46,7 @@ def validate_etags(autotags=False):
if (not etag) and autotags:
if status == 200:
etag = response.collapse_body()
- etag = '"%s"' % md5.new(etag).hexdigest()
+ etag = '"%s"' % md5(etag).hexdigest()
response.headers['ETag'] = etag
response.ETag = etag
diff --git a/src/cherrypy/lib/encoding.py b/src/cherrypy/lib/encoding.py
index b5b77f0d28..94dc908e08 100644
--- a/src/cherrypy/lib/encoding.py
+++ b/src/cherrypy/lib/encoding.py
@@ -241,7 +241,7 @@ def gzip(compress_level=9, mime_types=['text/html', 'text/plain']):
# to the client.
return
- ct = response.headers.get('Content-Type').split(';')[0]
+ ct = response.headers.get('Content-Type', '').split(';')[0]
for coding in acceptable:
if coding.value == 'identity' and coding.qvalue != 0:
return
diff --git a/src/cherrypy/lib/httpauth.py b/src/cherrypy/lib/httpauth.py
index bc658244cd..083f4c5f9e 100644
--- a/src/cherrypy/lib/httpauth.py
+++ b/src/cherrypy/lib/httpauth.py
@@ -59,7 +59,13 @@ __all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
"calculateNonce", "SUPPORTED_QOP")
################################################################################
-import md5
+
+try:
+ # Python 2.5+
+ from hashlib import md5
+except ImportError:
+ from md5 import new as md5
+
import time
import base64
import urllib2
@@ -76,9 +82,9 @@ SUPPORTED_QOP = (AUTH, AUTH_INT)
# doAuth
#
DIGEST_AUTH_ENCODERS = {
- MD5: lambda val: md5.new (val).hexdigest (),
- MD5_SESS: lambda val: md5.new (val).hexdigest (),
-# SHA: lambda val: sha.new (val).hexdigest (),
+ MD5: lambda val: md5(val).hexdigest(),
+ MD5_SESS: lambda val: md5(val).hexdigest(),
+# SHA: lambda val: sha(val).hexdigest(),
}
def calculateNonce (realm, algorithm = MD5):
diff --git a/src/cherrypy/lib/sessions-r2062.py b/src/cherrypy/lib/sessions-r2062.py
deleted file mode 100644
index 881002e6fd..0000000000
--- a/src/cherrypy/lib/sessions-r2062.py
+++ /dev/null
@@ -1,698 +0,0 @@
-"""Session implementation for CherryPy.
-
-We use cherrypy.request to store some convenient variables as
-well as data about the session for the current request. Instead of
-polluting cherrypy.request we use a Session object bound to
-cherrypy.session to store these variables.
-"""
-
-import datetime
-import os
-try:
- import cPickle as pickle
-except ImportError:
- import pickle
-import random
-import sha
-import time
-import threading
-import types
-from warnings import warn
-
-import cherrypy
-from cherrypy.lib import http
-
-
-missing = object()
-
-class Session(object):
- """A CherryPy dict-like Session object (one per request)."""
-
- __metaclass__ = cherrypy._AttributeDocstrings
-
- _id = None
- id_observers = None
- id_observers__doc = "A list of callbacks to which to pass new id's."
-
- id__doc = "The current session ID."
- def _get_id(self):
- return self._id
- def _set_id(self, value):
- self._id = value
- for o in self.id_observers:
- o(value)
- id = property(_get_id, _set_id, doc=id__doc)
-
- timeout = 60
- timeout__doc = "Number of minutes after which to delete session data."
-
- locked = False
- locked__doc = """
- If True, this session instance has exclusive read/write access
- to session data."""
-
- loaded = False
- loaded__doc = """
- If True, data has been retrieved from storage. This should happen
- automatically on the first attempt to access session data."""
-
- clean_thread = None
- clean_thread__doc = "Class-level Monitor which calls self.clean_up."
-
- clean_freq = 5
- clean_freq__doc = "The poll rate for expired session cleanup in minutes."
-
- def __init__(self, id=None, **kwargs):
- self.id_observers = []
- self._data = {}
-
- for k, v in kwargs.iteritems():
- setattr(self, k, v)
-
- if id is None:
- self.regenerate()
- else:
- self.id = id
- if not self._exists():
- # Expired or malicious session. Make a new one.
- # See http://www.cherrypy.org/ticket/709.
- self.id = None
- self.regenerate()
-
- def regenerate(self):
- """Replace the current session (with a new id)."""
- if self.id is not None:
- self.delete()
-
- old_session_was_locked = self.locked
- if old_session_was_locked:
- self.release_lock()
-
- self.id = None
- while self.id is None:
- self.id = self.generate_id()
- # Assert that the generated id is not already stored.
- if self._exists():
- self.id = None
-
- if old_session_was_locked:
- self.acquire_lock()
-
- def clean_up(self):
- """Clean up expired sessions."""
- pass
-
- try:
- os.urandom(20)
- except (AttributeError, NotImplementedError):
- # os.urandom not available until Python 2.4. Fall back to random.random.
- def generate_id(self):
- """Return a new session id."""
- return sha.new('%s' % random.random()).hexdigest()
- else:
- def generate_id(self):
- """Return a new session id."""
- return os.urandom(20).encode('hex')
-
- def save(self):
- """Save session data."""
- try:
- # If session data has never been loaded then it's never been
- # accessed: no need to delete it
- if self.loaded:
- t = datetime.timedelta(seconds = self.timeout * 60)
- expiration_time = datetime.datetime.now() + t
- self._save(expiration_time)
-
- finally:
- if self.locked:
- # Always release the lock if the user didn't release it
- self.release_lock()
-
- def load(self):
- """Copy stored session data into this session instance."""
- data = self._load()
- # data is either None or a tuple (session_data, expiration_time)
- if data is None or data[1] < datetime.datetime.now():
- # Expired session: flush session data
- self._data = {}
- else:
- self._data = data[0]
- self.loaded = True
-
- # Stick the clean_thread in the class, not the instance.
- # The instances are created and destroyed per-request.
- cls = self.__class__
- if self.clean_freq and not cls.clean_thread:
- # clean_up is in instancemethod and not a classmethod,
- # so that tool config can be accessed inside the method.
- t = cherrypy.process.plugins.Monitor(
- cherrypy.engine, self.clean_up, self.clean_freq * 60)
- t.subscribe()
- cls.clean_thread = t
- t.start()
-
- def delete(self):
- """Delete stored session data."""
- self._delete()
-
- def __getitem__(self, key):
- if not self.loaded: self.load()
- return self._data[key]
-
- def __setitem__(self, key, value):
- if not self.loaded: self.load()
- self._data[key] = value
-
- def __delitem__(self, key):
- if not self.loaded: self.load()
- del self._data[key]
-
- def pop(self, key, default=missing):
- """Remove the specified key and return the corresponding value.
- If key is not found, default is returned if given,
- otherwise KeyError is raised.
- """
- if not self.loaded: self.load()
- if default is missing:
- return self._data.pop(key)
- else:
- return self._data.pop(key, default)
-
- def __contains__(self, key):
- if not self.loaded: self.load()
- return key in self._data
-
- def has_key(self, key):
- """D.has_key(k) -> True if D has a key k, else False."""
- if not self.loaded: self.load()
- return self._data.has_key(key)
-
- def get(self, key, default=None):
- """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
- if not self.loaded: self.load()
- return self._data.get(key, default)
-
- def update(self, d):
- """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k]."""
- if not self.loaded: self.load()
- self._data.update(d)
-
- def setdefault(self, key, default=None):
- """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
- if not self.loaded: self.load()
- return self._data.setdefault(key, default)
-
- def clear(self):
- """D.clear() -> None. Remove all items from D."""
- if not self.loaded: self.load()
- self._data.clear()
-
- def keys(self):
- """D.keys() -> list of D's keys."""
- if not self.loaded: self.load()
- return self._data.keys()
-
- def items(self):
- """D.items() -> list of D's (key, value) pairs, as 2-tuples."""
- if not self.loaded: self.load()
- return self._data.items()
-
- def values(self):
- """D.values() -> list of D's values."""
- if not self.loaded: self.load()
- return self._data.values()
-
-
-class RamSession(Session):
-
- # Class-level objects. Don't rebind these!
- cache = {}
- locks = {}
-
- def clean_up(self):
- """Clean up expired sessions."""
- now = datetime.datetime.now()
- for id, (data, expiration_time) in self.cache.items():
- if expiration_time < now:
- try:
- del self.cache[id]
- except KeyError:
- pass
- try:
- del self.locks[id]
- except KeyError:
- pass
-
- def _exists(self):
- return self.id in self.cache
-
- def _load(self):
- return self.cache.get(self.id)
-
- def _save(self, expiration_time):
- self.cache[self.id] = (self._data, expiration_time)
-
- def _delete(self):
- del self.cache[self.id]
-
- def acquire_lock(self):
- """Acquire an exclusive lock on the currently-loaded session data."""
- self.locked = True
- self.locks.setdefault(self.id, threading.RLock()).acquire()
-
- def release_lock(self):
- """Release the lock on the currently-loaded session data."""
- self.locks[self.id].release()
- self.locked = False
-
- def __len__(self):
- """Return the number of active sessions."""
- return len(self.cache)
-
-
-class FileSession(Session):
- """Implementation of the File backend for sessions
-
- storage_path: the folder where session data will be saved. Each session
- will be saved as pickle.dump(data, expiration_time) in its own file;
- the filename will be self.SESSION_PREFIX + self.id.
- """
-
- SESSION_PREFIX = 'session-'
- LOCK_SUFFIX = '.lock'
-
- def __init__(self, id=None, **kwargs):
- # The 'storage_path' arg is required for file-based sessions.
- kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
- Session.__init__(self, id=id, **kwargs)
-
- def setup(cls, **kwargs):
- """Set up the storage system for file-based sessions.
-
- This should only be called once per process; this will be done
- automatically when using sessions.init (as the built-in Tool does).
- """
- # The 'storage_path' arg is required for file-based sessions.
- kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
-
- for k, v in kwargs.iteritems():
- setattr(cls, k, v)
-
- # Warn if any lock files exist at startup.
- lockfiles = [fname for fname in os.listdir(cls.storage_path)
- if (fname.startswith(cls.SESSION_PREFIX)
- and fname.endswith(cls.LOCK_SUFFIX))]
- if lockfiles:
- plural = ('', 's')[len(lockfiles) > 1]
- warn("%s session lockfile%s found at startup. If you are "
- "only running one process, then you may need to "
- "manually delete the lockfiles found at %r."
- % (len(lockfiles), plural, cls.storage_path))
- setup = classmethod(setup)
-
- def _get_file_path(self):
- f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
- if not os.path.abspath(f).startswith(self.storage_path):
- raise cherrypy.HTTPError(400, "Invalid session id in cookie.")
- return f
-
- def _exists(self):
- path = self._get_file_path()
- return os.path.exists(path)
-
- def _load(self, path=None):
- if path is None:
- path = self._get_file_path()
- try:
- f = open(path, "rb")
- try:
- return pickle.load(f)
- finally:
- f.close()
- except (IOError, EOFError):
- return None
-
- def _save(self, expiration_time):
- f = open(self._get_file_path(), "wb")
- try:
- pickle.dump((self._data, expiration_time), f)
- finally:
- f.close()
-
- def _delete(self):
- try:
- os.unlink(self._get_file_path())
- except OSError:
- pass
-
- def acquire_lock(self, path=None):
- """Acquire an exclusive lock on the currently-loaded session data."""
- if path is None:
- path = self._get_file_path()
- path += self.LOCK_SUFFIX
- while True:
- try:
- lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
- except OSError:
- time.sleep(0.1)
- else:
- os.close(lockfd)
- break
- self.locked = True
-
- def release_lock(self, path=None):
- """Release the lock on the currently-loaded session data."""
- if path is None:
- path = self._get_file_path()
- os.unlink(path + self.LOCK_SUFFIX)
- self.locked = False
-
- def clean_up(self):
- """Clean up expired sessions."""
- now = datetime.datetime.now()
- # Iterate over all session files in self.storage_path
- for fname in os.listdir(self.storage_path):
- if (fname.startswith(self.SESSION_PREFIX)
- and not fname.endswith(self.LOCK_SUFFIX)):
- # We have a session file: lock and load it and check
- # if it's expired. If it fails, nevermind.
- path = os.path.join(self.storage_path, fname)
- self.acquire_lock(path)
- try:
- contents = self._load(path)
- # _load returns None on IOError
- if contents is not None:
- data, expiration_time = contents
- if expiration_time < now:
- # Session expired: deleting it
- os.unlink(path)
- finally:
- self.release_lock(path)
-
- def __len__(self):
- """Return the number of active sessions."""
- return len([fname for fname in os.listdir(self.storage_path)
- if (fname.startswith(self.SESSION_PREFIX)
- and not fname.endswith(self.LOCK_SUFFIX))])
-
-
-class PostgresqlSession(Session):
- """ Implementation of the PostgreSQL backend for sessions. It assumes
- a table like this:
-
- create table session (
- id varchar(40),
- data text,
- expiration_time timestamp
- )
-
- You must provide your own get_db function.
- """
-
- def __init__(self, id=None, **kwargs):
- Session.__init__(self, id, **kwargs)
- self.cursor = self.db.cursor()
-
- def setup(cls, **kwargs):
- """Set up the storage system for Postgres-based sessions.
-
- This should only be called once per process; this will be done
- automatically when using sessions.init (as the built-in Tool does).
- """
- for k, v in kwargs.iteritems():
- setattr(cls, k, v)
-
- self.db = self.get_db()
- setup = classmethod(setup)
-
- def __del__(self):
- if self.cursor:
- self.cursor.close()
- self.db.commit()
-
- def _exists(self):
- # Select session data from table
- self.cursor.execute('select data, expiration_time from session '
- 'where id=%s', (self.id,))
- rows = self.cursor.fetchall()
- return bool(rows)
-
- def _load(self):
- # Select session data from table
- self.cursor.execute('select data, expiration_time from session '
- 'where id=%s', (self.id,))
- rows = self.cursor.fetchall()
- if not rows:
- return None
-
- pickled_data, expiration_time = rows[0]
- data = pickle.loads(pickled_data)
- return data, expiration_time
-
- def _save(self, expiration_time):
- pickled_data = pickle.dumps(self._data)
- self.cursor.execute('update session set data = %s, '
- 'expiration_time = %s where id = %s',
- (pickled_data, expiration_time, self.id))
-
- def _delete(self):
- self.cursor.execute('delete from session where id=%s', (self.id,))
-
- def acquire_lock(self):
- """Acquire an exclusive lock on the currently-loaded session data."""
- # We use the "for update" clause to lock the row
- self.locked = True
- self.cursor.execute('select id from session where id=%s for update',
- (self.id,))
-
- def release_lock(self):
- """Release the lock on the currently-loaded session data."""
- # We just close the cursor and that will remove the lock
- # introduced by the "for update" clause
- self.cursor.close()
- self.locked = False
-
- def clean_up(self):
- """Clean up expired sessions."""
- self.cursor.execute('delete from session where expiration_time < %s',
- (datetime.datetime.now(),))
-
-
-class MemcachedSession(Session):
-
- # The most popular memcached client for Python isn't thread-safe.
- # Wrap all .get and .set operations in a single lock.
- mc_lock = threading.RLock()
-
- # This is a seperate set of locks per session id.
- locks = {}
-
- servers = ['127.0.0.1:11211']
-
- def setup(cls, **kwargs):
- """Set up the storage system for memcached-based sessions.
-
- This should only be called once per process; this will be done
- automatically when using sessions.init (as the built-in Tool does).
- """
- for k, v in kwargs.iteritems():
- setattr(cls, k, v)
-
- import memcache
- cls.cache = memcache.Client(cls.servers)
- setup = classmethod(setup)
-
- def _exists(self):
- self.mc_lock.acquire()
- try:
- return bool(self.cache.get(self.id))
- finally:
- self.mc_lock.release()
-
- def _load(self):
- self.mc_lock.acquire()
- try:
- return self.cache.get(self.id)
- finally:
- self.mc_lock.release()
-
- def _save(self, expiration_time):
- # Send the expiration time as "Unix time" (seconds since 1/1/1970)
- td = int(time.mktime(expiration_time.timetuple()))
- self.mc_lock.acquire()
- try:
- if not self.cache.set(self.id, (self._data, expiration_time), td):
- raise AssertionError("Session data for id %r not set." % self.id)
- finally:
- self.mc_lock.release()
-
- def _delete(self):
- self.cache.delete(self.id)
-
- def acquire_lock(self):
- """Acquire an exclusive lock on the currently-loaded session data."""
- self.locked = True
- self.locks.setdefault(self.id, threading.RLock()).acquire()
-
- def release_lock(self):
- """Release the lock on the currently-loaded session data."""
- self.locks[self.id].release()
- self.locked = False
-
- def __len__(self):
- """Return the number of active sessions."""
- raise NotImplementedError
-
-
-# Hook functions (for CherryPy tools)
-
-def save():
- """Save any changed session data."""
-
- if not hasattr(cherrypy.serving, "session"):
- return
-
- # Guard against running twice
- if hasattr(cherrypy.request, "_sessionsaved"):
- return
- cherrypy.request._sessionsaved = True
-
- if cherrypy.response.stream:
- # If the body is being streamed, we have to save the data
- # *after* the response has been written out
- cherrypy.request.hooks.attach('on_end_request', cherrypy.session.save)
- else:
- # If the body is not being streamed, we save the data now
- # (so we can release the lock).
- if isinstance(cherrypy.response.body, types.GeneratorType):
- cherrypy.response.collapse_body()
- cherrypy.session.save()
-save.failsafe = True
-
-def close():
- """Close the session object for this request."""
- sess = getattr(cherrypy.serving, "session", None)
- if getattr(sess, "locked", False):
- # If the session is still locked we release the lock
- sess.release_lock()
-close.failsafe = True
-close.priority = 90
-
-
-def init(storage_type='ram', path=None, path_header=None, name='session_id',
- timeout=60, domain=None, secure=False, clean_freq=5,
- persistent=True, **kwargs):
- """Initialize session object (using cookies).
-
- storage_type: one of 'ram', 'file', 'postgresql'. This will be used
- to look up the corresponding class in cherrypy.lib.sessions
- globals. For example, 'file' will use the FileSession class.
- path: the 'path' value to stick in the response cookie metadata.
- path_header: if 'path' is None (the default), then the response
- cookie 'path' will be pulled from request.headers[path_header].
- name: the name of the cookie.
- timeout: the expiration timeout (in minutes) for the stored session data.
- If 'persistent' is True (the default), this is also the timeout
- for the cookie.
- domain: the cookie domain.
- secure: if False (the default) the cookie 'secure' value will not
- be set. If True, the cookie 'secure' value will be set (to 1).
- clean_freq (minutes): the poll rate for expired session cleanup.
- persistent: if True (the default), the 'timeout' argument will be used
- to expire the cookie. If False, the cookie will not have an expiry,
- and the cookie will be a "session cookie" which expires when the
- browser is closed.
-
- Any additional kwargs will be bound to the new Session instance,
- and may be specific to the storage type. See the subclass of Session
- you're using for more information.
- """
-
- request = cherrypy.request
-
- # Guard against running twice
- if hasattr(request, "_session_init_flag"):
- return
- request._session_init_flag = True
-
- # Check if request came with a session ID
- id = None
- if name in request.cookie:
- id = request.cookie[name].value
-
- # Find the storage class and call setup (first time only).
- storage_class = storage_type.title() + 'Session'
- storage_class = globals()[storage_class]
- if not hasattr(cherrypy, "session"):
- if hasattr(storage_class, "setup"):
- storage_class.setup(**kwargs)
-
- # Create and attach a new Session instance to cherrypy.serving.
- # It will possess a reference to (and lock, and lazily load)
- # the requested session data.
- kwargs['timeout'] = timeout
- kwargs['clean_freq'] = clean_freq
- cherrypy.serving.session = sess = storage_class(id, **kwargs)
- def update_cookie(id):
- """Update the cookie every time the session id changes."""
- cherrypy.response.cookie[name] = id
- sess.id_observers.append(update_cookie)
-
- # Create cherrypy.session which will proxy to cherrypy.serving.session
- if not hasattr(cherrypy, "session"):
- cherrypy.session = cherrypy._ThreadLocalProxy('session')
-
- if persistent:
- cookie_timeout = timeout
- else:
- # See http://support.microsoft.com/kb/223799/EN-US/
- # and http://support.mozilla.com/en-US/kb/Cookies
- cookie_timeout = None
- set_response_cookie(path=path, path_header=path_header, name=name,
- timeout=cookie_timeout, domain=domain, secure=secure)
-
-
-def set_response_cookie(path=None, path_header=None, name='session_id',
- timeout=60, domain=None, secure=False):
- """Set a response cookie for the client.
-
- path: the 'path' value to stick in the response cookie metadata.
- path_header: if 'path' is None (the default), then the response
- cookie 'path' will be pulled from request.headers[path_header].
- name: the name of the cookie.
- timeout: the expiration timeout for the cookie. If 0 or other boolean
- False, no 'expires' param will be set, and the cookie will be a
- "session cookie" which expires when the browser is closed.
- domain: the cookie domain.
- secure: if False (the default) the cookie 'secure' value will not
- be set. If True, the cookie 'secure' value will be set (to 1).
- """
- # Set response cookie
- cookie = cherrypy.response.cookie
- cookie[name] = cherrypy.serving.session.id
- cookie[name]['path'] = (path or cherrypy.request.headers.get(path_header)
- or '/')
-
- # We'd like to use the "max-age" param as indicated in
- # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
- # save it to disk and the session is lost if people close
- # the browser. So we have to use the old "expires" ... sigh ...
-## cookie[name]['max-age'] = timeout * 60
- if timeout:
- cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60))
- if domain is not None:
- cookie[name]['domain'] = domain
- if secure:
- cookie[name]['secure'] = 1
-
-
-def expire():
- """Expire the current session cookie."""
- name = cherrypy.request.config.get('tools.sessions.name', 'session_id')
- one_year = 60 * 60 * 24 * 365
- exp = time.gmtime(time.time() - one_year)
- t = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", exp)
- cherrypy.response.cookie[name]['expires'] = t
-
-
diff --git a/src/cherrypy/lib/sessions.py b/src/cherrypy/lib/sessions.py
index fb3676c557..f9b52d4e37 100644
--- a/src/cherrypy/lib/sessions.py
+++ b/src/cherrypy/lib/sessions.py
@@ -13,7 +13,13 @@ try:
except ImportError:
import pickle
import random
-import sha
+
+try:
+ # Python 2.5+
+ from hashlib import sha1 as sha
+except ImportError:
+ from sha import new as sha
+
import time
import threading
import types
@@ -108,7 +114,7 @@ class Session(object):
# os.urandom not available until Python 2.4. Fall back to random.random.
def generate_id(self):
"""Return a new session id."""
- return sha.new('%s' % random.random()).hexdigest()
+ return sha('%s' % random.random()).hexdigest()
else:
def generate_id(self):
"""Return a new session id."""
diff --git a/src/cherrypy/lib/static.py b/src/cherrypy/lib/static.py
index f4e3efe054..2a5a9f6829 100644
--- a/src/cherrypy/lib/static.py
+++ b/src/cherrypy/lib/static.py
@@ -167,6 +167,9 @@ def staticdir(section, dir, root="", match="", content_types=None, index=""):
'/home/me', the Request-URI is 'myapp', and the index arg is
'index.html', the file '/home/me/myapp/index.html' will be sought.
"""
+ if cherrypy.request.method not in ('GET', 'HEAD'):
+ return False
+
if match and not re.search(match, cherrypy.request.path_info):
return False
@@ -217,6 +220,9 @@ def staticfile(filename, root=None, match="", content_types=None):
a string (e.g. "gif") and 'content-type' is the value to write
out in the Content-Type response header (e.g. "image/gif").
"""
+ if cherrypy.request.method not in ('GET', 'HEAD'):
+ return False
+
if match and not re.search(match, cherrypy.request.path_info):
return False
diff --git a/src/cherrypy/process/servers.py b/src/cherrypy/process/servers.py
index ac4178db0b..932d28d01f 100644
--- a/src/cherrypy/process/servers.py
+++ b/src/cherrypy/process/servers.py
@@ -5,33 +5,33 @@ import time
class ServerAdapter(object):
"""Adapter for an HTTP server.
-
+
If you need to start more than one HTTP server (to serve on multiple
ports, or protocols, etc.), you can manually register each one and then
start them all with bus.start:
-
+
s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
s1.subscribe()
s2.subscribe()
bus.start()
"""
-
+
def __init__(self, bus, httpserver=None, bind_addr=None):
self.bus = bus
self.httpserver = httpserver
self.bind_addr = bind_addr
self.interrupt = None
self.running = False
-
+
def subscribe(self):
self.bus.subscribe('start', self.start)
self.bus.subscribe('stop', self.stop)
-
+
def unsubscribe(self):
self.bus.unsubscribe('start', self.start)
self.bus.unsubscribe('stop', self.stop)
-
+
def start(self):
"""Start the HTTP server."""
if isinstance(self.bind_addr, tuple):
@@ -39,29 +39,29 @@ class ServerAdapter(object):
on_what = "%s:%s" % (host, port)
else:
on_what = "socket file: %s" % self.bind_addr
-
+
if self.running:
self.bus.log("Already serving on %s" % on_what)
return
-
+
self.interrupt = None
if not self.httpserver:
raise ValueError("No HTTP server has been created.")
-
+
# Start the httpserver in a new thread.
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
-
+
import threading
t = threading.Thread(target=self._start_http_thread)
t.setName("HTTPServer " + t.getName())
t.start()
-
+
self.wait()
self.running = True
self.bus.log("Serving on %s" % on_what)
start.priority = 75
-
+
def _start_http_thread(self):
"""HTTP servers MUST be running in new threads, so that the
main thread persists to receive KeyboardInterrupt's. If an
@@ -87,19 +87,19 @@ class ServerAdapter(object):
traceback=True, level=40)
self.bus.exit()
raise
-
+
def wait(self):
"""Wait until the HTTP server is ready to receive requests."""
while not getattr(self.httpserver, "ready", False):
if self.interrupt:
raise self.interrupt
time.sleep(.1)
-
+
# Wait for port to be occupied
if isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
wait_for_occupied_port(host, port)
-
+
def stop(self):
"""Stop the HTTP server."""
if self.running:
@@ -113,7 +113,7 @@ class ServerAdapter(object):
else:
self.bus.log("HTTP Server %s already shut down" % self.httpserver)
stop.priority = 25
-
+
def restart(self):
"""Restart the HTTP server."""
self.stop()
@@ -122,12 +122,12 @@ class ServerAdapter(object):
class FlupFCGIServer(object):
"""Adapter for a flup.server.fcgi.WSGIServer."""
-
+
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.ready = False
-
+
def start(self):
"""Start the FCGI server."""
# We have to instantiate the server class here because its __init__
@@ -147,24 +147,24 @@ class FlupFCGIServer(object):
self.fcgiserver._oldSIGs = []
self.ready = True
self.fcgiserver.run()
-
+
def stop(self):
"""Stop the HTTP server."""
- self.ready = False
# Forcibly stop the fcgi server main event loop.
self.fcgiserver._keepGoing = False
# Force all worker threads to die off.
- self.fcgiserver._threadPool.maxSpare = 0
+ self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount
+ self.ready = False
class FlupSCGIServer(object):
"""Adapter for a flup.server.scgi.WSGIServer."""
-
+
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.ready = False
-
+
def start(self):
"""Start the SCGI server."""
# We have to instantiate the server class here because its __init__
@@ -184,7 +184,7 @@ class FlupSCGIServer(object):
self.scgiserver._oldSIGs = []
self.ready = True
self.scgiserver.run()
-
+
def stop(self):
"""Stop the HTTP server."""
self.ready = False
@@ -210,9 +210,9 @@ def check_port(host, port, timeout=1.0):
raise ValueError("Host values of '' or None are not allowed.")
host = client_host(host)
port = int(port)
-
+
import socket
-
+
# AF_INET or AF_INET6 socket
# Get the correct address family for our host (allows IPv6 addresses)
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
@@ -237,24 +237,24 @@ def wait_for_free_port(host, port):
"""Wait for the specified port to become free (drop requests)."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
-
+
for trial in xrange(50):
try:
# we are expecting a free port, so reduce the timeout
- check_port(host, port, timeout=0.1)
+ check_port(host, port, timeout=0.2)
except IOError:
# Give the old server thread time to free the port.
- time.sleep(0.1)
+ time.sleep(0.2)
else:
return
-
+
raise IOError("Port %r not free on %r" % (port, host))
def wait_for_occupied_port(host, port):
"""Wait for the specified port to become active (receive requests)."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
-
+
for trial in xrange(50):
try:
check_port(host, port)
@@ -262,5 +262,5 @@ def wait_for_occupied_port(host, port):
return
else:
time.sleep(.1)
-
+
raise IOError("Port %r not bound on %r" % (port, host))
diff --git a/src/cherrypy/process/wspbus.py b/src/cherrypy/process/wspbus.py
index 26abb4702c..5bbcb8c629 100644
--- a/src/cherrypy/process/wspbus.py
+++ b/src/cherrypy/process/wspbus.py
@@ -199,14 +199,21 @@ class Bus(object):
def exit(self):
"""Stop all services and prepare to exit the process."""
- self.stop()
-
- self.state = states.EXITING
- self.log('Bus EXITING')
- self.publish('exit')
- # This isn't strictly necessary, but it's better than seeing
- # "Waiting for child threads to terminate..." and then nothing.
- self.log('Bus EXITED')
+ try:
+ self.stop()
+
+ self.state = states.EXITING
+ self.log('Bus EXITING')
+ self.publish('exit')
+ # This isn't strictly necessary, but it's better than seeing
+ # "Waiting for child threads to terminate..." and then nothing.
+ self.log('Bus EXITED')
+ except:
+ # This method is often called asynchronously (whether thread,
+ # signal handler, console handler, or atexit handler), so we
+ # can't just let exceptions propagate out unhandled.
+ # Assume it's been logged and just die.
+ os._exit(70) # EX_SOFTWARE
def restart(self):
"""Restart the process (may close connections).
@@ -223,7 +230,14 @@ class Bus(object):
self.publish('graceful')
def block(self, interval=0.1):
- """Wait for the EXITING state, KeyboardInterrupt or SystemExit."""
+ """Wait for the EXITING state, KeyboardInterrupt or SystemExit.
+
+ This function is intended to be called only by the main thread.
+ After waiting for the EXITING state, it also waits for all threads
+ to terminate, and then calls os.execv if self.execv is True. This
+ design allows another thread to call bus.restart, yet have the main
+ thread perform the actual execv call (required on some platforms).
+ """
try:
self.wait(states.EXITING, interval=interval)
except (KeyboardInterrupt, IOError):
@@ -243,10 +257,15 @@ class Bus(object):
# See http://www.cherrypy.org/ticket/751.
self.log("Waiting for child threads to terminate...")
for t in threading.enumerate():
- if (t != threading.currentThread() and t.isAlive()
+ if t != threading.currentThread() and t.isAlive():
# Note that any dummy (external) threads are always daemonic.
- and not t.isDaemon()):
- t.join()
+ if hasattr(threading.Thread, "daemon"):
+ # Python 2.6+
+ d = t.daemon
+ else:
+ d = t.isDaemon()
+ if not d:
+ t.join()
if self.execv:
self._do_execv()
diff --git a/src/cherrypy/wsgiserver/__init__.py b/src/cherrypy/wsgiserver/__init__.py
index a92869f56d..c380e18b05 100644
--- a/src/cherrypy/wsgiserver/__init__.py
+++ b/src/cherrypy/wsgiserver/__init__.py
@@ -1191,28 +1191,39 @@ class HTTPConnection(object):
# Close the connection.
return
except NoSSLError:
- # Unwrap our wfile
- req.wfile = CP_fileobject(self.socket, "wb", -1)
if req and not req.sent_headers:
+ # Unwrap our wfile
+ req.wfile = CP_fileobject(self.socket._sock, "wb", -1)
req.simple_response("400 Bad Request",
"The client sent a plain HTTP request, but "
"this server only speaks HTTPS on this port.")
+ self.linger = True
except Exception, e:
if req and not req.sent_headers:
req.simple_response("500 Internal Server Error", format_exc())
+ linger = False
+
def close(self):
"""Close the socket underlying this connection."""
self.rfile.close()
- # Python's socket module does NOT call close on the kernel socket
- # when you call socket.close(). We do so manually here because we
- # want this server to send a FIN TCP segment immediately. Note this
- # must be called *before* calling socket.close(), because the latter
- # drops its reference to the kernel socket.
- self.socket._sock.close()
-
- self.socket.close()
+ if not self.linger:
+ # Python's socket module does NOT call close on the kernel socket
+ # when you call socket.close(). We do so manually here because we
+ # want this server to send a FIN TCP segment immediately. Note this
+ # must be called *before* calling socket.close(), because the latter
+ # drops its reference to the kernel socket.
+ self.socket._sock.close()
+ self.socket.close()
+ else:
+ # On the other hand, sometimes we want to hang around for a bit
+ # to make sure the client has a chance to read our entire
+ # response. Skipping the close() calls here delays the FIN
+ # packet until the socket object is garbage-collected later.
+ # Someday, perhaps, we'll do the full lingering_close that
+ # Apache does, but not today.
+ pass
def format_exc(limit=None):
@@ -1457,7 +1468,7 @@ class CherryPyWSGIServer(object):
protocol = "HTTP/1.1"
_bind_addr = "127.0.0.1"
- version = "CherryPy/3.1.1"
+ version = "CherryPy/3.1.2"
ready = False
_interrupt = None
@@ -1709,7 +1720,7 @@ class CherryPyWSGIServer(object):
try:
host, port = sock.getsockname()[:2]
except socket.error, x:
- if x.args[1] != "Bad file descriptor":
+ if x.args[0] not in socket_errors_to_ignore:
raise
else:
# Note that we're explicitly NOT using AI_PASSIVE,
From 5e159653d5d24561c81a27de3e39f041b3bcb505 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 27 Aug 2009 10:39:08 -0600
Subject: [PATCH 04/21] Fix CNN recipe
---
src/calibre/web/feeds/recipes/recipe_cnn.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/calibre/web/feeds/recipes/recipe_cnn.py b/src/calibre/web/feeds/recipes/recipe_cnn.py
index 6ce0f87d4e..369aff2e99 100644
--- a/src/calibre/web/feeds/recipes/recipe_cnn.py
+++ b/src/calibre/web/feeds/recipes/recipe_cnn.py
@@ -7,7 +7,7 @@ import re
from calibre.web.feeds.news import BasicNewsRecipe
class CNN(BasicNewsRecipe):
-
+
title = 'CNN'
description = 'Global news'
timefmt = ' [%d %b %Y]'
@@ -20,7 +20,7 @@ class CNN(BasicNewsRecipe):
preprocess_regexps = [(re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in [
(r'.*?.*?', lambda match : ''),
- (r'', lambda match : ''),
+ (r'', lambda match : ''),
(r'<\!\-\-Article End\-\->.*?', lambda match : '