##// END OF EJS Templates
zeroconf: improve the extension's documentation...
Vernon Tang -
r43806:6337b10c default
parent child Browse files
Show More
@@ -1,241 +1,241 b''
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 The zeroconf extension will advertise :hg:`serve` instances over
10 the need to configure a server or a service. They can be discovered
10 DNS-SD so that they can be discovered using the :hg:`paths` command
11 without knowing their actual IP address.
11 without knowing the server's 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 pycompat,
37 pycompat,
38 ui as uimod,
38 ui as uimod,
39 )
39 )
40 from mercurial.hgweb import server as servermod
40 from mercurial.hgweb import server as servermod
41
41
42 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
42 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
43 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
43 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
44 # be specifying the version(s) of Mercurial they are tested with, or
44 # be specifying the version(s) of Mercurial they are tested with, or
45 # leave the attribute unspecified.
45 # leave the attribute unspecified.
46 testedwith = b'ships-with-hg-core'
46 testedwith = b'ships-with-hg-core'
47
47
48 # publish
48 # publish
49
49
50 server = None
50 server = None
51 localip = None
51 localip = None
52
52
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((r'1.0.0.1', 0))
58 s.connect((r'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 r':' in dumbip:
67 if r':' in dumbip:
68 dumbip = r'127.0.0.1'
68 dumbip = r'127.0.0.1'
69 if not dumbip.startswith(r'127.'):
69 if not dumbip.startswith(r'127.'):
70 return dumbip
70 return dumbip
71 except (socket.gaierror, socket.herror):
71 except (socket.gaierror, socket.herror):
72 dumbip = r'127.0.0.1'
72 dumbip = r'127.0.0.1'
73
73
74 # works elsewhere, but actually sends a packet
74 # works elsewhere, but actually sends a packet
75 try:
75 try:
76 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
76 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
77 s.connect((r'1.0.0.1', 1))
77 s.connect((r'1.0.0.1', 1))
78 ip = s.getsockname()[0]
78 ip = s.getsockname()[0]
79 return ip
79 return ip
80 except socket.error:
80 except socket.error:
81 pass
81 pass
82
82
83 return dumbip
83 return dumbip
84
84
85
85
86 def publish(name, desc, path, port):
86 def publish(name, desc, path, port):
87 global server, localip
87 global server, localip
88 if not server:
88 if not server:
89 ip = getip()
89 ip = getip()
90 if ip.startswith(r'127.'):
90 if ip.startswith(r'127.'):
91 # if we have no internet connection, this can happen.
91 # if we have no internet connection, this can happen.
92 return
92 return
93 localip = socket.inet_aton(ip)
93 localip = socket.inet_aton(ip)
94 server = Zeroconf.Zeroconf(ip)
94 server = Zeroconf.Zeroconf(ip)
95
95
96 hostname = socket.gethostname().split(r'.')[0]
96 hostname = socket.gethostname().split(r'.')[0]
97 host = hostname + r".local"
97 host = hostname + r".local"
98 name = r"%s-%s" % (hostname, name)
98 name = r"%s-%s" % (hostname, name)
99
99
100 # advertise to browsers
100 # advertise to browsers
101 svc = Zeroconf.ServiceInfo(
101 svc = Zeroconf.ServiceInfo(
102 b'_http._tcp.local.',
102 b'_http._tcp.local.',
103 pycompat.bytestr(name + r'._http._tcp.local.'),
103 pycompat.bytestr(name + r'._http._tcp.local.'),
104 server=host,
104 server=host,
105 port=port,
105 port=port,
106 properties={b'description': desc, b'path': b"/" + path},
106 properties={b'description': desc, b'path': b"/" + path},
107 address=localip,
107 address=localip,
108 weight=0,
108 weight=0,
109 priority=0,
109 priority=0,
110 )
110 )
111 server.registerService(svc)
111 server.registerService(svc)
112
112
113 # advertise to Mercurial clients
113 # advertise to Mercurial clients
114 svc = Zeroconf.ServiceInfo(
114 svc = Zeroconf.ServiceInfo(
115 b'_hg._tcp.local.',
115 b'_hg._tcp.local.',
116 pycompat.bytestr(name + r'._hg._tcp.local.'),
116 pycompat.bytestr(name + r'._hg._tcp.local.'),
117 server=host,
117 server=host,
118 port=port,
118 port=port,
119 properties={b'description': desc, b'path': b"/" + path},
119 properties={b'description': desc, b'path': b"/" + path},
120 address=localip,
120 address=localip,
121 weight=0,
121 weight=0,
122 priority=0,
122 priority=0,
123 )
123 )
124 server.registerService(svc)
124 server.registerService(svc)
125
125
126
126
127 def zc_create_server(create_server, ui, app):
127 def zc_create_server(create_server, ui, app):
128 httpd = create_server(ui, app)
128 httpd = create_server(ui, app)
129 port = httpd.port
129 port = httpd.port
130
130
131 try:
131 try:
132 repos = app.repos
132 repos = app.repos
133 except AttributeError:
133 except AttributeError:
134 # single repo
134 # single repo
135 with app._obtainrepo() as repo:
135 with app._obtainrepo() as repo:
136 name = app.reponame or os.path.basename(repo.root)
136 name = app.reponame or os.path.basename(repo.root)
137 path = repo.ui.config(b"web", b"prefix", b"").strip(b'/')
137 path = repo.ui.config(b"web", b"prefix", b"").strip(b'/')
138 desc = repo.ui.config(b"web", b"description")
138 desc = repo.ui.config(b"web", b"description")
139 if not desc:
139 if not desc:
140 desc = name
140 desc = name
141 publish(name, desc, path, port)
141 publish(name, desc, path, port)
142 else:
142 else:
143 # webdir
143 # webdir
144 prefix = app.ui.config(b"web", b"prefix", b"").strip(b'/') + b'/'
144 prefix = app.ui.config(b"web", b"prefix", b"").strip(b'/') + b'/'
145 for repo, path in repos:
145 for repo, path in repos:
146 u = app.ui.copy()
146 u = app.ui.copy()
147 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
147 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
148 name = os.path.basename(repo)
148 name = os.path.basename(repo)
149 path = (prefix + repo).strip(b'/')
149 path = (prefix + repo).strip(b'/')
150 desc = u.config(b'web', b'description')
150 desc = u.config(b'web', b'description')
151 if not desc:
151 if not desc:
152 desc = name
152 desc = name
153 publish(name, desc, path, port)
153 publish(name, desc, path, port)
154 return httpd
154 return httpd
155
155
156
156
157 # listen
157 # listen
158
158
159
159
160 class listener(object):
160 class listener(object):
161 def __init__(self):
161 def __init__(self):
162 self.found = {}
162 self.found = {}
163
163
164 def removeService(self, server, type, name):
164 def removeService(self, server, type, name):
165 if repr(name) in self.found:
165 if repr(name) in self.found:
166 del self.found[repr(name)]
166 del self.found[repr(name)]
167
167
168 def addService(self, server, type, name):
168 def addService(self, server, type, name):
169 self.found[repr(name)] = server.getServiceInfo(type, name)
169 self.found[repr(name)] = server.getServiceInfo(type, name)
170
170
171
171
172 def getzcpaths():
172 def getzcpaths():
173 ip = getip()
173 ip = getip()
174 if ip.startswith(r'127.'):
174 if ip.startswith(r'127.'):
175 return
175 return
176 server = Zeroconf.Zeroconf(ip)
176 server = Zeroconf.Zeroconf(ip)
177 l = listener()
177 l = listener()
178 Zeroconf.ServiceBrowser(server, b"_hg._tcp.local.", l)
178 Zeroconf.ServiceBrowser(server, b"_hg._tcp.local.", l)
179 time.sleep(1)
179 time.sleep(1)
180 server.close()
180 server.close()
181 for value in l.found.values():
181 for value in l.found.values():
182 name = value.name[: value.name.index(b'.')]
182 name = value.name[: value.name.index(b'.')]
183 url = r"http://%s:%s%s" % (
183 url = r"http://%s:%s%s" % (
184 socket.inet_ntoa(value.address),
184 socket.inet_ntoa(value.address),
185 value.port,
185 value.port,
186 value.properties.get(r"path", r"/"),
186 value.properties.get(r"path", r"/"),
187 )
187 )
188 yield b"zc-" + name, pycompat.bytestr(url)
188 yield b"zc-" + name, pycompat.bytestr(url)
189
189
190
190
191 def config(orig, self, section, key, *args, **kwargs):
191 def config(orig, self, section, key, *args, **kwargs):
192 if section == b"paths" and key.startswith(b"zc-"):
192 if section == b"paths" and key.startswith(b"zc-"):
193 for name, path in getzcpaths():
193 for name, path in getzcpaths():
194 if name == key:
194 if name == key:
195 return path
195 return path
196 return orig(self, section, key, *args, **kwargs)
196 return orig(self, section, key, *args, **kwargs)
197
197
198
198
199 def configitems(orig, self, section, *args, **kwargs):
199 def configitems(orig, self, section, *args, **kwargs):
200 repos = orig(self, section, *args, **kwargs)
200 repos = orig(self, section, *args, **kwargs)
201 if section == b"paths":
201 if section == b"paths":
202 repos += getzcpaths()
202 repos += getzcpaths()
203 return repos
203 return repos
204
204
205
205
206 def configsuboptions(orig, self, section, name, *args, **kwargs):
206 def configsuboptions(orig, self, section, name, *args, **kwargs):
207 opt, sub = orig(self, section, name, *args, **kwargs)
207 opt, sub = orig(self, section, name, *args, **kwargs)
208 if section == b"paths" and name.startswith(b"zc-"):
208 if section == b"paths" and name.startswith(b"zc-"):
209 # We have to find the URL in the zeroconf paths. We can't cons up any
209 # We have to find the URL in the zeroconf paths. We can't cons up any
210 # suboptions, so we use any that we found in the original config.
210 # suboptions, so we use any that we found in the original config.
211 for zcname, zcurl in getzcpaths():
211 for zcname, zcurl in getzcpaths():
212 if zcname == name:
212 if zcname == name:
213 return zcurl, sub
213 return zcurl, sub
214 return opt, sub
214 return opt, sub
215
215
216
216
217 def defaultdest(orig, source):
217 def defaultdest(orig, source):
218 for name, path in getzcpaths():
218 for name, path in getzcpaths():
219 if path == source:
219 if path == source:
220 return name.encode(encoding.encoding)
220 return name.encode(encoding.encoding)
221 return orig(source)
221 return orig(source)
222
222
223
223
224 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
224 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
225 try:
225 try:
226 return orig(ui, options, cmd, cmdfunc)
226 return orig(ui, options, cmd, cmdfunc)
227 finally:
227 finally:
228 # we need to call close() on the server to notify() the various
228 # we need to call close() on the server to notify() the various
229 # threading Conditions and allow the background threads to exit
229 # threading Conditions and allow the background threads to exit
230 global server
230 global server
231 if server:
231 if server:
232 server.close()
232 server.close()
233
233
234
234
235 extensions.wrapfunction(dispatch, b'_runcommand', cleanupafterdispatch)
235 extensions.wrapfunction(dispatch, b'_runcommand', cleanupafterdispatch)
236
236
237 extensions.wrapfunction(uimod.ui, b'config', config)
237 extensions.wrapfunction(uimod.ui, b'config', config)
238 extensions.wrapfunction(uimod.ui, b'configitems', configitems)
238 extensions.wrapfunction(uimod.ui, b'configitems', configitems)
239 extensions.wrapfunction(uimod.ui, b'configsuboptions', configsuboptions)
239 extensions.wrapfunction(uimod.ui, b'configsuboptions', configsuboptions)
240 extensions.wrapfunction(hg, b'defaultdest', defaultdest)
240 extensions.wrapfunction(hg, b'defaultdest', defaultdest)
241 extensions.wrapfunction(servermod, b'create_server', zc_create_server)
241 extensions.wrapfunction(servermod, b'create_server', zc_create_server)
General Comments 0
You need to be logged in to leave comments. Login now