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