Repository: mitmproxy/mitmproxy

Issue:
mitmweb can't be closed by Ctrl+C on Windows anymore
##### Steps to reproduce the problem:

I even compared this behavior with 3.0.4.

**4.0.1:**

1. Run `mitmweb`
2. Press Ctrl+C in console
3. Nothing happens. 
4. if you close web browser, you will get a warning:
5. >`lib\asyncio\base_events.py:492`: RuntimeWarning: coroutine 'Master._shutdown' was never awaited
  self._ready.clear()
6. So, you have to kill `python.exe` manually. 

**3.0.4:**

1. Run `mitmweb`
2. Press Ctrl+C in console
3. `mitmweb` will be closed regardless of a browser.


##### Any other comments? What have you tried so far?

4.0.1 works fine on **Linux**. It may be connected with `asyncio`.

And just a guess: https://gist.github.com/lambdalisue/05d5654bd1ec04992ad316d50924137c (Ctrl+C hotfix patch of asyncio on Windows)

##### System information

> Mitmproxy: 4.0.1
Python:    3.6.2 (3.6.5 - the same issue)
OpenSSL:   OpenSSL 1.1.0h  27 Mar 2018
Platform:  Windows-7-6.1.7601-SP1



Discussion:
Thanks - most likely that's it. Do you want to send a quick PR?
Sorry, I doubt so. I have no idea where are `mitmweb`'s sources in this repository and where to insert that fix.
This also happens with `mitmdump` and the problem was introduced in this commit: https://github.com/mitmproxy/mitmproxy/commit/44016a0de519469792769f7b78c37e7de9bfde24
```
44016a0de519469792769f7b78c37e7de9bfde24 is the first bad commit
commit 44016a0de519469792769f7b78c37e7de9bfde24
Author: Aldo Cortesi <aldo@corte.si>
Date:   Sat Apr 7 11:46:34 2018 +1200

    asyncio: shift script reloading out of the tick event

    The tick event is a nasty compromise, left over from when we didn't have an
    event loop. This is the first patch in a series that explores moving our
    built-in addons to managing coroutines on the eventloop directly for periodic
    tasks.
```

I tried having a go at fixing this but unfortunately I don't know enough about asyncio.

Resolving patch:
diff --git a/mitmproxy/master.py b/mitmproxy/master.py
--- a/mitmproxy/master.py
+++ b/mitmproxy/master.py
@@ -119,7 +119,15 @@ def shutdown(self):
         """
         if not self.should_exit.is_set():
             self.should_exit.set()
-            asyncio.run_coroutine_threadsafe(self._shutdown(), loop = self.channel.loop)
+            ret = asyncio.run_coroutine_threadsafe(self._shutdown(), loop=self.channel.loop)
+            # Weird band-aid to make sure that self._shutdown() is actually executed,
+            # which otherwise hangs the process as the proxy server is threaded.
+            # This all needs to be simplified when the proxy server runs on asyncio as well.
+            if not self.channel.loop.is_running():  # pragma: no cover
+                try:
+                    self.channel.loop.run_until_complete(asyncio.wrap_future(ret))
+                except RuntimeError:
+                    pass  # Event loop stopped before Future completed.
 
     def _change_reverse_host(self, f):
         """
diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py
--- a/mitmproxy/tools/main.py
+++ b/mitmproxy/tools/main.py
@@ -119,9 +119,6 @@ def run(
         if extra:
             opts.update(**extra(args))
 
-        def cleankill(*args, **kwargs):
-            master.shutdown()
-        signal.signal(signal.SIGTERM, cleankill)
         loop = asyncio.get_event_loop()
         for signame in ('SIGINT', 'SIGTERM'):
             try:
@@ -129,6 +126,15 @@ def cleankill(*args, **kwargs):
             except NotImplementedError:
                 # Not supported on Windows
                 pass
+
+        # Make sure that we catch KeyboardInterrupts on Windows.
+        # https://stackoverflow.com/a/36925722/934719
+        if os.name == "nt":
+            async def wakeup():
+                while True:
+                    await asyncio.sleep(0.2)
+            asyncio.ensure_future(wakeup())
+
         master.run()
     except exceptions.OptionsError as e:
         print("%s: %s" % (sys.argv[0], e), file=sys.stderr)


Test patch:
diff --git a/test/mitmproxy/proxy/test_server.py b/test/mitmproxy/proxy/test_server.py
--- a/test/mitmproxy/proxy/test_server.py
+++ b/test/mitmproxy/proxy/test_server.py
@@ -787,7 +787,7 @@ async def test_unnecessary_serverconnect(self):
         """A replayed/fake response with no upstream_cert should not connect to an upstream server"""
         self.set_addons(AFakeResponse())
         assert self.pathod("200").status_code == 200
-        asyncio.sleep(0.1)
+        await asyncio.sleep(0.1)
         assert not self.proxy.tmaster.has_log("serverconnect")