##// END OF EJS Templates
zeroconf: fail nicely on IPv6 only system...
Simon Farnsworth -
r30885:564a96a5 default
parent child Browse files
Show More
@@ -1,213 +1,215
1 # zeroconf.py - zeroconf support for Mercurial
1 # zeroconf.py - zeroconf support 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 '''discover and advertise repositories on the local network
7 '''discover and advertise repositories on the local network
8
8
9 Zeroconf-enabled repositories will be announced in a network without
9 Zeroconf-enabled repositories will be announced in a network without
10 the need to configure a server or a service. They can be discovered
10 the need to configure a server or a service. They can be discovered
11 without knowing their actual IP address.
11 without knowing their actual IP address.
12
12
13 To allow other people to discover your repository using run
13 To allow other people to discover your repository using run
14 :hg:`serve` in your repository::
14 :hg:`serve` in your repository::
15
15
16 $ cd test
16 $ cd test
17 $ hg serve
17 $ hg serve
18
18
19 You can discover Zeroconf-enabled repositories by running
19 You can discover Zeroconf-enabled repositories by running
20 :hg:`paths`::
20 :hg:`paths`::
21
21
22 $ hg paths
22 $ hg paths
23 zc-test = http://example.com:8000/test
23 zc-test = http://example.com:8000/test
24 '''
24 '''
25 from __future__ import absolute_import
25 from __future__ import absolute_import
26
26
27 import os
27 import os
28 import socket
28 import socket
29 import time
29 import time
30
30
31 from . import Zeroconf
31 from . import Zeroconf
32 from mercurial import (
32 from mercurial import (
33 dispatch,
33 dispatch,
34 encoding,
34 encoding,
35 extensions,
35 extensions,
36 hg,
36 hg,
37 ui as uimod,
37 ui as uimod,
38 )
38 )
39 from mercurial.hgweb import (
39 from mercurial.hgweb import (
40 server as servermod
40 server as servermod
41 )
41 )
42
42
43 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
43 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
45 # be specifying the version(s) of Mercurial they are tested with, or
45 # be specifying the version(s) of Mercurial they are tested with, or
46 # leave the attribute unspecified.
46 # leave the attribute unspecified.
47 testedwith = 'ships-with-hg-core'
47 testedwith = 'ships-with-hg-core'
48
48
49 # publish
49 # publish
50
50
51 server = None
51 server = None
52 localip = None
52 localip = None
53
53
54 def getip():
54 def getip():
55 # finds external-facing interface without sending any packets (Linux)
55 # finds external-facing interface without sending any packets (Linux)
56 try:
56 try:
57 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
57 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
58 s.connect(('1.0.0.1', 0))
58 s.connect(('1.0.0.1', 0))
59 ip = s.getsockname()[0]
59 ip = s.getsockname()[0]
60 return ip
60 return ip
61 except socket.error:
61 except socket.error:
62 pass
62 pass
63
63
64 # Generic method, sometimes gives useless results
64 # Generic method, sometimes gives useless results
65 try:
65 try:
66 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
66 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
67 if not dumbip.startswith('127.') and ':' not in dumbip:
67 if ':' in dumbip:
68 dumbip = '127.0.0.1'
69 if not dumbip.startswith('127.'):
68 return dumbip
70 return dumbip
69 except (socket.gaierror, socket.herror):
71 except (socket.gaierror, socket.herror):
70 dumbip = '127.0.0.1'
72 dumbip = '127.0.0.1'
71
73
72 # works elsewhere, but actually sends a packet
74 # works elsewhere, but actually sends a packet
73 try:
75 try:
74 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
76 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
75 s.connect(('1.0.0.1', 1))
77 s.connect(('1.0.0.1', 1))
76 ip = s.getsockname()[0]
78 ip = s.getsockname()[0]
77 return ip
79 return ip
78 except socket.error:
80 except socket.error:
79 pass
81 pass
80
82
81 return dumbip
83 return dumbip
82
84
83 def publish(name, desc, path, port):
85 def publish(name, desc, path, port):
84 global server, localip
86 global server, localip
85 if not server:
87 if not server:
86 ip = getip()
88 ip = getip()
87 if ip.startswith('127.'):
89 if ip.startswith('127.'):
88 # if we have no internet connection, this can happen.
90 # if we have no internet connection, this can happen.
89 return
91 return
90 localip = socket.inet_aton(ip)
92 localip = socket.inet_aton(ip)
91 server = Zeroconf.Zeroconf(ip)
93 server = Zeroconf.Zeroconf(ip)
92
94
93 hostname = socket.gethostname().split('.')[0]
95 hostname = socket.gethostname().split('.')[0]
94 host = hostname + ".local"
96 host = hostname + ".local"
95 name = "%s-%s" % (hostname, name)
97 name = "%s-%s" % (hostname, name)
96
98
97 # advertise to browsers
99 # advertise to browsers
98 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
100 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
99 name + '._http._tcp.local.',
101 name + '._http._tcp.local.',
100 server = host,
102 server = host,
101 port = port,
103 port = port,
102 properties = {'description': desc,
104 properties = {'description': desc,
103 'path': "/" + path},
105 'path': "/" + path},
104 address = localip, weight = 0, priority = 0)
106 address = localip, weight = 0, priority = 0)
105 server.registerService(svc)
107 server.registerService(svc)
106
108
107 # advertise to Mercurial clients
109 # advertise to Mercurial clients
108 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
110 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
109 name + '._hg._tcp.local.',
111 name + '._hg._tcp.local.',
110 server = host,
112 server = host,
111 port = port,
113 port = port,
112 properties = {'description': desc,
114 properties = {'description': desc,
113 'path': "/" + path},
115 'path': "/" + path},
114 address = localip, weight = 0, priority = 0)
116 address = localip, weight = 0, priority = 0)
115 server.registerService(svc)
117 server.registerService(svc)
116
118
117 def zc_create_server(create_server, ui, app):
119 def zc_create_server(create_server, ui, app):
118 httpd = create_server(ui, app)
120 httpd = create_server(ui, app)
119 port = httpd.port
121 port = httpd.port
120
122
121 try:
123 try:
122 repos = app.repos
124 repos = app.repos
123 except AttributeError:
125 except AttributeError:
124 # single repo
126 # single repo
125 with app._obtainrepo() as repo:
127 with app._obtainrepo() as repo:
126 name = app.reponame or os.path.basename(repo.root)
128 name = app.reponame or os.path.basename(repo.root)
127 path = repo.ui.config("web", "prefix", "").strip('/')
129 path = repo.ui.config("web", "prefix", "").strip('/')
128 desc = repo.ui.config("web", "description", name)
130 desc = repo.ui.config("web", "description", name)
129 publish(name, desc, path, port)
131 publish(name, desc, path, port)
130 else:
132 else:
131 # webdir
133 # webdir
132 prefix = app.ui.config("web", "prefix", "").strip('/') + '/'
134 prefix = app.ui.config("web", "prefix", "").strip('/') + '/'
133 for repo, path in repos:
135 for repo, path in repos:
134 u = app.ui.copy()
136 u = app.ui.copy()
135 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
137 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
136 name = os.path.basename(repo)
138 name = os.path.basename(repo)
137 path = (prefix + repo).strip('/')
139 path = (prefix + repo).strip('/')
138 desc = u.config('web', 'description', name)
140 desc = u.config('web', 'description', name)
139 publish(name, desc, path, port)
141 publish(name, desc, path, port)
140 return httpd
142 return httpd
141
143
142 # listen
144 # listen
143
145
144 class listener(object):
146 class listener(object):
145 def __init__(self):
147 def __init__(self):
146 self.found = {}
148 self.found = {}
147 def removeService(self, server, type, name):
149 def removeService(self, server, type, name):
148 if repr(name) in self.found:
150 if repr(name) in self.found:
149 del self.found[repr(name)]
151 del self.found[repr(name)]
150 def addService(self, server, type, name):
152 def addService(self, server, type, name):
151 self.found[repr(name)] = server.getServiceInfo(type, name)
153 self.found[repr(name)] = server.getServiceInfo(type, name)
152
154
153 def getzcpaths():
155 def getzcpaths():
154 ip = getip()
156 ip = getip()
155 if ip.startswith('127.'):
157 if ip.startswith('127.'):
156 return
158 return
157 server = Zeroconf.Zeroconf(ip)
159 server = Zeroconf.Zeroconf(ip)
158 l = listener()
160 l = listener()
159 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
161 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
160 time.sleep(1)
162 time.sleep(1)
161 server.close()
163 server.close()
162 for value in l.found.values():
164 for value in l.found.values():
163 name = value.name[:value.name.index('.')]
165 name = value.name[:value.name.index('.')]
164 url = "http://%s:%s%s" % (socket.inet_ntoa(value.address), value.port,
166 url = "http://%s:%s%s" % (socket.inet_ntoa(value.address), value.port,
165 value.properties.get("path", "/"))
167 value.properties.get("path", "/"))
166 yield "zc-" + name, url
168 yield "zc-" + name, url
167
169
168 def config(orig, self, section, key, default=None, untrusted=False):
170 def config(orig, self, section, key, default=None, untrusted=False):
169 if section == "paths" and key.startswith("zc-"):
171 if section == "paths" and key.startswith("zc-"):
170 for name, path in getzcpaths():
172 for name, path in getzcpaths():
171 if name == key:
173 if name == key:
172 return path
174 return path
173 return orig(self, section, key, default, untrusted)
175 return orig(self, section, key, default, untrusted)
174
176
175 def configitems(orig, self, section, *args, **kwargs):
177 def configitems(orig, self, section, *args, **kwargs):
176 repos = orig(self, section, *args, **kwargs)
178 repos = orig(self, section, *args, **kwargs)
177 if section == "paths":
179 if section == "paths":
178 repos += getzcpaths()
180 repos += getzcpaths()
179 return repos
181 return repos
180
182
181 def configsuboptions(orig, self, section, name, *args, **kwargs):
183 def configsuboptions(orig, self, section, name, *args, **kwargs):
182 opt, sub = orig(self, section, name, *args, **kwargs)
184 opt, sub = orig(self, section, name, *args, **kwargs)
183 if section == "paths" and name.startswith("zc-"):
185 if section == "paths" and name.startswith("zc-"):
184 # We have to find the URL in the zeroconf paths. We can't cons up any
186 # We have to find the URL in the zeroconf paths. We can't cons up any
185 # suboptions, so we use any that we found in the original config.
187 # suboptions, so we use any that we found in the original config.
186 for zcname, zcurl in getzcpaths():
188 for zcname, zcurl in getzcpaths():
187 if zcname == name:
189 if zcname == name:
188 return zcurl, sub
190 return zcurl, sub
189 return opt, sub
191 return opt, sub
190
192
191 def defaultdest(orig, source):
193 def defaultdest(orig, source):
192 for name, path in getzcpaths():
194 for name, path in getzcpaths():
193 if path == source:
195 if path == source:
194 return name.encode(encoding.encoding)
196 return name.encode(encoding.encoding)
195 return orig(source)
197 return orig(source)
196
198
197 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
199 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
198 try:
200 try:
199 return orig(ui, options, cmd, cmdfunc)
201 return orig(ui, options, cmd, cmdfunc)
200 finally:
202 finally:
201 # we need to call close() on the server to notify() the various
203 # we need to call close() on the server to notify() the various
202 # threading Conditions and allow the background threads to exit
204 # threading Conditions and allow the background threads to exit
203 global server
205 global server
204 if server:
206 if server:
205 server.close()
207 server.close()
206
208
207 extensions.wrapfunction(dispatch, '_runcommand', cleanupafterdispatch)
209 extensions.wrapfunction(dispatch, '_runcommand', cleanupafterdispatch)
208
210
209 extensions.wrapfunction(uimod.ui, 'config', config)
211 extensions.wrapfunction(uimod.ui, 'config', config)
210 extensions.wrapfunction(uimod.ui, 'configitems', configitems)
212 extensions.wrapfunction(uimod.ui, 'configitems', configitems)
211 extensions.wrapfunction(uimod.ui, 'configsuboptions', configsuboptions)
213 extensions.wrapfunction(uimod.ui, 'configsuboptions', configsuboptions)
212 extensions.wrapfunction(hg, 'defaultdest', defaultdest)
214 extensions.wrapfunction(hg, 'defaultdest', defaultdest)
213 extensions.wrapfunction(servermod, 'create_server', zc_create_server)
215 extensions.wrapfunction(servermod, 'create_server', zc_create_server)
General Comments 0
You need to be logged in to leave comments. Login now