Server: Auto-fallback to detected interface if binding to specified interface fails

This commit is contained in:
Kovid Goyal 2015-06-03 19:17:40 +05:30
parent 35dec94173
commit 143d78ea47
4 changed files with 58 additions and 31 deletions

View File

@ -23,6 +23,7 @@ from calibre.srv.utils import (
from calibre.utils.socket_inheritance import set_socket_inherit from calibre.utils.socket_inheritance import set_socket_inherit
from calibre.utils.logging import ThreadSafeLog from calibre.utils.logging import ThreadSafeLog
from calibre.utils.monotonic import monotonic from calibre.utils.monotonic import monotonic
from calibre.utils.mdns import get_external_ip
READ, WRITE, RDWR, WAIT = 'READ', 'WRITE', 'RDWR', 'WAIT' READ, WRITE, RDWR, WAIT = 'READ', 'WRITE', 'RDWR', 'WAIT'
WAKEUP, JOB_DONE = bytes(bytearray(xrange(2))) WAKEUP, JOB_DONE = bytes(bytearray(xrange(2)))
@ -321,41 +322,53 @@ class ServerLoop(object):
def num_active_connections(self): def num_active_connections(self):
return len(self.connection_map) return len(self.connection_map)
def do_bind(self):
# Get the correct address family for our host (allows IPv6 addresses)
host, port = self.bind_address
try:
info = socket.getaddrinfo(
host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
except socket.gaierror:
if ':' in host:
info = [(socket.AF_INET6, socket.SOCK_STREAM,
0, "", self.bind_address + (0, 0))]
else:
info = [(socket.AF_INET, socket.SOCK_STREAM,
0, "", self.bind_address)]
self.socket = None
msg = "No socket could be created"
for res in info:
af, socktype, proto, canonname, sa = res
try:
self.bind(af, socktype, proto)
except socket.error as serr:
msg = "%s -- (%s: %s)" % (msg, sa, as_unicode(serr))
if self.socket:
self.socket.close()
self.socket = None
continue
break
if not self.socket:
raise socket.error(msg)
def serve_forever(self): def serve_forever(self):
""" Listen for incoming connections. """ """ Listen for incoming connections. """
if self.pre_activated_socket is None: if self.pre_activated_socket is None:
# AF_INET or AF_INET6 socket
# Get the correct address family for our host (allows IPv6
# addresses)
host, port = self.bind_address
try: try:
info = socket.getaddrinfo( self.do_bind()
host, port, socket.AF_UNSPEC, except socket.error as err:
socket.SOCK_STREAM, 0, socket.AI_PASSIVE) if not self.opts.fallback_to_detected_interface:
except socket.gaierror: raise
if ':' in host: ip = get_external_ip()
info = [(socket.AF_INET6, socket.SOCK_STREAM, if ip == self.bind_address[0]:
0, "", self.bind_address + (0, 0))] raise
else: self.log.warn('Failed to bind to %s with error: %s. Trying to bind to the default interface: %s instead' % (
info = [(socket.AF_INET, socket.SOCK_STREAM, self.bind_address[0], as_unicode(err), ip))
0, "", self.bind_address)] self.bind_address = (ip, self.bind_address[1])
self.do_bind()
self.socket = None
msg = "No socket could be created"
for res in info:
af, socktype, proto, canonname, sa = res
try:
self.bind(af, socktype, proto)
except socket.error, serr:
msg = "%s -- (%s: %s)" % (msg, sa, serr)
if self.socket:
self.socket.close()
self.socket = None
continue
break
if not self.socket:
raise socket.error(msg)
else: else:
self.socket = self.pre_activated_socket self.socket = self.pre_activated_socket
self.pre_activated_socket = None self.pre_activated_socket = None

View File

@ -67,6 +67,12 @@ raw_options = (
' example, "127.0.0.1" to only listen for connections from the local machine, or' ' example, "127.0.0.1" to only listen for connections from the local machine, or'
' to "::" to listen to all incoming IPv6 and IPv4 connections.', ' to "::" to listen to all incoming IPv6 and IPv4 connections.',
'Fallback to auto-detected interface',
'fallback_to_detected_interface', True,
'If for some reason the server is unable to bind to the interface specified in'
' the listen_on option, then it will try to detect an interface that connects'
' to the outside world and bind to that.',
'Enable/disable zero copy file transfers for increased performance', 'Enable/disable zero copy file transfers for increased performance',
'use_sendfile', True, 'use_sendfile', True,
'This will use zero-copy in-kernel transfers when sending files over the network,' 'This will use zero-copy in-kernel transfers when sending files over the network,'

View File

@ -23,7 +23,7 @@ class TestServer(Thread):
daemon = True daemon = True
def __init__(self, handler, plugins=(), **kwargs): def __init__(self, handler, plugins=(), specialize=lambda srv:None, **kwargs):
Thread.__init__(self, name='ServerMain') Thread.__init__(self, name='ServerMain')
from calibre.srv.opts import Options from calibre.srv.opts import Options
from calibre.srv.loop import ServerLoop from calibre.srv.loop import ServerLoop
@ -38,6 +38,7 @@ class TestServer(Thread):
log=ServerLog(level=ServerLog.WARN), log=ServerLog(level=ServerLog.WARN),
) )
self.log = self.loop.log self.log = self.loop.log
specialize(self)
def run(self): def run(self):
try: try:

View File

@ -94,6 +94,13 @@ class LoopTest(BaseTest):
server.join() server.join()
self.ae(1, sum(int(w.is_alive()) for w in pool.workers)) self.ae(1, sum(int(w.is_alive()) for w in pool.workers))
def test_fallback_interface(self):
'Test falling back to default interface'
def specialize(server):
server.loop.log.filter_level = server.loop.log.ERROR
with TestServer(lambda data:(data.path[0] + data.read()), listen_on='1.1.1.1', fallback_to_detected_interface=True, specialize=specialize) as server:
self.assertNotEqual('1.1.1.1', server.address[0])
def test_ring_buffer(self): def test_ring_buffer(self):
'Test the ring buffer used for reads' 'Test the ring buffer used for reads'
class FakeSocket(object): class FakeSocket(object):