__init__.py
244 lines
| 6.9 KiB
| text/x-python
|
PythonLexer
Matt Mackall
|
r7071 | # zeroconf.py - zeroconf support for Mercurial | ||
# | ||||
Raphaël Gomès
|
r47575 | # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> | ||
Matt Mackall
|
r7071 | # | ||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Cédric Duval
|
r8894 | '''discover and advertise repositories on the local network | ||
David Soria Parra
|
r7606 | |||
Vernon Tang
|
r43806 | The zeroconf extension will advertise :hg:`serve` instances over | ||
DNS-SD so that they can be discovered using the :hg:`paths` command | ||||
without knowing the server's IP address. | ||||
David Soria Parra
|
r7606 | |||
Martin Geisler
|
r11504 | To allow other people to discover your repository using run | ||
:hg:`serve` in your repository:: | ||||
David Soria Parra
|
r7606 | |||
Martin Geisler
|
r9218 | $ cd test | ||
$ hg serve | ||||
David Soria Parra
|
r7606 | |||
Martin Geisler
|
r11504 | You can discover Zeroconf-enabled repositories by running | ||
:hg:`paths`:: | ||||
David Soria Parra
|
r7606 | |||
Martin Geisler
|
r9218 | $ hg paths | ||
zc-test = http://example.com:8000/test | ||||
David Soria Parra
|
r7606 | ''' | ||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
timeless
|
r28296 | import os | ||
import socket | ||||
import time | ||||
Renato Cunha
|
r11340 | |||
timeless
|
r28296 | from . import Zeroconf | ||
from mercurial import ( | ||||
dispatch, | ||||
encoding, | ||||
extensions, | ||||
hg, | ||||
Gregory Szorc
|
r42752 | pycompat, | ||
r44730 | rcutil, | |||
Augie Fackler
|
r28308 | ui as uimod, | ||
timeless
|
r28296 | ) | ||
Augie Fackler
|
r43346 | from mercurial.hgweb import server as servermod | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
Augie Fackler
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
Augie Fackler
|
r43347 | testedwith = b'ships-with-hg-core' | ||
Augie Fackler
|
r16743 | |||
Matt Mackall
|
r7071 | # publish | ||
server = None | ||||
localip = None | ||||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | def getip(): | ||
# finds external-facing interface without sending any packets (Linux) | ||||
try: | ||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||||
Augie Fackler
|
r43906 | s.connect(('1.0.0.1', 0)) | ||
Matt Mackall
|
r7071 | ip = s.getsockname()[0] | ||
return ip | ||||
Brodie Rao
|
r16688 | except socket.error: | ||
Matt Mackall
|
r7071 | pass | ||
# Generic method, sometimes gives useless results | ||||
Alexander Solovyov
|
r8264 | try: | ||
dumbip = socket.gethostbyaddr(socket.gethostname())[2][0] | ||||
Augie Fackler
|
r43906 | if ':' in dumbip: | ||
dumbip = '127.0.0.1' | ||||
if not dumbip.startswith('127.'): | ||||
Alexander Solovyov
|
r8264 | return dumbip | ||
Augie Fackler
|
r10317 | except (socket.gaierror, socket.herror): | ||
Augie Fackler
|
r43906 | dumbip = '127.0.0.1' | ||
Matt Mackall
|
r7071 | |||
# works elsewhere, but actually sends a packet | ||||
try: | ||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||||
Augie Fackler
|
r43906 | s.connect(('1.0.0.1', 1)) | ||
Matt Mackall
|
r7071 | ip = s.getsockname()[0] | ||
return ip | ||||
Brodie Rao
|
r16688 | except socket.error: | ||
Matt Mackall
|
r7071 | pass | ||
return dumbip | ||||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | def publish(name, desc, path, port): | ||
global server, localip | ||||
if not server: | ||||
Alexander Solovyov
|
r8264 | ip = getip() | ||
Augie Fackler
|
r43906 | if ip.startswith('127.'): | ||
Augie Fackler
|
r7295 | # if we have no internet connection, this can happen. | ||
return | ||||
Matt Mackall
|
r7071 | localip = socket.inet_aton(ip) | ||
Alexander Solovyov
|
r8264 | server = Zeroconf.Zeroconf(ip) | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43906 | hostname = socket.gethostname().split('.')[0] | ||
host = hostname + ".local" | ||||
Augie Fackler
|
r43809 | name = "%s-%s" % (hostname, name) | ||
Matt Mackall
|
r7071 | |||
# advertise to browsers | ||||
Augie Fackler
|
r43346 | svc = Zeroconf.ServiceInfo( | ||
Augie Fackler
|
r43347 | b'_http._tcp.local.', | ||
Augie Fackler
|
r43906 | pycompat.bytestr(name + '._http._tcp.local.'), | ||
Augie Fackler
|
r43346 | server=host, | ||
port=port, | ||||
Augie Fackler
|
r43347 | properties={b'description': desc, b'path': b"/" + path}, | ||
Augie Fackler
|
r43346 | address=localip, | ||
weight=0, | ||||
priority=0, | ||||
) | ||||
Matt Mackall
|
r7071 | server.registerService(svc) | ||
# advertise to Mercurial clients | ||||
Augie Fackler
|
r43346 | svc = Zeroconf.ServiceInfo( | ||
Augie Fackler
|
r43347 | b'_hg._tcp.local.', | ||
Augie Fackler
|
r43906 | pycompat.bytestr(name + '._hg._tcp.local.'), | ||
Augie Fackler
|
r43346 | server=host, | ||
port=port, | ||||
Augie Fackler
|
r43347 | properties={b'description': desc, b'path': b"/" + path}, | ||
Augie Fackler
|
r43346 | address=localip, | ||
weight=0, | ||||
priority=0, | ||||
) | ||||
Matt Mackall
|
r7071 | server.registerService(svc) | ||
Augie Fackler
|
r43346 | |||
Benoit Boissinot
|
r18190 | def zc_create_server(create_server, ui, app): | ||
httpd = create_server(ui, app) | ||||
port = httpd.port | ||||
Matt Mackall
|
r7071 | |||
Benoit Boissinot
|
r18190 | try: | ||
repos = app.repos | ||||
except AttributeError: | ||||
# single repo | ||||
Gregory Szorc
|
r27910 | with app._obtainrepo() as repo: | ||
name = app.reponame or os.path.basename(repo.root) | ||||
Augie Fackler
|
r43347 | path = repo.ui.config(b"web", b"prefix", b"").strip(b'/') | ||
desc = repo.ui.config(b"web", b"description") | ||||
Boris Feld
|
r34235 | if not desc: | ||
desc = name | ||||
Benoit Boissinot
|
r18190 | publish(name, desc, path, port) | ||
else: | ||||
# webdir | ||||
Augie Fackler
|
r43347 | prefix = app.ui.config(b"web", b"prefix", b"").strip(b'/') + b'/' | ||
Benoit Boissinot
|
r18190 | for repo, path in repos: | ||
u = app.ui.copy() | ||||
r44730 | if rcutil.use_repo_hgrc(): | |||
u.readconfig(os.path.join(path, b'.hg', b'hgrc')) | ||||
Alexander Solovyov
|
r9488 | name = os.path.basename(repo) | ||
Augie Fackler
|
r43347 | path = (prefix + repo).strip(b'/') | ||
desc = u.config(b'web', b'description') | ||||
Boris Feld
|
r34235 | if not desc: | ||
desc = name | ||||
Benoit Boissinot
|
r18190 | publish(name, desc, path, port) | ||
return httpd | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | # listen | ||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r49801 | class listener: | ||
Matt Mackall
|
r7071 | def __init__(self): | ||
self.found = {} | ||||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | def removeService(self, server, type, name): | ||
if repr(name) in self.found: | ||||
del self.found[repr(name)] | ||||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | def addService(self, server, type, name): | ||
self.found[repr(name)] = server.getServiceInfo(type, name) | ||||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r7071 | def getzcpaths(): | ||
Alexander Solovyov
|
r8264 | ip = getip() | ||
Augie Fackler
|
r43906 | if ip.startswith('127.'): | ||
Alexander Solovyov
|
r8264 | return | ||
server = Zeroconf.Zeroconf(ip) | ||||
Matt Mackall
|
r7071 | l = listener() | ||
Augie Fackler
|
r43347 | Zeroconf.ServiceBrowser(server, b"_hg._tcp.local.", l) | ||
Matt Mackall
|
r7071 | time.sleep(1) | ||
server.close() | ||||
Alexander Solovyov
|
r9488 | for value in l.found.values(): | ||
Augie Fackler
|
r43346 | name = value.name[: value.name.index(b'.')] | ||
Augie Fackler
|
r43809 | url = "http://%s:%s%s" % ( | ||
Augie Fackler
|
r43346 | socket.inet_ntoa(value.address), | ||
value.port, | ||||
Augie Fackler
|
r43809 | value.properties.get("path", "/"), | ||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r42752 | yield b"zc-" + name, pycompat.bytestr(url) | ||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
r33175 | def config(orig, self, section, key, *args, **kwargs): | |||
Augie Fackler
|
r43347 | if section == b"paths" and key.startswith(b"zc-"): | ||
Alexander Solovyov
|
r9488 | for name, path in getzcpaths(): | ||
if name == key: | ||||
return path | ||||
r33175 | return orig(self, section, key, *args, **kwargs) | |||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r28038 | def configitems(orig, self, section, *args, **kwargs): | ||
repos = orig(self, section, *args, **kwargs) | ||||
Augie Fackler
|
r43347 | if section == b"paths": | ||
Alexander Solovyov
|
r9488 | repos += getzcpaths() | ||
return repos | ||||
Matt Mackall
|
r7071 | |||
Augie Fackler
|
r43346 | |||
Danek Duvall
|
r28250 | def configsuboptions(orig, self, section, name, *args, **kwargs): | ||
opt, sub = orig(self, section, name, *args, **kwargs) | ||||
Augie Fackler
|
r43347 | if section == b"paths" and name.startswith(b"zc-"): | ||
Danek Duvall
|
r28250 | # We have to find the URL in the zeroconf paths. We can't cons up any | ||
# suboptions, so we use any that we found in the original config. | ||||
for zcname, zcurl in getzcpaths(): | ||||
if zcname == name: | ||||
return zcurl, sub | ||||
return opt, sub | ||||
Augie Fackler
|
r43346 | |||
Henrik Stuart
|
r10342 | def defaultdest(orig, source): | ||
for name, path in getzcpaths(): | ||||
if path == source: | ||||
return name.encode(encoding.encoding) | ||||
return orig(source) | ||||
Augie Fackler
|
r43346 | |||
Nicolas Dumazet
|
r14104 | def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc): | ||
try: | ||||
return orig(ui, options, cmd, cmdfunc) | ||||
finally: | ||||
# we need to call close() on the server to notify() the various | ||||
# threading Conditions and allow the background threads to exit | ||||
global server | ||||
if server: | ||||
server.close() | ||||
Augie Fackler
|
r43346 | |||
r51687 | extensions.wrapfunction(dispatch, '_runcommand', cleanupafterdispatch) | |||
Nicolas Dumazet
|
r14104 | |||
r51687 | extensions.wrapfunction(uimod.ui, 'config', config) | |||
extensions.wrapfunction(uimod.ui, 'configitems', configitems) | ||||
extensions.wrapfunction(uimod.ui, 'configsuboptions', configsuboptions) | ||||
extensions.wrapfunction(hg, 'defaultdest', defaultdest) | ||||
extensions.wrapfunction(servermod, 'create_server', zc_create_server) | ||||