Repository: encode/uvicorn

Issue:
[BUG] Port collision on Windows is registered as the application not supporting the lifespan protocol
(Originally reported as tiangolo/fastapi#525, after having misdiagnosed this as a framework bug).

### Summary
Attempting to start any application through Uvicorn, whether is actually supports lifespan or not, on a socket which is already being used by another service will cause Uvicorn to abort. On Windows, when that occurs, the final error message being logged is `INFO: ASGI 'lifespan' protocol appears unsupported.`, which is incorrect and may confuse the user.

### Steps to reproduce
* Using [the lifespan example implementation from the ASGI spec](https://asgi.readthedocs.io/en/latest/specs/lifespan.html):

```py
async def app(scope, receive, send):
    if scope['type'] == 'lifespan':
        while True:
            message = await receive()
            if message['type'] == 'lifespan.startup':
                # Do some startup here!
                await send({'type': 'lifespan.startup.complete'})
            elif message['type'] == 'lifespan.shutdown':
                # Do some shutdown here!
                await send({'type': 'lifespan.shutdown.complete'})
                return
    else:
        pass # Handle other types
```

* In two separate terminals, start uvicorn with that application on the same port: `uvicorn hello:app --host=0.0.0.0 --port=8000`.

### Current behavior
The second termminal logs the following error message:

```
$ uvicorn hello:app --host=0.0.0.0 --port=8000

email-validator not installed, email fields will be treated as str.
To install, run: pip install email-validator
INFO: Started server process [15384]
INFO: Waiting for application startup.
Traceback (most recent call last):
  File "C:\Python36\lib\asyncio\base_events.py", line 1045, in create_server
    sock.bind(sa)
OSError: [WinError 10048] Une seule utilisation de chaque adresse de socket (protocole/adresse r▒seau/port) est habituellement autoris▒e

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "c:\users\Th▒odore\Documents\prog\fastapi-test\.pyenv\Scripts\uvicorn-script.py", line 11, in <module>
    load_entry_point('uvicorn==0.8.6', 'console_scripts', 'uvicorn')()
  File "c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\click\core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\click\core.py", line 717, in main
    rv = self.invoke(ctx)
  File "c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\click\core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\click\core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\uvicorn\main.py", line 258, in main
    run(**kwargs)
  File "c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\uvicorn\main.py", line 279, in run
    server.run()
  File "c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\uvicorn\main.py", line 307, in run
    loop.run_until_complete(self.serve(sockets=sockets))
  File "C:\Python36\lib\asyncio\base_events.py", line 468, in run_until_complete
    return future.result()
  File "c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\uvicorn\main.py", line 322, in serve
    await self.startup(sockets=sockets)
  File "c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\uvicorn\main.py", line 377, in startup
    create_protocol, host=config.host, port=config.port, ssl=config.ssl
  File "C:\Python36\lib\asyncio\base_events.py", line 1049, in create_server
    % (sa, err.strerror.lower()))
OSError: [Errno 10048] error while attempting to bind on address ('0.0.0.0', 8000): une seule utilisation de chaque adresse de socket (protocole/adresse r▒seau/port) est habituellement autoris▒e
ERROR: Task was destroyed but it is pending!
task: <Task pending coro=<LifespanOn.main() running at c:\users\Th▒odore\documents\prog\fastapi-test\.pyenv\lib\site-packages\uvicorn\lifespan\on.py:44> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x0000019248B3EF78>()]>>
INFO: ASGI 'lifespan' protocol appears unsupported.
```

### Expected behavior
In the above stack trace, the second exception (which says "ASGI 'lifespan' protocol appears unsupported.") should not occur.

### System information
OS: Windows 10 (I've been unable to reproduce this on Linux)
Python version: 3.6.5
Uvicorn version: 0.8.6


Discussion:

Resolving patch:
diff --git a/uvicorn/config.py b/uvicorn/config.py
--- a/uvicorn/config.py
+++ b/uvicorn/config.py
@@ -225,7 +225,11 @@ def setup_event_loop(self):
     def bind_socket(self):
         sock = socket.socket()
         sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        sock.bind((self.host, self.port))
+        try:
+            sock.bind((self.host, self.port))
+        except OSError as exc:
+            self.logger_instance.error(exc)
+            sys.exit(1)
         sock.set_inheritable(True)
 
         if platform.system() == "Windows" and (self.workers > 1 or self.should_reload):
diff --git a/uvicorn/main.py b/uvicorn/main.py
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -329,11 +329,6 @@ async def serve(self, sockets=None, shutdown_servers=True):
     async def startup(self, sockets=None):
         config = self.config
 
-        await self.lifespan.startup()
-        if self.lifespan.should_exit:
-            self.should_exit = True
-            return
-
         create_protocol = functools.partial(
             config.http_protocol_class, config=config, server_state=self.server_state
         )
@@ -373,15 +368,23 @@ async def startup(self, sockets=None):
 
         else:
             # Standard case. Create a socket from a host/port pair.
-            server = await loop.create_server(
-                create_protocol, host=config.host, port=config.port, ssl=config.ssl
-            )
+            try:
+                server = await loop.create_server(
+                    create_protocol, host=config.host, port=config.port, ssl=config.ssl
+                )
+            except OSError as exc:
+                self.logger.error(exc)
+                sys.exit(1)
             protocol_name = "https" if config.ssl else "http"
             message = "Uvicorn running on %s://%s:%d (Press CTRL+C to quit)"
             self.logger.info(message % (protocol_name, config.host, config.port))
             self.servers = [server]
 
-        self.started = True
+        await self.lifespan.startup()
+        if self.lifespan.should_exit:
+            self.should_exit = True
+        else:
+            self.started = True
 
     async def main_loop(self):
         counter = 0


Test patch: