Repository: aio-libs/aiohttp

Issue:
Access to socket information when using AppRunner
## Long story short

Sometimes I like to listen on port 0 to allow for any and need to access the actual port after binding.

## Expected behaviour

A public way to access socket information like the port number.

## Actual behaviour

I have to reach into the private `TCPSite._server`.


Discussion:
`TCPSite` works with IPv4/IPv6 addresses, `UnixSite` operates with Unix sockets.
The address format is different. Moreover, one *site* can have several sockets for dual IPv4/IPv6 stack.
I think we need a simple but robust solution that covers all cases.
Exposing `AbstractServer` is not the best idea may be: it opens `close()` method etc.

Perhaps the best approach is adding `addresses` property to *site* object which exposes a list of socket addresses in the same form as returned by `socket.getsockname()`.

> adding `addresses` property

I'd call it `bound_sockets`.

Also, it might be a good idea to have a data struct (class) for different socket binding types. Imagine if one would want their server to be bound to TCP socket, and then to UNIX file socket, and to abstract socket. It would be handy if one could `isinstance()` those sockets.
@webknjaz you can serve the single app on both Unix and TCP sockets right now: just create two sites with the same runner.

Let's postpone sockets exposing up to a concrete use case. I doubt if we need to expose the whole socket object, it is a leaking abstraction.

Resolving patch:
diff --git a/aiohttp/web_runner.py b/aiohttp/web_runner.py
--- a/aiohttp/web_runner.py
+++ b/aiohttp/web_runner.py
@@ -148,12 +148,18 @@ def __init__(self, *, handle_signals=False, **kwargs):
         self._handle_signals = handle_signals
         self._kwargs = kwargs
         self._server = None
-        self._sites = set()
+        self._sites = []
 
     @property
     def server(self):
         return self._server
 
+    @property
+    def addresses(self):
+        return [sock.getsockname()
+                for site in self._sites
+                for sock in site._server.sockets]
+
     @property
     def sites(self):
         return set(self._sites)
@@ -210,7 +216,7 @@ def _reg_site(self, site):
         if site in self._sites:
             raise RuntimeError("Site {} is already registered in runner {}"
                                .format(site, self))
-        self._sites.add(site)
+        self._sites.append(site)
 
     def _check_site(self, site):
         if site not in self._sites:


Test patch:
diff --git a/tests/test_web_runner.py b/tests/test_web_runner.py
--- a/tests/test_web_runner.py
+++ b/tests/test_web_runner.py
@@ -1,6 +1,7 @@
 import asyncio
 import platform
 import signal
+from unittest import mock
 
 import pytest
 
@@ -96,3 +97,17 @@ async def test_app_property(make_runner, app):
 def test_non_app():
     with pytest.raises(TypeError):
         web.AppRunner(object())
+
+
+@pytest.mark.skipif(platform.system() == "Windows",
+                    reason="Unix socket support is required")
+async def test_addresses(make_runner, tmpdir):
+    runner = make_runner()
+    await runner.setup()
+    tcp = web.TCPSite(runner)
+    await tcp.start()
+    path = str(tmpdir / 'tmp.sock')
+    unix = web.UnixSite(runner, path)
+    await unix.start()
+    addrs = runner.addresses
+    assert addrs == [('0.0.0.0', mock.ANY), path]