Repository: encode/uvicorn

Issue:
Reloader only reloads once
It looks like the reloader parent gets the kill signal when it sends the kill signal to the child. The child exits correctly, but the parent also stops the reloader process and leaves a new child hanging. 

I’m not really sure how to fix this. I can dig in more, but wanted to open an issue first. 

Discussion:

Patch:
diff --git a/docs/deployment.md b/docs/deployment.md
index 268a095..911a74c 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -4,7 +4,7 @@ Server deployment is a complex area, that will depend on what kind of service yo
 
 As a general rule, you probably want to:
 
-* Run `uvicorn --debug` from the command line for local development.
+* Run `uvicorn --reload` from the command line for local development.
 * Run `gunicorn -k uvicorn.workers.UvicornWorker` for production.
 * Additionally run behind Nginx for self-hosted deployments.
 * Finally, run everything behind a CDN for caching support, and serious DDOS protection.
@@ -14,12 +14,12 @@ As a general rule, you probably want to:
 Typically you'll run `uvicorn` from the command line.
 
 ```bash
-$ uvicorn app:App --debug --port 5000
+$ uvicorn app:App --reload --port 5000
 ```
 
 The ASGI application should be specified in the form `path.to.module:instance.path`.
 
-When running locally, use `--debug` to turn on auto-reloading, and display error tracebacks in the browser.
+When running locally, use `--reload` to turn on auto-reloading.
 
 To see the complete set of available options, use `uvicorn --help`:
 
@@ -33,15 +33,20 @@ Options:
   --port INTEGER                  Bind socket to this port.  [default: 8000]
   --uds TEXT                      Bind to a UNIX domain socket.
   --fd INTEGER                    Bind to socket from this file descriptor.
+  --reload                        Enable auto-reload.
+  --reload-dir TEXT               Set reload directories explicitly, instead
+                                  of using 'sys.path'.
+  --workers INTEGER               Number of worker processes. Not valid with
+                                  --reload.
   --loop [auto|asyncio|uvloop]    Event loop implementation.  [default: auto]
-  --http [auto|h11|httptools]     HTTP protocol implementation.  [default: auto]
+  --http [auto|h11|httptools]     HTTP protocol implementation.  [default:
+                                  auto]
   --ws [none|auto|websockets|wsproto]
                                   WebSocket protocol implementation.
                                   [default: auto]
   --lifespan [auto|on|off]        Lifespan implementation.  [default: auto]
   --wsgi                          Use WSGI as the application interface,
                                   instead of ASGI.
-  --debug                         Enable debug mode.
   --log-level [critical|error|warning|info|debug]
                                   Log level.  [default: info]
   --no-access-log                 Disable access log.
@@ -241,4 +246,4 @@ $ gunicorn --keyfile=./key.pem --certfile=./cert.pem -k uvicorn.workers.UvicornW
 ```
 
 [letsencrypt]: https://letsencrypt.org/
-[mkcert]: https://github.com/FiloSottile/mkcert
\ No newline at end of file
+[mkcert]: https://github.com/FiloSottile/mkcert
diff --git a/docs/index.md b/docs/index.md
index c0f8a75..c8b3df9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -80,14 +80,20 @@ Options:
   --port INTEGER                  Bind socket to this port.  [default: 8000]
   --uds TEXT                      Bind to a UNIX domain socket.
   --fd INTEGER                    Bind to socket from this file descriptor.
+  --reload                        Enable auto-reload.
+  --reload-dir TEXT               Set reload directories explicitly, instead
+                                  of using 'sys.path'.
+  --workers INTEGER               Number of worker processes. Not valid with
+                                  --reload.
   --loop [auto|asyncio|uvloop]    Event loop implementation.  [default: auto]
-  --http [auto|h11|httptools]     HTTP parser implementation.  [default: auto]
+  --http [auto|h11|httptools]     HTTP protocol implementation.  [default:
+                                  auto]
   --ws [none|auto|websockets|wsproto]
                                   WebSocket protocol implementation.
                                   [default: auto]
+  --lifespan [auto|on|off]        Lifespan implementation.  [default: auto]
   --wsgi                          Use WSGI as the application interface,
                                   instead of ASGI.
-  --debug                         Enable debug mode.
   --log-level [critical|error|warning|info|debug]
                                   Log level.  [default: info]
   --no-access-log                 Disable access log.
@@ -104,6 +110,15 @@ Options:
   --timeout-keep-alive INTEGER    Close Keep-Alive connections if no new data
                                   is received within this timeout.  [default:
                                   5]
+  --ssl-keyfile TEXT              SSL key file
+  --ssl-certfile TEXT             SSL certificate file
+  --ssl-version INTEGER           SSL version to use (see stdlib ssl module's)
+                                  [default: 2]
+  --ssl-cert-reqs INTEGER         Whether client certificate is required (see
+                                  stdlib ssl module's)  [default: 0]
+  --ssl-ca-certs TEXT             CA certificates file
+  --ssl-ciphers TEXT              Ciphers to use (see stdlib ssl module's)
+                                  [default: TLSv1]
   --help                          Show this message and exit.
 ```
 
