Skip to content

Commit

Permalink
Rewrite WerkzeugServer for better IPv4/v6 handling (#1449)
Browse files Browse the repository at this point in the history
  • Loading branch information
nfelt authored Sep 21, 2018
1 parent 7277d15 commit 5e4a8bd
Showing 1 changed file with 108 additions and 53 deletions.
161 changes: 108 additions & 53 deletions tensorboard/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from abc import ABCMeta
from abc import abstractmethod
import argparse
from collections import defaultdict
import errno
import logging
import os
Expand Down Expand Up @@ -187,9 +188,7 @@ def main(self, ignored_argv=('',)):
if self.flags.inspect:
logger.info('Not bringing up TensorBoard, but inspecting event files.')
event_file = os.path.expanduser(self.flags.event_file)
efi.inspect(self.flags.logdir,
self.flags.event_file,
self.flags.tag)
efi.inspect(self.flags.logdir, event_file, self.flags.tag)
return 0
try:
server = self._make_server()
Expand Down Expand Up @@ -266,61 +265,117 @@ def __init__(self, msg):
self.msg = msg


class WerkzeugServer(TensorBoardServer):
class WerkzeugServer(serving.ThreadedWSGIServer, TensorBoardServer):
"""Implementation of TensorBoardServer using the Werkzeug dev server."""
# ThreadedWSGIServer handles this in werkzeug 0.12+ but we allow 0.11.x.
daemon_threads = True

def __init__(self, wsgi_app, flags):
self._flags = flags
host = flags.host
port = flags.port
self._auto_wildcard = False
if not host:
# Without an explicit host, we default to serving on all interfaces,
# and will attempt to serve both IPv4 and IPv6 traffic through one socket.
host = self._get_wildcard_address(flags.port)
self._auto_wildcard = True
try:
if host:
# The user gave us an explicit host
server = serving.make_server(host, port, wsgi_app, threaded=True)
if ':' in host and not host.startswith('['):
# Display IPv6 addresses as [::1]:80 rather than ::1:80
final_host = '[{}]'.format(host)
super(WerkzeugServer, self).__init__(host, flags.port, wsgi_app)
except socket.error as e:
if hasattr(errno, 'EACCES') and e.errno == errno.EACCES:
raise TensorBoardServerException(
'TensorBoard must be run as superuser to bind to port %d' %
flags.port)
elif hasattr(errno, 'EADDRINUSE') and e.errno == errno.EADDRINUSE:
if flags.port == 0:
raise TensorBoardServerException(
'TensorBoard unable to find any open port')
else:
final_host = host
else:
# We've promised to bind to all interfaces on this host. However, we're
# not sure whether that means IPv4 or IPv6 interfaces.
try:
# First try passing in a blank host (meaning all interfaces). This,
# unfortunately, defaults to IPv4 even if no IPv4 interface is available
# (yielding a socket.error).
server = serving.make_server(host, port, wsgi_app, threaded=True)
except socket.error:
# If a blank host didn't work, we explicitly request IPv6 interfaces.
server = serving.make_server('::', port, wsgi_app, threaded=True)
final_host = socket.gethostname()
server.daemon_threads = True
except socket.error:
if port == 0:
msg = 'TensorBoard unable to find any open port'
else:
msg = (
'TensorBoard attempted to bind to port %d, but it was already in use'
% port)
raise TensorBoardServerException(msg)
server.handle_error = _handle_error
final_port = server.socket.getsockname()[1]
self._server = server
self._url = 'http://%s:%d%s' % (final_host, final_port, flags.path_prefix)

def serve_forever(self):
self._server.serve_forever()
raise TensorBoardServerException(
'TensorBoard could not bind to port %d, it was already in use' %
flags.port)
elif hasattr(errno, 'EADDRNOTAVAIL') and e.errno == errno.EADDRNOTAVAIL:
raise TensorBoardServerException(
'TensorBoard could not bind to unavailable address %s' % host)
elif hasattr(errno, 'EAFNOSUPPORT') and e.errno == errno.EAFNOSUPPORT:
raise TensorBoardServerException(
'Tensorboard could not bind to unsupported address family %s' %
host)
# Raise the raw exception if it wasn't identifiable as a user error.
raise

def _get_wildcard_address(self, port):
"""Returns a wildcard address for the port in question.
This will attempt to follow the best practice of calling getaddrinfo() with
a null host and AI_PASSIVE to request a server-side socket wildcard address.
If that succeeds, this returns the first IPv6 address found, or if none,
then returns the first IPv4 address. If that fails, then this returns the
hardcoded address "::" if socket.has_ipv6 is True, else "0.0.0.0".
"""
fallback_address = '::' if socket.has_ipv6 else '0.0.0.0'
if hasattr(socket, 'AI_PASSIVE'):
try:
addrinfos = socket.getaddrinfo(None, port, socket.AF_UNSPEC,
socket.SOCK_STREAM, socket.IPPROTO_TCP,
socket.AI_PASSIVE)
except socket.gaierror as e:
logger.warn('Failed to auto-detect wildcard address, assuming %s: %s',
fallback_address, str(e))
return fallback_address
addrs_by_family = defaultdict(list)
for family, _, _, _, sockaddr in addrinfos:
# Format of the "sockaddr" socket address varies by address family,
# but [0] is always the IP address portion.
addrs_by_family[family].append(sockaddr[0])
if hasattr(socket, 'AF_INET6') and addrs_by_family[socket.AF_INET6]:
return addrs_by_family[socket.AF_INET6][0]
if hasattr(socket, 'AF_INET') and addrs_by_family[socket.AF_INET]:
return addrs_by_family[socket.AF_INET][0]
logger.warn('Failed to auto-detect wildcard address, assuming %s',
fallback_address)
return fallback_address

def server_bind(self):
"""Override to enable IPV4 mapping for IPV6 sockets when desired.
The main use case for this is so that when no host is specified, TensorBoard
can listen on all interfaces for both IPv4 and IPv6 connections, rather than
having to choose v4 or v6 and hope the browser didn't choose the other one.
"""
socket_is_v6 = (
hasattr(socket, 'AF_INET6') and self.socket.family == socket.AF_INET6)
has_v6only_option = (
hasattr(socket, 'IPPROTO_IPV6') and hasattr(socket, 'IPV6_V6ONLY'))
if self._auto_wildcard and socket_is_v6 and has_v6only_option:
try:
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
except socket.error as e:
# Log a warning on failure to dual-bind, except for EAFNOSUPPORT
# since that's expected if IPv4 isn't supported at all (IPv6-only).
if hasattr(errno, 'EAFNOSUPPORT') and e.errno != errno.EAFNOSUPPORT:
logging.warn('Failed to dual-bind to IPv4 wildcard: %s', str(e))
super(WerkzeugServer, self).server_bind()

def handle_error(self, request, client_address):
"""Override to get rid of noisy EPIPE errors."""
del request # unused
# Kludge to override a SocketServer.py method so we can get rid of noisy
# EPIPE errors. They're kind of a red herring as far as errors go. For
# example, `curl -N http://localhost:6006/ | head` will cause an EPIPE.
exc_info = sys.exc_info()
e = exc_info[1]
if isinstance(e, IOError) and e.errno == errno.EPIPE:
logger.warn('EPIPE caused by %s in HTTP serving' % str(client_address))
else:
logger.error('HTTP serving error', exc_info=exc_info)

def get_url(self):
return self._url


# Kludge to override a SocketServer.py method so we can get rid of noisy
# EPIPE errors. They're kind of a red herring as far as errors go. For
# example, `curl -N http://localhost:6006/ | head` will cause an EPIPE.
def _handle_error(unused_request, client_address):
exc_info = sys.exc_info()
e = exc_info[1]
if isinstance(e, IOError) and e.errno == errno.EPIPE:
logger.warn('EPIPE caused by %s in HTTP serving' % client_address)
else:
logger.error('HTTP serving error', exc_info=exc_info)
if self._auto_wildcard:
display_host = socket.gethostname()
else:
host = self._flags.host
display_host = (
'[%s]' % host if ':' in host and not host.startswith('[') else host)
return 'http://%s:%d%s' % (display_host, self.server_port,
self._flags.path_prefix)

0 comments on commit 5e4a8bd

Please sign in to comment.