Repository: mitmproxy/mitmproxy

Issue:
add_signal_handler is strictly UNIX only. 
https://github.com/mitmproxy/mitmproxy/blob/4e126c0fbaafffd23e1a80926de6a99c897f9af0/mitmproxy/tools/main.py#L124
According to the `asyncio`  documentation for [Python 3.6](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.AbstractEventLoop.add_signal_handler) , `add_signal_handler` is supported for UNIX system only. I haven't experienced or tested this issue but it maybe a potential risk since mitmproxy supports multiple platforms. 

If someone has a Windows OS and/or some knowledge about asyncio maybe you could verify this.


Discussion:
Although not the best solution , but the best workaround is to add a simple coroutine with call_later of 0.1. Somehow this works well in Windows and CTRL+C can terminate the loop.
[Ref](https://stackoverflow.com/questions/24774980/why-cant-i-catch-sigint-when-asyncio-event-loop-is-running/24775107#24775107)
Good catch! We need someone on Windows to actually test the behaviour here. The StackOverflow question you included refers to a patch implementing add_signal_handler for the Windows event loops from 3.5 years ago which (sigh) appears never to have been merged.

Step one is to catch the `NotImplementedError` around that call and ignore it. 
I am on Windows and I pulled the commit from 2 days ago and since then, mitmdump and mitmweb weren't working anymore : 
![commit_error](https://user-images.githubusercontent.com/15815157/38795371-4bd90854-4158-11e8-9b13-906b4f70c5fe.JPG)
I thought it was because of some modifications I made so I didn't make an issue of that and I searched by myself without success.
But I just tested your solution of putting a try to catch the "NotImplementedError" : 
`try:
loop.add_signal_handler(getattr(signal, signame), master.shutdown)
except NotImplementedError as e:
print("ERROR : Method not implemented in Windows.")`
and then: 
![notimplement_exception](https://user-images.githubusercontent.com/15815157/38795589-1345aac8-4159-11e8-9bd4-f451f7456a9b.JPG)

So the behaviour here is that mitmdump and mitmweb are not usable until the exception is catched.

### UPDATE
It seems like signal handling is a mess on Windows, and that there is no clean way to make CTRL + C exit the process without an error message, but CTRL + Break seems to do the work.
@cortal Thanks for looking at this. Once the exception is caught, does mitmdump exit on ctrl-C?
Yes, mitmdump exits on CTRL-C. (I checked with tasklist)

And the message you can see from my terminal screenshot is because I set the asyncio logger to DEBUG level to get more information, on CRITICAL level, the message doesn't show.

Resolving patch:
diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py
--- a/mitmproxy/tools/main.py
+++ b/mitmproxy/tools/main.py
@@ -67,7 +67,7 @@ def run(
         make_parser: typing.Callable[[options.Options], argparse.ArgumentParser],
         arguments: typing.Sequence[str],
         extra: typing.Callable[[typing.Any], dict] = None
-):  # pragma: no cover
+) -> master.Master:  # pragma: no cover
     """
         extra: Extra argument processing callable which returns a dict of
         options.
@@ -121,7 +121,11 @@ def cleankill(*args, **kwargs):
         signal.signal(signal.SIGTERM, cleankill)
         loop = asyncio.get_event_loop()
         for signame in ('SIGINT', 'SIGTERM'):
-            loop.add_signal_handler(getattr(signal, signame), master.shutdown)
+            try:
+                loop.add_signal_handler(getattr(signal, signame), master.shutdown)
+            except NotImplementedError:
+                # Not supported on Windows
+                pass
         master.run()
     except exceptions.OptionsError as e:
         print("%s: %s" % (sys.argv[0], e), file=sys.stderr)
@@ -131,19 +135,18 @@ def cleankill(*args, **kwargs):
     return master
 
 
-def mitmproxy(args=None):  # pragma: no cover
+def mitmproxy(args=None) -> typing.Optional[int]:  # pragma: no cover
     if os.name == "nt":
         print("Error: mitmproxy's console interface is not supported on Windows. "
               "You can run mitmdump or mitmweb instead.", file=sys.stderr)
-        sys.exit(1)
-
+        return 1
     assert_utf8_env()
-
     from mitmproxy.tools import console
-    return run(console.master.ConsoleMaster, cmdline.mitmproxy, args)
+    run(console.master.ConsoleMaster, cmdline.mitmproxy, args)
+    return None
 
 
-def mitmdump(args=None):  # pragma: no cover
+def mitmdump(args=None) -> typing.Optional[int]:  # pragma: no cover
     from mitmproxy.tools import dump
 
     def extra(args):
@@ -157,11 +160,12 @@ def extra(args):
         return {}
 
     m = run(dump.DumpMaster, cmdline.mitmdump, args, extra)
-    if m and m.errorcheck.has_errored:
-        sys.exit(1)
-    return m
+    if m and m.errorcheck.has_errored:  # type: ignore
+        return 1
+    return None
 
 
-def mitmweb(args=None):  # pragma: no cover
+def mitmweb(args=None) -> typing.Optional[int]:  # pragma: no cover
     from mitmproxy.tools import web
-    return run(web.master.WebMaster, cmdline.mitmweb, args)
+    run(web.master.WebMaster, cmdline.mitmweb, args)
+    return None


Test patch:
diff --git a/test/mitmproxy/tools/test_main.py b/test/mitmproxy/tools/test_main.py
--- a/test/mitmproxy/tools/test_main.py
+++ b/test/mitmproxy/tools/test_main.py
@@ -1,18 +1,19 @@
 import pytest
 
 from mitmproxy.tools import main
+from mitmproxy import ctx
 
 
 @pytest.mark.asyncio
 async def test_mitmweb(event_loop):
-    m = main.mitmweb([
+    main.mitmweb([
         "--no-web-open-browser",
         "-q", "-p", "0",
     ])
-    await m._shutdown()
+    await ctx.master._shutdown()
 
 
 @pytest.mark.asyncio
 async def test_mitmdump():
-    m = main.mitmdump(["-q", "-p", "0"])
-    await m._shutdown()
+    main.mitmdump(["-q", "-p", "0"])
+    await ctx.master._shutdown()