diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index e0aaa2d1af47f30fb55aa29c3452a13719d418bf..9c400d11c2c71b09be547e2db0f3294ac488bcdd 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -69,7 +69,7 @@ class Core(Plugin): def available(self): return jsonified({ - 'succes': True + 'success': True }) def shutdown(self): @@ -101,7 +101,7 @@ class Core(Plugin): self.shutdown_started = True - fireEvent('app.shutdown') + fireEvent('app.do_shutdown') log.debug('Every plugin got shutdown event') loop = True @@ -177,6 +177,6 @@ class Core(Plugin): def signalHandler(self): def signal_handler(signal, frame): - fireEvent('app.shutdown') + fireEvent('app.do_shutdown') signal.signal(signal.SIGINT, signal_handler) diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 63c4c0e659419b361674ecbbb0addf48d8a223ef..22e01c2f5a108286277f5d6a96c3a49c885e9f16 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -31,9 +31,14 @@ class Transmission(Downloader): # Set parameters for Transmission folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1] + folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) + + # Create the empty folder to download too + self.makeDir(folder_path) + params = { 'paused': self.conf('paused', default = 0), - 'download-dir': os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) + 'download-dir': folder_path } torrent_params = { diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 46847154b7853699e9b236ee84a3793cdd6878f5..fc2bfdb12b090559e7042bb8301654a9441e1c3f 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -35,7 +35,7 @@ class Plugin(object): http_failed_disabled = {} def registerPlugin(self): - addEvent('app.shutdown', self.doShutdown) + addEvent('app.do_shutdown', self.doShutdown) addEvent('plugin.running', self.isRunning) def conf(self, attr, value = None, default = None): @@ -114,8 +114,11 @@ class Plugin(object): # Don't try for failed requests if self.http_failed_disabled.get(host, 0) > 0: if self.http_failed_disabled[host] > (time.time() - 900): - log.info('Disabled calls to %s for 15 minutes because so many failed requests.', host) - raise Exception + log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host) + if not show_error: + raise + else: + return '' else: del self.http_failed_request[host] del self.http_failed_disabled[host] diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index b4e481992696d3ce31f5d8936cc9ad9e24e66527..b61ea7e0c790eab2d36254337de797b570ce5e85 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -300,10 +300,11 @@ var MovieList = new Class({ }, deleteSelected: function(){ - var self = this; - var ids = self.getSelectedMovies() + var self = this, + ids = self.getSelectedMovies(), + help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list'; - var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!', [{ + var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{ 'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'), 'class': 'delete', 'events': { diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 0534cd1be4732dfcbbaae39290317c7198fce6bf..6282b751c3f66fcea85047353954c27d06b03a25 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -22,7 +22,7 @@ class QualityPlugin(Plugin): {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']}, - {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, + {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index 5855c4f1b41942fb63faabcd628fc63ac75d962d..a6dd6913ab617762776a5a0cd14e52f8e131ee22 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -24,7 +24,8 @@ config = [{ 'name': 'required_words', 'label': 'Required words', 'default': '', - 'description': 'Ignore releases that don\'t contain at least one of these words.' + 'placeholder': 'Example: DTS, AC3 & English', + 'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' }, { 'name': 'ignored_words', diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 55115122920ae2a4b87a6b88aabc68b2a3b8a79b..26f9eeab0a0da266cb8b2108932bab32580b129c 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -3,7 +3,7 @@ from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.helpers.request import jsonified, getParam -from couchpotato.core.helpers.variable import md5, getTitle +from couchpotato.core.helpers.variable import md5, getTitle, splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Movie, Release, ReleaseInfo @@ -204,6 +204,10 @@ class Searcher(Plugin): for nzb in sorted_results: + if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0): + log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name'])) + continue + if nzb['status_id'] == ignored_status.get('id'): log.info('Ignored: %s', nzb['name']) continue @@ -301,13 +305,18 @@ class Searcher(Plugin): movie_words = re.split('\W+', simplifyString(movie_name)) nzb_name = simplifyString(nzb['name']) nzb_words = re.split('\W+', nzb_name) - required_words = [x.strip().lower() for x in self.conf('required_words').lower().split(',')] + required_words = splitString(self.conf('required_words').lower()) - if self.conf('required_words') and not list(set(nzb_words) & set(required_words)): + req_match = 0 + for req_set in required_words: + req = splitString(req_set, '&') + req_match += len(list(set(nzb_words) & set(req))) == len(req) + + if self.conf('required_words') and req_match == 0: log.info2("Wrong: Required word missing: %s" % nzb['name']) return False - ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')] + ignored_words = splitString(self.conf('ignored_words').lower()) blacklisted = list(set(nzb_words) & set(ignored_words)) if self.conf('ignored_words') and blacklisted: log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted))) @@ -389,6 +398,11 @@ class Searcher(Plugin): if list(set(nzb_words) & set(quality['alternative'])): found[quality['identifier']] = True + # Try guessing via quality tags + guess = fireEvent('quality.guess', [nzb.get('name')], single = True) + if guess: + found[guess['identifier']] = True + # Hack for older movies that don't contain quality tag year_name = fireEvent('scanner.name_year', name, single = True) if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 50a659b0f12b978280e5d6b6e58daf03cd405644..c2187246d60d8e1b165d2b4b7fd331e8cbdaef23 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -4,9 +4,11 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from urlparse import urlparse +import cookielib import re import time import traceback +import urllib2 log = CPLog(__name__) @@ -48,6 +50,8 @@ class YarrProvider(Provider): sizeMb = ['mb', 'mib'] sizeKb = ['kb', 'kib'] + login_opener = None + def __init__(self): addEvent('provider.belongs_to', self.belongsTo) @@ -56,6 +60,34 @@ class YarrProvider(Provider): addEvent('nzb.feed', self.feed) + def login(self): + + try: + cookiejar = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar)) + urllib2.install_opener(opener) + log.info2('Logging into %s', self.urls['login']) + f = opener.open(self.urls['login'], self.getLoginParams()) + f.read() + f.close() + self.login_opener = opener + return True + except: + log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc())) + + return False + + def loginDownload(self, url = '', nzb_id = ''): + try: + if not self.login_opener and not self.login(): + log.error('Failed downloading from %s', self.getName()) + return self.urlopen(url, opener = self.login_opener) + except: + log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc())) + + def getLoginParams(self): + return '' + def download(self, url = '', nzb_id = ''): try: return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()}, show_error = False) diff --git a/couchpotato/core/providers/nzb/ftdworld/__init__.py b/couchpotato/core/providers/nzb/ftdworld/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e11f486af6d88e76fa103f24c8109592e1a486f4 --- /dev/null +++ b/couchpotato/core/providers/nzb/ftdworld/__init__.py @@ -0,0 +1,31 @@ +from .main import FTDWorld + +def start(): + return FTDWorld() + +config = [{ + 'name': 'ftdworld', + 'groups': [ + { + 'tab': 'searcher', + 'subtab': 'nzb_providers', + 'name': 'FTDWorld', + 'description': 'Free provider, less accurate. See <a href="http://ftdworld.net">FTDWorld</a>', + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/nzb/ftdworld/main.py b/couchpotato/core/providers/nzb/ftdworld/main.py new file mode 100644 index 0000000000000000000000000000000000000000..8b27e00b826d7bd28933b55b129f6d5acb8777f9 --- /dev/null +++ b/couchpotato/core/providers/nzb/ftdworld/main.py @@ -0,0 +1,107 @@ +from bs4 import BeautifulSoup +from couchpotato.core.event import fireEvent +from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \ + simplifyString +from couchpotato.core.helpers.variable import tryInt, getTitle +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.nzb.base import NZBProvider +from couchpotato.environment import Env +from dateutil.parser import parse +import re +import time + +log = CPLog(__name__) + + +class FTDWorld(NZBProvider): + + urls = { + 'search': 'http://ftdworld.net/category.php?%s', + 'detail': 'http://ftdworld.net/spotinfo.php?id=%s', + 'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s', + 'login': 'http://ftdworld.net/index.php', + } + + http_time_between_calls = 1 #seconds + + cat_ids = [ + ([4, 11], ['dvdr']), + ([1], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']), + ([10, 13, 14], ['bd50', '720p', '1080p']), + ] + cat_backup_id = [1] + + def search(self, movie, quality): + + results = [] + if self.isDisabled(): + return results + + q = '%s %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year']) + + params = { + 'ctitle': q, + 'customQuery': 'usr', + 'cage': Env.setting('retention', 'nzb'), + 'csizemin': quality.get('size_min'), + 'csizemax': quality.get('size_max'), + 'ccategory': 14, + 'ctype': ','.join([str(x) for x in self.getCatId(quality['identifier'])]), + } + + cache_key = 'ftdworld.%s.%s' % (movie['library']['identifier'], q) + data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params), opener = self.login_opener) + + if data: + try: + + html = BeautifulSoup(data) + main_table = html.find('table', attrs = {'id':'ftdresult'}) + + if not main_table: + return results + + items = main_table.find_all('tr', attrs = {'class': re.compile('tcontent')}) + + for item in items: + tds = item.find_all('td') + nzb_id = tryInt(item.attrs['data-spot']) + + up = item.find('img', attrs = {'src': re.compile('up.png')}) + down = item.find('img', attrs = {'src': re.compile('down.png')}) + + new = { + 'id': nzb_id, + 'type': 'nzb', + 'provider': self.getName(), + 'name': toUnicode(item.find('a', attrs = {'href': re.compile('./spotinfo')}).text.strip()), + 'age': self.calculateAge(int(time.mktime(parse(tds[2].text).timetuple()))), + 'size': 0, + 'url': self.urls['download'] % nzb_id, + 'download': self.loginDownload, + 'detail_url': self.urls['detail'] % nzb_id, + 'description': '', + 'score': (tryInt(up.attrs['title'].split(' ')[0]) * 3) - (tryInt(down.attrs['title'].split(' ')[0]) * 3), + } + + is_correct_movie = fireEvent('searcher.correct_movie', + nzb = new, movie = movie, quality = quality, + imdb_results = False, single = True) + + if is_correct_movie: + new['score'] += fireEvent('score.calculate', new, movie, single = True) + results.append(new) + self.found(new) + + return results + except SyntaxError: + log.error('Failed to parse XML response from NZBClub') + + return results + + def getLoginParams(self): + return tryUrlencode({ + 'userlogin': self.conf('username'), + 'passlogin': self.conf('password'), + 'submit': 'Log In', + }) diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index b0e8b4843f32906c50d0d76b4b0307ba312b9bd9..c9507f91ae0b2597e52b472c6293312ce727e8c7 100644 --- a/couchpotato/core/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/providers/nzb/newznab/__init__.py @@ -11,7 +11,9 @@ config = [{ 'subtab': 'nzb_providers', 'name': 'newznab', 'order': 10, - 'description': 'Enable multiple NewzNab providers such as <a href="https://nzb.su" target="_blank">NZB.su</a> and <a href="https://nzbs.org" target="_blank">nzbs.org</a>', + 'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab providers</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \ + <a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \ + <a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py index 47d6c8539d135b8475645ce31e065a70bd19d3bf..9f85348977a431738ba498edf5e8815f312e9a23 100644 --- a/couchpotato/core/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/providers/nzb/nzbclub/main.py @@ -29,8 +29,6 @@ class NZBClub(NZBProvider, RSS): return results q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier')) - for ignored in Env.setting('ignored_words', 'searcher').split(','): - q = '%s -%s' % (q, ignored.strip()) params = { 'q': q, diff --git a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py b/couchpotato/core/providers/nzb/nzbmatrix/__init__.py deleted file mode 100644 index c3a5bfaa83ecb0731cd5a4c216a567b972586b45..0000000000000000000000000000000000000000 --- a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from .main import NZBMatrix - -def start(): - return NZBMatrix() - -config = [{ - 'name': 'nzbmatrix', - 'groups': [ - { - 'tab': 'searcher', - 'subtab': 'nzb_providers', - 'name': 'nzbmatrix', - 'label': 'NZBMatrix', - 'description': 'See <a href="https://nzbmatrix.com/">NZBMatrix</a>', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - }, - { - 'name': 'username', - }, - { - 'name': 'api_key', - 'default': '', - 'label': 'Api Key', - }, - { - 'name': 'english_only', - 'default': 1, - 'type': 'bool', - 'label': 'English only', - 'description': 'Only search for English spoken movies on NZBMatrix', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/providers/nzb/nzbmatrix/main.py b/couchpotato/core/providers/nzb/nzbmatrix/main.py deleted file mode 100644 index 4da2fbd986c74081eb4f024969da00161e036ab9..0000000000000000000000000000000000000000 --- a/couchpotato/core/providers/nzb/nzbmatrix/main.py +++ /dev/null @@ -1,105 +0,0 @@ -from couchpotato.core.event import fireEvent -from couchpotato.core.helpers.encoding import tryUrlencode -from couchpotato.core.helpers.rss import RSS -from couchpotato.core.logger import CPLog -from couchpotato.core.providers.nzb.base import NZBProvider -from couchpotato.environment import Env -from dateutil.parser import parse -import time -import xml.etree.ElementTree as XMLTree - -log = CPLog(__name__) - - -class NZBMatrix(NZBProvider, RSS): - - urls = { - 'download': 'https://api.nzbmatrix.com/v1.1/download.php?id=%s', - 'detail': 'https://nzbmatrix.com/nzb-details.php?id=%s&hit=1', - 'search': 'https://rss.nzbmatrix.com/rss.php', - } - - cat_ids = [ - ([50], ['bd50']), - ([42, 53], ['720p', '1080p']), - ([2, 9], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), - ([54], ['brrip']), - ([1], ['dvdr']), - ] - cat_backup_id = 2 - - def search(self, movie, quality): - - results = [] - - if self.isDisabled(): - return results - - cat_ids = ','.join(['%s' % x for x in self.getCatId(quality.get('identifier'))]) - - arguments = tryUrlencode({ - 'term': movie['library']['identifier'], - 'subcat': cat_ids, - 'username': self.conf('username'), - 'apikey': self.conf('api_key'), - 'searchin': 'weblink', - 'maxage': Env.setting('retention', section = 'nzb'), - 'english': self.conf('english_only'), - }) - url = "%s?%s" % (self.urls['search'], arguments) - - cache_key = 'nzbmatrix.%s.%s' % (movie['library'].get('identifier'), cat_ids) - - data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) - if data: - try: - try: - data = XMLTree.fromstring(data) - nzbs = self.getElements(data, 'channel/item') - except Exception, e: - log.debug('%s, %s', (self.getName(), e)) - return results - - for nzb in nzbs: - - title = self.getTextElement(nzb, "title") - if 'error' in title.lower(): continue - - id = int(self.getTextElement(nzb, "link").split('&')[0].partition('id=')[2]) - size = self.getTextElement(nzb, "description").split('<br /><b>')[2].split('> ')[1] - date = str(self.getTextElement(nzb, "description").split('<br /><b>')[3].partition('Added:</b> ')[2]) - - new = { - 'id': id, - 'type': 'nzb', - 'provider': self.getName(), - 'name': title, - 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), - 'size': self.parseSize(size), - 'url': self.urls['download'] % id + self.getApiExt(), - 'download': self.download, - 'detail_url': self.urls['detail'] % id, - 'description': self.getTextElement(nzb, "description"), - 'check_nzb': True, - } - - is_correct_movie = fireEvent('searcher.correct_movie', - nzb = new, movie = movie, quality = quality, - imdb_results = True, single = True) - - if is_correct_movie: - new['score'] = fireEvent('score.calculate', new, movie, single = True) - results.append(new) - self.found(new) - - return results - except SyntaxError: - log.error('Failed to parse XML response from NZBMatrix.com') - - return results - - def getApiExt(self): - return '&username=%s&apikey=%s' % (self.conf('username'), self.conf('api_key')) - - def isEnabled(self): - return NZBProvider.isEnabled(self) and self.conf('username') and self.conf('api_key') diff --git a/couchpotato/core/providers/torrent/base.py b/couchpotato/core/providers/torrent/base.py index fc2d35e22cf80e7a91c952508be43b850d5b6093..79d5b1e60b79aca1c6337c792e46f29ece9452a1 100644 --- a/couchpotato/core/providers/torrent/base.py +++ b/couchpotato/core/providers/torrent/base.py @@ -1,9 +1,6 @@ from couchpotato.core.helpers.variable import getImdb, md5 from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import YarrProvider -import cookielib -import traceback -import urllib2 log = CPLog(__name__) @@ -11,7 +8,6 @@ log = CPLog(__name__) class TorrentProvider(YarrProvider): type = 'torrent' - login_opener = None def imdbMatch(self, url, imdbId): if getImdb(url) == imdbId: @@ -28,30 +24,3 @@ class TorrentProvider(YarrProvider): return getImdb(data) == imdbId return False - - def login(self): - - try: - cookiejar = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar)) - urllib2.install_opener(opener) - f = opener.open(self.urls['login'], self.getLoginParams()) - f.read() - f.close() - self.login_opener = opener - return True - except: - log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc())) - - return False - - def loginDownload(self, url = '', nzb_id = ''): - try: - if not self.login_opener and not self.login(): - log.error('Failed downloading from %s', self.getName()) - return self.urlopen(url, opener = self.login_opener) - except: - log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc())) - - def getLoginParams(self): - return '' diff --git a/couchpotato/core/providers/torrent/passthepopcorn/__init__.py b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py index 507c104294672167a1c522f96d7c15cd609bbd27..3291c9cdfc18a7fc3188cdcb5da66d6641426f60 100644 --- a/couchpotato/core/providers/torrent/passthepopcorn/__init__.py +++ b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py @@ -31,6 +31,10 @@ config = [{ 'name': 'password', 'default': '', 'type': 'password', + }, + { + 'name': 'passkey', + 'default': '', } ], } diff --git a/couchpotato/core/providers/torrent/passthepopcorn/main.py b/couchpotato/core/providers/torrent/passthepopcorn/main.py index f9416f7441e7ce117e6e0e29b954df7a20087675..80af1ed639424393dfd589436ebf3e4249725151 100644 --- a/couchpotato/core/providers/torrent/passthepopcorn/main.py +++ b/couchpotato/core/providers/torrent/passthepopcorn/main.py @@ -21,7 +21,7 @@ class PassThePopcorn(TorrentProvider): 'domain': 'https://tls.passthepopcorn.me', 'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s', 'torrent': 'https://tls.passthepopcorn.me/torrents.php', - 'login': 'https://tls.passthepopcorn.me/login.php', + 'login': 'https://tls.passthepopcorn.me/ajax.php?action=login', 'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d' } @@ -249,6 +249,7 @@ class PassThePopcorn(TorrentProvider): return tryUrlencode({ 'username': self.conf('username'), 'password': self.conf('password'), + 'passkey': self.conf('passkey'), 'keeplogged': '1', 'login': 'Login' }) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 973ac88b193973f58874a8591b6c71ed9f1d8d9f..9da584bb9f610554a600cf100935462e4e0ed3cc 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -4,10 +4,8 @@ from couchpotato.api import api, NonBlockHandler from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.helpers.variable import getDataDir, tryInt from logging import handlers -from tornado import autoreload -from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop -from tornado.web import RequestHandler, Application, FallbackHandler +from tornado.web import Application, FallbackHandler from tornado.wsgi import WSGIContainer from werkzeug.contrib.cache import FileSystemCache import locale @@ -57,10 +55,8 @@ def _log(status_code, request): if status_code < 400: return - elif status_code < 500: - log_method = logging.warning else: - log_method = logging.error + log_method = logging.debug request_time = 1000.0 * request.request_time() summary = request.method + " " + request.uri + " (" + \ request.remote_ip + ")" diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index f4afc84d65d3c8dfc9597ab141a2cce7a5238e12..44ae0f6b84cc059fd2bc1de32ae5dd49336d2b31 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -436,9 +436,14 @@ Option.String = new Class({ self.input = new Element('input.inlay', { 'type': 'text', 'name': self.postName(), - 'value': self.getSettingValue() + 'value': self.getSettingValue(), + 'placeholder': self.getPlaceholder() }) ); + }, + + getPlaceholder: function(){ + return this.options.placeholder } }); diff --git a/libs/tornado/__init__.py b/libs/tornado/__init__.py index 61551208933daa9dbc02dfb9c837893e3f2c5eba..2d1bba88399932bc7ebdc141427f4578a649b072 100755 --- a/libs/tornado/__init__.py +++ b/libs/tornado/__init__.py @@ -25,5 +25,5 @@ from __future__ import absolute_import, division, with_statement # is zero for an official release, positive for a development branch, # or negative for a release candidate (after the base version number # has been incremented) -version = "2.3.post1" -version_info = (2, 3, 0, 1) +version = "2.4.post2" +version_info = (2, 4, 0, 2) diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py index a61e359ac13d6a6dc4cd55f0b188dd92c464079e..964534fa8b535f745444719e421af5e86f132c88 100755 --- a/libs/tornado/auth.py +++ b/libs/tornado/auth.py @@ -50,7 +50,6 @@ import base64 import binascii import hashlib import hmac -import logging import time import urllib import urlparse @@ -59,6 +58,7 @@ import uuid from tornado import httpclient from tornado import escape from tornado.httputil import url_concat +from tornado.log import gen_log from tornado.util import bytes_type, b @@ -95,7 +95,7 @@ class OpenIdMixin(object): args["openid.mode"] = u"check_authentication" url = self._OPENID_ENDPOINT if http_client is None: - http_client = httpclient.AsyncHTTPClient() + http_client = self.get_auth_http_client() http_client.fetch(url, self.async_callback( self._on_authentication_verified, callback), method="POST", body=urllib.urlencode(args)) @@ -150,7 +150,7 @@ class OpenIdMixin(object): def _on_authentication_verified(self, callback, response): if response.error or b("is_valid:true") not in response.body: - logging.warning("Invalid OpenID response: %s", response.error or + gen_log.warning("Invalid OpenID response: %s", response.error or response.body) callback(None) return @@ -203,8 +203,19 @@ class OpenIdMixin(object): user["locale"] = locale if username: user["username"] = username + claimed_id = self.get_argument("openid.claimed_id", None) + if claimed_id: + user["claimed_id"] = claimed_id callback(user) + def get_auth_http_client(self): + """Returns the AsyncHTTPClient instance to be used for auth requests. + + May be overridden by subclasses to use an http client other than + the default. + """ + return httpclient.AsyncHTTPClient() + class OAuthMixin(object): """Abstract implementation of OAuth. @@ -229,7 +240,7 @@ class OAuthMixin(object): if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False): raise Exception("This service does not support oauth_callback") if http_client is None: - http_client = httpclient.AsyncHTTPClient() + http_client = self.get_auth_http_client() if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": http_client.fetch( self._oauth_request_token_url(callback_uri=callback_uri, @@ -260,21 +271,21 @@ class OAuthMixin(object): oauth_verifier = self.get_argument("oauth_verifier", None) request_cookie = self.get_cookie("_oauth_request_token") if not request_cookie: - logging.warning("Missing OAuth request token cookie") + gen_log.warning("Missing OAuth request token cookie") callback(None) return self.clear_cookie("_oauth_request_token") cookie_key, cookie_secret = [base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")] if cookie_key != request_key: - logging.info((cookie_key, request_key, request_cookie)) - logging.warning("Request token does not match cookie") + gen_log.info((cookie_key, request_key, request_cookie)) + gen_log.warning("Request token does not match cookie") callback(None) return token = dict(key=cookie_key, secret=cookie_secret) if oauth_verifier: token["verifier"] = oauth_verifier if http_client is None: - http_client = httpclient.AsyncHTTPClient() + http_client = self.get_auth_http_client() http_client.fetch(self._oauth_access_token_url(token), self.async_callback(self._on_access_token, callback)) @@ -282,14 +293,16 @@ class OAuthMixin(object): consumer_token = self._oauth_consumer_token() url = self._OAUTH_REQUEST_TOKEN_URL args = dict( - oauth_consumer_key=consumer_token["key"], + oauth_consumer_key=escape.to_basestring(consumer_token["key"]), oauth_signature_method="HMAC-SHA1", oauth_timestamp=str(int(time.time())), - oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes), + oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)), oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"), ) if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": - if callback_uri: + if callback_uri == "oob": + args["oauth_callback"] = "oob" + elif callback_uri: args["oauth_callback"] = urlparse.urljoin( self.request.full_url(), callback_uri) if extra_params: @@ -309,7 +322,10 @@ class OAuthMixin(object): base64.b64encode(request_token["secret"])) self.set_cookie("_oauth_request_token", data) args = dict(oauth_token=request_token["key"]) - if callback_uri: + if callback_uri == "oob": + self.finish(authorize_url + "?" + urllib.urlencode(args)) + return + elif callback_uri: args["oauth_callback"] = urlparse.urljoin( self.request.full_url(), callback_uri) self.redirect(authorize_url + "?" + urllib.urlencode(args)) @@ -318,11 +334,11 @@ class OAuthMixin(object): consumer_token = self._oauth_consumer_token() url = self._OAUTH_ACCESS_TOKEN_URL args = dict( - oauth_consumer_key=consumer_token["key"], - oauth_token=request_token["key"], + oauth_consumer_key=escape.to_basestring(consumer_token["key"]), + oauth_token=escape.to_basestring(request_token["key"]), oauth_signature_method="HMAC-SHA1", oauth_timestamp=str(int(time.time())), - oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes), + oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)), oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"), ) if "verifier" in request_token: @@ -340,7 +356,7 @@ class OAuthMixin(object): def _on_access_token(self, callback, response): if response.error: - logging.warning("Could not fetch access token") + gen_log.warning("Could not fetch access token") callback(None) return @@ -367,11 +383,11 @@ class OAuthMixin(object): """ consumer_token = self._oauth_consumer_token() base_args = dict( - oauth_consumer_key=consumer_token["key"], - oauth_token=access_token["key"], + oauth_consumer_key=escape.to_basestring(consumer_token["key"]), + oauth_token=escape.to_basestring(access_token["key"]), oauth_signature_method="HMAC-SHA1", oauth_timestamp=str(int(time.time())), - oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes), + oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)), oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"), ) args = {} @@ -386,6 +402,14 @@ class OAuthMixin(object): base_args["oauth_signature"] = signature return base_args + def get_auth_http_client(self): + """Returns the AsyncHTTPClient instance to be used for auth requests. + + May be overridden by subclasses to use an http client other than + the default. + """ + return httpclient.AsyncHTTPClient() + class OAuth2Mixin(object): """Abstract implementation of OAuth v 2.""" @@ -463,6 +487,7 @@ class TwitterMixin(OAuthMixin): _OAUTH_AUTHORIZE_URL = "http://api.twitter.com/oauth/authorize" _OAUTH_AUTHENTICATE_URL = "http://api.twitter.com/oauth/authenticate" _OAUTH_NO_CALLBACKS = False + _TWITTER_BASE_URL = "http://api.twitter.com/1" def authenticate_redirect(self, callback_uri=None): """Just like authorize_redirect(), but auto-redirects if authorized. @@ -470,7 +495,7 @@ class TwitterMixin(OAuthMixin): This is generally the right interface to use if you are using Twitter for single-sign on. """ - http = httpclient.AsyncHTTPClient() + http = self.get_auth_http_client() http.fetch(self._oauth_request_token_url(callback_uri=callback_uri), self.async_callback( self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None)) @@ -517,7 +542,7 @@ class TwitterMixin(OAuthMixin): # usual pattern: http://search.twitter.com/search.json url = path else: - url = "http://api.twitter.com/1" + path + ".json" + url = self._TWITTER_BASE_URL + path + ".json" # Add the OAuth resource request signature if we have credentials if access_token: all_args = {} @@ -530,7 +555,7 @@ class TwitterMixin(OAuthMixin): if args: url += "?" + urllib.urlencode(args) callback = self.async_callback(self._on_twitter_request, callback) - http = httpclient.AsyncHTTPClient() + http = self.get_auth_http_client() if post_args is not None: http.fetch(url, method="POST", body=urllib.urlencode(post_args), callback=callback) @@ -539,7 +564,7 @@ class TwitterMixin(OAuthMixin): def _on_twitter_request(self, callback, response): if response.error: - logging.warning("Error response %s fetching %s", response.error, + gen_log.warning("Error response %s fetching %s", response.error, response.request.url) callback(None) return @@ -555,7 +580,7 @@ class TwitterMixin(OAuthMixin): def _oauth_get_user(self, access_token, callback): callback = self.async_callback(self._parse_user_response, callback) self.twitter_request( - "/users/show/" + access_token["screen_name"], + "/users/show/" + escape.native_str(access_token[b("screen_name")]), access_token=access_token, callback=callback) def _parse_user_response(self, callback, user): @@ -652,7 +677,7 @@ class FriendFeedMixin(OAuthMixin): if args: url += "?" + urllib.urlencode(args) callback = self.async_callback(self._on_friendfeed_request, callback) - http = httpclient.AsyncHTTPClient() + http = self.get_auth_http_client() if post_args is not None: http.fetch(url, method="POST", body=urllib.urlencode(post_args), callback=callback) @@ -661,7 +686,7 @@ class FriendFeedMixin(OAuthMixin): def _on_friendfeed_request(self, callback, response): if response.error: - logging.warning("Error response %s fetching %s", response.error, + gen_log.warning("Error response %s fetching %s", response.error, response.request.url) callback(None) return @@ -743,7 +768,7 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): break token = self.get_argument("openid." + oauth_ns + ".request_token", "") if token: - http = httpclient.AsyncHTTPClient() + http = self.get_auth_http_client() token = dict(key=token, secret="") http.fetch(self._oauth_access_token_url(token), self.async_callback(self._on_access_token, callback)) @@ -854,7 +879,7 @@ class FacebookMixin(object): self._on_get_user_info, callback, session), session_key=session["session_key"], uids=session["uid"], - fields="uid,first_name,last_name,name,locale,pic_square," \ + fields="uid,first_name,last_name,name,locale,pic_square," "profile_url,username") def facebook_request(self, method, callback, **args): @@ -899,7 +924,7 @@ class FacebookMixin(object): args["sig"] = self._signature(args) url = "http://api.facebook.com/restserver.php?" + \ urllib.urlencode(args) - http = httpclient.AsyncHTTPClient() + http = self.get_auth_http_client() http.fetch(url, callback=self.async_callback( self._parse_response, callback)) @@ -922,17 +947,17 @@ class FacebookMixin(object): def _parse_response(self, callback, response): if response.error: - logging.warning("HTTP error from Facebook: %s", response.error) + gen_log.warning("HTTP error from Facebook: %s", response.error) callback(None) return try: json = escape.json_decode(response.body) except Exception: - logging.warning("Invalid JSON from Facebook: %r", response.body) + gen_log.warning("Invalid JSON from Facebook: %r", response.body) callback(None) return if isinstance(json, dict) and json.get("error_code"): - logging.warning("Facebook error: %d: %r", json["error_code"], + gen_log.warning("Facebook error: %d: %r", json["error_code"], json.get("error_msg")) callback(None) return @@ -945,6 +970,14 @@ class FacebookMixin(object): body = body.encode("utf-8") return hashlib.md5(body).hexdigest() + def get_auth_http_client(self): + """Returns the AsyncHTTPClient instance to be used for auth requests. + + May be overridden by subclasses to use an http client other than + the default. + """ + return httpclient.AsyncHTTPClient() + class FacebookGraphMixin(OAuth2Mixin): """Facebook authentication using the new Graph API and OAuth2.""" @@ -979,7 +1012,7 @@ class FacebookGraphMixin(OAuth2Mixin): self.finish() """ - http = httpclient.AsyncHTTPClient() + http = self.get_auth_http_client() args = { "redirect_uri": redirect_uri, "code": code, @@ -999,7 +1032,7 @@ class FacebookGraphMixin(OAuth2Mixin): def _on_access_token(self, redirect_uri, client_id, client_secret, callback, fields, response): if response.error: - logging.warning('Facebook auth error: %s' % str(response)) + gen_log.warning('Facebook auth error: %s' % str(response)) callback(None) return @@ -1073,7 +1106,7 @@ class FacebookGraphMixin(OAuth2Mixin): if all_args: url += "?" + urllib.urlencode(all_args) callback = self.async_callback(self._on_facebook_request, callback) - http = httpclient.AsyncHTTPClient() + http = self.get_auth_http_client() if post_args is not None: http.fetch(url, method="POST", body=urllib.urlencode(post_args), callback=callback) @@ -1082,12 +1115,20 @@ class FacebookGraphMixin(OAuth2Mixin): def _on_facebook_request(self, callback, response): if response.error: - logging.warning("Error response %s fetching %s", response.error, + gen_log.warning("Error response %s fetching %s", response.error, response.request.url) callback(None) return callback(escape.json_decode(response.body)) + def get_auth_http_client(self): + """Returns the AsyncHTTPClient instance to be used for auth requests. + + May be overridden by subclasses to use an http client other than + the default. + """ + return httpclient.AsyncHTTPClient() + def _oauth_signature(consumer_token, method, url, parameters={}, token=None): """Calculates the HMAC-SHA1 OAuth signature for the given request. diff --git a/libs/tornado/autoreload.py b/libs/tornado/autoreload.py index 55a10d10cc5aa2ff2d6f2386fd3aa6477175f389..62af0f3b05f32fae0be05318d11f43db47279e05 100755 --- a/libs/tornado/autoreload.py +++ b/libs/tornado/autoreload.py @@ -71,10 +71,13 @@ import logging import os import pkgutil import sys +import traceback import types import subprocess +import weakref from tornado import ioloop +from tornado.log import gen_log from tornado import process try: @@ -83,6 +86,11 @@ except ImportError: signal = None +_watched_files = set() +_reload_hooks = [] +_reload_attempted = False +_io_loops = weakref.WeakKeyDictionary() + def start(io_loop=None, check_time=500): """Restarts the process automatically when a module is modified. @@ -90,6 +98,11 @@ def start(io_loop=None, check_time=500): so will terminate any pending requests. """ io_loop = io_loop or ioloop.IOLoop.instance() + if io_loop in _io_loops: + return + _io_loops[io_loop] = True + if len(_io_loops) > 1: + gen_log.warning("tornado.autoreload started more than once in the same process") add_reload_hook(functools.partial(_close_all_fds, io_loop)) modify_times = {} callback = functools.partial(_reload_on_update, modify_times) @@ -108,8 +121,6 @@ def wait(): start(io_loop) io_loop.start() -_watched_files = set() - def watch(filename): """Add a file to the watch list. @@ -118,8 +129,6 @@ def watch(filename): """ _watched_files.add(filename) -_reload_hooks = [] - def add_reload_hook(fn): """Add a function to be called before reloading the process. @@ -139,8 +148,6 @@ def _close_all_fds(io_loop): except Exception: pass -_reload_attempted = False - def _reload_on_update(modify_times): if _reload_attempted: @@ -177,7 +184,7 @@ def _check_file(modify_times, path): modify_times[path] = modified return if modify_times[path] != modified: - logging.info("%s modified; restarting server", path) + gen_log.info("%s modified; restarting server", path) _reload() @@ -272,13 +279,25 @@ def main(): # module) will see the right things. exec f.read() in globals(), globals() except SystemExit, e: - logging.info("Script exited with status %s", e.code) + logging.basicConfig() + gen_log.info("Script exited with status %s", e.code) except Exception, e: - logging.warning("Script exited with uncaught exception", exc_info=True) + logging.basicConfig() + gen_log.warning("Script exited with uncaught exception", exc_info=True) + # If an exception occurred at import time, the file with the error + # never made it into sys.modules and so we won't know to watch it. + # Just to make sure we've covered everything, walk the stack trace + # from the exception and watch every file. + for (filename, lineno, name, line) in traceback.extract_tb(sys.exc_info()[2]): + watch(filename) if isinstance(e, SyntaxError): + # SyntaxErrors are special: their innermost stack frame is fake + # so extract_tb won't see it and we have to get the filename + # from the exception object. watch(e.filename) else: - logging.info("Script exited normally") + logging.basicConfig() + gen_log.info("Script exited normally") # restore sys.argv so subsequent executions will include autoreload sys.argv = original_argv diff --git a/libs/tornado/concurrent.py b/libs/tornado/concurrent.py new file mode 100755 index 0000000000000000000000000000000000000000..80596844db3161ce00f28706bc6d7c2c2b22b330 --- /dev/null +++ b/libs/tornado/concurrent.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# +# Copyright 2012 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import absolute_import, division, with_statement + +import functools +import sys + +from tornado.stack_context import ExceptionStackContext +from tornado.util import raise_exc_info + +try: + from concurrent import futures +except ImportError: + futures = None + + +class DummyFuture(object): + def __init__(self): + self._done = False + self._result = None + self._exception = None + self._callbacks = [] + + def cancel(self): + return False + + def cancelled(self): + return False + + def running(self): + return not self._done + + def done(self): + return self._done + + def result(self, timeout=None): + self._check_done() + if self._exception: + raise self._exception + return self._result + + def exception(self, timeout=None): + self._check_done() + if self._exception: + return self._exception + else: + return None + + def add_done_callback(self, fn): + if self._done: + fn(self) + else: + self._callbacks.append(fn) + + def set_result(self, result): + self._result = result + self._set_done() + + def set_exception(self, exception): + self._exception = exception + self._set_done() + + def _check_done(self): + if not self._done: + raise Exception("DummyFuture does not support blocking for results") + + def _set_done(self): + self._done = True + for cb in self._callbacks: + # TODO: error handling + cb(self) + self._callbacks = None + +if futures is None: + Future = DummyFuture +else: + Future = futures.Future + +class DummyExecutor(object): + def submit(self, fn, *args, **kwargs): + future = Future() + try: + future.set_result(fn(*args, **kwargs)) + except Exception, e: + future.set_exception(e) + return future + +dummy_executor = DummyExecutor() + +def run_on_executor(fn): + @functools.wraps(fn) + def wrapper(self, *args, **kwargs): + callback = kwargs.pop("callback") + future = self.executor.submit(fn, self, *args, **kwargs) + if callback: + self.io_loop.add_future(future, callback) + return future + return wrapper + +# TODO: this needs a better name +def future_wrap(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + future = Future() + if kwargs.get('callback') is not None: + future.add_done_callback(kwargs.pop('callback')) + kwargs['callback'] = future.set_result + def handle_error(typ, value, tb): + future.set_exception(value) + return True + with ExceptionStackContext(handle_error): + f(*args, **kwargs) + return future + return wrapper diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py index 95958c19c16fa7f973bcca78bdfe10d81a8d3f80..a6c0bb0de4fe49376f7fa8c40f31eaeb66078a82 100755 --- a/libs/tornado/curl_httpclient.py +++ b/libs/tornado/curl_httpclient.py @@ -27,21 +27,23 @@ import time from tornado import httputil from tornado import ioloop +from tornado.log import gen_log from tornado import stack_context from tornado.escape import utf8 -from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main +from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy class CurlAsyncHTTPClient(AsyncHTTPClient): - def initialize(self, io_loop=None, max_clients=10, - max_simultaneous_connections=None): + def initialize(self, io_loop=None, max_clients=10, defaults=None): self.io_loop = io_loop + self.defaults = dict(HTTPRequest._DEFAULTS) + if defaults is not None: + self.defaults.update(defaults) self._multi = pycurl.CurlMulti() self._multi.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout) self._multi.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket) - self._curls = [_curl_create(max_simultaneous_connections) - for i in xrange(max_clients)] + self._curls = [_curl_create() for i in xrange(max_clients)] self._free_list = self._curls[:] self._requests = collections.deque() self._fds = {} @@ -53,7 +55,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient): # socket_action is found in pycurl since 7.18.2 (it's been # in libcurl longer than that but wasn't accessible to # python). - logging.warning("socket_action method missing from pycurl; " + gen_log.warning("socket_action method missing from pycurl; " "falling back to socket_all. Upgrading " "libcurl and pycurl will improve performance") self._socket_action = \ @@ -78,6 +80,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient): def fetch(self, request, callback, **kwargs): if not isinstance(request, HTTPRequest): request = HTTPRequest(url=request, **kwargs) + request = _RequestProxy(request, self.defaults) self._requests.append((request, stack_context.wrap(callback))) self._process_queue() self._set_timeout(0) @@ -110,7 +113,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient): if self._timeout is not None: self.io_loop.remove_timeout(self._timeout) self._timeout = self.io_loop.add_timeout( - time.time() + msecs / 1000.0, self._handle_timeout) + self.io_loop.time() + msecs / 1000.0, self._handle_timeout) def _handle_events(self, fd, events): """Called by IOLoop when there is activity on one of our @@ -263,12 +266,11 @@ class CurlError(HTTPError): self.errno = errno -def _curl_create(max_simultaneous_connections=None): +def _curl_create(): curl = pycurl.Curl() - if logging.getLogger().isEnabledFor(logging.DEBUG): + if gen_log.isEnabledFor(logging.DEBUG): curl.setopt(pycurl.VERBOSE, 1) curl.setopt(pycurl.DEBUGFUNCTION, _curl_debug) - curl.setopt(pycurl.MAXCONNECTS, max_simultaneous_connections or 5) return curl @@ -389,11 +391,11 @@ def _curl_setup_request(curl, request, buffer, headers): userpwd = "%s:%s" % (request.auth_username, request.auth_password or '') curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) curl.setopt(pycurl.USERPWD, utf8(userpwd)) - logging.debug("%s %s (username: %r)", request.method, request.url, + gen_log.debug("%s %s (username: %r)", request.method, request.url, request.auth_username) else: curl.unsetopt(pycurl.USERPWD) - logging.debug("%s %s", request.method, request.url) + gen_log.debug("%s %s", request.method, request.url) if request.client_cert is not None: curl.setopt(pycurl.SSLCERT, request.client_cert) @@ -429,12 +431,12 @@ def _curl_header_callback(headers, header_line): def _curl_debug(debug_type, debug_msg): debug_types = ('I', '<', '>', '<', '>') if debug_type == 0: - logging.debug('%s', debug_msg.strip()) + gen_log.debug('%s', debug_msg.strip()) elif debug_type in (1, 2): for line in debug_msg.splitlines(): - logging.debug('%s %s', debug_types[debug_type], line) + gen_log.debug('%s %s', debug_types[debug_type], line) elif debug_type == 4: - logging.debug('%s %r', debug_types[debug_type], debug_msg) + gen_log.debug('%s %r', debug_types[debug_type], debug_msg) if __name__ == "__main__": AsyncHTTPClient.configure(CurlAsyncHTTPClient) diff --git a/libs/tornado/database.py b/libs/tornado/database.py deleted file mode 100755 index 982c5db5045ddd612fa23ebe97594ce520869a28..0000000000000000000000000000000000000000 --- a/libs/tornado/database.py +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2009 Facebook -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""A lightweight wrapper around MySQLdb.""" - -from __future__ import absolute_import, division, with_statement - -import copy -import itertools -import logging -import time - -try: - import MySQLdb.constants - import MySQLdb.converters - import MySQLdb.cursors -except ImportError: - # If MySQLdb isn't available this module won't actually be useable, - # but we want it to at least be importable (mainly for readthedocs.org, - # which has limitations on third-party modules) - MySQLdb = None - - -class Connection(object): - """A lightweight wrapper around MySQLdb DB-API connections. - - The main value we provide is wrapping rows in a dict/object so that - columns can be accessed by name. Typical usage:: - - db = database.Connection("localhost", "mydatabase") - for article in db.query("SELECT * FROM articles"): - print article.title - - Cursors are hidden by the implementation, but other than that, the methods - are very similar to the DB-API. - - We explicitly set the timezone to UTC and the character encoding to - UTF-8 on all connections to avoid time zone and encoding errors. - """ - def __init__(self, host, database, user=None, password=None, - max_idle_time=7 * 3600): - self.host = host - self.database = database - self.max_idle_time = max_idle_time - - args = dict(conv=CONVERSIONS, use_unicode=True, charset="utf8", - db=database, init_command='SET time_zone = "+0:00"', - sql_mode="TRADITIONAL") - if user is not None: - args["user"] = user - if password is not None: - args["passwd"] = password - - # We accept a path to a MySQL socket file or a host(:port) string - if "/" in host: - args["unix_socket"] = host - else: - self.socket = None - pair = host.split(":") - if len(pair) == 2: - args["host"] = pair[0] - args["port"] = int(pair[1]) - else: - args["host"] = host - args["port"] = 3306 - - self._db = None - self._db_args = args - self._last_use_time = time.time() - try: - self.reconnect() - except Exception: - logging.error("Cannot connect to MySQL on %s", self.host, - exc_info=True) - - def __del__(self): - self.close() - - def close(self): - """Closes this database connection.""" - if getattr(self, "_db", None) is not None: - self._db.close() - self._db = None - - def reconnect(self): - """Closes the existing database connection and re-opens it.""" - self.close() - self._db = MySQLdb.connect(**self._db_args) - self._db.autocommit(True) - - def iter(self, query, *parameters): - """Returns an iterator for the given query and parameters.""" - self._ensure_connected() - cursor = MySQLdb.cursors.SSCursor(self._db) - try: - self._execute(cursor, query, parameters) - column_names = [d[0] for d in cursor.description] - for row in cursor: - yield Row(zip(column_names, row)) - finally: - cursor.close() - - def query(self, query, *parameters): - """Returns a row list for the given query and parameters.""" - cursor = self._cursor() - try: - self._execute(cursor, query, parameters) - column_names = [d[0] for d in cursor.description] - return [Row(itertools.izip(column_names, row)) for row in cursor] - finally: - cursor.close() - - def get(self, query, *parameters): - """Returns the first row returned for the given query.""" - rows = self.query(query, *parameters) - if not rows: - return None - elif len(rows) > 1: - raise Exception("Multiple rows returned for Database.get() query") - else: - return rows[0] - - # rowcount is a more reasonable default return value than lastrowid, - # but for historical compatibility execute() must return lastrowid. - def execute(self, query, *parameters): - """Executes the given query, returning the lastrowid from the query.""" - return self.execute_lastrowid(query, *parameters) - - def execute_lastrowid(self, query, *parameters): - """Executes the given query, returning the lastrowid from the query.""" - cursor = self._cursor() - try: - self._execute(cursor, query, parameters) - return cursor.lastrowid - finally: - cursor.close() - - def execute_rowcount(self, query, *parameters): - """Executes the given query, returning the rowcount from the query.""" - cursor = self._cursor() - try: - self._execute(cursor, query, parameters) - return cursor.rowcount - finally: - cursor.close() - - def executemany(self, query, parameters): - """Executes the given query against all the given param sequences. - - We return the lastrowid from the query. - """ - return self.executemany_lastrowid(query, parameters) - - def executemany_lastrowid(self, query, parameters): - """Executes the given query against all the given param sequences. - - We return the lastrowid from the query. - """ - cursor = self._cursor() - try: - cursor.executemany(query, parameters) - return cursor.lastrowid - finally: - cursor.close() - - def executemany_rowcount(self, query, parameters): - """Executes the given query against all the given param sequences. - - We return the rowcount from the query. - """ - cursor = self._cursor() - try: - cursor.executemany(query, parameters) - return cursor.rowcount - finally: - cursor.close() - - def _ensure_connected(self): - # Mysql by default closes client connections that are idle for - # 8 hours, but the client library does not report this fact until - # you try to perform a query and it fails. Protect against this - # case by preemptively closing and reopening the connection - # if it has been idle for too long (7 hours by default). - if (self._db is None or - (time.time() - self._last_use_time > self.max_idle_time)): - self.reconnect() - self._last_use_time = time.time() - - def _cursor(self): - self._ensure_connected() - return self._db.cursor() - - def _execute(self, cursor, query, parameters): - try: - return cursor.execute(query, parameters) - except OperationalError: - logging.error("Error connecting to MySQL on %s", self.host) - self.close() - raise - - -class Row(dict): - """A dict that allows for object-like property access syntax.""" - def __getattr__(self, name): - try: - return self[name] - except KeyError: - raise AttributeError(name) - -if MySQLdb is not None: - # Fix the access conversions to properly recognize unicode/binary - FIELD_TYPE = MySQLdb.constants.FIELD_TYPE - FLAG = MySQLdb.constants.FLAG - CONVERSIONS = copy.copy(MySQLdb.converters.conversions) - - field_types = [FIELD_TYPE.BLOB, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING] - if 'VARCHAR' in vars(FIELD_TYPE): - field_types.append(FIELD_TYPE.VARCHAR) - - for field_type in field_types: - CONVERSIONS[field_type] = [(FLAG.BINARY, str)] + CONVERSIONS[field_type] - - # Alias some common MySQL exceptions - IntegrityError = MySQLdb.IntegrityError - OperationalError = MySQLdb.OperationalError diff --git a/libs/tornado/gen.py b/libs/tornado/gen.py index 506697d7b9a426c81bda2cb6456fef40947760ca..88ded2effbede315f03d886c55330af14a5e734d 100755 --- a/libs/tornado/gen.py +++ b/libs/tornado/gen.py @@ -69,6 +69,8 @@ import operator import sys import types +from tornado.concurrent import Future +from tornado.ioloop import IOLoop from tornado.stack_context import ExceptionStackContext @@ -247,6 +249,24 @@ class Task(YieldPoint): return self.runner.pop_result(self.key) +class YieldFuture(YieldPoint): + def __init__(self, future, io_loop=None): + self.future = future + self.io_loop = io_loop or IOLoop.current() + + def start(self, runner): + self.runner = runner + self.key = object() + runner.register_callback(self.key) + self.io_loop.add_future(self.future, runner.result_callback(self.key)) + + def is_ready(self): + return self.runner.is_ready(self.key) + + def get_result(self): + return self.runner.pop_result(self.key).result() + + class Multi(YieldPoint): """Runs multiple asynchronous operations in parallel. @@ -354,12 +374,16 @@ class Runner(object): "finished without waiting for callbacks %r" % self.pending_callbacks) self.deactivate_stack_context() + self.deactivate_stack_context = None return except Exception: self.finished = True raise if isinstance(yielded, list): yielded = Multi(yielded) + if isinstance(yielded, Future): + # TODO: lists of futures + yielded = YieldFuture(yielded) if isinstance(yielded, YieldPoint): self.yield_point = yielded try: diff --git a/libs/tornado/httpclient.py b/libs/tornado/httpclient.py index 0fcc943f9d0712f7c9bd9d4585f867aabb1ec6c8..7359a76cefb05591ace17beadf3abe0a7ba207bf 100755 --- a/libs/tornado/httpclient.py +++ b/libs/tornado/httpclient.py @@ -38,9 +38,9 @@ import time import weakref from tornado.escape import utf8 -from tornado import httputil +from tornado import httputil, stack_context from tornado.ioloop import IOLoop -from tornado.util import import_object, bytes_type +from tornado.util import Configurable class HTTPClient(object): @@ -95,7 +95,7 @@ class HTTPClient(object): return response -class AsyncHTTPClient(object): +class AsyncHTTPClient(Configurable): """An non-blocking HTTP client. Example usage:: @@ -121,46 +121,31 @@ class AsyncHTTPClient(object): are deprecated. The implementation subclass as well as arguments to its constructor can be set with the static method configure() """ - _impl_class = None - _impl_kwargs = None + @classmethod + def configurable_base(cls): + return AsyncHTTPClient - _DEFAULT_MAX_CLIENTS = 10 + @classmethod + def configurable_default(cls): + from tornado.simple_httpclient import SimpleAsyncHTTPClient + return SimpleAsyncHTTPClient @classmethod def _async_clients(cls): - assert cls is not AsyncHTTPClient, "should only be called on subclasses" - if not hasattr(cls, '_async_client_dict'): - cls._async_client_dict = weakref.WeakKeyDictionary() - return cls._async_client_dict + attr_name = '_async_client_dict_' + cls.__name__ + if not hasattr(cls, attr_name): + setattr(cls, attr_name, weakref.WeakKeyDictionary()) + return getattr(cls, attr_name) - def __new__(cls, io_loop=None, max_clients=None, force_instance=False, - **kwargs): + def __new__(cls, io_loop=None, force_instance=False, **kwargs): io_loop = io_loop or IOLoop.instance() - if cls is AsyncHTTPClient: - if cls._impl_class is None: - from tornado.simple_httpclient import SimpleAsyncHTTPClient - AsyncHTTPClient._impl_class = SimpleAsyncHTTPClient - impl = AsyncHTTPClient._impl_class - else: - impl = cls - if io_loop in impl._async_clients() and not force_instance: - return impl._async_clients()[io_loop] - else: - instance = super(AsyncHTTPClient, cls).__new__(impl) - args = {} - if cls._impl_kwargs: - args.update(cls._impl_kwargs) - args.update(kwargs) - if max_clients is not None: - # max_clients is special because it may be passed - # positionally instead of by keyword - args["max_clients"] = max_clients - elif "max_clients" not in args: - args["max_clients"] = AsyncHTTPClient._DEFAULT_MAX_CLIENTS - instance.initialize(io_loop, **args) - if not force_instance: - impl._async_clients()[io_loop] = instance - return instance + if io_loop in cls._async_clients() and not force_instance: + return cls._async_clients()[io_loop] + instance = super(AsyncHTTPClient, cls).__new__(cls, io_loop=io_loop, + **kwargs) + if not force_instance: + cls._async_clients()[io_loop] = instance + return instance def close(self): """Destroys this http client, freeing any file descriptors used. @@ -185,8 +170,8 @@ class AsyncHTTPClient(object): """ raise NotImplementedError() - @staticmethod - def configure(impl, **kwargs): + @classmethod + def configure(cls, impl, **kwargs): """Configures the AsyncHTTPClient subclass to use. AsyncHTTPClient() actually creates an instance of a subclass. @@ -205,38 +190,38 @@ class AsyncHTTPClient(object): AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient") """ - if isinstance(impl, (unicode, bytes_type)): - impl = import_object(impl) - if impl is not None and not issubclass(impl, AsyncHTTPClient): - raise ValueError("Invalid AsyncHTTPClient implementation") - AsyncHTTPClient._impl_class = impl - AsyncHTTPClient._impl_kwargs = kwargs - - @staticmethod - def _save_configuration(): - return (AsyncHTTPClient._impl_class, AsyncHTTPClient._impl_kwargs) - - @staticmethod - def _restore_configuration(saved): - AsyncHTTPClient._impl_class = saved[0] - AsyncHTTPClient._impl_kwargs = saved[1] + super(AsyncHTTPClient, cls).configure(impl, **kwargs) class HTTPRequest(object): """HTTP client request object.""" + + # Default values for HTTPRequest parameters. + # Merged with the values on the request object by AsyncHTTPClient + # implementations. + _DEFAULTS = dict( + connect_timeout=20.0, + request_timeout=20.0, + follow_redirects=True, + max_redirects=5, + use_gzip=True, + proxy_password='', + allow_nonstandard_methods=False, + validate_cert=True) + def __init__(self, url, method="GET", headers=None, body=None, auth_username=None, auth_password=None, - connect_timeout=20.0, request_timeout=20.0, - if_modified_since=None, follow_redirects=True, - max_redirects=5, user_agent=None, use_gzip=True, + connect_timeout=None, request_timeout=None, + if_modified_since=None, follow_redirects=None, + max_redirects=None, user_agent=None, use_gzip=None, network_interface=None, streaming_callback=None, header_callback=None, prepare_curl_callback=None, proxy_host=None, proxy_port=None, proxy_username=None, - proxy_password='', allow_nonstandard_methods=False, - validate_cert=True, ca_certs=None, + proxy_password=None, allow_nonstandard_methods=None, + validate_cert=None, ca_certs=None, allow_ipv6=None, client_key=None, client_cert=None): - """Creates an `HTTPRequest`. + r"""Creates an `HTTPRequest`. All parameters except `url` are optional. @@ -261,8 +246,13 @@ class HTTPRequest(object): `~HTTPResponse.body` and `~HTTPResponse.buffer` will be empty in the final response. :arg callable header_callback: If set, `header_callback` will - be run with each header line as it is received, and - `~HTTPResponse.headers` will be empty in the final response. + be run with each header line as it is received (including the + first line, e.g. ``HTTP/1.0 200 OK\r\n``, and a final line + containing only ``\r\n``. All lines include the trailing newline + characters). `~HTTPResponse.headers` will be empty in the final + response. This is most useful in conjunction with + `streaming_callback`, because it's the only way to get access to + header data while the request is in progress. :arg callable prepare_curl_callback: If set, will be called with a `pycurl.Curl` object to allow the application to make additional `setopt` calls. @@ -310,9 +300,9 @@ class HTTPRequest(object): self.user_agent = user_agent self.use_gzip = use_gzip self.network_interface = network_interface - self.streaming_callback = streaming_callback - self.header_callback = header_callback - self.prepare_curl_callback = prepare_curl_callback + self.streaming_callback = stack_context.wrap(streaming_callback) + self.header_callback = stack_context.wrap(header_callback) + self.prepare_curl_callback = stack_context.wrap(prepare_curl_callback) self.allow_nonstandard_methods = allow_nonstandard_methods self.validate_cert = validate_cert self.ca_certs = ca_certs @@ -331,11 +321,15 @@ class HTTPResponse(object): * code: numeric HTTP status code, e.g. 200 or 404 + * reason: human-readable reason phrase describing the status code + (with curl_httpclient, this is a default value rather than the + server's actual response) + * headers: httputil.HTTPHeaders object * buffer: cStringIO object for response body - * body: respose body as string (created on demand from self.buffer) + * body: response body as string (created on demand from self.buffer) * error: Exception object, if any @@ -349,9 +343,10 @@ class HTTPResponse(object): """ def __init__(self, request, code, headers=None, buffer=None, effective_url=None, error=None, request_time=None, - time_info=None): + time_info=None, reason=None): self.request = request self.code = code + self.reason = reason or httplib.responses.get(code, "Unknown") if headers is not None: self.headers = headers else: @@ -412,6 +407,24 @@ class HTTPError(Exception): self.response = response Exception.__init__(self, "HTTP %d: %s" % (self.code, message)) +class _RequestProxy(object): + """Combines an object with a dictionary of defaults. + + Used internally by AsyncHTTPClient implementations. + """ + def __init__(self, request, defaults): + self.request = request + self.defaults = defaults + + def __getattr__(self, name): + request_attr = getattr(self.request, name) + if request_attr is not None: + return request_attr + elif self.defaults is not None: + return self.defaults.get(name, None) + else: + return None + def main(): from tornado.options import define, options, parse_command_line diff --git a/libs/tornado/httpserver.py b/libs/tornado/httpserver.py index 952a6a26815485fb0539d16f1548668d775ec08a..af441d93061235c6d8a813793990036525bdcf6c 100755 --- a/libs/tornado/httpserver.py +++ b/libs/tornado/httpserver.py @@ -27,13 +27,13 @@ This module also defines the `HTTPRequest` class which is exposed via from __future__ import absolute_import, division, with_statement import Cookie -import logging import socket import time from tornado.escape import native_str, parse_qs_bytes from tornado import httputil from tornado import iostream +from tornado.log import gen_log from tornado.netutil import TCPServer from tornado import stack_context from tornado.util import b, bytes_type @@ -67,21 +67,30 @@ class HTTPServer(TCPServer): http_server.listen(8888) ioloop.IOLoop.instance().start() - `HTTPServer` is a very basic connection handler. Beyond parsing the - HTTP request body and headers, the only HTTP semantics implemented - in `HTTPServer` is HTTP/1.1 keep-alive connections. We do not, however, - implement chunked encoding, so the request callback must provide a - ``Content-Length`` header or implement chunked encoding for HTTP/1.1 - requests for the server to run correctly for HTTP/1.1 clients. If - the request handler is unable to do this, you can provide the - ``no_keep_alive`` argument to the `HTTPServer` constructor, which will - ensure the connection is closed on every request no matter what HTTP - version the client is using. - - If ``xheaders`` is ``True``, we support the ``X-Real-Ip`` and ``X-Scheme`` - headers, which override the remote IP and HTTP scheme for all requests. - These headers are useful when running Tornado behind a reverse proxy or - load balancer. + `HTTPServer` is a very basic connection handler. It parses the request + headers and body, but the request callback is responsible for producing + the response exactly as it will appear on the wire. This affords + maximum flexibility for applications to implement whatever parts + of HTTP responses are required. + + `HTTPServer` supports keep-alive connections by default + (automatically for HTTP/1.1, or for HTTP/1.0 when the client + requests ``Connection: keep-alive``). This means that the request + callback must generate a properly-framed response, using either + the ``Content-Length`` header or ``Transfer-Encoding: chunked``. + Applications that are unable to frame their responses properly + should instead return a ``Connection: close`` header in each + response and pass ``no_keep_alive=True`` to the `HTTPServer` + constructor. + + If ``xheaders`` is ``True``, we support the + ``X-Real-Ip``/``X-Forwarded-For`` and + ``X-Scheme``/``X-Forwarded-Proto`` headers, which override the + remote IP and URI scheme/protocol for all requests. These headers + are useful when running Tornado behind a reverse proxy or load + balancer. The ``protocol`` argument can also be set to ``https`` + if Tornado is run behind an SSL-decoding proxy that does not set one of + the supported ``xheaders``. `HTTPServer` can serve SSL traffic with Python 2.6+ and OpenSSL. To make this server serve SSL traffic, send the ssl_options dictionary @@ -134,16 +143,17 @@ class HTTPServer(TCPServer): """ def __init__(self, request_callback, no_keep_alive=False, io_loop=None, - xheaders=False, ssl_options=None, **kwargs): + xheaders=False, ssl_options=None, protocol=None, **kwargs): self.request_callback = request_callback self.no_keep_alive = no_keep_alive self.xheaders = xheaders + self.protocol = protocol TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options, **kwargs) def handle_stream(self, stream, address): HTTPConnection(stream, address, self.request_callback, - self.no_keep_alive, self.xheaders) + self.no_keep_alive, self.xheaders, self.protocol) class _BadRequestException(Exception): @@ -158,12 +168,13 @@ class HTTPConnection(object): until the HTTP conection is closed. """ def __init__(self, stream, address, request_callback, no_keep_alive=False, - xheaders=False): + xheaders=False, protocol=None): self.stream = stream self.address = address self.request_callback = request_callback self.no_keep_alive = no_keep_alive self.xheaders = xheaders + self.protocol = protocol self._request = None self._request_finished = False # Save stack context here, outside of any request. This keeps @@ -172,6 +183,12 @@ class HTTPConnection(object): self.stream.read_until(b("\r\n\r\n"), self._header_callback) self._write_callback = None + def close(self): + self.stream.close() + # Remove this reference to self, which would otherwise cause a + # cycle and delay garbage collection of this connection. + self._header_callback = None + def write(self, chunk, callback=None): """Writes a chunk of output to the stream.""" assert self._request, "Request closed" @@ -218,9 +235,15 @@ class HTTPConnection(object): self._request = None self._request_finished = False if disconnect: - self.stream.close() + self.close() return - self.stream.read_until(b("\r\n\r\n"), self._header_callback) + try: + # Use a try/except instead of checking stream.closed() + # directly, because in some cases the stream doesn't discover + # that it's closed until you try to read from it. + self.stream.read_until(b("\r\n\r\n"), self._header_callback) + except iostream.StreamClosedError: + self.close() def _on_headers(self, data): try: @@ -247,7 +270,7 @@ class HTTPConnection(object): self._request = HTTPRequest( connection=self, method=method, uri=uri, version=version, - headers=headers, remote_ip=remote_ip) + headers=headers, remote_ip=remote_ip, protocol=self.protocol) content_length = headers.get("Content-Length") if content_length: @@ -261,9 +284,9 @@ class HTTPConnection(object): self.request_callback(self._request) except _BadRequestException, e: - logging.info("Malformed HTTP request from %s: %s", + gen_log.info("Malformed HTTP request from %s: %s", self.address[0], e) - self.stream.close() + self.close() return def _on_request_body(self, data): @@ -382,12 +405,7 @@ class HTTPRequest(object): self._finish_time = None self.path, sep, self.query = uri.partition('?') - arguments = parse_qs_bytes(self.query) - self.arguments = {} - for name, values in arguments.iteritems(): - values = [v for v in values if v] - if values: - self.arguments[name] = values + self.arguments = parse_qs_bytes(self.query, keep_blank_values=True) def supports_http_1_1(self): """Returns True if this request supports HTTP/1.1 semantics""" @@ -427,7 +445,7 @@ class HTTPRequest(object): else: return self._finish_time - self._start_time - def get_ssl_certificate(self): + def get_ssl_certificate(self, binary_form=False): """Returns the client's SSL certificate, if any. To use client certificates, the HTTPServer must have been constructed @@ -440,12 +458,16 @@ class HTTPRequest(object): cert_reqs=ssl.CERT_REQUIRED, ca_certs="cacert.crt")) - The return value is a dictionary, see SSLSocket.getpeercert() in - the standard library for more details. + By default, the return value is a dictionary (or None, if no + client certificate is present). If ``binary_form`` is true, a + DER-encoded form of the certificate is returned instead. See + SSLSocket.getpeercert() in the standard library for more + details. http://docs.python.org/library/ssl.html#sslsocket-objects """ try: - return self.connection.stream.socket.getpeercert() + return self.connection.stream.socket.getpeercert( + binary_form=binary_form) except ssl.SSLError: return None diff --git a/libs/tornado/httputil.py b/libs/tornado/httputil.py index 6f5d07a689b3ccbaf21c118beedb081367fd0ed5..a7d543cc23571ae45f11c2902bd3f9d87a4879d2 100755 --- a/libs/tornado/httputil.py +++ b/libs/tornado/httputil.py @@ -18,11 +18,11 @@ from __future__ import absolute_import, division, with_statement -import logging import urllib import re from tornado.escape import native_str, parse_qs_bytes, utf8 +from tornado.log import gen_log from tornado.util import b, ObjectDict @@ -207,6 +207,13 @@ class HTTPFile(ObjectDict): def parse_body_arguments(content_type, body, arguments, files): + """Parses a form request body. + + Supports "application/x-www-form-urlencoded" and "multipart/form-data". + The content_type parameter should be a string and body should be + a byte string. The arguments and files parameters are dictionaries + that will be updated with the parsed contents. + """ if content_type.startswith("application/x-www-form-urlencoded"): uri_arguments = parse_qs_bytes(native_str(body)) for name, values in uri_arguments.iteritems(): @@ -221,7 +228,7 @@ def parse_body_arguments(content_type, body, arguments, files): parse_multipart_form_data(utf8(v), body, arguments, files) break else: - logging.warning("Invalid multipart/form-data") + gen_log.warning("Invalid multipart/form-data") def parse_multipart_form_data(boundary, data, arguments, files): @@ -240,7 +247,7 @@ def parse_multipart_form_data(boundary, data, arguments, files): boundary = boundary[1:-1] final_boundary_index = data.rfind(b("--") + boundary + b("--")) if final_boundary_index == -1: - logging.warning("Invalid multipart/form-data: no final boundary") + gen_log.warning("Invalid multipart/form-data: no final boundary") return parts = data[:final_boundary_index].split(b("--") + boundary + b("\r\n")) for part in parts: @@ -248,17 +255,17 @@ def parse_multipart_form_data(boundary, data, arguments, files): continue eoh = part.find(b("\r\n\r\n")) if eoh == -1: - logging.warning("multipart/form-data missing headers") + gen_log.warning("multipart/form-data missing headers") continue headers = HTTPHeaders.parse(part[:eoh].decode("utf-8")) disp_header = headers.get("Content-Disposition", "") disposition, disp_params = _parse_header(disp_header) if disposition != "form-data" or not part.endswith(b("\r\n")): - logging.warning("Invalid multipart/form-data") + gen_log.warning("Invalid multipart/form-data") continue value = part[eoh + 4:-2] if not disp_params.get("name"): - logging.warning("multipart/form-data value missing name") + gen_log.warning("multipart/form-data value missing name") continue name = disp_params["name"] if disp_params.get("filename"): diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py index 70eb5564b217d8e5fbf199bc33916f3d451e679a..3c9b05b9a998371cfa906b5545f0b48e72c0ccba 100755 --- a/libs/tornado/ioloop.py +++ b/libs/tornado/ioloop.py @@ -30,26 +30,36 @@ from __future__ import absolute_import, division, with_statement import datetime import errno +import functools import heapq -import os import logging +import os import select +import sys import thread import threading import time import traceback +from tornado.concurrent import DummyFuture +from tornado.log import app_log, gen_log from tornado import stack_context +from tornado.util import Configurable try: import signal except ImportError: signal = None +try: + from concurrent import futures +except ImportError: + futures = None + from tornado.platform.auto import set_close_exec, Waker -class IOLoop(object): +class IOLoop(Configurable): """A level-triggered I/O loop. We use epoll (Linux) or kqueue (BSD and Mac OS X; requires python @@ -107,26 +117,7 @@ class IOLoop(object): # Global lock for creating global IOLoop instance _instance_lock = threading.Lock() - def __init__(self, impl=None): - self._impl = impl or _poll() - if hasattr(self._impl, 'fileno'): - set_close_exec(self._impl.fileno()) - self._handlers = {} - self._events = {} - self._callbacks = [] - self._callback_lock = threading.Lock() - self._timeouts = [] - self._running = False - self._stopped = False - self._thread_ident = None - self._blocking_signal_threshold = None - - # Create a pipe that we send bogus data to when we want to wake - # the I/O loop when it is idle - self._waker = Waker() - self.add_handler(self._waker.fileno(), - lambda fd, events: self._waker.consume(), - self.READ) + _current = threading.local() @staticmethod def instance(): @@ -166,6 +157,43 @@ class IOLoop(object): assert not IOLoop.initialized() IOLoop._instance = self + @staticmethod + def current(): + current = getattr(IOLoop._current, "instance", None) + if current is None: + raise ValueError("no current IOLoop") + return current + + def make_current(self): + IOLoop._current.instance = self + + def clear_current(self): + assert IOLoop._current.instance is self + IOLoop._current.instance = None + + @classmethod + def configurable_base(cls): + return IOLoop + + @classmethod + def configurable_default(cls): + if hasattr(select, "epoll") or sys.platform.startswith('linux'): + try: + from tornado.platform.epoll import EPollIOLoop + return EPollIOLoop + except ImportError: + gen_log.warning("unable to import EPollIOLoop, falling back to SelectIOLoop") + pass + if hasattr(select, "kqueue"): + # Python 2.6+ on BSD or Mac + from tornado.platform.kqueue import KQueueIOLoop + return KQueueIOLoop + from tornado.platform.select import SelectIOLoop + return SelectIOLoop + + def initialize(self): + pass + def close(self, all_fds=False): """Closes the IOLoop, freeing any resources used. @@ -185,33 +213,19 @@ class IOLoop(object): Therefore the call to `close` will usually appear just after the call to `start` rather than near the call to `stop`. """ - self.remove_handler(self._waker.fileno()) - if all_fds: - for fd in self._handlers.keys()[:]: - try: - os.close(fd) - except Exception: - logging.debug("error closing fd %s", fd, exc_info=True) - self._waker.close() - self._impl.close() + raise NotImplementedError() def add_handler(self, fd, handler, events): """Registers the given handler to receive the given events for fd.""" - self._handlers[fd] = stack_context.wrap(handler) - self._impl.register(fd, events | self.ERROR) + raise NotImplementedError() def update_handler(self, fd, events): """Changes the events we listen for fd.""" - self._impl.modify(fd, events | self.ERROR) + raise NotImplementedError() def remove_handler(self, fd): """Stop listening for events on fd.""" - self._handlers.pop(fd, None) - self._events.pop(fd, None) - try: - self._impl.unregister(fd) - except (OSError, IOError): - logging.debug("Error deleting fd from IOLoop", exc_info=True) + raise NotImplementedError() def set_blocking_signal_threshold(self, seconds, action): """Sends a signal if the ioloop is blocked for more than s seconds. @@ -224,14 +238,7 @@ class IOLoop(object): If action is None, the process will be killed if it is blocked for too long. """ - if not hasattr(signal, "setitimer"): - logging.error("set_blocking_signal_threshold requires a signal module " - "with the setitimer method") - return - self._blocking_signal_threshold = seconds - if seconds is not None: - signal.signal(signal.SIGALRM, - action if action is not None else signal.SIG_DFL) + raise NotImplementedError() def set_blocking_log_threshold(self, seconds): """Logs a stack trace if the ioloop is blocked for more than s seconds. @@ -244,9 +251,9 @@ class IOLoop(object): For use with set_blocking_signal_threshold. """ - logging.warning('IOLoop blocked for %f seconds in\n%s', - self._blocking_signal_threshold, - ''.join(traceback.format_stack(frame))) + gen_log.warning('IOLoop blocked for %f seconds in\n%s', + self._blocking_signal_threshold, + ''.join(traceback.format_stack(frame))) def start(self): """Starts the I/O loop. @@ -254,11 +261,249 @@ class IOLoop(object): The loop will run until one of the I/O handlers calls stop(), which will make the loop stop after the current event iteration completes. """ + raise NotImplementedError() + + def stop(self): + """Stop the loop after the current event loop iteration is complete. + If the event loop is not currently running, the next call to start() + will return immediately. + + To use asynchronous methods from otherwise-synchronous code (such as + unit tests), you can start and stop the event loop like this:: + + ioloop = IOLoop() + async_method(ioloop=ioloop, callback=ioloop.stop) + ioloop.start() + + ioloop.start() will return after async_method has run its callback, + whether that callback was invoked before or after ioloop.start. + + Note that even after `stop` has been called, the IOLoop is not + completely stopped until `IOLoop.start` has also returned. + """ + raise NotImplementedError() + + def time(self): + """Returns the current time according to the IOLoop's clock. + + The return value is a floating-point number relative to an + unspecified time in the past. + + By default, the IOLoop's time function is `time.time`. However, + it may be configured to use e.g. `time.monotonic` instead. + Calls to `add_timeout` that pass a number instead of a + `datetime.timedelta` should use this function to compute the + appropriate time, so they can work no matter what time function + is chosen. + """ + return time.time() + + def add_timeout(self, deadline, callback): + """Calls the given callback at the time deadline from the I/O loop. + + Returns a handle that may be passed to remove_timeout to cancel. + + ``deadline`` may be a number denoting a time relative to + `IOLoop.time`, or a ``datetime.timedelta`` object for a + deadline relative to the current time. + + Note that it is not safe to call `add_timeout` from other threads. + Instead, you must use `add_callback` to transfer control to the + IOLoop's thread, and then call `add_timeout` from there. + """ + raise NotImplementedError() + + def remove_timeout(self, timeout): + """Cancels a pending timeout. + + The argument is a handle as returned by add_timeout. + """ + raise NotImplementedError() + + def add_callback(self, callback): + """Calls the given callback on the next I/O loop iteration. + + It is safe to call this method from any thread at any time, + except from a signal handler. Note that this is the *only* + method in IOLoop that makes this thread-safety guarantee; all + other interaction with the IOLoop must be done from that + IOLoop's thread. add_callback() may be used to transfer + control from other threads to the IOLoop's thread. + + To add a callback from a signal handler, see + `add_callback_from_signal`. + """ + raise NotImplementedError() + + def add_callback_from_signal(self, callback): + """Calls the given callback on the next I/O loop iteration. + + Safe for use from a Python signal handler; should not be used + otherwise. + + Callbacks added with this method will be run without any + stack_context, to avoid picking up the context of the function + that was interrupted by the signal. + """ + raise NotImplementedError() + + if futures is not None: + _FUTURE_TYPES = (futures.Future, DummyFuture) + else: + _FUTURE_TYPES = DummyFuture + def add_future(self, future, callback): + """Schedules a callback on the IOLoop when the given future is finished. + + The callback is invoked with one argument, the future. + """ + assert isinstance(future, IOLoop._FUTURE_TYPES) + callback = stack_context.wrap(callback) + future.add_done_callback( + lambda future: self.add_callback( + functools.partial(callback, future))) + + def _run_callback(self, callback): + """Runs a callback with error handling. + + For use in subclasses. + """ + try: + callback() + except Exception: + self.handle_callback_exception(callback) + + def handle_callback_exception(self, callback): + """This method is called whenever a callback run by the IOLoop + throws an exception. + + By default simply logs the exception as an error. Subclasses + may override this method to customize reporting of exceptions. + + The exception itself is not passed explicitly, but is available + in sys.exc_info. + """ + app_log.error("Exception in callback %r", callback, exc_info=True) + + + +class PollIOLoop(IOLoop): + """Base class for IOLoops built around a select-like function. + + For concrete implementations, see `tornado.platform.epoll.EPollIOLoop` + (Linux), `tornado.platform.kqueue.KQueueIOLoop` (BSD and Mac), or + `tornado.platform.select.SelectIOLoop` (all platforms). + """ + def initialize(self, impl, time_func=None): + super(PollIOLoop, self).initialize() + self._impl = impl + if hasattr(self._impl, 'fileno'): + set_close_exec(self._impl.fileno()) + self.time_func = time_func or time.time + self._handlers = {} + self._events = {} + self._callbacks = [] + self._callback_lock = threading.Lock() + self._timeouts = [] + self._running = False + self._stopped = False + self._closing = False + self._thread_ident = None + self._blocking_signal_threshold = None + + # Create a pipe that we send bogus data to when we want to wake + # the I/O loop when it is idle + self._waker = Waker() + self.add_handler(self._waker.fileno(), + lambda fd, events: self._waker.consume(), + self.READ) + + def close(self, all_fds=False): + with self._callback_lock: + self._closing = True + self.remove_handler(self._waker.fileno()) + if all_fds: + for fd in self._handlers.keys()[:]: + try: + os.close(fd) + except Exception: + gen_log.debug("error closing fd %s", fd, exc_info=True) + self._waker.close() + self._impl.close() + + def add_handler(self, fd, handler, events): + self._handlers[fd] = stack_context.wrap(handler) + self._impl.register(fd, events | self.ERROR) + + def update_handler(self, fd, events): + self._impl.modify(fd, events | self.ERROR) + + def remove_handler(self, fd): + self._handlers.pop(fd, None) + self._events.pop(fd, None) + try: + self._impl.unregister(fd) + except (OSError, IOError): + gen_log.debug("Error deleting fd from IOLoop", exc_info=True) + + def set_blocking_signal_threshold(self, seconds, action): + if not hasattr(signal, "setitimer"): + gen_log.error("set_blocking_signal_threshold requires a signal module " + "with the setitimer method") + return + self._blocking_signal_threshold = seconds + if seconds is not None: + signal.signal(signal.SIGALRM, + action if action is not None else signal.SIG_DFL) + + def start(self): + if not logging.getLogger().handlers: + # The IOLoop catches and logs exceptions, so it's + # important that log output be visible. However, python's + # default behavior for non-root loggers (prior to python + # 3.2) is to print an unhelpful "no handlers could be + # found" message rather than the actual log entry, so we + # must explicitly configure logging if we've made it this + # far without anything. + logging.basicConfig() if self._stopped: self._stopped = False return + old_current = getattr(IOLoop._current, "instance", None) + IOLoop._current.instance = self self._thread_ident = thread.get_ident() self._running = True + + # signal.set_wakeup_fd closes a race condition in event loops: + # a signal may arrive at the beginning of select/poll/etc + # before it goes into its interruptible sleep, so the signal + # will be consumed without waking the select. The solution is + # for the (C, synchronous) signal handler to write to a pipe, + # which will then be seen by select. + # + # In python's signal handling semantics, this only matters on the + # main thread (fortunately, set_wakeup_fd only works on the main + # thread and will raise a ValueError otherwise). + # + # If someone has already set a wakeup fd, we don't want to + # disturb it. This is an issue for twisted, which does its + # SIGCHILD processing in response to its own wakeup fd being + # written to. As long as the wakeup fd is registered on the IOLoop, + # the loop will still wake up and everything should work. + old_wakeup_fd = None + if hasattr(signal, 'set_wakeup_fd') and os.name == 'posix': + # requires python 2.6+, unix. set_wakeup_fd exists but crashes + # the python process on windows. + try: + old_wakeup_fd = signal.set_wakeup_fd(self._waker.write_fileno()) + if old_wakeup_fd != -1: + # Already set, restore previous value. This is a little racy, + # but there's no clean get_wakeup_fd and in real use the + # IOLoop is just started once at the beginning. + signal.set_wakeup_fd(old_wakeup_fd) + old_wakeup_fd = None + except ValueError: # non-main thread + pass + while True: poll_timeout = 3600.0 @@ -271,7 +516,7 @@ class IOLoop(object): self._run_callback(callback) if self._timeouts: - now = time.time() + now = self.time() while self._timeouts: if self._timeouts[0].callback is None: # the timeout was cancelled @@ -330,64 +575,33 @@ class IOLoop(object): # Happens when the client closes the connection pass else: - logging.error("Exception in I/O handler for fd %s", + app_log.error("Exception in I/O handler for fd %s", fd, exc_info=True) except Exception: - logging.error("Exception in I/O handler for fd %s", + app_log.error("Exception in I/O handler for fd %s", fd, exc_info=True) # reset the stopped flag so another start/stop pair can be issued self._stopped = False if self._blocking_signal_threshold is not None: signal.setitimer(signal.ITIMER_REAL, 0, 0) + IOLoop._current.instance = old_current + if old_wakeup_fd is not None: + signal.set_wakeup_fd(old_wakeup_fd) def stop(self): - """Stop the loop after the current event loop iteration is complete. - If the event loop is not currently running, the next call to start() - will return immediately. - - To use asynchronous methods from otherwise-synchronous code (such as - unit tests), you can start and stop the event loop like this:: - - ioloop = IOLoop() - async_method(ioloop=ioloop, callback=ioloop.stop) - ioloop.start() - - ioloop.start() will return after async_method has run its callback, - whether that callback was invoked before or after ioloop.start. - - Note that even after `stop` has been called, the IOLoop is not - completely stopped until `IOLoop.start` has also returned. - """ self._running = False self._stopped = True self._waker.wake() - def running(self): - """Returns true if this IOLoop is currently running.""" - return self._running + def time(self): + return self.time_func() def add_timeout(self, deadline, callback): - """Calls the given callback at the time deadline from the I/O loop. - - Returns a handle that may be passed to remove_timeout to cancel. - - ``deadline`` may be a number denoting a unix timestamp (as returned - by ``time.time()`` or a ``datetime.timedelta`` object for a deadline - relative to the current time. - - Note that it is not safe to call `add_timeout` from other threads. - Instead, you must use `add_callback` to transfer control to the - IOLoop's thread, and then call `add_timeout` from there. - """ - timeout = _Timeout(deadline, stack_context.wrap(callback)) + timeout = _Timeout(deadline, stack_context.wrap(callback), self) heapq.heappush(self._timeouts, timeout) return timeout def remove_timeout(self, timeout): - """Cancels a pending timeout. - - The argument is a handle as returned by add_timeout. - """ # Removing from a heap is complicated, so just leave the defunct # timeout object in the queue (see discussion in # http://docs.python.org/library/heapq.html). @@ -396,15 +610,9 @@ class IOLoop(object): timeout.callback = None def add_callback(self, callback): - """Calls the given callback on the next I/O loop iteration. - - It is safe to call this method from any thread at any time. - Note that this is the *only* method in IOLoop that makes this - guarantee; all other interaction with the IOLoop must be done - from that IOLoop's thread. add_callback() may be used to transfer - control from other threads to the IOLoop's thread. - """ with self._callback_lock: + if self._closing: + raise RuntimeError("IOLoop is closing") list_empty = not self._callbacks self._callbacks.append(stack_context.wrap(callback)) if list_empty and thread.get_ident() != self._thread_ident: @@ -416,23 +624,22 @@ class IOLoop(object): # avoid it when we can. self._waker.wake() - def _run_callback(self, callback): - try: - callback() - except Exception: - self.handle_callback_exception(callback) - - def handle_callback_exception(self, callback): - """This method is called whenever a callback run by the IOLoop - throws an exception. - - By default simply logs the exception as an error. Subclasses - may override this method to customize reporting of exceptions. - - The exception itself is not passed explicitly, but is available - in sys.exc_info. - """ - logging.error("Exception in callback %r", callback, exc_info=True) + def add_callback_from_signal(self, callback): + with stack_context.NullContext(): + if thread.get_ident() != self._thread_ident: + # if the signal is handled on another thread, we can add + # it normally (modulo the NullContext) + self.add_callback(callback) + else: + # If we're on the IOLoop's thread, we cannot use + # the regular add_callback because it may deadlock on + # _callback_lock. Blindly insert into self._callbacks. + # This is safe because the GIL makes list.append atomic. + # One subtlety is that if the signal interrupted the + # _callback_lock block in IOLoop.start, we may modify + # either the old or new version of self._callbacks, + # but either way will work. + self._callbacks.append(stack_context.wrap(callback)) class _Timeout(object): @@ -441,11 +648,11 @@ class _Timeout(object): # Reduce memory overhead when there are lots of pending callbacks __slots__ = ['deadline', 'callback'] - def __init__(self, deadline, callback): + def __init__(self, deadline, callback, io_loop): if isinstance(deadline, (int, long, float)): self.deadline = deadline elif isinstance(deadline, datetime.timedelta): - self.deadline = time.time() + _Timeout.timedelta_to_seconds(deadline) + self.deadline = io_loop.time() + _Timeout.timedelta_to_seconds(deadline) else: raise TypeError("Unsupported deadline %r" % deadline) self.callback = callback @@ -477,6 +684,8 @@ class PeriodicCallback(object): """ def __init__(self, callback, callback_time, io_loop=None): self.callback = callback + if callback_time <= 0: + raise ValueError("Periodic callback must have a positive callback_time") self.callback_time = callback_time self.io_loop = io_loop or IOLoop.instance() self._running = False @@ -485,7 +694,7 @@ class PeriodicCallback(object): def start(self): """Starts the timer.""" self._running = True - self._next_timeout = time.time() + self._next_timeout = self.io_loop.time() self._schedule_next() def stop(self): @@ -501,172 +710,12 @@ class PeriodicCallback(object): try: self.callback() except Exception: - logging.error("Error in periodic callback", exc_info=True) + app_log.error("Error in periodic callback", exc_info=True) self._schedule_next() def _schedule_next(self): if self._running: - current_time = time.time() + current_time = self.io_loop.time() while self._next_timeout <= current_time: self._next_timeout += self.callback_time / 1000.0 self._timeout = self.io_loop.add_timeout(self._next_timeout, self._run) - - -class _EPoll(object): - """An epoll-based event loop using our C module for Python 2.5 systems""" - _EPOLL_CTL_ADD = 1 - _EPOLL_CTL_DEL = 2 - _EPOLL_CTL_MOD = 3 - - def __init__(self): - self._epoll_fd = epoll.epoll_create() - - def fileno(self): - return self._epoll_fd - - def close(self): - os.close(self._epoll_fd) - - def register(self, fd, events): - epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_ADD, fd, events) - - def modify(self, fd, events): - epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_MOD, fd, events) - - def unregister(self, fd): - epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_DEL, fd, 0) - - def poll(self, timeout): - return epoll.epoll_wait(self._epoll_fd, int(timeout * 1000)) - - -class _KQueue(object): - """A kqueue-based event loop for BSD/Mac systems.""" - def __init__(self): - self._kqueue = select.kqueue() - self._active = {} - - def fileno(self): - return self._kqueue.fileno() - - def close(self): - self._kqueue.close() - - def register(self, fd, events): - if fd in self._active: - raise IOError("fd %d already registered" % fd) - self._control(fd, events, select.KQ_EV_ADD) - self._active[fd] = events - - def modify(self, fd, events): - self.unregister(fd) - self.register(fd, events) - - def unregister(self, fd): - events = self._active.pop(fd) - self._control(fd, events, select.KQ_EV_DELETE) - - def _control(self, fd, events, flags): - kevents = [] - if events & IOLoop.WRITE: - kevents.append(select.kevent( - fd, filter=select.KQ_FILTER_WRITE, flags=flags)) - if events & IOLoop.READ or not kevents: - # Always read when there is not a write - kevents.append(select.kevent( - fd, filter=select.KQ_FILTER_READ, flags=flags)) - # Even though control() takes a list, it seems to return EINVAL - # on Mac OS X (10.6) when there is more than one event in the list. - for kevent in kevents: - self._kqueue.control([kevent], 0) - - def poll(self, timeout): - kevents = self._kqueue.control(None, 1000, timeout) - events = {} - for kevent in kevents: - fd = kevent.ident - if kevent.filter == select.KQ_FILTER_READ: - events[fd] = events.get(fd, 0) | IOLoop.READ - if kevent.filter == select.KQ_FILTER_WRITE: - if kevent.flags & select.KQ_EV_EOF: - # If an asynchronous connection is refused, kqueue - # returns a write event with the EOF flag set. - # Turn this into an error for consistency with the - # other IOLoop implementations. - # Note that for read events, EOF may be returned before - # all data has been consumed from the socket buffer, - # so we only check for EOF on write events. - events[fd] = IOLoop.ERROR - else: - events[fd] = events.get(fd, 0) | IOLoop.WRITE - if kevent.flags & select.KQ_EV_ERROR: - events[fd] = events.get(fd, 0) | IOLoop.ERROR - return events.items() - - -class _Select(object): - """A simple, select()-based IOLoop implementation for non-Linux systems""" - def __init__(self): - self.read_fds = set() - self.write_fds = set() - self.error_fds = set() - self.fd_sets = (self.read_fds, self.write_fds, self.error_fds) - - def close(self): - pass - - def register(self, fd, events): - if fd in self.read_fds or fd in self.write_fds or fd in self.error_fds: - raise IOError("fd %d already registered" % fd) - if events & IOLoop.READ: - self.read_fds.add(fd) - if events & IOLoop.WRITE: - self.write_fds.add(fd) - if events & IOLoop.ERROR: - self.error_fds.add(fd) - # Closed connections are reported as errors by epoll and kqueue, - # but as zero-byte reads by select, so when errors are requested - # we need to listen for both read and error. - self.read_fds.add(fd) - - def modify(self, fd, events): - self.unregister(fd) - self.register(fd, events) - - def unregister(self, fd): - self.read_fds.discard(fd) - self.write_fds.discard(fd) - self.error_fds.discard(fd) - - def poll(self, timeout): - readable, writeable, errors = select.select( - self.read_fds, self.write_fds, self.error_fds, timeout) - events = {} - for fd in readable: - events[fd] = events.get(fd, 0) | IOLoop.READ - for fd in writeable: - events[fd] = events.get(fd, 0) | IOLoop.WRITE - for fd in errors: - events[fd] = events.get(fd, 0) | IOLoop.ERROR - return events.items() - - -# Choose a poll implementation. Use epoll if it is available, fall back to -# select() for non-Linux platforms -if hasattr(select, "epoll"): - # Python 2.6+ on Linux - _poll = select.epoll -elif hasattr(select, "kqueue"): - # Python 2.6+ on BSD or Mac - _poll = _KQueue -else: - try: - # Linux systems with our C module installed - from tornado import epoll - _poll = _EPoll - except Exception: - # All other systems - import sys - if "linux" in sys.platform: - logging.warning("epoll module not found; using select()") - _poll = _Select diff --git a/libs/tornado/iostream.py b/libs/tornado/iostream.py index cfe6b1ce89ee9f4ca27312d29751f58fc26a24f7..40ac4964fc59e9f3e6e0ae9b7b7b654d0f213a3a 100755 --- a/libs/tornado/iostream.py +++ b/libs/tornado/iostream.py @@ -14,19 +14,27 @@ # License for the specific language governing permissions and limitations # under the License. -"""A utility class to write to and read from a non-blocking socket.""" +"""Utility classes to write to and read from non-blocking files and sockets. + +Contents: + +* `BaseIOStream`: Generic interface for reading and writing. +* `IOStream`: Implementation of BaseIOStream using non-blocking sockets. +* `SSLIOStream`: SSL-aware version of IOStream. +* `PipeIOStream`: Pipe-based IOStream implementation. +""" from __future__ import absolute_import, division, with_statement import collections import errno -import logging import os import socket import sys import re from tornado import ioloop +from tornado.log import gen_log, app_log from tornado import stack_context from tornado.util import b, bytes_type @@ -35,56 +43,29 @@ try: except ImportError: ssl = None +try: + from tornado.platform.posix import _set_nonblocking +except ImportError: + _set_nonblocking = None + +class StreamClosedError(IOError): + pass -class IOStream(object): - r"""A utility class to write to and read from a non-blocking socket. +class BaseIOStream(object): + """A utility class to write to and read from a non-blocking file or socket. We support a non-blocking ``write()`` and a family of ``read_*()`` methods. All of the methods take callbacks (since writing and reading are non-blocking and asynchronous). - The socket parameter may either be connected or unconnected. For - server operations the socket is the result of calling socket.accept(). - For client operations the socket is created with socket.socket(), - and may either be connected before passing it to the IOStream or - connected with IOStream.connect. - When a stream is closed due to an error, the IOStream's `error` attribute contains the exception object. - A very simple (and broken) HTTP client using this class:: - - from tornado import ioloop - from tornado import iostream - import socket - - def send_request(): - stream.write("GET / HTTP/1.0\r\nHost: friendfeed.com\r\n\r\n") - stream.read_until("\r\n\r\n", on_headers) - - def on_headers(data): - headers = {} - for line in data.split("\r\n"): - parts = line.split(":") - if len(parts) == 2: - headers[parts[0].strip()] = parts[1].strip() - stream.read_bytes(int(headers["Content-Length"]), on_body) - - def on_body(data): - print data - stream.close() - ioloop.IOLoop.instance().stop() - - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) - stream = iostream.IOStream(s) - stream.connect(("friendfeed.com", 80), send_request) - ioloop.IOLoop.instance().start() - + Subclasses must implement `fileno`, `close_fd`, `write_to_fd`, + `read_from_fd`, and optionally `get_fd_error`. """ - def __init__(self, socket, io_loop=None, max_buffer_size=104857600, + def __init__(self, io_loop=None, max_buffer_size=104857600, read_chunk_size=4096): - self.socket = socket - self.socket.setblocking(False) self.io_loop = io_loop or ioloop.IOLoop.instance() self.max_buffer_size = max_buffer_size self.read_chunk_size = read_chunk_size @@ -105,40 +86,45 @@ class IOStream(object): self._connecting = False self._state = None self._pending_callbacks = 0 + self._closed = False - def connect(self, address, callback=None): - """Connects the socket to a remote address without blocking. + def fileno(self): + """Returns the file descriptor for this stream.""" + raise NotImplementedError() - May only be called if the socket passed to the constructor was - not previously connected. The address parameter is in the - same format as for socket.connect, i.e. a (host, port) tuple. - If callback is specified, it will be called when the - connection is completed. + def close_fd(self): + """Closes the file underlying this stream. - Note that it is safe to call IOStream.write while the - connection is pending, in which case the data will be written - as soon as the connection is ready. Calling IOStream read - methods before the socket is connected works on some platforms - but is non-portable. + ``close_fd`` is called by `BaseIOStream` and should not be called + elsewhere; other users should call `close` instead. """ - self._connecting = True - try: - self.socket.connect(address) - except socket.error, e: - # In non-blocking mode we expect connect() to raise an - # exception with EINPROGRESS or EWOULDBLOCK. - # - # On freebsd, other errors such as ECONNREFUSED may be - # returned immediately when attempting to connect to - # localhost, so handle them the same way as an error - # reported later in _handle_connect. - if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK): - logging.warning("Connect error on fd %d: %s", - self.socket.fileno(), e) - self.close() - return - self._connect_callback = stack_context.wrap(callback) - self._add_io_state(self.io_loop.WRITE) + raise NotImplementedError() + + def write_to_fd(self, data): + """Attempts to write ``data`` to the underlying file. + + Returns the number of bytes written. + """ + raise NotImplementedError() + + def read_from_fd(self): + """Attempts to read from the underlying file. + + Returns ``None`` if there was nothing to read (the socket returned + EWOULDBLOCK or equivalent), otherwise returns the data. When possible, + should return no more than ``self.read_chunk_size`` bytes at a time. + """ + raise NotImplementedError() + + def get_fd_error(self): + """Returns information about any error on the underlying file. + + This method is called after the IOLoop has signaled an error on the + file descriptor, and should return an Exception (such as `socket.error` + with additional information, or None if no such information is + available. + """ + return None def read_until_regex(self, regex, callback): """Call callback when we read the given regex pattern.""" @@ -176,8 +162,14 @@ class IOStream(object): a ``streaming_callback`` is not used. """ self._set_read_callback(callback) + self._streaming_callback = stack_context.wrap(streaming_callback) if self.closed(): - self._run_callback(callback, self._consume(self._read_buffer_size)) + if self._streaming_callback is not None: + self._run_callback(self._streaming_callback, + self._consume(self._read_buffer_size)) + self._run_callback(self._read_callback, + self._consume(self._read_buffer_size)) + self._streaming_callback = None self._read_callback = None return self._read_until_close = True @@ -207,10 +199,11 @@ class IOStream(object): else: self._write_buffer.append(data) self._write_callback = stack_context.wrap(callback) - self._handle_write() - if self._write_buffer: - self._add_io_state(self.io_loop.WRITE) - self._maybe_add_error_listener() + if not self._connecting: + self._handle_write() + if self._write_buffer: + self._add_io_state(self.io_loop.WRITE) + self._maybe_add_error_listener() def set_close_callback(self, callback): """Call the given callback when the stream is closed.""" @@ -218,7 +211,7 @@ class IOStream(object): def close(self): """Close this stream.""" - if self.socket is not None: + if not self.closed(): if any(sys.exc_info()): self.error = sys.exc_info()[1] if self._read_until_close: @@ -228,14 +221,14 @@ class IOStream(object): self._run_callback(callback, self._consume(self._read_buffer_size)) if self._state is not None: - self.io_loop.remove_handler(self.socket.fileno()) + self.io_loop.remove_handler(self.fileno()) self._state = None - self.socket.close() - self.socket = None + self.close_fd() + self._closed = True self._maybe_run_close_callback() def _maybe_run_close_callback(self): - if (self.socket is None and self._close_callback and + if (self.closed() and self._close_callback and self._pending_callbacks == 0): # if there are pending callbacks, don't run the close callback # until they're done (see _maybe_add_error_handler) @@ -253,27 +246,25 @@ class IOStream(object): def closed(self): """Returns true if the stream has been closed.""" - return self.socket is None + return self._closed def _handle_events(self, fd, events): - if not self.socket: - logging.warning("Got events for closed stream %d", fd) + if self.closed(): + gen_log.warning("Got events for closed stream %d", fd) return try: if events & self.io_loop.READ: self._handle_read() - if not self.socket: + if self.closed(): return if events & self.io_loop.WRITE: if self._connecting: self._handle_connect() self._handle_write() - if not self.socket: + if self.closed(): return if events & self.io_loop.ERROR: - errno = self.socket.getsockopt(socket.SOL_SOCKET, - socket.SO_ERROR) - self.error = socket.error(errno, os.strerror(errno)) + self.error = self.get_fd_error() # We may have queued up a user callback in _handle_read or # _handle_write, so don't close the IOStream until those # callbacks have had a chance to run. @@ -290,9 +281,9 @@ class IOStream(object): assert self._state is not None, \ "shouldn't happen: _handle_events without self._state" self._state = state - self.io_loop.update_handler(self.socket.fileno(), self._state) + self.io_loop.update_handler(self.fileno(), self._state) except Exception: - logging.error("Uncaught exception, closing connection.", + gen_log.error("Uncaught exception, closing connection.", exc_info=True) self.close() raise @@ -303,7 +294,7 @@ class IOStream(object): try: callback(*args) except Exception: - logging.error("Uncaught exception, closing connection.", + app_log.error("Uncaught exception, closing connection.", exc_info=True) # Close the socket on an uncaught exception from a user callback # (It would eventually get closed when the socket object is @@ -345,7 +336,7 @@ class IOStream(object): # clause below (which calls `close` and does need to # trigger the callback) self._pending_callbacks += 1 - while True: + while not self.closed(): # Read from the socket until we get EWOULDBLOCK or equivalent. # SSL sockets do some internal buffering, and if the data is # sitting in the SSL object's buffer select() and friends @@ -356,7 +347,7 @@ class IOStream(object): finally: self._pending_callbacks -= 1 except Exception: - logging.warning("error on read", exc_info=True) + gen_log.warning("error on read", exc_info=True) self.close() return if self._read_from_buffer(): @@ -382,33 +373,14 @@ class IOStream(object): try: # See comments in _handle_read about incrementing _pending_callbacks self._pending_callbacks += 1 - while True: + while not self.closed(): if self._read_to_buffer() == 0: break - self._check_closed() finally: self._pending_callbacks -= 1 if self._read_from_buffer(): return - self._add_io_state(self.io_loop.READ) - - def _read_from_socket(self): - """Attempts to read from the socket. - - Returns the data read or None if there is nothing to read. - May be overridden in subclasses. - """ - try: - chunk = self.socket.recv(self.read_chunk_size) - except socket.error, e: - if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): - return None - else: - raise - if not chunk: - self.close() - return None - return chunk + self._maybe_add_error_listener() def _read_to_buffer(self): """Reads from the socket and appends the result to the read buffer. @@ -418,11 +390,15 @@ class IOStream(object): error closes the socket and raises an exception. """ try: - chunk = self._read_from_socket() - except socket.error, e: + chunk = self.read_from_fd() + except (socket.error, IOError, OSError), e: # ssl.SSLError is a subclass of socket.error - logging.warning("Read error on %d: %s", - self.socket.fileno(), e) + if e.args[0] == errno.ECONNRESET: + # Treat ECONNRESET as a connection close rather than + # an error to minimize log spam (the exception will + # be available on self.error for apps that care). + self.close() + return self.close() raise if chunk is None: @@ -430,7 +406,7 @@ class IOStream(object): self._read_buffer.append(chunk) self._read_buffer_size += len(chunk) if self._read_buffer_size >= self.max_buffer_size: - logging.error("Reached maximum read buffer size") + gen_log.error("Reached maximum read buffer size") self.close() raise IOError("Reached maximum read buffer size") return len(chunk) @@ -495,24 +471,6 @@ class IOStream(object): _double_prefix(self._read_buffer) return False - def _handle_connect(self): - err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) - if err != 0: - self.error = socket.error(err, os.strerror(err)) - # IOLoop implementations may vary: some of them return - # an error state before the socket becomes writable, so - # in that case a connection failure would be handled by the - # error path in _handle_events instead of here. - logging.warning("Connect error on fd %d: %s", - self.socket.fileno(), errno.errorcode[err]) - self.close() - return - if self._connect_callback is not None: - callback = self._connect_callback - self._connect_callback = None - self._run_callback(callback) - self._connecting = False - def _handle_write(self): while self._write_buffer: try: @@ -523,7 +481,7 @@ class IOStream(object): # process. Therefore we must not call socket.send # with more than 128KB at a time. _merge_prefix(self._write_buffer, 128 * 1024) - num_bytes = self.socket.send(self._write_buffer[0]) + num_bytes = self.write_to_fd(self._write_buffer[0]) if num_bytes == 0: # With OpenSSL, if we couldn't write the entire buffer, # the very same string object must be used on the @@ -543,8 +501,8 @@ class IOStream(object): self._write_buffer_frozen = True break else: - logging.warning("Write error on %d: %s", - self.socket.fileno(), e) + gen_log.warning("Write error on %d: %s", + self.fileno(), e) self.close() return if not self._write_buffer and self._write_callback: @@ -560,12 +518,12 @@ class IOStream(object): return self._read_buffer.popleft() def _check_closed(self): - if not self.socket: - raise IOError("Stream is closed") + if self.closed(): + raise StreamClosedError("Stream is closed") def _maybe_add_error_listener(self): if self._state is None and self._pending_callbacks == 0: - if self.socket is None: + if self.closed(): self._maybe_run_close_callback() else: self._add_io_state(ioloop.IOLoop.READ) @@ -591,17 +549,143 @@ class IOStream(object): (since the write callback is optional so we can have a fast-path write with no `_run_callback`) """ - if self.socket is None: + if self.closed(): # connection has been closed, so there can be no future events return if self._state is None: self._state = ioloop.IOLoop.ERROR | state with stack_context.NullContext(): self.io_loop.add_handler( - self.socket.fileno(), self._handle_events, self._state) + self.fileno(), self._handle_events, self._state) elif not self._state & state: self._state = self._state | state - self.io_loop.update_handler(self.socket.fileno(), self._state) + self.io_loop.update_handler(self.fileno(), self._state) + + +class IOStream(BaseIOStream): + r"""Socket-based IOStream implementation. + + This class supports the read and write methods from `BaseIOStream` + plus a `connect` method. + + The socket parameter may either be connected or unconnected. For + server operations the socket is the result of calling socket.accept(). + For client operations the socket is created with socket.socket(), + and may either be connected before passing it to the IOStream or + connected with IOStream.connect. + + A very simple (and broken) HTTP client using this class:: + + from tornado import ioloop + from tornado import iostream + import socket + + def send_request(): + stream.write("GET / HTTP/1.0\r\nHost: friendfeed.com\r\n\r\n") + stream.read_until("\r\n\r\n", on_headers) + + def on_headers(data): + headers = {} + for line in data.split("\r\n"): + parts = line.split(":") + if len(parts) == 2: + headers[parts[0].strip()] = parts[1].strip() + stream.read_bytes(int(headers["Content-Length"]), on_body) + + def on_body(data): + print data + stream.close() + ioloop.IOLoop.instance().stop() + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + stream = iostream.IOStream(s) + stream.connect(("friendfeed.com", 80), send_request) + ioloop.IOLoop.instance().start() + """ + def __init__(self, socket, *args, **kwargs): + self.socket = socket + self.socket.setblocking(False) + super(IOStream, self).__init__(*args, **kwargs) + + def fileno(self): + return self.socket.fileno() + + def close_fd(self): + self.socket.close() + self.socket = None + + def get_fd_error(self): + errno = self.socket.getsockopt(socket.SOL_SOCKET, + socket.SO_ERROR) + return socket.error(errno, os.strerror(errno)) + + def read_from_fd(self): + try: + chunk = self.socket.recv(self.read_chunk_size) + except socket.error, e: + if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): + return None + else: + raise + if not chunk: + self.close() + return None + return chunk + + def write_to_fd(self, data): + return self.socket.send(data) + + def connect(self, address, callback=None): + """Connects the socket to a remote address without blocking. + + May only be called if the socket passed to the constructor was + not previously connected. The address parameter is in the + same format as for socket.connect, i.e. a (host, port) tuple. + If callback is specified, it will be called when the + connection is completed. + + Note that it is safe to call IOStream.write while the + connection is pending, in which case the data will be written + as soon as the connection is ready. Calling IOStream read + methods before the socket is connected works on some platforms + but is non-portable. + """ + self._connecting = True + try: + self.socket.connect(address) + except socket.error, e: + # In non-blocking mode we expect connect() to raise an + # exception with EINPROGRESS or EWOULDBLOCK. + # + # On freebsd, other errors such as ECONNREFUSED may be + # returned immediately when attempting to connect to + # localhost, so handle them the same way as an error + # reported later in _handle_connect. + if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK): + gen_log.warning("Connect error on fd %d: %s", + self.socket.fileno(), e) + self.close() + return + self._connect_callback = stack_context.wrap(callback) + self._add_io_state(self.io_loop.WRITE) + + def _handle_connect(self): + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if err != 0: + self.error = socket.error(err, os.strerror(err)) + # IOLoop implementations may vary: some of them return + # an error state before the socket becomes writable, so + # in that case a connection failure would be handled by the + # error path in _handle_events instead of here. + gen_log.warning("Connect error on fd %d: %s", + self.socket.fileno(), errno.errorcode[err]) + self.close() + return + if self._connect_callback is not None: + callback = self._connect_callback + self._connect_callback = None + self._run_callback(callback) + self._connecting = False class SSLIOStream(IOStream): @@ -626,6 +710,7 @@ class SSLIOStream(IOStream): self._ssl_accepting = True self._handshake_reading = False self._handshake_writing = False + self._ssl_connect_callback = None def reading(self): return self._handshake_reading or super(SSLIOStream, self).reading() @@ -650,7 +735,12 @@ class SSLIOStream(IOStream): ssl.SSL_ERROR_ZERO_RETURN): return self.close() elif err.args[0] == ssl.SSL_ERROR_SSL: - logging.warning("SSL Error on %d: %s", self.socket.fileno(), err) + try: + peer = self.socket.getpeername() + except: + peer = '(not connected)' + gen_log.warning("SSL Error on %d %s: %s", + self.socket.fileno(), peer, err) return self.close() raise except socket.error, err: @@ -658,7 +748,10 @@ class SSLIOStream(IOStream): return self.close() else: self._ssl_accepting = False - super(SSLIOStream, self)._handle_connect() + if self._ssl_connect_callback is not None: + callback = self._ssl_connect_callback + self._ssl_connect_callback = None + self._run_callback(callback) def _handle_read(self): if self._ssl_accepting: @@ -672,16 +765,25 @@ class SSLIOStream(IOStream): return super(SSLIOStream, self)._handle_write() + def connect(self, address, callback=None): + # Save the user's callback and run it after the ssl handshake + # has completed. + self._ssl_connect_callback = callback + super(SSLIOStream, self).connect(address, callback=None) + def _handle_connect(self): + # When the connection is complete, wrap the socket for SSL + # traffic. Note that we do this by overriding _handle_connect + # instead of by passing a callback to super().connect because + # user callbacks are enqueued asynchronously on the IOLoop, + # but since _handle_events calls _handle_connect immediately + # followed by _handle_write we need this to be synchronous. self.socket = ssl.wrap_socket(self.socket, do_handshake_on_connect=False, **self._ssl_options) - # Don't call the superclass's _handle_connect (which is responsible - # for telling the application that the connection is complete) - # until we've completed the SSL handshake (so certificates are - # available, etc). + super(SSLIOStream, self)._handle_connect() - def _read_from_socket(self): + def read_from_fd(self): if self._ssl_accepting: # If the handshake hasn't finished yet, there can't be anything # to read (attempting to read may or may not raise an exception @@ -711,6 +813,44 @@ class SSLIOStream(IOStream): return None return chunk +class PipeIOStream(BaseIOStream): + """Pipe-based IOStream implementation. + + The constructor takes an integer file descriptor (such as one returned + by `os.pipe`) rather than an open file object. + """ + def __init__(self, fd, *args, **kwargs): + self.fd = fd + _set_nonblocking(fd) + super(PipeIOStream, self).__init__(*args, **kwargs) + + def fileno(self): + return self.fd + + def close_fd(self): + os.close(self.fd) + + def write_to_fd(self, data): + return os.write(self.fd, data) + + def read_from_fd(self): + try: + chunk = os.read(self.fd, self.read_chunk_size) + except (IOError, OSError), e: + if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): + return None + elif e.args[0] == errno.EBADF: + # If the writing half of a pipe is closed, select will + # report it as readable but reads will fail with EBADF. + self.close() + return None + else: + raise + if not chunk: + self.close() + return None + return chunk + def _double_prefix(deque): """Grow by doubling, but don't split the second chunk just because the diff --git a/libs/tornado/locale.py b/libs/tornado/locale.py index 9f8ee7ea8217c1b6da85a1f10c8dcd05d757aa0d..918f0c415f7aa8a5454d3cc1ea19a62e04410bed 100755 --- a/libs/tornado/locale.py +++ b/libs/tornado/locale.py @@ -43,11 +43,11 @@ from __future__ import absolute_import, division, with_statement import csv import datetime -import logging import os import re from tornado import escape +from tornado.log import gen_log _default_locale = "en_US" _translations = {} @@ -118,7 +118,7 @@ def load_translations(directory): continue locale, extension = path.split(".") if not re.match("[a-z]+(_[A-Z]+)?$", locale): - logging.error("Unrecognized locale %r (path: %s)", locale, + gen_log.error("Unrecognized locale %r (path: %s)", locale, os.path.join(directory, path)) continue full_path = os.path.join(directory, path) @@ -142,13 +142,13 @@ def load_translations(directory): else: plural = "unknown" if plural not in ("plural", "singular", "unknown"): - logging.error("Unrecognized plural indicator %r in %s line %d", + gen_log.error("Unrecognized plural indicator %r in %s line %d", plural, path, i + 1) continue _translations[locale].setdefault(plural, {})[english] = translation f.close() _supported_locales = frozenset(_translations.keys() + [_default_locale]) - logging.debug("Supported locales: %s", sorted(_supported_locales)) + gen_log.debug("Supported locales: %s", sorted(_supported_locales)) def load_gettext_translations(directory, domain): @@ -184,11 +184,11 @@ def load_gettext_translations(directory, domain): _translations[lang] = gettext.translation(domain, directory, languages=[lang]) except Exception, e: - logging.error("Cannot load translation for '%s': %s", lang, str(e)) + gen_log.error("Cannot load translation for '%s': %s", lang, str(e)) continue _supported_locales = frozenset(_translations.keys() + [_default_locale]) _use_gettext = True - logging.debug("Supported locales: %s", sorted(_supported_locales)) + gen_log.debug("Supported locales: %s", sorted(_supported_locales)) def get_supported_locales(): diff --git a/libs/tornado/log.py b/libs/tornado/log.py new file mode 100755 index 0000000000000000000000000000000000000000..7a4a8332f29e67628c2dc3784314bc8529856bcc --- /dev/null +++ b/libs/tornado/log.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python +# +# Copyright 2012 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Logging support for Tornado. + +Tornado uses three logger streams: + +* ``tornado.access``: Per-request logging for Tornado's HTTP servers (and + potentially other servers in the future) +* ``tornado.application``: Logging of errors from application code (i.e. + uncaught exceptions from callbacks) +* ``tornado.general``: General-purpose logging, including any errors + or warnings from Tornado itself. + +These streams may be configured independently using the standard library's +`logging` module. For example, you may wish to send ``tornado.access`` logs +to a separate file for analysis. +""" +from __future__ import absolute_import, division, with_statement + +import logging +import sys +import time + +from tornado.escape import _unicode + +try: + import curses +except ImportError: + curses = None + +# Logger objects for internal tornado use +access_log = logging.getLogger("tornado.access") +app_log = logging.getLogger("tornado.application") +gen_log = logging.getLogger("tornado.general") + +def _stderr_supports_color(): + color = False + if curses and sys.stderr.isatty(): + try: + curses.setupterm() + if curses.tigetnum("colors") > 0: + color = True + except Exception: + pass + return color + + +class LogFormatter(logging.Formatter): + """Log formatter used in Tornado. + + Key features of this formatter are: + + * Color support when logging to a terminal that supports it. + * Timestamps on every log line. + * Robust against str/bytes encoding problems. + + This formatter is enabled automatically by + `tornado.options.parse_command_line` (unless ``--logging=none`` is + used). + """ + def __init__(self, color=True, *args, **kwargs): + logging.Formatter.__init__(self, *args, **kwargs) + self._color = color and _stderr_supports_color() + if self._color: + # The curses module has some str/bytes confusion in + # python3. Until version 3.2.3, most methods return + # bytes, but only accept strings. In addition, we want to + # output these strings with the logging module, which + # works with unicode strings. The explicit calls to + # unicode() below are harmless in python2 but will do the + # right conversion in python 3. + fg_color = (curses.tigetstr("setaf") or + curses.tigetstr("setf") or "") + if (3, 0) < sys.version_info < (3, 2, 3): + fg_color = unicode(fg_color, "ascii") + self._colors = { + logging.DEBUG: unicode(curses.tparm(fg_color, 4), # Blue + "ascii"), + logging.INFO: unicode(curses.tparm(fg_color, 2), # Green + "ascii"), + logging.WARNING: unicode(curses.tparm(fg_color, 3), # Yellow + "ascii"), + logging.ERROR: unicode(curses.tparm(fg_color, 1), # Red + "ascii"), + } + self._normal = unicode(curses.tigetstr("sgr0"), "ascii") + + def format(self, record): + try: + record.message = record.getMessage() + except Exception, e: + record.message = "Bad message (%r): %r" % (e, record.__dict__) + assert isinstance(record.message, basestring) # guaranteed by logging + record.asctime = time.strftime( + "%y%m%d %H:%M:%S", self.converter(record.created)) + prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \ + record.__dict__ + if self._color: + prefix = (self._colors.get(record.levelno, self._normal) + + prefix + self._normal) + + # Encoding notes: The logging module prefers to work with character + # strings, but only enforces that log messages are instances of + # basestring. In python 2, non-ascii bytestrings will make + # their way through the logging framework until they blow up with + # an unhelpful decoding error (with this formatter it happens + # when we attach the prefix, but there are other opportunities for + # exceptions further along in the framework). + # + # If a byte string makes it this far, convert it to unicode to + # ensure it will make it out to the logs. Use repr() as a fallback + # to ensure that all byte strings can be converted successfully, + # but don't do it by default so we don't add extra quotes to ascii + # bytestrings. This is a bit of a hacky place to do this, but + # it's worth it since the encoding errors that would otherwise + # result are so useless (and tornado is fond of using utf8-encoded + # byte strings whereever possible). + try: + message = _unicode(record.message) + except UnicodeDecodeError: + message = repr(record.message) + + formatted = prefix + " " + message + if record.exc_info: + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + if record.exc_text: + formatted = formatted.rstrip() + "\n" + record.exc_text + return formatted.replace("\n", "\n ") + +def enable_pretty_logging(options=None): + """Turns on formatted logging output as configured. + + This is called automaticaly by `tornado.options.parse_command_line` + and `tornado.options.parse_config_file`. + """ + if options is None: + from tornado.options import options + if options.logging == 'none': + return + root_logger = logging.getLogger() + root_logger.setLevel(getattr(logging, options.logging.upper())) + if options.log_file_prefix: + channel = logging.handlers.RotatingFileHandler( + filename=options.log_file_prefix, + maxBytes=options.log_file_max_size, + backupCount=options.log_file_num_backups) + channel.setFormatter(LogFormatter(color=False)) + root_logger.addHandler(channel) + + if (options.log_to_stderr or + (options.log_to_stderr is None and not root_logger.handlers)): + # Set up color if we are in a tty and curses is installed + channel = logging.StreamHandler() + channel.setFormatter(LogFormatter()) + root_logger.addHandler(channel) + + +def define_logging_options(options=None): + if options is None: + # late import to prevent cycle + from tornado.options import options + options.define("logging", default="info", + help=("Set the Python log level. If 'none', tornado won't touch the " + "logging configuration."), + metavar="debug|info|warning|error|none") + options.define("log_to_stderr", type=bool, default=None, + help=("Send log output to stderr (colorized if possible). " + "By default use stderr if --log_file_prefix is not set and " + "no other logging is configured.")) + options.define("log_file_prefix", type=str, default=None, metavar="PATH", + help=("Path prefix for log files. " + "Note that if you are running multiple tornado processes, " + "log_file_prefix must be different for each of them (e.g. " + "include the port number)")) + options.define("log_file_max_size", type=int, default=100 * 1000 * 1000, + help="max size of log files before rollover") + options.define("log_file_num_backups", type=int, default=10, + help="number of log files to keep") + + options.add_parse_callback(enable_pretty_logging) diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py index 1f3f2e5664ebb9965b1be67bb742b0e52e8cf6f3..291dbecd9201dc61e30ed79813c8e7578c339765 100755 --- a/libs/tornado/netutil.py +++ b/libs/tornado/netutil.py @@ -19,14 +19,15 @@ from __future__ import absolute_import, division, with_statement import errno -import logging import os import socket import stat from tornado import process +from tornado.concurrent import dummy_executor, run_on_executor from tornado.ioloop import IOLoop from tornado.iostream import IOStream, SSLIOStream +from tornado.log import app_log from tornado.platform.auto import set_close_exec try: @@ -234,10 +235,10 @@ class TCPServer(object): stream = IOStream(connection, io_loop=self.io_loop) self.handle_stream(stream, address) except Exception: - logging.error("Error in connection callback", exc_info=True) + app_log.error("Error in connection callback", exc_info=True) -def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128): +def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None): """Creates listening sockets bound to the given port and address. Returns a list of socket objects (multiple sockets are returned if @@ -253,11 +254,15 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128): The ``backlog`` argument has the same meaning as for ``socket.listen()``. + + ``flags`` is a bitmask of AI_* flags to ``getaddrinfo``, like + ``socket.AI_PASSIVE | socket.AI_NUMERICHOST``. """ sockets = [] if address == "": address = None - flags = socket.AI_PASSIVE + if flags is None: + flags = socket.AI_PASSIVE for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, 0, flags)): af, socktype, proto, canonname, sockaddr = res @@ -335,3 +340,13 @@ def add_accept_handler(sock, callback, io_loop=None): raise callback(connection, address) io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ) + + +class Resolver(object): + def __init__(self, io_loop=None, executor=None): + self.io_loop = io_loop or IOLoop.instance() + self.executor = executor or dummy_executor + + @run_on_executor + def getaddrinfo(self, *args, **kwargs): + return socket.getaddrinfo(*args, **kwargs) diff --git a/libs/tornado/options.py b/libs/tornado/options.py index a3d74d68d5d2caaf50a4f52239679e88bee4d897..ac1f07c19cbf6f094842e8b55334efe6d9888993 100755 --- a/libs/tornado/options.py +++ b/libs/tornado/options.py @@ -16,7 +16,8 @@ """A command line parsing module that lets modules define their own options. -Each module defines its own options, e.g.:: +Each module defines its own options which are added to the global +option namespace, e.g.:: from tornado.options import define, options @@ -30,12 +31,15 @@ Each module defines its own options, e.g.:: The main() method of your application does not need to be aware of all of the options used throughout your program; they are all automatically loaded -when the modules are loaded. Your main() method can parse the command line -or parse a config file with:: +when the modules are loaded. However, all modules that define options +must have been imported before the command line is parsed. + +Your main() method can parse the command line or parse a config file with +either:: - import tornado.options - tornado.options.parse_config_file("/etc/server.conf") tornado.options.parse_command_line() + # or + tornado.options.parse_config_file("/etc/server.conf") Command line formats are what you would expect ("--myoption=myvalue"). Config files are just Python files. Global names become options, e.g.:: @@ -46,26 +50,24 @@ Config files are just Python files. Global names become options, e.g.:: We support datetimes, timedeltas, ints, and floats (just pass a 'type' kwarg to define). We also accept multi-value options. See the documentation for define() below. + +`tornado.options.options` is a singleton instance of `OptionParser`, and +the top-level functions in this module (`define`, `parse_command_line`, etc) +simply call methods on it. You may create additional `OptionParser` +instances to define isolated sets of options, such as for subcommands. """ from __future__ import absolute_import, division, with_statement import datetime -import logging -import logging.handlers import re import sys import os -import time import textwrap from tornado.escape import _unicode - -# For pretty log messages, if available -try: - import curses -except ImportError: - curses = None +from tornado.log import define_logging_options +from tornado import stack_context class Error(Exception): @@ -73,27 +75,68 @@ class Error(Exception): pass -class _Options(dict): +class OptionParser(object): """A collection of options, a dictionary with object-like access. Normally accessed via static functions in the `tornado.options` module, which reference a global instance. """ + def __init__(self): + # we have to use self.__dict__ because we override setattr. + self.__dict__['_options'] = {} + self.__dict__['_parse_callbacks'] = [] + self.define("help", type=bool, help="show this help information", + callback=self._help_callback) + def __getattr__(self, name): - if isinstance(self.get(name), _Option): - return self[name].value() + if isinstance(self._options.get(name), _Option): + return self._options[name].value() raise AttributeError("Unrecognized option %r" % name) def __setattr__(self, name, value): - if isinstance(self.get(name), _Option): - return self[name].set(value) + if isinstance(self._options.get(name), _Option): + return self._options[name].set(value) raise AttributeError("Unrecognized option %r" % name) def define(self, name, default=None, type=None, help=None, metavar=None, - multiple=False, group=None): - if name in self: + multiple=False, group=None, callback=None): + """Defines a new command line option. + + If type is given (one of str, float, int, datetime, or timedelta) + or can be inferred from the default, we parse the command line + arguments based on the given type. If multiple is True, we accept + comma-separated values, and the option value is always a list. + + For multi-value integers, we also accept the syntax x:y, which + turns into range(x, y) - very useful for long integer ranges. + + help and metavar are used to construct the automatically generated + command line help string. The help message is formatted like:: + + --name=METAVAR help string + + group is used to group the defined options in logical + groups. By default, command line options are grouped by the + file in which they are defined. + + Command line option names must be unique globally. They can be parsed + from the command line with parse_command_line() or parsed from a + config file with parse_config_file. + + If a callback is given, it will be run with the new value whenever + the option is changed. This can be used to combine command-line + and file-based options:: + + define("config", type=str, help="path to config file", + callback=lambda path: parse_config_file(path, final=False)) + + With this definition, options in the file specified by ``--config`` will + override options set earlier on the command line, but can be overridden + by later flags. + """ + if name in self._options: raise Error("Option %r already defined in %s", name, - self[name].file_name) + self._options[name].file_name) frame = sys._getframe(0) options_file = frame.f_code.co_filename file_name = frame.f_back.f_code.co_filename @@ -108,11 +151,23 @@ class _Options(dict): group_name = group else: group_name = file_name - self[name] = _Option(name, file_name=file_name, default=default, - type=type, help=help, metavar=metavar, - multiple=multiple, group_name=group_name) + self._options[name] = _Option(name, file_name=file_name, + default=default, type=type, help=help, + metavar=metavar, multiple=multiple, + group_name=group_name, + callback=callback) + + def parse_command_line(self, args=None, final=True): + """Parses all options given on the command line (defaults to sys.argv). + + Note that args[0] is ignored since it is the program name in sys.argv. - def parse_command_line(self, args=None): + We return a list of all arguments that are not parsed as options. + + If ``final`` is ``False``, parse callbacks will not be run. + This is useful for applications that wish to combine configurations + from multiple sources. + """ if args is None: args = sys.argv remaining = [] @@ -127,40 +182,46 @@ class _Options(dict): arg = args[i].lstrip("-") name, equals, value = arg.partition("=") name = name.replace('-', '_') - if not name in self: - print_help() + if not name in self._options: + self.print_help() raise Error('Unrecognized command line option: %r' % name) - option = self[name] + option = self._options[name] if not equals: if option.type == bool: value = "true" else: raise Error('Option %r requires a value' % name) option.parse(value) - if self.help: - print_help() - sys.exit(0) - # Set up log level and pretty console logging by default - if self.logging != 'none': - logging.getLogger().setLevel(getattr(logging, self.logging.upper())) - enable_pretty_logging() + if final: + self.run_parse_callbacks() return remaining - def parse_config_file(self, path): + def parse_config_file(self, path, final=True): + """Parses and loads the Python config file at the given path. + + If ``final`` is ``False``, parse callbacks will not be run. + This is useful for applications that wish to combine configurations + from multiple sources. + """ config = {} execfile(path, config, config) for name in config: - if name in self: - self[name].set(config[name]) + if name in self._options: + self._options[name].set(config[name]) - def print_help(self, file=sys.stdout): - """Prints all the command line options to stdout.""" + if final: + self.run_parse_callbacks() + + def print_help(self, file=None): + """Prints all the command line options to stderr (or another file).""" + if file is None: + file = sys.stderr print >> file, "Usage: %s [OPTIONS]" % sys.argv[0] print >> file, "\nOptions:\n" by_group = {} - for option in self.itervalues(): + for option in self._options.itervalues(): by_group.setdefault(option.group_name, []).append(option) for filename, o in sorted(by_group.items()): @@ -182,10 +243,67 @@ class _Options(dict): print >> file, "%-34s %s" % (' ', line) print >> file + def _help_callback(self, value): + if value: + self.print_help() + sys.exit(0) + + def add_parse_callback(self, callback): + """Adds a parse callback, to be invoked when option parsing is done.""" + self._parse_callbacks.append(stack_context.wrap(callback)) + + def run_parse_callbacks(self): + for callback in self._parse_callbacks: + callback() + + def mockable(self): + """Returns a wrapper around self that is compatible with `mock.patch`. + + The `mock.patch` function (included in the standard library + `unittest.mock` package since Python 3.3, or in the + third-party `mock` package for older versions of Python) is + incompatible with objects like ``options`` that override + ``__getattr__`` and ``__setattr__``. This function returns an + object that can be used with `mock.patch.object` to modify + option values:: + + with mock.patch.object(options.mockable(), 'name', value): + assert options.name == value + """ + return _Mockable(self) + +class _Mockable(object): + """`mock.patch` compatible wrapper for `OptionParser`. + + As of ``mock`` version 1.0.1, when an object uses ``__getattr__`` + hooks instead of ``__dict__``, ``patch.__exit__`` tries to delete + the attribute it set instead of setting a new one (assuming that + the object does not catpure ``__setattr__``, so the patch + created a new attribute in ``__dict__``). + + _Mockable's getattr and setattr pass through to the underlying + OptionParser, and delattr undoes the effect of a previous setattr. + """ + def __init__(self, options): + # Modify __dict__ directly to bypass __setattr__ + self.__dict__['_options'] = options + self.__dict__['_originals'] = {} + + def __getattr__(self, name): + return getattr(self._options, name) + + def __setattr__(self, name, value): + assert name not in self._originals, "don't reuse mockable objects" + self._originals[name] = getattr(self._options, name) + setattr(self._options, name, value) + + def __delattr__(self, name): + setattr(self._options, name, self._originals.pop(name)) class _Option(object): - def __init__(self, name, default=None, type=basestring, help=None, metavar=None, - multiple=False, file_name=None, group_name=None): + def __init__(self, name, default=None, type=basestring, help=None, + metavar=None, multiple=False, file_name=None, group_name=None, + callback=None): if default is None and multiple: default = [] self.name = name @@ -195,6 +313,7 @@ class _Option(object): self.multiple = multiple self.file_name = file_name self.group_name = group_name + self.callback = callback self.default = default self._value = None @@ -221,6 +340,8 @@ class _Option(object): self._value.append(_parse(part)) else: self._value = _parse(value) + if self.callback is not None: + self.callback(self._value) return self.value() def set(self, value): @@ -237,6 +358,8 @@ class _Option(object): raise Error("Option %r is required to be a %s (%s given)" % (self.name, self.type.__name__, type(value))) self._value = value + if self.callback is not None: + self.callback(self._value) # Supported date/time formats in our options _DATETIME_FORMATS = [ @@ -303,179 +426,54 @@ class _Option(object): return _unicode(value) -options = _Options() -"""Global options dictionary. +options = OptionParser() +"""Global options object. -Supports both attribute-style and dict-style access. +All defined options are available as attributes on this object. """ def define(name, default=None, type=None, help=None, metavar=None, - multiple=False, group=None): - """Defines a new command line option. - - If type is given (one of str, float, int, datetime, or timedelta) - or can be inferred from the default, we parse the command line - arguments based on the given type. If multiple is True, we accept - comma-separated values, and the option value is always a list. - - For multi-value integers, we also accept the syntax x:y, which - turns into range(x, y) - very useful for long integer ranges. + multiple=False, group=None, callback=None): + """Defines an option in the global namespace. - help and metavar are used to construct the automatically generated - command line help string. The help message is formatted like:: + See `OptionParser.define`. + """ + return options.define(name, default=default, type=type, help=help, + metavar=metavar, multiple=multiple, group=group, + callback=callback) - --name=METAVAR help string - group is used to group the defined options in logical groups. By default, - command line options are grouped by the defined file. +def parse_command_line(args=None, final=True): + """Parses global options from the command line. - Command line option names must be unique globally. They can be parsed - from the command line with parse_command_line() or parsed from a - config file with parse_config_file. + See `OptionParser.parse_command_line`. """ - return options.define(name, default=default, type=type, help=help, - metavar=metavar, multiple=multiple, group=group) - + return options.parse_command_line(args, final=final) -def parse_command_line(args=None): - """Parses all options given on the command line (defaults to sys.argv). - Note that args[0] is ignored since it is the program name in sys.argv. +def parse_config_file(path, final=True): + """Parses global options from a config file. - We return a list of all arguments that are not parsed as options. + See `OptionParser.parse_config_file`. """ - return options.parse_command_line(args) - + return options.parse_config_file(path, final=final) -def parse_config_file(path): - """Parses and loads the Python config file at the given path.""" - return options.parse_config_file(path) +def print_help(file=None): + """Prints all the command line options to stderr (or another file). -def print_help(file=sys.stdout): - """Prints all the command line options to stdout.""" + See `OptionParser.print_help`. + """ return options.print_help(file) +def add_parse_callback(callback): + """Adds a parse callback, to be invoked when option parsing is done. -def enable_pretty_logging(options=options): - """Turns on formatted logging output as configured. - - This is called automatically by `parse_command_line`. + See `OptionParser.add_parse_callback` """ - root_logger = logging.getLogger() - if options.log_file_prefix: - channel = logging.handlers.RotatingFileHandler( - filename=options.log_file_prefix, - maxBytes=options.log_file_max_size, - backupCount=options.log_file_num_backups) - channel.setFormatter(_LogFormatter(color=False)) - root_logger.addHandler(channel) - - if (options.log_to_stderr or - (options.log_to_stderr is None and not root_logger.handlers)): - # Set up color if we are in a tty and curses is installed - color = False - if curses and sys.stderr.isatty(): - try: - curses.setupterm() - if curses.tigetnum("colors") > 0: - color = True - except Exception: - pass - channel = logging.StreamHandler() - channel.setFormatter(_LogFormatter(color=color)) - root_logger.addHandler(channel) - - -class _LogFormatter(logging.Formatter): - def __init__(self, color, *args, **kwargs): - logging.Formatter.__init__(self, *args, **kwargs) - self._color = color - if color: - # The curses module has some str/bytes confusion in - # python3. Until version 3.2.3, most methods return - # bytes, but only accept strings. In addition, we want to - # output these strings with the logging module, which - # works with unicode strings. The explicit calls to - # unicode() below are harmless in python2 but will do the - # right conversion in python 3. - fg_color = (curses.tigetstr("setaf") or - curses.tigetstr("setf") or "") - if (3, 0) < sys.version_info < (3, 2, 3): - fg_color = unicode(fg_color, "ascii") - self._colors = { - logging.DEBUG: unicode(curses.tparm(fg_color, 4), # Blue - "ascii"), - logging.INFO: unicode(curses.tparm(fg_color, 2), # Green - "ascii"), - logging.WARNING: unicode(curses.tparm(fg_color, 3), # Yellow - "ascii"), - logging.ERROR: unicode(curses.tparm(fg_color, 1), # Red - "ascii"), - } - self._normal = unicode(curses.tigetstr("sgr0"), "ascii") - - def format(self, record): - try: - record.message = record.getMessage() - except Exception, e: - record.message = "Bad message (%r): %r" % (e, record.__dict__) - assert isinstance(record.message, basestring) # guaranteed by logging - record.asctime = time.strftime( - "%y%m%d %H:%M:%S", self.converter(record.created)) - prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \ - record.__dict__ - if self._color: - prefix = (self._colors.get(record.levelno, self._normal) + - prefix + self._normal) - - # Encoding notes: The logging module prefers to work with character - # strings, but only enforces that log messages are instances of - # basestring. In python 2, non-ascii bytestrings will make - # their way through the logging framework until they blow up with - # an unhelpful decoding error (with this formatter it happens - # when we attach the prefix, but there are other opportunities for - # exceptions further along in the framework). - # - # If a byte string makes it this far, convert it to unicode to - # ensure it will make it out to the logs. Use repr() as a fallback - # to ensure that all byte strings can be converted successfully, - # but don't do it by default so we don't add extra quotes to ascii - # bytestrings. This is a bit of a hacky place to do this, but - # it's worth it since the encoding errors that would otherwise - # result are so useless (and tornado is fond of using utf8-encoded - # byte strings whereever possible). - try: - message = _unicode(record.message) - except UnicodeDecodeError: - message = repr(record.message) - - formatted = prefix + " " + message - if record.exc_info: - if not record.exc_text: - record.exc_text = self.formatException(record.exc_info) - if record.exc_text: - formatted = formatted.rstrip() + "\n" + record.exc_text - return formatted.replace("\n", "\n ") + options.add_parse_callback(callback) # Default options -define("help", type=bool, help="show this help information") -define("logging", default="info", - help=("Set the Python log level. If 'none', tornado won't touch the " - "logging configuration."), - metavar="debug|info|warning|error|none") -define("log_to_stderr", type=bool, default=None, - help=("Send log output to stderr (colorized if possible). " - "By default use stderr if --log_file_prefix is not set and " - "no other logging is configured.")) -define("log_file_prefix", type=str, default=None, metavar="PATH", - help=("Path prefix for log files. " - "Note that if you are running multiple tornado processes, " - "log_file_prefix must be different for each of them (e.g. " - "include the port number)")) -define("log_file_max_size", type=int, default=100 * 1000 * 1000, - help="max size of log files before rollover") -define("log_file_num_backups", type=int, default=10, - help="number of log files to keep") +define_logging_options(options) diff --git a/libs/tornado/platform/auto.py b/libs/tornado/platform/auto.py index 7bfec116d738b0a09f52134a49f4bd3c181bb74c..7199cb5ce5fa0f709e069b9f266b06ff9660bc27 100755 --- a/libs/tornado/platform/auto.py +++ b/libs/tornado/platform/auto.py @@ -32,3 +32,14 @@ if os.name == 'nt': from tornado.platform.windows import set_close_exec else: from tornado.platform.posix import set_close_exec, Waker + +try: + # monotime monkey-patches the time module to have a monotonic function + # in versions of python before 3.3. + import monotime +except ImportError: + pass +try: + from time import monotonic as monotonic_time +except ImportError: + monotonic_time = None diff --git a/libs/tornado/platform/common.py b/libs/tornado/platform/common.py index 176ce2e5292a94cabf57b52c05f000f275c9714c..39f60bd7989bc244911ef5aaf133a69df5a8b12f 100755 --- a/libs/tornado/platform/common.py +++ b/libs/tornado/platform/common.py @@ -69,6 +69,9 @@ class Waker(interface.Waker): def fileno(self): return self.reader.fileno() + def write_fileno(self): + return self.writer.fileno() + def wake(self): try: self.writer.send(b("x")) diff --git a/libs/tornado/platform/epoll.py b/libs/tornado/platform/epoll.py new file mode 100755 index 0000000000000000000000000000000000000000..fa5b68eda95e73a07b662e53b0582aeea32c2095 --- /dev/null +++ b/libs/tornado/platform/epoll.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# +# Copyright 2012 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""EPoll-based IOLoop implementation for Linux systems. + +Supports the standard library's `select.epoll` function for Python 2.6+, +and our own C module for Python 2.5. +""" +from __future__ import absolute_import, division, with_statement + +import os +import select + +from tornado.ioloop import PollIOLoop + +if hasattr(select, 'epoll'): + # Python 2.6+ + class EPollIOLoop(PollIOLoop): + def initialize(self, **kwargs): + super(EPollIOLoop, self).initialize(impl=select.epoll(), **kwargs) +else: + # Python 2.5 + from tornado import epoll + + class _EPoll(object): + """An epoll-based event loop using our C module for Python 2.5 systems""" + _EPOLL_CTL_ADD = 1 + _EPOLL_CTL_DEL = 2 + _EPOLL_CTL_MOD = 3 + + def __init__(self): + self._epoll_fd = epoll.epoll_create() + + def fileno(self): + return self._epoll_fd + + def close(self): + os.close(self._epoll_fd) + + def register(self, fd, events): + epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_ADD, fd, events) + + def modify(self, fd, events): + epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_MOD, fd, events) + + def unregister(self, fd): + epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_DEL, fd, 0) + + def poll(self, timeout): + return epoll.epoll_wait(self._epoll_fd, int(timeout * 1000)) + + + class EPollIOLoop(PollIOLoop): + def initialize(self, **kwargs): + super(EPollIOLoop, self).initialize(impl=_EPoll(), **kwargs) + diff --git a/libs/tornado/platform/interface.py b/libs/tornado/platform/interface.py index 21e72cd9eb7dc9e38c2bebec1c3f6b5cc8c729a7..5006f30b8fc0a5bfdffea00277ed081e8349075d 100755 --- a/libs/tornado/platform/interface.py +++ b/libs/tornado/platform/interface.py @@ -39,13 +39,17 @@ class Waker(object): the ``IOLoop`` is closed, it closes its waker too. """ def fileno(self): - """Returns a file descriptor for this waker. + """Returns the read file descriptor for this waker. Must be suitable for use with ``select()`` or equivalent on the local platform. """ raise NotImplementedError() + def write_fileno(self): + """Returns the write file descriptor for this waker.""" + raise NotImplementedError() + def wake(self): """Triggers activity on the waker's file descriptor.""" raise NotImplementedError() diff --git a/libs/tornado/platform/kqueue.py b/libs/tornado/platform/kqueue.py new file mode 100755 index 0000000000000000000000000000000000000000..2f14c15c3742c6a68741aafa4b3c2826d73aa243 --- /dev/null +++ b/libs/tornado/platform/kqueue.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# Copyright 2012 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""KQueue-based IOLoop implementation for BSD/Mac systems.""" +from __future__ import absolute_import, division, with_statement + +import select + +from tornado.ioloop import IOLoop, PollIOLoop + +assert hasattr(select, 'kqueue'), 'kqueue not supported' + +class _KQueue(object): + """A kqueue-based event loop for BSD/Mac systems.""" + def __init__(self): + self._kqueue = select.kqueue() + self._active = {} + + def fileno(self): + return self._kqueue.fileno() + + def close(self): + self._kqueue.close() + + def register(self, fd, events): + if fd in self._active: + raise IOError("fd %d already registered" % fd) + self._control(fd, events, select.KQ_EV_ADD) + self._active[fd] = events + + def modify(self, fd, events): + self.unregister(fd) + self.register(fd, events) + + def unregister(self, fd): + events = self._active.pop(fd) + self._control(fd, events, select.KQ_EV_DELETE) + + def _control(self, fd, events, flags): + kevents = [] + if events & IOLoop.WRITE: + kevents.append(select.kevent( + fd, filter=select.KQ_FILTER_WRITE, flags=flags)) + if events & IOLoop.READ or not kevents: + # Always read when there is not a write + kevents.append(select.kevent( + fd, filter=select.KQ_FILTER_READ, flags=flags)) + # Even though control() takes a list, it seems to return EINVAL + # on Mac OS X (10.6) when there is more than one event in the list. + for kevent in kevents: + self._kqueue.control([kevent], 0) + + def poll(self, timeout): + kevents = self._kqueue.control(None, 1000, timeout) + events = {} + for kevent in kevents: + fd = kevent.ident + if kevent.filter == select.KQ_FILTER_READ: + events[fd] = events.get(fd, 0) | IOLoop.READ + if kevent.filter == select.KQ_FILTER_WRITE: + if kevent.flags & select.KQ_EV_EOF: + # If an asynchronous connection is refused, kqueue + # returns a write event with the EOF flag set. + # Turn this into an error for consistency with the + # other IOLoop implementations. + # Note that for read events, EOF may be returned before + # all data has been consumed from the socket buffer, + # so we only check for EOF on write events. + events[fd] = IOLoop.ERROR + else: + events[fd] = events.get(fd, 0) | IOLoop.WRITE + if kevent.flags & select.KQ_EV_ERROR: + events[fd] = events.get(fd, 0) | IOLoop.ERROR + return events.items() + + +class KQueueIOLoop(PollIOLoop): + def initialize(self, **kwargs): + super(KQueueIOLoop, self).initialize(impl=_KQueue(), **kwargs) diff --git a/libs/tornado/platform/posix.py b/libs/tornado/platform/posix.py index 8d674c0e234b42a393c2ef5231a38474d9628cf0..487f97a9a7fc400e83dfd1fb793c59db4a597892 100755 --- a/libs/tornado/platform/posix.py +++ b/libs/tornado/platform/posix.py @@ -48,6 +48,9 @@ class Waker(interface.Waker): def fileno(self): return self.reader.fileno() + def write_fileno(self): + return self.writer.fileno() + def wake(self): try: self.writer.write(b("x")) diff --git a/libs/tornado/platform/select.py b/libs/tornado/platform/select.py new file mode 100755 index 0000000000000000000000000000000000000000..51dc964e02365942e97e6756e50998c1e16ba157 --- /dev/null +++ b/libs/tornado/platform/select.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# +# Copyright 2012 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Select-based IOLoop implementation. + +Used as a fallback for systems that don't support epoll or kqueue. +""" +from __future__ import absolute_import, division, with_statement + +import select + +from tornado.ioloop import IOLoop, PollIOLoop + +class _Select(object): + """A simple, select()-based IOLoop implementation for non-Linux systems""" + def __init__(self): + self.read_fds = set() + self.write_fds = set() + self.error_fds = set() + self.fd_sets = (self.read_fds, self.write_fds, self.error_fds) + + def close(self): + pass + + def register(self, fd, events): + if fd in self.read_fds or fd in self.write_fds or fd in self.error_fds: + raise IOError("fd %d already registered" % fd) + if events & IOLoop.READ: + self.read_fds.add(fd) + if events & IOLoop.WRITE: + self.write_fds.add(fd) + if events & IOLoop.ERROR: + self.error_fds.add(fd) + # Closed connections are reported as errors by epoll and kqueue, + # but as zero-byte reads by select, so when errors are requested + # we need to listen for both read and error. + self.read_fds.add(fd) + + def modify(self, fd, events): + self.unregister(fd) + self.register(fd, events) + + def unregister(self, fd): + self.read_fds.discard(fd) + self.write_fds.discard(fd) + self.error_fds.discard(fd) + + def poll(self, timeout): + readable, writeable, errors = select.select( + self.read_fds, self.write_fds, self.error_fds, timeout) + events = {} + for fd in readable: + events[fd] = events.get(fd, 0) | IOLoop.READ + for fd in writeable: + events[fd] = events.get(fd, 0) | IOLoop.WRITE + for fd in errors: + events[fd] = events.get(fd, 0) | IOLoop.ERROR + return events.items() + +class SelectIOLoop(PollIOLoop): + def initialize(self, **kwargs): + super(SelectIOLoop, self).initialize(impl=_Select(), **kwargs) + diff --git a/libs/tornado/platform/twisted.py b/libs/tornado/platform/twisted.py index 6474a4788b7800bef59420d667e682fa30d8a6b9..6c3cbf967a70228e5e24422067a22d57ae3e5239 100755 --- a/libs/tornado/platform/twisted.py +++ b/libs/tornado/platform/twisted.py @@ -16,19 +16,25 @@ # Note: This module's docs are not currently extracted automatically, # so changes must be made manually to twisted.rst # TODO: refactor doc build process to use an appropriate virtualenv -"""A Twisted reactor built on the Tornado IOLoop. +"""Bridges between the Twisted reactor and Tornado IOLoop. This module lets you run applications and libraries written for -Twisted in a Tornado application. To use it, simply call `install` at -the beginning of the application:: +Twisted in a Tornado application. It can be used in two modes, +depending on which library's underlying event loop you want to use. + +Twisted on Tornado +------------------ + +`TornadoReactor` implements the Twisted reactor interface on top of +the Tornado IOLoop. To use it, simply call `install` at the beginning +of the application:: import tornado.platform.twisted tornado.platform.twisted.install() from twisted.internet import reactor When the app is ready to start, call `IOLoop.instance().start()` -instead of `reactor.run()`. This will allow you to use a mixture of -Twisted and Tornado code in the same process. +instead of `reactor.run()`. It is also possible to create a non-global reactor by calling `tornado.platform.twisted.TornadoReactor(io_loop)`. However, if @@ -41,18 +47,32 @@ recommended to call:: before closing the `IOLoop`. -This module has been tested with Twisted versions 11.0.0, 11.1.0, and 12.0.0 +Tornado on Twisted +------------------ + +`TwistedIOLoop` implements the Tornado IOLoop interface on top of the Twisted +reactor. Recommended usage:: + + from tornado.platform.twisted import TwistedIOLoop + from twisted.internet import reactor + TwistedIOLoop().install() + # Set up your tornado application as usual using `IOLoop.instance` + reactor.run() + +`TwistedIOLoop` always uses the global Twisted reactor. + +This module has been tested with Twisted versions 11.0.0 and newer. """ from __future__ import absolute_import, division, with_statement import functools -import logging +import datetime import time from twisted.internet.posixbase import PosixReactorBase from twisted.internet.interfaces import \ - IReactorFDSet, IDelayedCall, IReactorTime + IReactorFDSet, IDelayedCall, IReactorTime, IReadDescriptor, IWriteDescriptor from twisted.python import failure, log from twisted.internet import error @@ -60,7 +80,8 @@ from zope.interface import implementer import tornado import tornado.ioloop -from tornado.stack_context import NullContext +from tornado.log import app_log +from tornado.stack_context import NullContext, wrap from tornado.ioloop import IOLoop @@ -80,7 +101,7 @@ class TornadoDelayedCall(object): try: self._func() except: - logging.error("_called caught exception", exc_info=True) + app_log.error("_called caught exception", exc_info=True) def getTime(self): return self._time @@ -127,6 +148,7 @@ class TornadoReactor(PosixReactorBase): self._fds = {} # a map of fd to a (reader, writer) tuple self._delayedCalls = {} PosixReactorBase.__init__(self) + self.addSystemEventTrigger('during', 'shutdown', self.crash) # IOLoop.start() bypasses some of the reactor initialization. # Fire off the necessary events if they weren't already triggered @@ -138,7 +160,7 @@ class TornadoReactor(PosixReactorBase): # IReactorTime def seconds(self): - return time.time() + return self._io_loop.time() def callLater(self, seconds, f, *args, **kw): dc = TornadoDelayedCall(self, seconds, f, *args, **kw) @@ -169,6 +191,8 @@ class TornadoReactor(PosixReactorBase): # IReactorFDSet def _invoke_callback(self, fd, events): + if fd not in self._fds: + return (reader, writer) = self._fds[fd] if reader: err = None @@ -280,7 +304,8 @@ class TornadoReactor(PosixReactorBase): # IOLoop.start() instead of Reactor.run(). def stop(self): PosixReactorBase.stop(self) - self._io_loop.stop() + fire_shutdown = functools.partial(self.fireSystemEvent, "shutdown") + self._io_loop.add_callback(fire_shutdown) def crash(self): PosixReactorBase.crash(self) @@ -291,8 +316,6 @@ class TornadoReactor(PosixReactorBase): def mainLoop(self): self._io_loop.start() - if self._stopped: - self.fireSystemEvent("shutdown") TornadoReactor = implementer(IReactorTime, IReactorFDSet)(TornadoReactor) @@ -328,3 +351,113 @@ def install(io_loop=None): from twisted.internet.main import installReactor installReactor(reactor) return reactor + +class _FD(object): + def __init__(self, fd, handler): + self.fd = fd + self.handler = handler + self.reading = False + self.writing = False + self.lost = False + + def fileno(self): + return self.fd + + def doRead(self): + if not self.lost: + self.handler(self.fd, tornado.ioloop.IOLoop.READ) + + def doWrite(self): + if not self.lost: + self.handler(self.fd, tornado.ioloop.IOLoop.WRITE) + + def connectionLost(self, reason): + if not self.lost: + self.handler(self.fd, tornado.ioloop.IOLoop.ERROR) + self.lost = True + + def logPrefix(self): + return '' +_FD = implementer(IReadDescriptor, IWriteDescriptor)(_FD) + +class TwistedIOLoop(tornado.ioloop.IOLoop): + """IOLoop implementation that runs on Twisted. + + Uses the global Twisted reactor. It is possible to create multiple + TwistedIOLoops in the same process, but it doesn't really make sense + because they will all run in the same thread. + + Not compatible with `tornado.process.Subprocess.set_exit_callback` + because the ``SIGCHLD`` handlers used by Tornado and Twisted conflict + with each other. + """ + def initialize(self): + from twisted.internet import reactor + self.reactor = reactor + self.fds = {} + + def close(self, all_fds=False): + self.reactor.removeAll() + for c in self.reactor.getDelayedCalls(): + c.cancel() + + def add_handler(self, fd, handler, events): + if fd in self.fds: + raise ValueError('fd %d added twice' % fd) + self.fds[fd] = _FD(fd, wrap(handler)) + if events | tornado.ioloop.IOLoop.READ: + self.fds[fd].reading = True + self.reactor.addReader(self.fds[fd]) + if events | tornado.ioloop.IOLoop.WRITE: + self.fds[fd].writing = True + self.reactor.addWriter(self.fds[fd]) + + def update_handler(self, fd, events): + if events | tornado.ioloop.IOLoop.READ: + if not self.fds[fd].reading: + self.fds[fd].reading = True + self.reactor.addReader(self.fds[fd]) + else: + if self.fds[fd].reading: + self.fds[fd].reading = False + self.reactor.removeReader(self.fds[fd]) + if events | tornado.ioloop.IOLoop.WRITE: + if not self.fds[fd].writing: + self.fds[fd].writing = True + self.reactor.addWriter(self.fds[fd]) + else: + if self.fds[fd].writing: + self.fds[fd].writing = False + self.reactor.removeWriter(self.fds[fd]) + + def remove_handler(self, fd): + self.fds[fd].lost = True + if self.fds[fd].reading: + self.reactor.removeReader(self.fds[fd]) + if self.fds[fd].writing: + self.reactor.removeWriter(self.fds[fd]) + del self.fds[fd] + + def start(self): + self.reactor.run() + + def stop(self): + self.reactor.crash() + + def add_timeout(self, deadline, callback): + if isinstance(deadline, (int, long, float)): + delay = max(deadline - self.time(), 0) + elif isinstance(deadline, datetime.timedelta): + delay = deadline.total_seconds() + else: + raise TypeError("Unsupported deadline %r") + return self.reactor.callLater(delay, wrap(callback)) + + def remove_timeout(self, timeout): + timeout.cancel() + + def add_callback(self, callback): + self.reactor.callFromThread(wrap(callback)) + + def add_callback_from_signal(self, callback): + self.add_callback(callback) diff --git a/libs/tornado/process.py b/libs/tornado/process.py index 28a61bcd3e9392b9bc07f577630cfafa60e14e70..9e048c19347dcf20192f79ff9164e68e0f13135f 100755 --- a/libs/tornado/process.py +++ b/libs/tornado/process.py @@ -19,14 +19,19 @@ from __future__ import absolute_import, division, with_statement import errno -import logging +import functools import os +import signal +import subprocess import sys import time from binascii import hexlify from tornado import ioloop +from tornado.iostream import PipeIOStream +from tornado.log import gen_log +from tornado import stack_context try: import multiprocessing # Python 2.6+ @@ -45,7 +50,7 @@ def cpu_count(): return os.sysconf("SC_NPROCESSORS_CONF") except ValueError: pass - logging.error("Could not detect number of processors; assuming 1") + gen_log.error("Could not detect number of processors; assuming 1") return 1 @@ -98,7 +103,7 @@ def fork_processes(num_processes, max_restarts=100): raise RuntimeError("Cannot run in multiple processes: IOLoop instance " "has already been initialized. You cannot call " "IOLoop.instance() before calling start_processes()") - logging.info("Starting %d processes", num_processes) + gen_log.info("Starting %d processes", num_processes) children = {} def start_child(i): @@ -128,13 +133,13 @@ def fork_processes(num_processes, max_restarts=100): continue id = children.pop(pid) if os.WIFSIGNALED(status): - logging.warning("child %d (pid %d) killed by signal %d, restarting", + gen_log.warning("child %d (pid %d) killed by signal %d, restarting", id, pid, os.WTERMSIG(status)) elif os.WEXITSTATUS(status) != 0: - logging.warning("child %d (pid %d) exited with status %d, restarting", + gen_log.warning("child %d (pid %d) exited with status %d, restarting", id, pid, os.WEXITSTATUS(status)) else: - logging.info("child %d (pid %d) exited normally", id, pid) + gen_log.info("child %d (pid %d) exited normally", id, pid) continue num_restarts += 1 if num_restarts > max_restarts: @@ -156,3 +161,122 @@ def task_id(): """ global _task_id return _task_id + +class Subprocess(object): + """Wraps ``subprocess.Popen`` with IOStream support. + + The constructor is the same as ``subprocess.Popen`` with the following + additions: + + * ``stdin``, ``stdout``, and ``stderr`` may have the value + `tornado.process.Subprocess.STREAM`, which will make the corresponding + attribute of the resulting Subprocess a `PipeIOStream`. + * A new keyword argument ``io_loop`` may be used to pass in an IOLoop. + """ + STREAM = object() + + _initialized = False + _waiting = {} + + def __init__(self, *args, **kwargs): + self.io_loop = kwargs.pop('io_loop', None) + to_close = [] + if kwargs.get('stdin') is Subprocess.STREAM: + in_r, in_w = os.pipe() + kwargs['stdin'] = in_r + to_close.append(in_r) + self.stdin = PipeIOStream(in_w, io_loop=self.io_loop) + if kwargs.get('stdout') is Subprocess.STREAM: + out_r, out_w = os.pipe() + kwargs['stdout'] = out_w + to_close.append(out_w) + self.stdout = PipeIOStream(out_r, io_loop=self.io_loop) + if kwargs.get('stderr') is Subprocess.STREAM: + err_r, err_w = os.pipe() + kwargs['stderr'] = err_w + to_close.append(err_w) + self.stdout = PipeIOStream(err_r, io_loop=self.io_loop) + self.proc = subprocess.Popen(*args, **kwargs) + for fd in to_close: + os.close(fd) + for attr in ['stdin', 'stdout', 'stderr', 'pid']: + if not hasattr(self, attr): # don't clobber streams set above + setattr(self, attr, getattr(self.proc, attr)) + self._exit_callback = None + self.returncode = None + + def set_exit_callback(self, callback): + """Runs ``callback`` when this process exits. + + The callback takes one argument, the return code of the process. + + This method uses a ``SIGCHILD`` handler, which is a global setting + and may conflict if you have other libraries trying to handle the + same signal. If you are using more than one ``IOLoop`` it may + be necessary to call `Subprocess.initialize` first to designate + one ``IOLoop`` to run the signal handlers. + + In many cases a close callback on the stdout or stderr streams + can be used as an alternative to an exit callback if the + signal handler is causing a problem. + """ + self._exit_callback = stack_context.wrap(callback) + Subprocess.initialize(self.io_loop) + Subprocess._waiting[self.pid] = self + Subprocess._try_cleanup_process(self.pid) + + @classmethod + def initialize(cls, io_loop=None): + """Initializes the ``SIGCHILD`` handler. + + The signal handler is run on an IOLoop to avoid locking issues. + Note that the IOLoop used for signal handling need not be the + same one used by individual Subprocess objects (as long as the + IOLoops are each running in separate threads). + """ + if cls._initialized: + return + if io_loop is None: + io_loop = ioloop.IOLoop.instance() + cls._old_sigchld = signal.signal( + signal.SIGCHLD, + lambda sig, frame: io_loop.add_callback_from_signal(cls._cleanup)) + cls._initialized = True + + @classmethod + def uninitialize(cls): + """Removes the ``SIGCHILD`` handler.""" + if not cls._initialized: + return + signal.signal(signal.SIGCHLD, cls._old_sigchld) + cls._initialized = False + + @classmethod + def _cleanup(cls): + for pid in cls._waiting.keys(): + cls._try_cleanup_process(pid) + + @classmethod + def _try_cleanup_process(cls, pid): + try: + ret_pid, status = os.waitpid(pid, os.WNOHANG) + except OSError, e: + if e.args[0] == errno.ECHILD: + return + if ret_pid == 0: + return + assert ret_pid == pid + subproc = cls._waiting.pop(pid) + subproc.io_loop.add_callback_from_signal( + functools.partial(subproc._set_returncode, status)) + + def _set_returncode(self, status): + if os.WIFSIGNALED(status): + self.returncode = -os.WTERMSIG(status) + else: + assert os.WIFEXITED(status) + self.returncode = os.WEXITSTATUS(status) + if self._exit_callback: + callback = self._exit_callback + self._exit_callback = None + callback(self.returncode) diff --git a/libs/tornado/simple_httpclient.py b/libs/tornado/simple_httpclient.py index c6e8a3a7ce0e0594d725cf5a09571651ab69b779..faff83c8f06eb35c4eddbb016a8a470c38a2ade9 100755 --- a/libs/tornado/simple_httpclient.py +++ b/libs/tornado/simple_httpclient.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, with_statement from tornado.escape import utf8, _unicode, native_str -from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main +from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy from tornado.httputil import HTTPHeaders from tornado.iostream import IOStream, SSLIOStream +from tornado.netutil import Resolver +from tornado.log import gen_log from tornado import stack_context from tornado.util import b, GzipDecompressor @@ -13,7 +15,6 @@ import collections import contextlib import copy import functools -import logging import os.path import re import socket @@ -39,17 +40,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): This class implements an HTTP 1.1 client on top of Tornado's IOStreams. It does not currently implement all applicable parts of the HTTP - specification, but it does enough to work with major web service APIs - (mostly tested against the Twitter API so far). - - This class has not been tested extensively in production and - should be considered somewhat experimental as of the release of - tornado 1.2. It is intended to become the default AsyncHTTPClient - implementation in a future release. It may either be used - directly, or to facilitate testing of this class with an existing - application, setting the environment variable - USE_SIMPLE_HTTPCLIENT=1 will cause this class to transparently - replace tornado.httpclient.AsyncHTTPClient. + specification, but it does enough to work with major web service APIs. Some features found in the curl-based AsyncHTTPClient are not yet supported. In particular, proxies are not supported, connections @@ -61,19 +52,18 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): """ def initialize(self, io_loop=None, max_clients=10, - max_simultaneous_connections=None, - hostname_mapping=None, max_buffer_size=104857600): + hostname_mapping=None, max_buffer_size=104857600, + resolver=None, defaults=None): """Creates a AsyncHTTPClient. Only a single AsyncHTTPClient instance exists per IOLoop in order to provide limitations on the number of pending connections. force_instance=True may be used to suppress this behavior. - max_clients is the number of concurrent requests that can be in - progress. max_simultaneous_connections has no effect and is accepted - only for compatibility with the curl-based AsyncHTTPClient. Note - that these arguments are only used when the client is first created, - and will be ignored when an existing client is reused. + max_clients is the number of concurrent requests that can be + in progress. Note that this arguments are only used when the + client is first created, and will be ignored when an existing + client is reused. hostname_mapping is a dictionary mapping hostnames to IP addresses. It can be used to make local DNS changes when modifying system-wide @@ -89,6 +79,10 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.active = {} self.hostname_mapping = hostname_mapping self.max_buffer_size = max_buffer_size + self.resolver = resolver or Resolver(io_loop=io_loop) + self.defaults = dict(HTTPRequest._DEFAULTS) + if defaults is not None: + self.defaults.update(defaults) def fetch(self, request, callback, **kwargs): if not isinstance(request, HTTPRequest): @@ -97,11 +91,12 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): # so make sure we don't modify the caller's object. This is also # where normal dicts get converted to HTTPHeaders objects. request.headers = HTTPHeaders(request.headers) + request = _RequestProxy(request, self.defaults) callback = stack_context.wrap(callback) self.queue.append((request, callback)) self._process_queue() if self.queue: - logging.debug("max_clients limit reached, request queued. " + gen_log.debug("max_clients limit reached, request queued. " "%d active, %d queued requests." % ( len(self.active), len(self.queue))) @@ -126,12 +121,13 @@ class _HTTPConnection(object): def __init__(self, io_loop, client, request, release_callback, final_callback, max_buffer_size): - self.start_time = time.time() + self.start_time = io_loop.time() self.io_loop = io_loop self.client = client self.request = request self.release_callback = release_callback self.final_callback = final_callback + self.max_buffer_size = max_buffer_size self.code = None self.headers = None self.chunks = None @@ -139,16 +135,16 @@ class _HTTPConnection(object): # Timeout handle returned by IOLoop.add_timeout self._timeout = None with stack_context.StackContext(self.cleanup): - parsed = urlparse.urlsplit(_unicode(self.request.url)) - if ssl is None and parsed.scheme == "https": + self.parsed = urlparse.urlsplit(_unicode(self.request.url)) + if ssl is None and self.parsed.scheme == "https": raise ValueError("HTTPS requires either python2.6+ or " "curl_httpclient") - if parsed.scheme not in ("http", "https"): + if self.parsed.scheme not in ("http", "https"): raise ValueError("Unsupported url scheme: %s" % self.request.url) # urlsplit results have hostname and port results, but they # didn't support ipv6 literals until python 2.7. - netloc = parsed.netloc + netloc = self.parsed.netloc if "@" in netloc: userpass, _, netloc = netloc.rpartition("@") match = re.match(r'^(.+):(\d+)$', netloc) @@ -157,11 +153,11 @@ class _HTTPConnection(object): port = int(match.group(2)) else: host = netloc - port = 443 if parsed.scheme == "https" else 80 + port = 443 if self.parsed.scheme == "https" else 80 if re.match(r'^\[.*\]$', host): # raw ipv6 addresses in urls are enclosed in brackets host = host[1:-1] - parsed_hostname = host # save final parsed host for _on_connect + self.parsed_hostname = host # save final host for _on_connect if self.client.hostname_mapping is not None: host = self.client.hostname_mapping.get(host, host) @@ -172,66 +168,67 @@ class _HTTPConnection(object): # so restrict to ipv4 by default. af = socket.AF_INET - addrinfo = socket.getaddrinfo(host, port, af, socket.SOCK_STREAM, - 0, 0) - af, socktype, proto, canonname, sockaddr = addrinfo[0] - - if parsed.scheme == "https": - ssl_options = {} - if request.validate_cert: - ssl_options["cert_reqs"] = ssl.CERT_REQUIRED - if request.ca_certs is not None: - ssl_options["ca_certs"] = request.ca_certs - else: - ssl_options["ca_certs"] = _DEFAULT_CA_CERTS - if request.client_key is not None: - ssl_options["keyfile"] = request.client_key - if request.client_cert is not None: - ssl_options["certfile"] = request.client_cert - - # SSL interoperability is tricky. We want to disable - # SSLv2 for security reasons; it wasn't disabled by default - # until openssl 1.0. The best way to do this is to use - # the SSL_OP_NO_SSLv2, but that wasn't exposed to python - # until 3.2. Python 2.7 adds the ciphers argument, which - # can also be used to disable SSLv2. As a last resort - # on python 2.6, we set ssl_version to SSLv3. This is - # more narrow than we'd like since it also breaks - # compatibility with servers configured for TLSv1 only, - # but nearly all servers support SSLv3: - # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html - if sys.version_info >= (2, 7): - ssl_options["ciphers"] = "DEFAULT:!SSLv2" - else: - # This is really only necessary for pre-1.0 versions - # of openssl, but python 2.6 doesn't expose version - # information. - ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3 - - self.stream = SSLIOStream(socket.socket(af, socktype, proto), - io_loop=self.io_loop, - ssl_options=ssl_options, - max_buffer_size=max_buffer_size) + self.client.resolver.getaddrinfo( + host, port, af, socket.SOCK_STREAM, 0, 0, + callback=self._on_resolve) + + def _on_resolve(self, future): + af, socktype, proto, canonname, sockaddr = future.result()[0] + + if self.parsed.scheme == "https": + ssl_options = {} + if self.request.validate_cert: + ssl_options["cert_reqs"] = ssl.CERT_REQUIRED + if self.request.ca_certs is not None: + ssl_options["ca_certs"] = self.request.ca_certs else: - self.stream = IOStream(socket.socket(af, socktype, proto), - io_loop=self.io_loop, - max_buffer_size=max_buffer_size) - timeout = min(request.connect_timeout, request.request_timeout) - if timeout: - self._timeout = self.io_loop.add_timeout( - self.start_time + timeout, - stack_context.wrap(self._on_timeout)) - self.stream.set_close_callback(self._on_close) - self.stream.connect(sockaddr, - functools.partial(self._on_connect, parsed, - parsed_hostname)) + ssl_options["ca_certs"] = _DEFAULT_CA_CERTS + if self.request.client_key is not None: + ssl_options["keyfile"] = self.request.client_key + if self.request.client_cert is not None: + ssl_options["certfile"] = self.request.client_cert + + # SSL interoperability is tricky. We want to disable + # SSLv2 for security reasons; it wasn't disabled by default + # until openssl 1.0. The best way to do this is to use + # the SSL_OP_NO_SSLv2, but that wasn't exposed to python + # until 3.2. Python 2.7 adds the ciphers argument, which + # can also be used to disable SSLv2. As a last resort + # on python 2.6, we set ssl_version to SSLv3. This is + # more narrow than we'd like since it also breaks + # compatibility with servers configured for TLSv1 only, + # but nearly all servers support SSLv3: + # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html + if sys.version_info >= (2, 7): + ssl_options["ciphers"] = "DEFAULT:!SSLv2" + else: + # This is really only necessary for pre-1.0 versions + # of openssl, but python 2.6 doesn't expose version + # information. + ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3 + + self.stream = SSLIOStream(socket.socket(af, socktype, proto), + io_loop=self.io_loop, + ssl_options=ssl_options, + max_buffer_size=self.max_buffer_size) + else: + self.stream = IOStream(socket.socket(af, socktype, proto), + io_loop=self.io_loop, + max_buffer_size=self.max_buffer_size) + timeout = min(self.request.connect_timeout, self.request.request_timeout) + if timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + timeout, + stack_context.wrap(self._on_timeout)) + self.stream.set_close_callback(self._on_close) + self.stream.connect(sockaddr, self._on_connect) def _on_timeout(self): self._timeout = None if self.final_callback is not None: raise HTTPError(599, "Timeout") - def _on_connect(self, parsed, parsed_hostname): + def _on_connect(self): if self._timeout is not None: self.io_loop.remove_timeout(self._timeout) self._timeout = None @@ -243,10 +240,10 @@ class _HTTPConnection(object): isinstance(self.stream, SSLIOStream)): match_hostname(self.stream.socket.getpeercert(), # ipv6 addresses are broken (in - # parsed.hostname) until 2.7, here is + # self.parsed.hostname) until 2.7, here is # correctly parsed value calculated in # __init__ - parsed_hostname) + self.parsed_hostname) if (self.request.method not in self._SUPPORTED_METHODS and not self.request.allow_nonstandard_methods): raise KeyError("unknown method %s" % self.request.method) @@ -258,13 +255,13 @@ class _HTTPConnection(object): if "Connection" not in self.request.headers: self.request.headers["Connection"] = "close" if "Host" not in self.request.headers: - if '@' in parsed.netloc: - self.request.headers["Host"] = parsed.netloc.rpartition('@')[-1] + if '@' in self.parsed.netloc: + self.request.headers["Host"] = self.parsed.netloc.rpartition('@')[-1] else: - self.request.headers["Host"] = parsed.netloc + self.request.headers["Host"] = self.parsed.netloc username, password = None, None - if parsed.username is not None: - username, password = parsed.username, parsed.password + if self.parsed.username is not None: + username, password = self.parsed.username, self.parsed.password elif self.request.auth_username is not None: username = self.request.auth_username password = self.request.auth_password or '' @@ -287,8 +284,8 @@ class _HTTPConnection(object): self.request.headers["Content-Type"] = "application/x-www-form-urlencoded" if self.request.use_gzip: self.request.headers["Accept-Encoding"] = "gzip" - req_path = ((parsed.path or '/') + - (('?' + parsed.query) if parsed.query else '')) + req_path = ((self.parsed.path or '/') + + (('?' + self.parsed.query) if self.parsed.query else '')) request_lines = [utf8("%s %s HTTP/1.1" % (self.request.method, req_path))] for k, v in self.request.headers.get_all(): @@ -319,23 +316,32 @@ class _HTTPConnection(object): try: yield except Exception, e: - logging.warning("uncaught exception", exc_info=True) + gen_log.warning("uncaught exception", exc_info=True) self._run_callback(HTTPResponse(self.request, 599, error=e, - request_time=time.time() - self.start_time, + request_time=self.io_loop.time() - self.start_time, )) if hasattr(self, "stream"): self.stream.close() def _on_close(self): if self.final_callback is not None: - raise HTTPError(599, "Connection closed") + message = "Connection closed" + if self.stream.error: + message = str(self.stream.error) + raise HTTPError(599, message) def _on_headers(self, data): data = native_str(data.decode("latin1")) first_line, _, header_data = data.partition("\n") - match = re.match("HTTP/1.[01] ([0-9]+)", first_line) + match = re.match("HTTP/1.[01] ([0-9]+) ([^\r]*)", first_line) assert match - self.code = int(match.group(1)) + code = int(match.group(1)) + if 100 <= code < 200: + self.stream.read_until_regex(b("\r?\n\r?\n"), self._on_headers) + return + else: + self.code = code + self.reason = match.group(2) self.headers = HTTPHeaders.parse(header_data) if "Content-Length" in self.headers: @@ -353,15 +359,18 @@ class _HTTPConnection(object): content_length = None if self.request.header_callback is not None: + # re-attach the newline we split on earlier + self.request.header_callback(first_line + _) for k, v in self.headers.get_all(): self.request.header_callback("%s: %s\r\n" % (k, v)) + self.request.header_callback('\r\n') - if self.request.method == "HEAD": - # HEAD requests never have content, even though they may have - # content-length headers + if self.request.method == "HEAD" or self.code == 304: + # HEAD requests and 304 responses never have content, even + # though they may have content-length headers self._on_body(b("")) return - if 100 <= self.code < 200 or self.code in (204, 304): + if 100 <= self.code < 200 or self.code == 204: # These response codes never have bodies # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 if ("Transfer-Encoding" in self.headers or @@ -391,14 +400,20 @@ class _HTTPConnection(object): if (self.request.follow_redirects and self.request.max_redirects > 0 and self.code in (301, 302, 303, 307)): - new_request = copy.copy(self.request) + assert isinstance(self.request, _RequestProxy) + new_request = copy.copy(self.request.request) new_request.url = urlparse.urljoin(self.request.url, self.headers["Location"]) - new_request.max_redirects -= 1 + new_request.max_redirects = self.request.max_redirects - 1 del new_request.headers["Host"] # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 - # client SHOULD make a GET request - if self.code == 303: + # Client SHOULD make a GET request after a 303. + # According to the spec, 302 should be followed by the same + # method as the original request, but in practice browsers + # treat 302 the same as 303, and many servers use 302 for + # compatibility with pre-HTTP/1.1 user agents which don't + # understand the 303 status. + if self.code in (302, 303): new_request.method = "GET" new_request.body = None for h in ["Content-Length", "Content-Type", @@ -426,8 +441,9 @@ class _HTTPConnection(object): else: buffer = BytesIO(data) # TODO: don't require one big string? response = HTTPResponse(original_request, - self.code, headers=self.headers, - request_time=time.time() - self.start_time, + self.code, reason=self.reason, + headers=self.headers, + request_time=self.io_loop.time() - self.start_time, buffer=buffer, effective_url=self.request.url) self._run_callback(response) diff --git a/libs/tornado/stack_context.py b/libs/tornado/stack_context.py index afce681ba061d81cbb6967277f04d216e5b90a75..d4aec3c5eeb30f43ced1c2dce7a5a958139a12ec 100755 --- a/libs/tornado/stack_context.py +++ b/libs/tornado/stack_context.py @@ -70,7 +70,6 @@ from __future__ import absolute_import, division, with_statement import contextlib import functools -import itertools import operator import sys import threading @@ -161,6 +160,7 @@ class ExceptionStackContext(object): return self.exception_handler(type, value, traceback) finally: _state.contexts = self.old_contexts + self.old_contexts = None class NullContext(object): @@ -197,32 +197,15 @@ def wrap(fn): def wrapped(*args, **kwargs): callback, contexts, args = args[0], args[1], args[2:] - - if contexts is _state.contexts or not contexts: - callback(*args, **kwargs) - return - if not _state.contexts: - new_contexts = [cls(arg, active_cell) - for (cls, arg, active_cell) in contexts - if active_cell[0]] - # If we're moving down the stack, _state.contexts is a prefix - # of contexts. For each element of contexts not in that prefix, - # create a new StackContext object. - # If we're moving up the stack (or to an entirely different stack), - # _state.contexts will have elements not in contexts. Use - # NullContext to clear the state and then recreate from contexts. - elif (len(_state.contexts) > len(contexts) or - any(a[1] is not b[1] - for a, b in itertools.izip(_state.contexts, contexts))): - # contexts have been removed or changed, so start over - new_contexts = ([NullContext()] + - [cls(arg, active_cell) - for (cls, arg, active_cell) in contexts - if active_cell[0]]) + + if _state.contexts: + new_contexts = [NullContext()] else: - new_contexts = [cls(arg, active_cell) - for (cls, arg, active_cell) in contexts[len(_state.contexts):] - if active_cell[0]] + new_contexts = [] + if contexts: + new_contexts.extend(cls(arg, active_cell) + for (cls, arg, active_cell) in contexts + if active_cell[0]) if len(new_contexts) > 1: with _nested(*new_contexts): callback(*args, **kwargs) @@ -231,10 +214,7 @@ def wrap(fn): callback(*args, **kwargs) else: callback(*args, **kwargs) - if _state.contexts: - return _StackContextWrapper(wrapped, fn, _state.contexts) - else: - return _StackContextWrapper(fn) + return _StackContextWrapper(wrapped, fn, _state.contexts) @contextlib.contextmanager diff --git a/libs/tornado/template.py b/libs/tornado/template.py index 13eb7808131fc7e7ec465dd31e74118861d34c12..0cd8124e8e5c44a7fca487df99b7c1100f92ef15 100755 --- a/libs/tornado/template.py +++ b/libs/tornado/template.py @@ -184,13 +184,13 @@ from __future__ import absolute_import, division, with_statement import cStringIO import datetime import linecache -import logging import os.path import posixpath import re import threading from tornado import escape +from tornado.log import app_log from tornado.util import bytes_type, ObjectDict _DEFAULT_AUTOESCAPE = "xhtml_escape" @@ -203,6 +203,9 @@ class Template(object): We compile into Python from the given template_string. You can generate the template from variables with generate(). """ + # note that the constructor's signature is not extracted with + # autodoc because _UNSET looks like garbage. When changing + # this signature update website/sphinx/template.rst too. def __init__(self, template_string, name="<string>", loader=None, compress_whitespace=None, autoescape=_UNSET): self.name = name @@ -229,7 +232,7 @@ class Template(object): "exec") except Exception: formatted_code = _format_code(self.code).rstrip() - logging.error("%s code:\n%s", self.name, formatted_code) + app_log.error("%s code:\n%s", self.name, formatted_code) raise def generate(self, **kwargs): @@ -257,12 +260,7 @@ class Template(object): # we've generated a new template (mainly for this module's # unittests, where different tests reuse the same name). linecache.clearcache() - try: - return execute() - except Exception: - formatted_code = _format_code(self.code).rstrip() - logging.error("%s code:\n%s", self.name, formatted_code) - raise + return execute() def _generate_python(self, loader, compress_whitespace): buffer = cStringIO.StringIO() @@ -484,7 +482,7 @@ class _ApplyBlock(_Node): writer.write_line("_append = _buffer.append", self.line) self.body.generate(writer) writer.write_line("return _utf8('').join(_buffer)", self.line) - writer.write_line("_append(%s(%s()))" % ( + writer.write_line("_append(_utf8(%s(%s())))" % ( self.method, method_name), self.line) @@ -501,6 +499,8 @@ class _ControlBlock(_Node): writer.write_line("%s:" % self.statement, self.line) with writer.indent(): self.body.generate(writer) + # Just in case the body was empty + writer.write_line("pass", self.line) class _IntermediateControlBlock(_Node): @@ -509,6 +509,8 @@ class _IntermediateControlBlock(_Node): self.line = line def generate(self, writer): + # In case the previous block was empty + writer.write_line("pass", self.line) writer.write_line("%s:" % self.statement, self.line, writer.indent_size() - 1) diff --git a/libs/tornado/testing.py b/libs/tornado/testing.py index 42fec8e722867fff57943bab68c618b245dd2e6a..5945643366f7c8f7f844ec284ea774b435187846 100755 --- a/libs/tornado/testing.py +++ b/libs/tornado/testing.py @@ -26,34 +26,65 @@ try: from tornado.httpserver import HTTPServer from tornado.simple_httpclient import SimpleAsyncHTTPClient from tornado.ioloop import IOLoop + from tornado import netutil except ImportError: # These modules are not importable on app engine. Parts of this module # won't work, but e.g. LogTrapTestCase and main() will. AsyncHTTPClient = None HTTPServer = None IOLoop = None + netutil = None SimpleAsyncHTTPClient = None -from tornado.stack_context import StackContext, NullContext +from tornado.log import gen_log +from tornado.stack_context import StackContext from tornado.util import raise_exc_info import contextlib import logging import os +import re import signal +import socket import sys import time -import unittest + +# Tornado's own test suite requires the updated unittest module +# (either py27+ or unittest2) so tornado.test.util enforces +# this requirement, but for other users of tornado.testing we want +# to allow the older version if unitest2 is not available. +try: + import unittest2 as unittest +except ImportError: + import unittest _next_port = 10000 def get_unused_port(): - """Returns a (hopefully) unused port number.""" + """Returns a (hopefully) unused port number. + + This function does not guarantee that the port it returns is available, + only that a series of get_unused_port calls in a single process return + distinct ports. + + **Deprecated**. Use bind_unused_port instead, which is guaranteed + to find an unused port. + """ global _next_port port = _next_port _next_port = _next_port + 1 return port +def bind_unused_port(): + """Binds a server socket to an available port on localhost. + + Returns a tuple (socket, port). + """ + [sock] = netutil.bind_sockets(0, 'localhost', family=socket.AF_INET) + port = sock.getsockname()[1] + return sock, port + + class AsyncTestCase(unittest.TestCase): """TestCase subclass for testing IOLoop-based asynchronous code. @@ -116,8 +147,10 @@ class AsyncTestCase(unittest.TestCase): def setUp(self): super(AsyncTestCase, self).setUp() self.io_loop = self.get_new_ioloop() + self.io_loop.make_current() def tearDown(self): + self.io_loop.clear_current() if (not IOLoop.initialized() or self.io_loop is not IOLoop.instance()): # Try to clean up any file descriptors left open in the ioloop. @@ -189,14 +222,10 @@ class AsyncTestCase(unittest.TestCase): self.stop() if self.__timeout is not None: self.io_loop.remove_timeout(self.__timeout) - self.__timeout = self.io_loop.add_timeout(time.time() + timeout, timeout_func) + self.__timeout = self.io_loop.add_timeout(self.io_loop.time() + timeout, timeout_func) while True: self.__running = True - with NullContext(): - # Wipe out the StackContext that was established in - # self.run() so that all callbacks executed inside the - # IOLoop will re-run it. - self.io_loop.start() + self.io_loop.start() if (self.__failure is not None or condition is None or condition()): break @@ -233,12 +262,13 @@ class AsyncHTTPTestCase(AsyncTestCase): ''' def setUp(self): super(AsyncHTTPTestCase, self).setUp() - self.__port = None + sock, port = bind_unused_port() + self.__port = port self.http_client = self.get_http_client() self._app = self.get_app() self.http_server = self.get_http_server() - self.http_server.listen(self.get_http_port(), address="127.0.0.1") + self.http_server.add_sockets([sock]) def get_http_client(self): return AsyncHTTPClient(io_loop=self.io_loop) @@ -247,7 +277,6 @@ class AsyncHTTPTestCase(AsyncTestCase): return HTTPServer(self._app, io_loop=self.io_loop, **self.get_httpserver_options()) - def get_app(self): """Should be overridden by subclasses to return a tornado.web.Application or other HTTPServer callback. @@ -276,8 +305,6 @@ class AsyncHTTPTestCase(AsyncTestCase): A new port is chosen for each test. """ - if self.__port is None: - self.__port = get_unused_port() return self.__port def get_protocol(self): @@ -290,7 +317,9 @@ class AsyncHTTPTestCase(AsyncTestCase): def tearDown(self): self.http_server.stop() - self.http_client.close() + if (not IOLoop.initialized() or + self.http_client.io_loop is not IOLoop.instance()): + self.http_client.close() super(AsyncHTTPTestCase, self).tearDown() @@ -302,7 +331,8 @@ class AsyncHTTPSTestCase(AsyncHTTPTestCase): def get_http_client(self): # Some versions of libcurl have deadlock bugs with ssl, # so always run these tests with SimpleAsyncHTTPClient. - return SimpleAsyncHTTPClient(io_loop=self.io_loop, force_instance=True) + return SimpleAsyncHTTPClient(io_loop=self.io_loop, force_instance=True, + defaults=dict(validate_cert=False)) def get_httpserver_options(self): return dict(ssl_options=self.get_ssl_options()) @@ -322,10 +352,6 @@ class AsyncHTTPSTestCase(AsyncHTTPTestCase): def get_protocol(self): return 'https' - def fetch(self, path, **kwargs): - return AsyncHTTPTestCase.fetch(self, path, validate_cert=False, - **kwargs) - class LogTrapTestCase(unittest.TestCase): """A test case that captures and discards all logging output @@ -357,7 +383,7 @@ class LogTrapTestCase(unittest.TestCase): old_stream = handler.stream try: handler.stream = StringIO() - logging.info("RUNNING TEST: " + str(self)) + gen_log.info("RUNNING TEST: " + str(self)) old_error_count = len(result.failures) + len(result.errors) super(LogTrapTestCase, self).run(result) new_error_count = len(result.failures) + len(result.errors) @@ -367,6 +393,50 @@ class LogTrapTestCase(unittest.TestCase): handler.stream = old_stream +class ExpectLog(logging.Filter): + """Context manager to capture and suppress expected log output. + + Useful to make tests of error conditions less noisy, while still + leaving unexpected log entries visible. *Not thread safe.* + + Usage:: + + with ExpectLog('tornado.application', "Uncaught exception"): + error_response = self.fetch("/some_page") + """ + def __init__(self, logger, regex, required=True): + """Constructs an ExpectLog context manager. + + :param logger: Logger object (or name of logger) to watch. Pass + an empty string to watch the root logger. + :param regex: Regular expression to match. Any log entries on + the specified logger that match this regex will be suppressed. + :param required: If true, an exeption will be raised if the end of + the ``with`` statement is reached without matching any log entries. + """ + if isinstance(logger, basestring): + logger = logging.getLogger(logger) + self.logger = logger + self.regex = re.compile(regex) + self.required = required + self.matched = False + + def filter(self, record): + message = record.getMessage() + if self.regex.match(message): + self.matched = True + return False + return True + + def __enter__(self): + self.logger.addFilter(self) + + def __exit__(self, typ, value, tb): + self.logger.removeFilter(self) + if not typ and self.required and not self.matched: + raise Exception("did not get expected log message") + + def main(**kwargs): """A simple test runner. @@ -400,23 +470,35 @@ def main(**kwargs): """ from tornado.options import define, options, parse_command_line - define('autoreload', type=bool, default=False, - help="DEPRECATED: use tornado.autoreload.main instead") - define('httpclient', type=str, default=None) define('exception_on_interrupt', type=bool, default=True, help=("If true (default), ctrl-c raises a KeyboardInterrupt " "exception. This prints a stack trace but cannot interrupt " "certain operations. If false, the process is more reliably " "killed, but does not print a stack trace.")) - argv = [sys.argv[0]] + parse_command_line(sys.argv) - if options.httpclient: - from tornado.httpclient import AsyncHTTPClient - AsyncHTTPClient.configure(options.httpclient) + # support the same options as unittest's command-line interface + define('verbose', type=bool) + define('quiet', type=bool) + define('failfast', type=bool) + define('catch', type=bool) + define('buffer', type=bool) + + argv = [sys.argv[0]] + parse_command_line(sys.argv) if not options.exception_on_interrupt: signal.signal(signal.SIGINT, signal.SIG_DFL) + if options.verbose is not None: + kwargs['verbosity'] = 2 + if options.quiet is not None: + kwargs['verbosity'] = 0 + if options.failfast is not None: + kwargs['failfast'] = True + if options.catch is not None: + kwargs['catchbreak'] = True + if options.buffer is not None: + kwargs['buffer'] = True + if __name__ == '__main__' and len(argv) == 1: print >> sys.stderr, "No tests specified" sys.exit(1) @@ -433,14 +515,10 @@ def main(**kwargs): unittest.main(defaultTest="all", argv=argv, **kwargs) except SystemExit, e: if e.code == 0: - logging.info('PASS') + gen_log.info('PASS') else: - logging.error('FAIL') - if not options.autoreload: - raise - if options.autoreload: - import tornado.autoreload - tornado.autoreload.wait() + gen_log.error('FAIL') + raise if __name__ == '__main__': main() diff --git a/libs/tornado/util.py b/libs/tornado/util.py index 80cab89801bc76a188bcbf2fd54a6b3f4fb2b0da..f550449a0725adb6fb32e3d8ee99211caf459a0a 100755 --- a/libs/tornado/util.py +++ b/libs/tornado/util.py @@ -96,6 +96,102 @@ def raise_exc_info(exc_info): # After 2to3: raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) +class Configurable(object): + """Base class for configurable interfaces. + + A configurable interface is an (abstract) class whose constructor + acts as a factory function for one of its implementation subclasses. + The implementation subclass as well as optional keyword arguments to + its initializer can be set globally at runtime with `configure`. + + By using the constructor as the factory method, the interface looks like + a normal class, ``isinstance()`` works as usual, etc. This pattern + is most useful when the choice of implementation is likely to be a + global decision (e.g. when epoll is available, always use it instead of + select), or when a previously-monolithic class has been split into + specialized subclasses. + + Configurable subclasses must define the class methods + `configurable_base` and `configurable_default`, and use the instance + method `initialize` instead of `__init__`. + """ + __impl_class = None + __impl_kwargs = None + + def __new__(cls, **kwargs): + base = cls.configurable_base() + args = {} + if cls is base: + impl = cls.configured_class() + if base.__impl_kwargs: + args.update(base.__impl_kwargs) + else: + impl = cls + args.update(kwargs) + instance = super(Configurable, cls).__new__(impl) + # initialize vs __init__ chosen for compatiblity with AsyncHTTPClient + # singleton magic. If we get rid of that we can switch to __init__ + # here too. + instance.initialize(**args) + return instance + + @classmethod + def configurable_base(cls): + """Returns the base class of a configurable hierarchy. + + This will normally return the class in which it is defined. + (which is *not* necessarily the same as the cls classmethod parameter). + """ + raise NotImplementedError() + + @classmethod + def configurable_default(cls): + """Returns the implementation class to be used if none is configured.""" + raise NotImplementedError() + + def initialize(self): + """Initialize a `Configurable` subclass instance. + + Configurable classes should use `initialize` instead of `__init__`. + """ + + @classmethod + def configure(cls, impl, **kwargs): + """Sets the class to use when the base class is instantiated. + + Keyword arguments will be saved and added to the arguments passed + to the constructor. This can be used to set global defaults for + some parameters. + """ + base = cls.configurable_base() + if isinstance(impl, (unicode, bytes_type)): + impl = import_object(impl) + if impl is not None and not issubclass(impl, cls): + raise ValueError("Invalid subclass of %s" % cls) + base.__impl_class = impl + base.__impl_kwargs = kwargs + + @classmethod + def configured_class(cls): + """Returns the currently configured class.""" + base = cls.configurable_base() + if cls.__impl_class is None: + base.__impl_class = cls.configurable_default() + return base.__impl_class + + + @classmethod + def _save_configuration(cls): + base = cls.configurable_base() + return (base.__impl_class, base.__impl_kwargs) + + @classmethod + def _restore_configuration(cls, saved): + base = cls.configurable_base() + base.__impl_class = saved[0] + base.__impl_kwargs = saved[1] + + def doctests(): import doctest return doctest.DocTestSuite() diff --git a/libs/tornado/web.py b/libs/tornado/web.py index 99c6858d1a1b7320e6c367bccf14d49ddca7443b..7d45ce5384d9aadeec909040e76120346245efa7 100755 --- a/libs/tornado/web.py +++ b/libs/tornado/web.py @@ -63,7 +63,6 @@ import hashlib import hmac import httplib import itertools -import logging import mimetypes import os.path import re @@ -80,6 +79,7 @@ import uuid from tornado import escape from tornado import locale +from tornado.log import access_log, app_log, gen_log from tornado import stack_context from tornado import template from tornado.escape import utf8, _unicode @@ -105,12 +105,16 @@ class RequestHandler(object): _template_loader_lock = threading.Lock() def __init__(self, application, request, **kwargs): + super(RequestHandler, self).__init__() + self.application = application self.request = request self._headers_written = False self._finished = False self._auto_finish = True self._transforms = None # will be set in _execute + self.path_args = None + self.path_kwargs = None self.ui = ObjectDict((n, self._ui_method(m)) for n, m in application.ui_methods.iteritems()) # UIModules are available as both `modules` and `_modules` in the @@ -219,6 +223,8 @@ class RequestHandler(object): self._headers = { "Server": "TornadoServer/%s" % tornado.version, "Content-Type": "text/html; charset=UTF-8", + "Date": datetime.datetime.utcnow().strftime( + "%a, %d %b %Y %H:%M:%S GMT"), } self._list_headers = [] self.set_default_headers() @@ -227,6 +233,7 @@ class RequestHandler(object): self.set_header("Connection", "Keep-Alive") self._write_buffer = [] self._status_code = 200 + self._reason = httplib.responses[200] def set_default_headers(self): """Override this to set HTTP headers at the beginning of the request. @@ -238,10 +245,22 @@ class RequestHandler(object): """ pass - def set_status(self, status_code): - """Sets the status code for our response.""" - assert status_code in httplib.responses + def set_status(self, status_code, reason=None): + """Sets the status code for our response. + + :arg int status_code: Response status code. If `reason` is ``None``, + it must be present in `httplib.responses`. + :arg string reason: Human-readable reason phrase describing the status + code. If ``None``, it will be filled in from `httplib.responses`. + """ self._status_code = status_code + if reason is not None: + self._reason = escape.native_str(reason) + else: + try: + self._reason = httplib.responses[status_code] + except KeyError: + raise ValueError("unknown status code %d", status_code) def get_status(self): """Returns the status code for our response.""" @@ -600,7 +619,20 @@ class RequestHandler(object): else: loader = RequestHandler._template_loaders[template_path] t = loader.load(template_name) - args = dict( + namespace = self.get_template_namespace() + namespace.update(kwargs) + return t.generate(**namespace) + + def get_template_namespace(self): + """Returns a dictionary to be used as the default template namespace. + + May be overridden by subclasses to add or modify values. + + The results of this method will be combined with additional + defaults in the `tornado.template` module and keyword arguments + to `render` or `render_string`. + """ + namespace = dict( handler=self, request=self.request, current_user=self.current_user, @@ -610,11 +642,17 @@ class RequestHandler(object): xsrf_form_html=self.xsrf_form_html, reverse_url=self.reverse_url ) - args.update(self.ui) - args.update(kwargs) - return t.generate(**args) + namespace.update(self.ui) + return namespace def create_template_loader(self, template_path): + """Returns a new template loader for the given path. + + May be overridden by subclasses. By default returns a + directory-based loader on the given path, using the + ``autoescape`` application setting. If a ``template_loader`` + application setting is supplied, uses that instead. + """ settings = self.application.settings if "template_loader" in settings: return settings["template_loader"] @@ -715,16 +753,22 @@ class RequestHandler(object): Additional keyword arguments are passed through to `write_error`. """ if self._headers_written: - logging.error("Cannot send error response after headers written") + gen_log.error("Cannot send error response after headers written") if not self._finished: self.finish() return self.clear() - self.set_status(status_code) + + reason = None + if 'exc_info' in kwargs: + exception = kwargs['exc_info'][1] + if isinstance(exception, HTTPError) and exception.reason: + reason = exception.reason + self.set_status(status_code, reason=reason) try: self.write_error(status_code, **kwargs) except Exception: - logging.error("Uncaught exception in write_error", exc_info=True) + app_log.error("Uncaught exception in write_error", exc_info=True) if not self._finished: self.finish() @@ -734,10 +778,11 @@ class RequestHandler(object): ``write_error`` may call `write`, `render`, `set_header`, etc to produce output as usual. - If this error was caused by an uncaught exception, an ``exc_info`` - triple will be available as ``kwargs["exc_info"]``. Note that this - exception may not be the "current" exception for purposes of - methods like ``sys.exc_info()`` or ``traceback.format_exc``. + If this error was caused by an uncaught exception (including + HTTPError), an ``exc_info`` triple will be available as + ``kwargs["exc_info"]``. Note that this exception may not be + the "current" exception for purposes of methods like + ``sys.exc_info()`` or ``traceback.format_exc``. For historical reasons, if a method ``get_error_html`` exists, it will be used instead of the default ``write_error`` implementation. @@ -768,7 +813,7 @@ class RequestHandler(object): self.finish("<html><title>%(code)d: %(message)s</title>" "<body>%(code)d: %(message)s</body></html>" % { "code": status_code, - "message": httplib.responses[status_code], + "message": self._reason, }) @property @@ -964,7 +1009,7 @@ class RequestHandler(object): return callback(*args, **kwargs) except Exception, e: if self._headers_written: - logging.error("Exception after headers written", + app_log.error("Exception after headers written", exc_info=True) else: self._handle_request_exception(e) @@ -1008,6 +1053,9 @@ class RequestHandler(object): try: if self.request.method not in self.SUPPORTED_METHODS: raise HTTPError(405) + self.path_args = [self.decode_argument(arg) for arg in args] + self.path_kwargs = dict((k, self.decode_argument(v, name=k)) + for (k, v) in kwargs.iteritems()) # If XSRF cookies are turned on, reject form submissions without # the proper cookie if self.request.method not in ("GET", "HEAD", "OPTIONS") and \ @@ -1015,19 +1063,18 @@ class RequestHandler(object): self.check_xsrf_cookie() self.prepare() if not self._finished: - args = [self.decode_argument(arg) for arg in args] - kwargs = dict((k, self.decode_argument(v, name=k)) - for (k, v) in kwargs.iteritems()) - getattr(self, self.request.method.lower())(*args, **kwargs) + getattr(self, self.request.method.lower())( + *self.path_args, **self.path_kwargs) if self._auto_finish and not self._finished: self.finish() except Exception, e: self._handle_request_exception(e) def _generate_headers(self): + reason = self._reason lines = [utf8(self.request.version + " " + str(self._status_code) + - " " + httplib.responses[self._status_code])] + " " + reason)] lines.extend([(utf8(n) + b(": ") + utf8(v)) for n, v in itertools.chain(self._headers.iteritems(), self._list_headers)]) if hasattr(self, "_new_cookie"): @@ -1053,14 +1100,14 @@ class RequestHandler(object): if e.log_message: format = "%d %s: " + e.log_message args = [e.status_code, self._request_summary()] + list(e.args) - logging.warning(format, *args) - if e.status_code not in httplib.responses: - logging.error("Bad HTTP status code: %d", e.status_code) + gen_log.warning(format, *args) + if e.status_code not in httplib.responses and not e.reason: + gen_log.error("Bad HTTP status code: %d", e.status_code) self.send_error(500, exc_info=sys.exc_info()) else: self.send_error(e.status_code, exc_info=sys.exc_info()) else: - logging.error("Uncaught exception %s\n%r", self._request_summary(), + app_log.error("Uncaught exception %s\n%r", self._request_summary(), self.request, exc_info=True) self.send_error(500, exc_info=sys.exc_info()) @@ -1205,18 +1252,6 @@ class Application(object): and we will serve /favicon.ico and /robots.txt from the same directory. A custom subclass of StaticFileHandler can be specified with the static_handler_class setting. - - .. attribute:: settings - - Additional keyword arguments passed to the constructor are saved in the - `settings` dictionary, and are often referred to in documentation as - "application settings". - - .. attribute:: debug - - If `True` the application runs in debug mode, described in - :ref:`debug-mode`. This is an application setting in the `settings` - dictionary, so handlers can access it. """ def __init__(self, handlers=None, default_host="", transforms=None, wsgi=False, **settings): @@ -1319,7 +1354,7 @@ class Application(object): handlers.append(spec) if spec.name: if spec.name in self.named_handlers: - logging.warning( + app_log.warning( "Multiple handlers named %s; replacing previous value", spec.name) self.named_handlers[spec.name] = spec @@ -1443,26 +1478,40 @@ class Application(object): self.settings["log_function"](handler) return if handler.get_status() < 400: - log_method = logging.info + log_method = access_log.info elif handler.get_status() < 500: - log_method = logging.warning + log_method = access_log.warning else: - log_method = logging.error + log_method = access_log.error request_time = 1000.0 * handler.request.request_time() log_method("%d %s %.2fms", handler.get_status(), handler._request_summary(), request_time) class HTTPError(Exception): - """An exception that will turn into an HTTP error response.""" - def __init__(self, status_code, log_message=None, *args): + """An exception that will turn into an HTTP error response. + + :arg int status_code: HTTP status code. Must be listed in + `httplib.responses` unless the ``reason`` keyword argument is given. + :arg string log_message: Message to be written to the log for this error + (will not be shown to the user unless the `Application` is in debug + mode). May contain ``%s``-style placeholders, which will be filled + in with remaining positional parameters. + :arg string reason: Keyword-only argument. The HTTP "reason" phrase + to pass in the status line along with ``status_code``. Normally + determined automatically from ``status_code``, but can be used + to use a non-standard numeric code. + """ + def __init__(self, status_code, log_message=None, *args, **kwargs): self.status_code = status_code self.log_message = log_message self.args = args + self.reason = kwargs.get('reason', None) def __str__(self): message = "HTTP %d: %s" % ( - self.status_code, httplib.responses[self.status_code]) + self.status_code, + self.reason or httplib.responses.get(self.status_code, 'Unknown')) if self.log_message: return message + " (" + (self.log_message % self.args) + ")" else: @@ -1477,6 +1526,12 @@ class ErrorHandler(RequestHandler): def prepare(self): raise HTTPError(self._status_code) + def check_xsrf_cookie(self): + # POSTs to an ErrorHandler don't actually have side effects, + # so we don't need to check the xsrf token. This allows POSTs + # to the wrong url to return a 404 instead of 403. + pass + class RedirectHandler(RequestHandler): """Redirects the client to the given URL for all GET requests. @@ -1563,11 +1618,9 @@ class StaticFileHandler(RequestHandler): cache_time = self.get_cache_time(path, modified, mime_type) if cache_time > 0: - self.set_header("Expires", datetime.datetime.utcnow() + \ + self.set_header("Expires", datetime.datetime.utcnow() + datetime.timedelta(seconds=cache_time)) self.set_header("Cache-Control", "max-age=" + str(cache_time)) - else: - self.set_header("Cache-Control", "public") self.set_extra_headers(path) @@ -1583,9 +1636,6 @@ class StaticFileHandler(RequestHandler): with open(abspath, "rb") as file: data = file.read() - hasher = hashlib.sha1() - hasher.update(data) - self.set_header("Etag", '"%s"' % hasher.hexdigest()) if include_body: self.write(data) else: @@ -1646,7 +1696,7 @@ class StaticFileHandler(RequestHandler): hashes[abs_path] = hashlib.md5(f.read()).hexdigest() f.close() except Exception: - logging.error("Could not open static file %r", path) + gen_log.error("Could not open static file %r", path) hashes[abs_path] = None hsh = hashes.get(abs_path) if hsh: @@ -1721,6 +1771,10 @@ class GZipContentEncoding(OutputTransform): "gzip" in request.headers.get("Accept-Encoding", "") def transform_first_chunk(self, status_code, headers, chunk, finishing): + if 'Vary' in headers: + headers['Vary'] += b(', Accept-Encoding') + else: + headers['Vary'] = b('Accept-Encoding') if self._gzipping: ctype = _unicode(headers.get("Content-Type", "")).split(";")[0] self._gzipping = (ctype in self.CONTENT_TYPES) and \ @@ -1956,6 +2010,11 @@ class URLSpec(object): self.name = name self._path, self._group_count = self._find_groups() + def __repr__(self): + return '%s(%r, %s, kwargs=%r, name=%r)' % \ + (self.__class__.__name__, self.regex.pattern, + self.handler_class, self.kwargs, self.name) + def _find_groups(self): """Returns a tuple (reverse string, group count) for a url. @@ -2001,17 +2060,20 @@ class URLSpec(object): url = URLSpec -def _time_independent_equals(a, b): - if len(a) != len(b): - return False - result = 0 - if type(a[0]) is int: # python3 byte strings - for x, y in zip(a, b): - result |= x ^ y - else: # python2 - for x, y in zip(a, b): - result |= ord(x) ^ ord(y) - return result == 0 +if hasattr(hmac, 'compare_digest'): # python 3.3 + _time_independent_equals = hmac.compare_digest +else: + def _time_independent_equals(a, b): + if len(a) != len(b): + return False + result = 0 + if type(a[0]) is int: # python3 byte strings + for x, y in zip(a, b): + result |= x ^ y + else: # python2 + for x, y in zip(a, b): + result |= ord(x) ^ ord(y) + return result == 0 def create_signed_value(secret, name, value): @@ -2030,11 +2092,11 @@ def decode_signed_value(secret, name, value, max_age_days=31): return None signature = _create_signature(secret, name, parts[0], parts[1]) if not _time_independent_equals(parts[2], signature): - logging.warning("Invalid cookie signature %r", value) + gen_log.warning("Invalid cookie signature %r", value) return None timestamp = int(parts[1]) if timestamp < time.time() - max_age_days * 86400: - logging.warning("Expired cookie %r", value) + gen_log.warning("Expired cookie %r", value) return None if timestamp > time.time() + 31 * 86400: # _cookie_signature does not hash a delimiter between the @@ -2042,10 +2104,10 @@ def decode_signed_value(secret, name, value, max_age_days=31): # digits from the payload to the timestamp without altering the # signature. For backwards compatibility, sanity-check timestamp # here instead of modifying _cookie_signature. - logging.warning("Cookie timestamp in future; possible tampering %r", value) + gen_log.warning("Cookie timestamp in future; possible tampering %r", value) return None if parts[1].startswith(b("0")): - logging.warning("Tampered cookie %r", value) + gen_log.warning("Tampered cookie %r", value) return None try: return base64.b64decode(parts[0]) diff --git a/libs/tornado/websocket.py b/libs/tornado/websocket.py index 266b114b0306676cbe81d55556e455e6eeb3b119..08f2e0fe6148cb3a6e795d44d2f0391079eb7288 100755 --- a/libs/tornado/websocket.py +++ b/libs/tornado/websocket.py @@ -23,13 +23,13 @@ from __future__ import absolute_import, division, with_statement import array import functools import hashlib -import logging import struct import time import base64 import tornado.escape import tornado.web +from tornado.log import gen_log, app_log from tornado.util import bytes_type, b @@ -172,6 +172,14 @@ class WebSocketHandler(tornado.web.RequestHandler): """ raise NotImplementedError + def ping(self, data): + """Send ping frame to the remote end.""" + self.ws_connection.write_ping(data) + + def on_pong(self, data): + """Invoked when the response to a ping frame is received.""" + pass + def on_close(self): """Invoked when the WebSocket is closed.""" pass @@ -257,7 +265,7 @@ class WebSocketProtocol(object): try: return callback(*args, **kwargs) except Exception: - logging.error("Uncaught exception in %s", + app_log.error("Uncaught exception in %s", self.request.path, exc_info=True) self._abort() return wrapper @@ -289,7 +297,7 @@ class WebSocketProtocol76(WebSocketProtocol): try: self._handle_websocket_headers() except ValueError: - logging.debug("Malformed WebSocket request received") + gen_log.debug("Malformed WebSocket request received") self._abort() return @@ -344,7 +352,7 @@ class WebSocketProtocol76(WebSocketProtocol): try: challenge_response = self.challenge_response(challenge) except ValueError: - logging.debug("Malformed key data in WebSocket request") + gen_log.debug("Malformed key data in WebSocket request") self._abort() return self._write_response(challenge_response) @@ -420,6 +428,10 @@ class WebSocketProtocol76(WebSocketProtocol): assert isinstance(message, bytes_type) self.stream.write(b("\x00") + message + b("\xff")) + def write_ping(self, data): + """Send ping frame.""" + raise ValueError("Ping messages not supported by this version of websockets") + def close(self): """Closes the WebSocket connection.""" if not self.server_terminated: @@ -457,7 +469,7 @@ class WebSocketProtocol13(WebSocketProtocol): self._handle_websocket_headers() self._accept_connection() except ValueError: - logging.debug("Malformed WebSocket request received") + gen_log.debug("Malformed WebSocket request received") self._abort() return @@ -525,6 +537,11 @@ class WebSocketProtocol13(WebSocketProtocol): assert isinstance(message, bytes_type) self._write_frame(True, opcode, message) + def write_ping(self, data): + """Send ping frame.""" + assert isinstance(data, bytes_type) + self._write_frame(True, 0x9, data) + def _receive_frame(self): self.stream.read_bytes(2, self._on_frame_start) @@ -632,7 +649,7 @@ class WebSocketProtocol13(WebSocketProtocol): self._write_frame(True, 0xA, data) elif opcode == 0xA: # Pong - pass + self.async_callback(self.handler.on_pong)(data) else: self._abort() @@ -651,4 +668,4 @@ class WebSocketProtocol13(WebSocketProtocol): # Give the client a few seconds to complete a clean shutdown, # otherwise just close the connection. self._waiting = self.stream.io_loop.add_timeout( - time.time() + 5, self._abort) + self.stream.io_loop.time() + 5, self._abort) diff --git a/libs/tornado/wsgi.py b/libs/tornado/wsgi.py index ca34ff3e8829a787af888b6584d33b7806aff0de..3cb08502ed1546c6f4065227bf0447415e50f404 100755 --- a/libs/tornado/wsgi.py +++ b/libs/tornado/wsgi.py @@ -33,7 +33,6 @@ from __future__ import absolute_import, division, with_statement import Cookie import httplib -import logging import sys import time import tornado @@ -41,6 +40,7 @@ import urllib from tornado import escape from tornado import httputil +from tornado.log import access_log from tornado import web from tornado.escape import native_str, utf8, parse_qs_bytes from tornado.util import b, bytes_type @@ -114,8 +114,8 @@ class WSGIApplication(web.Application): def __call__(self, environ, start_response): handler = web.Application.__call__(self, HTTPRequest(environ)) assert handler._finished - status = str(handler._status_code) + " " + \ - httplib.responses[handler._status_code] + reason = handler._reason + status = str(handler._status_code) + " " + reason headers = handler._headers.items() + handler._list_headers if hasattr(handler, "_new_cookie"): for cookie in handler._new_cookie.values(): @@ -137,11 +137,8 @@ class HTTPRequest(object): self.query = environ.get("QUERY_STRING", "") if self.query: self.uri += "?" + self.query - arguments = parse_qs_bytes(native_str(self.query)) - for name, values in arguments.iteritems(): - values = [v for v in values if v] - if values: - self.arguments[name] = values + self.arguments = parse_qs_bytes(native_str(self.query), + keep_blank_values=True) self.version = "HTTP/1.1" self.headers = httputil.HTTPHeaders() if environ.get("CONTENT_TYPE"): @@ -248,10 +245,11 @@ class WSGIContainer(object): headers = data["headers"] header_set = set(k.lower() for (k, v) in headers) body = escape.utf8(body) - if "content-length" not in header_set: - headers.append(("Content-Length", str(len(body)))) - if "content-type" not in header_set: - headers.append(("Content-Type", "text/html; charset=UTF-8")) + if status_code != 304: + if "content-length" not in header_set: + headers.append(("Content-Length", str(len(body)))) + if "content-type" not in header_set: + headers.append(("Content-Type", "text/html; charset=UTF-8")) if "server" not in header_set: headers.append(("Server", "TornadoServer/%s" % tornado.version)) @@ -302,11 +300,11 @@ class WSGIContainer(object): def _log(self, status_code, request): if status_code < 400: - log_method = logging.info + log_method = access_log.info elif status_code < 500: - log_method = logging.warning + log_method = access_log.warning else: - log_method = logging.error + log_method = access_log.error request_time = 1000.0 * request.request_time() summary = request.method + " " + request.uri + " (" + \ request.remote_ip + ")"