We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
User's script error handler function
As discussed with @cortesi on slack, right now whenever a user's script throws an error due to various reasons, it is being handled at different places differently.
Therefore we can have a consistent error handler function which can be invoked whenever there is an error
This will also handle #2837 #2838 #2839 
### Function

Signature
`script_error(path, message, lineno, exception)`

What function will do
>"Error in script XXX:NNN MMM” where XXX is the path as specified by the user (the .path attribute of Script), NNN is a line number if we have one, and MMM is a short message

The idea here is to display the above mentioned message in the console app and display the traceback related to the error in the event log. 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `mitmproxy/addons/script.py`
Content:
```
1 import os
2 import importlib.util
3 import importlib.machinery
4 import time
5 import sys
6 import types
7 import typing
8 
9 from mitmproxy import addonmanager
10 from mitmproxy import exceptions
11 from mitmproxy import flow
12 from mitmproxy import command
13 from mitmproxy import eventsequence
14 from mitmproxy import ctx
15 import mitmproxy.types as mtypes
16 
17 
18 def load_script(path: str) -> types.ModuleType:
19     fullname = "__mitmproxy_script__.{}".format(
20         os.path.splitext(os.path.basename(path))[0]
21     )
22     # the fullname is not unique among scripts, so if there already is an existing script with said
23     # fullname, remove it.
24     sys.modules.pop(fullname, None)
25     oldpath = sys.path
26     sys.path.insert(0, os.path.dirname(path))
27     try:
28         loader = importlib.machinery.SourceFileLoader(fullname, path)
29         spec = importlib.util.spec_from_loader(fullname, loader=loader)
30         m = importlib.util.module_from_spec(spec)
31         loader.exec_module(m)
32         if not getattr(m, "name", None):
33             m.name = path  # type: ignore
34         return m
35     finally:
36         sys.path[:] = oldpath
37 
38 
39 class Script:
40     """
41         An addon that manages a single script.
42     """
43     ReloadInterval = 2
44 
45     def __init__(self, path):
46         self.name = "scriptmanager:" + path
47         self.path = path
48         self.fullpath = os.path.expanduser(
49             path.strip("'\" ")
50         )
51         self.ns = None
52 
53         self.last_load = 0
54         self.last_mtime = 0
55         if not os.path.isfile(self.fullpath):
56             raise exceptions.OptionsError('No such script: "%s"' % self.fullpath)
57 
58     @property
59     def addons(self):
60         return [self.ns] if self.ns else []
61 
62     def tick(self):
63         if time.time() - self.last_load > self.ReloadInterval:
64             try:
65                 mtime = os.stat(self.fullpath).st_mtime
66             except FileNotFoundError:
67                 scripts = list(ctx.options.scripts)
68                 scripts.remove(self.path)
69                 ctx.options.update(scripts=scripts)
70                 return
71 
72             if mtime > self.last_mtime:
73                 ctx.log.info("Loading script: %s" % self.path)
74                 if self.ns:
75                     ctx.master.addons.remove(self.ns)
76                 self.ns = None
77                 with addonmanager.safecall():
78                     ns = load_script(self.fullpath)
79                     ctx.master.addons.register(ns)
80                     self.ns = ns
81                 if self.ns:
82                     # We're already running, so we have to explicitly register and
83                     # configure the addon
84                     ctx.master.addons.invoke_addon(self.ns, "running")
85                     ctx.master.addons.invoke_addon(
86                         self.ns,
87                         "configure",
88                         ctx.options.keys()
89                     )
90                 self.last_load = time.time()
91                 self.last_mtime = mtime
92 
93 
94 class ScriptLoader:
95     """
96         An addon that manages loading scripts from options.
97     """
98     def __init__(self):
99         self.is_running = False
100         self.addons = []
101 
102     def load(self, loader):
103         loader.add_option(
104             "scripts", typing.Sequence[str], [],
105             """
106             Execute a script.
107             """
108         )
109 
110     def running(self):
111         self.is_running = True
112 
113     @command.command("script.run")
114     def script_run(self, flows: typing.Sequence[flow.Flow], path: mtypes.Path) -> None:
115         """
116             Run a script on the specified flows. The script is loaded with
117             default options, and all lifecycle events for each flow are
118             simulated.
119         """
120         try:
121             s = Script(path)
122             l = addonmanager.Loader(ctx.master)
123             ctx.master.addons.invoke_addon(s, "load", l)
124             ctx.master.addons.invoke_addon(s, "configure", ctx.options.keys())
125             # Script is loaded on the first tick
126             ctx.master.addons.invoke_addon(s, "tick")
127             for f in flows:
128                 for evt, arg in eventsequence.iterate(f):
129                     ctx.master.addons.invoke_addon(s, evt, arg)
130         except exceptions.OptionsError as e:
131             raise exceptions.CommandError("Error running script: %s" % e) from e
132 
133     def configure(self, updated):
134         if "scripts" in updated:
135             for s in ctx.options.scripts:
136                 if ctx.options.scripts.count(s) > 1:
137                     raise exceptions.OptionsError("Duplicate script: %s" % s)
138 
139             for a in self.addons[:]:
140                 if a.path not in ctx.options.scripts:
141                     ctx.log.info("Un-loading script: %s" % a.name)
142                     ctx.master.addons.remove(a)
143                     self.addons.remove(a)
144 
145             # The machinations below are to ensure that:
146             #   - Scripts remain in the same order
147             #   - Scripts are not initialized un-necessarily. If only a
148             #   script's order in the script list has changed, it is just
149             #   moved.
150 
151             current = {}
152             for a in self.addons:
153                 current[a.path] = a
154 
155             ordered = []
156             newscripts = []
157             for s in ctx.options.scripts:
158                 if s in current:
159                     ordered.append(current[s])
160                 else:
161                     sc = Script(s)
162                     ordered.append(sc)
163                     newscripts.append(sc)
164 
165             self.addons = ordered
166 
167             for s in newscripts:
168                 ctx.master.addons.register(s)
169                 if self.is_running:
170                     # If we're already running, we configure and tell the addon
171                     # we're up and running.
172                     ctx.master.addons.invoke_addon(s, "running")
173
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/mitmproxy/addons/script.py b/mitmproxy/addons/script.py
--- a/mitmproxy/addons/script.py
+++ b/mitmproxy/addons/script.py
@@ -5,6 +5,7 @@
 import sys
 import types
 import typing
+import traceback
 
 from mitmproxy import addonmanager
 from mitmproxy import exceptions
@@ -36,6 +37,25 @@
         sys.path[:] = oldpath
 
 
+def script_error_handler(path, exc, msg="", tb=False):
+    """
+        Handles all the user's script errors with
+        an optional traceback
+    """
+    exception = type(exc).__name__
+    if msg:
+        exception = msg
+    lineno = ""
+    if hasattr(exc, "lineno"):
+        lineno = str(exc.lineno)
+    log_msg = "in Script {}:{} {}".format(path, lineno, exception)
+    if tb:
+        etype, value, tback = sys.exc_info()
+        tback = addonmanager.cut_traceback(tback, "invoke_addon")
+        log_msg = log_msg.join(["\n"] + traceback.format_exception(etype, value, tback))
+    ctx.log.error(log_msg)
+
+
 class Script:
     """
         An addon that manages a single script.
@@ -53,7 +73,7 @@
         self.last_load = 0
         self.last_mtime = 0
         if not os.path.isfile(self.fullpath):
-            raise exceptions.OptionsError('No such script: "%s"' % self.fullpath)
+            raise exceptions.OptionsError('No such script')
 
     @property
     def addons(self):
@@ -128,13 +148,13 @@
                 for evt, arg in eventsequence.iterate(f):
                     ctx.master.addons.invoke_addon(s, evt, arg)
         except exceptions.OptionsError as e:
-            raise exceptions.CommandError("Error running script: %s" % e) from e
+            script_error_handler(path, e, msg=str(e))
 
     def configure(self, updated):
         if "scripts" in updated:
             for s in ctx.options.scripts:
                 if ctx.options.scripts.count(s) > 1:
-                    raise exceptions.OptionsError("Duplicate script: %s" % s)
+                    raise exceptions.OptionsError("Duplicate script")
 
             for a in self.addons[:]:
                 if a.path not in ctx.options.scripts:

{"golden_diff": "diff --git a/mitmproxy/addons/script.py b/mitmproxy/addons/script.py\n--- a/mitmproxy/addons/script.py\n+++ b/mitmproxy/addons/script.py\n@@ -5,6 +5,7 @@\n import sys\n import types\n import typing\n+import traceback\n \n from mitmproxy import addonmanager\n from mitmproxy import exceptions\n@@ -36,6 +37,25 @@\n         sys.path[:] = oldpath\n \n \n+def script_error_handler(path, exc, msg=\"\", tb=False):\n+    \"\"\"\n+        Handles all the user's script errors with\n+        an optional traceback\n+    \"\"\"\n+    exception = type(exc).__name__\n+    if msg:\n+        exception = msg\n+    lineno = \"\"\n+    if hasattr(exc, \"lineno\"):\n+        lineno = str(exc.lineno)\n+    log_msg = \"in Script {}:{} {}\".format(path, lineno, exception)\n+    if tb:\n+        etype, value, tback = sys.exc_info()\n+        tback = addonmanager.cut_traceback(tback, \"invoke_addon\")\n+        log_msg = log_msg.join([\"\\n\"] + traceback.format_exception(etype, value, tback))\n+    ctx.log.error(log_msg)\n+\n+\n class Script:\n     \"\"\"\n         An addon that manages a single script.\n@@ -53,7 +73,7 @@\n         self.last_load = 0\n         self.last_mtime = 0\n         if not os.path.isfile(self.fullpath):\n-            raise exceptions.OptionsError('No such script: \"%s\"' % self.fullpath)\n+            raise exceptions.OptionsError('No such script')\n \n     @property\n     def addons(self):\n@@ -128,13 +148,13 @@\n                 for evt, arg in eventsequence.iterate(f):\n                     ctx.master.addons.invoke_addon(s, evt, arg)\n         except exceptions.OptionsError as e:\n-            raise exceptions.CommandError(\"Error running script: %s\" % e) from e\n+            script_error_handler(path, e, msg=str(e))\n \n     def configure(self, updated):\n         if \"scripts\" in updated:\n             for s in ctx.options.scripts:\n                 if ctx.options.scripts.count(s) > 1:\n-                    raise exceptions.OptionsError(\"Duplicate script: %s\" % s)\n+                    raise exceptions.OptionsError(\"Duplicate script\")\n \n             for a in self.addons[:]:\n                 if a.path not in ctx.options.scripts:\n", "issue": "User's script error handler function\nAs discussed with @cortesi on slack, right now whenever a user's script throws an error due to various reasons, it is being handled at different places differently.\r\nTherefore we can have a consistent error handler function which can be invoked whenever there is an error\r\nThis will also handle #2837 #2838 #2839 \r\n### Function\r\n\r\nSignature\r\n`script_error(path, message, lineno, exception)`\r\n\r\nWhat function will do\r\n>\"Error in script XXX:NNN MMM\u201d where XXX is the path as specified by the user (the .path attribute of Script), NNN is a line number if we have one, and MMM is a short message\r\n\r\nThe idea here is to display the above mentioned message in the console app and display the traceback related to the error in the event log. \n", "before_files": [{"content": "import os\nimport importlib.util\nimport importlib.machinery\nimport time\nimport sys\nimport types\nimport typing\n\nfrom mitmproxy import addonmanager\nfrom mitmproxy import exceptions\nfrom mitmproxy import flow\nfrom mitmproxy import command\nfrom mitmproxy import eventsequence\nfrom mitmproxy import ctx\nimport mitmproxy.types as mtypes\n\n\ndef load_script(path: str) -> types.ModuleType:\n    fullname = \"__mitmproxy_script__.{}\".format(\n        os.path.splitext(os.path.basename(path))[0]\n    )\n    # the fullname is not unique among scripts, so if there already is an existing script with said\n    # fullname, remove it.\n    sys.modules.pop(fullname, None)\n    oldpath = sys.path\n    sys.path.insert(0, os.path.dirname(path))\n    try:\n        loader = importlib.machinery.SourceFileLoader(fullname, path)\n        spec = importlib.util.spec_from_loader(fullname, loader=loader)\n        m = importlib.util.module_from_spec(spec)\n        loader.exec_module(m)\n        if not getattr(m, \"name\", None):\n            m.name = path  # type: ignore\n        return m\n    finally:\n        sys.path[:] = oldpath\n\n\nclass Script:\n    \"\"\"\n        An addon that manages a single script.\n    \"\"\"\n    ReloadInterval = 2\n\n    def __init__(self, path):\n        self.name = \"scriptmanager:\" + path\n        self.path = path\n        self.fullpath = os.path.expanduser(\n            path.strip(\"'\\\" \")\n        )\n        self.ns = None\n\n        self.last_load = 0\n        self.last_mtime = 0\n        if not os.path.isfile(self.fullpath):\n            raise exceptions.OptionsError('No such script: \"%s\"' % self.fullpath)\n\n    @property\n    def addons(self):\n        return [self.ns] if self.ns else []\n\n    def tick(self):\n        if time.time() - self.last_load > self.ReloadInterval:\n            try:\n                mtime = os.stat(self.fullpath).st_mtime\n            except FileNotFoundError:\n                scripts = list(ctx.options.scripts)\n                scripts.remove(self.path)\n                ctx.options.update(scripts=scripts)\n                return\n\n            if mtime > self.last_mtime:\n                ctx.log.info(\"Loading script: %s\" % self.path)\n                if self.ns:\n                    ctx.master.addons.remove(self.ns)\n                self.ns = None\n                with addonmanager.safecall():\n                    ns = load_script(self.fullpath)\n                    ctx.master.addons.register(ns)\n                    self.ns = ns\n                if self.ns:\n                    # We're already running, so we have to explicitly register and\n                    # configure the addon\n                    ctx.master.addons.invoke_addon(self.ns, \"running\")\n                    ctx.master.addons.invoke_addon(\n                        self.ns,\n                        \"configure\",\n                        ctx.options.keys()\n                    )\n                self.last_load = time.time()\n                self.last_mtime = mtime\n\n\nclass ScriptLoader:\n    \"\"\"\n        An addon that manages loading scripts from options.\n    \"\"\"\n    def __init__(self):\n        self.is_running = False\n        self.addons = []\n\n    def load(self, loader):\n        loader.add_option(\n            \"scripts\", typing.Sequence[str], [],\n            \"\"\"\n            Execute a script.\n            \"\"\"\n        )\n\n    def running(self):\n        self.is_running = True\n\n    @command.command(\"script.run\")\n    def script_run(self, flows: typing.Sequence[flow.Flow], path: mtypes.Path) -> None:\n        \"\"\"\n            Run a script on the specified flows. The script is loaded with\n            default options, and all lifecycle events for each flow are\n            simulated.\n        \"\"\"\n        try:\n            s = Script(path)\n            l = addonmanager.Loader(ctx.master)\n            ctx.master.addons.invoke_addon(s, \"load\", l)\n            ctx.master.addons.invoke_addon(s, \"configure\", ctx.options.keys())\n            # Script is loaded on the first tick\n            ctx.master.addons.invoke_addon(s, \"tick\")\n            for f in flows:\n                for evt, arg in eventsequence.iterate(f):\n                    ctx.master.addons.invoke_addon(s, evt, arg)\n        except exceptions.OptionsError as e:\n            raise exceptions.CommandError(\"Error running script: %s\" % e) from e\n\n    def configure(self, updated):\n        if \"scripts\" in updated:\n            for s in ctx.options.scripts:\n                if ctx.options.scripts.count(s) > 1:\n                    raise exceptions.OptionsError(\"Duplicate script: %s\" % s)\n\n            for a in self.addons[:]:\n                if a.path not in ctx.options.scripts:\n                    ctx.log.info(\"Un-loading script: %s\" % a.name)\n                    ctx.master.addons.remove(a)\n                    self.addons.remove(a)\n\n            # The machinations below are to ensure that:\n            #   - Scripts remain in the same order\n            #   - Scripts are not initialized un-necessarily. If only a\n            #   script's order in the script list has changed, it is just\n            #   moved.\n\n            current = {}\n            for a in self.addons:\n                current[a.path] = a\n\n            ordered = []\n            newscripts = []\n            for s in ctx.options.scripts:\n                if s in current:\n                    ordered.append(current[s])\n                else:\n                    sc = Script(s)\n                    ordered.append(sc)\n                    newscripts.append(sc)\n\n            self.addons = ordered\n\n            for s in newscripts:\n                ctx.master.addons.register(s)\n                if self.is_running:\n                    # If we're already running, we configure and tell the addon\n                    # we're up and running.\n                    ctx.master.addons.invoke_addon(s, \"running\")\n", "path": "mitmproxy/addons/script.py"}], "after_files": [{"content": "import os\nimport importlib.util\nimport importlib.machinery\nimport time\nimport sys\nimport types\nimport typing\nimport traceback\n\nfrom mitmproxy import addonmanager\nfrom mitmproxy import exceptions\nfrom mitmproxy import flow\nfrom mitmproxy import command\nfrom mitmproxy import eventsequence\nfrom mitmproxy import ctx\nimport mitmproxy.types as mtypes\n\n\ndef load_script(path: str) -> types.ModuleType:\n    fullname = \"__mitmproxy_script__.{}\".format(\n        os.path.splitext(os.path.basename(path))[0]\n    )\n    # the fullname is not unique among scripts, so if there already is an existing script with said\n    # fullname, remove it.\n    sys.modules.pop(fullname, None)\n    oldpath = sys.path\n    sys.path.insert(0, os.path.dirname(path))\n    try:\n        loader = importlib.machinery.SourceFileLoader(fullname, path)\n        spec = importlib.util.spec_from_loader(fullname, loader=loader)\n        m = importlib.util.module_from_spec(spec)\n        loader.exec_module(m)\n        if not getattr(m, \"name\", None):\n            m.name = path  # type: ignore\n        return m\n    finally:\n        sys.path[:] = oldpath\n\n\ndef script_error_handler(path, exc, msg=\"\", tb=False):\n    \"\"\"\n        Handles all the user's script errors with\n        an optional traceback\n    \"\"\"\n    exception = type(exc).__name__\n    if msg:\n        exception = msg\n    lineno = \"\"\n    if hasattr(exc, \"lineno\"):\n        lineno = str(exc.lineno)\n    log_msg = \"in Script {}:{} {}\".format(path, lineno, exception)\n    if tb:\n        etype, value, tback = sys.exc_info()\n        tback = addonmanager.cut_traceback(tback, \"invoke_addon\")\n        log_msg = log_msg.join([\"\\n\"] + traceback.format_exception(etype, value, tback))\n    ctx.log.error(log_msg)\n\n\nclass Script:\n    \"\"\"\n        An addon that manages a single script.\n    \"\"\"\n    ReloadInterval = 2\n\n    def __init__(self, path):\n        self.name = \"scriptmanager:\" + path\n        self.path = path\n        self.fullpath = os.path.expanduser(\n            path.strip(\"'\\\" \")\n        )\n        self.ns = None\n\n        self.last_load = 0\n        self.last_mtime = 0\n        if not os.path.isfile(self.fullpath):\n            raise exceptions.OptionsError('No such script')\n\n    @property\n    def addons(self):\n        return [self.ns] if self.ns else []\n\n    def tick(self):\n        if time.time() - self.last_load > self.ReloadInterval:\n            try:\n                mtime = os.stat(self.fullpath).st_mtime\n            except FileNotFoundError:\n                scripts = list(ctx.options.scripts)\n                scripts.remove(self.path)\n                ctx.options.update(scripts=scripts)\n                return\n\n            if mtime > self.last_mtime:\n                ctx.log.info(\"Loading script: %s\" % self.path)\n                if self.ns:\n                    ctx.master.addons.remove(self.ns)\n                self.ns = None\n                with addonmanager.safecall():\n                    ns = load_script(self.fullpath)\n                    ctx.master.addons.register(ns)\n                    self.ns = ns\n                if self.ns:\n                    # We're already running, so we have to explicitly register and\n                    # configure the addon\n                    ctx.master.addons.invoke_addon(self.ns, \"running\")\n                    ctx.master.addons.invoke_addon(\n                        self.ns,\n                        \"configure\",\n                        ctx.options.keys()\n                    )\n                self.last_load = time.time()\n                self.last_mtime = mtime\n\n\nclass ScriptLoader:\n    \"\"\"\n        An addon that manages loading scripts from options.\n    \"\"\"\n    def __init__(self):\n        self.is_running = False\n        self.addons = []\n\n    def load(self, loader):\n        loader.add_option(\n            \"scripts\", typing.Sequence[str], [],\n            \"\"\"\n            Execute a script.\n            \"\"\"\n        )\n\n    def running(self):\n        self.is_running = True\n\n    @command.command(\"script.run\")\n    def script_run(self, flows: typing.Sequence[flow.Flow], path: mtypes.Path) -> None:\n        \"\"\"\n            Run a script on the specified flows. The script is loaded with\n            default options, and all lifecycle events for each flow are\n            simulated.\n        \"\"\"\n        try:\n            s = Script(path)\n            l = addonmanager.Loader(ctx.master)\n            ctx.master.addons.invoke_addon(s, \"load\", l)\n            ctx.master.addons.invoke_addon(s, \"configure\", ctx.options.keys())\n            # Script is loaded on the first tick\n            ctx.master.addons.invoke_addon(s, \"tick\")\n            for f in flows:\n                for evt, arg in eventsequence.iterate(f):\n                    ctx.master.addons.invoke_addon(s, evt, arg)\n        except exceptions.OptionsError as e:\n            script_error_handler(path, e, msg=str(e))\n\n    def configure(self, updated):\n        if \"scripts\" in updated:\n            for s in ctx.options.scripts:\n                if ctx.options.scripts.count(s) > 1:\n                    raise exceptions.OptionsError(\"Duplicate script\")\n\n            for a in self.addons[:]:\n                if a.path not in ctx.options.scripts:\n                    ctx.log.info(\"Un-loading script: %s\" % a.name)\n                    ctx.master.addons.remove(a)\n                    self.addons.remove(a)\n\n            # The machinations below are to ensure that:\n            #   - Scripts remain in the same order\n            #   - Scripts are not initialized un-necessarily. If only a\n            #   script's order in the script list has changed, it is just\n            #   moved.\n\n            current = {}\n            for a in self.addons:\n                current[a.path] = a\n\n            ordered = []\n            newscripts = []\n            for s in ctx.options.scripts:\n                if s in current:\n                    ordered.append(current[s])\n                else:\n                    sc = Script(s)\n                    ordered.append(sc)\n                    newscripts.append(sc)\n\n            self.addons = ordered\n\n            for s in newscripts:\n                ctx.master.addons.register(s)\n                if self.is_running:\n                    # If we're already running, we configure and tell the addon\n                    # we're up and running.\n                    ctx.master.addons.invoke_addon(s, \"running\")\n", "path": "mitmproxy/addons/script.py"}]}