Repository: mitmproxy/mitmproxy

Issue:
The mitmproxy program failed to start because the default port 8080 was occupied.
#### Problem Description
Because the default port 8080 is occupied, the mitmproxy program fails to start, and there is no output reason for the failure.

#### Steps to reproduce the behavior:
1. Listen on port 8080 using the nc command in a terminal window.
2. Start the mitmproxy program in another terminal window.
3. The mitmproxy program failed to start, and there was no output reason for the failure, and the normal terminal configuration was not restored.
![image](https://github.com/mitmproxy/mitmproxy/assets/32811372/e8333100-fceb-4fdb-b781-829a9ce89945)



#### System Information
Mitmproxy: 10.2.3 binary
Python:    3.12.2
OpenSSL:   OpenSSL 3.2.1 30 Jan 2024
Platform:  macOS-14.2.1-arm64-arm-64bit



Discussion:
That's bad, thanks. Possibly refs https://github.com/mitmproxy/mitmproxy/issues/6707
bisected to 6e38a56f4c399551c3fe3d399bddfa8083ccd0f1

Resolving patch:
diff --git a/mitmproxy/addons/errorcheck.py b/mitmproxy/addons/errorcheck.py
--- a/mitmproxy/addons/errorcheck.py
+++ b/mitmproxy/addons/errorcheck.py
@@ -3,6 +3,8 @@
 import sys
 
 from mitmproxy import log
+from mitmproxy.contrib import click as miniclick
+from mitmproxy.utils import vt_codes
 
 
 class ErrorCheck:
@@ -29,8 +31,13 @@ async def shutdown_if_errored(self):
         if self.logger.has_errored:
             plural = "s" if len(self.logger.has_errored) > 1 else ""
             if self.repeat_errors_on_stderr:
-                msg = "\n".join(self.logger.format(r) for r in self.logger.has_errored)
-                print(f"Error{plural} logged during startup:\n{msg}", file=sys.stderr)
+                message = f"Error{plural} logged during startup:"
+                if vt_codes.ensure_supported(sys.stderr):  # pragma: no cover
+                    message = miniclick.style(message, fg="red")
+                details = "\n".join(
+                    self.logger.format(r) for r in self.logger.has_errored
+                )
+                print(f"{message}\n{details}", file=sys.stderr)
             else:
                 print(
                     f"Error{plural} logged during startup, exiting...", file=sys.stderr
diff --git a/mitmproxy/master.py b/mitmproxy/master.py
--- a/mitmproxy/master.py
+++ b/mitmproxy/master.py
@@ -58,6 +58,7 @@ async def run(self) -> None:
         ):
             self.should_exit.clear()
 
+            # Can we exit before even bringing up servers?
             if ec := self.addons.get("errorcheck"):
                 await ec.shutdown_if_errored()
             if ps := self.addons.get("proxyserver"):
@@ -69,14 +70,23 @@ async def run(self) -> None:
                     ],
                     return_when=asyncio.FIRST_COMPLETED,
                 )
-            await self.running()
-            if ec := self.addons.get("errorcheck"):
-                await ec.shutdown_if_errored()
-                ec.finish()
+                if self.should_exit.is_set():
+                    return
+                # Did bringing up servers fail?
+                if ec := self.addons.get("errorcheck"):
+                    await ec.shutdown_if_errored()
+
             try:
+                await self.running()
+                # Any errors in the final part of startup?
+                if ec := self.addons.get("errorcheck"):
+                    await ec.shutdown_if_errored()
+                    ec.finish()
+
                 await self.should_exit.wait()
             finally:
-                # .wait might be cancelled (e.g. by sys.exit)
+                # if running() was called, we also always want to call done().
+                # .wait might be cancelled (e.g. by sys.exit), so  this needs to be in a finally block.
                 await self.done()
 
     def shutdown(self):


Test patch:
diff --git a/test/mitmproxy/addons/test_errorcheck.py b/test/mitmproxy/addons/test_errorcheck.py
--- a/test/mitmproxy/addons/test_errorcheck.py
+++ b/test/mitmproxy/addons/test_errorcheck.py
@@ -6,10 +6,11 @@
 from mitmproxy.tools import main
 
 
-def test_errorcheck(tdata, capsys):
+@pytest.mark.parametrize("run_main", [main.mitmdump, main.mitmproxy])
+def test_errorcheck(tdata, capsys, run_main):
     """Integration test: Make sure that we catch errors on startup an exit."""
     with pytest.raises(SystemExit):
-        main.mitmdump(
+        run_main(
             [
                 "-n",
                 "-s",