##// END OF EJS Templates
zeroconf: use absolute_import
timeless -
r28296:a73394e7 default
parent child Browse files
Show More
@@ -1,1582 +1,1582
1 from __future__ import print_function
1 from __future__ import absolute_import, print_function
2 2
3 3 """ Multicast DNS Service Discovery for Python, v0.12
4 4 Copyright (C) 2003, Paul Scott-Murphy
5 5
6 6 This module provides a framework for the use of DNS Service Discovery
7 7 using IP multicast. It has been tested against the JRendezvous
8 8 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
9 9 and against the mDNSResponder from Mac OS X 10.3.8.
10 10
11 11 This library is free software; you can redistribute it and/or
12 12 modify it under the terms of the GNU Lesser General Public
13 13 License as published by the Free Software Foundation; either
14 14 version 2.1 of the License, or (at your option) any later version.
15 15
16 16 This library is distributed in the hope that it will be useful,
17 17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 19 Lesser General Public License for more details.
20 20
21 21 You should have received a copy of the GNU Lesser General Public
22 22 License along with this library; if not, see
23 23 <http://www.gnu.org/licenses/>.
24 24
25 25 """
26 26
27 27 """0.12 update - allow selection of binding interface
28 28 typo fix - Thanks A. M. Kuchlingi
29 29 removed all use of word 'Rendezvous' - this is an API change"""
30 30
31 31 """0.11 update - correction to comments for addListener method
32 32 support for new record types seen from OS X
33 33 - IPv6 address
34 34 - hostinfo
35 35 ignore unknown DNS record types
36 36 fixes to name decoding
37 37 works alongside other processes using port 5353 (e.g. on Mac OS X)
38 38 tested against Mac OS X 10.3.2's mDNSResponder
39 39 corrections to removal of list entries for service browser"""
40 40
41 41 """0.10 update - Jonathon Paisley contributed these corrections:
42 42 always multicast replies, even when query is unicast
43 43 correct a pointer encoding problem
44 44 can now write records in any order
45 45 traceback shown on failure
46 46 better TXT record parsing
47 47 server is now separate from name
48 48 can cancel a service browser
49 49
50 50 modified some unit tests to accommodate these changes"""
51 51
52 52 """0.09 update - remove all records on service unregistration
53 53 fix DOS security problem with readName"""
54 54
55 55 """0.08 update - changed licensing to LGPL"""
56 56
57 57 """0.07 update - faster shutdown on engine
58 58 pointer encoding of outgoing names
59 59 ServiceBrowser now works
60 60 new unit tests"""
61 61
62 62 """0.06 update - small improvements with unit tests
63 63 added defined exception types
64 64 new style objects
65 65 fixed hostname/interface problem
66 66 fixed socket timeout problem
67 67 fixed addServiceListener() typo bug
68 68 using select() for socket reads
69 69 tested on Debian unstable with Python 2.2.2"""
70 70
71 71 """0.05 update - ensure case insensitivity on domain names
72 72 support for unicast DNS queries"""
73 73
74 74 """0.04 update - added some unit tests
75 75 added __ne__ adjuncts where required
76 76 ensure names end in '.local.'
77 77 timeout on receiving socket for clean shutdown"""
78 78
79 79 __author__ = "Paul Scott-Murphy"
80 80 __email__ = "paul at scott dash murphy dot com"
81 81 __version__ = "0.12"
82 82
83 import select
84 import socket
83 85 import string
84 import time
85 86 import struct
86 import socket
87 87 import threading
88 import select
88 import time
89 89 import traceback
90 90
91 91 __all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
92 92
93 93 # hook for threads
94 94
95 95 globals()['_GLOBAL_DONE'] = 0
96 96
97 97 # Some timing constants
98 98
99 99 _UNREGISTER_TIME = 125
100 100 _CHECK_TIME = 175
101 101 _REGISTER_TIME = 225
102 102 _LISTENER_TIME = 200
103 103 _BROWSER_TIME = 500
104 104
105 105 # Some DNS constants
106 106
107 107 _MDNS_ADDR = '224.0.0.251'
108 108 _MDNS_PORT = 5353;
109 109 _DNS_PORT = 53;
110 110 _DNS_TTL = 60 * 60; # one hour default TTL
111 111
112 112 _MAX_MSG_TYPICAL = 1460 # unused
113 113 _MAX_MSG_ABSOLUTE = 8972
114 114
115 115 _FLAGS_QR_MASK = 0x8000 # query response mask
116 116 _FLAGS_QR_QUERY = 0x0000 # query
117 117 _FLAGS_QR_RESPONSE = 0x8000 # response
118 118
119 119 _FLAGS_AA = 0x0400 # Authoritative answer
120 120 _FLAGS_TC = 0x0200 # Truncated
121 121 _FLAGS_RD = 0x0100 # Recursion desired
122 122 _FLAGS_RA = 0x8000 # Recursion available
123 123
124 124 _FLAGS_Z = 0x0040 # Zero
125 125 _FLAGS_AD = 0x0020 # Authentic data
126 126 _FLAGS_CD = 0x0010 # Checking disabled
127 127
128 128 _CLASS_IN = 1
129 129 _CLASS_CS = 2
130 130 _CLASS_CH = 3
131 131 _CLASS_HS = 4
132 132 _CLASS_NONE = 254
133 133 _CLASS_ANY = 255
134 134 _CLASS_MASK = 0x7FFF
135 135 _CLASS_UNIQUE = 0x8000
136 136
137 137 _TYPE_A = 1
138 138 _TYPE_NS = 2
139 139 _TYPE_MD = 3
140 140 _TYPE_MF = 4
141 141 _TYPE_CNAME = 5
142 142 _TYPE_SOA = 6
143 143 _TYPE_MB = 7
144 144 _TYPE_MG = 8
145 145 _TYPE_MR = 9
146 146 _TYPE_NULL = 10
147 147 _TYPE_WKS = 11
148 148 _TYPE_PTR = 12
149 149 _TYPE_HINFO = 13
150 150 _TYPE_MINFO = 14
151 151 _TYPE_MX = 15
152 152 _TYPE_TXT = 16
153 153 _TYPE_AAAA = 28
154 154 _TYPE_SRV = 33
155 155 _TYPE_ANY = 255
156 156
157 157 # Mapping constants to names
158 158
159 159 _CLASSES = { _CLASS_IN : "in",
160 160 _CLASS_CS : "cs",
161 161 _CLASS_CH : "ch",
162 162 _CLASS_HS : "hs",
163 163 _CLASS_NONE : "none",
164 164 _CLASS_ANY : "any" }
165 165
166 166 _TYPES = { _TYPE_A : "a",
167 167 _TYPE_NS : "ns",
168 168 _TYPE_MD : "md",
169 169 _TYPE_MF : "mf",
170 170 _TYPE_CNAME : "cname",
171 171 _TYPE_SOA : "soa",
172 172 _TYPE_MB : "mb",
173 173 _TYPE_MG : "mg",
174 174 _TYPE_MR : "mr",
175 175 _TYPE_NULL : "null",
176 176 _TYPE_WKS : "wks",
177 177 _TYPE_PTR : "ptr",
178 178 _TYPE_HINFO : "hinfo",
179 179 _TYPE_MINFO : "minfo",
180 180 _TYPE_MX : "mx",
181 181 _TYPE_TXT : "txt",
182 182 _TYPE_AAAA : "quada",
183 183 _TYPE_SRV : "srv",
184 184 _TYPE_ANY : "any" }
185 185
186 186 # utility functions
187 187
188 188 def currentTimeMillis():
189 189 """Current system time in milliseconds"""
190 190 return time.time() * 1000
191 191
192 192 # Exceptions
193 193
194 194 class NonLocalNameException(Exception):
195 195 pass
196 196
197 197 class NonUniqueNameException(Exception):
198 198 pass
199 199
200 200 class NamePartTooLongException(Exception):
201 201 pass
202 202
203 203 class AbstractMethodException(Exception):
204 204 pass
205 205
206 206 class BadTypeInNameException(Exception):
207 207 pass
208 208
209 209 class BadDomainName(Exception):
210 210 def __init__(self, pos):
211 211 Exception.__init__(self, "at position %s" % pos)
212 212
213 213 class BadDomainNameCircular(BadDomainName):
214 214 pass
215 215
216 216 # implementation classes
217 217
218 218 class DNSEntry(object):
219 219 """A DNS entry"""
220 220
221 221 def __init__(self, name, type, clazz):
222 222 self.key = string.lower(name)
223 223 self.name = name
224 224 self.type = type
225 225 self.clazz = clazz & _CLASS_MASK
226 226 self.unique = (clazz & _CLASS_UNIQUE) != 0
227 227
228 228 def __eq__(self, other):
229 229 """Equality test on name, type, and class"""
230 230 if isinstance(other, DNSEntry):
231 231 return self.name == other.name and self.type == other.type and self.clazz == other.clazz
232 232 return 0
233 233
234 234 def __ne__(self, other):
235 235 """Non-equality test"""
236 236 return not self.__eq__(other)
237 237
238 238 def getClazz(self, clazz):
239 239 """Class accessor"""
240 240 try:
241 241 return _CLASSES[clazz]
242 242 except KeyError:
243 243 return "?(%s)" % (clazz)
244 244
245 245 def getType(self, type):
246 246 """Type accessor"""
247 247 try:
248 248 return _TYPES[type]
249 249 except KeyError:
250 250 return "?(%s)" % (type)
251 251
252 252 def toString(self, hdr, other):
253 253 """String representation with additional information"""
254 254 result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))
255 255 if self.unique:
256 256 result += "-unique,"
257 257 else:
258 258 result += ","
259 259 result += self.name
260 260 if other is not None:
261 261 result += ",%s]" % (other)
262 262 else:
263 263 result += "]"
264 264 return result
265 265
266 266 class DNSQuestion(DNSEntry):
267 267 """A DNS question entry"""
268 268
269 269 def __init__(self, name, type, clazz):
270 270 if not name.endswith(".local."):
271 271 raise NonLocalNameException(name)
272 272 DNSEntry.__init__(self, name, type, clazz)
273 273
274 274 def answeredBy(self, rec):
275 275 """Returns true if the question is answered by the record"""
276 276 return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name
277 277
278 278 def __repr__(self):
279 279 """String representation"""
280 280 return DNSEntry.toString(self, "question", None)
281 281
282 282
283 283 class DNSRecord(DNSEntry):
284 284 """A DNS record - like a DNS entry, but has a TTL"""
285 285
286 286 def __init__(self, name, type, clazz, ttl):
287 287 DNSEntry.__init__(self, name, type, clazz)
288 288 self.ttl = ttl
289 289 self.created = currentTimeMillis()
290 290
291 291 def __eq__(self, other):
292 292 """Tests equality as per DNSRecord"""
293 293 if isinstance(other, DNSRecord):
294 294 return DNSEntry.__eq__(self, other)
295 295 return 0
296 296
297 297 def suppressedBy(self, msg):
298 298 """Returns true if any answer in a message can suffice for the
299 299 information held in this record."""
300 300 for record in msg.answers:
301 301 if self.suppressedByAnswer(record):
302 302 return 1
303 303 return 0
304 304
305 305 def suppressedByAnswer(self, other):
306 306 """Returns true if another record has same name, type and class,
307 307 and if its TTL is at least half of this record's."""
308 308 if self == other and other.ttl > (self.ttl / 2):
309 309 return 1
310 310 return 0
311 311
312 312 def getExpirationTime(self, percent):
313 313 """Returns the time at which this record will have expired
314 314 by a certain percentage."""
315 315 return self.created + (percent * self.ttl * 10)
316 316
317 317 def getRemainingTTL(self, now):
318 318 """Returns the remaining TTL in seconds."""
319 319 return max(0, (self.getExpirationTime(100) - now) / 1000)
320 320
321 321 def isExpired(self, now):
322 322 """Returns true if this record has expired."""
323 323 return self.getExpirationTime(100) <= now
324 324
325 325 def isStale(self, now):
326 326 """Returns true if this record is at least half way expired."""
327 327 return self.getExpirationTime(50) <= now
328 328
329 329 def resetTTL(self, other):
330 330 """Sets this record's TTL and created time to that of
331 331 another record."""
332 332 self.created = other.created
333 333 self.ttl = other.ttl
334 334
335 335 def write(self, out):
336 336 """Abstract method"""
337 337 raise AbstractMethodException
338 338
339 339 def toString(self, other):
340 340 """String representation with additional information"""
341 341 arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)
342 342 return DNSEntry.toString(self, "record", arg)
343 343
344 344 class DNSAddress(DNSRecord):
345 345 """A DNS address record"""
346 346
347 347 def __init__(self, name, type, clazz, ttl, address):
348 348 DNSRecord.__init__(self, name, type, clazz, ttl)
349 349 self.address = address
350 350
351 351 def write(self, out):
352 352 """Used in constructing an outgoing packet"""
353 353 out.writeString(self.address, len(self.address))
354 354
355 355 def __eq__(self, other):
356 356 """Tests equality on address"""
357 357 if isinstance(other, DNSAddress):
358 358 return self.address == other.address
359 359 return 0
360 360
361 361 def __repr__(self):
362 362 """String representation"""
363 363 try:
364 364 return socket.inet_ntoa(self.address)
365 365 except Exception:
366 366 return self.address
367 367
368 368 class DNSHinfo(DNSRecord):
369 369 """A DNS host information record"""
370 370
371 371 def __init__(self, name, type, clazz, ttl, cpu, os):
372 372 DNSRecord.__init__(self, name, type, clazz, ttl)
373 373 self.cpu = cpu
374 374 self.os = os
375 375
376 376 def write(self, out):
377 377 """Used in constructing an outgoing packet"""
378 378 out.writeString(self.cpu, len(self.cpu))
379 379 out.writeString(self.os, len(self.os))
380 380
381 381 def __eq__(self, other):
382 382 """Tests equality on cpu and os"""
383 383 if isinstance(other, DNSHinfo):
384 384 return self.cpu == other.cpu and self.os == other.os
385 385 return 0
386 386
387 387 def __repr__(self):
388 388 """String representation"""
389 389 return self.cpu + " " + self.os
390 390
391 391 class DNSPointer(DNSRecord):
392 392 """A DNS pointer record"""
393 393
394 394 def __init__(self, name, type, clazz, ttl, alias):
395 395 DNSRecord.__init__(self, name, type, clazz, ttl)
396 396 self.alias = alias
397 397
398 398 def write(self, out):
399 399 """Used in constructing an outgoing packet"""
400 400 out.writeName(self.alias)
401 401
402 402 def __eq__(self, other):
403 403 """Tests equality on alias"""
404 404 if isinstance(other, DNSPointer):
405 405 return self.alias == other.alias
406 406 return 0
407 407
408 408 def __repr__(self):
409 409 """String representation"""
410 410 return self.toString(self.alias)
411 411
412 412 class DNSText(DNSRecord):
413 413 """A DNS text record"""
414 414
415 415 def __init__(self, name, type, clazz, ttl, text):
416 416 DNSRecord.__init__(self, name, type, clazz, ttl)
417 417 self.text = text
418 418
419 419 def write(self, out):
420 420 """Used in constructing an outgoing packet"""
421 421 out.writeString(self.text, len(self.text))
422 422
423 423 def __eq__(self, other):
424 424 """Tests equality on text"""
425 425 if isinstance(other, DNSText):
426 426 return self.text == other.text
427 427 return 0
428 428
429 429 def __repr__(self):
430 430 """String representation"""
431 431 if len(self.text) > 10:
432 432 return self.toString(self.text[:7] + "...")
433 433 else:
434 434 return self.toString(self.text)
435 435
436 436 class DNSService(DNSRecord):
437 437 """A DNS service record"""
438 438
439 439 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
440 440 DNSRecord.__init__(self, name, type, clazz, ttl)
441 441 self.priority = priority
442 442 self.weight = weight
443 443 self.port = port
444 444 self.server = server
445 445
446 446 def write(self, out):
447 447 """Used in constructing an outgoing packet"""
448 448 out.writeShort(self.priority)
449 449 out.writeShort(self.weight)
450 450 out.writeShort(self.port)
451 451 out.writeName(self.server)
452 452
453 453 def __eq__(self, other):
454 454 """Tests equality on priority, weight, port and server"""
455 455 if isinstance(other, DNSService):
456 456 return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server
457 457 return 0
458 458
459 459 def __repr__(self):
460 460 """String representation"""
461 461 return self.toString("%s:%s" % (self.server, self.port))
462 462
463 463 class DNSIncoming(object):
464 464 """Object representation of an incoming DNS packet"""
465 465
466 466 def __init__(self, data):
467 467 """Constructor from string holding bytes of packet"""
468 468 self.offset = 0
469 469 self.data = data
470 470 self.questions = []
471 471 self.answers = []
472 472 self.numQuestions = 0
473 473 self.numAnswers = 0
474 474 self.numAuthorities = 0
475 475 self.numAdditionals = 0
476 476
477 477 self.readHeader()
478 478 self.readQuestions()
479 479 self.readOthers()
480 480
481 481 def readHeader(self):
482 482 """Reads header portion of packet"""
483 483 format = '!HHHHHH'
484 484 length = struct.calcsize(format)
485 485 info = struct.unpack(format, self.data[self.offset:self.offset+length])
486 486 self.offset += length
487 487
488 488 self.id = info[0]
489 489 self.flags = info[1]
490 490 self.numQuestions = info[2]
491 491 self.numAnswers = info[3]
492 492 self.numAuthorities = info[4]
493 493 self.numAdditionals = info[5]
494 494
495 495 def readQuestions(self):
496 496 """Reads questions section of packet"""
497 497 format = '!HH'
498 498 length = struct.calcsize(format)
499 499 for i in range(0, self.numQuestions):
500 500 name = self.readName()
501 501 info = struct.unpack(format, self.data[self.offset:self.offset+length])
502 502 self.offset += length
503 503
504 504 try:
505 505 question = DNSQuestion(name, info[0], info[1])
506 506 self.questions.append(question)
507 507 except NonLocalNameException:
508 508 pass
509 509
510 510 def readInt(self):
511 511 """Reads an integer from the packet"""
512 512 format = '!I'
513 513 length = struct.calcsize(format)
514 514 info = struct.unpack(format, self.data[self.offset:self.offset+length])
515 515 self.offset += length
516 516 return info[0]
517 517
518 518 def readCharacterString(self):
519 519 """Reads a character string from the packet"""
520 520 length = ord(self.data[self.offset])
521 521 self.offset += 1
522 522 return self.readString(length)
523 523
524 524 def readString(self, len):
525 525 """Reads a string of a given length from the packet"""
526 526 format = '!' + str(len) + 's'
527 527 length = struct.calcsize(format)
528 528 info = struct.unpack(format, self.data[self.offset:self.offset+length])
529 529 self.offset += length
530 530 return info[0]
531 531
532 532 def readUnsignedShort(self):
533 533 """Reads an unsigned short from the packet"""
534 534 format = '!H'
535 535 length = struct.calcsize(format)
536 536 info = struct.unpack(format, self.data[self.offset:self.offset+length])
537 537 self.offset += length
538 538 return info[0]
539 539
540 540 def readOthers(self):
541 541 """Reads the answers, authorities and additionals section of the packet"""
542 542 format = '!HHiH'
543 543 length = struct.calcsize(format)
544 544 n = self.numAnswers + self.numAuthorities + self.numAdditionals
545 545 for i in range(0, n):
546 546 domain = self.readName()
547 547 info = struct.unpack(format, self.data[self.offset:self.offset+length])
548 548 self.offset += length
549 549
550 550 rec = None
551 551 if info[0] == _TYPE_A:
552 552 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4))
553 553 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
554 554 rec = DNSPointer(domain, info[0], info[1], info[2], self.readName())
555 555 elif info[0] == _TYPE_TXT:
556 556 rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3]))
557 557 elif info[0] == _TYPE_SRV:
558 558 rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName())
559 559 elif info[0] == _TYPE_HINFO:
560 560 rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString())
561 561 elif info[0] == _TYPE_AAAA:
562 562 rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16))
563 563 else:
564 564 # Try to ignore types we don't know about
565 565 # this may mean the rest of the name is
566 566 # unable to be parsed, and may show errors
567 567 # so this is left for debugging. New types
568 568 # encountered need to be parsed properly.
569 569 #
570 570 #print "UNKNOWN TYPE = " + str(info[0])
571 571 #raise BadTypeInNameException
572 572 self.offset += info[3]
573 573
574 574 if rec is not None:
575 575 self.answers.append(rec)
576 576
577 577 def isQuery(self):
578 578 """Returns true if this is a query"""
579 579 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
580 580
581 581 def isResponse(self):
582 582 """Returns true if this is a response"""
583 583 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
584 584
585 585 def readUTF(self, offset, len):
586 586 """Reads a UTF-8 string of a given length from the packet"""
587 587 return self.data[offset:offset+len].decode('utf-8')
588 588
589 589 def readName(self):
590 590 """Reads a domain name from the packet"""
591 591 result = ''
592 592 off = self.offset
593 593 next = -1
594 594 first = off
595 595
596 596 while True:
597 597 len = ord(self.data[off])
598 598 off += 1
599 599 if len == 0:
600 600 break
601 601 t = len & 0xC0
602 602 if t == 0x00:
603 603 result = ''.join((result, self.readUTF(off, len) + '.'))
604 604 off += len
605 605 elif t == 0xC0:
606 606 if next < 0:
607 607 next = off + 1
608 608 off = ((len & 0x3F) << 8) | ord(self.data[off])
609 609 if off >= first:
610 610 raise BadDomainNameCircular(off)
611 611 first = off
612 612 else:
613 613 raise BadDomainName(off)
614 614
615 615 if next >= 0:
616 616 self.offset = next
617 617 else:
618 618 self.offset = off
619 619
620 620 return result
621 621
622 622
623 623 class DNSOutgoing(object):
624 624 """Object representation of an outgoing packet"""
625 625
626 626 def __init__(self, flags, multicast = 1):
627 627 self.finished = 0
628 628 self.id = 0
629 629 self.multicast = multicast
630 630 self.flags = flags
631 631 self.names = {}
632 632 self.data = []
633 633 self.size = 12
634 634
635 635 self.questions = []
636 636 self.answers = []
637 637 self.authorities = []
638 638 self.additionals = []
639 639
640 640 def addQuestion(self, record):
641 641 """Adds a question"""
642 642 self.questions.append(record)
643 643
644 644 def addAnswer(self, inp, record):
645 645 """Adds an answer"""
646 646 if not record.suppressedBy(inp):
647 647 self.addAnswerAtTime(record, 0)
648 648
649 649 def addAnswerAtTime(self, record, now):
650 650 """Adds an answer if if does not expire by a certain time"""
651 651 if record is not None:
652 652 if now == 0 or not record.isExpired(now):
653 653 self.answers.append((record, now))
654 654
655 655 def addAuthoritativeAnswer(self, record):
656 656 """Adds an authoritative answer"""
657 657 self.authorities.append(record)
658 658
659 659 def addAdditionalAnswer(self, record):
660 660 """Adds an additional answer"""
661 661 self.additionals.append(record)
662 662
663 663 def writeByte(self, value):
664 664 """Writes a single byte to the packet"""
665 665 format = '!c'
666 666 self.data.append(struct.pack(format, chr(value)))
667 667 self.size += 1
668 668
669 669 def insertShort(self, index, value):
670 670 """Inserts an unsigned short in a certain position in the packet"""
671 671 format = '!H'
672 672 self.data.insert(index, struct.pack(format, value))
673 673 self.size += 2
674 674
675 675 def writeShort(self, value):
676 676 """Writes an unsigned short to the packet"""
677 677 format = '!H'
678 678 self.data.append(struct.pack(format, value))
679 679 self.size += 2
680 680
681 681 def writeInt(self, value):
682 682 """Writes an unsigned integer to the packet"""
683 683 format = '!I'
684 684 self.data.append(struct.pack(format, int(value)))
685 685 self.size += 4
686 686
687 687 def writeString(self, value, length):
688 688 """Writes a string to the packet"""
689 689 format = '!' + str(length) + 's'
690 690 self.data.append(struct.pack(format, value))
691 691 self.size += length
692 692
693 693 def writeUTF(self, s):
694 694 """Writes a UTF-8 string of a given length to the packet"""
695 695 utfstr = s.encode('utf-8')
696 696 length = len(utfstr)
697 697 if length > 64:
698 698 raise NamePartTooLongException
699 699 self.writeByte(length)
700 700 self.writeString(utfstr, length)
701 701
702 702 def writeName(self, name):
703 703 """Writes a domain name to the packet"""
704 704
705 705 try:
706 706 # Find existing instance of this name in packet
707 707 #
708 708 index = self.names[name]
709 709 except KeyError:
710 710 # No record of this name already, so write it
711 711 # out as normal, recording the location of the name
712 712 # for future pointers to it.
713 713 #
714 714 self.names[name] = self.size
715 715 parts = name.split('.')
716 716 if parts[-1] == '':
717 717 parts = parts[:-1]
718 718 for part in parts:
719 719 self.writeUTF(part)
720 720 self.writeByte(0)
721 721 return
722 722
723 723 # An index was found, so write a pointer to it
724 724 #
725 725 self.writeByte((index >> 8) | 0xC0)
726 726 self.writeByte(index)
727 727
728 728 def writeQuestion(self, question):
729 729 """Writes a question to the packet"""
730 730 self.writeName(question.name)
731 731 self.writeShort(question.type)
732 732 self.writeShort(question.clazz)
733 733
734 734 def writeRecord(self, record, now):
735 735 """Writes a record (answer, authoritative answer, additional) to
736 736 the packet"""
737 737 self.writeName(record.name)
738 738 self.writeShort(record.type)
739 739 if record.unique and self.multicast:
740 740 self.writeShort(record.clazz | _CLASS_UNIQUE)
741 741 else:
742 742 self.writeShort(record.clazz)
743 743 if now == 0:
744 744 self.writeInt(record.ttl)
745 745 else:
746 746 self.writeInt(record.getRemainingTTL(now))
747 747 index = len(self.data)
748 748 # Adjust size for the short we will write before this record
749 749 #
750 750 self.size += 2
751 751 record.write(self)
752 752 self.size -= 2
753 753
754 754 length = len(''.join(self.data[index:]))
755 755 self.insertShort(index, length) # Here is the short we adjusted for
756 756
757 757 def packet(self):
758 758 """Returns a string containing the packet's bytes
759 759
760 760 No further parts should be added to the packet once this
761 761 is done."""
762 762 if not self.finished:
763 763 self.finished = 1
764 764 for question in self.questions:
765 765 self.writeQuestion(question)
766 for answer, time in self.answers:
767 self.writeRecord(answer, time)
766 for answer, time_ in self.answers:
767 self.writeRecord(answer, time_)
768 768 for authority in self.authorities:
769 769 self.writeRecord(authority, 0)
770 770 for additional in self.additionals:
771 771 self.writeRecord(additional, 0)
772 772
773 773 self.insertShort(0, len(self.additionals))
774 774 self.insertShort(0, len(self.authorities))
775 775 self.insertShort(0, len(self.answers))
776 776 self.insertShort(0, len(self.questions))
777 777 self.insertShort(0, self.flags)
778 778 if self.multicast:
779 779 self.insertShort(0, 0)
780 780 else:
781 781 self.insertShort(0, self.id)
782 782 return ''.join(self.data)
783 783
784 784
785 785 class DNSCache(object):
786 786 """A cache of DNS entries"""
787 787
788 788 def __init__(self):
789 789 self.cache = {}
790 790
791 791 def add(self, entry):
792 792 """Adds an entry"""
793 793 try:
794 794 list = self.cache[entry.key]
795 795 except KeyError:
796 796 list = self.cache[entry.key] = []
797 797 list.append(entry)
798 798
799 799 def remove(self, entry):
800 800 """Removes an entry"""
801 801 try:
802 802 list = self.cache[entry.key]
803 803 list.remove(entry)
804 804 except KeyError:
805 805 pass
806 806
807 807 def get(self, entry):
808 808 """Gets an entry by key. Will return None if there is no
809 809 matching entry."""
810 810 try:
811 811 list = self.cache[entry.key]
812 812 return list[list.index(entry)]
813 813 except (KeyError, ValueError):
814 814 return None
815 815
816 816 def getByDetails(self, name, type, clazz):
817 817 """Gets an entry by details. Will return None if there is
818 818 no matching entry."""
819 819 entry = DNSEntry(name, type, clazz)
820 820 return self.get(entry)
821 821
822 822 def entriesWithName(self, name):
823 823 """Returns a list of entries whose key matches the name."""
824 824 try:
825 825 return self.cache[name]
826 826 except KeyError:
827 827 return []
828 828
829 829 def entries(self):
830 830 """Returns a list of all entries"""
831 831 def add(x, y): return x+y
832 832 try:
833 833 return reduce(add, self.cache.values())
834 834 except Exception:
835 835 return []
836 836
837 837
838 838 class Engine(threading.Thread):
839 839 """An engine wraps read access to sockets, allowing objects that
840 840 need to receive data from sockets to be called back when the
841 841 sockets are ready.
842 842
843 843 A reader needs a handle_read() method, which is called when the socket
844 844 it is interested in is ready for reading.
845 845
846 846 Writers are not implemented here, because we only send short
847 847 packets.
848 848 """
849 849
850 850 def __init__(self, zeroconf):
851 851 threading.Thread.__init__(self)
852 852 self.zeroconf = zeroconf
853 853 self.readers = {} # maps socket to reader
854 854 self.timeout = 5
855 855 self.condition = threading.Condition()
856 856 self.start()
857 857
858 858 def run(self):
859 859 while not globals()['_GLOBAL_DONE']:
860 860 rs = self.getReaders()
861 861 if len(rs) == 0:
862 862 # No sockets to manage, but we wait for the timeout
863 863 # or addition of a socket
864 864 #
865 865 self.condition.acquire()
866 866 self.condition.wait(self.timeout)
867 867 self.condition.release()
868 868 else:
869 869 try:
870 870 rr, wr, er = select.select(rs, [], [], self.timeout)
871 for socket in rr:
871 for sock in rr:
872 872 try:
873 self.readers[socket].handle_read()
873 self.readers[sock].handle_read()
874 874 except Exception:
875 875 if not globals()['_GLOBAL_DONE']:
876 876 traceback.print_exc()
877 877 except Exception:
878 878 pass
879 879
880 880 def getReaders(self):
881 881 self.condition.acquire()
882 882 result = self.readers.keys()
883 883 self.condition.release()
884 884 return result
885 885
886 886 def addReader(self, reader, socket):
887 887 self.condition.acquire()
888 888 self.readers[socket] = reader
889 889 self.condition.notify()
890 890 self.condition.release()
891 891
892 892 def delReader(self, socket):
893 893 self.condition.acquire()
894 894 del(self.readers[socket])
895 895 self.condition.notify()
896 896 self.condition.release()
897 897
898 898 def notify(self):
899 899 self.condition.acquire()
900 900 self.condition.notify()
901 901 self.condition.release()
902 902
903 903 class Listener(object):
904 904 """A Listener is used by this module to listen on the multicast
905 905 group to which DNS messages are sent, allowing the implementation
906 906 to cache information as it arrives.
907 907
908 908 It requires registration with an Engine object in order to have
909 909 the read() method called when a socket is available for reading."""
910 910
911 911 def __init__(self, zeroconf):
912 912 self.zeroconf = zeroconf
913 913 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
914 914
915 915 def handle_read(self):
916 916 data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
917 917 self.data = data
918 918 msg = DNSIncoming(data)
919 919 if msg.isQuery():
920 920 # Always multicast responses
921 921 #
922 922 if port == _MDNS_PORT:
923 923 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
924 924 # If it's not a multicast query, reply via unicast
925 925 # and multicast
926 926 #
927 927 elif port == _DNS_PORT:
928 928 self.zeroconf.handleQuery(msg, addr, port)
929 929 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
930 930 else:
931 931 self.zeroconf.handleResponse(msg)
932 932
933 933
934 934 class Reaper(threading.Thread):
935 935 """A Reaper is used by this module to remove cache entries that
936 936 have expired."""
937 937
938 938 def __init__(self, zeroconf):
939 939 threading.Thread.__init__(self)
940 940 self.zeroconf = zeroconf
941 941 self.start()
942 942
943 943 def run(self):
944 944 while True:
945 945 self.zeroconf.wait(10 * 1000)
946 946 if globals()['_GLOBAL_DONE']:
947 947 return
948 948 now = currentTimeMillis()
949 949 for record in self.zeroconf.cache.entries():
950 950 if record.isExpired(now):
951 951 self.zeroconf.updateRecord(now, record)
952 952 self.zeroconf.cache.remove(record)
953 953
954 954
955 955 class ServiceBrowser(threading.Thread):
956 956 """Used to browse for a service of a specific type.
957 957
958 958 The listener object will have its addService() and
959 959 removeService() methods called when this browser
960 960 discovers changes in the services availability."""
961 961
962 962 def __init__(self, zeroconf, type, listener):
963 963 """Creates a browser for a specific type"""
964 964 threading.Thread.__init__(self)
965 965 self.zeroconf = zeroconf
966 966 self.type = type
967 967 self.listener = listener
968 968 self.services = {}
969 969 self.nextTime = currentTimeMillis()
970 970 self.delay = _BROWSER_TIME
971 971 self.list = []
972 972
973 973 self.done = 0
974 974
975 975 self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
976 976 self.start()
977 977
978 978 def updateRecord(self, zeroconf, now, record):
979 979 """Callback invoked by Zeroconf when new information arrives.
980 980
981 981 Updates information required by browser in the Zeroconf cache."""
982 982 if record.type == _TYPE_PTR and record.name == self.type:
983 983 expired = record.isExpired(now)
984 984 try:
985 985 oldrecord = self.services[record.alias.lower()]
986 986 if not expired:
987 987 oldrecord.resetTTL(record)
988 988 else:
989 989 del(self.services[record.alias.lower()])
990 990 callback = lambda x: self.listener.removeService(x, self.type, record.alias)
991 991 self.list.append(callback)
992 992 return
993 993 except Exception:
994 994 if not expired:
995 995 self.services[record.alias.lower()] = record
996 996 callback = lambda x: self.listener.addService(x, self.type, record.alias)
997 997 self.list.append(callback)
998 998
999 999 expires = record.getExpirationTime(75)
1000 1000 if expires < self.nextTime:
1001 1001 self.nextTime = expires
1002 1002
1003 1003 def cancel(self):
1004 1004 self.done = 1
1005 1005 self.zeroconf.notifyAll()
1006 1006
1007 1007 def run(self):
1008 1008 while True:
1009 1009 event = None
1010 1010 now = currentTimeMillis()
1011 1011 if len(self.list) == 0 and self.nextTime > now:
1012 1012 self.zeroconf.wait(self.nextTime - now)
1013 1013 if globals()['_GLOBAL_DONE'] or self.done:
1014 1014 return
1015 1015 now = currentTimeMillis()
1016 1016
1017 1017 if self.nextTime <= now:
1018 1018 out = DNSOutgoing(_FLAGS_QR_QUERY)
1019 1019 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1020 1020 for record in self.services.values():
1021 1021 if not record.isExpired(now):
1022 1022 out.addAnswerAtTime(record, now)
1023 1023 self.zeroconf.send(out)
1024 1024 self.nextTime = now + self.delay
1025 1025 self.delay = min(20 * 1000, self.delay * 2)
1026 1026
1027 1027 if len(self.list) > 0:
1028 1028 event = self.list.pop(0)
1029 1029
1030 1030 if event is not None:
1031 1031 event(self.zeroconf)
1032 1032
1033 1033
1034 1034 class ServiceInfo(object):
1035 1035 """Service information"""
1036 1036
1037 1037 def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
1038 1038 """Create a service description.
1039 1039
1040 1040 type: fully qualified service type name
1041 1041 name: fully qualified service name
1042 1042 address: IP address as unsigned short, network byte order
1043 1043 port: port that the service runs on
1044 1044 weight: weight of the service
1045 1045 priority: priority of the service
1046 1046 properties: dictionary of properties (or a string holding the bytes for the text field)
1047 1047 server: fully qualified name for service host (defaults to name)"""
1048 1048
1049 1049 if not name.endswith(type):
1050 1050 raise BadTypeInNameException
1051 1051 self.type = type
1052 1052 self.name = name
1053 1053 self.address = address
1054 1054 self.port = port
1055 1055 self.weight = weight
1056 1056 self.priority = priority
1057 1057 if server:
1058 1058 self.server = server
1059 1059 else:
1060 1060 self.server = name
1061 1061 self.setProperties(properties)
1062 1062
1063 1063 def setProperties(self, properties):
1064 1064 """Sets properties and text of this info from a dictionary"""
1065 1065 if isinstance(properties, dict):
1066 1066 self.properties = properties
1067 1067 list = []
1068 1068 result = ''
1069 1069 for key in properties:
1070 1070 value = properties[key]
1071 1071 if value is None:
1072 1072 suffix = ''
1073 1073 elif isinstance(value, str):
1074 1074 suffix = value
1075 1075 elif isinstance(value, int):
1076 1076 if value:
1077 1077 suffix = 'true'
1078 1078 else:
1079 1079 suffix = 'false'
1080 1080 else:
1081 1081 suffix = ''
1082 1082 list.append('='.join((key, suffix)))
1083 1083 for item in list:
1084 1084 result = ''.join((result, struct.pack('!c', chr(len(item))), item))
1085 1085 self.text = result
1086 1086 else:
1087 1087 self.text = properties
1088 1088
1089 1089 def setText(self, text):
1090 1090 """Sets properties and text given a text field"""
1091 1091 self.text = text
1092 1092 try:
1093 1093 result = {}
1094 1094 end = len(text)
1095 1095 index = 0
1096 1096 strs = []
1097 1097 while index < end:
1098 1098 length = ord(text[index])
1099 1099 index += 1
1100 1100 strs.append(text[index:index+length])
1101 1101 index += length
1102 1102
1103 1103 for s in strs:
1104 1104 eindex = s.find('=')
1105 1105 if eindex == -1:
1106 1106 # No equals sign at all
1107 1107 key = s
1108 1108 value = 0
1109 1109 else:
1110 1110 key = s[:eindex]
1111 1111 value = s[eindex+1:]
1112 1112 if value == 'true':
1113 1113 value = 1
1114 1114 elif value == 'false' or not value:
1115 1115 value = 0
1116 1116
1117 1117 # Only update non-existent properties
1118 1118 if key and result.get(key) == None:
1119 1119 result[key] = value
1120 1120
1121 1121 self.properties = result
1122 1122 except Exception:
1123 1123 traceback.print_exc()
1124 1124 self.properties = None
1125 1125
1126 1126 def getType(self):
1127 1127 """Type accessor"""
1128 1128 return self.type
1129 1129
1130 1130 def getName(self):
1131 1131 """Name accessor"""
1132 1132 if self.type is not None and self.name.endswith("." + self.type):
1133 1133 return self.name[:len(self.name) - len(self.type) - 1]
1134 1134 return self.name
1135 1135
1136 1136 def getAddress(self):
1137 1137 """Address accessor"""
1138 1138 return self.address
1139 1139
1140 1140 def getPort(self):
1141 1141 """Port accessor"""
1142 1142 return self.port
1143 1143
1144 1144 def getPriority(self):
1145 1145 """Priority accessor"""
1146 1146 return self.priority
1147 1147
1148 1148 def getWeight(self):
1149 1149 """Weight accessor"""
1150 1150 return self.weight
1151 1151
1152 1152 def getProperties(self):
1153 1153 """Properties accessor"""
1154 1154 return self.properties
1155 1155
1156 1156 def getText(self):
1157 1157 """Text accessor"""
1158 1158 return self.text
1159 1159
1160 1160 def getServer(self):
1161 1161 """Server accessor"""
1162 1162 return self.server
1163 1163
1164 1164 def updateRecord(self, zeroconf, now, record):
1165 1165 """Updates service information from a DNS record"""
1166 1166 if record is not None and not record.isExpired(now):
1167 1167 if record.type == _TYPE_A:
1168 1168 #if record.name == self.name:
1169 1169 if record.name == self.server:
1170 1170 self.address = record.address
1171 1171 elif record.type == _TYPE_SRV:
1172 1172 if record.name == self.name:
1173 1173 self.server = record.server
1174 1174 self.port = record.port
1175 1175 self.weight = record.weight
1176 1176 self.priority = record.priority
1177 1177 #self.address = None
1178 1178 self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
1179 1179 elif record.type == _TYPE_TXT:
1180 1180 if record.name == self.name:
1181 1181 self.setText(record.text)
1182 1182
1183 1183 def request(self, zeroconf, timeout):
1184 1184 """Returns true if the service could be discovered on the
1185 1185 network, and updates this object with details discovered.
1186 1186 """
1187 1187 now = currentTimeMillis()
1188 1188 delay = _LISTENER_TIME
1189 1189 next = now + delay
1190 1190 last = now + timeout
1191 1191 result = 0
1192 1192 try:
1193 1193 zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1194 1194 while self.server is None or self.address is None or self.text is None:
1195 1195 if last <= now:
1196 1196 return 0
1197 1197 if next <= now:
1198 1198 out = DNSOutgoing(_FLAGS_QR_QUERY)
1199 1199 out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
1200 1200 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
1201 1201 out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
1202 1202 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
1203 1203 if self.server is not None:
1204 1204 out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
1205 1205 out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
1206 1206 zeroconf.send(out)
1207 1207 next = now + delay
1208 1208 delay = delay * 2
1209 1209
1210 1210 zeroconf.wait(min(next, last) - now)
1211 1211 now = currentTimeMillis()
1212 1212 result = 1
1213 1213 finally:
1214 1214 zeroconf.removeListener(self)
1215 1215
1216 1216 return result
1217 1217
1218 1218 def __eq__(self, other):
1219 1219 """Tests equality of service name"""
1220 1220 if isinstance(other, ServiceInfo):
1221 1221 return other.name == self.name
1222 1222 return 0
1223 1223
1224 1224 def __ne__(self, other):
1225 1225 """Non-equality test"""
1226 1226 return not self.__eq__(other)
1227 1227
1228 1228 def __repr__(self):
1229 1229 """String representation"""
1230 1230 result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
1231 1231 if self.text is None:
1232 1232 result += "None"
1233 1233 else:
1234 1234 if len(self.text) < 20:
1235 1235 result += self.text
1236 1236 else:
1237 1237 result += self.text[:17] + "..."
1238 1238 result += "]"
1239 1239 return result
1240 1240
1241 1241
1242 1242 class Zeroconf(object):
1243 1243 """Implementation of Zeroconf Multicast DNS Service Discovery
1244 1244
1245 1245 Supports registration, unregistration, queries and browsing.
1246 1246 """
1247 1247 def __init__(self, bindaddress=None):
1248 1248 """Creates an instance of the Zeroconf class, establishing
1249 1249 multicast communications, listening and reaping threads."""
1250 1250 globals()['_GLOBAL_DONE'] = 0
1251 1251 if bindaddress is None:
1252 1252 self.intf = socket.gethostbyname(socket.gethostname())
1253 1253 else:
1254 1254 self.intf = bindaddress
1255 1255 self.group = ('', _MDNS_PORT)
1256 1256 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1257 1257 try:
1258 1258 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1259 1259 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1260 1260 except Exception:
1261 1261 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1262 1262 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1263 1263 # Volume 2"), but some BSD-derived systems require
1264 1264 # SO_REUSEPORT to be specified explicitly. Also, not all
1265 1265 # versions of Python have SO_REUSEPORT available. So
1266 1266 # if you're on a BSD-based system, and haven't upgraded
1267 1267 # to Python 2.3 yet, you may find this library doesn't
1268 1268 # work as expected.
1269 1269 #
1270 1270 pass
1271 1271 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, "\xff")
1272 1272 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, "\x01")
1273 1273 try:
1274 1274 self.socket.bind(self.group)
1275 1275 except Exception:
1276 1276 # Some versions of linux raise an exception even though
1277 1277 # SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it
1278 1278 pass
1279 1279 self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1280 1280
1281 1281 self.listeners = []
1282 1282 self.browsers = []
1283 1283 self.services = {}
1284 1284 self.servicetypes = {}
1285 1285
1286 1286 self.cache = DNSCache()
1287 1287
1288 1288 self.condition = threading.Condition()
1289 1289
1290 1290 self.engine = Engine(self)
1291 1291 self.listener = Listener(self)
1292 1292 self.reaper = Reaper(self)
1293 1293
1294 1294 def isLoopback(self):
1295 1295 return self.intf.startswith("127.0.0.1")
1296 1296
1297 1297 def isLinklocal(self):
1298 1298 return self.intf.startswith("169.254.")
1299 1299
1300 1300 def wait(self, timeout):
1301 1301 """Calling thread waits for a given number of milliseconds or
1302 1302 until notified."""
1303 1303 self.condition.acquire()
1304 1304 self.condition.wait(timeout/1000)
1305 1305 self.condition.release()
1306 1306
1307 1307 def notifyAll(self):
1308 1308 """Notifies all waiting threads"""
1309 1309 self.condition.acquire()
1310 1310 self.condition.notifyAll()
1311 1311 self.condition.release()
1312 1312
1313 1313 def getServiceInfo(self, type, name, timeout=3000):
1314 1314 """Returns network's service information for a particular
1315 1315 name and type, or None if no service matches by the timeout,
1316 1316 which defaults to 3 seconds."""
1317 1317 info = ServiceInfo(type, name)
1318 1318 if info.request(self, timeout):
1319 1319 return info
1320 1320 return None
1321 1321
1322 1322 def addServiceListener(self, type, listener):
1323 1323 """Adds a listener for a particular service type. This object
1324 1324 will then have its updateRecord method called when information
1325 1325 arrives for that type."""
1326 1326 self.removeServiceListener(listener)
1327 1327 self.browsers.append(ServiceBrowser(self, type, listener))
1328 1328
1329 1329 def removeServiceListener(self, listener):
1330 1330 """Removes a listener from the set that is currently listening."""
1331 1331 for browser in self.browsers:
1332 1332 if browser.listener == listener:
1333 1333 browser.cancel()
1334 1334 del(browser)
1335 1335
1336 1336 def registerService(self, info, ttl=_DNS_TTL):
1337 1337 """Registers service information to the network with a default TTL
1338 1338 of 60 seconds. Zeroconf will then respond to requests for
1339 1339 information for that service. The name of the service may be
1340 1340 changed if needed to make it unique on the network."""
1341 1341 self.checkService(info)
1342 1342 self.services[info.name.lower()] = info
1343 1343 if self.servicetypes.has_key(info.type):
1344 1344 self.servicetypes[info.type]+=1
1345 1345 else:
1346 1346 self.servicetypes[info.type]=1
1347 1347 now = currentTimeMillis()
1348 1348 nextTime = now
1349 1349 i = 0
1350 1350 while i < 3:
1351 1351 if now < nextTime:
1352 1352 self.wait(nextTime - now)
1353 1353 now = currentTimeMillis()
1354 1354 continue
1355 1355 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1356 1356 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
1357 1357 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0)
1358 1358 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
1359 1359 if info.address:
1360 1360 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
1361 1361 self.send(out)
1362 1362 i += 1
1363 1363 nextTime += _REGISTER_TIME
1364 1364
1365 1365 def unregisterService(self, info):
1366 1366 """Unregister a service."""
1367 1367 try:
1368 1368 del(self.services[info.name.lower()])
1369 1369 if self.servicetypes[info.type]>1:
1370 1370 self.servicetypes[info.type]-=1
1371 1371 else:
1372 1372 del self.servicetypes[info.type]
1373 1373 except KeyError:
1374 1374 pass
1375 1375 now = currentTimeMillis()
1376 1376 nextTime = now
1377 1377 i = 0
1378 1378 while i < 3:
1379 1379 if now < nextTime:
1380 1380 self.wait(nextTime - now)
1381 1381 now = currentTimeMillis()
1382 1382 continue
1383 1383 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1384 1384 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1385 1385 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0)
1386 1386 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1387 1387 if info.address:
1388 1388 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1389 1389 self.send(out)
1390 1390 i += 1
1391 1391 nextTime += _UNREGISTER_TIME
1392 1392
1393 1393 def unregisterAllServices(self):
1394 1394 """Unregister all registered services."""
1395 1395 if len(self.services) > 0:
1396 1396 now = currentTimeMillis()
1397 1397 nextTime = now
1398 1398 i = 0
1399 1399 while i < 3:
1400 1400 if now < nextTime:
1401 1401 self.wait(nextTime - now)
1402 1402 now = currentTimeMillis()
1403 1403 continue
1404 1404 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1405 1405 for info in self.services.values():
1406 1406 out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
1407 1407 out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0)
1408 1408 out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1409 1409 if info.address:
1410 1410 out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
1411 1411 self.send(out)
1412 1412 i += 1
1413 1413 nextTime += _UNREGISTER_TIME
1414 1414
1415 1415 def checkService(self, info):
1416 1416 """Checks the network for a unique service name, modifying the
1417 1417 ServiceInfo passed in if it is not unique."""
1418 1418 now = currentTimeMillis()
1419 1419 nextTime = now
1420 1420 i = 0
1421 1421 while i < 3:
1422 1422 for record in self.cache.entriesWithName(info.type):
1423 1423 if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
1424 1424 if (info.name.find('.') < 0):
1425 1425 info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
1426 1426 self.checkService(info)
1427 1427 return
1428 1428 raise NonUniqueNameException
1429 1429 if now < nextTime:
1430 1430 self.wait(nextTime - now)
1431 1431 now = currentTimeMillis()
1432 1432 continue
1433 1433 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1434 1434 self.debug = out
1435 1435 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1436 1436 out.addAuthoritativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
1437 1437 self.send(out)
1438 1438 i += 1
1439 1439 nextTime += _CHECK_TIME
1440 1440
1441 1441 def addListener(self, listener, question):
1442 1442 """Adds a listener for a given question. The listener will have
1443 1443 its updateRecord method called when information is available to
1444 1444 answer the question."""
1445 1445 now = currentTimeMillis()
1446 1446 self.listeners.append(listener)
1447 1447 if question is not None:
1448 1448 for record in self.cache.entriesWithName(question.name):
1449 1449 if question.answeredBy(record) and not record.isExpired(now):
1450 1450 listener.updateRecord(self, now, record)
1451 1451 self.notifyAll()
1452 1452
1453 1453 def removeListener(self, listener):
1454 1454 """Removes a listener."""
1455 1455 try:
1456 1456 self.listeners.remove(listener)
1457 1457 self.notifyAll()
1458 1458 except Exception:
1459 1459 pass
1460 1460
1461 1461 def updateRecord(self, now, rec):
1462 1462 """Used to notify listeners of new information that has updated
1463 1463 a record."""
1464 1464 for listener in self.listeners:
1465 1465 listener.updateRecord(self, now, rec)
1466 1466 self.notifyAll()
1467 1467
1468 1468 def handleResponse(self, msg):
1469 1469 """Deal with incoming response packets. All answers
1470 1470 are held in the cache, and listeners are notified."""
1471 1471 now = currentTimeMillis()
1472 1472 for record in msg.answers:
1473 1473 expired = record.isExpired(now)
1474 1474 if record in self.cache.entries():
1475 1475 if expired:
1476 1476 self.cache.remove(record)
1477 1477 else:
1478 1478 entry = self.cache.get(record)
1479 1479 if entry is not None:
1480 1480 entry.resetTTL(record)
1481 1481 record = entry
1482 1482 else:
1483 1483 self.cache.add(record)
1484 1484
1485 1485 self.updateRecord(now, record)
1486 1486
1487 1487 def handleQuery(self, msg, addr, port):
1488 1488 """Deal with incoming query packets. Provides a response if
1489 1489 possible."""
1490 1490 out = None
1491 1491
1492 1492 # Support unicast client responses
1493 1493 #
1494 1494 if port != _MDNS_PORT:
1495 1495 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1496 1496 for question in msg.questions:
1497 1497 out.addQuestion(question)
1498 1498
1499 1499 for question in msg.questions:
1500 1500 if question.type == _TYPE_PTR:
1501 1501 if question.name == "_services._dns-sd._udp.local.":
1502 1502 for stype in self.servicetypes.keys():
1503 1503 if out is None:
1504 1504 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1505 1505 out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype))
1506 1506 for service in self.services.values():
1507 1507 if question.name == service.type:
1508 1508 if out is None:
1509 1509 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1510 1510 out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
1511 1511 else:
1512 1512 try:
1513 1513 if out is None:
1514 1514 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1515 1515
1516 1516 # Answer A record queries for any service addresses we know
1517 1517 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1518 1518 for service in self.services.values():
1519 1519 if service.server == question.name.lower():
1520 1520 out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1521 1521
1522 1522 service = self.services.get(question.name.lower(), None)
1523 1523 if not service: continue
1524 1524
1525 1525 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1526 1526 out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server))
1527 1527 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1528 1528 out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text))
1529 1529 if question.type == _TYPE_SRV:
1530 1530 out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
1531 1531 except Exception:
1532 1532 traceback.print_exc()
1533 1533
1534 1534 if out is not None and out.answers:
1535 1535 out.id = msg.id
1536 1536 self.send(out, addr, port)
1537 1537
1538 1538 def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
1539 1539 """Sends an outgoing packet."""
1540 1540 # This is a quick test to see if we can parse the packets we generate
1541 1541 #temp = DNSIncoming(out.packet())
1542 1542 try:
1543 1543 self.socket.sendto(out.packet(), 0, (addr, port))
1544 1544 except Exception:
1545 1545 # Ignore this, it may be a temporary loss of network connection
1546 1546 pass
1547 1547
1548 1548 def close(self):
1549 1549 """Ends the background threads, and prevent this instance from
1550 1550 servicing further queries."""
1551 1551 if globals()['_GLOBAL_DONE'] == 0:
1552 1552 globals()['_GLOBAL_DONE'] = 1
1553 1553 self.notifyAll()
1554 1554 self.engine.notify()
1555 1555 self.unregisterAllServices()
1556 1556 self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
1557 1557 self.socket.close()
1558 1558
1559 1559 # Test a few module features, including service registration, service
1560 1560 # query (for Zoe), and service unregistration.
1561 1561
1562 1562 if __name__ == '__main__':
1563 1563 print("Multicast DNS Service Discovery for Python, version", __version__)
1564 1564 r = Zeroconf()
1565 1565 print("1. Testing registration of a service...")
1566 1566 desc = {'version':'0.10','a':'test value', 'b':'another value'}
1567 1567 info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
1568 1568 print(" Registering service...")
1569 1569 r.registerService(info)
1570 1570 print(" Registration done.")
1571 1571 print("2. Testing query of service information...")
1572 1572 print(" Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local.")))
1573 1573 print(" Query done.")
1574 1574 print("3. Testing query of own service...")
1575 1575 print(" Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.")))
1576 1576 print(" Query done.")
1577 1577 print("4. Testing unregister of service information...")
1578 1578 r.unregisterService(info)
1579 1579 print(" Unregister done.")
1580 1580 r.close()
1581 1581
1582 1582 # no-check-code
@@ -1,204 +1,213
1 1 # zeroconf.py - zeroconf support for Mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7
8 7 '''discover and advertise repositories on the local network
9 8
10 9 Zeroconf-enabled repositories will be announced in a network without
11 10 the need to configure a server or a service. They can be discovered
12 11 without knowing their actual IP address.
13 12
14 13 To allow other people to discover your repository using run
15 14 :hg:`serve` in your repository::
16 15
17 16 $ cd test
18 17 $ hg serve
19 18
20 19 You can discover Zeroconf-enabled repositories by running
21 20 :hg:`paths`::
22 21
23 22 $ hg paths
24 23 zc-test = http://example.com:8000/test
25 24 '''
25 from __future__ import absolute_import
26 26
27 import socket, time, os
27 import os
28 import socket
29 import time
28 30
29 import Zeroconf
30 from mercurial import ui, hg, encoding, dispatch
31 from mercurial import extensions
32 from mercurial.hgweb import server as servermod
31 from . import Zeroconf
32 from mercurial import (
33 dispatch,
34 encoding,
35 extensions,
36 hg,
37 ui,
38 )
39 from mercurial.hgweb import (
40 server as servermod
41 )
33 42
34 43 # Note for extension authors: ONLY specify testedwith = 'internal' for
35 44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
36 45 # be specifying the version(s) of Mercurial they are tested with, or
37 46 # leave the attribute unspecified.
38 47 testedwith = 'internal'
39 48
40 49 # publish
41 50
42 51 server = None
43 52 localip = None
44 53
45 54 def getip():
46 55 # finds external-facing interface without sending any packets (Linux)
47 56 try:
48 57 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
49 58 s.connect(('1.0.0.1', 0))
50 59 ip = s.getsockname()[0]
51 60 return ip
52 61 except socket.error:
53 62 pass
54 63
55 64 # Generic method, sometimes gives useless results
56 65 try:
57 66 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
58 67 if not dumbip.startswith('127.') and ':' not in dumbip:
59 68 return dumbip
60 69 except (socket.gaierror, socket.herror):
61 70 dumbip = '127.0.0.1'
62 71
63 72 # works elsewhere, but actually sends a packet
64 73 try:
65 74 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
66 75 s.connect(('1.0.0.1', 1))
67 76 ip = s.getsockname()[0]
68 77 return ip
69 78 except socket.error:
70 79 pass
71 80
72 81 return dumbip
73 82
74 83 def publish(name, desc, path, port):
75 84 global server, localip
76 85 if not server:
77 86 ip = getip()
78 87 if ip.startswith('127.'):
79 88 # if we have no internet connection, this can happen.
80 89 return
81 90 localip = socket.inet_aton(ip)
82 91 server = Zeroconf.Zeroconf(ip)
83 92
84 93 hostname = socket.gethostname().split('.')[0]
85 94 host = hostname + ".local"
86 95 name = "%s-%s" % (hostname, name)
87 96
88 97 # advertise to browsers
89 98 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
90 99 name + '._http._tcp.local.',
91 100 server = host,
92 101 port = port,
93 102 properties = {'description': desc,
94 103 'path': "/" + path},
95 104 address = localip, weight = 0, priority = 0)
96 105 server.registerService(svc)
97 106
98 107 # advertise to Mercurial clients
99 108 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
100 109 name + '._hg._tcp.local.',
101 110 server = host,
102 111 port = port,
103 112 properties = {'description': desc,
104 113 'path': "/" + path},
105 114 address = localip, weight = 0, priority = 0)
106 115 server.registerService(svc)
107 116
108 117 def zc_create_server(create_server, ui, app):
109 118 httpd = create_server(ui, app)
110 119 port = httpd.port
111 120
112 121 try:
113 122 repos = app.repos
114 123 except AttributeError:
115 124 # single repo
116 125 with app._obtainrepo() as repo:
117 126 name = app.reponame or os.path.basename(repo.root)
118 127 path = repo.ui.config("web", "prefix", "").strip('/')
119 128 desc = repo.ui.config("web", "description", name)
120 129 publish(name, desc, path, port)
121 130 else:
122 131 # webdir
123 132 prefix = app.ui.config("web", "prefix", "").strip('/') + '/'
124 133 for repo, path in repos:
125 134 u = app.ui.copy()
126 135 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
127 136 name = os.path.basename(repo)
128 137 path = (prefix + repo).strip('/')
129 138 desc = u.config('web', 'description', name)
130 139 publish(name, desc, path, port)
131 140 return httpd
132 141
133 142 # listen
134 143
135 144 class listener(object):
136 145 def __init__(self):
137 146 self.found = {}
138 147 def removeService(self, server, type, name):
139 148 if repr(name) in self.found:
140 149 del self.found[repr(name)]
141 150 def addService(self, server, type, name):
142 151 self.found[repr(name)] = server.getServiceInfo(type, name)
143 152
144 153 def getzcpaths():
145 154 ip = getip()
146 155 if ip.startswith('127.'):
147 156 return
148 157 server = Zeroconf.Zeroconf(ip)
149 158 l = listener()
150 159 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
151 160 time.sleep(1)
152 161 server.close()
153 162 for value in l.found.values():
154 163 name = value.name[:value.name.index('.')]
155 164 url = "http://%s:%s%s" % (socket.inet_ntoa(value.address), value.port,
156 165 value.properties.get("path", "/"))
157 166 yield "zc-" + name, url
158 167
159 168 def config(orig, self, section, key, default=None, untrusted=False):
160 169 if section == "paths" and key.startswith("zc-"):
161 170 for name, path in getzcpaths():
162 171 if name == key:
163 172 return path
164 173 return orig(self, section, key, default, untrusted)
165 174
166 175 def configitems(orig, self, section, *args, **kwargs):
167 176 repos = orig(self, section, *args, **kwargs)
168 177 if section == "paths":
169 178 repos += getzcpaths()
170 179 return repos
171 180
172 181 def configsuboptions(orig, self, section, name, *args, **kwargs):
173 182 opt, sub = orig(self, section, name, *args, **kwargs)
174 183 if section == "paths" and name.startswith("zc-"):
175 184 # We have to find the URL in the zeroconf paths. We can't cons up any
176 185 # suboptions, so we use any that we found in the original config.
177 186 for zcname, zcurl in getzcpaths():
178 187 if zcname == name:
179 188 return zcurl, sub
180 189 return opt, sub
181 190
182 191 def defaultdest(orig, source):
183 192 for name, path in getzcpaths():
184 193 if path == source:
185 194 return name.encode(encoding.encoding)
186 195 return orig(source)
187 196
188 197 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
189 198 try:
190 199 return orig(ui, options, cmd, cmdfunc)
191 200 finally:
192 201 # we need to call close() on the server to notify() the various
193 202 # threading Conditions and allow the background threads to exit
194 203 global server
195 204 if server:
196 205 server.close()
197 206
198 207 extensions.wrapfunction(dispatch, '_runcommand', cleanupafterdispatch)
199 208
200 209 extensions.wrapfunction(ui.ui, 'config', config)
201 210 extensions.wrapfunction(ui.ui, 'configitems', configitems)
202 211 extensions.wrapfunction(ui.ui, 'configsuboptions', configsuboptions)
203 212 extensions.wrapfunction(hg, 'defaultdest', defaultdest)
204 213 extensions.wrapfunction(servermod, 'create_server', zc_create_server)
@@ -1,173 +1,171
1 1 #require test-repo
2 2
3 3 $ cd "$TESTDIR"/..
4 4
5 5 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
6 6 contrib/casesmash.py not using absolute_import
7 7 contrib/check-code.py not using absolute_import
8 8 contrib/check-code.py requires print_function
9 9 contrib/check-config.py not using absolute_import
10 10 contrib/check-config.py requires print_function
11 11 contrib/debugcmdserver.py not using absolute_import
12 12 contrib/debugcmdserver.py requires print_function
13 13 contrib/debugshell.py not using absolute_import
14 14 contrib/fixpax.py not using absolute_import
15 15 contrib/fixpax.py requires print_function
16 16 contrib/hgclient.py not using absolute_import
17 17 contrib/hgclient.py requires print_function
18 18 contrib/hgfixes/fix_bytes.py not using absolute_import
19 19 contrib/hgfixes/fix_bytesmod.py not using absolute_import
20 20 contrib/hgfixes/fix_leftover_imports.py not using absolute_import
21 21 contrib/import-checker.py not using absolute_import
22 22 contrib/import-checker.py requires print_function
23 23 contrib/memory.py not using absolute_import
24 24 contrib/perf.py not using absolute_import
25 25 contrib/python-hook-examples.py not using absolute_import
26 26 contrib/revsetbenchmarks.py not using absolute_import
27 27 contrib/revsetbenchmarks.py requires print_function
28 28 contrib/showstack.py not using absolute_import
29 29 contrib/synthrepo.py not using absolute_import
30 30 contrib/win32/hgwebdir_wsgi.py not using absolute_import
31 31 doc/check-seclevel.py not using absolute_import
32 32 doc/gendoc.py not using absolute_import
33 33 doc/hgmanpage.py not using absolute_import
34 34 hgext/__init__.py not using absolute_import
35 35 hgext/color.py not using absolute_import
36 36 hgext/convert/__init__.py not using absolute_import
37 37 hgext/convert/bzr.py not using absolute_import
38 38 hgext/convert/common.py not using absolute_import
39 39 hgext/convert/convcmd.py not using absolute_import
40 40 hgext/convert/cvs.py not using absolute_import
41 41 hgext/convert/cvsps.py not using absolute_import
42 42 hgext/convert/darcs.py not using absolute_import
43 43 hgext/convert/filemap.py not using absolute_import
44 44 hgext/convert/git.py not using absolute_import
45 45 hgext/convert/gnuarch.py not using absolute_import
46 46 hgext/convert/hg.py not using absolute_import
47 47 hgext/convert/monotone.py not using absolute_import
48 48 hgext/convert/p4.py not using absolute_import
49 49 hgext/convert/subversion.py not using absolute_import
50 50 hgext/convert/transport.py not using absolute_import
51 51 hgext/eol.py not using absolute_import
52 52 hgext/extdiff.py not using absolute_import
53 53 hgext/factotum.py not using absolute_import
54 54 hgext/fetch.py not using absolute_import
55 55 hgext/gpg.py not using absolute_import
56 56 hgext/graphlog.py not using absolute_import
57 57 hgext/hgcia.py not using absolute_import
58 58 hgext/hgk.py not using absolute_import
59 59 hgext/highlight/__init__.py not using absolute_import
60 60 hgext/highlight/highlight.py not using absolute_import
61 61 hgext/histedit.py not using absolute_import
62 62 hgext/keyword.py not using absolute_import
63 63 hgext/largefiles/__init__.py not using absolute_import
64 64 hgext/largefiles/basestore.py not using absolute_import
65 65 hgext/largefiles/lfcommands.py not using absolute_import
66 66 hgext/largefiles/lfutil.py not using absolute_import
67 67 hgext/largefiles/localstore.py not using absolute_import
68 68 hgext/largefiles/overrides.py not using absolute_import
69 69 hgext/largefiles/proto.py not using absolute_import
70 70 hgext/largefiles/remotestore.py not using absolute_import
71 71 hgext/largefiles/reposetup.py not using absolute_import
72 72 hgext/largefiles/uisetup.py not using absolute_import
73 73 hgext/largefiles/wirestore.py not using absolute_import
74 74 hgext/mq.py not using absolute_import
75 75 hgext/notify.py not using absolute_import
76 76 hgext/pager.py not using absolute_import
77 77 hgext/patchbomb.py not using absolute_import
78 78 hgext/purge.py not using absolute_import
79 79 hgext/rebase.py not using absolute_import
80 80 hgext/record.py not using absolute_import
81 81 hgext/relink.py not using absolute_import
82 82 hgext/schemes.py not using absolute_import
83 83 hgext/share.py not using absolute_import
84 84 hgext/shelve.py not using absolute_import
85 85 hgext/strip.py not using absolute_import
86 86 hgext/transplant.py not using absolute_import
87 87 hgext/win32mbcs.py not using absolute_import
88 88 hgext/win32text.py not using absolute_import
89 hgext/zeroconf/Zeroconf.py not using absolute_import
90 hgext/zeroconf/__init__.py not using absolute_import
91 89 i18n/check-translation.py not using absolute_import
92 90 i18n/polib.py not using absolute_import
93 91 mercurial/cmdutil.py not using absolute_import
94 92 mercurial/commands.py not using absolute_import
95 93 setup.py not using absolute_import
96 94 tests/filterpyflakes.py requires print_function
97 95 tests/generate-working-copy-states.py requires print_function
98 96 tests/get-with-headers.py requires print_function
99 97 tests/heredoctest.py requires print_function
100 98 tests/hypothesishelpers.py not using absolute_import
101 99 tests/hypothesishelpers.py requires print_function
102 100 tests/killdaemons.py not using absolute_import
103 101 tests/md5sum.py not using absolute_import
104 102 tests/mockblackbox.py not using absolute_import
105 103 tests/printenv.py not using absolute_import
106 104 tests/readlink.py not using absolute_import
107 105 tests/readlink.py requires print_function
108 106 tests/revlog-formatv0.py not using absolute_import
109 107 tests/run-tests.py not using absolute_import
110 108 tests/seq.py not using absolute_import
111 109 tests/seq.py requires print_function
112 110 tests/silenttestrunner.py not using absolute_import
113 111 tests/silenttestrunner.py requires print_function
114 112 tests/sitecustomize.py not using absolute_import
115 113 tests/svn-safe-append.py not using absolute_import
116 114 tests/svnxml.py not using absolute_import
117 115 tests/test-ancestor.py requires print_function
118 116 tests/test-atomictempfile.py not using absolute_import
119 117 tests/test-batching.py not using absolute_import
120 118 tests/test-batching.py requires print_function
121 119 tests/test-bdiff.py not using absolute_import
122 120 tests/test-bdiff.py requires print_function
123 121 tests/test-context.py not using absolute_import
124 122 tests/test-context.py requires print_function
125 123 tests/test-demandimport.py not using absolute_import
126 124 tests/test-demandimport.py requires print_function
127 125 tests/test-dispatch.py not using absolute_import
128 126 tests/test-dispatch.py requires print_function
129 127 tests/test-doctest.py not using absolute_import
130 128 tests/test-duplicateoptions.py not using absolute_import
131 129 tests/test-duplicateoptions.py requires print_function
132 130 tests/test-filecache.py not using absolute_import
133 131 tests/test-filecache.py requires print_function
134 132 tests/test-filelog.py not using absolute_import
135 133 tests/test-filelog.py requires print_function
136 134 tests/test-hg-parseurl.py not using absolute_import
137 135 tests/test-hg-parseurl.py requires print_function
138 136 tests/test-hgweb-auth.py not using absolute_import
139 137 tests/test-hgweb-auth.py requires print_function
140 138 tests/test-hgwebdir-paths.py not using absolute_import
141 139 tests/test-hybridencode.py not using absolute_import
142 140 tests/test-hybridencode.py requires print_function
143 141 tests/test-lrucachedict.py not using absolute_import
144 142 tests/test-lrucachedict.py requires print_function
145 143 tests/test-manifest.py not using absolute_import
146 144 tests/test-minirst.py not using absolute_import
147 145 tests/test-minirst.py requires print_function
148 146 tests/test-parseindex2.py not using absolute_import
149 147 tests/test-parseindex2.py requires print_function
150 148 tests/test-pathencode.py not using absolute_import
151 149 tests/test-pathencode.py requires print_function
152 150 tests/test-propertycache.py not using absolute_import
153 151 tests/test-propertycache.py requires print_function
154 152 tests/test-revlog-ancestry.py not using absolute_import
155 153 tests/test-revlog-ancestry.py requires print_function
156 154 tests/test-run-tests.py not using absolute_import
157 155 tests/test-simplemerge.py not using absolute_import
158 156 tests/test-status-inprocess.py not using absolute_import
159 157 tests/test-status-inprocess.py requires print_function
160 158 tests/test-symlink-os-yes-fs-no.py not using absolute_import
161 159 tests/test-trusted.py not using absolute_import
162 160 tests/test-trusted.py requires print_function
163 161 tests/test-ui-color.py not using absolute_import
164 162 tests/test-ui-color.py requires print_function
165 163 tests/test-ui-config.py not using absolute_import
166 164 tests/test-ui-config.py requires print_function
167 165 tests/test-ui-verbosity.py not using absolute_import
168 166 tests/test-ui-verbosity.py requires print_function
169 167 tests/test-url.py not using absolute_import
170 168 tests/test-url.py requires print_function
171 169 tests/test-walkrepo.py requires print_function
172 170 tests/test-wireproto.py requires print_function
173 171 tests/tinyproxy.py requires print_function
General Comments 0
You need to be logged in to leave comments. Login now