##// END OF EJS Templates
avoid using abbreviations that look like spelling errors
Mads Kiilerich -
r17428:72803c8e default
parent child Browse files
Show More
@@ -1,450 +1,450
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # check-code - a style and portability checker for Mercurial
3 # check-code - a style and portability checker for Mercurial
4 #
4 #
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 import re, glob, os, sys
10 import re, glob, os, sys
11 import keyword
11 import keyword
12 import optparse
12 import optparse
13
13
14 def repquote(m):
14 def repquote(m):
15 t = re.sub(r"\w", "x", m.group('text'))
15 t = re.sub(r"\w", "x", m.group('text'))
16 t = re.sub(r"[^\s\nx]", "o", t)
16 t = re.sub(r"[^\s\nx]", "o", t)
17 return m.group('quote') + t + m.group('quote')
17 return m.group('quote') + t + m.group('quote')
18
18
19 def reppython(m):
19 def reppython(m):
20 comment = m.group('comment')
20 comment = m.group('comment')
21 if comment:
21 if comment:
22 return "#" * len(comment)
22 return "#" * len(comment)
23 return repquote(m)
23 return repquote(m)
24
24
25 def repcomment(m):
25 def repcomment(m):
26 return m.group(1) + "#" * len(m.group(2))
26 return m.group(1) + "#" * len(m.group(2))
27
27
28 def repccomment(m):
28 def repccomment(m):
29 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
29 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
30 return m.group(1) + t + "*/"
30 return m.group(1) + t + "*/"
31
31
32 def repcallspaces(m):
32 def repcallspaces(m):
33 t = re.sub(r"\n\s+", "\n", m.group(2))
33 t = re.sub(r"\n\s+", "\n", m.group(2))
34 return m.group(1) + t
34 return m.group(1) + t
35
35
36 def repinclude(m):
36 def repinclude(m):
37 return m.group(1) + "<foo>"
37 return m.group(1) + "<foo>"
38
38
39 def rephere(m):
39 def rephere(m):
40 t = re.sub(r"\S", "x", m.group(2))
40 t = re.sub(r"\S", "x", m.group(2))
41 return m.group(1) + t
41 return m.group(1) + t
42
42
43
43
44 testpats = [
44 testpats = [
45 [
45 [
46 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
46 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
47 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
47 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
48 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
48 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
49 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
49 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
50 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
50 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
51 (r'echo -n', "don't use 'echo -n', use printf"),
51 (r'echo -n', "don't use 'echo -n', use printf"),
52 (r'(^| )wc[^|]*$\n(?!.*\(re\))', "filter wc output"),
52 (r'(^| )wc[^|]*$\n(?!.*\(re\))', "filter wc output"),
53 (r'head -c', "don't use 'head -c', use 'dd'"),
53 (r'head -c', "don't use 'head -c', use 'dd'"),
54 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
54 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
55 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
55 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
56 (r'printf.*\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
56 (r'printf.*\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
57 (r'printf.*\\x', "don't use printf \\x, use Python"),
57 (r'printf.*\\x', "don't use printf \\x, use Python"),
58 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
58 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
59 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
59 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
60 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
60 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
61 "use egrep for extended grep syntax"),
61 "use egrep for extended grep syntax"),
62 (r'/bin/', "don't use explicit paths for tools"),
62 (r'/bin/', "don't use explicit paths for tools"),
63 (r'[^\n]\Z', "no trailing newline"),
63 (r'[^\n]\Z', "no trailing newline"),
64 (r'export.*=', "don't export and assign at once"),
64 (r'export.*=', "don't export and assign at once"),
65 (r'^source\b', "don't use 'source', use '.'"),
65 (r'^source\b', "don't use 'source', use '.'"),
66 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
66 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
67 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
67 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
68 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
68 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
69 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
69 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
70 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
70 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
71 (r'^alias\b.*=', "don't use alias, use a function"),
71 (r'^alias\b.*=', "don't use alias, use a function"),
72 (r'if\s*!', "don't use '!' to negate exit status"),
72 (r'if\s*!', "don't use '!' to negate exit status"),
73 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
73 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
74 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
74 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
75 (r'^( *)\t', "don't use tabs to indent"),
75 (r'^( *)\t', "don't use tabs to indent"),
76 ],
76 ],
77 # warnings
77 # warnings
78 [
78 [
79 (r'^function', "don't use 'function', use old style"),
79 (r'^function', "don't use 'function', use old style"),
80 (r'^diff.*-\w*N', "don't use 'diff -N'"),
80 (r'^diff.*-\w*N', "don't use 'diff -N'"),
81 (r'\$PWD', "don't use $PWD, use `pwd`"),
81 (r'\$PWD', "don't use $PWD, use `pwd`"),
82 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
82 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
83 ]
83 ]
84 ]
84 ]
85
85
86 testfilters = [
86 testfilters = [
87 (r"( *)(#([^\n]*\S)?)", repcomment),
87 (r"( *)(#([^\n]*\S)?)", repcomment),
88 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
88 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
89 ]
89 ]
90
90
91 uprefix = r"^ \$ "
91 uprefix = r"^ \$ "
92 utestpats = [
92 utestpats = [
93 [
93 [
94 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
94 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
95 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
95 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
96 "use regex test output patterns instead of sed"),
96 "use regex test output patterns instead of sed"),
97 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
97 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
98 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
98 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
99 (uprefix + r'.*\|\| echo.*(fail|error)',
99 (uprefix + r'.*\|\| echo.*(fail|error)',
100 "explicit exit code checks unnecessary"),
100 "explicit exit code checks unnecessary"),
101 (uprefix + r'set -e', "don't use set -e"),
101 (uprefix + r'set -e', "don't use set -e"),
102 (uprefix + r'\s', "don't indent commands, use > for continued lines"),
102 (uprefix + r'\s', "don't indent commands, use > for continued lines"),
103 (r'^ saved backup bundle to \$TESTTMP.*\.hg$',
103 (r'^ saved backup bundle to \$TESTTMP.*\.hg$',
104 "use (glob) to match Windows paths too"),
104 "use (glob) to match Windows paths too"),
105 ],
105 ],
106 # warnings
106 # warnings
107 []
107 []
108 ]
108 ]
109
109
110 for i in [0, 1]:
110 for i in [0, 1]:
111 for p, m in testpats[i]:
111 for p, m in testpats[i]:
112 if p.startswith(r'^'):
112 if p.startswith(r'^'):
113 p = r"^ [$>] (%s)" % p[1:]
113 p = r"^ [$>] (%s)" % p[1:]
114 else:
114 else:
115 p = r"^ [$>] .*(%s)" % p
115 p = r"^ [$>] .*(%s)" % p
116 utestpats[i].append((p, m))
116 utestpats[i].append((p, m))
117
117
118 utestfilters = [
118 utestfilters = [
119 (r"( *)(#([^\n]*\S)?)", repcomment),
119 (r"( *)(#([^\n]*\S)?)", repcomment),
120 ]
120 ]
121
121
122 pypats = [
122 pypats = [
123 [
123 [
124 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
124 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
125 "tuple parameter unpacking not available in Python 3+"),
125 "tuple parameter unpacking not available in Python 3+"),
126 (r'lambda\s*\(.*,.*\)',
126 (r'lambda\s*\(.*,.*\)',
127 "tuple parameter unpacking not available in Python 3+"),
127 "tuple parameter unpacking not available in Python 3+"),
128 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
128 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
129 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
129 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
130 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
130 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
131 (r'^\s*\t', "don't use tabs"),
131 (r'^\s*\t', "don't use tabs"),
132 (r'\S;\s*\n', "semicolon"),
132 (r'\S;\s*\n', "semicolon"),
133 (r'[^_]_\("[^"]+"\s*%', "don't use % inside _()"),
133 (r'[^_]_\("[^"]+"\s*%', "don't use % inside _()"),
134 (r"[^_]_\('[^']+'\s*%", "don't use % inside _()"),
134 (r"[^_]_\('[^']+'\s*%", "don't use % inside _()"),
135 (r'\w,\w', "missing whitespace after ,"),
135 (r'\w,\w', "missing whitespace after ,"),
136 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
136 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
137 (r'^\s+\w+=\w+[^,)\n]$', "missing whitespace in assignment"),
137 (r'^\s+\w+=\w+[^,)\n]$', "missing whitespace in assignment"),
138 (r'(\s+)try:\n((?:\n|\1\s.*\n)+?)\1except.*?:\n'
138 (r'(\s+)try:\n((?:\n|\1\s.*\n)+?)\1except.*?:\n'
139 r'((?:\n|\1\s.*\n)+?)\1finally:', 'no try/except/finally in Py2.4'),
139 r'((?:\n|\1\s.*\n)+?)\1finally:', 'no try/except/finally in Python 2.4'),
140 (r'.{81}', "line too long"),
140 (r'.{81}', "line too long"),
141 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
141 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
142 (r'[^\n]\Z', "no trailing newline"),
142 (r'[^\n]\Z', "no trailing newline"),
143 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
143 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
144 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
144 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
145 # "don't use underbars in identifiers"),
145 # "don't use underbars in identifiers"),
146 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
146 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
147 "don't use camelcase in identifiers"),
147 "don't use camelcase in identifiers"),
148 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
148 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
149 "linebreak after :"),
149 "linebreak after :"),
150 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
150 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
151 (r'class\s[^( \n]+\(\):',
151 (r'class\s[^( \n]+\(\):',
152 "class foo() not available in Python 2.4, use class foo(object)"),
152 "class foo() not available in Python 2.4, use class foo(object)"),
153 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
153 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
154 "Python keyword is not a function"),
154 "Python keyword is not a function"),
155 (r',]', "unneeded trailing ',' in list"),
155 (r',]', "unneeded trailing ',' in list"),
156 # (r'class\s[A-Z][^\(]*\((?!Exception)',
156 # (r'class\s[A-Z][^\(]*\((?!Exception)',
157 # "don't capitalize non-exception classes"),
157 # "don't capitalize non-exception classes"),
158 # (r'in range\(', "use xrange"),
158 # (r'in range\(', "use xrange"),
159 # (r'^\s*print\s+', "avoid using print in core and extensions"),
159 # (r'^\s*print\s+', "avoid using print in core and extensions"),
160 (r'[\x80-\xff]', "non-ASCII character literal"),
160 (r'[\x80-\xff]', "non-ASCII character literal"),
161 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
161 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
162 (r'^\s*with\s+', "with not available in Python 2.4"),
162 (r'^\s*with\s+', "with not available in Python 2.4"),
163 (r'\.isdisjoint\(', "set.isdisjoint not available in Python 2.4"),
163 (r'\.isdisjoint\(', "set.isdisjoint not available in Python 2.4"),
164 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
164 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
165 (r'^\s*os\.path\.relpath', "relpath not available in Python 2.4"),
165 (r'^\s*os\.path\.relpath', "relpath not available in Python 2.4"),
166 (r'(?<!def)\s+(any|all|format)\(',
166 (r'(?<!def)\s+(any|all|format)\(',
167 "any/all/format not available in Python 2.4"),
167 "any/all/format not available in Python 2.4"),
168 (r'(?<!def)\s+(callable)\(',
168 (r'(?<!def)\s+(callable)\(',
169 "callable not available in Python 3, use getattr(f, '__call__', None)"),
169 "callable not available in Python 3, use getattr(f, '__call__', None)"),
170 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
170 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
171 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
171 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
172 "gratuitous whitespace after Python keyword"),
172 "gratuitous whitespace after Python keyword"),
173 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
173 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
174 # (r'\s\s=', "gratuitous whitespace before ="),
174 # (r'\s\s=', "gratuitous whitespace before ="),
175 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
175 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
176 "missing whitespace around operator"),
176 "missing whitespace around operator"),
177 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
177 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
178 "missing whitespace around operator"),
178 "missing whitespace around operator"),
179 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
179 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
180 "missing whitespace around operator"),
180 "missing whitespace around operator"),
181 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
181 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
182 "wrong whitespace around ="),
182 "wrong whitespace around ="),
183 (r'raise Exception', "don't raise generic exceptions"),
183 (r'raise Exception', "don't raise generic exceptions"),
184 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
184 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
185 (r' [=!]=\s+(True|False|None)',
185 (r' [=!]=\s+(True|False|None)',
186 "comparison with singleton, use 'is' or 'is not' instead"),
186 "comparison with singleton, use 'is' or 'is not' instead"),
187 (r'^\s*(while|if) [01]:',
187 (r'^\s*(while|if) [01]:',
188 "use True/False for constant Boolean expression"),
188 "use True/False for constant Boolean expression"),
189 (r'(?:(?<!def)\s+|\()hasattr',
189 (r'(?:(?<!def)\s+|\()hasattr',
190 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
190 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
191 (r'opener\([^)]*\).read\(',
191 (r'opener\([^)]*\).read\(',
192 "use opener.read() instead"),
192 "use opener.read() instead"),
193 (r'BaseException', 'not in Py2.4, use Exception'),
193 (r'BaseException', 'not in Python 2.4, use Exception'),
194 (r'os\.path\.relpath', 'os.path.relpath is not in Py2.5'),
194 (r'os\.path\.relpath', 'os.path.relpath is not in Python 2.5'),
195 (r'opener\([^)]*\).write\(',
195 (r'opener\([^)]*\).write\(',
196 "use opener.write() instead"),
196 "use opener.write() instead"),
197 (r'[\s\(](open|file)\([^)]*\)\.read\(',
197 (r'[\s\(](open|file)\([^)]*\)\.read\(',
198 "use util.readfile() instead"),
198 "use util.readfile() instead"),
199 (r'[\s\(](open|file)\([^)]*\)\.write\(',
199 (r'[\s\(](open|file)\([^)]*\)\.write\(',
200 "use util.readfile() instead"),
200 "use util.readfile() instead"),
201 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
201 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
202 "always assign an opened file to a variable, and close it afterwards"),
202 "always assign an opened file to a variable, and close it afterwards"),
203 (r'[\s\(](open|file)\([^)]*\)\.',
203 (r'[\s\(](open|file)\([^)]*\)\.',
204 "always assign an opened file to a variable, and close it afterwards"),
204 "always assign an opened file to a variable, and close it afterwards"),
205 (r'(?i)descendent', "the proper spelling is descendAnt"),
205 (r'(?i)descendent', "the proper spelling is descendAnt"),
206 (r'\.debug\(\_', "don't mark debug messages for translation"),
206 (r'\.debug\(\_', "don't mark debug messages for translation"),
207 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
207 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
208 (r'^\s*except\s*:', "warning: naked except clause", r'#.*re-raises'),
208 (r'^\s*except\s*:', "warning: naked except clause", r'#.*re-raises'),
209 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
209 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
210 ],
210 ],
211 # warnings
211 # warnings
212 [
212 [
213 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
213 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
214 "warning: unwrapped ui message"),
214 "warning: unwrapped ui message"),
215 ]
215 ]
216 ]
216 ]
217
217
218 pyfilters = [
218 pyfilters = [
219 (r"""(?msx)(?P<comment>\#.*?$)|
219 (r"""(?msx)(?P<comment>\#.*?$)|
220 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
220 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
221 (?P<text>(([^\\]|\\.)*?))
221 (?P<text>(([^\\]|\\.)*?))
222 (?P=quote))""", reppython),
222 (?P=quote))""", reppython),
223 ]
223 ]
224
224
225 cpats = [
225 cpats = [
226 [
226 [
227 (r'//', "don't use //-style comments"),
227 (r'//', "don't use //-style comments"),
228 (r'^ ', "don't use spaces to indent"),
228 (r'^ ', "don't use spaces to indent"),
229 (r'\S\t', "don't use tabs except for indent"),
229 (r'\S\t', "don't use tabs except for indent"),
230 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
230 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
231 (r'.{81}', "line too long"),
231 (r'.{81}', "line too long"),
232 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
232 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
233 (r'return\(', "return is not a function"),
233 (r'return\(', "return is not a function"),
234 (r' ;', "no space before ;"),
234 (r' ;', "no space before ;"),
235 (r'\w+\* \w+', "use int *foo, not int* foo"),
235 (r'\w+\* \w+', "use int *foo, not int* foo"),
236 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
236 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
237 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
237 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
238 (r'\w,\w', "missing whitespace after ,"),
238 (r'\w,\w', "missing whitespace after ,"),
239 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
239 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
240 (r'^#\s+\w', "use #foo, not # foo"),
240 (r'^#\s+\w', "use #foo, not # foo"),
241 (r'[^\n]\Z', "no trailing newline"),
241 (r'[^\n]\Z', "no trailing newline"),
242 (r'^\s*#import\b', "use only #include in standard C code"),
242 (r'^\s*#import\b', "use only #include in standard C code"),
243 ],
243 ],
244 # warnings
244 # warnings
245 []
245 []
246 ]
246 ]
247
247
248 cfilters = [
248 cfilters = [
249 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
249 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
250 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
250 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
251 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
251 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
252 (r'(\()([^)]+\))', repcallspaces),
252 (r'(\()([^)]+\))', repcallspaces),
253 ]
253 ]
254
254
255 inutilpats = [
255 inutilpats = [
256 [
256 [
257 (r'\bui\.', "don't use ui in util"),
257 (r'\bui\.', "don't use ui in util"),
258 ],
258 ],
259 # warnings
259 # warnings
260 []
260 []
261 ]
261 ]
262
262
263 inrevlogpats = [
263 inrevlogpats = [
264 [
264 [
265 (r'\brepo\.', "don't use repo in revlog"),
265 (r'\brepo\.', "don't use repo in revlog"),
266 ],
266 ],
267 # warnings
267 # warnings
268 []
268 []
269 ]
269 ]
270
270
271 checks = [
271 checks = [
272 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
272 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
273 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
273 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
274 ('c', r'.*\.c$', cfilters, cpats),
274 ('c', r'.*\.c$', cfilters, cpats),
275 ('unified test', r'.*\.t$', utestfilters, utestpats),
275 ('unified test', r'.*\.t$', utestfilters, utestpats),
276 ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
276 ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
277 inrevlogpats),
277 inrevlogpats),
278 ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
278 ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
279 inutilpats),
279 inutilpats),
280 ]
280 ]
281
281
282 class norepeatlogger(object):
282 class norepeatlogger(object):
283 def __init__(self):
283 def __init__(self):
284 self._lastseen = None
284 self._lastseen = None
285
285
286 def log(self, fname, lineno, line, msg, blame):
286 def log(self, fname, lineno, line, msg, blame):
287 """print error related a to given line of a given file.
287 """print error related a to given line of a given file.
288
288
289 The faulty line will also be printed but only once in the case
289 The faulty line will also be printed but only once in the case
290 of multiple errors.
290 of multiple errors.
291
291
292 :fname: filename
292 :fname: filename
293 :lineno: line number
293 :lineno: line number
294 :line: actual content of the line
294 :line: actual content of the line
295 :msg: error message
295 :msg: error message
296 """
296 """
297 msgid = fname, lineno, line
297 msgid = fname, lineno, line
298 if msgid != self._lastseen:
298 if msgid != self._lastseen:
299 if blame:
299 if blame:
300 print "%s:%d (%s):" % (fname, lineno, blame)
300 print "%s:%d (%s):" % (fname, lineno, blame)
301 else:
301 else:
302 print "%s:%d:" % (fname, lineno)
302 print "%s:%d:" % (fname, lineno)
303 print " > %s" % line
303 print " > %s" % line
304 self._lastseen = msgid
304 self._lastseen = msgid
305 print " " + msg
305 print " " + msg
306
306
307 _defaultlogger = norepeatlogger()
307 _defaultlogger = norepeatlogger()
308
308
309 def getblame(f):
309 def getblame(f):
310 lines = []
310 lines = []
311 for l in os.popen('hg annotate -un %s' % f):
311 for l in os.popen('hg annotate -un %s' % f):
312 start, line = l.split(':', 1)
312 start, line = l.split(':', 1)
313 user, rev = start.split()
313 user, rev = start.split()
314 lines.append((line[1:-1], user, rev))
314 lines.append((line[1:-1], user, rev))
315 return lines
315 return lines
316
316
317 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
317 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
318 blame=False, debug=False, lineno=True):
318 blame=False, debug=False, lineno=True):
319 """checks style and portability of a given file
319 """checks style and portability of a given file
320
320
321 :f: filepath
321 :f: filepath
322 :logfunc: function used to report error
322 :logfunc: function used to report error
323 logfunc(filename, linenumber, linecontent, errormessage)
323 logfunc(filename, linenumber, linecontent, errormessage)
324 :maxerr: number of error to display before aborting.
324 :maxerr: number of error to display before aborting.
325 Set to false (default) to report all errors
325 Set to false (default) to report all errors
326
326
327 return True if no error is found, False otherwise.
327 return True if no error is found, False otherwise.
328 """
328 """
329 blamecache = None
329 blamecache = None
330 result = True
330 result = True
331 for name, match, filters, pats in checks:
331 for name, match, filters, pats in checks:
332 if debug:
332 if debug:
333 print name, f
333 print name, f
334 fc = 0
334 fc = 0
335 if not re.match(match, f):
335 if not re.match(match, f):
336 if debug:
336 if debug:
337 print "Skipping %s for %s it doesn't match %s" % (
337 print "Skipping %s for %s it doesn't match %s" % (
338 name, match, f)
338 name, match, f)
339 continue
339 continue
340 fp = open(f)
340 fp = open(f)
341 pre = post = fp.read()
341 pre = post = fp.read()
342 fp.close()
342 fp.close()
343 if "no-" + "check-code" in pre:
343 if "no-" + "check-code" in pre:
344 if debug:
344 if debug:
345 print "Skipping %s for %s it has no- and check-code" % (
345 print "Skipping %s for %s it has no- and check-code" % (
346 name, f)
346 name, f)
347 break
347 break
348 for p, r in filters:
348 for p, r in filters:
349 post = re.sub(p, r, post)
349 post = re.sub(p, r, post)
350 if warnings:
350 if warnings:
351 pats = pats[0] + pats[1]
351 pats = pats[0] + pats[1]
352 else:
352 else:
353 pats = pats[0]
353 pats = pats[0]
354 # print post # uncomment to show filtered version
354 # print post # uncomment to show filtered version
355
355
356 if debug:
356 if debug:
357 print "Checking %s for %s" % (name, f)
357 print "Checking %s for %s" % (name, f)
358
358
359 prelines = None
359 prelines = None
360 errors = []
360 errors = []
361 for pat in pats:
361 for pat in pats:
362 if len(pat) == 3:
362 if len(pat) == 3:
363 p, msg, ignore = pat
363 p, msg, ignore = pat
364 else:
364 else:
365 p, msg = pat
365 p, msg = pat
366 ignore = None
366 ignore = None
367
367
368 # fix-up regexes for multi-line searches
368 # fix-up regexes for multi-line searches
369 po = p
369 po = p
370 # \s doesn't match \n
370 # \s doesn't match \n
371 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
371 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
372 # [^...] doesn't match newline
372 # [^...] doesn't match newline
373 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
373 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
374
374
375 #print po, '=>', p
375 #print po, '=>', p
376
376
377 pos = 0
377 pos = 0
378 n = 0
378 n = 0
379 for m in re.finditer(p, post, re.MULTILINE):
379 for m in re.finditer(p, post, re.MULTILINE):
380 if prelines is None:
380 if prelines is None:
381 prelines = pre.splitlines()
381 prelines = pre.splitlines()
382 postlines = post.splitlines(True)
382 postlines = post.splitlines(True)
383
383
384 start = m.start()
384 start = m.start()
385 while n < len(postlines):
385 while n < len(postlines):
386 step = len(postlines[n])
386 step = len(postlines[n])
387 if pos + step > start:
387 if pos + step > start:
388 break
388 break
389 pos += step
389 pos += step
390 n += 1
390 n += 1
391 l = prelines[n]
391 l = prelines[n]
392
392
393 if "check-code" + "-ignore" in l:
393 if "check-code" + "-ignore" in l:
394 if debug:
394 if debug:
395 print "Skipping %s for %s:%s (check-code -ignore)" % (
395 print "Skipping %s for %s:%s (check-code -ignore)" % (
396 name, f, n)
396 name, f, n)
397 continue
397 continue
398 elif ignore and re.search(ignore, l, re.MULTILINE):
398 elif ignore and re.search(ignore, l, re.MULTILINE):
399 continue
399 continue
400 bd = ""
400 bd = ""
401 if blame:
401 if blame:
402 bd = 'working directory'
402 bd = 'working directory'
403 if not blamecache:
403 if not blamecache:
404 blamecache = getblame(f)
404 blamecache = getblame(f)
405 if n < len(blamecache):
405 if n < len(blamecache):
406 bl, bu, br = blamecache[n]
406 bl, bu, br = blamecache[n]
407 if bl == l:
407 if bl == l:
408 bd = '%s@%s' % (bu, br)
408 bd = '%s@%s' % (bu, br)
409 errors.append((f, lineno and n + 1, l, msg, bd))
409 errors.append((f, lineno and n + 1, l, msg, bd))
410 result = False
410 result = False
411
411
412 errors.sort()
412 errors.sort()
413 for e in errors:
413 for e in errors:
414 logfunc(*e)
414 logfunc(*e)
415 fc += 1
415 fc += 1
416 if maxerr and fc >= maxerr:
416 if maxerr and fc >= maxerr:
417 print " (too many errors, giving up)"
417 print " (too many errors, giving up)"
418 break
418 break
419
419
420 return result
420 return result
421
421
422 if __name__ == "__main__":
422 if __name__ == "__main__":
423 parser = optparse.OptionParser("%prog [options] [files]")
423 parser = optparse.OptionParser("%prog [options] [files]")
424 parser.add_option("-w", "--warnings", action="store_true",
424 parser.add_option("-w", "--warnings", action="store_true",
425 help="include warning-level checks")
425 help="include warning-level checks")
426 parser.add_option("-p", "--per-file", type="int",
426 parser.add_option("-p", "--per-file", type="int",
427 help="max warnings per file")
427 help="max warnings per file")
428 parser.add_option("-b", "--blame", action="store_true",
428 parser.add_option("-b", "--blame", action="store_true",
429 help="use annotate to generate blame info")
429 help="use annotate to generate blame info")
430 parser.add_option("", "--debug", action="store_true",
430 parser.add_option("", "--debug", action="store_true",
431 help="show debug information")
431 help="show debug information")
432 parser.add_option("", "--nolineno", action="store_false",
432 parser.add_option("", "--nolineno", action="store_false",
433 dest='lineno', help="don't show line numbers")
433 dest='lineno', help="don't show line numbers")
434
434
435 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
435 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
436 lineno=True)
436 lineno=True)
437 (options, args) = parser.parse_args()
437 (options, args) = parser.parse_args()
438
438
439 if len(args) == 0:
439 if len(args) == 0:
440 check = glob.glob("*")
440 check = glob.glob("*")
441 else:
441 else:
442 check = args
442 check = args
443
443
444 ret = 0
444 ret = 0
445 for f in check:
445 for f in check:
446 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
446 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
447 blame=options.blame, debug=options.debug,
447 blame=options.blame, debug=options.debug,
448 lineno=options.lineno):
448 lineno=options.lineno):
449 ret = 1
449 ret = 1
450 sys.exit(ret)
450 sys.exit(ret)
@@ -1,167 +1,167
1 # win32mbcs.py -- MBCS filename support for Mercurial
1 # win32mbcs.py -- MBCS filename support for Mercurial
2 #
2 #
3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
4 #
4 #
5 # Version: 0.3
5 # Version: 0.3
6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10 #
10 #
11
11
12 '''allow the use of MBCS paths with problematic encodings
12 '''allow the use of MBCS paths with problematic encodings
13
13
14 Some MBCS encodings are not good for some path operations (i.e.
14 Some MBCS encodings are not good for some path operations (i.e.
15 splitting path, case conversion, etc.) with its encoded bytes. We call
15 splitting path, case conversion, etc.) with its encoded bytes. We call
16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
17 This extension can be used to fix the issue with those encodings by
17 This extension can be used to fix the issue with those encodings by
18 wrapping some functions to convert to Unicode string before path
18 wrapping some functions to convert to Unicode string before path
19 operation.
19 operation.
20
20
21 This extension is useful for:
21 This extension is useful for:
22
22
23 - Japanese Windows users using shift_jis encoding.
23 - Japanese Windows users using shift_jis encoding.
24 - Chinese Windows users using big5 encoding.
24 - Chinese Windows users using big5 encoding.
25 - All users who use a repository with one of problematic encodings on
25 - All users who use a repository with one of problematic encodings on
26 case-insensitive file system.
26 case-insensitive file system.
27
27
28 This extension is not needed for:
28 This extension is not needed for:
29
29
30 - Any user who use only ASCII chars in path.
30 - Any user who use only ASCII chars in path.
31 - Any user who do not use any of problematic encodings.
31 - Any user who do not use any of problematic encodings.
32
32
33 Note that there are some limitations on using this extension:
33 Note that there are some limitations on using this extension:
34
34
35 - You should use single encoding in one repository.
35 - You should use single encoding in one repository.
36 - If the repository path ends with 0x5c, .hg/hgrc cannot be read.
36 - If the repository path ends with 0x5c, .hg/hgrc cannot be read.
37 - win32mbcs is not compatible with fixutf8 extension.
37 - win32mbcs is not compatible with fixutf8 extension.
38
38
39 By default, win32mbcs uses encoding.encoding decided by Mercurial.
39 By default, win32mbcs uses encoding.encoding decided by Mercurial.
40 You can specify the encoding by config option::
40 You can specify the encoding by config option::
41
41
42 [win32mbcs]
42 [win32mbcs]
43 encoding = sjis
43 encoding = sjis
44
44
45 It is useful for the users who want to commit with UTF-8 log message.
45 It is useful for the users who want to commit with UTF-8 log message.
46 '''
46 '''
47
47
48 import os, sys
48 import os, sys
49 from mercurial.i18n import _
49 from mercurial.i18n import _
50 from mercurial import util, encoding
50 from mercurial import util, encoding
51 testedwith = 'internal'
51 testedwith = 'internal'
52
52
53 _encoding = None # see extsetup
53 _encoding = None # see extsetup
54
54
55 def decode(arg):
55 def decode(arg):
56 if isinstance(arg, str):
56 if isinstance(arg, str):
57 uarg = arg.decode(_encoding)
57 uarg = arg.decode(_encoding)
58 if arg == uarg.encode(_encoding):
58 if arg == uarg.encode(_encoding):
59 return uarg
59 return uarg
60 raise UnicodeError("Not local encoding")
60 raise UnicodeError("Not local encoding")
61 elif isinstance(arg, tuple):
61 elif isinstance(arg, tuple):
62 return tuple(map(decode, arg))
62 return tuple(map(decode, arg))
63 elif isinstance(arg, list):
63 elif isinstance(arg, list):
64 return map(decode, arg)
64 return map(decode, arg)
65 elif isinstance(arg, dict):
65 elif isinstance(arg, dict):
66 for k, v in arg.items():
66 for k, v in arg.items():
67 arg[k] = decode(v)
67 arg[k] = decode(v)
68 return arg
68 return arg
69
69
70 def encode(arg):
70 def encode(arg):
71 if isinstance(arg, unicode):
71 if isinstance(arg, unicode):
72 return arg.encode(_encoding)
72 return arg.encode(_encoding)
73 elif isinstance(arg, tuple):
73 elif isinstance(arg, tuple):
74 return tuple(map(encode, arg))
74 return tuple(map(encode, arg))
75 elif isinstance(arg, list):
75 elif isinstance(arg, list):
76 return map(encode, arg)
76 return map(encode, arg)
77 elif isinstance(arg, dict):
77 elif isinstance(arg, dict):
78 for k, v in arg.items():
78 for k, v in arg.items():
79 arg[k] = encode(v)
79 arg[k] = encode(v)
80 return arg
80 return arg
81
81
82 def appendsep(s):
82 def appendsep(s):
83 # ensure the path ends with os.sep, appending it if necessary.
83 # ensure the path ends with os.sep, appending it if necessary.
84 try:
84 try:
85 us = decode(s)
85 us = decode(s)
86 except UnicodeError:
86 except UnicodeError:
87 us = s
87 us = s
88 if us and us[-1] not in ':/\\':
88 if us and us[-1] not in ':/\\':
89 s += os.sep
89 s += os.sep
90 return s
90 return s
91
91
92 def wrapper(func, args, kwds):
92 def wrapper(func, args, kwds):
93 # check argument is unicode, then call original
93 # check argument is unicode, then call original
94 for arg in args:
94 for arg in args:
95 if isinstance(arg, unicode):
95 if isinstance(arg, unicode):
96 return func(*args, **kwds)
96 return func(*args, **kwds)
97
97
98 try:
98 try:
99 # convert arguments to unicode, call func, then convert back
99 # convert arguments to unicode, call func, then convert back
100 return encode(func(*decode(args), **decode(kwds)))
100 return encode(func(*decode(args), **decode(kwds)))
101 except UnicodeError:
101 except UnicodeError:
102 raise util.Abort(_("[win32mbcs] filename conversion failed with"
102 raise util.Abort(_("[win32mbcs] filename conversion failed with"
103 " %s encoding\n") % (_encoding))
103 " %s encoding\n") % (_encoding))
104
104
105 def wrapperforlistdir(func, args, kwds):
105 def wrapperforlistdir(func, args, kwds):
106 # Ensure 'path' argument ends with os.sep to avoids
106 # Ensure 'path' argument ends with os.sep to avoids
107 # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
107 # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
108 if args:
108 if args:
109 args = list(args)
109 args = list(args)
110 args[0] = appendsep(args[0])
110 args[0] = appendsep(args[0])
111 if 'path' in kwds:
111 if 'path' in kwds:
112 kwds['path'] = appendsep(kwds['path'])
112 kwds['path'] = appendsep(kwds['path'])
113 return func(*args, **kwds)
113 return func(*args, **kwds)
114
114
115 def wrapname(name, wrapper):
115 def wrapname(name, wrapper):
116 module, name = name.rsplit('.', 1)
116 module, name = name.rsplit('.', 1)
117 module = sys.modules[module]
117 module = sys.modules[module]
118 func = getattr(module, name)
118 func = getattr(module, name)
119 def f(*args, **kwds):
119 def f(*args, **kwds):
120 return wrapper(func, args, kwds)
120 return wrapper(func, args, kwds)
121 try:
121 try:
122 f.__name__ = func.__name__ # fail with python23
122 f.__name__ = func.__name__ # fails with Python 2.3
123 except Exception:
123 except Exception:
124 pass
124 pass
125 setattr(module, name, f)
125 setattr(module, name, f)
126
126
127 # List of functions to be wrapped.
127 # List of functions to be wrapped.
128 # NOTE: os.path.dirname() and os.path.basename() are safe because
128 # NOTE: os.path.dirname() and os.path.basename() are safe because
129 # they use result of os.path.split()
129 # they use result of os.path.split()
130 funcs = '''os.path.join os.path.split os.path.splitext
130 funcs = '''os.path.join os.path.split os.path.splitext
131 os.path.normpath os.makedirs
131 os.path.normpath os.makedirs
132 mercurial.util.endswithsep mercurial.util.splitpath mercurial.util.checkcase
132 mercurial.util.endswithsep mercurial.util.splitpath mercurial.util.checkcase
133 mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath
133 mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath
134 mercurial.util.checkwinfilename mercurial.util.checkosfilename'''
134 mercurial.util.checkwinfilename mercurial.util.checkosfilename'''
135
135
136 # List of Windows specific functions to be wrapped.
136 # List of Windows specific functions to be wrapped.
137 winfuncs = '''os.path.splitunc'''
137 winfuncs = '''os.path.splitunc'''
138
138
139 # codec and alias names of sjis and big5 to be faked.
139 # codec and alias names of sjis and big5 to be faked.
140 problematic_encodings = '''big5 big5-tw csbig5 big5hkscs big5-hkscs
140 problematic_encodings = '''big5 big5-tw csbig5 big5hkscs big5-hkscs
141 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
141 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
142 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
142 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
143 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
143 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
144
144
145 def extsetup(ui):
145 def extsetup(ui):
146 # TODO: decide use of config section for this extension
146 # TODO: decide use of config section for this extension
147 if ((not os.path.supports_unicode_filenames) and
147 if ((not os.path.supports_unicode_filenames) and
148 (sys.platform != 'cygwin')):
148 (sys.platform != 'cygwin')):
149 ui.warn(_("[win32mbcs] cannot activate on this platform.\n"))
149 ui.warn(_("[win32mbcs] cannot activate on this platform.\n"))
150 return
150 return
151 # determine encoding for filename
151 # determine encoding for filename
152 global _encoding
152 global _encoding
153 _encoding = ui.config('win32mbcs', 'encoding', encoding.encoding)
153 _encoding = ui.config('win32mbcs', 'encoding', encoding.encoding)
154 # fake is only for relevant environment.
154 # fake is only for relevant environment.
155 if _encoding.lower() in problematic_encodings.split():
155 if _encoding.lower() in problematic_encodings.split():
156 for f in funcs.split():
156 for f in funcs.split():
157 wrapname(f, wrapper)
157 wrapname(f, wrapper)
158 if os.name == 'nt':
158 if os.name == 'nt':
159 for f in winfuncs.split():
159 for f in winfuncs.split():
160 wrapname(f, wrapper)
160 wrapname(f, wrapper)
161 wrapname("mercurial.osutil.listdir", wrapperforlistdir)
161 wrapname("mercurial.osutil.listdir", wrapperforlistdir)
162 # Check sys.args manually instead of using ui.debug() because
162 # Check sys.args manually instead of using ui.debug() because
163 # command line options is not yet applied when
163 # command line options is not yet applied when
164 # extensions.loadall() is called.
164 # extensions.loadall() is called.
165 if '--debug' in sys.argv:
165 if '--debug' in sys.argv:
166 ui.write("[win32mbcs] activated with encoding: %s\n"
166 ui.write("[win32mbcs] activated with encoding: %s\n"
167 % _encoding)
167 % _encoding)
@@ -1,1581 +1,1580
1 """ Multicast DNS Service Discovery for Python, v0.12
1 """ Multicast DNS Service Discovery for Python, v0.12
2 Copyright (C) 2003, Paul Scott-Murphy
2 Copyright (C) 2003, Paul Scott-Murphy
3
3
4 This module provides a framework for the use of DNS Service Discovery
4 This module provides a framework for the use of DNS Service Discovery
5 using IP multicast. It has been tested against the JRendezvous
5 using IP multicast. It has been tested against the JRendezvous
6 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
6 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
7 and against the mDNSResponder from Mac OS X 10.3.8.
7 and against the mDNSResponder from Mac OS X 10.3.8.
8
8
9 This library is free software; you can redistribute it and/or
9 This library is free software; you can redistribute it and/or
10 modify it under the terms of the GNU Lesser General Public
10 modify it under the terms of the GNU Lesser General Public
11 License as published by the Free Software Foundation; either
11 License as published by the Free Software Foundation; either
12 version 2.1 of the License, or (at your option) any later version.
12 version 2.1 of the License, or (at your option) any later version.
13
13
14 This library is distributed in the hope that it will be useful,
14 This library is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 Lesser General Public License for more details.
17 Lesser General Public License for more details.
18
18
19 You should have received a copy of the GNU Lesser General Public
19 You should have received a copy of the GNU Lesser General Public
20 License along with this library; if not, see
20 License along with this library; if not, see
21 <http://www.gnu.org/licenses/>.
21 <http://www.gnu.org/licenses/>.
22
22
23 """
23 """
24
24
25 """0.12 update - allow selection of binding interface
25 """0.12 update - allow selection of binding interface
26 typo fix - Thanks A. M. Kuchlingi
26 typo fix - Thanks A. M. Kuchlingi
27 removed all use of word 'Rendezvous' - this is an API change"""
27 removed all use of word 'Rendezvous' - this is an API change"""
28
28
29 """0.11 update - correction to comments for addListener method
29 """0.11 update - correction to comments for addListener method
30 support for new record types seen from OS X
30 support for new record types seen from OS X
31 - IPv6 address
31 - IPv6 address
32 - hostinfo
32 - hostinfo
33 ignore unknown DNS record types
33 ignore unknown DNS record types
34 fixes to name decoding
34 fixes to name decoding
35 works alongside other processes using port 5353 (e.g. on Mac OS X)
35 works alongside other processes using port 5353 (e.g. on Mac OS X)
36 tested against Mac OS X 10.3.2's mDNSResponder
36 tested against Mac OS X 10.3.2's mDNSResponder
37 corrections to removal of list entries for service browser"""
37 corrections to removal of list entries for service browser"""
38
38
39 """0.10 update - Jonathon Paisley contributed these corrections:
39 """0.10 update - Jonathon Paisley contributed these corrections:
40 always multicast replies, even when query is unicast
40 always multicast replies, even when query is unicast
41 correct a pointer encoding problem
41 correct a pointer encoding problem
42 can now write records in any order
42 can now write records in any order
43 traceback shown on failure
43 traceback shown on failure
44 better TXT record parsing
44 better TXT record parsing
45 server is now separate from name
45 server is now separate from name
46 can cancel a service browser
46 can cancel a service browser
47
47
48 modified some unit tests to accommodate these changes"""
48 modified some unit tests to accommodate these changes"""
49
49
50 """0.09 update - remove all records on service unregistration
50 """0.09 update - remove all records on service unregistration
51 fix DOS security problem with readName"""
51 fix DOS security problem with readName"""
52
52
53 """0.08 update - changed licensing to LGPL"""
53 """0.08 update - changed licensing to LGPL"""
54
54
55 """0.07 update - faster shutdown on engine
55 """0.07 update - faster shutdown on engine
56 pointer encoding of outgoing names
56 pointer encoding of outgoing names
57 ServiceBrowser now works
57 ServiceBrowser now works
58 new unit tests"""
58 new unit tests"""
59
59
60 """0.06 update - small improvements with unit tests
60 """0.06 update - small improvements with unit tests
61 added defined exception types
61 added defined exception types
62 new style objects
62 new style objects
63 fixed hostname/interface problem
63 fixed hostname/interface problem
64 fixed socket timeout problem
64 fixed socket timeout problem
65 fixed addServiceListener() typo bug
65 fixed addServiceListener() typo bug
66 using select() for socket reads
66 using select() for socket reads
67 tested on Debian unstable with Python 2.2.2"""
67 tested on Debian unstable with Python 2.2.2"""
68
68
69 """0.05 update - ensure case insensitivity on domain names
69 """0.05 update - ensure case insensitivity on domain names
70 support for unicast DNS queries"""
70 support for unicast DNS queries"""
71
71
72 """0.04 update - added some unit tests
72 """0.04 update - added some unit tests
73 added __ne__ adjuncts where required
73 added __ne__ adjuncts where required
74 ensure names end in '.local.'
74 ensure names end in '.local.'
75 timeout on receiving socket for clean shutdown"""
75 timeout on receiving socket for clean shutdown"""
76
76
77 __author__ = "Paul Scott-Murphy"
77 __author__ = "Paul Scott-Murphy"
78 __email__ = "paul at scott dash murphy dot com"
78 __email__ = "paul at scott dash murphy dot com"
79 __version__ = "0.12"
79 __version__ = "0.12"
80
80
81 import string
81 import string
82 import time
82 import time
83 import struct
83 import struct
84 import socket
84 import socket
85 import threading
85 import threading
86 import select
86 import select
87 import traceback
87 import traceback
88
88
89 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
89 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
90
90
91 # hook for threads
91 # hook for threads
92
92
93 globals()['_GLOBAL_DONE'] = 0
93 globals()['_GLOBAL_DONE'] = 0
94
94
95 # Some timing constants
95 # Some timing constants
96
96
97 _UNREGISTER_TIME = 125
97 _UNREGISTER_TIME = 125
98 _CHECK_TIME = 175
98 _CHECK_TIME = 175
99 _REGISTER_TIME = 225
99 _REGISTER_TIME = 225
100 _LISTENER_TIME = 200
100 _LISTENER_TIME = 200
101 _BROWSER_TIME = 500
101 _BROWSER_TIME = 500
102
102
103 # Some DNS constants
103 # Some DNS constants
104
104
105 _MDNS_ADDR = '224.0.0.251'
105 _MDNS_ADDR = '224.0.0.251'
106 _MDNS_PORT = 5353;
106 _MDNS_PORT = 5353;
107 _DNS_PORT = 53;
107 _DNS_PORT = 53;
108 _DNS_TTL = 60 * 60; # one hour default TTL
108 _DNS_TTL = 60 * 60; # one hour default TTL
109
109
110 _MAX_MSG_TYPICAL = 1460 # unused
110 _MAX_MSG_TYPICAL = 1460 # unused
111 _MAX_MSG_ABSOLUTE = 8972
111 _MAX_MSG_ABSOLUTE = 8972
112
112
113 _FLAGS_QR_MASK = 0x8000 # query response mask
113 _FLAGS_QR_MASK = 0x8000 # query response mask
114 _FLAGS_QR_QUERY = 0x0000 # query
114 _FLAGS_QR_QUERY = 0x0000 # query
115 _FLAGS_QR_RESPONSE = 0x8000 # response
115 _FLAGS_QR_RESPONSE = 0x8000 # response
116
116
117 _FLAGS_AA = 0x0400 # Authoritative answer
117 _FLAGS_AA = 0x0400 # Authoritative answer
118 _FLAGS_TC = 0x0200 # Truncated
118 _FLAGS_TC = 0x0200 # Truncated
119 _FLAGS_RD = 0x0100 # Recursion desired
119 _FLAGS_RD = 0x0100 # Recursion desired
120 _FLAGS_RA = 0x8000 # Recursion available
120 _FLAGS_RA = 0x8000 # Recursion available
121
121
122 _FLAGS_Z = 0x0040 # Zero
122 _FLAGS_Z = 0x0040 # Zero
123 _FLAGS_AD = 0x0020 # Authentic data
123 _FLAGS_AD = 0x0020 # Authentic data
124 _FLAGS_CD = 0x0010 # Checking disabled
124 _FLAGS_CD = 0x0010 # Checking disabled
125
125
126 _CLASS_IN = 1
126 _CLASS_IN = 1
127 _CLASS_CS = 2
127 _CLASS_CS = 2
128 _CLASS_CH = 3
128 _CLASS_CH = 3
129 _CLASS_HS = 4
129 _CLASS_HS = 4
130 _CLASS_NONE = 254
130 _CLASS_NONE = 254
131 _CLASS_ANY = 255
131 _CLASS_ANY = 255
132 _CLASS_MASK = 0x7FFF
132 _CLASS_MASK = 0x7FFF
133 _CLASS_UNIQUE = 0x8000
133 _CLASS_UNIQUE = 0x8000
134
134
135 _TYPE_A = 1
135 _TYPE_A = 1
136 _TYPE_NS = 2
136 _TYPE_NS = 2
137 _TYPE_MD = 3
137 _TYPE_MD = 3
138 _TYPE_MF = 4
138 _TYPE_MF = 4
139 _TYPE_CNAME = 5
139 _TYPE_CNAME = 5
140 _TYPE_SOA = 6
140 _TYPE_SOA = 6
141 _TYPE_MB = 7
141 _TYPE_MB = 7
142 _TYPE_MG = 8
142 _TYPE_MG = 8
143 _TYPE_MR = 9
143 _TYPE_MR = 9
144 _TYPE_NULL = 10
144 _TYPE_NULL = 10
145 _TYPE_WKS = 11
145 _TYPE_WKS = 11
146 _TYPE_PTR = 12
146 _TYPE_PTR = 12
147 _TYPE_HINFO = 13
147 _TYPE_HINFO = 13
148 _TYPE_MINFO = 14
148 _TYPE_MINFO = 14
149 _TYPE_MX = 15
149 _TYPE_MX = 15
150 _TYPE_TXT = 16
150 _TYPE_TXT = 16
151 _TYPE_AAAA = 28
151 _TYPE_AAAA = 28
152 _TYPE_SRV = 33
152 _TYPE_SRV = 33
153 _TYPE_ANY = 255
153 _TYPE_ANY = 255
154
154
155 # Mapping constants to names
155 # Mapping constants to names
156
156
157 _CLASSES = { _CLASS_IN : "in",
157 _CLASSES = { _CLASS_IN : "in",
158 _CLASS_CS : "cs",
158 _CLASS_CS : "cs",
159 _CLASS_CH : "ch",
159 _CLASS_CH : "ch",
160 _CLASS_HS : "hs",
160 _CLASS_HS : "hs",
161 _CLASS_NONE : "none",
161 _CLASS_NONE : "none",
162 _CLASS_ANY : "any" }
162 _CLASS_ANY : "any" }
163
163
164 _TYPES = { _TYPE_A : "a",
164 _TYPES = { _TYPE_A : "a",
165 _TYPE_NS : "ns",
165 _TYPE_NS : "ns",
166 _TYPE_MD : "md",
166 _TYPE_MD : "md",
167 _TYPE_MF : "mf",
167 _TYPE_MF : "mf",
168 _TYPE_CNAME : "cname",
168 _TYPE_CNAME : "cname",
169 _TYPE_SOA : "soa",
169 _TYPE_SOA : "soa",
170 _TYPE_MB : "mb",
170 _TYPE_MB : "mb",
171 _TYPE_MG : "mg",
171 _TYPE_MG : "mg",
172 _TYPE_MR : "mr",
172 _TYPE_MR : "mr",
173 _TYPE_NULL : "null",
173 _TYPE_NULL : "null",
174 _TYPE_WKS : "wks",
174 _TYPE_WKS : "wks",
175 _TYPE_PTR : "ptr",
175 _TYPE_PTR : "ptr",
176 _TYPE_HINFO : "hinfo",
176 _TYPE_HINFO : "hinfo",
177 _TYPE_MINFO : "minfo",
177 _TYPE_MINFO : "minfo",
178 _TYPE_MX : "mx",
178 _TYPE_MX : "mx",
179 _TYPE_TXT : "txt",
179 _TYPE_TXT : "txt",
180 _TYPE_AAAA : "quada",
180 _TYPE_AAAA : "quada",
181 _TYPE_SRV : "srv",
181 _TYPE_SRV : "srv",
182 _TYPE_ANY : "any" }
182 _TYPE_ANY : "any" }
183
183
184 # utility functions
184 # utility functions
185
185
186 def currentTimeMillis():
186 def currentTimeMillis():
187 """Current system time in milliseconds"""
187 """Current system time in milliseconds"""
188 return time.time() * 1000
188 return time.time() * 1000
189
189
190 # Exceptions
190 # Exceptions
191
191
192 class NonLocalNameException(Exception):
192 class NonLocalNameException(Exception):
193 pass
193 pass
194
194
195 class NonUniqueNameException(Exception):
195 class NonUniqueNameException(Exception):
196 pass
196 pass
197
197
198 class NamePartTooLongException(Exception):
198 class NamePartTooLongException(Exception):
199 pass
199 pass
200
200
201 class AbstractMethodException(Exception):
201 class AbstractMethodException(Exception):
202 pass
202 pass
203
203
204 class BadTypeInNameException(Exception):
204 class BadTypeInNameException(Exception):
205 pass
205 pass
206
206
207 class BadDomainName(Exception):
207 class BadDomainName(Exception):
208 def __init__(self, pos):
208 def __init__(self, pos):
209 Exception.__init__(self, "at position %s" % pos)
209 Exception.__init__(self, "at position %s" % pos)
210
210
211 class BadDomainNameCircular(BadDomainName):
211 class BadDomainNameCircular(BadDomainName):
212 pass
212 pass
213
213
214 # implementation classes
214 # implementation classes
215
215
216 class DNSEntry(object):
216 class DNSEntry(object):
217 """A DNS entry"""
217 """A DNS entry"""
218
218
219 def __init__(self, name, type, clazz):
219 def __init__(self, name, type, clazz):
220 self.key = string.lower(name)
220 self.key = string.lower(name)
221 self.name = name
221 self.name = name
222 self.type = type
222 self.type = type
223 self.clazz = clazz & _CLASS_MASK
223 self.clazz = clazz & _CLASS_MASK
224 self.unique = (clazz & _CLASS_UNIQUE) != 0
224 self.unique = (clazz & _CLASS_UNIQUE) != 0
225
225
226 def __eq__(self, other):
226 def __eq__(self, other):
227 """Equality test on name, type, and class"""
227 """Equality test on name, type, and class"""
228 if isinstance(other, DNSEntry):
228 if isinstance(other, DNSEntry):
229 return self.name == other.name and self.type == other.type and self.clazz == other.clazz
229 return self.name == other.name and self.type == other.type and self.clazz == other.clazz
230 return 0
230 return 0
231
231
232 def __ne__(self, other):
232 def __ne__(self, other):
233 """Non-equality test"""
233 """Non-equality test"""
234 return not self.__eq__(other)
234 return not self.__eq__(other)
235
235
236 def getClazz(self, clazz):
236 def getClazz(self, clazz):
237 """Class accessor"""
237 """Class accessor"""
238 try:
238 try:
239 return _CLASSES[clazz]
239 return _CLASSES[clazz]
240 except KeyError:
240 except KeyError:
241 return "?(%s)" % (clazz)
241 return "?(%s)" % (clazz)
242
242
243 def getType(self, type):
243 def getType(self, type):
244 """Type accessor"""
244 """Type accessor"""
245 try:
245 try:
246 return _TYPES[type]
246 return _TYPES[type]
247 except KeyError:
247 except KeyError:
248 return "?(%s)" % (type)
248 return "?(%s)" % (type)
249
249
250 def toString(self, hdr, other):
250 def toString(self, hdr, other):
251 """String representation with additional information"""
251 """String representation with additional information"""
252 result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))
252 result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))
253 if self.unique:
253 if self.unique:
254 result += "-unique,"
254 result += "-unique,"
255 else:
255 else:
256 result += ","
256 result += ","
257 result += self.name
257 result += self.name
258 if other is not None:
258 if other is not None:
259 result += ",%s]" % (other)
259 result += ",%s]" % (other)
260 else:
260 else:
261 result += "]"
261 result += "]"
262 return result
262 return result
263
263
264 class DNSQuestion(DNSEntry):
264 class DNSQuestion(DNSEntry):
265 """A DNS question entry"""
265 """A DNS question entry"""
266
266
267 def __init__(self, name, type, clazz):
267 def __init__(self, name, type, clazz):
268 if not name.endswith(".local."):
268 if not name.endswith(".local."):
269 raise NonLocalNameException(name)
269 raise NonLocalNameException(name)
270 DNSEntry.__init__(self, name, type, clazz)
270 DNSEntry.__init__(self, name, type, clazz)
271
271
272 def answeredBy(self, rec):
272 def answeredBy(self, rec):
273 """Returns true if the question is answered by the record"""
273 """Returns true if the question is answered by the record"""
274 return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name
274 return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name
275
275
276 def __repr__(self):
276 def __repr__(self):
277 """String representation"""
277 """String representation"""
278 return DNSEntry.toString(self, "question", None)
278 return DNSEntry.toString(self, "question", None)
279
279
280
280
281 class DNSRecord(DNSEntry):
281 class DNSRecord(DNSEntry):
282 """A DNS record - like a DNS entry, but has a TTL"""
282 """A DNS record - like a DNS entry, but has a TTL"""
283
283
284 def __init__(self, name, type, clazz, ttl):
284 def __init__(self, name, type, clazz, ttl):
285 DNSEntry.__init__(self, name, type, clazz)
285 DNSEntry.__init__(self, name, type, clazz)
286 self.ttl = ttl
286 self.ttl = ttl
287 self.created = currentTimeMillis()
287 self.created = currentTimeMillis()
288
288
289 def __eq__(self, other):
289 def __eq__(self, other):
290 """Tests equality as per DNSRecord"""
290 """Tests equality as per DNSRecord"""
291 if isinstance(other, DNSRecord):
291 if isinstance(other, DNSRecord):
292 return DNSEntry.__eq__(self, other)
292 return DNSEntry.__eq__(self, other)
293 return 0
293 return 0
294
294
295 def suppressedBy(self, msg):
295 def suppressedBy(self, msg):
296 """Returns true if any answer in a message can suffice for the
296 """Returns true if any answer in a message can suffice for the
297 information held in this record."""
297 information held in this record."""
298 for record in msg.answers:
298 for record in msg.answers:
299 if self.suppressedByAnswer(record):
299 if self.suppressedByAnswer(record):
300 return 1
300 return 1
301 return 0
301 return 0
302
302
303 def suppressedByAnswer(self, other):
303 def suppressedByAnswer(self, other):
304 """Returns true if another record has same name, type and class,
304 """Returns true if another record has same name, type and class,
305 and if its TTL is at least half of this record's."""
305 and if its TTL is at least half of this record's."""
306 if self == other and other.ttl > (self.ttl / 2):
306 if self == other and other.ttl > (self.ttl / 2):
307 return 1
307 return 1
308 return 0
308 return 0
309
309
310 def getExpirationTime(self, percent):
310 def getExpirationTime(self, percent):
311 """Returns the time at which this record will have expired
311 """Returns the time at which this record will have expired
312 by a certain percentage."""
312 by a certain percentage."""
313 return self.created + (percent * self.ttl * 10)
313 return self.created + (percent * self.ttl * 10)
314
314
315 def getRemainingTTL(self, now):
315 def getRemainingTTL(self, now):
316 """Returns the remaining TTL in seconds."""
316 """Returns the remaining TTL in seconds."""
317 return max(0, (self.getExpirationTime(100) - now) / 1000)
317 return max(0, (self.getExpirationTime(100) - now) / 1000)
318
318
319 def isExpired(self, now):
319 def isExpired(self, now):
320 """Returns true if this record has expired."""
320 """Returns true if this record has expired."""
321 return self.getExpirationTime(100) <= now
321 return self.getExpirationTime(100) <= now
322
322
323 def isStale(self, now):
323 def isStale(self, now):
324 """Returns true if this record is at least half way expired."""
324 """Returns true if this record is at least half way expired."""
325 return self.getExpirationTime(50) <= now
325 return self.getExpirationTime(50) <= now
326
326
327 def resetTTL(self, other):
327 def resetTTL(self, other):
328 """Sets this record's TTL and created time to that of
328 """Sets this record's TTL and created time to that of
329 another record."""
329 another record."""
330 self.created = other.created
330 self.created = other.created
331 self.ttl = other.ttl
331 self.ttl = other.ttl
332
332
333 def write(self, out):
333 def write(self, out):
334 """Abstract method"""
334 """Abstract method"""
335 raise AbstractMethodException
335 raise AbstractMethodException
336
336
337 def toString(self, other):
337 def toString(self, other):
338 """String representation with additional information"""
338 """String representation with additional information"""
339 arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)
339 arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)
340 return DNSEntry.toString(self, "record", arg)
340 return DNSEntry.toString(self, "record", arg)
341
341
342 class DNSAddress(DNSRecord):
342 class DNSAddress(DNSRecord):
343 """A DNS address record"""
343 """A DNS address record"""
344
344
345 def __init__(self, name, type, clazz, ttl, address):
345 def __init__(self, name, type, clazz, ttl, address):
346 DNSRecord.__init__(self, name, type, clazz, ttl)
346 DNSRecord.__init__(self, name, type, clazz, ttl)
347 self.address = address
347 self.address = address
348
348
349 def write(self, out):
349 def write(self, out):
350 """Used in constructing an outgoing packet"""
350 """Used in constructing an outgoing packet"""
351 out.writeString(self.address, len(self.address))
351 out.writeString(self.address, len(self.address))
352
352
353 def __eq__(self, other):
353 def __eq__(self, other):
354 """Tests equality on address"""
354 """Tests equality on address"""
355 if isinstance(other, DNSAddress):
355 if isinstance(other, DNSAddress):
356 return self.address == other.address
356 return self.address == other.address
357 return 0
357 return 0
358
358
359 def __repr__(self):
359 def __repr__(self):
360 """String representation"""
360 """String representation"""
361 try:
361 try:
362 return socket.inet_ntoa(self.address)
362 return socket.inet_ntoa(self.address)
363 except Exception:
363 except Exception:
364 return self.address
364 return self.address
365
365
366 class DNSHinfo(DNSRecord):
366 class DNSHinfo(DNSRecord):
367 """A DNS host information record"""
367 """A DNS host information record"""
368
368
369 def __init__(self, name, type, clazz, ttl, cpu, os):
369 def __init__(self, name, type, clazz, ttl, cpu, os):
370 DNSRecord.__init__(self, name, type, clazz, ttl)
370 DNSRecord.__init__(self, name, type, clazz, ttl)
371 self.cpu = cpu
371 self.cpu = cpu
372 self.os = os
372 self.os = os
373
373
374 def write(self, out):
374 def write(self, out):
375 """Used in constructing an outgoing packet"""
375 """Used in constructing an outgoing packet"""
376 out.writeString(self.cpu, len(self.cpu))
376 out.writeString(self.cpu, len(self.cpu))
377 out.writeString(self.os, len(self.os))
377 out.writeString(self.os, len(self.os))
378
378
379 def __eq__(self, other):
379 def __eq__(self, other):
380 """Tests equality on cpu and os"""
380 """Tests equality on cpu and os"""
381 if isinstance(other, DNSHinfo):
381 if isinstance(other, DNSHinfo):
382 return self.cpu == other.cpu and self.os == other.os
382 return self.cpu == other.cpu and self.os == other.os
383 return 0
383 return 0
384
384
385 def __repr__(self):
385 def __repr__(self):
386 """String representation"""
386 """String representation"""
387 return self.cpu + " " + self.os
387 return self.cpu + " " + self.os
388
388
389 class DNSPointer(DNSRecord):
389 class DNSPointer(DNSRecord):
390 """A DNS pointer record"""
390 """A DNS pointer record"""
391
391
392 def __init__(self, name, type, clazz, ttl, alias):
392 def __init__(self, name, type, clazz, ttl, alias):
393 DNSRecord.__init__(self, name, type, clazz, ttl)
393 DNSRecord.__init__(self, name, type, clazz, ttl)
394 self.alias = alias
394 self.alias = alias
395
395
396 def write(self, out):
396 def write(self, out):
397 """Used in constructing an outgoing packet"""
397 """Used in constructing an outgoing packet"""
398 out.writeName(self.alias)
398 out.writeName(self.alias)
399
399
400 def __eq__(self, other):
400 def __eq__(self, other):
401 """Tests equality on alias"""
401 """Tests equality on alias"""
402 if isinstance(other, DNSPointer):
402 if isinstance(other, DNSPointer):
403 return self.alias == other.alias
403 return self.alias == other.alias
404 return 0
404 return 0
405
405
406 def __repr__(self):
406 def __repr__(self):
407 """String representation"""
407 """String representation"""
408 return self.toString(self.alias)
408 return self.toString(self.alias)
409
409
410 class DNSText(DNSRecord):
410 class DNSText(DNSRecord):
411 """A DNS text record"""
411 """A DNS text record"""
412
412
413 def __init__(self, name, type, clazz, ttl, text):
413 def __init__(self, name, type, clazz, ttl, text):
414 DNSRecord.__init__(self, name, type, clazz, ttl)
414 DNSRecord.__init__(self, name, type, clazz, ttl)
415 self.text = text
415 self.text = text
416
416
417 def write(self, out):
417 def write(self, out):
418 """Used in constructing an outgoing packet"""
418 """Used in constructing an outgoing packet"""
419 out.writeString(self.text, len(self.text))
419 out.writeString(self.text, len(self.text))
420
420
421 def __eq__(self, other):
421 def __eq__(self, other):
422 """Tests equality on text"""
422 """Tests equality on text"""
423 if isinstance(other, DNSText):
423 if isinstance(other, DNSText):
424 return self.text == other.text
424 return self.text == other.text
425 return 0
425 return 0
426
426
427 def __repr__(self):
427 def __repr__(self):
428 """String representation"""
428 """String representation"""
429 if len(self.text) > 10:
429 if len(self.text) > 10:
430 return self.toString(self.text[:7] + "...")
430 return self.toString(self.text[:7] + "...")
431 else:
431 else:
432 return self.toString(self.text)
432 return self.toString(self.text)
433
433
434 class DNSService(DNSRecord):
434 class DNSService(DNSRecord):
435 """A DNS service record"""
435 """A DNS service record"""
436
436
437 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
437 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
438 DNSRecord.__init__(self, name, type, clazz, ttl)
438 DNSRecord.__init__(self, name, type, clazz, ttl)
439 self.priority = priority
439 self.priority = priority
440 self.weight = weight
440 self.weight = weight
441 self.port = port
441 self.port = port
442 self.server = server
442 self.server = server
443
443
444 def write(self, out):
444 def write(self, out):
445 """Used in constructing an outgoing packet"""
445 """Used in constructing an outgoing packet"""
446 out.writeShort(self.priority)
446 out.writeShort(self.priority)
447 out.writeShort(self.weight)
447 out.writeShort(self.weight)
448 out.writeShort(self.port)
448 out.writeShort(self.port)
449 out.writeName(self.server)
449 out.writeName(self.server)
450
450
451 def __eq__(self, other):
451 def __eq__(self, other):
452 """Tests equality on priority, weight, port and server"""
452 """Tests equality on priority, weight, port and server"""
453 if isinstance(other, DNSService):
453 if isinstance(other, DNSService):
454 return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server
454 return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server
455 return 0
455 return 0
456
456
457 def __repr__(self):
457 def __repr__(self):
458 """String representation"""
458 """String representation"""
459 return self.toString("%s:%s" % (self.server, self.port))
459 return self.toString("%s:%s" % (self.server, self.port))
460
460
461 class DNSIncoming(object):
461 class DNSIncoming(object):
462 """Object representation of an incoming DNS packet"""
462 """Object representation of an incoming DNS packet"""
463
463
464 def __init__(self, data):
464 def __init__(self, data):
465 """Constructor from string holding bytes of packet"""
465 """Constructor from string holding bytes of packet"""
466 self.offset = 0
466 self.offset = 0
467 self.data = data
467 self.data = data
468 self.questions = []
468 self.questions = []
469 self.answers = []
469 self.answers = []
470 self.numQuestions = 0
470 self.numQuestions = 0
471 self.numAnswers = 0
471 self.numAnswers = 0
472 self.numAuthorities = 0
472 self.numAuthorities = 0
473 self.numAdditionals = 0
473 self.numAdditionals = 0
474
474
475 self.readHeader()
475 self.readHeader()
476 self.readQuestions()
476 self.readQuestions()
477 self.readOthers()
477 self.readOthers()
478
478
479 def readHeader(self):
479 def readHeader(self):
480 """Reads header portion of packet"""
480 """Reads header portion of packet"""
481 format = '!HHHHHH'
481 format = '!HHHHHH'
482 length = struct.calcsize(format)
482 length = struct.calcsize(format)
483 info = struct.unpack(format, self.data[self.offset:self.offset+length])
483 info = struct.unpack(format, self.data[self.offset:self.offset+length])
484 self.offset += length
484 self.offset += length
485
485
486 self.id = info[0]
486 self.id = info[0]
487 self.flags = info[1]
487 self.flags = info[1]
488 self.numQuestions = info[2]
488 self.numQuestions = info[2]
489 self.numAnswers = info[3]
489 self.numAnswers = info[3]
490 self.numAuthorities = info[4]
490 self.numAuthorities = info[4]
491 self.numAdditionals = info[5]
491 self.numAdditionals = info[5]
492
492
493 def readQuestions(self):
493 def readQuestions(self):
494 """Reads questions section of packet"""
494 """Reads questions section of packet"""
495 format = '!HH'
495 format = '!HH'
496 length = struct.calcsize(format)
496 length = struct.calcsize(format)
497 for i in range(0, self.numQuestions):
497 for i in range(0, self.numQuestions):
498 name = self.readName()
498 name = self.readName()
499 info = struct.unpack(format, self.data[self.offset:self.offset+length])
499 info = struct.unpack(format, self.data[self.offset:self.offset+length])
500 self.offset += length
500 self.offset += length
501
501
502 try:
502 try:
503 question = DNSQuestion(name, info[0], info[1])
503 question = DNSQuestion(name, info[0], info[1])
504 self.questions.append(question)
504 self.questions.append(question)
505 except NonLocalNameException:
505 except NonLocalNameException:
506 pass
506 pass
507
507
508 def readInt(self):
508 def readInt(self):
509 """Reads an integer from the packet"""
509 """Reads an integer from the packet"""
510 format = '!I'
510 format = '!I'
511 length = struct.calcsize(format)
511 length = struct.calcsize(format)
512 info = struct.unpack(format, self.data[self.offset:self.offset+length])
512 info = struct.unpack(format, self.data[self.offset:self.offset+length])
513 self.offset += length
513 self.offset += length
514 return info[0]
514 return info[0]
515
515
516 def readCharacterString(self):
516 def readCharacterString(self):
517 """Reads a character string from the packet"""
517 """Reads a character string from the packet"""
518 length = ord(self.data[self.offset])
518 length = ord(self.data[self.offset])
519 self.offset += 1
519 self.offset += 1
520 return self.readString(length)
520 return self.readString(length)
521
521
522 def readString(self, len):
522 def readString(self, len):
523 """Reads a string of a given length from the packet"""
523 """Reads a string of a given length from the packet"""
524 format = '!' + str(len) + 's'
524 format = '!' + str(len) + 's'
525 length = struct.calcsize(format)
525 length = struct.calcsize(format)
526 info = struct.unpack(format, self.data[self.offset:self.offset+length])
526 info = struct.unpack(format, self.data[self.offset:self.offset+length])
527 self.offset += length
527 self.offset += length
528 return info[0]
528 return info[0]
529
529
530 def readUnsignedShort(self):
530 def readUnsignedShort(self):
531 """Reads an unsigned short from the packet"""
531 """Reads an unsigned short from the packet"""
532 format = '!H'
532 format = '!H'
533 length = struct.calcsize(format)
533 length = struct.calcsize(format)
534 info = struct.unpack(format, self.data[self.offset:self.offset+length])
534 info = struct.unpack(format, self.data[self.offset:self.offset+length])
535 self.offset += length
535 self.offset += length
536 return info[0]
536 return info[0]
537
537
538 def readOthers(self):
538 def readOthers(self):
539 """Reads the answers, authorities and additionals section of the packet"""
539 """Reads the answers, authorities and additionals section of the packet"""
540 format = '!HHiH'
540 format = '!HHiH'
541 length = struct.calcsize(format)
541 length = struct.calcsize(format)
542 n = self.numAnswers + self.numAuthorities + self.numAdditionals
542 n = self.numAnswers + self.numAuthorities + self.numAdditionals
543 for i in range(0, n):
543 for i in range(0, n):
544 domain = self.readName()
544 domain = self.readName()
545 info = struct.unpack(format, self.data[self.offset:self.offset+length])
545 info = struct.unpack(format, self.data[self.offset:self.offset+length])
546 self.offset += length
546 self.offset += length
547
547
548 rec = None
548 rec = None
549 if info[0] == _TYPE_A:
549 if info[0] == _TYPE_A:
550 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4))
550 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4))
551 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
551 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
552 rec = DNSPointer(domain, info[0], info[1], info[2], self.readName())
552 rec = DNSPointer(domain, info[0], info[1], info[2], self.readName())
553 elif info[0] == _TYPE_TXT:
553 elif info[0] == _TYPE_TXT:
554 rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3]))
554 rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3]))
555 elif info[0] == _TYPE_SRV:
555 elif info[0] == _TYPE_SRV:
556 rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName())
556 rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName())
557 elif info[0] == _TYPE_HINFO:
557 elif info[0] == _TYPE_HINFO:
558 rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString())
558 rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString())
559 elif info[0] == _TYPE_AAAA:
559 elif info[0] == _TYPE_AAAA:
560 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16))
560 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16))
561 else:
561 else:
562 # Try to ignore types we don't know about
562 # Try to ignore types we don't know about
563 # this may mean the rest of the name is
563 # this may mean the rest of the name is
564 # unable to be parsed, and may show errors
564 # unable to be parsed, and may show errors
565 # so this is left for debugging. New types
565 # so this is left for debugging. New types
566 # encountered need to be parsed properly.
566 # encountered need to be parsed properly.
567 #
567 #
568 #print "UNKNOWN TYPE = " + str(info[0])
568 #print "UNKNOWN TYPE = " + str(info[0])
569 #raise BadTypeInNameException
569 #raise BadTypeInNameException
570 self.offset += info[3]
570 self.offset += info[3]
571
571
572 if rec is not None:
572 if rec is not None:
573 self.answers.append(rec)
573 self.answers.append(rec)
574
574
575 def isQuery(self):
575 def isQuery(self):
576 """Returns true if this is a query"""
576 """Returns true if this is a query"""
577 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
577 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
578
578
579 def isResponse(self):
579 def isResponse(self):
580 """Returns true if this is a response"""
580 """Returns true if this is a response"""
581 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
581 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
582
582
583 def readUTF(self, offset, len):
583 def readUTF(self, offset, len):
584 """Reads a UTF-8 string of a given length from the packet"""
584 """Reads a UTF-8 string of a given length from the packet"""
585 return self.data[offset:offset+len].decode('utf-8')
585 return self.data[offset:offset+len].decode('utf-8')
586
586
587 def readName(self):
587 def readName(self):
588 """Reads a domain name from the packet"""
588 """Reads a domain name from the packet"""
589 result = ''
589 result = ''
590 off = self.offset
590 off = self.offset
591 next = -1
591 next = -1
592 first = off
592 first = off
593
593
594 while True:
594 while True:
595 len = ord(self.data[off])
595 len = ord(self.data[off])
596 off += 1
596 off += 1
597 if len == 0:
597 if len == 0:
598 break
598 break
599 t = len & 0xC0
599 t = len & 0xC0
600 if t == 0x00:
600 if t == 0x00:
601 result = ''.join((result, self.readUTF(off, len) + '.'))
601 result = ''.join((result, self.readUTF(off, len) + '.'))
602 off += len
602 off += len
603 elif t == 0xC0:
603 elif t == 0xC0:
604 if next < 0:
604 if next < 0:
605 next = off + 1
605 next = off + 1
606 off = ((len & 0x3F) << 8) | ord(self.data[off])
606 off = ((len & 0x3F) << 8) | ord(self.data[off])
607 if off >= first:
607 if off >= first:
608 raise BadDomainNameCircular(off)
608 raise BadDomainNameCircular(off)
609 first = off
609 first = off
610 else:
610 else:
611 raise BadDomainName(off)
611 raise BadDomainName(off)
612
612
613 if next >= 0:
613 if next >= 0:
614 self.offset = next
614 self.offset = next
615 else:
615 else:
616 self.offset = off
616 self.offset = off
617
617
618 return result
618 return result
619
619
620
620
621 class DNSOutgoing(object):
621 class DNSOutgoing(object):
622 """Object representation of an outgoing packet"""
622 """Object representation of an outgoing packet"""
623
623
624 def __init__(self, flags, multicast = 1):
624 def __init__(self, flags, multicast = 1):
625 self.finished = 0
625 self.finished = 0
626 self.id = 0
626 self.id = 0
627 self.multicast = multicast
627 self.multicast = multicast
628 self.flags = flags
628 self.flags = flags
629 self.names = {}
629 self.names = {}
630 self.data = []
630 self.data = []
631 self.size = 12
631 self.size = 12
632
632
633 self.questions = []
633 self.questions = []
634 self.answers = []
634 self.answers = []
635 self.authorities = []
635 self.authorities = []
636 self.additionals = []
636 self.additionals = []
637
637
638 def addQuestion(self, record):
638 def addQuestion(self, record):
639 """Adds a question"""
639 """Adds a question"""
640 self.questions.append(record)
640 self.questions.append(record)
641
641
642 def addAnswer(self, inp, record):
642 def addAnswer(self, inp, record):
643 """Adds an answer"""
643 """Adds an answer"""
644 if not record.suppressedBy(inp):
644 if not record.suppressedBy(inp):
645 self.addAnswerAtTime(record, 0)
645 self.addAnswerAtTime(record, 0)
646
646
647 def addAnswerAtTime(self, record, now):
647 def addAnswerAtTime(self, record, now):
648 """Adds an answer if if does not expire by a certain time"""
648 """Adds an answer if if does not expire by a certain time"""
649 if record is not None:
649 if record is not None:
650 if now == 0 or not record.isExpired(now):
650 if now == 0 or not record.isExpired(now):
651 self.answers.append((record, now))
651 self.answers.append((record, now))
652
652
653 def addAuthoritativeAnswer(self, record):
653 def addAuthoritativeAnswer(self, record):
654 """Adds an authoritative answer"""
654 """Adds an authoritative answer"""
655 self.authorities.append(record)
655 self.authorities.append(record)
656
656
657 def addAdditionalAnswer(self, record):
657 def addAdditionalAnswer(self, record):
658 """Adds an additional answer"""
658 """Adds an additional answer"""
659 self.additionals.append(record)
659 self.additionals.append(record)
660
660
661 def writeByte(self, value):
661 def writeByte(self, value):
662 """Writes a single byte to the packet"""
662 """Writes a single byte to the packet"""
663 format = '!c'
663 format = '!c'
664 self.data.append(struct.pack(format, chr(value)))
664 self.data.append(struct.pack(format, chr(value)))
665 self.size += 1
665 self.size += 1
666
666
667 def insertShort(self, index, value):
667 def insertShort(self, index, value):
668 """Inserts an unsigned short in a certain position in the packet"""
668 """Inserts an unsigned short in a certain position in the packet"""
669 format = '!H'
669 format = '!H'
670 self.data.insert(index, struct.pack(format, value))
670 self.data.insert(index, struct.pack(format, value))
671 self.size += 2
671 self.size += 2
672
672
673 def writeShort(self, value):
673 def writeShort(self, value):
674 """Writes an unsigned short to the packet"""
674 """Writes an unsigned short to the packet"""
675 format = '!H'
675 format = '!H'
676 self.data.append(struct.pack(format, value))
676 self.data.append(struct.pack(format, value))
677 self.size += 2
677 self.size += 2
678
678
679 def writeInt(self, value):
679 def writeInt(self, value):
680 """Writes an unsigned integer to the packet"""
680 """Writes an unsigned integer to the packet"""
681 format = '!I'
681 format = '!I'
682 self.data.append(struct.pack(format, int(value)))
682 self.data.append(struct.pack(format, int(value)))
683 self.size += 4
683 self.size += 4
684
684
685 def writeString(self, value, length):
685 def writeString(self, value, length):
686 """Writes a string to the packet"""
686 """Writes a string to the packet"""
687 format = '!' + str(length) + 's'
687 format = '!' + str(length) + 's'
688 self.data.append(struct.pack(format, value))
688 self.data.append(struct.pack(format, value))
689 self.size += length
689 self.size += length
690
690
691 def writeUTF(self, s):
691 def writeUTF(self, s):
692 """Writes a UTF-8 string of a given length to the packet"""
692 """Writes a UTF-8 string of a given length to the packet"""
693 utfstr = s.encode('utf-8')
693 utfstr = s.encode('utf-8')
694 length = len(utfstr)
694 length = len(utfstr)
695 if length > 64:
695 if length > 64:
696 raise NamePartTooLongException
696 raise NamePartTooLongException
697 self.writeByte(length)
697 self.writeByte(length)
698 self.writeString(utfstr, length)
698 self.writeString(utfstr, length)
699
699
700 def writeName(self, name):
700 def writeName(self, name):
701 """Writes a domain name to the packet"""
701 """Writes a domain name to the packet"""
702
702
703 try:
703 try:
704 # Find existing instance of this name in packet
704 # Find existing instance of this name in packet
705 #
705 #
706 index = self.names[name]
706 index = self.names[name]
707 except KeyError:
707 except KeyError:
708 # No record of this name already, so write it
708 # No record of this name already, so write it
709 # out as normal, recording the location of the name
709 # out as normal, recording the location of the name
710 # for future pointers to it.
710 # for future pointers to it.
711 #
711 #
712 self.names[name] = self.size
712 self.names[name] = self.size
713 parts = name.split('.')
713 parts = name.split('.')
714 if parts[-1] == '':
714 if parts[-1] == '':
715 parts = parts[:-1]
715 parts = parts[:-1]
716 for part in parts:
716 for part in parts:
717 self.writeUTF(part)
717 self.writeUTF(part)
718 self.writeByte(0)
718 self.writeByte(0)
719 return
719 return
720
720
721 # An index was found, so write a pointer to it
721 # An index was found, so write a pointer to it
722 #
722 #
723 self.writeByte((index >> 8) | 0xC0)
723 self.writeByte((index >> 8) | 0xC0)
724 self.writeByte(index)
724 self.writeByte(index)
725
725
726 def writeQuestion(self, question):
726 def writeQuestion(self, question):
727 """Writes a question to the packet"""
727 """Writes a question to the packet"""
728 self.writeName(question.name)
728 self.writeName(question.name)
729 self.writeShort(question.type)
729 self.writeShort(question.type)
730 self.writeShort(question.clazz)
730 self.writeShort(question.clazz)
731
731
732 def writeRecord(self, record, now):
732 def writeRecord(self, record, now):
733 """Writes a record (answer, authoritative answer, additional) to
733 """Writes a record (answer, authoritative answer, additional) to
734 the packet"""
734 the packet"""
735 self.writeName(record.name)
735 self.writeName(record.name)
736 self.writeShort(record.type)
736 self.writeShort(record.type)
737 if record.unique and self.multicast:
737 if record.unique and self.multicast:
738 self.writeShort(record.clazz | _CLASS_UNIQUE)
738 self.writeShort(record.clazz | _CLASS_UNIQUE)
739 else:
739 else:
740 self.writeShort(record.clazz)
740 self.writeShort(record.clazz)
741 if now == 0:
741 if now == 0:
742 self.writeInt(record.ttl)
742 self.writeInt(record.ttl)
743 else:
743 else:
744 self.writeInt(record.getRemainingTTL(now))
744 self.writeInt(record.getRemainingTTL(now))
745 index = len(self.data)
745 index = len(self.data)
746 # Adjust size for the short we will write before this record
746 # Adjust size for the short we will write before this record
747 #
747 #
748 self.size += 2
748 self.size += 2
749 record.write(self)
749 record.write(self)
750 self.size -= 2
750 self.size -= 2
751
751
752 length = len(''.join(self.data[index:]))
752 length = len(''.join(self.data[index:]))
753 self.insertShort(index, length) # Here is the short we adjusted for
753 self.insertShort(index, length) # Here is the short we adjusted for
754
754
755 def packet(self):
755 def packet(self):
756 """Returns a string containing the packet's bytes
756 """Returns a string containing the packet's bytes
757
757
758 No further parts should be added to the packet once this
758 No further parts should be added to the packet once this
759 is done."""
759 is done."""
760 if not self.finished:
760 if not self.finished:
761 self.finished = 1
761 self.finished = 1
762 for question in self.questions:
762 for question in self.questions:
763 self.writeQuestion(question)
763 self.writeQuestion(question)
764 for answer, time in self.answers:
764 for answer, time in self.answers:
765 self.writeRecord(answer, time)
765 self.writeRecord(answer, time)
766 for authority in self.authorities:
766 for authority in self.authorities:
767 self.writeRecord(authority, 0)
767 self.writeRecord(authority, 0)
768 for additional in self.additionals:
768 for additional in self.additionals:
769 self.writeRecord(additional, 0)
769 self.writeRecord(additional, 0)
770
770
771 self.insertShort(0, len(self.additionals))
771 self.insertShort(0, len(self.additionals))
772 self.insertShort(0, len(self.authorities))
772 self.insertShort(0, len(self.authorities))
773 self.insertShort(0, len(self.answers))
773 self.insertShort(0, len(self.answers))
774 self.insertShort(0, len(self.questions))
774 self.insertShort(0, len(self.questions))
775 self.insertShort(0, self.flags)
775 self.insertShort(0, self.flags)
776 if self.multicast:
776 if self.multicast:
777 self.insertShort(0, 0)
777 self.insertShort(0, 0)
778 else:
778 else:
779 self.insertShort(0, self.id)
779 self.insertShort(0, self.id)
780 return ''.join(self.data)
780 return ''.join(self.data)
781
781
782
782
783 class DNSCache(object):
783 class DNSCache(object):
784 """A cache of DNS entries"""
784 """A cache of DNS entries"""
785
785
786 def __init__(self):
786 def __init__(self):
787 self.cache = {}
787 self.cache = {}
788
788
789 def add(self, entry):
789 def add(self, entry):
790 """Adds an entry"""
790 """Adds an entry"""
791 try:
791 try:
792 list = self.cache[entry.key]
792 list = self.cache[entry.key]
793 except KeyError:
793 except KeyError:
794 list = self.cache[entry.key] = []
794 list = self.cache[entry.key] = []
795 list.append(entry)
795 list.append(entry)
796
796
797 def remove(self, entry):
797 def remove(self, entry):
798 """Removes an entry"""
798 """Removes an entry"""
799 try:
799 try:
800 list = self.cache[entry.key]
800 list = self.cache[entry.key]
801 list.remove(entry)
801 list.remove(entry)
802 except KeyError:
802 except KeyError:
803 pass
803 pass
804
804
805 def get(self, entry):
805 def get(self, entry):
806 """Gets an entry by key. Will return None if there is no
806 """Gets an entry by key. Will return None if there is no
807 matching entry."""
807 matching entry."""
808 try:
808 try:
809 list = self.cache[entry.key]
809 list = self.cache[entry.key]
810 return list[list.index(entry)]
810 return list[list.index(entry)]
811 except (KeyError, ValueError):
811 except (KeyError, ValueError):
812 return None
812 return None
813
813
814 def getByDetails(self, name, type, clazz):
814 def getByDetails(self, name, type, clazz):
815 """Gets an entry by details. Will return None if there is
815 """Gets an entry by details. Will return None if there is
816 no matching entry."""
816 no matching entry."""
817 entry = DNSEntry(name, type, clazz)
817 entry = DNSEntry(name, type, clazz)
818 return self.get(entry)
818 return self.get(entry)
819
819
820 def entriesWithName(self, name):
820 def entriesWithName(self, name):
821 """Returns a list of entries whose key matches the name."""
821 """Returns a list of entries whose key matches the name."""
822 try:
822 try:
823 return self.cache[name]
823 return self.cache[name]
824 except KeyError:
824 except KeyError:
825 return []
825 return []
826
826
827 def entries(self):
827 def entries(self):
828 """Returns a list of all entries"""
828 """Returns a list of all entries"""
829 def add(x, y): return x+y
829 def add(x, y): return x+y
830 try:
830 try:
831 return reduce(add, self.cache.values())
831 return reduce(add, self.cache.values())
832 except Exception:
832 except Exception:
833 return []
833 return []
834
834
835
835
836 class Engine(threading.Thread):
836 class Engine(threading.Thread):
837 """An engine wraps read access to sockets, allowing objects that
837 """An engine wraps read access to sockets, allowing objects that
838 need to receive data from sockets to be called back when the
838 need to receive data from sockets to be called back when the
839 sockets are ready.
839 sockets are ready.
840
840
841 A reader needs a handle_read() method, which is called when the socket
841 A reader needs a handle_read() method, which is called when the socket
842 it is interested in is ready for reading.
842 it is interested in is ready for reading.
843
843
844 Writers are not implemented here, because we only send short
844 Writers are not implemented here, because we only send short
845 packets.
845 packets.
846 """
846 """
847
847
848 def __init__(self, zeroconf):
848 def __init__(self, zeroconf):
849 threading.Thread.__init__(self)
849 threading.Thread.__init__(self)
850 self.zeroconf = zeroconf
850 self.zeroconf = zeroconf
851 self.readers = {} # maps socket to reader
851 self.readers = {} # maps socket to reader
852 self.timeout = 5
852 self.timeout = 5
853 self.condition = threading.Condition()
853 self.condition = threading.Condition()
854 self.start()
854 self.start()
855
855
856 def run(self):
856 def run(self):
857 while not globals()['_GLOBAL_DONE']:
857 while not globals()['_GLOBAL_DONE']:
858 rs = self.getReaders()
858 rs = self.getReaders()
859 if len(rs) == 0:
859 if len(rs) == 0:
860 # No sockets to manage, but we wait for the timeout
860 # No sockets to manage, but we wait for the timeout
861 # or addition of a socket
861 # or addition of a socket
862 #
862 #
863 self.condition.acquire()
863 self.condition.acquire()
864 self.condition.wait(self.timeout)
864 self.condition.wait(self.timeout)
865 self.condition.release()
865 self.condition.release()
866 else:
866 else:
867 try:
867 try:
868 rr, wr, er = select.select(rs, [], [], self.timeout)
868 rr, wr, er = select.select(rs, [], [], self.timeout)
869 for socket in rr:
869 for socket in rr:
870 try:
870 try:
871 self.readers[socket].handle_read()
871 self.readers[socket].handle_read()
872 except Exception:
872 except Exception:
873 if not globals()['_GLOBAL_DONE']:
873 if not globals()['_GLOBAL_DONE']:
874 traceback.print_exc()
874 traceback.print_exc()
875 except Exception:
875 except Exception:
876 pass
876 pass
877
877
878 def getReaders(self):
878 def getReaders(self):
879 self.condition.acquire()
879 self.condition.acquire()
880 result = self.readers.keys()
880 result = self.readers.keys()
881 self.condition.release()
881 self.condition.release()
882 return result
882 return result
883
883
884 def addReader(self, reader, socket):
884 def addReader(self, reader, socket):
885 self.condition.acquire()
885 self.condition.acquire()
886 self.readers[socket] = reader
886 self.readers[socket] = reader
887 self.condition.notify()
887 self.condition.notify()
888 self.condition.release()
888 self.condition.release()
889
889
890 def delReader(self, socket):
890 def delReader(self, socket):
891 self.condition.acquire()
891 self.condition.acquire()
892 del(self.readers[socket])
892 del(self.readers[socket])
893 self.condition.notify()
893 self.condition.notify()
894 self.condition.release()
894 self.condition.release()
895
895
896 def notify(self):
896 def notify(self):
897 self.condition.acquire()
897 self.condition.acquire()
898 self.condition.notify()
898 self.condition.notify()
899 self.condition.release()
899 self.condition.release()
900
900
901 class Listener(object):
901 class Listener(object):
902 """A Listener is used by this module to listen on the multicast
902 """A Listener is used by this module to listen on the multicast
903 group to which DNS messages are sent, allowing the implementation
903 group to which DNS messages are sent, allowing the implementation
904 to cache information as it arrives.
904 to cache information as it arrives.
905
905
906 It requires registration with an Engine object in order to have
906 It requires registration with an Engine object in order to have
907 the read() method called when a socket is available for reading."""
907 the read() method called when a socket is available for reading."""
908
908
909 def __init__(self, zeroconf):
909 def __init__(self, zeroconf):
910 self.zeroconf = zeroconf
910 self.zeroconf = zeroconf
911 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
911 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
912
912
913 def handle_read(self):
913 def handle_read(self):
914 data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
914 data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
915 self.data = data
915 self.data = data
916 msg = DNSIncoming(data)
916 msg = DNSIncoming(data)
917 if msg.isQuery():
917 if msg.isQuery():
918 # Always multicast responses
918 # Always multicast responses
919 #
919 #
920 if port == _MDNS_PORT:
920 if port == _MDNS_PORT:
921 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
921 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
922 # If it's not a multicast query, reply via unicast
922 # If it's not a multicast query, reply via unicast
923 # and multicast
923 # and multicast
924 #
924 #
925 elif port == _DNS_PORT:
925 elif port == _DNS_PORT:
926 self.zeroconf.handleQuery(msg, addr, port)
926 self.zeroconf.handleQuery(msg, addr, port)
927 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
927 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
928 else:
928 else:
929 self.zeroconf.handleResponse(msg)
929 self.zeroconf.handleResponse(msg)
930
930
931
931
932 class Reaper(threading.Thread):
932 class Reaper(threading.Thread):
933 """A Reaper is used by this module to remove cache entries that
933 """A Reaper is used by this module to remove cache entries that
934 have expired."""
934 have expired."""
935
935
936 def __init__(self, zeroconf):
936 def __init__(self, zeroconf):
937 threading.Thread.__init__(self)
937 threading.Thread.__init__(self)
938 self.zeroconf = zeroconf
938 self.zeroconf = zeroconf
939 self.start()
939 self.start()
940
940
941 def run(self):
941 def run(self):
942 while True:
942 while True:
943 self.zeroconf.wait(10 * 1000)
943 self.zeroconf.wait(10 * 1000)
944 if globals()['_GLOBAL_DONE']:
944 if globals()['_GLOBAL_DONE']:
945 return
945 return
946 now = currentTimeMillis()
946 now = currentTimeMillis()
947 for record in self.zeroconf.cache.entries():
947 for record in self.zeroconf.cache.entries():
948 if record.isExpired(now):
948 if record.isExpired(now):
949 self.zeroconf.updateRecord(now, record)
949 self.zeroconf.updateRecord(now, record)
950 self.zeroconf.cache.remove(record)
950 self.zeroconf.cache.remove(record)
951
951
952
952
953 class ServiceBrowser(threading.Thread):
953 class ServiceBrowser(threading.Thread):
954 """Used to browse for a service of a specific type.
954 """Used to browse for a service of a specific type.
955
955
956 The listener object will have its addService() and
956 The listener object will have its addService() and
957 removeService() methods called when this browser
957 removeService() methods called when this browser
958 discovers changes in the services availability."""
958 discovers changes in the services availability."""
959
959
960 def __init__(self, zeroconf, type, listener):
960 def __init__(self, zeroconf, type, listener):
961 """Creates a browser for a specific type"""
961 """Creates a browser for a specific type"""
962 threading.Thread.__init__(self)
962 threading.Thread.__init__(self)
963 self.zeroconf = zeroconf
963 self.zeroconf = zeroconf
964 self.type = type
964 self.type = type
965 self.listener = listener
965 self.listener = listener
966 self.services = {}
966 self.services = {}
967 self.nextTime = currentTimeMillis()
967 self.nextTime = currentTimeMillis()
968 self.delay = _BROWSER_TIME
968 self.delay = _BROWSER_TIME
969 self.list = []
969 self.list = []
970
970
971 self.done = 0
971 self.done = 0
972
972
973 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
973 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
974 self.start()
974 self.start()
975
975
976 def updateRecord(self, zeroconf, now, record):
976 def updateRecord(self, zeroconf, now, record):
977 """Callback invoked by Zeroconf when new information arrives.
977 """Callback invoked by Zeroconf when new information arrives.
978
978
979 Updates information required by browser in the Zeroconf cache."""
979 Updates information required by browser in the Zeroconf cache."""
980 if record.type == _TYPE_PTR and record.name == self.type:
980 if record.type == _TYPE_PTR and record.name == self.type:
981 expired = record.isExpired(now)
981 expired = record.isExpired(now)
982 try:
982 try:
983 oldrecord = self.services[record.alias.lower()]
983 oldrecord = self.services[record.alias.lower()]
984 if not expired:
984 if not expired:
985 oldrecord.resetTTL(record)
985 oldrecord.resetTTL(record)
986 else:
986 else:
987 del(self.services[record.alias.lower()])
987 del(self.services[record.alias.lower()])
988 callback = lambda x: self.listener.removeService(x, self.type, record.alias)
988 callback = lambda x: self.listener.removeService(x, self.type, record.alias)
989 self.list.append(callback)
989 self.list.append(callback)
990 return
990 return
991 except Exception:
991 except Exception:
992 if not expired:
992 if not expired:
993 self.services[record.alias.lower()] = record
993 self.services[record.alias.lower()] = record
994 callback = lambda x: self.listener.addService(x, self.type, record.alias)
994 callback = lambda x: self.listener.addService(x, self.type, record.alias)
995 self.list.append(callback)
995 self.list.append(callback)
996
996
997 expires = record.getExpirationTime(75)
997 expires = record.getExpirationTime(75)
998 if expires < self.nextTime:
998 if expires < self.nextTime:
999 self.nextTime = expires
999 self.nextTime = expires
1000
1000
1001 def cancel(self):
1001 def cancel(self):
1002 self.done = 1
1002 self.done = 1
1003 self.zeroconf.notifyAll()
1003 self.zeroconf.notifyAll()
1004
1004
1005 def run(self):
1005 def run(self):
1006 while True:
1006 while True:
1007 event = None
1007 event = None
1008 now = currentTimeMillis()
1008 now = currentTimeMillis()
1009 if len(self.list) == 0 and self.nextTime > now:
1009 if len(self.list) == 0 and self.nextTime > now:
1010 self.zeroconf.wait(self.nextTime - now)
1010 self.zeroconf.wait(self.nextTime - now)
1011 if globals()['_GLOBAL_DONE'] or self.done:
1011 if globals()['_GLOBAL_DONE'] or self.done:
1012 return
1012 return
1013 now = currentTimeMillis()
1013 now = currentTimeMillis()
1014
1014
1015 if self.nextTime <= now:
1015 if self.nextTime <= now:
1016 out = DNSOutgoing(_FLAGS_QR_QUERY)
1016 out = DNSOutgoing(_FLAGS_QR_QUERY)
1017 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1017 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1018 for record in self.services.values():
1018 for record in self.services.values():
1019 if not record.isExpired(now):
1019 if not record.isExpired(now):
1020 out.addAnswerAtTime(record, now)
1020 out.addAnswerAtTime(record, now)
1021 self.zeroconf.send(out)
1021 self.zeroconf.send(out)
1022 self.nextTime = now + self.delay
1022 self.nextTime = now + self.delay
1023 self.delay = min(20 * 1000, self.delay * 2)
1023 self.delay = min(20 * 1000, self.delay * 2)
1024
1024
1025 if len(self.list) > 0:
1025 if len(self.list) > 0:
1026 event = self.list.pop(0)
1026 event = self.list.pop(0)
1027
1027
1028 if event is not None:
1028 if event is not None:
1029 event(self.zeroconf)
1029 event(self.zeroconf)
1030
1030
1031
1031
1032 class ServiceInfo(object):
1032 class ServiceInfo(object):
1033 """Service information"""
1033 """Service information"""
1034
1034
1035 def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
1035 def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
1036 """Create a service description.
1036 """Create a service description.
1037
1037
1038 type: fully qualified service type name
1038 type: fully qualified service type name
1039 name: fully qualified service name
1039 name: fully qualified service name
1040 address: IP address as unsigned short, network byte order
1040 address: IP address as unsigned short, network byte order
1041 port: port that the service runs on
1041 port: port that the service runs on
1042 weight: weight of the service
1042 weight: weight of the service
1043 priority: priority of the service
1043 priority: priority of the service
1044 properties: dictionary of properties (or a string holding the bytes for the text field)
1044 properties: dictionary of properties (or a string holding the bytes for the text field)
1045 server: fully qualified name for service host (defaults to name)"""
1045 server: fully qualified name for service host (defaults to name)"""
1046
1046
1047 if not name.endswith(type):
1047 if not name.endswith(type):
1048 raise BadTypeInNameException
1048 raise BadTypeInNameException
1049 self.type = type
1049 self.type = type
1050 self.name = name
1050 self.name = name
1051 self.address = address
1051 self.address = address
1052 self.port = port
1052 self.port = port
1053 self.weight = weight
1053 self.weight = weight
1054 self.priority = priority
1054 self.priority = priority
1055 if server:
1055 if server:
1056 self.server = server
1056 self.server = server
1057 else:
1057 else:
1058 self.server = name
1058 self.server = name
1059 self.setProperties(properties)
1059 self.setProperties(properties)
1060
1060
1061 def setProperties(self, properties):
1061 def setProperties(self, properties):
1062 """Sets properties and text of this info from a dictionary"""
1062 """Sets properties and text of this info from a dictionary"""
1063 if isinstance(properties, dict):
1063 if isinstance(properties, dict):
1064 self.properties = properties
1064 self.properties = properties
1065 list = []
1065 list = []
1066 result = ''
1066 result = ''
1067 for key in properties:
1067 for key in properties:
1068 value = properties[key]
1068 value = properties[key]
1069 if value is None:
1069 if value is None:
1070 suffix = ''
1070 suffix = ''
1071 elif isinstance(value, str):
1071 elif isinstance(value, str):
1072 suffix = value
1072 suffix = value
1073 elif isinstance(value, int):
1073 elif isinstance(value, int):
1074 if value:
1074 if value:
1075 suffix = 'true'
1075 suffix = 'true'
1076 else:
1076 else:
1077 suffix = 'false'
1077 suffix = 'false'
1078 else:
1078 else:
1079 suffix = ''
1079 suffix = ''
1080 list.append('='.join((key, suffix)))
1080 list.append('='.join((key, suffix)))
1081 for item in list:
1081 for item in list:
1082 result = ''.join((result, struct.pack('!c', chr(len(item))), item))
1082 result = ''.join((result, struct.pack('!c', chr(len(item))), item))
1083 self.text = result
1083 self.text = result
1084 else:
1084 else:
1085 self.text = properties
1085 self.text = properties
1086
1086
1087 def setText(self, text):
1087 def setText(self, text):
1088 """Sets properties and text given a text field"""
1088 """Sets properties and text given a text field"""
1089 self.text = text
1089 self.text = text
1090 try:
1090 try:
1091 result = {}
1091 result = {}
1092 end = len(text)
1092 end = len(text)
1093 index = 0
1093 index = 0
1094 strs = []
1094 strs = []
1095 while index < end:
1095 while index < end:
1096 length = ord(text[index])
1096 length = ord(text[index])
1097 index += 1
1097 index += 1
1098 strs.append(text[index:index+length])
1098 strs.append(text[index:index+length])
1099 index += length
1099 index += length
1100
1100
1101 for s in strs:
1101 for s in strs:
1102 eindex = s.find('=')
1102 eindex = s.find('=')
1103 if eindex == -1:
1103 if eindex == -1:
1104 # No equals sign at all
1104 # No equals sign at all
1105 key = s
1105 key = s
1106 value = 0
1106 value = 0
1107 else:
1107 else:
1108 key = s[:eindex]
1108 key = s[:eindex]
1109 value = s[eindex+1:]
1109 value = s[eindex+1:]
1110 if value == 'true':
1110 if value == 'true':
1111 value = 1
1111 value = 1
1112 elif value == 'false' or not value:
1112 elif value == 'false' or not value:
1113 value = 0
1113 value = 0
1114
1114
1115 # Only update non-existent properties
1115 # Only update non-existent properties
1116 if key and result.get(key) == None:
1116 if key and result.get(key) == None:
1117 result[key] = value
1117 result[key] = value
1118
1118
1119 self.properties = result
1119 self.properties = result
1120 except Exception:
1120 except Exception:
1121 traceback.print_exc()
1121 traceback.print_exc()
1122 self.properties = None
1122 self.properties = None
1123
1123
1124 def getType(self):
1124 def getType(self):
1125 """Type accessor"""
1125 """Type accessor"""
1126 return self.type
1126 return self.type
1127
1127
1128 def getName(self):
1128 def getName(self):
1129 """Name accessor"""
1129 """Name accessor"""
1130 if self.type is not None and self.name.endswith("." + self.type):
1130 if self.type is not None and self.name.endswith("." + self.type):
1131 return self.name[:len(self.name) - len(self.type) - 1]
1131 return self.name[:len(self.name) - len(self.type) - 1]
1132 return self.name
1132 return self.name
1133
1133
1134 def getAddress(self):
1134 def getAddress(self):
1135 """Address accessor"""
1135 """Address accessor"""
1136 return self.address
1136 return self.address
1137
1137
1138 def getPort(self):
1138 def getPort(self):
1139 """Port accessor"""
1139 """Port accessor"""
1140 return self.port
1140 return self.port
1141
1141
1142 def getPriority(self):
1142 def getPriority(self):
1143 """Priority accessor"""
1143 """Priority accessor"""
1144 return self.priority
1144 return self.priority
1145
1145
1146 def getWeight(self):
1146 def getWeight(self):
1147 """Weight accessor"""
1147 """Weight accessor"""
1148 return self.weight
1148 return self.weight
1149
1149
1150 def getProperties(self):
1150 def getProperties(self):
1151 """Properties accessor"""
1151 """Properties accessor"""
1152 return self.properties
1152 return self.properties
1153
1153
1154 def getText(self):
1154 def getText(self):
1155 """Text accessor"""
1155 """Text accessor"""
1156 return self.text
1156 return self.text
1157
1157
1158 def getServer(self):
1158 def getServer(self):
1159 """Server accessor"""
1159 """Server accessor"""
1160 return self.server
1160 return self.server
1161
1161
1162 def updateRecord(self, zeroconf, now, record):
1162 def updateRecord(self, zeroconf, now, record):
1163 """Updates service information from a DNS record"""
1163 """Updates service information from a DNS record"""
1164 if record is not None and not record.isExpired(now):
1164 if record is not None and not record.isExpired(now):
1165 if record.type == _TYPE_A:
1165 if record.type == _TYPE_A:
1166 #if record.name == self.name:
1166 #if record.name == self.name:
1167 if record.name == self.server:
1167 if record.name == self.server:
1168 self.address = record.address
1168 self.address = record.address
1169 elif record.type == _TYPE_SRV:
1169 elif record.type == _TYPE_SRV:
1170 if record.name == self.name:
1170 if record.name == self.name:
1171 self.server = record.server
1171 self.server = record.server
1172 self.port = record.port
1172 self.port = record.port
1173 self.weight = record.weight
1173 self.weight = record.weight
1174 self.priority = record.priority
1174 self.priority = record.priority
1175 #self.address = None
1175 #self.address = None
1176 self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1176 self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1177 elif record.type == _TYPE_TXT:
1177 elif record.type == _TYPE_TXT:
1178 if record.name == self.name:
1178 if record.name == self.name:
1179 self.setText(record.text)
1179 self.setText(record.text)
1180
1180
1181 def request(self, zeroconf, timeout):
1181 def request(self, zeroconf, timeout):
1182 """Returns true if the service could be discovered on the
1182 """Returns true if the service could be discovered on the
1183 network, and updates this object with details discovered.
1183 network, and updates this object with details discovered.
1184 """
1184 """
1185 now = currentTimeMillis()
1185 now = currentTimeMillis()
1186 delay = _LISTENER_TIME
1186 delay = _LISTENER_TIME
1187 next = now + delay
1187 next = now + delay
1188 last = now + timeout
1188 last = now + timeout
1189 result = 0
1189 result = 0
1190 try:
1190 try:
1191 zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1191 zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1192 while self.server is None or self.address is None or self.text is None:
1192 while self.server is None or self.address is None or self.text is None:
1193 if last <= now:
1193 if last <= now:
1194 return 0
1194 return 0
1195 if next <= now:
1195 if next <= now:
1196 out = DNSOutgoing(_FLAGS_QR_QUERY)
1196 out = DNSOutgoing(_FLAGS_QR_QUERY)
1197 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
1197 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
1198 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
1198 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
1199 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
1199 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
1200 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
1200 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
1201 if self.server is not None:
1201 if self.server is not None:
1202 out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
1202 out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
1203 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
1203 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
1204 zeroconf.send(out)
1204 zeroconf.send(out)
1205 next = now + delay
1205 next = now + delay
1206 delay = delay * 2
1206 delay = delay * 2
1207
1207
1208 zeroconf.wait(min(next, last) - now)
1208 zeroconf.wait(min(next, last) - now)
1209 now = currentTimeMillis()
1209 now = currentTimeMillis()
1210 result = 1
1210 result = 1
1211 finally:
1211 finally:
1212 zeroconf.removeListener(self)
1212 zeroconf.removeListener(self)
1213
1213
1214 return result
1214 return result
1215
1215
1216 def __eq__(self, other):
1216 def __eq__(self, other):
1217 """Tests equality of service name"""
1217 """Tests equality of service name"""
1218 if isinstance(other, ServiceInfo):
1218 if isinstance(other, ServiceInfo):
1219 return other.name == self.name
1219 return other.name == self.name
1220 return 0
1220 return 0
1221
1221
1222 def __ne__(self, other):
1222 def __ne__(self, other):
1223 """Non-equality test"""
1223 """Non-equality test"""
1224 return not self.__eq__(other)
1224 return not self.__eq__(other)
1225
1225
1226 def __repr__(self):
1226 def __repr__(self):
1227 """String representation"""
1227 """String representation"""
1228 result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
1228 result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
1229 if self.text is None:
1229 if self.text is None:
1230 result += "None"
1230 result += "None"
1231 else:
1231 else:
1232 if len(self.text) < 20:
1232 if len(self.text) < 20:
1233 result += self.text
1233 result += self.text
1234 else:
1234 else:
1235 result += self.text[:17] + "..."
1235 result += self.text[:17] + "..."
1236 result += "]"
1236 result += "]"
1237 return result
1237 return result
1238
1238
1239
1239
1240 class Zeroconf(object):
1240 class Zeroconf(object):
1241 """Implementation of Zeroconf Multicast DNS Service Discovery
1241 """Implementation of Zeroconf Multicast DNS Service Discovery
1242
1242
1243 Supports registration, unregistration, queries and browsing.
1243 Supports registration, unregistration, queries and browsing.
1244 """
1244 """
1245 def __init__(self, bindaddress=None):
1245 def __init__(self, bindaddress=None):
1246 """Creates an instance of the Zeroconf class, establishing
1246 """Creates an instance of the Zeroconf class, establishing
1247 multicast communications, listening and reaping threads."""
1247 multicast communications, listening and reaping threads."""
1248 globals()['_GLOBAL_DONE'] = 0
1248 globals()['_GLOBAL_DONE'] = 0
1249 if bindaddress is None:
1249 if bindaddress is None:
1250 self.intf = socket.gethostbyname(socket.gethostname())
1250 self.intf = socket.gethostbyname(socket.gethostname())
1251 else:
1251 else:
1252 self.intf = bindaddress
1252 self.intf = bindaddress
1253 self.group = ('', _MDNS_PORT)
1253 self.group = ('', _MDNS_PORT)
1254 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1254 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1255 try:
1255 try:
1256 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1256 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1257 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1257 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1258 except Exception:
1258 except Exception:
1259 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1259 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1260 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1260 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1261 # Volume 2"), but some BSD-derived systems require
1261 # Volume 2"), but some BSD-derived systems require
1262 # SO_REUSEPORT to be specified explicitly. Also, not all
1262 # SO_REUSEPORT to be specified explicitly. Also, not all
1263 # versions of Python have SO_REUSEPORT available. So
1263 # versions of Python have SO_REUSEPORT available. So
1264 # if you're on a BSD-based system, and haven't upgraded
1264 # if you're on a BSD-based system, and haven't upgraded
1265 # to Python 2.3 yet, you may find this library doesn't
1265 # to Python 2.3 yet, you may find this library doesn't
1266 # work as expected.
1266 # work as expected.
1267 #
1267 #
1268 pass
1268 pass
1269 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
1269 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
1270 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
1270 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
1271 try:
1271 try:
1272 self.socket.bind(self.group)
1272 self.socket.bind(self.group)
1273 except Exception:
1273 except Exception:
1274 # Some versions of linux raise an exception even though
1274 # Some versions of linux raise an exception even though
1275 # the SO_REUSE* options have been set, so ignore it
1275 # SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it
1276 #
1277 pass
1276 pass
1278 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1277 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1279
1278
1280 self.listeners = []
1279 self.listeners = []
1281 self.browsers = []
1280 self.browsers = []
1282 self.services = {}
1281 self.services = {}
1283 self.servicetypes = {}
1282 self.servicetypes = {}
1284
1283
1285 self.cache = DNSCache()
1284 self.cache = DNSCache()
1286
1285
1287 self.condition = threading.Condition()
1286 self.condition = threading.Condition()
1288
1287
1289 self.engine = Engine(self)
1288 self.engine = Engine(self)
1290 self.listener = Listener(self)
1289 self.listener = Listener(self)
1291 self.reaper = Reaper(self)
1290 self.reaper = Reaper(self)
1292
1291
1293 def isLoopback(self):
1292 def isLoopback(self):
1294 return self.intf.startswith("127.0.0.1")
1293 return self.intf.startswith("127.0.0.1")
1295
1294
1296 def isLinklocal(self):
1295 def isLinklocal(self):
1297 return self.intf.startswith("169.254.")
1296 return self.intf.startswith("169.254.")
1298
1297
1299 def wait(self, timeout):
1298 def wait(self, timeout):
1300 """Calling thread waits for a given number of milliseconds or
1299 """Calling thread waits for a given number of milliseconds or
1301 until notified."""
1300 until notified."""
1302 self.condition.acquire()
1301 self.condition.acquire()
1303 self.condition.wait(timeout/1000)
1302 self.condition.wait(timeout/1000)
1304 self.condition.release()
1303 self.condition.release()
1305
1304
1306 def notifyAll(self):
1305 def notifyAll(self):
1307 """Notifies all waiting threads"""
1306 """Notifies all waiting threads"""
1308 self.condition.acquire()
1307 self.condition.acquire()
1309 self.condition.notifyAll()
1308 self.condition.notifyAll()
1310 self.condition.release()
1309 self.condition.release()
1311
1310
1312 def getServiceInfo(self, type, name, timeout=3000):
1311 def getServiceInfo(self, type, name, timeout=3000):
1313 """Returns network's service information for a particular
1312 """Returns network's service information for a particular
1314 name and type, or None if no service matches by the timeout,
1313 name and type, or None if no service matches by the timeout,
1315 which defaults to 3 seconds."""
1314 which defaults to 3 seconds."""
1316 info = ServiceInfo(type, name)
1315 info = ServiceInfo(type, name)
1317 if info.request(self, timeout):
1316 if info.request(self, timeout):
1318 return info
1317 return info
1319 return None
1318 return None
1320
1319
1321 def addServiceListener(self, type, listener):
1320 def addServiceListener(self, type, listener):
1322 """Adds a listener for a particular service type. This object
1321 """Adds a listener for a particular service type. This object
1323 will then have its updateRecord method called when information
1322 will then have its updateRecord method called when information
1324 arrives for that type."""
1323 arrives for that type."""
1325 self.removeServiceListener(listener)
1324 self.removeServiceListener(listener)
1326 self.browsers.append(ServiceBrowser(self, type, listener))
1325 self.browsers.append(ServiceBrowser(self, type, listener))
1327
1326
1328 def removeServiceListener(self, listener):
1327 def removeServiceListener(self, listener):
1329 """Removes a listener from the set that is currently listening."""
1328 """Removes a listener from the set that is currently listening."""
1330 for browser in self.browsers:
1329 for browser in self.browsers:
1331 if browser.listener == listener:
1330 if browser.listener == listener:
1332 browser.cancel()
1331 browser.cancel()
1333 del(browser)
1332 del(browser)
1334
1333
1335 def registerService(self, info, ttl=_DNS_TTL):
1334 def registerService(self, info, ttl=_DNS_TTL):
1336 """Registers service information to the network with a default TTL
1335 """Registers service information to the network with a default TTL
1337 of 60 seconds. Zeroconf will then respond to requests for
1336 of 60 seconds. Zeroconf will then respond to requests for
1338 information for that service. The name of the service may be
1337 information for that service. The name of the service may be
1339 changed if needed to make it unique on the network."""
1338 changed if needed to make it unique on the network."""
1340 self.checkService(info)
1339 self.checkService(info)
1341 self.services[info.name.lower()] = info
1340 self.services[info.name.lower()] = info
1342 if self.servicetypes.has_key(info.type):
1341 if self.servicetypes.has_key(info.type):
1343 self.servicetypes[info.type]+=1
1342 self.servicetypes[info.type]+=1
1344 else:
1343 else:
1345 self.servicetypes[info.type]=1
1344 self.servicetypes[info.type]=1
1346 now = currentTimeMillis()
1345 now = currentTimeMillis()
1347 nextTime = now
1346 nextTime = now
1348 i = 0
1347 i = 0
1349 while i < 3:
1348 while i < 3:
1350 if now < nextTime:
1349 if now < nextTime:
1351 self.wait(nextTime - now)
1350 self.wait(nextTime - now)
1352 now = currentTimeMillis()
1351 now = currentTimeMillis()
1353 continue
1352 continue
1354 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1353 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1355 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
1354 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
1356 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0)
1355 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0)
1357 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
1356 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
1358 if info.address:
1357 if info.address:
1359 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
1358 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
1360 self.send(out)
1359 self.send(out)
1361 i += 1
1360 i += 1
1362 nextTime += _REGISTER_TIME
1361 nextTime += _REGISTER_TIME
1363
1362
1364 def unregisterService(self, info):
1363 def unregisterService(self, info):
1365 """Unregister a service."""
1364 """Unregister a service."""
1366 try:
1365 try:
1367 del(self.services[info.name.lower()])
1366 del(self.services[info.name.lower()])
1368 if self.servicetypes[info.type]>1:
1367 if self.servicetypes[info.type]>1:
1369 self.servicetypes[info.type]-=1
1368 self.servicetypes[info.type]-=1
1370 else:
1369 else:
1371 del self.servicetypes[info.type]
1370 del self.servicetypes[info.type]
1372 except KeyError:
1371 except KeyError:
1373 pass
1372 pass
1374 now = currentTimeMillis()
1373 now = currentTimeMillis()
1375 nextTime = now
1374 nextTime = now
1376 i = 0
1375 i = 0
1377 while i < 3:
1376 while i < 3:
1378 if now < nextTime:
1377 if now < nextTime:
1379 self.wait(nextTime - now)
1378 self.wait(nextTime - now)
1380 now = currentTimeMillis()
1379 now = currentTimeMillis()
1381 continue
1380 continue
1382 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1381 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1383 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1382 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1384 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0)
1383 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0)
1385 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1384 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1386 if info.address:
1385 if info.address:
1387 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1386 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1388 self.send(out)
1387 self.send(out)
1389 i += 1
1388 i += 1
1390 nextTime += _UNREGISTER_TIME
1389 nextTime += _UNREGISTER_TIME
1391
1390
1392 def unregisterAllServices(self):
1391 def unregisterAllServices(self):
1393 """Unregister all registered services."""
1392 """Unregister all registered services."""
1394 if len(self.services) > 0:
1393 if len(self.services) > 0:
1395 now = currentTimeMillis()
1394 now = currentTimeMillis()
1396 nextTime = now
1395 nextTime = now
1397 i = 0
1396 i = 0
1398 while i < 3:
1397 while i < 3:
1399 if now < nextTime:
1398 if now < nextTime:
1400 self.wait(nextTime - now)
1399 self.wait(nextTime - now)
1401 now = currentTimeMillis()
1400 now = currentTimeMillis()
1402 continue
1401 continue
1403 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1402 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1404 for info in self.services.values():
1403 for info in self.services.values():
1405 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1404 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1406 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0)
1405 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0)
1407 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1406 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1408 if info.address:
1407 if info.address:
1409 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1408 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1410 self.send(out)
1409 self.send(out)
1411 i += 1
1410 i += 1
1412 nextTime += _UNREGISTER_TIME
1411 nextTime += _UNREGISTER_TIME
1413
1412
1414 def checkService(self, info):
1413 def checkService(self, info):
1415 """Checks the network for a unique service name, modifying the
1414 """Checks the network for a unique service name, modifying the
1416 ServiceInfo passed in if it is not unique."""
1415 ServiceInfo passed in if it is not unique."""
1417 now = currentTimeMillis()
1416 now = currentTimeMillis()
1418 nextTime = now
1417 nextTime = now
1419 i = 0
1418 i = 0
1420 while i < 3:
1419 while i < 3:
1421 for record in self.cache.entriesWithName(info.type):
1420 for record in self.cache.entriesWithName(info.type):
1422 if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
1421 if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
1423 if (info.name.find('.') < 0):
1422 if (info.name.find('.') < 0):
1424 info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
1423 info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
1425 self.checkService(info)
1424 self.checkService(info)
1426 return
1425 return
1427 raise NonUniqueNameException
1426 raise NonUniqueNameException
1428 if now < nextTime:
1427 if now < nextTime:
1429 self.wait(nextTime - now)
1428 self.wait(nextTime - now)
1430 now = currentTimeMillis()
1429 now = currentTimeMillis()
1431 continue
1430 continue
1432 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1431 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1433 self.debug = out
1432 self.debug = out
1434 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1433 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1435 out.addAuthoritativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
1434 out.addAuthoritativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
1436 self.send(out)
1435 self.send(out)
1437 i += 1
1436 i += 1
1438 nextTime += _CHECK_TIME
1437 nextTime += _CHECK_TIME
1439
1438
1440 def addListener(self, listener, question):
1439 def addListener(self, listener, question):
1441 """Adds a listener for a given question. The listener will have
1440 """Adds a listener for a given question. The listener will have
1442 its updateRecord method called when information is available to
1441 its updateRecord method called when information is available to
1443 answer the question."""
1442 answer the question."""
1444 now = currentTimeMillis()
1443 now = currentTimeMillis()
1445 self.listeners.append(listener)
1444 self.listeners.append(listener)
1446 if question is not None:
1445 if question is not None:
1447 for record in self.cache.entriesWithName(question.name):
1446 for record in self.cache.entriesWithName(question.name):
1448 if question.answeredBy(record) and not record.isExpired(now):
1447 if question.answeredBy(record) and not record.isExpired(now):
1449 listener.updateRecord(self, now, record)
1448 listener.updateRecord(self, now, record)
1450 self.notifyAll()
1449 self.notifyAll()
1451
1450
1452 def removeListener(self, listener):
1451 def removeListener(self, listener):
1453 """Removes a listener."""
1452 """Removes a listener."""
1454 try:
1453 try:
1455 self.listeners.remove(listener)
1454 self.listeners.remove(listener)
1456 self.notifyAll()
1455 self.notifyAll()
1457 except Exception:
1456 except Exception:
1458 pass
1457 pass
1459
1458
1460 def updateRecord(self, now, rec):
1459 def updateRecord(self, now, rec):
1461 """Used to notify listeners of new information that has updated
1460 """Used to notify listeners of new information that has updated
1462 a record."""
1461 a record."""
1463 for listener in self.listeners:
1462 for listener in self.listeners:
1464 listener.updateRecord(self, now, rec)
1463 listener.updateRecord(self, now, rec)
1465 self.notifyAll()
1464 self.notifyAll()
1466
1465
1467 def handleResponse(self, msg):
1466 def handleResponse(self, msg):
1468 """Deal with incoming response packets. All answers
1467 """Deal with incoming response packets. All answers
1469 are held in the cache, and listeners are notified."""
1468 are held in the cache, and listeners are notified."""
1470 now = currentTimeMillis()
1469 now = currentTimeMillis()
1471 for record in msg.answers:
1470 for record in msg.answers:
1472 expired = record.isExpired(now)
1471 expired = record.isExpired(now)
1473 if record in self.cache.entries():
1472 if record in self.cache.entries():
1474 if expired:
1473 if expired:
1475 self.cache.remove(record)
1474 self.cache.remove(record)
1476 else:
1475 else:
1477 entry = self.cache.get(record)
1476 entry = self.cache.get(record)
1478 if entry is not None:
1477 if entry is not None:
1479 entry.resetTTL(record)
1478 entry.resetTTL(record)
1480 record = entry
1479 record = entry
1481 else:
1480 else:
1482 self.cache.add(record)
1481 self.cache.add(record)
1483
1482
1484 self.updateRecord(now, record)
1483 self.updateRecord(now, record)
1485
1484
1486 def handleQuery(self, msg, addr, port):
1485 def handleQuery(self, msg, addr, port):
1487 """Deal with incoming query packets. Provides a response if
1486 """Deal with incoming query packets. Provides a response if
1488 possible."""
1487 possible."""
1489 out = None
1488 out = None
1490
1489
1491 # Support unicast client responses
1490 # Support unicast client responses
1492 #
1491 #
1493 if port != _MDNS_PORT:
1492 if port != _MDNS_PORT:
1494 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1493 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1495 for question in msg.questions:
1494 for question in msg.questions:
1496 out.addQuestion(question)
1495 out.addQuestion(question)
1497
1496
1498 for question in msg.questions:
1497 for question in msg.questions:
1499 if question.type == _TYPE_PTR:
1498 if question.type == _TYPE_PTR:
1500 if question.name == "_services._dns-sd._udp.local.":
1499 if question.name == "_services._dns-sd._udp.local.":
1501 for stype in self.servicetypes.keys():
1500 for stype in self.servicetypes.keys():
1502 if out is None:
1501 if out is None:
1503 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1502 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1504 out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1503 out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1505 for service in self.services.values():
1504 for service in self.services.values():
1506 if question.name == service.type:
1505 if question.name == service.type:
1507 if out is None:
1506 if out is None:
1508 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1507 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1509 out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
1508 out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
1510 else:
1509 else:
1511 try:
1510 try:
1512 if out is None:
1511 if out is None:
1513 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1512 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1514
1513
1515 # Answer A record queries for any service addresses we know
1514 # Answer A record queries for any service addresses we know
1516 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1515 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1517 for service in self.services.values():
1516 for service in self.services.values():
1518 if service.server == question.name.lower():
1517 if service.server == question.name.lower():
1519 out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1518 out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1520
1519
1521 service = self.services.get(question.name.lower(), None)
1520 service = self.services.get(question.name.lower(), None)
1522 if not service: continue
1521 if not service: continue
1523
1522
1524 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1523 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1525 out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server))
1524 out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server))
1526 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1525 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1527 out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text))
1526 out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text))
1528 if question.type == _TYPE_SRV:
1527 if question.type == _TYPE_SRV:
1529 out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1528 out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1530 except Exception:
1529 except Exception:
1531 traceback.print_exc()
1530 traceback.print_exc()
1532
1531
1533 if out is not None and out.answers:
1532 if out is not None and out.answers:
1534 out.id = msg.id
1533 out.id = msg.id
1535 self.send(out, addr, port)
1534 self.send(out, addr, port)
1536
1535
1537 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1536 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1538 """Sends an outgoing packet."""
1537 """Sends an outgoing packet."""
1539 # This is a quick test to see if we can parse the packets we generate
1538 # This is a quick test to see if we can parse the packets we generate
1540 #temp = DNSIncoming(out.packet())
1539 #temp = DNSIncoming(out.packet())
1541 try:
1540 try:
1542 self.socket.sendto(out.packet(), 0, (addr, port))
1541 self.socket.sendto(out.packet(), 0, (addr, port))
1543 except Exception:
1542 except Exception:
1544 # Ignore this, it may be a temporary loss of network connection
1543 # Ignore this, it may be a temporary loss of network connection
1545 pass
1544 pass
1546
1545
1547 def close(self):
1546 def close(self):
1548 """Ends the background threads, and prevent this instance from
1547 """Ends the background threads, and prevent this instance from
1549 servicing further queries."""
1548 servicing further queries."""
1550 if globals()['_GLOBAL_DONE'] == 0:
1549 if globals()['_GLOBAL_DONE'] == 0:
1551 globals()['_GLOBAL_DONE'] = 1
1550 globals()['_GLOBAL_DONE'] = 1
1552 self.notifyAll()
1551 self.notifyAll()
1553 self.engine.notify()
1552 self.engine.notify()
1554 self.unregisterAllServices()
1553 self.unregisterAllServices()
1555 self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1554 self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1556 self.socket.close()
1555 self.socket.close()
1557
1556
1558 # Test a few module features, including service registration, service
1557 # Test a few module features, including service registration, service
1559 # query (for Zoe), and service unregistration.
1558 # query (for Zoe), and service unregistration.
1560
1559
1561 if __name__ == '__main__':
1560 if __name__ == '__main__':
1562 print "Multicast DNS Service Discovery for Python, version", __version__
1561 print "Multicast DNS Service Discovery for Python, version", __version__
1563 r = Zeroconf()
1562 r = Zeroconf()
1564 print "1. Testing registration of a service..."
1563 print "1. Testing registration of a service..."
1565 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1564 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1566 info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1565 info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1567 print " Registering service..."
1566 print " Registering service..."
1568 r.registerService(info)
1567 r.registerService(info)
1569 print " Registration done."
1568 print " Registration done."
1570 print "2. Testing query of service information..."
1569 print "2. Testing query of service information..."
1571 print " Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1570 print " Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
1572 print " Query done."
1571 print " Query done."
1573 print "3. Testing query of own service..."
1572 print "3. Testing query of own service..."
1574 print " Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
1573 print " Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
1575 print " Query done."
1574 print " Query done."
1576 print "4. Testing unregister of service information..."
1575 print "4. Testing unregister of service information..."
1577 r.unregisterService(info)
1576 r.unregisterService(info)
1578 print " Unregister done."
1577 print " Unregister done."
1579 r.close()
1578 r.close()
1580
1579
1581 # no-check-code
1580 # no-check-code
@@ -1,188 +1,188
1 # hook.py - hook support for mercurial
1 # hook.py - hook support for mercurial
2 #
2 #
3 # Copyright 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from i18n import _
8 from i18n import _
9 import os, sys
9 import os, sys
10 import extensions, util
10 import extensions, util
11
11
12 def _pythonhook(ui, repo, name, hname, funcname, args, throw):
12 def _pythonhook(ui, repo, name, hname, funcname, args, throw):
13 '''call python hook. hook is callable object, looked up as
13 '''call python hook. hook is callable object, looked up as
14 name in python module. if callable returns "true", hook
14 name in python module. if callable returns "true", hook
15 fails, else passes. if hook raises exception, treated as
15 fails, else passes. if hook raises exception, treated as
16 hook failure. exception propagates if throw is "true".
16 hook failure. exception propagates if throw is "true".
17
17
18 reason for "true" meaning "hook failed" is so that
18 reason for "true" meaning "hook failed" is so that
19 unmodified commands (e.g. mercurial.commands.update) can
19 unmodified commands (e.g. mercurial.commands.update) can
20 be run as hooks without wrappers to convert return values.'''
20 be run as hooks without wrappers to convert return values.'''
21
21
22 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
22 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
23 obj = funcname
23 obj = funcname
24 if not util.safehasattr(obj, '__call__'):
24 if not util.safehasattr(obj, '__call__'):
25 d = funcname.rfind('.')
25 d = funcname.rfind('.')
26 if d == -1:
26 if d == -1:
27 raise util.Abort(_('%s hook is invalid ("%s" not in '
27 raise util.Abort(_('%s hook is invalid ("%s" not in '
28 'a module)') % (hname, funcname))
28 'a module)') % (hname, funcname))
29 modname = funcname[:d]
29 modname = funcname[:d]
30 oldpaths = sys.path
30 oldpaths = sys.path
31 if util.mainfrozen():
31 if util.mainfrozen():
32 # binary installs require sys.path manipulation
32 # binary installs require sys.path manipulation
33 modpath, modfile = os.path.split(modname)
33 modpath, modfile = os.path.split(modname)
34 if modpath and modfile:
34 if modpath and modfile:
35 sys.path = sys.path[:] + [modpath]
35 sys.path = sys.path[:] + [modpath]
36 modname = modfile
36 modname = modfile
37 try:
37 try:
38 obj = __import__(modname)
38 obj = __import__(modname)
39 except ImportError:
39 except ImportError:
40 e1 = sys.exc_type, sys.exc_value, sys.exc_traceback
40 e1 = sys.exc_type, sys.exc_value, sys.exc_traceback
41 try:
41 try:
42 # extensions are loaded with hgext_ prefix
42 # extensions are loaded with hgext_ prefix
43 obj = __import__("hgext_%s" % modname)
43 obj = __import__("hgext_%s" % modname)
44 except ImportError:
44 except ImportError:
45 e2 = sys.exc_type, sys.exc_value, sys.exc_traceback
45 e2 = sys.exc_type, sys.exc_value, sys.exc_traceback
46 if ui.tracebackflag:
46 if ui.tracebackflag:
47 ui.warn(_('exception from first failed import attempt:\n'))
47 ui.warn(_('exception from first failed import attempt:\n'))
48 ui.traceback(e1)
48 ui.traceback(e1)
49 if ui.tracebackflag:
49 if ui.tracebackflag:
50 ui.warn(_('exception from second failed import attempt:\n'))
50 ui.warn(_('exception from second failed import attempt:\n'))
51 ui.traceback(e2)
51 ui.traceback(e2)
52 raise util.Abort(_('%s hook is invalid '
52 raise util.Abort(_('%s hook is invalid '
53 '(import of "%s" failed)') %
53 '(import of "%s" failed)') %
54 (hname, modname))
54 (hname, modname))
55 sys.path = oldpaths
55 sys.path = oldpaths
56 try:
56 try:
57 for p in funcname.split('.')[1:]:
57 for p in funcname.split('.')[1:]:
58 obj = getattr(obj, p)
58 obj = getattr(obj, p)
59 except AttributeError:
59 except AttributeError:
60 raise util.Abort(_('%s hook is invalid '
60 raise util.Abort(_('%s hook is invalid '
61 '("%s" is not defined)') %
61 '("%s" is not defined)') %
62 (hname, funcname))
62 (hname, funcname))
63 if not util.safehasattr(obj, '__call__'):
63 if not util.safehasattr(obj, '__call__'):
64 raise util.Abort(_('%s hook is invalid '
64 raise util.Abort(_('%s hook is invalid '
65 '("%s" is not callable)') %
65 '("%s" is not callable)') %
66 (hname, funcname))
66 (hname, funcname))
67 try:
67 try:
68 try:
68 try:
69 # redirect IO descriptors to the ui descriptors so hooks
69 # redirect IO descriptors to the ui descriptors so hooks
70 # that write directly to these don't mess up the command
70 # that write directly to these don't mess up the command
71 # protocol when running through the command server
71 # protocol when running through the command server
72 old = sys.stdout, sys.stderr, sys.stdin
72 old = sys.stdout, sys.stderr, sys.stdin
73 sys.stdout, sys.stderr, sys.stdin = ui.fout, ui.ferr, ui.fin
73 sys.stdout, sys.stderr, sys.stdin = ui.fout, ui.ferr, ui.fin
74
74
75 r = obj(ui=ui, repo=repo, hooktype=name, **args)
75 r = obj(ui=ui, repo=repo, hooktype=name, **args)
76 except KeyboardInterrupt:
76 except KeyboardInterrupt:
77 raise
77 raise
78 except Exception, exc:
78 except Exception, exc:
79 if isinstance(exc, util.Abort):
79 if isinstance(exc, util.Abort):
80 ui.warn(_('error: %s hook failed: %s\n') %
80 ui.warn(_('error: %s hook failed: %s\n') %
81 (hname, exc.args[0]))
81 (hname, exc.args[0]))
82 else:
82 else:
83 ui.warn(_('error: %s hook raised an exception: '
83 ui.warn(_('error: %s hook raised an exception: '
84 '%s\n') % (hname, exc))
84 '%s\n') % (hname, exc))
85 if throw:
85 if throw:
86 raise
86 raise
87 ui.traceback()
87 ui.traceback()
88 return True
88 return True
89 finally:
89 finally:
90 sys.stdout, sys.stderr, sys.stdin = old
90 sys.stdout, sys.stderr, sys.stdin = old
91 if r:
91 if r:
92 if throw:
92 if throw:
93 raise util.Abort(_('%s hook failed') % hname)
93 raise util.Abort(_('%s hook failed') % hname)
94 ui.warn(_('warning: %s hook failed\n') % hname)
94 ui.warn(_('warning: %s hook failed\n') % hname)
95 return r
95 return r
96
96
97 def _exthook(ui, repo, name, cmd, args, throw):
97 def _exthook(ui, repo, name, cmd, args, throw):
98 ui.note(_("running hook %s: %s\n") % (name, cmd))
98 ui.note(_("running hook %s: %s\n") % (name, cmd))
99
99
100 env = {}
100 env = {}
101 for k, v in args.iteritems():
101 for k, v in args.iteritems():
102 if util.safehasattr(v, '__call__'):
102 if util.safehasattr(v, '__call__'):
103 v = v()
103 v = v()
104 if isinstance(v, dict):
104 if isinstance(v, dict):
105 # make the dictionary element order stable across Python
105 # make the dictionary element order stable across Python
106 # implementations
106 # implementations
107 v = ('{' +
107 v = ('{' +
108 ', '.join('%r: %r' % i for i in sorted(v.iteritems())) +
108 ', '.join('%r: %r' % i for i in sorted(v.iteritems())) +
109 '}')
109 '}')
110 env['HG_' + k.upper()] = v
110 env['HG_' + k.upper()] = v
111
111
112 if repo:
112 if repo:
113 cwd = repo.root
113 cwd = repo.root
114 else:
114 else:
115 cwd = os.getcwd()
115 cwd = os.getcwd()
116 if 'HG_URL' in env and env['HG_URL'].startswith('remote:http'):
116 if 'HG_URL' in env and env['HG_URL'].startswith('remote:http'):
117 r = util.system(cmd, environ=env, cwd=cwd, out=ui)
117 r = util.system(cmd, environ=env, cwd=cwd, out=ui)
118 else:
118 else:
119 r = util.system(cmd, environ=env, cwd=cwd, out=ui.fout)
119 r = util.system(cmd, environ=env, cwd=cwd, out=ui.fout)
120 if r:
120 if r:
121 desc, r = util.explainexit(r)
121 desc, r = util.explainexit(r)
122 if throw:
122 if throw:
123 raise util.Abort(_('%s hook %s') % (name, desc))
123 raise util.Abort(_('%s hook %s') % (name, desc))
124 ui.warn(_('warning: %s hook %s\n') % (name, desc))
124 ui.warn(_('warning: %s hook %s\n') % (name, desc))
125 return r
125 return r
126
126
127 def _allhooks(ui):
127 def _allhooks(ui):
128 hooks = []
128 hooks = []
129 for name, cmd in ui.configitems('hooks'):
129 for name, cmd in ui.configitems('hooks'):
130 if not name.startswith('priority'):
130 if not name.startswith('priority'):
131 priority = ui.configint('hooks', 'priority.%s' % name, 0)
131 priority = ui.configint('hooks', 'priority.%s' % name, 0)
132 hooks.append((-priority, len(hooks), name, cmd))
132 hooks.append((-priority, len(hooks), name, cmd))
133 return [(k, v) for p, o, k, v in sorted(hooks)]
133 return [(k, v) for p, o, k, v in sorted(hooks)]
134
134
135 _redirect = False
135 _redirect = False
136 def redirect(state):
136 def redirect(state):
137 global _redirect
137 global _redirect
138 _redirect = state
138 _redirect = state
139
139
140 def hook(ui, repo, name, throw=False, **args):
140 def hook(ui, repo, name, throw=False, **args):
141 if not ui.callhooks:
141 if not ui.callhooks:
142 return False
142 return False
143
143
144 r = False
144 r = False
145
145
146 oldstdout = -1
146 oldstdout = -1
147 if _redirect:
147 if _redirect:
148 try:
148 try:
149 stdoutno = sys.__stdout__.fileno()
149 stdoutno = sys.__stdout__.fileno()
150 stderrno = sys.__stderr__.fileno()
150 stderrno = sys.__stderr__.fileno()
151 # temporarily redirect stdout to stderr, if possible
151 # temporarily redirect stdout to stderr, if possible
152 if stdoutno >= 0 and stderrno >= 0:
152 if stdoutno >= 0 and stderrno >= 0:
153 sys.__stdout__.flush()
153 sys.__stdout__.flush()
154 oldstdout = os.dup(stdoutno)
154 oldstdout = os.dup(stdoutno)
155 os.dup2(stderrno, stdoutno)
155 os.dup2(stderrno, stdoutno)
156 except AttributeError:
156 except AttributeError:
157 # __stdout/err__ doesn't have fileno(), it's not a real file
157 # __stdout__/__stderr__ doesn't have fileno(), it's not a real file
158 pass
158 pass
159
159
160 try:
160 try:
161 for hname, cmd in _allhooks(ui):
161 for hname, cmd in _allhooks(ui):
162 if hname.split('.')[0] != name or not cmd:
162 if hname.split('.')[0] != name or not cmd:
163 continue
163 continue
164 if util.safehasattr(cmd, '__call__'):
164 if util.safehasattr(cmd, '__call__'):
165 r = _pythonhook(ui, repo, name, hname, cmd, args, throw) or r
165 r = _pythonhook(ui, repo, name, hname, cmd, args, throw) or r
166 elif cmd.startswith('python:'):
166 elif cmd.startswith('python:'):
167 if cmd.count(':') >= 2:
167 if cmd.count(':') >= 2:
168 path, cmd = cmd[7:].rsplit(':', 1)
168 path, cmd = cmd[7:].rsplit(':', 1)
169 path = util.expandpath(path)
169 path = util.expandpath(path)
170 if repo:
170 if repo:
171 path = os.path.join(repo.root, path)
171 path = os.path.join(repo.root, path)
172 try:
172 try:
173 mod = extensions.loadpath(path, 'hghook.%s' % hname)
173 mod = extensions.loadpath(path, 'hghook.%s' % hname)
174 except Exception:
174 except Exception:
175 ui.write(_("loading %s hook failed:\n") % hname)
175 ui.write(_("loading %s hook failed:\n") % hname)
176 raise
176 raise
177 hookfn = getattr(mod, cmd)
177 hookfn = getattr(mod, cmd)
178 else:
178 else:
179 hookfn = cmd[7:].strip()
179 hookfn = cmd[7:].strip()
180 r = _pythonhook(ui, repo, name, hname, hookfn, args, throw) or r
180 r = _pythonhook(ui, repo, name, hname, hookfn, args, throw) or r
181 else:
181 else:
182 r = _exthook(ui, repo, hname, cmd, args, throw) or r
182 r = _exthook(ui, repo, hname, cmd, args, throw) or r
183 finally:
183 finally:
184 if _redirect and oldstdout >= 0:
184 if _redirect and oldstdout >= 0:
185 os.dup2(oldstdout, stdoutno)
185 os.dup2(oldstdout, stdoutno)
186 os.close(oldstdout)
186 os.close(oldstdout)
187
187
188 return r
188 return r
@@ -1,674 +1,674
1 # Copyright 2010, Google Inc.
1 # Copyright 2010, Google Inc.
2 # All rights reserved.
2 # All rights reserved.
3 #
3 #
4 # Redistribution and use in source and binary forms, with or without
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
5 # modification, are permitted provided that the following conditions are
6 # met:
6 # met:
7 #
7 #
8 # * Redistributions of source code must retain the above copyright
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
12 # in the documentation and/or other materials provided with the
13 # distribution.
13 # distribution.
14 # * Neither the name of Google Inc. nor the names of its
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
16 # this software without specific prior written permission.
17
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 """Improved HTTP/1.1 client library
29 """Improved HTTP/1.1 client library
30
30
31 This library contains an HTTPConnection which is similar to the one in
31 This library contains an HTTPConnection which is similar to the one in
32 httplib, but has several additional features:
32 httplib, but has several additional features:
33
33
34 * supports keepalives natively
34 * supports keepalives natively
35 * uses select() to block for incoming data
35 * uses select() to block for incoming data
36 * notices when the server responds early to a request
36 * notices when the server responds early to a request
37 * implements ssl inline instead of in a different class
37 * implements ssl inline instead of in a different class
38 """
38 """
39
39
40 import cStringIO
40 import cStringIO
41 import errno
41 import errno
42 import httplib
42 import httplib
43 import logging
43 import logging
44 import rfc822
44 import rfc822
45 import select
45 import select
46 import socket
46 import socket
47
47
48 import _readers
48 import _readers
49 import socketutil
49 import socketutil
50
50
51 logger = logging.getLogger(__name__)
51 logger = logging.getLogger(__name__)
52
52
53 __all__ = ['HTTPConnection', 'HTTPResponse']
53 __all__ = ['HTTPConnection', 'HTTPResponse']
54
54
55 HTTP_VER_1_0 = 'HTTP/1.0'
55 HTTP_VER_1_0 = 'HTTP/1.0'
56 HTTP_VER_1_1 = 'HTTP/1.1'
56 HTTP_VER_1_1 = 'HTTP/1.1'
57
57
58 OUTGOING_BUFFER_SIZE = 1 << 15
58 OUTGOING_BUFFER_SIZE = 1 << 15
59 INCOMING_BUFFER_SIZE = 1 << 20
59 INCOMING_BUFFER_SIZE = 1 << 20
60
60
61 HDR_ACCEPT_ENCODING = 'accept-encoding'
61 HDR_ACCEPT_ENCODING = 'accept-encoding'
62 HDR_CONNECTION_CTRL = 'connection'
62 HDR_CONNECTION_CTRL = 'connection'
63 HDR_CONTENT_LENGTH = 'content-length'
63 HDR_CONTENT_LENGTH = 'content-length'
64 HDR_XFER_ENCODING = 'transfer-encoding'
64 HDR_XFER_ENCODING = 'transfer-encoding'
65
65
66 XFER_ENCODING_CHUNKED = 'chunked'
66 XFER_ENCODING_CHUNKED = 'chunked'
67
67
68 CONNECTION_CLOSE = 'close'
68 CONNECTION_CLOSE = 'close'
69
69
70 EOL = '\r\n'
70 EOL = '\r\n'
71 _END_HEADERS = EOL * 2
71 _END_HEADERS = EOL * 2
72
72
73 # Based on some searching around, 1 second seems like a reasonable
73 # Based on some searching around, 1 second seems like a reasonable
74 # default here.
74 # default here.
75 TIMEOUT_ASSUME_CONTINUE = 1
75 TIMEOUT_ASSUME_CONTINUE = 1
76 TIMEOUT_DEFAULT = None
76 TIMEOUT_DEFAULT = None
77
77
78
78
79 class HTTPResponse(object):
79 class HTTPResponse(object):
80 """Response from an HTTP server.
80 """Response from an HTTP server.
81
81
82 The response will continue to load as available. If you need the
82 The response will continue to load as available. If you need the
83 complete response before continuing, check the .complete() method.
83 complete response before continuing, check the .complete() method.
84 """
84 """
85 def __init__(self, sock, timeout, method):
85 def __init__(self, sock, timeout, method):
86 self.sock = sock
86 self.sock = sock
87 self.method = method
87 self.method = method
88 self.raw_response = ''
88 self.raw_response = ''
89 self._headers_len = 0
89 self._headers_len = 0
90 self.headers = None
90 self.headers = None
91 self.will_close = False
91 self.will_close = False
92 self.status_line = ''
92 self.status_line = ''
93 self.status = None
93 self.status = None
94 self.continued = False
94 self.continued = False
95 self.http_version = None
95 self.http_version = None
96 self.reason = None
96 self.reason = None
97 self._reader = None
97 self._reader = None
98
98
99 self._read_location = 0
99 self._read_location = 0
100 self._eol = EOL
100 self._eol = EOL
101
101
102 self._timeout = timeout
102 self._timeout = timeout
103
103
104 @property
104 @property
105 def _end_headers(self):
105 def _end_headers(self):
106 return self._eol * 2
106 return self._eol * 2
107
107
108 def complete(self):
108 def complete(self):
109 """Returns true if this response is completely loaded.
109 """Returns true if this response is completely loaded.
110
110
111 Note that if this is a connection where complete means the
111 Note that if this is a connection where complete means the
112 socket is closed, this will nearly always return False, even
112 socket is closed, this will nearly always return False, even
113 in cases where all the data has actually been loaded.
113 in cases where all the data has actually been loaded.
114 """
114 """
115 if self._reader:
115 if self._reader:
116 return self._reader.done()
116 return self._reader.done()
117
117
118 def _close(self):
118 def _close(self):
119 if self._reader is not None:
119 if self._reader is not None:
120 self._reader._close()
120 self._reader._close()
121
121
122 def readline(self):
122 def readline(self):
123 """Read a single line from the response body.
123 """Read a single line from the response body.
124
124
125 This may block until either a line ending is found or the
125 This may block until either a line ending is found or the
126 response is complete.
126 response is complete.
127 """
127 """
128 # TODO: move this into the reader interface where it can be
128 # TODO: move this into the reader interface where it can be
129 # smarter (and probably avoid copies)
129 # smarter (and probably avoid copies)
130 bytes = []
130 bytes = []
131 while not bytes:
131 while not bytes:
132 try:
132 try:
133 bytes = [self._reader.read(1)]
133 bytes = [self._reader.read(1)]
134 except _readers.ReadNotReady:
134 except _readers.ReadNotReady:
135 self._select()
135 self._select()
136 while bytes[-1] != '\n' and not self.complete():
136 while bytes[-1] != '\n' and not self.complete():
137 self._select()
137 self._select()
138 bytes.append(self._reader.read(1))
138 bytes.append(self._reader.read(1))
139 if bytes[-1] != '\n':
139 if bytes[-1] != '\n':
140 next = self._reader.read(1)
140 next = self._reader.read(1)
141 while next and next != '\n':
141 while next and next != '\n':
142 bytes.append(next)
142 bytes.append(next)
143 next = self._reader.read(1)
143 next = self._reader.read(1)
144 bytes.append(next)
144 bytes.append(next)
145 return ''.join(bytes)
145 return ''.join(bytes)
146
146
147 def read(self, length=None):
147 def read(self, length=None):
148 # if length is None, unbounded read
148 # if length is None, unbounded read
149 while (not self.complete() # never select on a finished read
149 while (not self.complete() # never select on a finished read
150 and (not length # unbounded, so we wait for complete()
150 and (not length # unbounded, so we wait for complete()
151 or length > self._reader.available_data)):
151 or length > self._reader.available_data)):
152 self._select()
152 self._select()
153 if not length:
153 if not length:
154 length = self._reader.available_data
154 length = self._reader.available_data
155 r = self._reader.read(length)
155 r = self._reader.read(length)
156 if self.complete() and self.will_close:
156 if self.complete() and self.will_close:
157 self.sock.close()
157 self.sock.close()
158 return r
158 return r
159
159
160 def _select(self):
160 def _select(self):
161 r, _, _ = select.select([self.sock], [], [], self._timeout)
161 r, _, _ = select.select([self.sock], [], [], self._timeout)
162 if not r:
162 if not r:
163 # socket was not readable. If the response is not
163 # socket was not readable. If the response is not
164 # complete, raise a timeout.
164 # complete, raise a timeout.
165 if not self.complete():
165 if not self.complete():
166 logger.info('timed out with timeout of %s', self._timeout)
166 logger.info('timed out with timeout of %s', self._timeout)
167 raise HTTPTimeoutException('timeout reading data')
167 raise HTTPTimeoutException('timeout reading data')
168 try:
168 try:
169 data = self.sock.recv(INCOMING_BUFFER_SIZE)
169 data = self.sock.recv(INCOMING_BUFFER_SIZE)
170 except socket.sslerror, e:
170 except socket.sslerror, e:
171 if e.args[0] != socket.SSL_ERROR_WANT_READ:
171 if e.args[0] != socket.SSL_ERROR_WANT_READ:
172 raise
172 raise
173 logger.debug('SSL_WANT_READ in _select, should retry later')
173 logger.debug('SSL_ERROR_WANT_READ in _select, should retry later')
174 return True
174 return True
175 logger.debug('response read %d data during _select', len(data))
175 logger.debug('response read %d data during _select', len(data))
176 # If the socket was readable and no data was read, that means
176 # If the socket was readable and no data was read, that means
177 # the socket was closed. Inform the reader (if any) so it can
177 # the socket was closed. Inform the reader (if any) so it can
178 # raise an exception if this is an invalid situation.
178 # raise an exception if this is an invalid situation.
179 if not data:
179 if not data:
180 if self._reader:
180 if self._reader:
181 self._reader._close()
181 self._reader._close()
182 return False
182 return False
183 else:
183 else:
184 self._load_response(data)
184 self._load_response(data)
185 return True
185 return True
186
186
187 def _load_response(self, data):
187 def _load_response(self, data):
188 # Being here implies we're not at the end of the headers yet,
188 # Being here implies we're not at the end of the headers yet,
189 # since at the end of this method if headers were completely
189 # since at the end of this method if headers were completely
190 # loaded we replace this method with the load() method of the
190 # loaded we replace this method with the load() method of the
191 # reader we created.
191 # reader we created.
192 self.raw_response += data
192 self.raw_response += data
193 # This is a bogus server with bad line endings
193 # This is a bogus server with bad line endings
194 if self._eol not in self.raw_response:
194 if self._eol not in self.raw_response:
195 for bad_eol in ('\n', '\r'):
195 for bad_eol in ('\n', '\r'):
196 if (bad_eol in self.raw_response
196 if (bad_eol in self.raw_response
197 # verify that bad_eol is not the end of the incoming data
197 # verify that bad_eol is not the end of the incoming data
198 # as this could be a response line that just got
198 # as this could be a response line that just got
199 # split between \r and \n.
199 # split between \r and \n.
200 and (self.raw_response.index(bad_eol) <
200 and (self.raw_response.index(bad_eol) <
201 (len(self.raw_response) - 1))):
201 (len(self.raw_response) - 1))):
202 logger.info('bogus line endings detected, '
202 logger.info('bogus line endings detected, '
203 'using %r for EOL', bad_eol)
203 'using %r for EOL', bad_eol)
204 self._eol = bad_eol
204 self._eol = bad_eol
205 break
205 break
206 # exit early if not at end of headers
206 # exit early if not at end of headers
207 if self._end_headers not in self.raw_response or self.headers:
207 if self._end_headers not in self.raw_response or self.headers:
208 return
208 return
209
209
210 # handle 100-continue response
210 # handle 100-continue response
211 hdrs, body = self.raw_response.split(self._end_headers, 1)
211 hdrs, body = self.raw_response.split(self._end_headers, 1)
212 http_ver, status = hdrs.split(' ', 1)
212 http_ver, status = hdrs.split(' ', 1)
213 if status.startswith('100'):
213 if status.startswith('100'):
214 self.raw_response = body
214 self.raw_response = body
215 self.continued = True
215 self.continued = True
216 logger.debug('continue seen, setting body to %r', body)
216 logger.debug('continue seen, setting body to %r', body)
217 return
217 return
218
218
219 # arriving here means we should parse response headers
219 # arriving here means we should parse response headers
220 # as all headers have arrived completely
220 # as all headers have arrived completely
221 hdrs, body = self.raw_response.split(self._end_headers, 1)
221 hdrs, body = self.raw_response.split(self._end_headers, 1)
222 del self.raw_response
222 del self.raw_response
223 if self._eol in hdrs:
223 if self._eol in hdrs:
224 self.status_line, hdrs = hdrs.split(self._eol, 1)
224 self.status_line, hdrs = hdrs.split(self._eol, 1)
225 else:
225 else:
226 self.status_line = hdrs
226 self.status_line = hdrs
227 hdrs = ''
227 hdrs = ''
228 # TODO HTTP < 1.0 support
228 # TODO HTTP < 1.0 support
229 (self.http_version, self.status,
229 (self.http_version, self.status,
230 self.reason) = self.status_line.split(' ', 2)
230 self.reason) = self.status_line.split(' ', 2)
231 self.status = int(self.status)
231 self.status = int(self.status)
232 if self._eol != EOL:
232 if self._eol != EOL:
233 hdrs = hdrs.replace(self._eol, '\r\n')
233 hdrs = hdrs.replace(self._eol, '\r\n')
234 headers = rfc822.Message(cStringIO.StringIO(hdrs))
234 headers = rfc822.Message(cStringIO.StringIO(hdrs))
235 content_len = None
235 content_len = None
236 if HDR_CONTENT_LENGTH in headers:
236 if HDR_CONTENT_LENGTH in headers:
237 content_len = int(headers[HDR_CONTENT_LENGTH])
237 content_len = int(headers[HDR_CONTENT_LENGTH])
238 if self.http_version == HTTP_VER_1_0:
238 if self.http_version == HTTP_VER_1_0:
239 self.will_close = True
239 self.will_close = True
240 elif HDR_CONNECTION_CTRL in headers:
240 elif HDR_CONNECTION_CTRL in headers:
241 self.will_close = (
241 self.will_close = (
242 headers[HDR_CONNECTION_CTRL].lower() == CONNECTION_CLOSE)
242 headers[HDR_CONNECTION_CTRL].lower() == CONNECTION_CLOSE)
243 if (HDR_XFER_ENCODING in headers
243 if (HDR_XFER_ENCODING in headers
244 and headers[HDR_XFER_ENCODING].lower() == XFER_ENCODING_CHUNKED):
244 and headers[HDR_XFER_ENCODING].lower() == XFER_ENCODING_CHUNKED):
245 self._reader = _readers.ChunkedReader(self._eol)
245 self._reader = _readers.ChunkedReader(self._eol)
246 logger.debug('using a chunked reader')
246 logger.debug('using a chunked reader')
247 else:
247 else:
248 # HEAD responses are forbidden from returning a body, and
248 # HEAD responses are forbidden from returning a body, and
249 # it's implausible for a CONNECT response to use
249 # it's implausible for a CONNECT response to use
250 # close-is-end logic for an OK response.
250 # close-is-end logic for an OK response.
251 if (self.method == 'HEAD' or
251 if (self.method == 'HEAD' or
252 (self.method == 'CONNECT' and content_len is None)):
252 (self.method == 'CONNECT' and content_len is None)):
253 content_len = 0
253 content_len = 0
254 if content_len is not None:
254 if content_len is not None:
255 logger.debug('using a content-length reader with length %d',
255 logger.debug('using a content-length reader with length %d',
256 content_len)
256 content_len)
257 self._reader = _readers.ContentLengthReader(content_len)
257 self._reader = _readers.ContentLengthReader(content_len)
258 else:
258 else:
259 # Response body had no length specified and is not
259 # Response body had no length specified and is not
260 # chunked, so the end of the body will only be
260 # chunked, so the end of the body will only be
261 # identifiable by the termination of the socket by the
261 # identifiable by the termination of the socket by the
262 # server. My interpretation of the spec means that we
262 # server. My interpretation of the spec means that we
263 # are correct in hitting this case if
263 # are correct in hitting this case if
264 # transfer-encoding, content-length, and
264 # transfer-encoding, content-length, and
265 # connection-control were left unspecified.
265 # connection-control were left unspecified.
266 self._reader = _readers.CloseIsEndReader()
266 self._reader = _readers.CloseIsEndReader()
267 logger.debug('using a close-is-end reader')
267 logger.debug('using a close-is-end reader')
268 self.will_close = True
268 self.will_close = True
269
269
270 if body:
270 if body:
271 self._reader._load(body)
271 self._reader._load(body)
272 logger.debug('headers complete')
272 logger.debug('headers complete')
273 self.headers = headers
273 self.headers = headers
274 self._load_response = self._reader._load
274 self._load_response = self._reader._load
275
275
276
276
277 class HTTPConnection(object):
277 class HTTPConnection(object):
278 """Connection to a single http server.
278 """Connection to a single http server.
279
279
280 Supports 100-continue and keepalives natively. Uses select() for
280 Supports 100-continue and keepalives natively. Uses select() for
281 non-blocking socket operations.
281 non-blocking socket operations.
282 """
282 """
283 http_version = HTTP_VER_1_1
283 http_version = HTTP_VER_1_1
284 response_class = HTTPResponse
284 response_class = HTTPResponse
285
285
286 def __init__(self, host, port=None, use_ssl=None, ssl_validator=None,
286 def __init__(self, host, port=None, use_ssl=None, ssl_validator=None,
287 timeout=TIMEOUT_DEFAULT,
287 timeout=TIMEOUT_DEFAULT,
288 continue_timeout=TIMEOUT_ASSUME_CONTINUE,
288 continue_timeout=TIMEOUT_ASSUME_CONTINUE,
289 proxy_hostport=None, **ssl_opts):
289 proxy_hostport=None, **ssl_opts):
290 """Create a new HTTPConnection.
290 """Create a new HTTPConnection.
291
291
292 Args:
292 Args:
293 host: The host to which we'll connect.
293 host: The host to which we'll connect.
294 port: Optional. The port over which we'll connect. Default 80 for
294 port: Optional. The port over which we'll connect. Default 80 for
295 non-ssl, 443 for ssl.
295 non-ssl, 443 for ssl.
296 use_ssl: Optional. Whether to use ssl. Defaults to False if port is
296 use_ssl: Optional. Whether to use ssl. Defaults to False if port is
297 not 443, true if port is 443.
297 not 443, true if port is 443.
298 ssl_validator: a function(socket) to validate the ssl cert
298 ssl_validator: a function(socket) to validate the ssl cert
299 timeout: Optional. Connection timeout, default is TIMEOUT_DEFAULT.
299 timeout: Optional. Connection timeout, default is TIMEOUT_DEFAULT.
300 continue_timeout: Optional. Timeout for waiting on an expected
300 continue_timeout: Optional. Timeout for waiting on an expected
301 "100 Continue" response. Default is TIMEOUT_ASSUME_CONTINUE.
301 "100 Continue" response. Default is TIMEOUT_ASSUME_CONTINUE.
302 proxy_hostport: Optional. Tuple of (host, port) to use as an http
302 proxy_hostport: Optional. Tuple of (host, port) to use as an http
303 proxy for the connection. Default is to not use a proxy.
303 proxy for the connection. Default is to not use a proxy.
304 """
304 """
305 if port is None and host.count(':') == 1 or ']:' in host:
305 if port is None and host.count(':') == 1 or ']:' in host:
306 host, port = host.rsplit(':', 1)
306 host, port = host.rsplit(':', 1)
307 port = int(port)
307 port = int(port)
308 if '[' in host:
308 if '[' in host:
309 host = host[1:-1]
309 host = host[1:-1]
310 if use_ssl is None and port is None:
310 if use_ssl is None and port is None:
311 use_ssl = False
311 use_ssl = False
312 port = 80
312 port = 80
313 elif use_ssl is None:
313 elif use_ssl is None:
314 use_ssl = (port == 443)
314 use_ssl = (port == 443)
315 elif port is None:
315 elif port is None:
316 port = (use_ssl and 443 or 80)
316 port = (use_ssl and 443 or 80)
317 self.port = port
317 self.port = port
318 if use_ssl and not socketutil.have_ssl:
318 if use_ssl and not socketutil.have_ssl:
319 raise Exception('ssl requested but unavailable on this Python')
319 raise Exception('ssl requested but unavailable on this Python')
320 self.ssl = use_ssl
320 self.ssl = use_ssl
321 self.ssl_opts = ssl_opts
321 self.ssl_opts = ssl_opts
322 self._ssl_validator = ssl_validator
322 self._ssl_validator = ssl_validator
323 self.host = host
323 self.host = host
324 self.sock = None
324 self.sock = None
325 self._current_response = None
325 self._current_response = None
326 self._current_response_taken = False
326 self._current_response_taken = False
327 if proxy_hostport is None:
327 if proxy_hostport is None:
328 self._proxy_host = self._proxy_port = None
328 self._proxy_host = self._proxy_port = None
329 else:
329 else:
330 self._proxy_host, self._proxy_port = proxy_hostport
330 self._proxy_host, self._proxy_port = proxy_hostport
331
331
332 self.timeout = timeout
332 self.timeout = timeout
333 self.continue_timeout = continue_timeout
333 self.continue_timeout = continue_timeout
334
334
335 def _connect(self):
335 def _connect(self):
336 """Connect to the host and port specified in __init__."""
336 """Connect to the host and port specified in __init__."""
337 if self.sock:
337 if self.sock:
338 return
338 return
339 if self._proxy_host is not None:
339 if self._proxy_host is not None:
340 logger.info('Connecting to http proxy %s:%s',
340 logger.info('Connecting to http proxy %s:%s',
341 self._proxy_host, self._proxy_port)
341 self._proxy_host, self._proxy_port)
342 sock = socketutil.create_connection((self._proxy_host,
342 sock = socketutil.create_connection((self._proxy_host,
343 self._proxy_port))
343 self._proxy_port))
344 if self.ssl:
344 if self.ssl:
345 # TODO proxy header support
345 # TODO proxy header support
346 data = self.buildheaders('CONNECT', '%s:%d' % (self.host,
346 data = self.buildheaders('CONNECT', '%s:%d' % (self.host,
347 self.port),
347 self.port),
348 {}, HTTP_VER_1_0)
348 {}, HTTP_VER_1_0)
349 sock.send(data)
349 sock.send(data)
350 sock.setblocking(0)
350 sock.setblocking(0)
351 r = self.response_class(sock, self.timeout, 'CONNECT')
351 r = self.response_class(sock, self.timeout, 'CONNECT')
352 timeout_exc = HTTPTimeoutException(
352 timeout_exc = HTTPTimeoutException(
353 'Timed out waiting for CONNECT response from proxy')
353 'Timed out waiting for CONNECT response from proxy')
354 while not r.complete():
354 while not r.complete():
355 try:
355 try:
356 if not r._select():
356 if not r._select():
357 if not r.complete():
357 if not r.complete():
358 raise timeout_exc
358 raise timeout_exc
359 except HTTPTimeoutException:
359 except HTTPTimeoutException:
360 # This raise/except pattern looks goofy, but
360 # This raise/except pattern looks goofy, but
361 # _select can raise the timeout as well as the
361 # _select can raise the timeout as well as the
362 # loop body. I wish it wasn't this convoluted,
362 # loop body. I wish it wasn't this convoluted,
363 # but I don't have a better solution
363 # but I don't have a better solution
364 # immediately handy.
364 # immediately handy.
365 raise timeout_exc
365 raise timeout_exc
366 if r.status != 200:
366 if r.status != 200:
367 raise HTTPProxyConnectFailedException(
367 raise HTTPProxyConnectFailedException(
368 'Proxy connection failed: %d %s' % (r.status,
368 'Proxy connection failed: %d %s' % (r.status,
369 r.read()))
369 r.read()))
370 logger.info('CONNECT (for SSL) to %s:%s via proxy succeeded.',
370 logger.info('CONNECT (for SSL) to %s:%s via proxy succeeded.',
371 self.host, self.port)
371 self.host, self.port)
372 else:
372 else:
373 sock = socketutil.create_connection((self.host, self.port))
373 sock = socketutil.create_connection((self.host, self.port))
374 if self.ssl:
374 if self.ssl:
375 # This is the default, but in the case of proxied SSL
375 # This is the default, but in the case of proxied SSL
376 # requests the proxy logic above will have cleared
376 # requests the proxy logic above will have cleared
377 # blocking mode, so re-enable it just to be safe.
377 # blocking mode, so re-enable it just to be safe.
378 sock.setblocking(1)
378 sock.setblocking(1)
379 logger.debug('wrapping socket for ssl with options %r',
379 logger.debug('wrapping socket for ssl with options %r',
380 self.ssl_opts)
380 self.ssl_opts)
381 sock = socketutil.wrap_socket(sock, **self.ssl_opts)
381 sock = socketutil.wrap_socket(sock, **self.ssl_opts)
382 if self._ssl_validator:
382 if self._ssl_validator:
383 self._ssl_validator(sock)
383 self._ssl_validator(sock)
384 sock.setblocking(0)
384 sock.setblocking(0)
385 self.sock = sock
385 self.sock = sock
386
386
387 def buildheaders(self, method, path, headers, http_ver):
387 def buildheaders(self, method, path, headers, http_ver):
388 if self.ssl and self.port == 443 or self.port == 80:
388 if self.ssl and self.port == 443 or self.port == 80:
389 # default port for protocol, so leave it out
389 # default port for protocol, so leave it out
390 hdrhost = self.host
390 hdrhost = self.host
391 else:
391 else:
392 # include nonstandard port in header
392 # include nonstandard port in header
393 if ':' in self.host: # must be IPv6
393 if ':' in self.host: # must be IPv6
394 hdrhost = '[%s]:%d' % (self.host, self.port)
394 hdrhost = '[%s]:%d' % (self.host, self.port)
395 else:
395 else:
396 hdrhost = '%s:%d' % (self.host, self.port)
396 hdrhost = '%s:%d' % (self.host, self.port)
397 if self._proxy_host and not self.ssl:
397 if self._proxy_host and not self.ssl:
398 # When talking to a regular http proxy we must send the
398 # When talking to a regular http proxy we must send the
399 # full URI, but in all other cases we must not (although
399 # full URI, but in all other cases we must not (although
400 # technically RFC 2616 says servers must accept our
400 # technically RFC 2616 says servers must accept our
401 # request if we screw up, experimentally few do that
401 # request if we screw up, experimentally few do that
402 # correctly.)
402 # correctly.)
403 assert path[0] == '/', 'path must start with a /'
403 assert path[0] == '/', 'path must start with a /'
404 path = 'http://%s%s' % (hdrhost, path)
404 path = 'http://%s%s' % (hdrhost, path)
405 outgoing = ['%s %s %s%s' % (method, path, http_ver, EOL)]
405 outgoing = ['%s %s %s%s' % (method, path, http_ver, EOL)]
406 headers['host'] = ('Host', hdrhost)
406 headers['host'] = ('Host', hdrhost)
407 headers[HDR_ACCEPT_ENCODING] = (HDR_ACCEPT_ENCODING, 'identity')
407 headers[HDR_ACCEPT_ENCODING] = (HDR_ACCEPT_ENCODING, 'identity')
408 for hdr, val in headers.itervalues():
408 for hdr, val in headers.itervalues():
409 outgoing.append('%s: %s%s' % (hdr, val, EOL))
409 outgoing.append('%s: %s%s' % (hdr, val, EOL))
410 outgoing.append(EOL)
410 outgoing.append(EOL)
411 return ''.join(outgoing)
411 return ''.join(outgoing)
412
412
413 def close(self):
413 def close(self):
414 """Close the connection to the server.
414 """Close the connection to the server.
415
415
416 This is a no-op if the connection is already closed. The
416 This is a no-op if the connection is already closed. The
417 connection may automatically close if requested by the server
417 connection may automatically close if requested by the server
418 or required by the nature of a response.
418 or required by the nature of a response.
419 """
419 """
420 if self.sock is None:
420 if self.sock is None:
421 return
421 return
422 self.sock.close()
422 self.sock.close()
423 self.sock = None
423 self.sock = None
424 logger.info('closed connection to %s on %s', self.host, self.port)
424 logger.info('closed connection to %s on %s', self.host, self.port)
425
425
426 def busy(self):
426 def busy(self):
427 """Returns True if this connection object is currently in use.
427 """Returns True if this connection object is currently in use.
428
428
429 If a response is still pending, this will return True, even if
429 If a response is still pending, this will return True, even if
430 the request has finished sending. In the future,
430 the request has finished sending. In the future,
431 HTTPConnection may transparently juggle multiple connections
431 HTTPConnection may transparently juggle multiple connections
432 to the server, in which case this will be useful to detect if
432 to the server, in which case this will be useful to detect if
433 any of those connections is ready for use.
433 any of those connections is ready for use.
434 """
434 """
435 cr = self._current_response
435 cr = self._current_response
436 if cr is not None:
436 if cr is not None:
437 if self._current_response_taken:
437 if self._current_response_taken:
438 if cr.will_close:
438 if cr.will_close:
439 self.sock = None
439 self.sock = None
440 self._current_response = None
440 self._current_response = None
441 return False
441 return False
442 elif cr.complete():
442 elif cr.complete():
443 self._current_response = None
443 self._current_response = None
444 return False
444 return False
445 return True
445 return True
446 return False
446 return False
447
447
448 def request(self, method, path, body=None, headers={},
448 def request(self, method, path, body=None, headers={},
449 expect_continue=False):
449 expect_continue=False):
450 """Send a request to the server.
450 """Send a request to the server.
451
451
452 For increased flexibility, this does not return the response
452 For increased flexibility, this does not return the response
453 object. Future versions of HTTPConnection that juggle multiple
453 object. Future versions of HTTPConnection that juggle multiple
454 sockets will be able to send (for example) 5 requests all at
454 sockets will be able to send (for example) 5 requests all at
455 once, and then let the requests arrive as data is
455 once, and then let the requests arrive as data is
456 available. Use the `getresponse()` method to retrieve the
456 available. Use the `getresponse()` method to retrieve the
457 response.
457 response.
458 """
458 """
459 if self.busy():
459 if self.busy():
460 raise httplib.CannotSendRequest(
460 raise httplib.CannotSendRequest(
461 'Can not send another request before '
461 'Can not send another request before '
462 'current response is read!')
462 'current response is read!')
463 self._current_response_taken = False
463 self._current_response_taken = False
464
464
465 logger.info('sending %s request for %s to %s on port %s',
465 logger.info('sending %s request for %s to %s on port %s',
466 method, path, self.host, self.port)
466 method, path, self.host, self.port)
467 hdrs = dict((k.lower(), (k, v)) for k, v in headers.iteritems())
467 hdrs = dict((k.lower(), (k, v)) for k, v in headers.iteritems())
468 if hdrs.get('expect', ('', ''))[1].lower() == '100-continue':
468 if hdrs.get('expect', ('', ''))[1].lower() == '100-continue':
469 expect_continue = True
469 expect_continue = True
470 elif expect_continue:
470 elif expect_continue:
471 hdrs['expect'] = ('Expect', '100-Continue')
471 hdrs['expect'] = ('Expect', '100-Continue')
472
472
473 chunked = False
473 chunked = False
474 if body and HDR_CONTENT_LENGTH not in hdrs:
474 if body and HDR_CONTENT_LENGTH not in hdrs:
475 if getattr(body, '__len__', False):
475 if getattr(body, '__len__', False):
476 hdrs[HDR_CONTENT_LENGTH] = (HDR_CONTENT_LENGTH, len(body))
476 hdrs[HDR_CONTENT_LENGTH] = (HDR_CONTENT_LENGTH, len(body))
477 elif getattr(body, 'read', False):
477 elif getattr(body, 'read', False):
478 hdrs[HDR_XFER_ENCODING] = (HDR_XFER_ENCODING,
478 hdrs[HDR_XFER_ENCODING] = (HDR_XFER_ENCODING,
479 XFER_ENCODING_CHUNKED)
479 XFER_ENCODING_CHUNKED)
480 chunked = True
480 chunked = True
481 else:
481 else:
482 raise BadRequestData('body has no __len__() nor read()')
482 raise BadRequestData('body has no __len__() nor read()')
483
483
484 self._connect()
484 self._connect()
485 outgoing_headers = self.buildheaders(
485 outgoing_headers = self.buildheaders(
486 method, path, hdrs, self.http_version)
486 method, path, hdrs, self.http_version)
487 response = None
487 response = None
488 first = True
488 first = True
489
489
490 def reconnect(where):
490 def reconnect(where):
491 logger.info('reconnecting during %s', where)
491 logger.info('reconnecting during %s', where)
492 self.close()
492 self.close()
493 self._connect()
493 self._connect()
494
494
495 while ((outgoing_headers or body)
495 while ((outgoing_headers or body)
496 and not (response and response.complete())):
496 and not (response and response.complete())):
497 select_timeout = self.timeout
497 select_timeout = self.timeout
498 out = outgoing_headers or body
498 out = outgoing_headers or body
499 blocking_on_continue = False
499 blocking_on_continue = False
500 if expect_continue and not outgoing_headers and not (
500 if expect_continue and not outgoing_headers and not (
501 response and (response.headers or response.continued)):
501 response and (response.headers or response.continued)):
502 logger.info(
502 logger.info(
503 'waiting up to %s seconds for'
503 'waiting up to %s seconds for'
504 ' continue response from server',
504 ' continue response from server',
505 self.continue_timeout)
505 self.continue_timeout)
506 select_timeout = self.continue_timeout
506 select_timeout = self.continue_timeout
507 blocking_on_continue = True
507 blocking_on_continue = True
508 out = False
508 out = False
509 if out:
509 if out:
510 w = [self.sock]
510 w = [self.sock]
511 else:
511 else:
512 w = []
512 w = []
513 r, w, x = select.select([self.sock], w, [], select_timeout)
513 r, w, x = select.select([self.sock], w, [], select_timeout)
514 # if we were expecting a 100 continue and it's been long
514 # if we were expecting a 100 continue and it's been long
515 # enough, just go ahead and assume it's ok. This is the
515 # enough, just go ahead and assume it's ok. This is the
516 # recommended behavior from the RFC.
516 # recommended behavior from the RFC.
517 if r == w == x == []:
517 if r == w == x == []:
518 if blocking_on_continue:
518 if blocking_on_continue:
519 expect_continue = False
519 expect_continue = False
520 logger.info('no response to continue expectation from '
520 logger.info('no response to continue expectation from '
521 'server, optimistically sending request body')
521 'server, optimistically sending request body')
522 else:
522 else:
523 raise HTTPTimeoutException('timeout sending data')
523 raise HTTPTimeoutException('timeout sending data')
524 was_first = first
524 was_first = first
525
525
526 # incoming data
526 # incoming data
527 if r:
527 if r:
528 try:
528 try:
529 try:
529 try:
530 data = r[0].recv(INCOMING_BUFFER_SIZE)
530 data = r[0].recv(INCOMING_BUFFER_SIZE)
531 except socket.sslerror, e:
531 except socket.sslerror, e:
532 if e.args[0] != socket.SSL_ERROR_WANT_READ:
532 if e.args[0] != socket.SSL_ERROR_WANT_READ:
533 raise
533 raise
534 logger.debug(
534 logger.debug(
535 'SSL_WANT_READ while sending data, retrying...')
535 'SSL_ERROR_WANT_READ while sending data, retrying...')
536 continue
536 continue
537 if not data:
537 if not data:
538 logger.info('socket appears closed in read')
538 logger.info('socket appears closed in read')
539 self.sock = None
539 self.sock = None
540 self._current_response = None
540 self._current_response = None
541 if response is not None:
541 if response is not None:
542 response._close()
542 response._close()
543 # This if/elif ladder is a bit subtle,
543 # This if/elif ladder is a bit subtle,
544 # comments in each branch should help.
544 # comments in each branch should help.
545 if response is not None and response.complete():
545 if response is not None and response.complete():
546 # Server responded completely and then
546 # Server responded completely and then
547 # closed the socket. We should just shut
547 # closed the socket. We should just shut
548 # things down and let the caller get their
548 # things down and let the caller get their
549 # response.
549 # response.
550 logger.info('Got an early response, '
550 logger.info('Got an early response, '
551 'aborting remaining request.')
551 'aborting remaining request.')
552 break
552 break
553 elif was_first and response is None:
553 elif was_first and response is None:
554 # Most likely a keepalive that got killed
554 # Most likely a keepalive that got killed
555 # on the server's end. Commonly happens
555 # on the server's end. Commonly happens
556 # after getting a really large response
556 # after getting a really large response
557 # from the server.
557 # from the server.
558 logger.info(
558 logger.info(
559 'Connection appeared closed in read on first'
559 'Connection appeared closed in read on first'
560 ' request loop iteration, will retry.')
560 ' request loop iteration, will retry.')
561 reconnect('read')
561 reconnect('read')
562 continue
562 continue
563 else:
563 else:
564 # We didn't just send the first data hunk,
564 # We didn't just send the first data hunk,
565 # and either have a partial response or no
565 # and either have a partial response or no
566 # response at all. There's really nothing
566 # response at all. There's really nothing
567 # meaningful we can do here.
567 # meaningful we can do here.
568 raise HTTPStateError(
568 raise HTTPStateError(
569 'Connection appears closed after '
569 'Connection appears closed after '
570 'some request data was written, but the '
570 'some request data was written, but the '
571 'response was missing or incomplete!')
571 'response was missing or incomplete!')
572 logger.debug('read %d bytes in request()', len(data))
572 logger.debug('read %d bytes in request()', len(data))
573 if response is None:
573 if response is None:
574 response = self.response_class(r[0], self.timeout, method)
574 response = self.response_class(r[0], self.timeout, method)
575 response._load_response(data)
575 response._load_response(data)
576 # Jump to the next select() call so we load more
576 # Jump to the next select() call so we load more
577 # data if the server is still sending us content.
577 # data if the server is still sending us content.
578 continue
578 continue
579 except socket.error, e:
579 except socket.error, e:
580 if e[0] != errno.EPIPE and not was_first:
580 if e[0] != errno.EPIPE and not was_first:
581 raise
581 raise
582
582
583 # outgoing data
583 # outgoing data
584 if w and out:
584 if w and out:
585 try:
585 try:
586 if getattr(out, 'read', False):
586 if getattr(out, 'read', False):
587 data = out.read(OUTGOING_BUFFER_SIZE)
587 data = out.read(OUTGOING_BUFFER_SIZE)
588 if not data:
588 if not data:
589 continue
589 continue
590 if len(data) < OUTGOING_BUFFER_SIZE:
590 if len(data) < OUTGOING_BUFFER_SIZE:
591 if chunked:
591 if chunked:
592 body = '0' + EOL + EOL
592 body = '0' + EOL + EOL
593 else:
593 else:
594 body = None
594 body = None
595 if chunked:
595 if chunked:
596 out = hex(len(data))[2:] + EOL + data + EOL
596 out = hex(len(data))[2:] + EOL + data + EOL
597 else:
597 else:
598 out = data
598 out = data
599 amt = w[0].send(out)
599 amt = w[0].send(out)
600 except socket.error, e:
600 except socket.error, e:
601 if e[0] == socket.SSL_ERROR_WANT_WRITE and self.ssl:
601 if e[0] == socket.SSL_ERROR_WANT_WRITE and self.ssl:
602 # This means that SSL hasn't flushed its buffer into
602 # This means that SSL hasn't flushed its buffer into
603 # the socket yet.
603 # the socket yet.
604 # TODO: find a way to block on ssl flushing its buffer
604 # TODO: find a way to block on ssl flushing its buffer
605 # similar to selecting on a raw socket.
605 # similar to selecting on a raw socket.
606 continue
606 continue
607 elif (e[0] not in (errno.ECONNRESET, errno.EPIPE)
607 elif (e[0] not in (errno.ECONNRESET, errno.EPIPE)
608 and not first):
608 and not first):
609 raise
609 raise
610 reconnect('write')
610 reconnect('write')
611 amt = self.sock.send(out)
611 amt = self.sock.send(out)
612 logger.debug('sent %d', amt)
612 logger.debug('sent %d', amt)
613 first = False
613 first = False
614 # stash data we think we sent in case the socket breaks
614 # stash data we think we sent in case the socket breaks
615 # when we read from it
615 # when we read from it
616 if was_first:
616 if was_first:
617 sent_data = out[:amt]
617 sent_data = out[:amt]
618 if out is body:
618 if out is body:
619 body = out[amt:]
619 body = out[amt:]
620 else:
620 else:
621 outgoing_headers = out[amt:]
621 outgoing_headers = out[amt:]
622
622
623 # close if the server response said to or responded before eating
623 # close if the server response said to or responded before eating
624 # the whole request
624 # the whole request
625 if response is None:
625 if response is None:
626 response = self.response_class(self.sock, self.timeout, method)
626 response = self.response_class(self.sock, self.timeout, method)
627 complete = response.complete()
627 complete = response.complete()
628 data_left = bool(outgoing_headers or body)
628 data_left = bool(outgoing_headers or body)
629 if data_left:
629 if data_left:
630 logger.info('stopped sending request early, '
630 logger.info('stopped sending request early, '
631 'will close the socket to be safe.')
631 'will close the socket to be safe.')
632 response.will_close = True
632 response.will_close = True
633 if response.will_close:
633 if response.will_close:
634 # The socket will be closed by the response, so we disown
634 # The socket will be closed by the response, so we disown
635 # the socket
635 # the socket
636 self.sock = None
636 self.sock = None
637 self._current_response = response
637 self._current_response = response
638
638
639 def getresponse(self):
639 def getresponse(self):
640 if self._current_response is None:
640 if self._current_response is None:
641 raise httplib.ResponseNotReady()
641 raise httplib.ResponseNotReady()
642 r = self._current_response
642 r = self._current_response
643 while r.headers is None:
643 while r.headers is None:
644 if not r._select() and not r.complete():
644 if not r._select() and not r.complete():
645 raise _readers.HTTPRemoteClosedError()
645 raise _readers.HTTPRemoteClosedError()
646 if r.will_close:
646 if r.will_close:
647 self.sock = None
647 self.sock = None
648 self._current_response = None
648 self._current_response = None
649 elif r.complete():
649 elif r.complete():
650 self._current_response = None
650 self._current_response = None
651 else:
651 else:
652 self._current_response_taken = True
652 self._current_response_taken = True
653 return r
653 return r
654
654
655
655
656 class HTTPTimeoutException(httplib.HTTPException):
656 class HTTPTimeoutException(httplib.HTTPException):
657 """A timeout occurred while waiting on the server."""
657 """A timeout occurred while waiting on the server."""
658
658
659
659
660 class BadRequestData(httplib.HTTPException):
660 class BadRequestData(httplib.HTTPException):
661 """Request body object has neither __len__ nor read."""
661 """Request body object has neither __len__ nor read."""
662
662
663
663
664 class HTTPProxyConnectFailedException(httplib.HTTPException):
664 class HTTPProxyConnectFailedException(httplib.HTTPException):
665 """Connecting to the HTTP proxy failed."""
665 """Connecting to the HTTP proxy failed."""
666
666
667
667
668 class HTTPStateError(httplib.HTTPException):
668 class HTTPStateError(httplib.HTTPException):
669 """Invalid internal state encountered."""
669 """Invalid internal state encountered."""
670
670
671 # Forward this exception type from _readers since it needs to be part
671 # Forward this exception type from _readers since it needs to be part
672 # of the public API.
672 # of the public API.
673 HTTPRemoteClosedError = _readers.HTTPRemoteClosedError
673 HTTPRemoteClosedError = _readers.HTTPRemoteClosedError
674 # no-check-code
674 # no-check-code
@@ -1,284 +1,284
1 # httpconnection.py - urllib2 handler for new http support
1 # httpconnection.py - urllib2 handler for new http support
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 # Copyright 2011 Google, Inc.
6 # Copyright 2011 Google, Inc.
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10 import logging
10 import logging
11 import socket
11 import socket
12 import urllib
12 import urllib
13 import urllib2
13 import urllib2
14 import os
14 import os
15
15
16 from mercurial import httpclient
16 from mercurial import httpclient
17 from mercurial import sslutil
17 from mercurial import sslutil
18 from mercurial import util
18 from mercurial import util
19 from mercurial.i18n import _
19 from mercurial.i18n import _
20
20
21 # moved here from url.py to avoid a cycle
21 # moved here from url.py to avoid a cycle
22 class httpsendfile(object):
22 class httpsendfile(object):
23 """This is a wrapper around the objects returned by python's "open".
23 """This is a wrapper around the objects returned by python's "open".
24
24
25 Its purpose is to send file-like objects via HTTP.
25 Its purpose is to send file-like objects via HTTP.
26 It do however not define a __len__ attribute because the length
26 It do however not define a __len__ attribute because the length
27 might be more than Py_ssize_t can handle.
27 might be more than Py_ssize_t can handle.
28 """
28 """
29
29
30 def __init__(self, ui, *args, **kwargs):
30 def __init__(self, ui, *args, **kwargs):
31 # We can't just "self._data = open(*args, **kwargs)" here because there
31 # We can't just "self._data = open(*args, **kwargs)" here because there
32 # is an "open" function defined in this module that shadows the global
32 # is an "open" function defined in this module that shadows the global
33 # one
33 # one
34 self.ui = ui
34 self.ui = ui
35 self._data = open(*args, **kwargs)
35 self._data = open(*args, **kwargs)
36 self.seek = self._data.seek
36 self.seek = self._data.seek
37 self.close = self._data.close
37 self.close = self._data.close
38 self.write = self._data.write
38 self.write = self._data.write
39 self.length = os.fstat(self._data.fileno()).st_size
39 self.length = os.fstat(self._data.fileno()).st_size
40 self._pos = 0
40 self._pos = 0
41 self._total = self.length // 1024 * 2
41 self._total = self.length // 1024 * 2
42
42
43 def read(self, *args, **kwargs):
43 def read(self, *args, **kwargs):
44 try:
44 try:
45 ret = self._data.read(*args, **kwargs)
45 ret = self._data.read(*args, **kwargs)
46 except EOFError:
46 except EOFError:
47 self.ui.progress(_('sending'), None)
47 self.ui.progress(_('sending'), None)
48 self._pos += len(ret)
48 self._pos += len(ret)
49 # We pass double the max for total because we currently have
49 # We pass double the max for total because we currently have
50 # to send the bundle twice in the case of a server that
50 # to send the bundle twice in the case of a server that
51 # requires authentication. Since we can't know until we try
51 # requires authentication. Since we can't know until we try
52 # once whether authentication will be required, just lie to
52 # once whether authentication will be required, just lie to
53 # the user and maybe the push succeeds suddenly at 50%.
53 # the user and maybe the push succeeds suddenly at 50%.
54 self.ui.progress(_('sending'), self._pos // 1024,
54 self.ui.progress(_('sending'), self._pos // 1024,
55 unit=_('kb'), total=self._total)
55 unit=_('kb'), total=self._total)
56 return ret
56 return ret
57
57
58 # moved here from url.py to avoid a cycle
58 # moved here from url.py to avoid a cycle
59 def readauthforuri(ui, uri, user):
59 def readauthforuri(ui, uri, user):
60 # Read configuration
60 # Read configuration
61 config = dict()
61 config = dict()
62 for key, val in ui.configitems('auth'):
62 for key, val in ui.configitems('auth'):
63 if '.' not in key:
63 if '.' not in key:
64 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
64 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
65 continue
65 continue
66 group, setting = key.rsplit('.', 1)
66 group, setting = key.rsplit('.', 1)
67 gdict = config.setdefault(group, dict())
67 gdict = config.setdefault(group, dict())
68 if setting in ('username', 'cert', 'key'):
68 if setting in ('username', 'cert', 'key'):
69 val = util.expandpath(val)
69 val = util.expandpath(val)
70 gdict[setting] = val
70 gdict[setting] = val
71
71
72 # Find the best match
72 # Find the best match
73 if '://' in uri:
73 if '://' in uri:
74 scheme, hostpath = uri.split('://', 1)
74 scheme, hostpath = uri.split('://', 1)
75 else:
75 else:
76 # py2.4.1 doesn't provide the full URI
76 # Python 2.4.1 doesn't provide the full URI
77 scheme, hostpath = 'http', uri
77 scheme, hostpath = 'http', uri
78 bestuser = None
78 bestuser = None
79 bestlen = 0
79 bestlen = 0
80 bestauth = None
80 bestauth = None
81 for group, auth in config.iteritems():
81 for group, auth in config.iteritems():
82 if user and user != auth.get('username', user):
82 if user and user != auth.get('username', user):
83 # If a username was set in the URI, the entry username
83 # If a username was set in the URI, the entry username
84 # must either match it or be unset
84 # must either match it or be unset
85 continue
85 continue
86 prefix = auth.get('prefix')
86 prefix = auth.get('prefix')
87 if not prefix:
87 if not prefix:
88 continue
88 continue
89 p = prefix.split('://', 1)
89 p = prefix.split('://', 1)
90 if len(p) > 1:
90 if len(p) > 1:
91 schemes, prefix = [p[0]], p[1]
91 schemes, prefix = [p[0]], p[1]
92 else:
92 else:
93 schemes = (auth.get('schemes') or 'https').split()
93 schemes = (auth.get('schemes') or 'https').split()
94 if (prefix == '*' or hostpath.startswith(prefix)) and \
94 if (prefix == '*' or hostpath.startswith(prefix)) and \
95 (len(prefix) > bestlen or (len(prefix) == bestlen and \
95 (len(prefix) > bestlen or (len(prefix) == bestlen and \
96 not bestuser and 'username' in auth)) \
96 not bestuser and 'username' in auth)) \
97 and scheme in schemes:
97 and scheme in schemes:
98 bestlen = len(prefix)
98 bestlen = len(prefix)
99 bestauth = group, auth
99 bestauth = group, auth
100 bestuser = auth.get('username')
100 bestuser = auth.get('username')
101 if user and not bestuser:
101 if user and not bestuser:
102 auth['username'] = user
102 auth['username'] = user
103 return bestauth
103 return bestauth
104
104
105 # Mercurial (at least until we can remove the old codepath) requires
105 # Mercurial (at least until we can remove the old codepath) requires
106 # that the http response object be sufficiently file-like, so we
106 # that the http response object be sufficiently file-like, so we
107 # provide a close() method here.
107 # provide a close() method here.
108 class HTTPResponse(httpclient.HTTPResponse):
108 class HTTPResponse(httpclient.HTTPResponse):
109 def close(self):
109 def close(self):
110 pass
110 pass
111
111
112 class HTTPConnection(httpclient.HTTPConnection):
112 class HTTPConnection(httpclient.HTTPConnection):
113 response_class = HTTPResponse
113 response_class = HTTPResponse
114 def request(self, method, uri, body=None, headers={}):
114 def request(self, method, uri, body=None, headers={}):
115 if isinstance(body, httpsendfile):
115 if isinstance(body, httpsendfile):
116 body.seek(0)
116 body.seek(0)
117 httpclient.HTTPConnection.request(self, method, uri, body=body,
117 httpclient.HTTPConnection.request(self, method, uri, body=body,
118 headers=headers)
118 headers=headers)
119
119
120
120
121 _configuredlogging = False
121 _configuredlogging = False
122 LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
122 LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
123 # Subclass BOTH of these because otherwise urllib2 "helpfully"
123 # Subclass BOTH of these because otherwise urllib2 "helpfully"
124 # reinserts them since it notices we don't include any subclasses of
124 # reinserts them since it notices we don't include any subclasses of
125 # them.
125 # them.
126 class http2handler(urllib2.HTTPHandler, urllib2.HTTPSHandler):
126 class http2handler(urllib2.HTTPHandler, urllib2.HTTPSHandler):
127 def __init__(self, ui, pwmgr):
127 def __init__(self, ui, pwmgr):
128 global _configuredlogging
128 global _configuredlogging
129 urllib2.AbstractHTTPHandler.__init__(self)
129 urllib2.AbstractHTTPHandler.__init__(self)
130 self.ui = ui
130 self.ui = ui
131 self.pwmgr = pwmgr
131 self.pwmgr = pwmgr
132 self._connections = {}
132 self._connections = {}
133 loglevel = ui.config('ui', 'http2debuglevel', default=None)
133 loglevel = ui.config('ui', 'http2debuglevel', default=None)
134 if loglevel and not _configuredlogging:
134 if loglevel and not _configuredlogging:
135 _configuredlogging = True
135 _configuredlogging = True
136 logger = logging.getLogger('mercurial.httpclient')
136 logger = logging.getLogger('mercurial.httpclient')
137 logger.setLevel(getattr(logging, loglevel.upper()))
137 logger.setLevel(getattr(logging, loglevel.upper()))
138 handler = logging.StreamHandler()
138 handler = logging.StreamHandler()
139 handler.setFormatter(logging.Formatter(LOGFMT))
139 handler.setFormatter(logging.Formatter(LOGFMT))
140 logger.addHandler(handler)
140 logger.addHandler(handler)
141
141
142 def close_all(self):
142 def close_all(self):
143 """Close and remove all connection objects being kept for reuse."""
143 """Close and remove all connection objects being kept for reuse."""
144 for openconns in self._connections.values():
144 for openconns in self._connections.values():
145 for conn in openconns:
145 for conn in openconns:
146 conn.close()
146 conn.close()
147 self._connections = {}
147 self._connections = {}
148
148
149 # shamelessly borrowed from urllib2.AbstractHTTPHandler
149 # shamelessly borrowed from urllib2.AbstractHTTPHandler
150 def do_open(self, http_class, req, use_ssl):
150 def do_open(self, http_class, req, use_ssl):
151 """Return an addinfourl object for the request, using http_class.
151 """Return an addinfourl object for the request, using http_class.
152
152
153 http_class must implement the HTTPConnection API from httplib.
153 http_class must implement the HTTPConnection API from httplib.
154 The addinfourl return value is a file-like object. It also
154 The addinfourl return value is a file-like object. It also
155 has methods and attributes including:
155 has methods and attributes including:
156 - info(): return a mimetools.Message object for the headers
156 - info(): return a mimetools.Message object for the headers
157 - geturl(): return the original request URL
157 - geturl(): return the original request URL
158 - code: HTTP status code
158 - code: HTTP status code
159 """
159 """
160 # If using a proxy, the host returned by get_host() is
160 # If using a proxy, the host returned by get_host() is
161 # actually the proxy. On Python 2.6.1, the real destination
161 # actually the proxy. On Python 2.6.1, the real destination
162 # hostname is encoded in the URI in the urllib2 request
162 # hostname is encoded in the URI in the urllib2 request
163 # object. On Python 2.6.5, it's stored in the _tunnel_host
163 # object. On Python 2.6.5, it's stored in the _tunnel_host
164 # attribute which has no accessor.
164 # attribute which has no accessor.
165 tunhost = getattr(req, '_tunnel_host', None)
165 tunhost = getattr(req, '_tunnel_host', None)
166 host = req.get_host()
166 host = req.get_host()
167 if tunhost:
167 if tunhost:
168 proxyhost = host
168 proxyhost = host
169 host = tunhost
169 host = tunhost
170 elif req.has_proxy():
170 elif req.has_proxy():
171 proxyhost = req.get_host()
171 proxyhost = req.get_host()
172 host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
172 host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
173 else:
173 else:
174 proxyhost = None
174 proxyhost = None
175
175
176 if proxyhost:
176 if proxyhost:
177 if ':' in proxyhost:
177 if ':' in proxyhost:
178 # Note: this means we'll explode if we try and use an
178 # Note: this means we'll explode if we try and use an
179 # IPv6 http proxy. This isn't a regression, so we
179 # IPv6 http proxy. This isn't a regression, so we
180 # won't worry about it for now.
180 # won't worry about it for now.
181 proxyhost, proxyport = proxyhost.rsplit(':', 1)
181 proxyhost, proxyport = proxyhost.rsplit(':', 1)
182 else:
182 else:
183 proxyport = 3128 # squid default
183 proxyport = 3128 # squid default
184 proxy = (proxyhost, proxyport)
184 proxy = (proxyhost, proxyport)
185 else:
185 else:
186 proxy = None
186 proxy = None
187
187
188 if not host:
188 if not host:
189 raise urllib2.URLError('no host given')
189 raise urllib2.URLError('no host given')
190
190
191 connkey = use_ssl, host, proxy
191 connkey = use_ssl, host, proxy
192 allconns = self._connections.get(connkey, [])
192 allconns = self._connections.get(connkey, [])
193 conns = [c for c in allconns if not c.busy()]
193 conns = [c for c in allconns if not c.busy()]
194 if conns:
194 if conns:
195 h = conns[0]
195 h = conns[0]
196 else:
196 else:
197 if allconns:
197 if allconns:
198 self.ui.debug('all connections for %s busy, making a new '
198 self.ui.debug('all connections for %s busy, making a new '
199 'one\n' % host)
199 'one\n' % host)
200 timeout = None
200 timeout = None
201 if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
201 if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
202 timeout = req.timeout
202 timeout = req.timeout
203 h = http_class(host, timeout=timeout, proxy_hostport=proxy)
203 h = http_class(host, timeout=timeout, proxy_hostport=proxy)
204 self._connections.setdefault(connkey, []).append(h)
204 self._connections.setdefault(connkey, []).append(h)
205
205
206 headers = dict(req.headers)
206 headers = dict(req.headers)
207 headers.update(req.unredirected_hdrs)
207 headers.update(req.unredirected_hdrs)
208 headers = dict(
208 headers = dict(
209 (name.title(), val) for name, val in headers.items())
209 (name.title(), val) for name, val in headers.items())
210 try:
210 try:
211 path = req.get_selector()
211 path = req.get_selector()
212 if '://' in path:
212 if '://' in path:
213 path = path.split('://', 1)[1].split('/', 1)[1]
213 path = path.split('://', 1)[1].split('/', 1)[1]
214 if path[0] != '/':
214 if path[0] != '/':
215 path = '/' + path
215 path = '/' + path
216 h.request(req.get_method(), path, req.data, headers)
216 h.request(req.get_method(), path, req.data, headers)
217 r = h.getresponse()
217 r = h.getresponse()
218 except socket.error, err: # XXX what error?
218 except socket.error, err: # XXX what error?
219 raise urllib2.URLError(err)
219 raise urllib2.URLError(err)
220
220
221 # Pick apart the HTTPResponse object to get the addinfourl
221 # Pick apart the HTTPResponse object to get the addinfourl
222 # object initialized properly.
222 # object initialized properly.
223 r.recv = r.read
223 r.recv = r.read
224
224
225 resp = urllib.addinfourl(r, r.headers, req.get_full_url())
225 resp = urllib.addinfourl(r, r.headers, req.get_full_url())
226 resp.code = r.status
226 resp.code = r.status
227 resp.msg = r.reason
227 resp.msg = r.reason
228 return resp
228 return resp
229
229
230 # httplib always uses the given host/port as the socket connect
230 # httplib always uses the given host/port as the socket connect
231 # target, and then allows full URIs in the request path, which it
231 # target, and then allows full URIs in the request path, which it
232 # then observes and treats as a signal to do proxying instead.
232 # then observes and treats as a signal to do proxying instead.
233 def http_open(self, req):
233 def http_open(self, req):
234 if req.get_full_url().startswith('https'):
234 if req.get_full_url().startswith('https'):
235 return self.https_open(req)
235 return self.https_open(req)
236 return self.do_open(HTTPConnection, req, False)
236 return self.do_open(HTTPConnection, req, False)
237
237
238 def https_open(self, req):
238 def https_open(self, req):
239 # req.get_full_url() does not contain credentials and we may
239 # req.get_full_url() does not contain credentials and we may
240 # need them to match the certificates.
240 # need them to match the certificates.
241 url = req.get_full_url()
241 url = req.get_full_url()
242 user, password = self.pwmgr.find_stored_password(url)
242 user, password = self.pwmgr.find_stored_password(url)
243 res = readauthforuri(self.ui, url, user)
243 res = readauthforuri(self.ui, url, user)
244 if res:
244 if res:
245 group, auth = res
245 group, auth = res
246 self.auth = auth
246 self.auth = auth
247 self.ui.debug("using auth.%s.* for authentication\n" % group)
247 self.ui.debug("using auth.%s.* for authentication\n" % group)
248 else:
248 else:
249 self.auth = None
249 self.auth = None
250 return self.do_open(self._makesslconnection, req, True)
250 return self.do_open(self._makesslconnection, req, True)
251
251
252 def _makesslconnection(self, host, port=443, *args, **kwargs):
252 def _makesslconnection(self, host, port=443, *args, **kwargs):
253 keyfile = None
253 keyfile = None
254 certfile = None
254 certfile = None
255
255
256 if args: # key_file
256 if args: # key_file
257 keyfile = args.pop(0)
257 keyfile = args.pop(0)
258 if args: # cert_file
258 if args: # cert_file
259 certfile = args.pop(0)
259 certfile = args.pop(0)
260
260
261 # if the user has specified different key/cert files in
261 # if the user has specified different key/cert files in
262 # hgrc, we prefer these
262 # hgrc, we prefer these
263 if self.auth and 'key' in self.auth and 'cert' in self.auth:
263 if self.auth and 'key' in self.auth and 'cert' in self.auth:
264 keyfile = self.auth['key']
264 keyfile = self.auth['key']
265 certfile = self.auth['cert']
265 certfile = self.auth['cert']
266
266
267 # let host port take precedence
267 # let host port take precedence
268 if ':' in host and '[' not in host or ']:' in host:
268 if ':' in host and '[' not in host or ']:' in host:
269 host, port = host.rsplit(':', 1)
269 host, port = host.rsplit(':', 1)
270 port = int(port)
270 port = int(port)
271 if '[' in host:
271 if '[' in host:
272 host = host[1:-1]
272 host = host[1:-1]
273
273
274 if keyfile:
274 if keyfile:
275 kwargs['keyfile'] = keyfile
275 kwargs['keyfile'] = keyfile
276 if certfile:
276 if certfile:
277 kwargs['certfile'] = certfile
277 kwargs['certfile'] = certfile
278
278
279 kwargs.update(sslutil.sslkwargs(self.ui, host))
279 kwargs.update(sslutil.sslkwargs(self.ui, host))
280
280
281 con = HTTPConnection(host, port, use_ssl=True,
281 con = HTTPConnection(host, port, use_ssl=True,
282 ssl_validator=sslutil.validator(self.ui, host),
282 ssl_validator=sslutil.validator(self.ui, host),
283 **kwargs)
283 **kwargs)
284 return con
284 return con
@@ -1,255 +1,255
1 # mail.py - mail sending bits for mercurial
1 # mail.py - mail sending bits for mercurial
2 #
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from i18n import _
8 from i18n import _
9 import util, encoding
9 import util, encoding
10 import os, smtplib, socket, quopri, time
10 import os, smtplib, socket, quopri, time
11 import email.Header, email.MIMEText, email.Utils
11 import email.Header, email.MIMEText, email.Utils
12
12
13 _oldheaderinit = email.Header.Header.__init__
13 _oldheaderinit = email.Header.Header.__init__
14 def _unifiedheaderinit(self, *args, **kw):
14 def _unifiedheaderinit(self, *args, **kw):
15 """
15 """
16 Python2.7 introduces a backwards incompatible change
16 Python 2.7 introduces a backwards incompatible change
17 (Python issue1974, r70772) in email.Generator.Generator code:
17 (Python issue1974, r70772) in email.Generator.Generator code:
18 pre-2.7 code passed "continuation_ws='\t'" to the Header
18 pre-2.7 code passed "continuation_ws='\t'" to the Header
19 constructor, and 2.7 removed this parameter.
19 constructor, and 2.7 removed this parameter.
20
20
21 Default argument is continuation_ws=' ', which means that the
21 Default argument is continuation_ws=' ', which means that the
22 behaviour is different in <2.7 and 2.7
22 behaviour is different in <2.7 and 2.7
23
23
24 We consider the 2.7 behaviour to be preferable, but need
24 We consider the 2.7 behaviour to be preferable, but need
25 to have an unified behaviour for versions 2.4 to 2.7
25 to have an unified behaviour for versions 2.4 to 2.7
26 """
26 """
27 # override continuation_ws
27 # override continuation_ws
28 kw['continuation_ws'] = ' '
28 kw['continuation_ws'] = ' '
29 _oldheaderinit(self, *args, **kw)
29 _oldheaderinit(self, *args, **kw)
30
30
31 email.Header.Header.__dict__['__init__'] = _unifiedheaderinit
31 email.Header.Header.__dict__['__init__'] = _unifiedheaderinit
32
32
33 def _smtp(ui):
33 def _smtp(ui):
34 '''build an smtp connection and return a function to send mail'''
34 '''build an smtp connection and return a function to send mail'''
35 local_hostname = ui.config('smtp', 'local_hostname')
35 local_hostname = ui.config('smtp', 'local_hostname')
36 tls = ui.config('smtp', 'tls', 'none')
36 tls = ui.config('smtp', 'tls', 'none')
37 # backward compatible: when tls = true, we use starttls.
37 # backward compatible: when tls = true, we use starttls.
38 starttls = tls == 'starttls' or util.parsebool(tls)
38 starttls = tls == 'starttls' or util.parsebool(tls)
39 smtps = tls == 'smtps'
39 smtps = tls == 'smtps'
40 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
40 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
41 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
41 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
42 if smtps:
42 if smtps:
43 ui.note(_('(using smtps)\n'))
43 ui.note(_('(using smtps)\n'))
44 s = smtplib.SMTP_SSL(local_hostname=local_hostname)
44 s = smtplib.SMTP_SSL(local_hostname=local_hostname)
45 else:
45 else:
46 s = smtplib.SMTP(local_hostname=local_hostname)
46 s = smtplib.SMTP(local_hostname=local_hostname)
47 mailhost = ui.config('smtp', 'host')
47 mailhost = ui.config('smtp', 'host')
48 if not mailhost:
48 if not mailhost:
49 raise util.Abort(_('smtp.host not configured - cannot send mail'))
49 raise util.Abort(_('smtp.host not configured - cannot send mail'))
50 mailport = util.getport(ui.config('smtp', 'port', 25))
50 mailport = util.getport(ui.config('smtp', 'port', 25))
51 ui.note(_('sending mail: smtp host %s, port %s\n') %
51 ui.note(_('sending mail: smtp host %s, port %s\n') %
52 (mailhost, mailport))
52 (mailhost, mailport))
53 s.connect(host=mailhost, port=mailport)
53 s.connect(host=mailhost, port=mailport)
54 if starttls:
54 if starttls:
55 ui.note(_('(using starttls)\n'))
55 ui.note(_('(using starttls)\n'))
56 s.ehlo()
56 s.ehlo()
57 s.starttls()
57 s.starttls()
58 s.ehlo()
58 s.ehlo()
59 username = ui.config('smtp', 'username')
59 username = ui.config('smtp', 'username')
60 password = ui.config('smtp', 'password')
60 password = ui.config('smtp', 'password')
61 if username and not password:
61 if username and not password:
62 password = ui.getpass()
62 password = ui.getpass()
63 if username and password:
63 if username and password:
64 ui.note(_('(authenticating to mail server as %s)\n') %
64 ui.note(_('(authenticating to mail server as %s)\n') %
65 (username))
65 (username))
66 try:
66 try:
67 s.login(username, password)
67 s.login(username, password)
68 except smtplib.SMTPException, inst:
68 except smtplib.SMTPException, inst:
69 raise util.Abort(inst)
69 raise util.Abort(inst)
70
70
71 def send(sender, recipients, msg):
71 def send(sender, recipients, msg):
72 try:
72 try:
73 return s.sendmail(sender, recipients, msg)
73 return s.sendmail(sender, recipients, msg)
74 except smtplib.SMTPRecipientsRefused, inst:
74 except smtplib.SMTPRecipientsRefused, inst:
75 recipients = [r[1] for r in inst.recipients.values()]
75 recipients = [r[1] for r in inst.recipients.values()]
76 raise util.Abort('\n' + '\n'.join(recipients))
76 raise util.Abort('\n' + '\n'.join(recipients))
77 except smtplib.SMTPException, inst:
77 except smtplib.SMTPException, inst:
78 raise util.Abort(inst)
78 raise util.Abort(inst)
79
79
80 return send
80 return send
81
81
82 def _sendmail(ui, sender, recipients, msg):
82 def _sendmail(ui, sender, recipients, msg):
83 '''send mail using sendmail.'''
83 '''send mail using sendmail.'''
84 program = ui.config('email', 'method')
84 program = ui.config('email', 'method')
85 cmdline = '%s -f %s %s' % (program, util.email(sender),
85 cmdline = '%s -f %s %s' % (program, util.email(sender),
86 ' '.join(map(util.email, recipients)))
86 ' '.join(map(util.email, recipients)))
87 ui.note(_('sending mail: %s\n') % cmdline)
87 ui.note(_('sending mail: %s\n') % cmdline)
88 fp = util.popen(cmdline, 'w')
88 fp = util.popen(cmdline, 'w')
89 fp.write(msg)
89 fp.write(msg)
90 ret = fp.close()
90 ret = fp.close()
91 if ret:
91 if ret:
92 raise util.Abort('%s %s' % (
92 raise util.Abort('%s %s' % (
93 os.path.basename(program.split(None, 1)[0]),
93 os.path.basename(program.split(None, 1)[0]),
94 util.explainexit(ret)[0]))
94 util.explainexit(ret)[0]))
95
95
96 def _mbox(mbox, sender, recipients, msg):
96 def _mbox(mbox, sender, recipients, msg):
97 '''write mails to mbox'''
97 '''write mails to mbox'''
98 fp = open(mbox, 'ab+')
98 fp = open(mbox, 'ab+')
99 # Should be time.asctime(), but Windows prints 2-characters day
99 # Should be time.asctime(), but Windows prints 2-characters day
100 # of month instead of one. Make them print the same thing.
100 # of month instead of one. Make them print the same thing.
101 date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime())
101 date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime())
102 fp.write('From %s %s\n' % (sender, date))
102 fp.write('From %s %s\n' % (sender, date))
103 fp.write(msg)
103 fp.write(msg)
104 fp.write('\n\n')
104 fp.write('\n\n')
105 fp.close()
105 fp.close()
106
106
107 def connect(ui, mbox=None):
107 def connect(ui, mbox=None):
108 '''make a mail connection. return a function to send mail.
108 '''make a mail connection. return a function to send mail.
109 call as sendmail(sender, list-of-recipients, msg).'''
109 call as sendmail(sender, list-of-recipients, msg).'''
110 if mbox:
110 if mbox:
111 open(mbox, 'wb').close()
111 open(mbox, 'wb').close()
112 return lambda s, r, m: _mbox(mbox, s, r, m)
112 return lambda s, r, m: _mbox(mbox, s, r, m)
113 if ui.config('email', 'method', 'smtp') == 'smtp':
113 if ui.config('email', 'method', 'smtp') == 'smtp':
114 return _smtp(ui)
114 return _smtp(ui)
115 return lambda s, r, m: _sendmail(ui, s, r, m)
115 return lambda s, r, m: _sendmail(ui, s, r, m)
116
116
117 def sendmail(ui, sender, recipients, msg, mbox=None):
117 def sendmail(ui, sender, recipients, msg, mbox=None):
118 send = connect(ui, mbox=mbox)
118 send = connect(ui, mbox=mbox)
119 return send(sender, recipients, msg)
119 return send(sender, recipients, msg)
120
120
121 def validateconfig(ui):
121 def validateconfig(ui):
122 '''determine if we have enough config data to try sending email.'''
122 '''determine if we have enough config data to try sending email.'''
123 method = ui.config('email', 'method', 'smtp')
123 method = ui.config('email', 'method', 'smtp')
124 if method == 'smtp':
124 if method == 'smtp':
125 if not ui.config('smtp', 'host'):
125 if not ui.config('smtp', 'host'):
126 raise util.Abort(_('smtp specified as email transport, '
126 raise util.Abort(_('smtp specified as email transport, '
127 'but no smtp host configured'))
127 'but no smtp host configured'))
128 else:
128 else:
129 if not util.findexe(method):
129 if not util.findexe(method):
130 raise util.Abort(_('%r specified as email transport, '
130 raise util.Abort(_('%r specified as email transport, '
131 'but not in PATH') % method)
131 'but not in PATH') % method)
132
132
133 def mimetextpatch(s, subtype='plain', display=False):
133 def mimetextpatch(s, subtype='plain', display=False):
134 '''Return MIME message suitable for a patch.
134 '''Return MIME message suitable for a patch.
135 Charset will be detected as utf-8 or (possibly fake) us-ascii.
135 Charset will be detected as utf-8 or (possibly fake) us-ascii.
136 Transfer encodings will be used if necessary.'''
136 Transfer encodings will be used if necessary.'''
137
137
138 cs = 'us-ascii'
138 cs = 'us-ascii'
139 if not display:
139 if not display:
140 try:
140 try:
141 s.decode('us-ascii')
141 s.decode('us-ascii')
142 except UnicodeDecodeError:
142 except UnicodeDecodeError:
143 try:
143 try:
144 s.decode('utf-8')
144 s.decode('utf-8')
145 cs = 'utf-8'
145 cs = 'utf-8'
146 except UnicodeDecodeError:
146 except UnicodeDecodeError:
147 # We'll go with us-ascii as a fallback.
147 # We'll go with us-ascii as a fallback.
148 pass
148 pass
149
149
150 return mimetextqp(s, subtype, cs)
150 return mimetextqp(s, subtype, cs)
151
151
152 def mimetextqp(body, subtype, charset):
152 def mimetextqp(body, subtype, charset):
153 '''Return MIME message.
153 '''Return MIME message.
154 Quoted-printable transfer encoding will be used if necessary.
154 Quoted-printable transfer encoding will be used if necessary.
155 '''
155 '''
156 enc = None
156 enc = None
157 for line in body.splitlines():
157 for line in body.splitlines():
158 if len(line) > 950:
158 if len(line) > 950:
159 body = quopri.encodestring(body)
159 body = quopri.encodestring(body)
160 enc = "quoted-printable"
160 enc = "quoted-printable"
161 break
161 break
162
162
163 msg = email.MIMEText.MIMEText(body, subtype, charset)
163 msg = email.MIMEText.MIMEText(body, subtype, charset)
164 if enc:
164 if enc:
165 del msg['Content-Transfer-Encoding']
165 del msg['Content-Transfer-Encoding']
166 msg['Content-Transfer-Encoding'] = enc
166 msg['Content-Transfer-Encoding'] = enc
167 return msg
167 return msg
168
168
169 def _charsets(ui):
169 def _charsets(ui):
170 '''Obtains charsets to send mail parts not containing patches.'''
170 '''Obtains charsets to send mail parts not containing patches.'''
171 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
171 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
172 fallbacks = [encoding.fallbackencoding.lower(),
172 fallbacks = [encoding.fallbackencoding.lower(),
173 encoding.encoding.lower(), 'utf-8']
173 encoding.encoding.lower(), 'utf-8']
174 for cs in fallbacks: # find unique charsets while keeping order
174 for cs in fallbacks: # find unique charsets while keeping order
175 if cs not in charsets:
175 if cs not in charsets:
176 charsets.append(cs)
176 charsets.append(cs)
177 return [cs for cs in charsets if not cs.endswith('ascii')]
177 return [cs for cs in charsets if not cs.endswith('ascii')]
178
178
179 def _encode(ui, s, charsets):
179 def _encode(ui, s, charsets):
180 '''Returns (converted) string, charset tuple.
180 '''Returns (converted) string, charset tuple.
181 Finds out best charset by cycling through sendcharsets in descending
181 Finds out best charset by cycling through sendcharsets in descending
182 order. Tries both encoding and fallbackencoding for input. Only as
182 order. Tries both encoding and fallbackencoding for input. Only as
183 last resort send as is in fake ascii.
183 last resort send as is in fake ascii.
184 Caveat: Do not use for mail parts containing patches!'''
184 Caveat: Do not use for mail parts containing patches!'''
185 try:
185 try:
186 s.decode('ascii')
186 s.decode('ascii')
187 except UnicodeDecodeError:
187 except UnicodeDecodeError:
188 sendcharsets = charsets or _charsets(ui)
188 sendcharsets = charsets or _charsets(ui)
189 for ics in (encoding.encoding, encoding.fallbackencoding):
189 for ics in (encoding.encoding, encoding.fallbackencoding):
190 try:
190 try:
191 u = s.decode(ics)
191 u = s.decode(ics)
192 except UnicodeDecodeError:
192 except UnicodeDecodeError:
193 continue
193 continue
194 for ocs in sendcharsets:
194 for ocs in sendcharsets:
195 try:
195 try:
196 return u.encode(ocs), ocs
196 return u.encode(ocs), ocs
197 except UnicodeEncodeError:
197 except UnicodeEncodeError:
198 pass
198 pass
199 except LookupError:
199 except LookupError:
200 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
200 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
201 # if ascii, or all conversion attempts fail, send (broken) ascii
201 # if ascii, or all conversion attempts fail, send (broken) ascii
202 return s, 'us-ascii'
202 return s, 'us-ascii'
203
203
204 def headencode(ui, s, charsets=None, display=False):
204 def headencode(ui, s, charsets=None, display=False):
205 '''Returns RFC-2047 compliant header from given string.'''
205 '''Returns RFC-2047 compliant header from given string.'''
206 if not display:
206 if not display:
207 # split into words?
207 # split into words?
208 s, cs = _encode(ui, s, charsets)
208 s, cs = _encode(ui, s, charsets)
209 return str(email.Header.Header(s, cs))
209 return str(email.Header.Header(s, cs))
210 return s
210 return s
211
211
212 def _addressencode(ui, name, addr, charsets=None):
212 def _addressencode(ui, name, addr, charsets=None):
213 name = headencode(ui, name, charsets)
213 name = headencode(ui, name, charsets)
214 try:
214 try:
215 acc, dom = addr.split('@')
215 acc, dom = addr.split('@')
216 acc = acc.encode('ascii')
216 acc = acc.encode('ascii')
217 dom = dom.decode(encoding.encoding).encode('idna')
217 dom = dom.decode(encoding.encoding).encode('idna')
218 addr = '%s@%s' % (acc, dom)
218 addr = '%s@%s' % (acc, dom)
219 except UnicodeDecodeError:
219 except UnicodeDecodeError:
220 raise util.Abort(_('invalid email address: %s') % addr)
220 raise util.Abort(_('invalid email address: %s') % addr)
221 except ValueError:
221 except ValueError:
222 try:
222 try:
223 # too strict?
223 # too strict?
224 addr = addr.encode('ascii')
224 addr = addr.encode('ascii')
225 except UnicodeDecodeError:
225 except UnicodeDecodeError:
226 raise util.Abort(_('invalid local address: %s') % addr)
226 raise util.Abort(_('invalid local address: %s') % addr)
227 return email.Utils.formataddr((name, addr))
227 return email.Utils.formataddr((name, addr))
228
228
229 def addressencode(ui, address, charsets=None, display=False):
229 def addressencode(ui, address, charsets=None, display=False):
230 '''Turns address into RFC-2047 compliant header.'''
230 '''Turns address into RFC-2047 compliant header.'''
231 if display or not address:
231 if display or not address:
232 return address or ''
232 return address or ''
233 name, addr = email.Utils.parseaddr(address)
233 name, addr = email.Utils.parseaddr(address)
234 return _addressencode(ui, name, addr, charsets)
234 return _addressencode(ui, name, addr, charsets)
235
235
236 def addrlistencode(ui, addrs, charsets=None, display=False):
236 def addrlistencode(ui, addrs, charsets=None, display=False):
237 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
237 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
238 A single element of input list may contain multiple addresses, but output
238 A single element of input list may contain multiple addresses, but output
239 always has one address per item'''
239 always has one address per item'''
240 if display:
240 if display:
241 return [a.strip() for a in addrs if a.strip()]
241 return [a.strip() for a in addrs if a.strip()]
242
242
243 result = []
243 result = []
244 for name, addr in email.Utils.getaddresses(addrs):
244 for name, addr in email.Utils.getaddresses(addrs):
245 if name or addr:
245 if name or addr:
246 result.append(_addressencode(ui, name, addr, charsets))
246 result.append(_addressencode(ui, name, addr, charsets))
247 return result
247 return result
248
248
249 def mimeencode(ui, s, charsets=None, display=False):
249 def mimeencode(ui, s, charsets=None, display=False):
250 '''creates mime text object, encodes it if needed, and sets
250 '''creates mime text object, encodes it if needed, and sets
251 charset and transfer-encoding accordingly.'''
251 charset and transfer-encoding accordingly.'''
252 cs = 'us-ascii'
252 cs = 'us-ascii'
253 if not display:
253 if not display:
254 s, cs = _encode(ui, s, charsets)
254 s, cs = _encode(ui, s, charsets)
255 return mimetextqp(s, 'plain', cs)
255 return mimetextqp(s, 'plain', cs)
@@ -1,204 +1,204
1 # manifest.py - manifest revision class for mercurial
1 # manifest.py - manifest revision class for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from i18n import _
8 from i18n import _
9 import mdiff, parsers, error, revlog, util
9 import mdiff, parsers, error, revlog, util
10 import array, struct
10 import array, struct
11
11
12 class manifestdict(dict):
12 class manifestdict(dict):
13 def __init__(self, mapping=None, flags=None):
13 def __init__(self, mapping=None, flags=None):
14 if mapping is None:
14 if mapping is None:
15 mapping = {}
15 mapping = {}
16 if flags is None:
16 if flags is None:
17 flags = {}
17 flags = {}
18 dict.__init__(self, mapping)
18 dict.__init__(self, mapping)
19 self._flags = flags
19 self._flags = flags
20 def flags(self, f):
20 def flags(self, f):
21 return self._flags.get(f, "")
21 return self._flags.get(f, "")
22 def withflags(self):
22 def withflags(self):
23 return set(self._flags.keys())
23 return set(self._flags.keys())
24 def set(self, f, flags):
24 def set(self, f, flags):
25 self._flags[f] = flags
25 self._flags[f] = flags
26 def copy(self):
26 def copy(self):
27 return manifestdict(self, dict.copy(self._flags))
27 return manifestdict(self, dict.copy(self._flags))
28
28
29 class manifest(revlog.revlog):
29 class manifest(revlog.revlog):
30 def __init__(self, opener):
30 def __init__(self, opener):
31 self._mancache = None
31 self._mancache = None
32 revlog.revlog.__init__(self, opener, "00manifest.i")
32 revlog.revlog.__init__(self, opener, "00manifest.i")
33
33
34 def parse(self, lines):
34 def parse(self, lines):
35 mfdict = manifestdict()
35 mfdict = manifestdict()
36 parsers.parse_manifest(mfdict, mfdict._flags, lines)
36 parsers.parse_manifest(mfdict, mfdict._flags, lines)
37 return mfdict
37 return mfdict
38
38
39 def readdelta(self, node):
39 def readdelta(self, node):
40 r = self.rev(node)
40 r = self.rev(node)
41 return self.parse(mdiff.patchtext(self.revdiff(self.deltaparent(r), r)))
41 return self.parse(mdiff.patchtext(self.revdiff(self.deltaparent(r), r)))
42
42
43 def readfast(self, node):
43 def readfast(self, node):
44 '''use the faster of readdelta or read'''
44 '''use the faster of readdelta or read'''
45 r = self.rev(node)
45 r = self.rev(node)
46 deltaparent = self.deltaparent(r)
46 deltaparent = self.deltaparent(r)
47 if deltaparent != revlog.nullrev and deltaparent in self.parentrevs(r):
47 if deltaparent != revlog.nullrev and deltaparent in self.parentrevs(r):
48 return self.readdelta(node)
48 return self.readdelta(node)
49 return self.read(node)
49 return self.read(node)
50
50
51 def read(self, node):
51 def read(self, node):
52 if node == revlog.nullid:
52 if node == revlog.nullid:
53 return manifestdict() # don't upset local cache
53 return manifestdict() # don't upset local cache
54 if self._mancache and self._mancache[0] == node:
54 if self._mancache and self._mancache[0] == node:
55 return self._mancache[1]
55 return self._mancache[1]
56 text = self.revision(node)
56 text = self.revision(node)
57 arraytext = array.array('c', text)
57 arraytext = array.array('c', text)
58 mapping = self.parse(text)
58 mapping = self.parse(text)
59 self._mancache = (node, mapping, arraytext)
59 self._mancache = (node, mapping, arraytext)
60 return mapping
60 return mapping
61
61
62 def _search(self, m, s, lo=0, hi=None):
62 def _search(self, m, s, lo=0, hi=None):
63 '''return a tuple (start, end) that says where to find s within m.
63 '''return a tuple (start, end) that says where to find s within m.
64
64
65 If the string is found m[start:end] are the line containing
65 If the string is found m[start:end] are the line containing
66 that string. If start == end the string was not found and
66 that string. If start == end the string was not found and
67 they indicate the proper sorted insertion point.
67 they indicate the proper sorted insertion point.
68
68
69 m should be a buffer or a string
69 m should be a buffer or a string
70 s is a string'''
70 s is a string'''
71 def advance(i, c):
71 def advance(i, c):
72 while i < lenm and m[i] != c:
72 while i < lenm and m[i] != c:
73 i += 1
73 i += 1
74 return i
74 return i
75 if not s:
75 if not s:
76 return (lo, lo)
76 return (lo, lo)
77 lenm = len(m)
77 lenm = len(m)
78 if not hi:
78 if not hi:
79 hi = lenm
79 hi = lenm
80 while lo < hi:
80 while lo < hi:
81 mid = (lo + hi) // 2
81 mid = (lo + hi) // 2
82 start = mid
82 start = mid
83 while start > 0 and m[start - 1] != '\n':
83 while start > 0 and m[start - 1] != '\n':
84 start -= 1
84 start -= 1
85 end = advance(start, '\0')
85 end = advance(start, '\0')
86 if m[start:end] < s:
86 if m[start:end] < s:
87 # we know that after the null there are 40 bytes of sha1
87 # we know that after the null there are 40 bytes of sha1
88 # this translates to the bisect lo = mid + 1
88 # this translates to the bisect lo = mid + 1
89 lo = advance(end + 40, '\n') + 1
89 lo = advance(end + 40, '\n') + 1
90 else:
90 else:
91 # this translates to the bisect hi = mid
91 # this translates to the bisect hi = mid
92 hi = start
92 hi = start
93 end = advance(lo, '\0')
93 end = advance(lo, '\0')
94 found = m[lo:end]
94 found = m[lo:end]
95 if s == found:
95 if s == found:
96 # we know that after the null there are 40 bytes of sha1
96 # we know that after the null there are 40 bytes of sha1
97 end = advance(end + 40, '\n')
97 end = advance(end + 40, '\n')
98 return (lo, end + 1)
98 return (lo, end + 1)
99 else:
99 else:
100 return (lo, lo)
100 return (lo, lo)
101
101
102 def find(self, node, f):
102 def find(self, node, f):
103 '''look up entry for a single file efficiently.
103 '''look up entry for a single file efficiently.
104 return (node, flags) pair if found, (None, None) if not.'''
104 return (node, flags) pair if found, (None, None) if not.'''
105 if self._mancache and self._mancache[0] == node:
105 if self._mancache and self._mancache[0] == node:
106 return self._mancache[1].get(f), self._mancache[1].flags(f)
106 return self._mancache[1].get(f), self._mancache[1].flags(f)
107 text = self.revision(node)
107 text = self.revision(node)
108 start, end = self._search(text, f)
108 start, end = self._search(text, f)
109 if start == end:
109 if start == end:
110 return None, None
110 return None, None
111 l = text[start:end]
111 l = text[start:end]
112 f, n = l.split('\0')
112 f, n = l.split('\0')
113 return revlog.bin(n[:40]), n[40:-1]
113 return revlog.bin(n[:40]), n[40:-1]
114
114
115 def add(self, map, transaction, link, p1=None, p2=None,
115 def add(self, map, transaction, link, p1=None, p2=None,
116 changed=None):
116 changed=None):
117 # apply the changes collected during the bisect loop to our addlist
117 # apply the changes collected during the bisect loop to our addlist
118 # return a delta suitable for addrevision
118 # return a delta suitable for addrevision
119 def addlistdelta(addlist, x):
119 def addlistdelta(addlist, x):
120 # start from the bottom up
120 # start from the bottom up
121 # so changes to the offsets don't mess things up.
121 # so changes to the offsets don't mess things up.
122 for start, end, content in reversed(x):
122 for start, end, content in reversed(x):
123 if content:
123 if content:
124 addlist[start:end] = array.array('c', content)
124 addlist[start:end] = array.array('c', content)
125 else:
125 else:
126 del addlist[start:end]
126 del addlist[start:end]
127 return "".join(struct.pack(">lll", start, end, len(content))
127 return "".join(struct.pack(">lll", start, end, len(content))
128 + content for start, end, content in x)
128 + content for start, end, content in x)
129
129
130 def checkforbidden(l):
130 def checkforbidden(l):
131 for f in l:
131 for f in l:
132 if '\n' in f or '\r' in f:
132 if '\n' in f or '\r' in f:
133 raise error.RevlogError(
133 raise error.RevlogError(
134 _("'\\n' and '\\r' disallowed in filenames: %r") % f)
134 _("'\\n' and '\\r' disallowed in filenames: %r") % f)
135
135
136 # if we're using the cache, make sure it is valid and
136 # if we're using the cache, make sure it is valid and
137 # parented by the same node we're diffing against
137 # parented by the same node we're diffing against
138 if not (changed and self._mancache and p1 and self._mancache[0] == p1):
138 if not (changed and self._mancache and p1 and self._mancache[0] == p1):
139 files = sorted(map)
139 files = sorted(map)
140 checkforbidden(files)
140 checkforbidden(files)
141
141
142 # if this is changed to support newlines in filenames,
142 # if this is changed to support newlines in filenames,
143 # be sure to check the templates/ dir again (especially *-raw.tmpl)
143 # be sure to check the templates/ dir again (especially *-raw.tmpl)
144 hex, flags = revlog.hex, map.flags
144 hex, flags = revlog.hex, map.flags
145 text = ''.join("%s\0%s%s\n" % (f, hex(map[f]), flags(f))
145 text = ''.join("%s\0%s%s\n" % (f, hex(map[f]), flags(f))
146 for f in files)
146 for f in files)
147 arraytext = array.array('c', text)
147 arraytext = array.array('c', text)
148 cachedelta = None
148 cachedelta = None
149 else:
149 else:
150 added, removed = changed
150 added, removed = changed
151 addlist = self._mancache[2]
151 addlist = self._mancache[2]
152
152
153 checkforbidden(added)
153 checkforbidden(added)
154 # combine the changed lists into one list for sorting
154 # combine the changed lists into one list for sorting
155 work = [(x, False) for x in added]
155 work = [(x, False) for x in added]
156 work.extend((x, True) for x in removed)
156 work.extend((x, True) for x in removed)
157 # this could use heapq.merge() (from python2.6+) or equivalent
157 # this could use heapq.merge() (from Python 2.6+) or equivalent
158 # since the lists are already sorted
158 # since the lists are already sorted
159 work.sort()
159 work.sort()
160
160
161 delta = []
161 delta = []
162 dstart = None
162 dstart = None
163 dend = None
163 dend = None
164 dline = [""]
164 dline = [""]
165 start = 0
165 start = 0
166 # zero copy representation of addlist as a buffer
166 # zero copy representation of addlist as a buffer
167 addbuf = util.buffer(addlist)
167 addbuf = util.buffer(addlist)
168
168
169 # start with a readonly loop that finds the offset of
169 # start with a readonly loop that finds the offset of
170 # each line and creates the deltas
170 # each line and creates the deltas
171 for f, todelete in work:
171 for f, todelete in work:
172 # bs will either be the index of the item or the insert point
172 # bs will either be the index of the item or the insert point
173 start, end = self._search(addbuf, f, start)
173 start, end = self._search(addbuf, f, start)
174 if not todelete:
174 if not todelete:
175 l = "%s\0%s%s\n" % (f, revlog.hex(map[f]), map.flags(f))
175 l = "%s\0%s%s\n" % (f, revlog.hex(map[f]), map.flags(f))
176 else:
176 else:
177 if start == end:
177 if start == end:
178 # item we want to delete was not found, error out
178 # item we want to delete was not found, error out
179 raise AssertionError(
179 raise AssertionError(
180 _("failed to remove %s from manifest") % f)
180 _("failed to remove %s from manifest") % f)
181 l = ""
181 l = ""
182 if dstart is not None and dstart <= start and dend >= start:
182 if dstart is not None and dstart <= start and dend >= start:
183 if dend < end:
183 if dend < end:
184 dend = end
184 dend = end
185 if l:
185 if l:
186 dline.append(l)
186 dline.append(l)
187 else:
187 else:
188 if dstart is not None:
188 if dstart is not None:
189 delta.append([dstart, dend, "".join(dline)])
189 delta.append([dstart, dend, "".join(dline)])
190 dstart = start
190 dstart = start
191 dend = end
191 dend = end
192 dline = [l]
192 dline = [l]
193
193
194 if dstart is not None:
194 if dstart is not None:
195 delta.append([dstart, dend, "".join(dline)])
195 delta.append([dstart, dend, "".join(dline)])
196 # apply the delta to the addlist, and get a delta for addrevision
196 # apply the delta to the addlist, and get a delta for addrevision
197 cachedelta = (self.rev(p1), addlistdelta(addlist, delta))
197 cachedelta = (self.rev(p1), addlistdelta(addlist, delta))
198 arraytext = addlist
198 arraytext = addlist
199 text = util.buffer(arraytext)
199 text = util.buffer(arraytext)
200
200
201 n = self.addrevision(text, transaction, link, p1, p2, cachedelta)
201 n = self.addrevision(text, transaction, link, p1, p2, cachedelta)
202 self._mancache = (n, map, arraytext)
202 self._mancache = (n, map, arraytext)
203
203
204 return n
204 return n
@@ -1,474 +1,475
1 # url.py - HTTP handling for mercurial
1 # url.py - HTTP handling for mercurial
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 import urllib, urllib2, httplib, os, socket, cStringIO
10 import urllib, urllib2, httplib, os, socket, cStringIO
11 from i18n import _
11 from i18n import _
12 import keepalive, util, sslutil
12 import keepalive, util, sslutil
13 import httpconnection as httpconnectionmod
13 import httpconnection as httpconnectionmod
14
14
15 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
15 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
16 def __init__(self, ui):
16 def __init__(self, ui):
17 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
17 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
18 self.ui = ui
18 self.ui = ui
19
19
20 def find_user_password(self, realm, authuri):
20 def find_user_password(self, realm, authuri):
21 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
21 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
22 self, realm, authuri)
22 self, realm, authuri)
23 user, passwd = authinfo
23 user, passwd = authinfo
24 if user and passwd:
24 if user and passwd:
25 self._writedebug(user, passwd)
25 self._writedebug(user, passwd)
26 return (user, passwd)
26 return (user, passwd)
27
27
28 if not user or not passwd:
28 if not user or not passwd:
29 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
29 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
30 if res:
30 if res:
31 group, auth = res
31 group, auth = res
32 user, passwd = auth.get('username'), auth.get('password')
32 user, passwd = auth.get('username'), auth.get('password')
33 self.ui.debug("using auth.%s.* for authentication\n" % group)
33 self.ui.debug("using auth.%s.* for authentication\n" % group)
34 if not user or not passwd:
34 if not user or not passwd:
35 if not self.ui.interactive():
35 if not self.ui.interactive():
36 raise util.Abort(_('http authorization required'))
36 raise util.Abort(_('http authorization required'))
37
37
38 self.ui.write(_("http authorization required\n"))
38 self.ui.write(_("http authorization required\n"))
39 self.ui.write(_("realm: %s\n") % realm)
39 self.ui.write(_("realm: %s\n") % realm)
40 if user:
40 if user:
41 self.ui.write(_("user: %s\n") % user)
41 self.ui.write(_("user: %s\n") % user)
42 else:
42 else:
43 user = self.ui.prompt(_("user:"), default=None)
43 user = self.ui.prompt(_("user:"), default=None)
44
44
45 if not passwd:
45 if not passwd:
46 passwd = self.ui.getpass()
46 passwd = self.ui.getpass()
47
47
48 self.add_password(realm, authuri, user, passwd)
48 self.add_password(realm, authuri, user, passwd)
49 self._writedebug(user, passwd)
49 self._writedebug(user, passwd)
50 return (user, passwd)
50 return (user, passwd)
51
51
52 def _writedebug(self, user, passwd):
52 def _writedebug(self, user, passwd):
53 msg = _('http auth: user %s, password %s\n')
53 msg = _('http auth: user %s, password %s\n')
54 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
54 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
55
55
56 def find_stored_password(self, authuri):
56 def find_stored_password(self, authuri):
57 return urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
57 return urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
58 self, None, authuri)
58 self, None, authuri)
59
59
60 class proxyhandler(urllib2.ProxyHandler):
60 class proxyhandler(urllib2.ProxyHandler):
61 def __init__(self, ui):
61 def __init__(self, ui):
62 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
62 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
63 # XXX proxyauthinfo = None
63 # XXX proxyauthinfo = None
64
64
65 if proxyurl:
65 if proxyurl:
66 # proxy can be proper url or host[:port]
66 # proxy can be proper url or host[:port]
67 if not (proxyurl.startswith('http:') or
67 if not (proxyurl.startswith('http:') or
68 proxyurl.startswith('https:')):
68 proxyurl.startswith('https:')):
69 proxyurl = 'http://' + proxyurl + '/'
69 proxyurl = 'http://' + proxyurl + '/'
70 proxy = util.url(proxyurl)
70 proxy = util.url(proxyurl)
71 if not proxy.user:
71 if not proxy.user:
72 proxy.user = ui.config("http_proxy", "user")
72 proxy.user = ui.config("http_proxy", "user")
73 proxy.passwd = ui.config("http_proxy", "passwd")
73 proxy.passwd = ui.config("http_proxy", "passwd")
74
74
75 # see if we should use a proxy for this url
75 # see if we should use a proxy for this url
76 no_list = ["localhost", "127.0.0.1"]
76 no_list = ["localhost", "127.0.0.1"]
77 no_list.extend([p.lower() for
77 no_list.extend([p.lower() for
78 p in ui.configlist("http_proxy", "no")])
78 p in ui.configlist("http_proxy", "no")])
79 no_list.extend([p.strip().lower() for
79 no_list.extend([p.strip().lower() for
80 p in os.getenv("no_proxy", '').split(',')
80 p in os.getenv("no_proxy", '').split(',')
81 if p.strip()])
81 if p.strip()])
82 # "http_proxy.always" config is for running tests on localhost
82 # "http_proxy.always" config is for running tests on localhost
83 if ui.configbool("http_proxy", "always"):
83 if ui.configbool("http_proxy", "always"):
84 self.no_list = []
84 self.no_list = []
85 else:
85 else:
86 self.no_list = no_list
86 self.no_list = no_list
87
87
88 proxyurl = str(proxy)
88 proxyurl = str(proxy)
89 proxies = {'http': proxyurl, 'https': proxyurl}
89 proxies = {'http': proxyurl, 'https': proxyurl}
90 ui.debug('proxying through http://%s:%s\n' %
90 ui.debug('proxying through http://%s:%s\n' %
91 (proxy.host, proxy.port))
91 (proxy.host, proxy.port))
92 else:
92 else:
93 proxies = {}
93 proxies = {}
94
94
95 # urllib2 takes proxy values from the environment and those
95 # urllib2 takes proxy values from the environment and those
96 # will take precedence if found. So, if there's a config entry
96 # will take precedence if found. So, if there's a config entry
97 # defining a proxy, drop the environment ones
97 # defining a proxy, drop the environment ones
98 if ui.config("http_proxy", "host"):
98 if ui.config("http_proxy", "host"):
99 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
99 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
100 try:
100 try:
101 if env in os.environ:
101 if env in os.environ:
102 del os.environ[env]
102 del os.environ[env]
103 except OSError:
103 except OSError:
104 pass
104 pass
105
105
106 urllib2.ProxyHandler.__init__(self, proxies)
106 urllib2.ProxyHandler.__init__(self, proxies)
107 self.ui = ui
107 self.ui = ui
108
108
109 def proxy_open(self, req, proxy, type_):
109 def proxy_open(self, req, proxy, type_):
110 host = req.get_host().split(':')[0]
110 host = req.get_host().split(':')[0]
111 if host in self.no_list:
111 if host in self.no_list:
112 return None
112 return None
113
113
114 # work around a bug in Python < 2.4.2
114 # work around a bug in Python < 2.4.2
115 # (it leaves a "\n" at the end of Proxy-authorization headers)
115 # (it leaves a "\n" at the end of Proxy-authorization headers)
116 baseclass = req.__class__
116 baseclass = req.__class__
117 class _request(baseclass):
117 class _request(baseclass):
118 def add_header(self, key, val):
118 def add_header(self, key, val):
119 if key.lower() == 'proxy-authorization':
119 if key.lower() == 'proxy-authorization':
120 val = val.strip()
120 val = val.strip()
121 return baseclass.add_header(self, key, val)
121 return baseclass.add_header(self, key, val)
122 req.__class__ = _request
122 req.__class__ = _request
123
123
124 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type_)
124 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type_)
125
125
126 def _gen_sendfile(orgsend):
126 def _gen_sendfile(orgsend):
127 def _sendfile(self, data):
127 def _sendfile(self, data):
128 # send a file
128 # send a file
129 if isinstance(data, httpconnectionmod.httpsendfile):
129 if isinstance(data, httpconnectionmod.httpsendfile):
130 # if auth required, some data sent twice, so rewind here
130 # if auth required, some data sent twice, so rewind here
131 data.seek(0)
131 data.seek(0)
132 for chunk in util.filechunkiter(data):
132 for chunk in util.filechunkiter(data):
133 orgsend(self, chunk)
133 orgsend(self, chunk)
134 else:
134 else:
135 orgsend(self, data)
135 orgsend(self, data)
136 return _sendfile
136 return _sendfile
137
137
138 has_https = util.safehasattr(urllib2, 'HTTPSHandler')
138 has_https = util.safehasattr(urllib2, 'HTTPSHandler')
139 if has_https:
139 if has_https:
140 try:
140 try:
141 _create_connection = socket.create_connection
141 _create_connection = socket.create_connection
142 except AttributeError:
142 except AttributeError:
143 _GLOBAL_DEFAULT_TIMEOUT = object()
143 _GLOBAL_DEFAULT_TIMEOUT = object()
144
144
145 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
145 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
146 source_address=None):
146 source_address=None):
147 # lifted from Python 2.6
147 # lifted from Python 2.6
148
148
149 msg = "getaddrinfo returns an empty list"
149 msg = "getaddrinfo returns an empty list"
150 host, port = address
150 host, port = address
151 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
151 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
152 af, socktype, proto, canonname, sa = res
152 af, socktype, proto, canonname, sa = res
153 sock = None
153 sock = None
154 try:
154 try:
155 sock = socket.socket(af, socktype, proto)
155 sock = socket.socket(af, socktype, proto)
156 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
156 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
157 sock.settimeout(timeout)
157 sock.settimeout(timeout)
158 if source_address:
158 if source_address:
159 sock.bind(source_address)
159 sock.bind(source_address)
160 sock.connect(sa)
160 sock.connect(sa)
161 return sock
161 return sock
162
162
163 except socket.error, msg:
163 except socket.error, msg:
164 if sock is not None:
164 if sock is not None:
165 sock.close()
165 sock.close()
166
166
167 raise socket.error, msg
167 raise socket.error, msg
168
168
169 class httpconnection(keepalive.HTTPConnection):
169 class httpconnection(keepalive.HTTPConnection):
170 # must be able to send big bundle as stream.
170 # must be able to send big bundle as stream.
171 send = _gen_sendfile(keepalive.HTTPConnection.send)
171 send = _gen_sendfile(keepalive.HTTPConnection.send)
172
172
173 def connect(self):
173 def connect(self):
174 if has_https and self.realhostport: # use CONNECT proxy
174 if has_https and self.realhostport: # use CONNECT proxy
175 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
175 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
176 self.sock.connect((self.host, self.port))
176 self.sock.connect((self.host, self.port))
177 if _generic_proxytunnel(self):
177 if _generic_proxytunnel(self):
178 # we do not support client X.509 certificates
178 # we do not support client X.509 certificates
179 self.sock = sslutil.ssl_wrap_socket(self.sock, None, None)
179 self.sock = sslutil.ssl_wrap_socket(self.sock, None, None)
180 else:
180 else:
181 keepalive.HTTPConnection.connect(self)
181 keepalive.HTTPConnection.connect(self)
182
182
183 def getresponse(self):
183 def getresponse(self):
184 proxyres = getattr(self, 'proxyres', None)
184 proxyres = getattr(self, 'proxyres', None)
185 if proxyres:
185 if proxyres:
186 if proxyres.will_close:
186 if proxyres.will_close:
187 self.close()
187 self.close()
188 self.proxyres = None
188 self.proxyres = None
189 return proxyres
189 return proxyres
190 return keepalive.HTTPConnection.getresponse(self)
190 return keepalive.HTTPConnection.getresponse(self)
191
191
192 # general transaction handler to support different ways to handle
192 # general transaction handler to support different ways to handle
193 # HTTPS proxying before and after Python 2.6.3.
193 # HTTPS proxying before and after Python 2.6.3.
194 def _generic_start_transaction(handler, h, req):
194 def _generic_start_transaction(handler, h, req):
195 tunnel_host = getattr(req, '_tunnel_host', None)
195 tunnel_host = getattr(req, '_tunnel_host', None)
196 if tunnel_host:
196 if tunnel_host:
197 if tunnel_host[:7] not in ['http://', 'https:/']:
197 if tunnel_host[:7] not in ['http://', 'https:/']:
198 tunnel_host = 'https://' + tunnel_host
198 tunnel_host = 'https://' + tunnel_host
199 new_tunnel = True
199 new_tunnel = True
200 else:
200 else:
201 tunnel_host = req.get_selector()
201 tunnel_host = req.get_selector()
202 new_tunnel = False
202 new_tunnel = False
203
203
204 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
204 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
205 u = util.url(tunnel_host)
205 u = util.url(tunnel_host)
206 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
206 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
207 h.realhostport = ':'.join([u.host, (u.port or '443')])
207 h.realhostport = ':'.join([u.host, (u.port or '443')])
208 h.headers = req.headers.copy()
208 h.headers = req.headers.copy()
209 h.headers.update(handler.parent.addheaders)
209 h.headers.update(handler.parent.addheaders)
210 return
210 return
211
211
212 h.realhostport = None
212 h.realhostport = None
213 h.headers = None
213 h.headers = None
214
214
215 def _generic_proxytunnel(self):
215 def _generic_proxytunnel(self):
216 proxyheaders = dict(
216 proxyheaders = dict(
217 [(x, self.headers[x]) for x in self.headers
217 [(x, self.headers[x]) for x in self.headers
218 if x.lower().startswith('proxy-')])
218 if x.lower().startswith('proxy-')])
219 self._set_hostport(self.host, self.port)
219 self._set_hostport(self.host, self.port)
220 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
220 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
221 for header in proxyheaders.iteritems():
221 for header in proxyheaders.iteritems():
222 self.send('%s: %s\r\n' % header)
222 self.send('%s: %s\r\n' % header)
223 self.send('\r\n')
223 self.send('\r\n')
224
224
225 # majority of the following code is duplicated from
225 # majority of the following code is duplicated from
226 # httplib.HTTPConnection as there are no adequate places to
226 # httplib.HTTPConnection as there are no adequate places to
227 # override functions to provide the needed functionality
227 # override functions to provide the needed functionality
228 res = self.response_class(self.sock,
228 res = self.response_class(self.sock,
229 strict=self.strict,
229 strict=self.strict,
230 method=self._method)
230 method=self._method)
231
231
232 while True:
232 while True:
233 version, status, reason = res._read_status()
233 version, status, reason = res._read_status()
234 if status != httplib.CONTINUE:
234 if status != httplib.CONTINUE:
235 break
235 break
236 while True:
236 while True:
237 skip = res.fp.readline().strip()
237 skip = res.fp.readline().strip()
238 if not skip:
238 if not skip:
239 break
239 break
240 res.status = status
240 res.status = status
241 res.reason = reason.strip()
241 res.reason = reason.strip()
242
242
243 if res.status == 200:
243 if res.status == 200:
244 while True:
244 while True:
245 line = res.fp.readline()
245 line = res.fp.readline()
246 if line == '\r\n':
246 if line == '\r\n':
247 break
247 break
248 return True
248 return True
249
249
250 if version == 'HTTP/1.0':
250 if version == 'HTTP/1.0':
251 res.version = 10
251 res.version = 10
252 elif version.startswith('HTTP/1.'):
252 elif version.startswith('HTTP/1.'):
253 res.version = 11
253 res.version = 11
254 elif version == 'HTTP/0.9':
254 elif version == 'HTTP/0.9':
255 res.version = 9
255 res.version = 9
256 else:
256 else:
257 raise httplib.UnknownProtocol(version)
257 raise httplib.UnknownProtocol(version)
258
258
259 if res.version == 9:
259 if res.version == 9:
260 res.length = None
260 res.length = None
261 res.chunked = 0
261 res.chunked = 0
262 res.will_close = 1
262 res.will_close = 1
263 res.msg = httplib.HTTPMessage(cStringIO.StringIO())
263 res.msg = httplib.HTTPMessage(cStringIO.StringIO())
264 return False
264 return False
265
265
266 res.msg = httplib.HTTPMessage(res.fp)
266 res.msg = httplib.HTTPMessage(res.fp)
267 res.msg.fp = None
267 res.msg.fp = None
268
268
269 # are we using the chunked-style of transfer encoding?
269 # are we using the chunked-style of transfer encoding?
270 trenc = res.msg.getheader('transfer-encoding')
270 trenc = res.msg.getheader('transfer-encoding')
271 if trenc and trenc.lower() == "chunked":
271 if trenc and trenc.lower() == "chunked":
272 res.chunked = 1
272 res.chunked = 1
273 res.chunk_left = None
273 res.chunk_left = None
274 else:
274 else:
275 res.chunked = 0
275 res.chunked = 0
276
276
277 # will the connection close at the end of the response?
277 # will the connection close at the end of the response?
278 res.will_close = res._check_close()
278 res.will_close = res._check_close()
279
279
280 # do we have a Content-Length?
280 # do we have a Content-Length?
281 # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
281 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
282 # transfer-encoding is "chunked"
282 length = res.msg.getheader('content-length')
283 length = res.msg.getheader('content-length')
283 if length and not res.chunked:
284 if length and not res.chunked:
284 try:
285 try:
285 res.length = int(length)
286 res.length = int(length)
286 except ValueError:
287 except ValueError:
287 res.length = None
288 res.length = None
288 else:
289 else:
289 if res.length < 0: # ignore nonsensical negative lengths
290 if res.length < 0: # ignore nonsensical negative lengths
290 res.length = None
291 res.length = None
291 else:
292 else:
292 res.length = None
293 res.length = None
293
294
294 # does the body have a fixed length? (of zero)
295 # does the body have a fixed length? (of zero)
295 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
296 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
296 100 <= status < 200 or # 1xx codes
297 100 <= status < 200 or # 1xx codes
297 res._method == 'HEAD'):
298 res._method == 'HEAD'):
298 res.length = 0
299 res.length = 0
299
300
300 # if the connection remains open, and we aren't using chunked, and
301 # if the connection remains open, and we aren't using chunked, and
301 # a content-length was not provided, then assume that the connection
302 # a content-length was not provided, then assume that the connection
302 # WILL close.
303 # WILL close.
303 if (not res.will_close and
304 if (not res.will_close and
304 not res.chunked and
305 not res.chunked and
305 res.length is None):
306 res.length is None):
306 res.will_close = 1
307 res.will_close = 1
307
308
308 self.proxyres = res
309 self.proxyres = res
309
310
310 return False
311 return False
311
312
312 class httphandler(keepalive.HTTPHandler):
313 class httphandler(keepalive.HTTPHandler):
313 def http_open(self, req):
314 def http_open(self, req):
314 return self.do_open(httpconnection, req)
315 return self.do_open(httpconnection, req)
315
316
316 def _start_transaction(self, h, req):
317 def _start_transaction(self, h, req):
317 _generic_start_transaction(self, h, req)
318 _generic_start_transaction(self, h, req)
318 return keepalive.HTTPHandler._start_transaction(self, h, req)
319 return keepalive.HTTPHandler._start_transaction(self, h, req)
319
320
320 if has_https:
321 if has_https:
321 class httpsconnection(httplib.HTTPSConnection):
322 class httpsconnection(httplib.HTTPSConnection):
322 response_class = keepalive.HTTPResponse
323 response_class = keepalive.HTTPResponse
323 # must be able to send big bundle as stream.
324 # must be able to send big bundle as stream.
324 send = _gen_sendfile(keepalive.safesend)
325 send = _gen_sendfile(keepalive.safesend)
325 getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection)
326 getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection)
326
327
327 def connect(self):
328 def connect(self):
328 self.sock = _create_connection((self.host, self.port))
329 self.sock = _create_connection((self.host, self.port))
329
330
330 host = self.host
331 host = self.host
331 if self.realhostport: # use CONNECT proxy
332 if self.realhostport: # use CONNECT proxy
332 _generic_proxytunnel(self)
333 _generic_proxytunnel(self)
333 host = self.realhostport.rsplit(':', 1)[0]
334 host = self.realhostport.rsplit(':', 1)[0]
334 self.sock = sslutil.ssl_wrap_socket(
335 self.sock = sslutil.ssl_wrap_socket(
335 self.sock, self.key_file, self.cert_file,
336 self.sock, self.key_file, self.cert_file,
336 **sslutil.sslkwargs(self.ui, host))
337 **sslutil.sslkwargs(self.ui, host))
337 sslutil.validator(self.ui, host)(self.sock)
338 sslutil.validator(self.ui, host)(self.sock)
338
339
339 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler):
340 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler):
340 def __init__(self, ui):
341 def __init__(self, ui):
341 keepalive.KeepAliveHandler.__init__(self)
342 keepalive.KeepAliveHandler.__init__(self)
342 urllib2.HTTPSHandler.__init__(self)
343 urllib2.HTTPSHandler.__init__(self)
343 self.ui = ui
344 self.ui = ui
344 self.pwmgr = passwordmgr(self.ui)
345 self.pwmgr = passwordmgr(self.ui)
345
346
346 def _start_transaction(self, h, req):
347 def _start_transaction(self, h, req):
347 _generic_start_transaction(self, h, req)
348 _generic_start_transaction(self, h, req)
348 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
349 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
349
350
350 def https_open(self, req):
351 def https_open(self, req):
351 # req.get_full_url() does not contain credentials and we may
352 # req.get_full_url() does not contain credentials and we may
352 # need them to match the certificates.
353 # need them to match the certificates.
353 url = req.get_full_url()
354 url = req.get_full_url()
354 user, password = self.pwmgr.find_stored_password(url)
355 user, password = self.pwmgr.find_stored_password(url)
355 res = httpconnectionmod.readauthforuri(self.ui, url, user)
356 res = httpconnectionmod.readauthforuri(self.ui, url, user)
356 if res:
357 if res:
357 group, auth = res
358 group, auth = res
358 self.auth = auth
359 self.auth = auth
359 self.ui.debug("using auth.%s.* for authentication\n" % group)
360 self.ui.debug("using auth.%s.* for authentication\n" % group)
360 else:
361 else:
361 self.auth = None
362 self.auth = None
362 return self.do_open(self._makeconnection, req)
363 return self.do_open(self._makeconnection, req)
363
364
364 def _makeconnection(self, host, port=None, *args, **kwargs):
365 def _makeconnection(self, host, port=None, *args, **kwargs):
365 keyfile = None
366 keyfile = None
366 certfile = None
367 certfile = None
367
368
368 if len(args) >= 1: # key_file
369 if len(args) >= 1: # key_file
369 keyfile = args[0]
370 keyfile = args[0]
370 if len(args) >= 2: # cert_file
371 if len(args) >= 2: # cert_file
371 certfile = args[1]
372 certfile = args[1]
372 args = args[2:]
373 args = args[2:]
373
374
374 # if the user has specified different key/cert files in
375 # if the user has specified different key/cert files in
375 # hgrc, we prefer these
376 # hgrc, we prefer these
376 if self.auth and 'key' in self.auth and 'cert' in self.auth:
377 if self.auth and 'key' in self.auth and 'cert' in self.auth:
377 keyfile = self.auth['key']
378 keyfile = self.auth['key']
378 certfile = self.auth['cert']
379 certfile = self.auth['cert']
379
380
380 conn = httpsconnection(host, port, keyfile, certfile, *args,
381 conn = httpsconnection(host, port, keyfile, certfile, *args,
381 **kwargs)
382 **kwargs)
382 conn.ui = self.ui
383 conn.ui = self.ui
383 return conn
384 return conn
384
385
385 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
386 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
386 def __init__(self, *args, **kwargs):
387 def __init__(self, *args, **kwargs):
387 urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs)
388 urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs)
388 self.retried_req = None
389 self.retried_req = None
389
390
390 def reset_retry_count(self):
391 def reset_retry_count(self):
391 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
392 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
392 # forever. We disable reset_retry_count completely and reset in
393 # forever. We disable reset_retry_count completely and reset in
393 # http_error_auth_reqed instead.
394 # http_error_auth_reqed instead.
394 pass
395 pass
395
396
396 def http_error_auth_reqed(self, auth_header, host, req, headers):
397 def http_error_auth_reqed(self, auth_header, host, req, headers):
397 # Reset the retry counter once for each request.
398 # Reset the retry counter once for each request.
398 if req is not self.retried_req:
399 if req is not self.retried_req:
399 self.retried_req = req
400 self.retried_req = req
400 self.retried = 0
401 self.retried = 0
401 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
402 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
402 # it doesn't know about the auth type requested. This can happen if
403 # it doesn't know about the auth type requested. This can happen if
403 # somebody is using BasicAuth and types a bad password.
404 # somebody is using BasicAuth and types a bad password.
404 try:
405 try:
405 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
406 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
406 self, auth_header, host, req, headers)
407 self, auth_header, host, req, headers)
407 except ValueError, inst:
408 except ValueError, inst:
408 arg = inst.args[0]
409 arg = inst.args[0]
409 if arg.startswith("AbstractDigestAuthHandler doesn't know "):
410 if arg.startswith("AbstractDigestAuthHandler doesn't know "):
410 return
411 return
411 raise
412 raise
412
413
413 class httpbasicauthhandler(urllib2.HTTPBasicAuthHandler):
414 class httpbasicauthhandler(urllib2.HTTPBasicAuthHandler):
414 def __init__(self, *args, **kwargs):
415 def __init__(self, *args, **kwargs):
415 urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
416 urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
416 self.retried_req = None
417 self.retried_req = None
417
418
418 def reset_retry_count(self):
419 def reset_retry_count(self):
419 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
420 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
420 # forever. We disable reset_retry_count completely and reset in
421 # forever. We disable reset_retry_count completely and reset in
421 # http_error_auth_reqed instead.
422 # http_error_auth_reqed instead.
422 pass
423 pass
423
424
424 def http_error_auth_reqed(self, auth_header, host, req, headers):
425 def http_error_auth_reqed(self, auth_header, host, req, headers):
425 # Reset the retry counter once for each request.
426 # Reset the retry counter once for each request.
426 if req is not self.retried_req:
427 if req is not self.retried_req:
427 self.retried_req = req
428 self.retried_req = req
428 self.retried = 0
429 self.retried = 0
429 return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed(
430 return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed(
430 self, auth_header, host, req, headers)
431 self, auth_header, host, req, headers)
431
432
432 handlerfuncs = []
433 handlerfuncs = []
433
434
434 def opener(ui, authinfo=None):
435 def opener(ui, authinfo=None):
435 '''
436 '''
436 construct an opener suitable for urllib2
437 construct an opener suitable for urllib2
437 authinfo will be added to the password manager
438 authinfo will be added to the password manager
438 '''
439 '''
439 if ui.configbool('ui', 'usehttp2', False):
440 if ui.configbool('ui', 'usehttp2', False):
440 handlers = [httpconnectionmod.http2handler(ui, passwordmgr(ui))]
441 handlers = [httpconnectionmod.http2handler(ui, passwordmgr(ui))]
441 else:
442 else:
442 handlers = [httphandler()]
443 handlers = [httphandler()]
443 if has_https:
444 if has_https:
444 handlers.append(httpshandler(ui))
445 handlers.append(httpshandler(ui))
445
446
446 handlers.append(proxyhandler(ui))
447 handlers.append(proxyhandler(ui))
447
448
448 passmgr = passwordmgr(ui)
449 passmgr = passwordmgr(ui)
449 if authinfo is not None:
450 if authinfo is not None:
450 passmgr.add_password(*authinfo)
451 passmgr.add_password(*authinfo)
451 user, passwd = authinfo[2:4]
452 user, passwd = authinfo[2:4]
452 ui.debug('http auth: user %s, password %s\n' %
453 ui.debug('http auth: user %s, password %s\n' %
453 (user, passwd and '*' * len(passwd) or 'not set'))
454 (user, passwd and '*' * len(passwd) or 'not set'))
454
455
455 handlers.extend((httpbasicauthhandler(passmgr),
456 handlers.extend((httpbasicauthhandler(passmgr),
456 httpdigestauthhandler(passmgr)))
457 httpdigestauthhandler(passmgr)))
457 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
458 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
458 opener = urllib2.build_opener(*handlers)
459 opener = urllib2.build_opener(*handlers)
459
460
460 # 1.0 here is the _protocol_ version
461 # 1.0 here is the _protocol_ version
461 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
462 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
462 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
463 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
463 return opener
464 return opener
464
465
465 def open(ui, url_, data=None):
466 def open(ui, url_, data=None):
466 u = util.url(url_)
467 u = util.url(url_)
467 if u.scheme:
468 if u.scheme:
468 u.scheme = u.scheme.lower()
469 u.scheme = u.scheme.lower()
469 url_, authinfo = u.authinfo()
470 url_, authinfo = u.authinfo()
470 else:
471 else:
471 path = util.normpath(os.path.abspath(url_))
472 path = util.normpath(os.path.abspath(url_))
472 url_ = 'file://' + urllib.pathname2url(path)
473 url_ = 'file://' + urllib.pathname2url(path)
473 authinfo = None
474 authinfo = None
474 return opener(ui, authinfo).open(url_, data)
475 return opener(ui, authinfo).open(url_, data)
@@ -1,393 +1,393
1 # win32.py - utility functions that use win32 API
1 # win32.py - utility functions that use win32 API
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import ctypes, errno, os, subprocess, random
8 import ctypes, errno, os, subprocess, random
9
9
10 _kernel32 = ctypes.windll.kernel32
10 _kernel32 = ctypes.windll.kernel32
11 _advapi32 = ctypes.windll.advapi32
11 _advapi32 = ctypes.windll.advapi32
12 _user32 = ctypes.windll.user32
12 _user32 = ctypes.windll.user32
13
13
14 _BOOL = ctypes.c_long
14 _BOOL = ctypes.c_long
15 _WORD = ctypes.c_ushort
15 _WORD = ctypes.c_ushort
16 _DWORD = ctypes.c_ulong
16 _DWORD = ctypes.c_ulong
17 _UINT = ctypes.c_uint
17 _UINT = ctypes.c_uint
18 _LONG = ctypes.c_long
18 _LONG = ctypes.c_long
19 _LPCSTR = _LPSTR = ctypes.c_char_p
19 _LPCSTR = _LPSTR = ctypes.c_char_p
20 _HANDLE = ctypes.c_void_p
20 _HANDLE = ctypes.c_void_p
21 _HWND = _HANDLE
21 _HWND = _HANDLE
22
22
23 _INVALID_HANDLE_VALUE = _HANDLE(-1).value
23 _INVALID_HANDLE_VALUE = _HANDLE(-1).value
24
24
25 # GetLastError
25 # GetLastError
26 _ERROR_SUCCESS = 0
26 _ERROR_SUCCESS = 0
27 _ERROR_INVALID_PARAMETER = 87
27 _ERROR_INVALID_PARAMETER = 87
28 _ERROR_INSUFFICIENT_BUFFER = 122
28 _ERROR_INSUFFICIENT_BUFFER = 122
29
29
30 # WPARAM is defined as UINT_PTR (unsigned type)
30 # WPARAM is defined as UINT_PTR (unsigned type)
31 # LPARAM is defined as LONG_PTR (signed type)
31 # LPARAM is defined as LONG_PTR (signed type)
32 if ctypes.sizeof(ctypes.c_long) == ctypes.sizeof(ctypes.c_void_p):
32 if ctypes.sizeof(ctypes.c_long) == ctypes.sizeof(ctypes.c_void_p):
33 _WPARAM = ctypes.c_ulong
33 _WPARAM = ctypes.c_ulong
34 _LPARAM = ctypes.c_long
34 _LPARAM = ctypes.c_long
35 elif ctypes.sizeof(ctypes.c_longlong) == ctypes.sizeof(ctypes.c_void_p):
35 elif ctypes.sizeof(ctypes.c_longlong) == ctypes.sizeof(ctypes.c_void_p):
36 _WPARAM = ctypes.c_ulonglong
36 _WPARAM = ctypes.c_ulonglong
37 _LPARAM = ctypes.c_longlong
37 _LPARAM = ctypes.c_longlong
38
38
39 class _FILETIME(ctypes.Structure):
39 class _FILETIME(ctypes.Structure):
40 _fields_ = [('dwLowDateTime', _DWORD),
40 _fields_ = [('dwLowDateTime', _DWORD),
41 ('dwHighDateTime', _DWORD)]
41 ('dwHighDateTime', _DWORD)]
42
42
43 class _BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
43 class _BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
44 _fields_ = [('dwFileAttributes', _DWORD),
44 _fields_ = [('dwFileAttributes', _DWORD),
45 ('ftCreationTime', _FILETIME),
45 ('ftCreationTime', _FILETIME),
46 ('ftLastAccessTime', _FILETIME),
46 ('ftLastAccessTime', _FILETIME),
47 ('ftLastWriteTime', _FILETIME),
47 ('ftLastWriteTime', _FILETIME),
48 ('dwVolumeSerialNumber', _DWORD),
48 ('dwVolumeSerialNumber', _DWORD),
49 ('nFileSizeHigh', _DWORD),
49 ('nFileSizeHigh', _DWORD),
50 ('nFileSizeLow', _DWORD),
50 ('nFileSizeLow', _DWORD),
51 ('nNumberOfLinks', _DWORD),
51 ('nNumberOfLinks', _DWORD),
52 ('nFileIndexHigh', _DWORD),
52 ('nFileIndexHigh', _DWORD),
53 ('nFileIndexLow', _DWORD)]
53 ('nFileIndexLow', _DWORD)]
54
54
55 # CreateFile
55 # CreateFile
56 _FILE_SHARE_READ = 0x00000001
56 _FILE_SHARE_READ = 0x00000001
57 _FILE_SHARE_WRITE = 0x00000002
57 _FILE_SHARE_WRITE = 0x00000002
58 _FILE_SHARE_DELETE = 0x00000004
58 _FILE_SHARE_DELETE = 0x00000004
59
59
60 _OPEN_EXISTING = 3
60 _OPEN_EXISTING = 3
61
61
62 _FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
62 _FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
63
63
64 # SetFileAttributes
64 # SetFileAttributes
65 _FILE_ATTRIBUTE_NORMAL = 0x80
65 _FILE_ATTRIBUTE_NORMAL = 0x80
66 _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x2000
66 _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x2000
67
67
68 # Process Security and Access Rights
68 # Process Security and Access Rights
69 _PROCESS_QUERY_INFORMATION = 0x0400
69 _PROCESS_QUERY_INFORMATION = 0x0400
70
70
71 # GetExitCodeProcess
71 # GetExitCodeProcess
72 _STILL_ACTIVE = 259
72 _STILL_ACTIVE = 259
73
73
74 class _STARTUPINFO(ctypes.Structure):
74 class _STARTUPINFO(ctypes.Structure):
75 _fields_ = [('cb', _DWORD),
75 _fields_ = [('cb', _DWORD),
76 ('lpReserved', _LPSTR),
76 ('lpReserved', _LPSTR),
77 ('lpDesktop', _LPSTR),
77 ('lpDesktop', _LPSTR),
78 ('lpTitle', _LPSTR),
78 ('lpTitle', _LPSTR),
79 ('dwX', _DWORD),
79 ('dwX', _DWORD),
80 ('dwY', _DWORD),
80 ('dwY', _DWORD),
81 ('dwXSize', _DWORD),
81 ('dwXSize', _DWORD),
82 ('dwYSize', _DWORD),
82 ('dwYSize', _DWORD),
83 ('dwXCountChars', _DWORD),
83 ('dwXCountChars', _DWORD),
84 ('dwYCountChars', _DWORD),
84 ('dwYCountChars', _DWORD),
85 ('dwFillAttribute', _DWORD),
85 ('dwFillAttribute', _DWORD),
86 ('dwFlags', _DWORD),
86 ('dwFlags', _DWORD),
87 ('wShowWindow', _WORD),
87 ('wShowWindow', _WORD),
88 ('cbReserved2', _WORD),
88 ('cbReserved2', _WORD),
89 ('lpReserved2', ctypes.c_char_p),
89 ('lpReserved2', ctypes.c_char_p),
90 ('hStdInput', _HANDLE),
90 ('hStdInput', _HANDLE),
91 ('hStdOutput', _HANDLE),
91 ('hStdOutput', _HANDLE),
92 ('hStdError', _HANDLE)]
92 ('hStdError', _HANDLE)]
93
93
94 class _PROCESS_INFORMATION(ctypes.Structure):
94 class _PROCESS_INFORMATION(ctypes.Structure):
95 _fields_ = [('hProcess', _HANDLE),
95 _fields_ = [('hProcess', _HANDLE),
96 ('hThread', _HANDLE),
96 ('hThread', _HANDLE),
97 ('dwProcessId', _DWORD),
97 ('dwProcessId', _DWORD),
98 ('dwThreadId', _DWORD)]
98 ('dwThreadId', _DWORD)]
99
99
100 _CREATE_NO_WINDOW = 0x08000000
100 _CREATE_NO_WINDOW = 0x08000000
101 _SW_HIDE = 0
101 _SW_HIDE = 0
102
102
103 class _COORD(ctypes.Structure):
103 class _COORD(ctypes.Structure):
104 _fields_ = [('X', ctypes.c_short),
104 _fields_ = [('X', ctypes.c_short),
105 ('Y', ctypes.c_short)]
105 ('Y', ctypes.c_short)]
106
106
107 class _SMALL_RECT(ctypes.Structure):
107 class _SMALL_RECT(ctypes.Structure):
108 _fields_ = [('Left', ctypes.c_short),
108 _fields_ = [('Left', ctypes.c_short),
109 ('Top', ctypes.c_short),
109 ('Top', ctypes.c_short),
110 ('Right', ctypes.c_short),
110 ('Right', ctypes.c_short),
111 ('Bottom', ctypes.c_short)]
111 ('Bottom', ctypes.c_short)]
112
112
113 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
113 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
114 _fields_ = [('dwSize', _COORD),
114 _fields_ = [('dwSize', _COORD),
115 ('dwCursorPosition', _COORD),
115 ('dwCursorPosition', _COORD),
116 ('wAttributes', _WORD),
116 ('wAttributes', _WORD),
117 ('srWindow', _SMALL_RECT),
117 ('srWindow', _SMALL_RECT),
118 ('dwMaximumWindowSize', _COORD)]
118 ('dwMaximumWindowSize', _COORD)]
119
119
120 _STD_ERROR_HANDLE = _DWORD(-12).value
120 _STD_ERROR_HANDLE = _DWORD(-12).value
121
121
122 # types of parameters of C functions used (required by pypy)
122 # types of parameters of C functions used (required by pypy)
123
123
124 _kernel32.CreateFileA.argtypes = [_LPCSTR, _DWORD, _DWORD, ctypes.c_void_p,
124 _kernel32.CreateFileA.argtypes = [_LPCSTR, _DWORD, _DWORD, ctypes.c_void_p,
125 _DWORD, _DWORD, _HANDLE]
125 _DWORD, _DWORD, _HANDLE]
126 _kernel32.CreateFileA.restype = _HANDLE
126 _kernel32.CreateFileA.restype = _HANDLE
127
127
128 _kernel32.GetFileInformationByHandle.argtypes = [_HANDLE, ctypes.c_void_p]
128 _kernel32.GetFileInformationByHandle.argtypes = [_HANDLE, ctypes.c_void_p]
129 _kernel32.GetFileInformationByHandle.restype = _BOOL
129 _kernel32.GetFileInformationByHandle.restype = _BOOL
130
130
131 _kernel32.CloseHandle.argtypes = [_HANDLE]
131 _kernel32.CloseHandle.argtypes = [_HANDLE]
132 _kernel32.CloseHandle.restype = _BOOL
132 _kernel32.CloseHandle.restype = _BOOL
133
133
134 try:
134 try:
135 _kernel32.CreateHardLinkA.argtypes = [_LPCSTR, _LPCSTR, ctypes.c_void_p]
135 _kernel32.CreateHardLinkA.argtypes = [_LPCSTR, _LPCSTR, ctypes.c_void_p]
136 _kernel32.CreateHardLinkA.restype = _BOOL
136 _kernel32.CreateHardLinkA.restype = _BOOL
137 except AttributeError:
137 except AttributeError:
138 pass
138 pass
139
139
140 _kernel32.SetFileAttributesA.argtypes = [_LPCSTR, _DWORD]
140 _kernel32.SetFileAttributesA.argtypes = [_LPCSTR, _DWORD]
141 _kernel32.SetFileAttributesA.restype = _BOOL
141 _kernel32.SetFileAttributesA.restype = _BOOL
142
142
143 _kernel32.OpenProcess.argtypes = [_DWORD, _BOOL, _DWORD]
143 _kernel32.OpenProcess.argtypes = [_DWORD, _BOOL, _DWORD]
144 _kernel32.OpenProcess.restype = _HANDLE
144 _kernel32.OpenProcess.restype = _HANDLE
145
145
146 _kernel32.GetExitCodeProcess.argtypes = [_HANDLE, ctypes.c_void_p]
146 _kernel32.GetExitCodeProcess.argtypes = [_HANDLE, ctypes.c_void_p]
147 _kernel32.GetExitCodeProcess.restype = _BOOL
147 _kernel32.GetExitCodeProcess.restype = _BOOL
148
148
149 _kernel32.GetLastError.argtypes = []
149 _kernel32.GetLastError.argtypes = []
150 _kernel32.GetLastError.restype = _DWORD
150 _kernel32.GetLastError.restype = _DWORD
151
151
152 _kernel32.GetModuleFileNameA.argtypes = [_HANDLE, ctypes.c_void_p, _DWORD]
152 _kernel32.GetModuleFileNameA.argtypes = [_HANDLE, ctypes.c_void_p, _DWORD]
153 _kernel32.GetModuleFileNameA.restype = _DWORD
153 _kernel32.GetModuleFileNameA.restype = _DWORD
154
154
155 _kernel32.CreateProcessA.argtypes = [_LPCSTR, _LPCSTR, ctypes.c_void_p,
155 _kernel32.CreateProcessA.argtypes = [_LPCSTR, _LPCSTR, ctypes.c_void_p,
156 ctypes.c_void_p, _BOOL, _DWORD, ctypes.c_void_p, _LPCSTR, ctypes.c_void_p,
156 ctypes.c_void_p, _BOOL, _DWORD, ctypes.c_void_p, _LPCSTR, ctypes.c_void_p,
157 ctypes.c_void_p]
157 ctypes.c_void_p]
158 _kernel32.CreateProcessA.restype = _BOOL
158 _kernel32.CreateProcessA.restype = _BOOL
159
159
160 _kernel32.ExitProcess.argtypes = [_UINT]
160 _kernel32.ExitProcess.argtypes = [_UINT]
161 _kernel32.ExitProcess.restype = None
161 _kernel32.ExitProcess.restype = None
162
162
163 _kernel32.GetCurrentProcessId.argtypes = []
163 _kernel32.GetCurrentProcessId.argtypes = []
164 _kernel32.GetCurrentProcessId.restype = _DWORD
164 _kernel32.GetCurrentProcessId.restype = _DWORD
165
165
166 _SIGNAL_HANDLER = ctypes.WINFUNCTYPE(_BOOL, _DWORD)
166 _SIGNAL_HANDLER = ctypes.WINFUNCTYPE(_BOOL, _DWORD)
167 _kernel32.SetConsoleCtrlHandler.argtypes = [_SIGNAL_HANDLER, _BOOL]
167 _kernel32.SetConsoleCtrlHandler.argtypes = [_SIGNAL_HANDLER, _BOOL]
168 _kernel32.SetConsoleCtrlHandler.restype = _BOOL
168 _kernel32.SetConsoleCtrlHandler.restype = _BOOL
169
169
170 _kernel32.GetStdHandle.argtypes = [_DWORD]
170 _kernel32.GetStdHandle.argtypes = [_DWORD]
171 _kernel32.GetStdHandle.restype = _HANDLE
171 _kernel32.GetStdHandle.restype = _HANDLE
172
172
173 _kernel32.GetConsoleScreenBufferInfo.argtypes = [_HANDLE, ctypes.c_void_p]
173 _kernel32.GetConsoleScreenBufferInfo.argtypes = [_HANDLE, ctypes.c_void_p]
174 _kernel32.GetConsoleScreenBufferInfo.restype = _BOOL
174 _kernel32.GetConsoleScreenBufferInfo.restype = _BOOL
175
175
176 _advapi32.GetUserNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
176 _advapi32.GetUserNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
177 _advapi32.GetUserNameA.restype = _BOOL
177 _advapi32.GetUserNameA.restype = _BOOL
178
178
179 _user32.GetWindowThreadProcessId.argtypes = [_HANDLE, ctypes.c_void_p]
179 _user32.GetWindowThreadProcessId.argtypes = [_HANDLE, ctypes.c_void_p]
180 _user32.GetWindowThreadProcessId.restype = _DWORD
180 _user32.GetWindowThreadProcessId.restype = _DWORD
181
181
182 _user32.ShowWindow.argtypes = [_HANDLE, ctypes.c_int]
182 _user32.ShowWindow.argtypes = [_HANDLE, ctypes.c_int]
183 _user32.ShowWindow.restype = _BOOL
183 _user32.ShowWindow.restype = _BOOL
184
184
185 _WNDENUMPROC = ctypes.WINFUNCTYPE(_BOOL, _HWND, _LPARAM)
185 _WNDENUMPROC = ctypes.WINFUNCTYPE(_BOOL, _HWND, _LPARAM)
186 _user32.EnumWindows.argtypes = [_WNDENUMPROC, _LPARAM]
186 _user32.EnumWindows.argtypes = [_WNDENUMPROC, _LPARAM]
187 _user32.EnumWindows.restype = _BOOL
187 _user32.EnumWindows.restype = _BOOL
188
188
189 def _raiseoserror(name):
189 def _raiseoserror(name):
190 err = ctypes.WinError()
190 err = ctypes.WinError()
191 raise OSError(err.errno, '%s: %s' % (name, err.strerror))
191 raise OSError(err.errno, '%s: %s' % (name, err.strerror))
192
192
193 def _getfileinfo(name):
193 def _getfileinfo(name):
194 fh = _kernel32.CreateFileA(name, 0,
194 fh = _kernel32.CreateFileA(name, 0,
195 _FILE_SHARE_READ | _FILE_SHARE_WRITE | _FILE_SHARE_DELETE,
195 _FILE_SHARE_READ | _FILE_SHARE_WRITE | _FILE_SHARE_DELETE,
196 None, _OPEN_EXISTING, _FILE_FLAG_BACKUP_SEMANTICS, None)
196 None, _OPEN_EXISTING, _FILE_FLAG_BACKUP_SEMANTICS, None)
197 if fh == _INVALID_HANDLE_VALUE:
197 if fh == _INVALID_HANDLE_VALUE:
198 _raiseoserror(name)
198 _raiseoserror(name)
199 try:
199 try:
200 fi = _BY_HANDLE_FILE_INFORMATION()
200 fi = _BY_HANDLE_FILE_INFORMATION()
201 if not _kernel32.GetFileInformationByHandle(fh, ctypes.byref(fi)):
201 if not _kernel32.GetFileInformationByHandle(fh, ctypes.byref(fi)):
202 _raiseoserror(name)
202 _raiseoserror(name)
203 return fi
203 return fi
204 finally:
204 finally:
205 _kernel32.CloseHandle(fh)
205 _kernel32.CloseHandle(fh)
206
206
207 def oslink(src, dst):
207 def oslink(src, dst):
208 try:
208 try:
209 if not _kernel32.CreateHardLinkA(dst, src, None):
209 if not _kernel32.CreateHardLinkA(dst, src, None):
210 _raiseoserror(src)
210 _raiseoserror(src)
211 except AttributeError: # Wine doesn't support this function
211 except AttributeError: # Wine doesn't support this function
212 _raiseoserror(src)
212 _raiseoserror(src)
213
213
214 def nlinks(name):
214 def nlinks(name):
215 '''return number of hardlinks for the given file'''
215 '''return number of hardlinks for the given file'''
216 return _getfileinfo(name).nNumberOfLinks
216 return _getfileinfo(name).nNumberOfLinks
217
217
218 def samefile(path1, path2):
218 def samefile(path1, path2):
219 '''Returns whether path1 and path2 refer to the same file or directory.'''
219 '''Returns whether path1 and path2 refer to the same file or directory.'''
220 res1 = _getfileinfo(path1)
220 res1 = _getfileinfo(path1)
221 res2 = _getfileinfo(path2)
221 res2 = _getfileinfo(path2)
222 return (res1.dwVolumeSerialNumber == res2.dwVolumeSerialNumber
222 return (res1.dwVolumeSerialNumber == res2.dwVolumeSerialNumber
223 and res1.nFileIndexHigh == res2.nFileIndexHigh
223 and res1.nFileIndexHigh == res2.nFileIndexHigh
224 and res1.nFileIndexLow == res2.nFileIndexLow)
224 and res1.nFileIndexLow == res2.nFileIndexLow)
225
225
226 def samedevice(path1, path2):
226 def samedevice(path1, path2):
227 '''Returns whether path1 and path2 are on the same device.'''
227 '''Returns whether path1 and path2 are on the same device.'''
228 res1 = _getfileinfo(path1)
228 res1 = _getfileinfo(path1)
229 res2 = _getfileinfo(path2)
229 res2 = _getfileinfo(path2)
230 return res1.dwVolumeSerialNumber == res2.dwVolumeSerialNumber
230 return res1.dwVolumeSerialNumber == res2.dwVolumeSerialNumber
231
231
232 def testpid(pid):
232 def testpid(pid):
233 '''return True if pid is still running or unable to
233 '''return True if pid is still running or unable to
234 determine, False otherwise'''
234 determine, False otherwise'''
235 h = _kernel32.OpenProcess(_PROCESS_QUERY_INFORMATION, False, pid)
235 h = _kernel32.OpenProcess(_PROCESS_QUERY_INFORMATION, False, pid)
236 if h:
236 if h:
237 try:
237 try:
238 status = _DWORD()
238 status = _DWORD()
239 if _kernel32.GetExitCodeProcess(h, ctypes.byref(status)):
239 if _kernel32.GetExitCodeProcess(h, ctypes.byref(status)):
240 return status.value == _STILL_ACTIVE
240 return status.value == _STILL_ACTIVE
241 finally:
241 finally:
242 _kernel32.CloseHandle(h)
242 _kernel32.CloseHandle(h)
243 return _kernel32.GetLastError() != _ERROR_INVALID_PARAMETER
243 return _kernel32.GetLastError() != _ERROR_INVALID_PARAMETER
244
244
245 def executablepath():
245 def executablepath():
246 '''return full path of hg.exe'''
246 '''return full path of hg.exe'''
247 size = 600
247 size = 600
248 buf = ctypes.create_string_buffer(size + 1)
248 buf = ctypes.create_string_buffer(size + 1)
249 len = _kernel32.GetModuleFileNameA(None, ctypes.byref(buf), size)
249 len = _kernel32.GetModuleFileNameA(None, ctypes.byref(buf), size)
250 if len == 0:
250 if len == 0:
251 raise ctypes.WinError
251 raise ctypes.WinError
252 elif len == size:
252 elif len == size:
253 raise ctypes.WinError(_ERROR_INSUFFICIENT_BUFFER)
253 raise ctypes.WinError(_ERROR_INSUFFICIENT_BUFFER)
254 return buf.value
254 return buf.value
255
255
256 def getuser():
256 def getuser():
257 '''return name of current user'''
257 '''return name of current user'''
258 size = _DWORD(300)
258 size = _DWORD(300)
259 buf = ctypes.create_string_buffer(size.value + 1)
259 buf = ctypes.create_string_buffer(size.value + 1)
260 if not _advapi32.GetUserNameA(ctypes.byref(buf), ctypes.byref(size)):
260 if not _advapi32.GetUserNameA(ctypes.byref(buf), ctypes.byref(size)):
261 raise ctypes.WinError
261 raise ctypes.WinError
262 return buf.value
262 return buf.value
263
263
264 _signalhandler = []
264 _signalhandler = []
265
265
266 def setsignalhandler():
266 def setsignalhandler():
267 '''Register a termination handler for console events including
267 '''Register a termination handler for console events including
268 CTRL+C. python signal handlers do not work well with socket
268 CTRL+C. python signal handlers do not work well with socket
269 operations.
269 operations.
270 '''
270 '''
271 def handler(event):
271 def handler(event):
272 _kernel32.ExitProcess(1)
272 _kernel32.ExitProcess(1)
273
273
274 if _signalhandler:
274 if _signalhandler:
275 return # already registered
275 return # already registered
276 h = _SIGNAL_HANDLER(handler)
276 h = _SIGNAL_HANDLER(handler)
277 _signalhandler.append(h) # needed to prevent garbage collection
277 _signalhandler.append(h) # needed to prevent garbage collection
278 if not _kernel32.SetConsoleCtrlHandler(h, True):
278 if not _kernel32.SetConsoleCtrlHandler(h, True):
279 raise ctypes.WinError
279 raise ctypes.WinError
280
280
281 def hidewindow():
281 def hidewindow():
282
282
283 def callback(hwnd, pid):
283 def callback(hwnd, pid):
284 wpid = _DWORD()
284 wpid = _DWORD()
285 _user32.GetWindowThreadProcessId(hwnd, ctypes.byref(wpid))
285 _user32.GetWindowThreadProcessId(hwnd, ctypes.byref(wpid))
286 if pid == wpid.value:
286 if pid == wpid.value:
287 _user32.ShowWindow(hwnd, _SW_HIDE)
287 _user32.ShowWindow(hwnd, _SW_HIDE)
288 return False # stop enumerating windows
288 return False # stop enumerating windows
289 return True
289 return True
290
290
291 pid = _kernel32.GetCurrentProcessId()
291 pid = _kernel32.GetCurrentProcessId()
292 _user32.EnumWindows(_WNDENUMPROC(callback), pid)
292 _user32.EnumWindows(_WNDENUMPROC(callback), pid)
293
293
294 def termwidth():
294 def termwidth():
295 # cmd.exe does not handle CR like a unix console, the CR is
295 # cmd.exe does not handle CR like a unix console, the CR is
296 # counted in the line length. On 80 columns consoles, if 80
296 # counted in the line length. On 80 columns consoles, if 80
297 # characters are written, the following CR won't apply on the
297 # characters are written, the following CR won't apply on the
298 # current line but on the new one. Keep room for it.
298 # current line but on the new one. Keep room for it.
299 width = 79
299 width = 79
300 # Query stderr to avoid problems with redirections
300 # Query stderr to avoid problems with redirections
301 screenbuf = _kernel32.GetStdHandle(
301 screenbuf = _kernel32.GetStdHandle(
302 _STD_ERROR_HANDLE) # don't close the handle returned
302 _STD_ERROR_HANDLE) # don't close the handle returned
303 if screenbuf is None or screenbuf == _INVALID_HANDLE_VALUE:
303 if screenbuf is None or screenbuf == _INVALID_HANDLE_VALUE:
304 return width
304 return width
305 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
305 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
306 if not _kernel32.GetConsoleScreenBufferInfo(
306 if not _kernel32.GetConsoleScreenBufferInfo(
307 screenbuf, ctypes.byref(csbi)):
307 screenbuf, ctypes.byref(csbi)):
308 return width
308 return width
309 width = csbi.srWindow.Right - csbi.srWindow.Left
309 width = csbi.srWindow.Right - csbi.srWindow.Left
310 return width
310 return width
311
311
312 def spawndetached(args):
312 def spawndetached(args):
313 # No standard library function really spawns a fully detached
313 # No standard library function really spawns a fully detached
314 # process under win32 because they allocate pipes or other objects
314 # process under win32 because they allocate pipes or other objects
315 # to handle standard streams communications. Passing these objects
315 # to handle standard streams communications. Passing these objects
316 # to the child process requires handle inheritance to be enabled
316 # to the child process requires handle inheritance to be enabled
317 # which makes really detached processes impossible.
317 # which makes really detached processes impossible.
318 si = _STARTUPINFO()
318 si = _STARTUPINFO()
319 si.cb = ctypes.sizeof(_STARTUPINFO)
319 si.cb = ctypes.sizeof(_STARTUPINFO)
320
320
321 pi = _PROCESS_INFORMATION()
321 pi = _PROCESS_INFORMATION()
322
322
323 env = ''
323 env = ''
324 for k in os.environ:
324 for k in os.environ:
325 env += "%s=%s\0" % (k, os.environ[k])
325 env += "%s=%s\0" % (k, os.environ[k])
326 if not env:
326 if not env:
327 env = '\0'
327 env = '\0'
328 env += '\0'
328 env += '\0'
329
329
330 args = subprocess.list2cmdline(args)
330 args = subprocess.list2cmdline(args)
331 # Not running the command in shell mode makes python26 hang when
331 # Not running the command in shell mode makes Python 2.6 hang when
332 # writing to hgweb output socket.
332 # writing to hgweb output socket.
333 comspec = os.environ.get("COMSPEC", "cmd.exe")
333 comspec = os.environ.get("COMSPEC", "cmd.exe")
334 args = comspec + " /c " + args
334 args = comspec + " /c " + args
335
335
336 res = _kernel32.CreateProcessA(
336 res = _kernel32.CreateProcessA(
337 None, args, None, None, False, _CREATE_NO_WINDOW,
337 None, args, None, None, False, _CREATE_NO_WINDOW,
338 env, os.getcwd(), ctypes.byref(si), ctypes.byref(pi))
338 env, os.getcwd(), ctypes.byref(si), ctypes.byref(pi))
339 if not res:
339 if not res:
340 raise ctypes.WinError
340 raise ctypes.WinError
341
341
342 return pi.dwProcessId
342 return pi.dwProcessId
343
343
344 def unlink(f):
344 def unlink(f):
345 '''try to implement POSIX' unlink semantics on Windows'''
345 '''try to implement POSIX' unlink semantics on Windows'''
346
346
347 # POSIX allows to unlink and rename open files. Windows has serious
347 # POSIX allows to unlink and rename open files. Windows has serious
348 # problems with doing that:
348 # problems with doing that:
349 # - Calling os.unlink (or os.rename) on a file f fails if f or any
349 # - Calling os.unlink (or os.rename) on a file f fails if f or any
350 # hardlinked copy of f has been opened with Python's open(). There is no
350 # hardlinked copy of f has been opened with Python's open(). There is no
351 # way such a file can be deleted or renamed on Windows (other than
351 # way such a file can be deleted or renamed on Windows (other than
352 # scheduling the delete or rename for the next reboot).
352 # scheduling the delete or rename for the next reboot).
353 # - Calling os.unlink on a file that has been opened with Mercurial's
353 # - Calling os.unlink on a file that has been opened with Mercurial's
354 # posixfile (or comparable methods) will delay the actual deletion of
354 # posixfile (or comparable methods) will delay the actual deletion of
355 # the file for as long as the file is held open. The filename is blocked
355 # the file for as long as the file is held open. The filename is blocked
356 # during that time and cannot be used for recreating a new file under
356 # during that time and cannot be used for recreating a new file under
357 # that same name ("zombie file"). Directories containing such zombie files
357 # that same name ("zombie file"). Directories containing such zombie files
358 # cannot be removed or moved.
358 # cannot be removed or moved.
359 # A file that has been opened with posixfile can be renamed, so we rename
359 # A file that has been opened with posixfile can be renamed, so we rename
360 # f to a random temporary name before calling os.unlink on it. This allows
360 # f to a random temporary name before calling os.unlink on it. This allows
361 # callers to recreate f immediately while having other readers do their
361 # callers to recreate f immediately while having other readers do their
362 # implicit zombie filename blocking on a temporary name.
362 # implicit zombie filename blocking on a temporary name.
363
363
364 for tries in xrange(10):
364 for tries in xrange(10):
365 temp = '%s-%08x' % (f, random.randint(0, 0xffffffff))
365 temp = '%s-%08x' % (f, random.randint(0, 0xffffffff))
366 try:
366 try:
367 os.rename(f, temp) # raises OSError EEXIST if temp exists
367 os.rename(f, temp) # raises OSError EEXIST if temp exists
368 break
368 break
369 except OSError, e:
369 except OSError, e:
370 if e.errno != errno.EEXIST:
370 if e.errno != errno.EEXIST:
371 raise
371 raise
372 else:
372 else:
373 raise IOError, (errno.EEXIST, "No usable temporary filename found")
373 raise IOError, (errno.EEXIST, "No usable temporary filename found")
374
374
375 try:
375 try:
376 os.unlink(temp)
376 os.unlink(temp)
377 except OSError:
377 except OSError:
378 # The unlink might have failed because the READONLY attribute may heave
378 # The unlink might have failed because the READONLY attribute may heave
379 # been set on the original file. Rename works fine with READONLY set,
379 # been set on the original file. Rename works fine with READONLY set,
380 # but not os.unlink. Reset all attributes and try again.
380 # but not os.unlink. Reset all attributes and try again.
381 _kernel32.SetFileAttributesA(temp, _FILE_ATTRIBUTE_NORMAL)
381 _kernel32.SetFileAttributesA(temp, _FILE_ATTRIBUTE_NORMAL)
382 try:
382 try:
383 os.unlink(temp)
383 os.unlink(temp)
384 except OSError:
384 except OSError:
385 # The unlink might have failed due to some very rude AV-Scanners.
385 # The unlink might have failed due to some very rude AV-Scanners.
386 # Leaking a tempfile is the lesser evil than aborting here and
386 # Leaking a tempfile is the lesser evil than aborting here and
387 # leaving some potentially serious inconsistencies.
387 # leaving some potentially serious inconsistencies.
388 pass
388 pass
389
389
390 def makedir(path, notindexed):
390 def makedir(path, notindexed):
391 os.mkdir(path)
391 os.mkdir(path)
392 if notindexed:
392 if notindexed:
393 _kernel32.SetFileAttributesA(path, _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED)
393 _kernel32.SetFileAttributesA(path, _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED)
@@ -1,142 +1,142
1 $ cat > correct.py <<EOF
1 $ cat > correct.py <<EOF
2 > def toto(arg1, arg2):
2 > def toto(arg1, arg2):
3 > del arg2
3 > del arg2
4 > return (5 + 6, 9)
4 > return (5 + 6, 9)
5 > EOF
5 > EOF
6 $ cat > wrong.py <<EOF
6 $ cat > wrong.py <<EOF
7 > def toto( arg1, arg2):
7 > def toto( arg1, arg2):
8 > del(arg2)
8 > del(arg2)
9 > return ( 5+6, 9)
9 > return ( 5+6, 9)
10 > EOF
10 > EOF
11 $ cat > quote.py <<EOF
11 $ cat > quote.py <<EOF
12 > # let's use quote in comments
12 > # let's use quote in comments
13 > (''' ( 4x5 )
13 > (''' ( 4x5 )
14 > but """\\''' and finally''',
14 > but """\\''' and finally''',
15 > """let's fool checkpatch""", '1+2',
15 > """let's fool checkpatch""", '1+2',
16 > '"""', 42+1, """and
16 > '"""', 42+1, """and
17 > ( 4-1 ) """, "( 1+1 )\" and ")
17 > ( 4-1 ) """, "( 1+1 )\" and ")
18 > a, '\\\\\\\\', "\\\\\\" x-2", "c-1"
18 > a, '\\\\\\\\', "\\\\\\" x-2", "c-1"
19 > EOF
19 > EOF
20 $ cat > non-py24.py <<EOF
20 $ cat > non-py24.py <<EOF
21 > # Using builtins that does not exist in Python 2.4
21 > # Using builtins that does not exist in Python 2.4
22 > if any():
22 > if any():
23 > x = all()
23 > x = all()
24 > y = format(x)
24 > y = format(x)
25 >
25 >
26 > # Do not complain about our own definition
26 > # Do not complain about our own definition
27 > def any(x):
27 > def any(x):
28 > pass
28 > pass
29 >
29 >
30 > # try/except/finally block does not exist in Python 2.4
30 > # try/except/finally block does not exist in Python 2.4
31 > try:
31 > try:
32 > pass
32 > pass
33 > except StandardError, inst:
33 > except StandardError, inst:
34 > pass
34 > pass
35 > finally:
35 > finally:
36 > pass
36 > pass
37 >
37 >
38 > # nested try/finally+try/except is allowed
38 > # nested try/finally+try/except is allowed
39 > try:
39 > try:
40 > try:
40 > try:
41 > pass
41 > pass
42 > except StandardError, inst:
42 > except StandardError, inst:
43 > pass
43 > pass
44 > finally:
44 > finally:
45 > pass
45 > pass
46 > EOF
46 > EOF
47 $ cat > classstyle.py <<EOF
47 $ cat > classstyle.py <<EOF
48 > class newstyle_class(object):
48 > class newstyle_class(object):
49 > pass
49 > pass
50 >
50 >
51 > class oldstyle_class:
51 > class oldstyle_class:
52 > pass
52 > pass
53 >
53 >
54 > class empty():
54 > class empty():
55 > pass
55 > pass
56 >
56 >
57 > no_class = 1:
57 > no_class = 1:
58 > pass
58 > pass
59 > EOF
59 > EOF
60 $ check_code="$TESTDIR"/../contrib/check-code.py
60 $ check_code="$TESTDIR"/../contrib/check-code.py
61 $ "$check_code" ./wrong.py ./correct.py ./quote.py ./non-py24.py ./classstyle.py
61 $ "$check_code" ./wrong.py ./correct.py ./quote.py ./non-py24.py ./classstyle.py
62 ./wrong.py:1:
62 ./wrong.py:1:
63 > def toto( arg1, arg2):
63 > def toto( arg1, arg2):
64 gratuitous whitespace in () or []
64 gratuitous whitespace in () or []
65 ./wrong.py:2:
65 ./wrong.py:2:
66 > del(arg2)
66 > del(arg2)
67 Python keyword is not a function
67 Python keyword is not a function
68 ./wrong.py:3:
68 ./wrong.py:3:
69 > return ( 5+6, 9)
69 > return ( 5+6, 9)
70 gratuitous whitespace in () or []
70 gratuitous whitespace in () or []
71 missing whitespace in expression
71 missing whitespace in expression
72 ./quote.py:5:
72 ./quote.py:5:
73 > '"""', 42+1, """and
73 > '"""', 42+1, """and
74 missing whitespace in expression
74 missing whitespace in expression
75 ./non-py24.py:2:
75 ./non-py24.py:2:
76 > if any():
76 > if any():
77 any/all/format not available in Python 2.4
77 any/all/format not available in Python 2.4
78 ./non-py24.py:3:
78 ./non-py24.py:3:
79 > x = all()
79 > x = all()
80 any/all/format not available in Python 2.4
80 any/all/format not available in Python 2.4
81 ./non-py24.py:4:
81 ./non-py24.py:4:
82 > y = format(x)
82 > y = format(x)
83 any/all/format not available in Python 2.4
83 any/all/format not available in Python 2.4
84 ./non-py24.py:11:
84 ./non-py24.py:11:
85 > try:
85 > try:
86 no try/except/finally in Py2.4
86 no try/except/finally in Python 2.4
87 ./classstyle.py:4:
87 ./classstyle.py:4:
88 > class oldstyle_class:
88 > class oldstyle_class:
89 old-style class, use class foo(object)
89 old-style class, use class foo(object)
90 ./classstyle.py:7:
90 ./classstyle.py:7:
91 > class empty():
91 > class empty():
92 class foo() not available in Python 2.4, use class foo(object)
92 class foo() not available in Python 2.4, use class foo(object)
93 [1]
93 [1]
94
94
95 $ cat > is-op.py <<EOF
95 $ cat > is-op.py <<EOF
96 > # is-operator comparing number or string literal
96 > # is-operator comparing number or string literal
97 > x = None
97 > x = None
98 > y = x is 'foo'
98 > y = x is 'foo'
99 > y = x is "foo"
99 > y = x is "foo"
100 > y = x is 5346
100 > y = x is 5346
101 > y = x is -6
101 > y = x is -6
102 > y = x is not 'foo'
102 > y = x is not 'foo'
103 > y = x is not "foo"
103 > y = x is not "foo"
104 > y = x is not 5346
104 > y = x is not 5346
105 > y = x is not -6
105 > y = x is not -6
106 > EOF
106 > EOF
107
107
108 $ "$check_code" ./is-op.py
108 $ "$check_code" ./is-op.py
109 ./is-op.py:3:
109 ./is-op.py:3:
110 > y = x is 'foo'
110 > y = x is 'foo'
111 object comparison with literal
111 object comparison with literal
112 ./is-op.py:4:
112 ./is-op.py:4:
113 > y = x is "foo"
113 > y = x is "foo"
114 object comparison with literal
114 object comparison with literal
115 ./is-op.py:5:
115 ./is-op.py:5:
116 > y = x is 5346
116 > y = x is 5346
117 object comparison with literal
117 object comparison with literal
118 ./is-op.py:6:
118 ./is-op.py:6:
119 > y = x is -6
119 > y = x is -6
120 object comparison with literal
120 object comparison with literal
121 ./is-op.py:7:
121 ./is-op.py:7:
122 > y = x is not 'foo'
122 > y = x is not 'foo'
123 object comparison with literal
123 object comparison with literal
124 ./is-op.py:8:
124 ./is-op.py:8:
125 > y = x is not "foo"
125 > y = x is not "foo"
126 object comparison with literal
126 object comparison with literal
127 ./is-op.py:9:
127 ./is-op.py:9:
128 > y = x is not 5346
128 > y = x is not 5346
129 object comparison with literal
129 object comparison with literal
130 ./is-op.py:10:
130 ./is-op.py:10:
131 > y = x is not -6
131 > y = x is not -6
132 object comparison with literal
132 object comparison with literal
133 [1]
133 [1]
134
134
135 $ cat > warning.py <<EOF
135 $ cat > warning.py <<EOF
136 > except:
136 > except:
137 > EOF
137 > EOF
138 $ "$check_code" warning.py --warning --nolineno
138 $ "$check_code" warning.py --warning --nolineno
139 warning.py:0:
139 warning.py:0:
140 > except:
140 > except:
141 warning: naked except clause
141 warning: naked except clause
142 [1]
142 [1]
General Comments 0
You need to be logged in to leave comments. Login now