diff --git a/docs/server-behavior.md b/docs/server-behavior.md
index 2e2a6d3..33cca11 100644
--- a/docs/server-behavior.md
+++ b/docs/server-behavior.md
@@ -83,8 +83,6 @@ Server errors will be logged at the `error` log level. All logging defaults to b
 
 If an exception is raised by an ASGI application, and a response has not yet been sent on the connection, then a `500 Server Error` HTTP response will be sent.
 
-If running with `--debug` then the response will include an error traceback, in either HTML or plaintext, depending on the request `Accept` header.
-
 ### Invalid responses
 
 Uvicorn will ensure that ASGI applications send the correct sequence of messages, and will raise errors otherwise. This includes checking for no response sent, partial response sent, or invalid message sequences being sent.
diff --git a/docs/settings.md b/docs/settings.md
index d7309d6..568e673 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -11,25 +11,31 @@ equivalent keyword arguments, eg. `uvicorn.run(App, port=5000, debug=True, acces
 
 ## Socket Binding
 
-* `--host` - Bind socket to this host. Use `--host 0.0.0.0` to make the application available on your local network. **Default:** *'127.0.0.1'*.
-* `--port` - Bind to a socket with this port. **Default:** *8000*.
-* `--uds` - Bind to a UNIX domain socket. Useful if you want to run Uvicorn behind a reverse proxy.
-* `--fd` - Bind to socket from this file descriptor. Useful if you want to run Uvicorn within a process manager.
+* `--host <str>` - Bind socket to this host. Use `--host 0.0.0.0` to make the application available on your local network. **Default:** *'127.0.0.1'*.
+* `--port <int>` - Bind to a socket with this port. **Default:** *8000*.
+* `--uds <str>` - Bind to a UNIX domain socket. Useful if you want to run Uvicorn behind a reverse proxy.
+* `--fd <int>` - Bind to socket from this file descriptor. Useful if you want to run Uvicorn within a process manager.
 
 ## Development
 
-* `--debug` - Enable debug mode. Provides error tracebacks in the browser, and enables auto-reloading.
+* `--reload` - Enable auto-reload.
+* `--reload-dir <path>` - Specify which directories to watch for python file changes. May be used multiple times. If unused, then by default all directories in `sys.path` will be watched.
+
+## Production
+
+* `--workers <int>` - Use multiple worker processes.
 
 ## Logging
 
-* `--log-level` - Set the log level. **Options:** *'critical', 'error', 'warning', 'info', 'debug'.* **Default:** *'info'*.
+* `--log-level <str>` - Set the log level. **Options:** *'critical', 'error', 'warning', 'info', 'debug'.* **Default:** *'info'*.
 * `--no-access-log` - Disable access log only, without changing log level.
 
 ## Implementation
 
-* `--loop` - Set the event loop implementation. The uvloop implementation provides greater performance, but is not compatible with Windows or PyPy. **Options:** *'auto', 'asyncio', 'uvloop'.* **Default:** *'auto'*.
-* `--http` - Set the HTTP protocol implementation. The httptools implementation provides greater performance, but it not compatible with PyPy, and requires compilation on Windows. **Options:** *'auto', 'h11', 'httptools'.* **Default:** *'auto'*.
-* `--ws` - Set the WebSockets protocol implementation. Either of the `websockets` and `wsproto` packages are supported. Use `'none'` to deny all websocket requests. **Options:** *'auto', 'none', 'websockets', 'wsproto'.* **Default:** *'auto'*.
+* `--loop <str>` - Set the event loop implementation. The uvloop implementation provides greater performance, but is not compatible with Windows or PyPy. **Options:** *'auto', 'asyncio', 'uvloop'.* **Default:** *'auto'*.
+* `--http <str>` - Set the HTTP protocol implementation. The httptools implementation provides greater performance, but it not compatible with PyPy, and requires compilation on Windows. **Options:** *'auto', 'h11', 'httptools'.* **Default:** *'auto'*.
+* `--ws <str>` - Set the WebSockets protocol implementation. Either of the `websockets` and `wsproto` packages are supported. Use `'none'` to deny all websocket requests. **Options:** *'auto', 'none', 'websockets', 'wsproto'.* **Default:** *'auto'*.
+* `--lifespan <str>` - Set the Lifespan protocol implementation. **Options:** *'auto', 'on', 'off'.* **Default:** *'auto'*.
 
 ## Application Interface
 
@@ -37,27 +43,23 @@ equivalent keyword arguments, eg. `uvicorn.run(App, port=5000, debug=True, acces
 
 ## HTTP
 
-* `--root-path` - Set the ASGI `root_path` for applications submounted below a given URL path.
+* `--root-path <str>` - Set the ASGI `root_path` for applications submounted below a given URL path.
 * `--proxy-headers` - Use the X-Forwarded-Proto and X-Forwarded-For headers to populate remote scheme/address info.
 
 ## HTTPS
 
-* `--ssl-keyfile` - SSL key file
-* `--ssl-certfile` - SSL certificate file
-* `--ssl-version` - SSL version to use (see stdlib ssl module's)
-* `--ssl-cert-reqs` - Whether client certificate is required (see stdlib ssl module's)
-* `--ssl-ca-certs` - CA certificates file
-* `--ssl-ciphers` - Ciphers to use (see stdlib ssl module's)
+* `--ssl-keyfile <path>` - SSL key file
+* `--ssl-certfile <path>` - SSL certificate file
+* `--ssl-version <int>` - SSL version to use (see stdlib ssl module's)
+* `--ssl-cert-reqs <int>` - Whether client certificate is required (see stdlib ssl module's)
+* `--ssl-ca-certs <str>` - CA certificates file
+* `--ssl-ciphers <str>` - Ciphers to use (see stdlib ssl module's)
 
 ## Resource Limits
 
-* `--limit-concurrency` - Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses. Useful for ensuring known memory usage patterns even under over-resourced loads.
-* `--limit-max-requests` - Maximum number of requests to service before terminating the process. Useful when running together with a process manager, for preventing memory leaks from impacting long-running processes.
+* `--limit-concurrency <int>` - Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses. Useful for ensuring known memory usage patterns even under over-resourced loads.
+* `--limit-max-requests <int>` - Maximum number of requests to service before terminating the process. Useful when running together with a process manager, for preventing memory leaks from impacting long-running processes.
 
 ## Timeouts
 
-* `--timeout-keep-alive` - Close Keep-Alive connections if no new data is received within this timeout. **Default:** *5*.
-
-## Lifespan
-
-* `--disable-lifespan` - Disable lifespan events (such as startup and shutdown) within an ASGI application.
+* `--timeout-keep-alive <int>` - Close Keep-Alive connections if no new data is received within this timeout. **Default:** *5*.
diff --git a/uvicorn/__init__.py b/uvicorn/__init__.py
index 8b85b72..c0ca9e9 100644
--- a/uvicorn/__init__.py
+++ b/uvicorn/__init__.py
@@ -1,5 +1,5 @@
 from uvicorn.config import Config
 from uvicorn.main import Server, main, run
 
-__version__ = "0.4.6"
+__version__ = "0.5.0"
 __all__ = ["main", "run", "Config", "Server"]
diff --git a/uvicorn/config.py b/uvicorn/config.py
index d688730..6cff92e 100644
--- a/uvicorn/config.py
+++ b/uvicorn/config.py
@@ -1,4 +1,5 @@
 import logging
+import socket
 import ssl
 import sys
 
@@ -66,7 +67,6 @@ class Config:
         port=8000,
         uds=None,
         fd=None,
-        sockets=None,
         loop="auto",
         http="auto",
         ws="auto",
@@ -76,6 +76,9 @@ class Config:
         access_log=True,
         wsgi=False,
         debug=False,
+        reload=False,
+        reload_dirs=None,
+        workers=1,
         proxy_headers=False,
         root_path="",
         limit_concurrency=None,
@@ -95,7 +98,6 @@ class Config:
         self.port = port
         self.uds = uds
         self.fd = fd
-        self.sockets = sockets
         self.loop = loop
         self.http = http
         self.ws = ws
@@ -105,6 +107,8 @@ class Config:
         self.access_log = access_log
         self.wsgi = wsgi
         self.debug = debug
+        self.reload = reload
+        self.workers = workers
         self.proxy_headers = proxy_headers
         self.root_path = root_path
         self.limit_concurrency = limit_concurrency
@@ -119,16 +123,28 @@ class Config:
         self.ssl_ca_certs = ssl_ca_certs
         self.ssl_ciphers = ssl_ciphers
 
+        if self.ssl_keyfile or self.ssl_certfile:
+            self.ssl = create_ssl_context(
+                keyfile=self.ssl_keyfile,
+                certfile=self.ssl_certfile,
+                ssl_version=self.ssl_version,
+                cert_reqs=self.ssl_cert_reqs,
+                ca_certs=self.ssl_ca_certs,
+                ciphers=self.ssl_ciphers,
+            )
+        else:
+            self.ssl = None
+
+        if reload_dirs is None:
+            self.reload_dirs = sys.path
+        else:
+            self.reload_dirs = reload_dirs
+
         self.loaded = False
 
     def load(self):
         assert not self.loaded
 
-        if self.logger is None:
-            self.logger_instance = get_logger(self.log_level)
-        else:
-            self.logger_instance = self.logger
-
         if isinstance(self.http, str):
             self.http_protocol_class = import_from_string(HTTP_PROTOCOLS[self.http])
         else:
@@ -157,20 +173,24 @@ class Config:
         if self.proxy_headers:
             self.loaded_app = ProxyHeadersMiddleware(self.loaded_app)
 
-        if self.ssl_keyfile or self.ssl_certfile:
-            self.ssl = create_ssl_context(
-                keyfile=self.ssl_keyfile,
-                certfile=self.ssl_certfile,
-                ssl_version=self.ssl_version,
-                cert_reqs=self.ssl_cert_reqs,
-                ca_certs=self.ssl_ca_certs,
-                ciphers=self.ssl_ciphers,
-            )
-        else:
-            self.ssl = None
-
         self.loaded = True
 
     def setup_event_loop(self):
         loop_setup = import_from_string(LOOP_SETUPS[self.loop])
         loop_setup()
+
+    def bind_socket(self):
+        sock = socket.socket()
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        sock.bind((self.host, self.port))
+        sock.set_inheritable(True)
+        message = "Uvicorn running on %s://%s:%d (Press CTRL+C to quit)"
+        protocol_name = "https" if self.ssl else "http"
+        self.logger_instance.info(message % (protocol_name, self.host, self.port))
+        return sock
+
+    @property
+    def logger_instance(self):
+        if self.logger is not None:
+            return self.logger
+        return get_logger(self.log_level)
diff --git a/uvicorn/main.py b/uvicorn/main.py
index 2a622af..9021ea6 100644
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -5,6 +5,7 @@ import signal
 import ssl
 import sys
 import time
+import typing
 from email.utils import formatdate
 
 import click
@@ -18,7 +19,7 @@ from uvicorn.config import (
     Config,
     get_logger,
 )
-from uvicorn.reloaders.statreload import StatReload
+from uvicorn.supervisors import Multiprocess, StatReload
 
 LEVEL_CHOICES = click.Choice(LOG_LEVELS.keys())
 HTTP_CHOICES = click.Choice(HTTP_PROTOCOLS.keys())
@@ -52,6 +53,22 @@ HANDLED_SIGNALS = (
 @click.option(
     "--fd", type=int, default=None, help="Bind to socket from this file descriptor."
 )
+@click.option(
+    "--debug", is_flag=True, default=False, help="Enable debug mode.", hidden=True
+)
+@click.option("--reload", is_flag=True, default=False, help="Enable auto-reload.")
+@click.option(
+    "--reload-dir",
+    "reload_dirs",
+    multiple=True,
+    help="Set reload directories explicitly, instead of using 'sys.path'.",
+)
+@click.option(
+    "--workers",
+    default=1,
+    type=int,
+    help="Number of worker processes. Not valid with --reload.",
+)
 @click.option(
     "--loop",
     type=LOOP_CHOICES,
@@ -86,7 +103,6 @@ HANDLED_SIGNALS = (
     default=False,
     help="Use WSGI as the application interface, instead of ASGI.",
 )
-@click.option("--debug", is_flag=True, default=False, help="Enable debug mode.")
 @click.option(
     "--log-level",
     type=LEVEL_CHOICES,
@@ -178,6 +194,9 @@ def main(
     lifespan: str,
     wsgi: bool,
     debug: bool,
+    reload: bool,
+    reload_dirs: typing.List[str],
+    workers: int,
     log_level: str,
     no_access_log: bool,
     proxy_headers: bool,
@@ -208,6 +227,9 @@ def main(
         "access_log": not no_access_log,
         "wsgi": wsgi,
         "debug": debug,
+        "reload": reload,
+        "reload_dirs": reload_dirs if reload_dirs else None,
+        "workers": workers,
         "proxy_headers": proxy_headers,
         "root_path": root_path,
         "limit_concurrency": limit_concurrency,
@@ -220,19 +242,23 @@ def main(
         "ssl_ca_certs": ssl_ca_certs,
         "ssl_ciphers": ssl_ciphers,
     }
-
-    if debug:
-        logger = get_logger(log_level)
-        reloader = StatReload(logger)
-        reloader.run(run, kwargs)
-    else:
-        run(**kwargs)
+    run(**kwargs)
 
 
 def run(app, **kwargs):
     config = Config(app, **kwargs)
     server = Server(config=config)
-    server.run()
+
+    if config.debug or config.reload:
+        socket = config.bind_socket()
+        supervisor = StatReload(config)
+        supervisor.run(server.run, sockets=[socket])
+    elif config.workers > 1:
+        socket = config.bind_socket()
+        supervisor = Multiprocess(config)
+        supervisor.run(server.run, sockets=[socket])
+    else:
+        server.run()
 
 
 class ServerState:
@@ -256,12 +282,12 @@ class Server:
         self.should_exit = False
         self.force_exit = False
 
-    def run(self):
+    def run(self, sockets=None, shutdown_servers=True):
         self.config.setup_event_loop()
         loop = asyncio.get_event_loop()
-        loop.run_until_complete(self.serve())
+        loop.run_until_complete(self.serve(sockets=sockets))
 
-    async def serve(self):
+    async def serve(self, sockets=None, shutdown_servers=True):
         process_id = os.getpid()
 
         config = self.config
@@ -274,12 +300,12 @@ class Server:
         self.install_signal_handlers()
 
         self.logger.info("Started server process [{}]".format(process_id))
-        await self.startup()
+        await self.startup(sockets=sockets)
         await self.main_loop()
-        await self.shutdown()
+        await self.shutdown(shutdown_servers=shutdown_servers)
         self.logger.info("Finished server process [{}]".format(process_id))
 
-    async def startup(self):
+    async def startup(self, sockets=None):
         config = self.config
 
         await self.lifespan.startup()
@@ -290,11 +316,11 @@ class Server:
 
         loop = asyncio.get_event_loop()
 
-        if config.sockets is not None:
+        if sockets is not None:
             # Explicitly passed a list of open sockets.
             # We use this when the server is run from a Gunicorn worker.
             self.servers = []
-            for socket in config.sockets:
+            for socket in sockets:
                 server = await loop.create_server(
                     create_protocol, sock=socket, ssl=config.ssl
                 )
@@ -360,11 +386,11 @@ class Server:
             return self.server_state.total_requests >= self.config.limit_max_requests
         return False
 
-    async def shutdown(self):
+    async def shutdown(self, shutdown_servers=True):
         self.logger.info("Shutting down")
 
         # Stop accepting new connections.
-        if not self.config.sockets:
+        if shutdown_servers:
             for server in self.servers:
                 server.close()
             for server in self.servers:
diff --git a/uvicorn/reloaders/__init__.py b/uvicorn/reloaders/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/uvicorn/supervisors/__init__.py b/uvicorn/supervisors/__init__.py
new file mode 100644
index 0000000..dab32c1
--- /dev/null
+++ b/uvicorn/supervisors/__init__.py
@@ -0,0 +1,4 @@
+from uvicorn.supervisors.multiprocess import Multiprocess
+from uvicorn.supervisors.statreload import StatReload
+
+__all__ = ["Multiprocess", "StatReload"]
diff --git a/uvicorn/supervisors/multiprocess.py b/uvicorn/supervisors/multiprocess.py
new file mode 100644
index 0000000..3fc780b
--- /dev/null
+++ b/uvicorn/supervisors/multiprocess.py
@@ -0,0 +1,42 @@
+import multiprocessing
+import os
+import signal
+import time
+
+HANDLED_SIGNALS = (
+    signal.SIGINT,  # Unix signal 2. Sent by Ctrl+C.
+    signal.SIGTERM,  # Unix signal 15. Sent by `kill <pid>`.
+)
+
+
+class Multiprocess:
+    def __init__(self, config):
+        self.config = config
+        self.workers = config.workers
+        self.should_exit = False
+
+    def handle_exit(self, sig, frame):
+        self.should_exit = True
+
+    def run(self, target, *args, **kwargs):
+        pid = os.getpid()
+        logger = self.config.logger_instance
+
+        logger.info("Started parent process [{}]".format(pid))
+
+        for sig in HANDLED_SIGNALS:
+            signal.signal(sig, self.handle_exit)
+
+        spawn = multiprocessing.get_context("spawn")
+        processes = []
+        for idx in range(self.workers):
+            process = spawn.Process(target=target, args=args, kwargs=kwargs)
+            process.start()
+            processes.append(process)
+
+        while (
+            any([process.is_alive() for process in processes]) and not self.should_exit
+        ):
+            time.sleep(0.1)
+
+        logger.info("Stopping parent process [{}]".format(pid))
diff --git a/uvicorn/reloaders/statreload.py b/uvicorn/supervisors/statreload.py
similarity index 51%
rename from uvicorn/reloaders/statreload.py
rename to uvicorn/supervisors/statreload.py
index 9e578cc..ef4122e 100644
--- a/uvicorn/reloaders/statreload.py
+++ b/uvicorn/supervisors/statreload.py
@@ -1,8 +1,8 @@
 import multiprocessing
 import os
 import signal
-import sys
 import time
+from pathlib import Path
 
 HANDLED_SIGNALS = (
     signal.SIGINT,  # Unix signal 2. Sent by Ctrl+C.
@@ -11,37 +11,39 @@ HANDLED_SIGNALS = (
 
 
 class StatReload:
-    def __init__(self, logger):
-        self.logger = logger
+    def __init__(self, config):
+        self.config = config
         self.should_exit = False
+        self.reload_count = 0
         self.mtimes = {}
 
     def handle_exit(self, sig, frame):
         self.should_exit = True
 
-    def run(self, target, kwargs):
+    def run(self, target, *args, **kwargs):
         pid = os.getpid()
+        logger = self.config.logger_instance
 
-        self.logger.info("Started reloader process [{}]".format(pid))
+        logger.info("Started reloader process [{}]".format(pid))
 
         for sig in HANDLED_SIGNALS:
             signal.signal(sig, self.handle_exit)
 
-        process = multiprocessing.Process(target=target, kwargs=kwargs)
+        spawn = multiprocessing.get_context("spawn")
+        process = spawn.Process(target=target, args=args, kwargs=kwargs)
         process.start()
 
         while process.is_alive() and not self.should_exit:
-            time.sleep(0.2)
+            time.sleep(0.1)
             if self.should_restart():
                 self.clear()
                 os.kill(process.pid, signal.SIGTERM)
                 process.join()
-                process = multiprocessing.Process(target=target, kwargs=kwargs)
+                process = spawn.Process(target=target, args=args, kwargs=kwargs)
                 process.start()
+                self.reload_count += 1
 
-        self.logger.info("Stopping reloader process [{}]".format(pid))
-
-        sys.exit(process.exitcode)
+        logger.info("Stopping reloader process [{}]".format(pid))
 
     def clear(self):
         self.mtimes = {}
@@ -50,7 +52,7 @@ class StatReload:
         for filename in self.iter_py_files():
             try:
                 mtime = os.stat(filename).st_mtime
-            except OSError as exc:
+            except OSError as exc:  # pragma: nocover
                 continue
 
             old_time = self.mtimes.get(filename)
@@ -58,14 +60,18 @@ class StatReload:
                 self.mtimes[filename] = mtime
                 continue
             elif mtime > old_time:
+                display_path = os.path.normpath(filename)
+                if Path.cwd() in Path(filename).parents:
+                    display_path = os.path.normpath(os.path.relpath(filename))
                 message = "Detected file change in '%s'. Reloading..."
-                self.logger.warning(message, filename)
+                self.config.logger_instance.warning(message, display_path)
                 return True
         return False
 
     def iter_py_files(self):
-        for subdir, dirs, files in os.walk("."):
-            for file in files:
-                filepath = subdir + os.sep + file
-                if filepath.endswith(".py"):
-                    yield filepath
+        for reload_dir in self.config.reload_dirs:
+            for subdir, dirs, files in os.walk(reload_dir):
+                for file in files:
+                    filepath = subdir + os.sep + file
+                    if filepath.endswith(".py"):
+                        yield filepath
diff --git a/uvicorn/workers.py b/uvicorn/workers.py
index 228d794..0f6de7a 100644
--- a/uvicorn/workers.py
+++ b/uvicorn/workers.py
@@ -21,7 +21,6 @@ class UvicornWorker(Worker):
 
         config_kwargs = {
             "app": None,
-            "sockets": self.sockets,
             "logger": self.log,
             "timeout_keep_alive": self.cfg.keepalive,
             "timeout_notify": self.timeout,
@@ -54,7 +53,9 @@ class UvicornWorker(Worker):
         self.config.app = self.wsgi
         server = Server(config=self.config)
         loop = asyncio.get_event_loop()
-        loop.run_until_complete(server.serve())
+        loop.run_until_complete(
+            server.serve(sockets=self.sockets, shutdown_servers=False)
+        )
 
     async def callback_notify(self):
         self.notify()


Test patch:
diff --git a/tests/protocols/test_http.py b/tests/protocols/test_http.py
index 946a78c..5dfc287 100644
--- a/tests/protocols/test_http.py
+++ b/tests/protocols/test_http.py
@@ -11,7 +11,7 @@ from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
 
 try:
     from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
-except ImportError:
+except ImportError:  # pragma: nocover
     HttpToolsProtocol = None
 
 
@@ -695,8 +695,4 @@ def test_supported_upgrade_request(protocol_cls):
 
     protocol = get_connected_protocol(app, protocol_cls, ws="wsproto")
     protocol.data_received(UPGRADE_REQUEST)
-    if b"HTTP/1.1 426 " in protocol.transport.buffer:
-        pass  # wsproto 0.13
-    else:
-        assert b"HTTP/1.1 400 Bad Request" in protocol.transport.buffer
-        assert b"Missing Sec-WebSocket-Version header" in protocol.transport.buffer
+    assert b"HTTP/1.1 426 " in protocol.transport.buffer
diff --git a/tests/protocols/test_websocket.py b/tests/protocols/test_websocket.py
index 7d4fd2a..2a6e014 100644
--- a/tests/protocols/test_websocket.py
+++ b/tests/protocols/test_websocket.py
@@ -15,7 +15,7 @@ from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
 try:
     import websockets
     from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
-except ImportError:
+except ImportError:  # pragma: nocover
     websockets = None
     WebSocketProtocol = None
 
diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py
new file mode 100644
index 0000000..67bc9a0
--- /dev/null
+++ b/tests/supervisors/test_multiprocess.py
@@ -0,0 +1,22 @@
+from uvicorn.config import Config
+from uvicorn.supervisors import Multiprocess
+
+
+def no_op():
+    pass
+
+
+def mock_signal(handle_exit):
+    handle_exit(None, None)
+
+
+def test_multiprocess():
+    config = Config(app=None, workers=2)
+    reloader = Multiprocess(config)
+    reloader.run(no_op)
+
+
+def test_exit_signal():
+    config = Config(app=None, workers=2)
+    reloader = Multiprocess(config)
+    reloader.run(mock_signal, handle_exit=reloader.handle_exit)
diff --git a/tests/supervisors/test_statreload.py b/tests/supervisors/test_statreload.py
new file mode 100644
index 0000000..5592399
--- /dev/null
+++ b/tests/supervisors/test_statreload.py
@@ -0,0 +1,40 @@
+import os
+import time
+from pathlib import Path
+
+from uvicorn.config import Config
+from uvicorn.supervisors import StatReload
+
+
+def no_op():
+    pass
+
+
+def wait_for_reload(reloader, until, update_file):
+    # I think coverage doesn't fully track this, since it runs in a spawned subprocess.
+    while reloader.reload_count < until:  # pragma: nocover
+        time.sleep(0.1)
+        Path(update_file).touch()
+
+
+def mock_signal(handle_exit):
+    handle_exit(None, None)
+
+
+def test_statreload():
+    config = Config(app=None)
+    reloader = StatReload(config)
+    reloader.run(no_op)
+
+
+def test_reload_dirs(tmpdir):
+    update_file = os.path.join(str(tmpdir), "example.py")
+    config = Config(app=None, reload_dirs=[str(tmpdir)])
+    reloader = StatReload(config)
+    reloader.run(wait_for_reload, reloader=reloader, until=1, update_file=update_file)
+
+
+def test_exit_signal(tmpdir):
+    config = Config(app=None)
+    reloader = StatReload(config)
+    reloader.run(mock_signal, handle_exit=reloader.handle_exit)
diff --git a/tests/test_auto_detection.py b/tests/test_auto_detection.py
index 1a758e8..3e21db2 100644
--- a/tests/test_auto_detection.py
+++ b/tests/test_auto_detection.py
@@ -8,17 +8,18 @@ from uvicorn.protocols.websockets.auto import AutoWebSocketsProtocol
 
 try:
     import uvloop
-except ImportError:
+except ImportError:  # pragma: no cover
     uvloop = None
 
 try:
     import httptools
-except ImportError:
+except ImportError:  # pragma: no cover
     httptools = None
 
 try:
     import websockets
-except ImportError:  # Note that when this happens, the websocket tests are skipped
+except ImportError:  # pragma: no cover
+    # Note that we skip the websocket tests completely in this case.
     websockets = None
 
 
@@ -30,28 +31,21 @@ def test_loop_auto():
     loop = auto_loop_setup()
     policy = asyncio.get_event_loop_policy()
     assert isinstance(policy, asyncio.events.BaseDefaultEventLoopPolicy)
-    if uvloop is None:
-        assert type(policy).__module__.startswith("asyncio")
-    else:
-        assert isinstance(policy, uvloop.EventLoopPolicy)
-        assert type(policy).__module__.startswith("uvloop")
+    expected_loop = "asyncio" if uvloop is None else "uvloop"
+    assert type(policy).__module__.startswith(expected_loop)
 
 
 def test_http_auto():
     config = Config(app=None)
     server_state = ServerState()
     protocol = AutoHTTPProtocol(config=config, server_state=server_state)
-    if httptools is None:
-        assert type(protocol).__name__ == "H11Protocol"
-    else:
-        assert type(protocol).__name__ == "HttpToolsProtocol"
+    expected_http = "H11Protocol" if httptools is None else "HttpToolsProtocol"
+    assert type(protocol).__name__ == expected_http
 
 
 def test_websocket_auto():
     config = Config(app=None)
     server_state = ServerState()
     protocol = AutoWebSocketsProtocol(config=config, server_state=server_state)
-    if websockets is None:
-        assert type(protocol).__name__ == "WSProtocol"
-    else:
-        assert type(protocol).__name__ == "WebSocketProtocol"
+    expected_websockets = "WSProtocol" if websockets is None else "WebSocketProtocol"
+    assert type(protocol).__name__ == expected_websockets
diff --git a/tests/test_ssl.py b/tests/test_ssl.py
index 05758fe..14511af 100644
--- a/tests/test_ssl.py
+++ b/tests/test_ssl.py
@@ -104,11 +104,10 @@ def create_certfile_and_keyfile(request):
     return certfile, keyfile
 
 
+@pytest.mark.skipif(
+    sys.platform.startswith("win"), reason="Skipping SSL test on Windows"
+)
 def test_run(create_certfile_and_keyfile):
-
-    if sys.platform.startswith("win"):
-        pytest.skip("Skipping SSL test on Windows for now :(")
-
     certfile, keyfile = create_certfile_and_keyfile
 
     class App: