prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Script Reloader does not unload old script
  ##### Steps to reproduce the problem:
  
  1. mitmdump -s script.py
  2. Modify script on disk.
  
  ##### System information
  
  Mitmproxy version: 3.0.0 (2.0.0dev0407-0x315daa0)
  Python version: 3.5.2
  Platform: Linux-4.4.0-43-Microsoft-x86_64-with-Ubuntu-16.04-xenial
  SSL version: OpenSSL 1.0.2g  1 Mar 2016
  Linux distro: Ubuntu 16.04 xenial
  
  --- 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
  3 import time
  4 import sys
  5 import typing
  6 
  7 from mitmproxy import addonmanager
  8 from mitmproxy import exceptions
  9 from mitmproxy import flow
  10 from mitmproxy import command
  11 from mitmproxy import eventsequence
  12 from mitmproxy import ctx
  13 
  14 
  15 def load_script(actx, path):
  16     if not os.path.exists(path):
  17         ctx.log.info("No such file: %s" % path)
  18         return
  19     loader = importlib.machinery.SourceFileLoader(os.path.basename(path), path)
  20     try:
  21         oldpath = sys.path
  22         sys.path.insert(0, os.path.dirname(path))
  23         with addonmanager.safecall():
  24             m = loader.load_module()
  25             if not getattr(m, "name", None):
  26                 m.name = path
  27             return m
  28     finally:
  29         sys.path[:] = oldpath
  30 
  31 
  32 class Script:
  33     """
  34         An addon that manages a single script.
  35     """
  36     ReloadInterval = 2
  37 
  38     def __init__(self, path):
  39         self.name = "scriptmanager:" + path
  40         self.path = path
  41         self.fullpath = os.path.expanduser(path)
  42         self.ns = None
  43 
  44         self.last_load = 0
  45         self.last_mtime = 0
  46         if not os.path.isfile(self.fullpath):
  47             raise exceptions.OptionsError("No such script: %s" % path)
  48 
  49     @property
  50     def addons(self):
  51         return [self.ns] if self.ns else []
  52 
  53     def tick(self):
  54         if time.time() - self.last_load > self.ReloadInterval:
  55             mtime = os.stat(self.fullpath).st_mtime
  56             if mtime > self.last_mtime:
  57                 ctx.log.info("Loading script: %s" % self.path)
  58                 if self.ns:
  59                     ctx.master.addons.remove(self.ns)
  60                 self.ns = load_script(ctx, self.fullpath)
  61                 if self.ns:
  62                     # We're already running, so we have to explicitly register and
  63                     # configure the addon
  64                     ctx.master.addons.register(self.ns)
  65                     ctx.master.addons.invoke_addon(self.ns, "running")
  66                     ctx.master.addons.invoke_addon(
  67                         self.ns,
  68                         "configure",
  69                         ctx.options.keys()
  70                     )
  71                 self.last_load = time.time()
  72                 self.last_mtime = mtime
  73 
  74 
  75 class ScriptLoader:
  76     """
  77         An addon that manages loading scripts from options.
  78     """
  79     def __init__(self):
  80         self.is_running = False
  81         self.addons = []
  82 
  83     def running(self):
  84         self.is_running = True
  85 
  86     @command.command("script.run")
  87     def script_run(self, flows: typing.Sequence[flow.Flow], path: str) -> None:
  88         """
  89             Run a script on the specified flows. The script is loaded with
  90             default options, and all lifecycle events for each flow are
  91             simulated.
  92         """
  93         try:
  94             s = Script(path)
  95             l = addonmanager.Loader(ctx.master)
  96             ctx.master.addons.invoke_addon(s, "load", l)
  97             ctx.master.addons.invoke_addon(s, "configure", ctx.options.keys())
  98             # Script is loaded on the first tick
  99             ctx.master.addons.invoke_addon(s, "tick")
  100             for f in flows:
  101                 for evt, arg in eventsequence.iterate(f):
  102                     ctx.master.addons.invoke_addon(s, evt, arg)
  103         except exceptions.OptionsError as e:
  104             raise exceptions.CommandError("Error running script: %s" % e) from e
  105 
  106     def configure(self, updated):
  107         if "scripts" in updated:
  108             for s in ctx.options.scripts:
  109                 if ctx.options.scripts.count(s) > 1:
  110                     raise exceptions.OptionsError("Duplicate script: %s" % s)
  111 
  112             for a in self.addons[:]:
  113                 if a.path not in ctx.options.scripts:
  114                     ctx.log.info("Un-loading script: %s" % a.name)
  115                     ctx.master.addons.remove(a)
  116                     self.addons.remove(a)
  117 
  118             # The machinations below are to ensure that:
  119             #   - Scripts remain in the same order
  120             #   - Scripts are not initialized un-necessarily. If only a
  121             #   script's order in the script list has changed, it is just
  122             #   moved.
  123 
  124             current = {}
  125             for a in self.addons:
  126                 current[a.path] = a
  127 
  128             ordered = []
  129             newscripts = []
  130             for s in ctx.options.scripts:
  131                 if s in current:
  132                     ordered.append(current[s])
  133                 else:
  134                     sc = Script(s)
  135                     ordered.append(sc)
  136                     newscripts.append(sc)
  137 
  138             self.addons = ordered
  139 
  140             for s in newscripts:
  141                 ctx.master.addons.register(s)
  142                 if self.is_running:
  143                     # If we're already running, we configure and tell the addon
  144                     # we're up and running.
  145                     ctx.master.addons.invoke_addon(s, "running")
  146
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/mitmproxy/addons/script.py b/mitmproxy/addons/script.py
  --- a/mitmproxy/addons/script.py
  +++ b/mitmproxy/addons/script.py
  @@ -52,11 +52,19 @@
   
       def tick(self):
           if time.time() - self.last_load > self.ReloadInterval:
  -            mtime = os.stat(self.fullpath).st_mtime
  +            try:
  +                mtime = os.stat(self.fullpath).st_mtime
  +            except FileNotFoundError:
  +                scripts = ctx.options.scripts
  +                scripts.remove(self.path)
  +                ctx.options.update(scripts=scripts)
  +                return
  +
               if mtime > self.last_mtime:
                   ctx.log.info("Loading script: %s" % self.path)
                   if self.ns:
                       ctx.master.addons.remove(self.ns)
  +                    del sys.modules[self.ns.__name__]
                   self.ns = load_script(ctx, self.fullpath)
                   if self.ns:
                       # We're already running, so we have to explicitly register and
  
verification_info: {"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@@ -52,11 +52,19 @@\n \n     def tick(self):\n         if time.time() - self.last_load > self.ReloadInterval:\n-            mtime = os.stat(self.fullpath).st_mtime\n+            try:\n+                mtime = os.stat(self.fullpath).st_mtime\n+            except FileNotFoundError:\n+                scripts = 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+                    del sys.modules[self.ns.__name__]\n                 self.ns = load_script(ctx, self.fullpath)\n                 if self.ns:\n                     # We're already running, so we have to explicitly register and\n", "issue": "Script Reloader does not unload old script\n##### Steps to reproduce the problem:\r\n\r\n1. mitmdump -s script.py\r\n2. Modify script on disk.\r\n\r\n##### System information\r\n\r\nMitmproxy version: 3.0.0 (2.0.0dev0407-0x315daa0)\r\nPython version: 3.5.2\r\nPlatform: Linux-4.4.0-43-Microsoft-x86_64-with-Ubuntu-16.04-xenial\r\nSSL version: OpenSSL 1.0.2g  1 Mar 2016\r\nLinux distro: Ubuntu 16.04 xenial\n", "before_files": [{"content": "import os\nimport importlib\nimport time\nimport sys\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\n\n\ndef load_script(actx, path):\n    if not os.path.exists(path):\n        ctx.log.info(\"No such file: %s\" % path)\n        return\n    loader = importlib.machinery.SourceFileLoader(os.path.basename(path), path)\n    try:\n        oldpath = sys.path\n        sys.path.insert(0, os.path.dirname(path))\n        with addonmanager.safecall():\n            m = loader.load_module()\n            if not getattr(m, \"name\", None):\n                m.name = path\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(path)\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\" % path)\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            mtime = os.stat(self.fullpath).st_mtime\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 = load_script(ctx, self.fullpath)\n                if self.ns:\n                    # We're already running, so we have to explicitly register and\n                    # configure the addon\n                    ctx.master.addons.register(self.ns)\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 running(self):\n        self.is_running = True\n\n    @command.command(\"script.run\")\n    def script_run(self, flows: typing.Sequence[flow.Flow], path: str) -> 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\nimport time\nimport sys\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\n\n\ndef load_script(actx, path):\n    if not os.path.exists(path):\n        ctx.log.info(\"No such file: %s\" % path)\n        return\n    loader = importlib.machinery.SourceFileLoader(os.path.basename(path), path)\n    try:\n        oldpath = sys.path\n        sys.path.insert(0, os.path.dirname(path))\n        with addonmanager.safecall():\n            m = loader.load_module()\n            if not getattr(m, \"name\", None):\n                m.name = path\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(path)\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\" % path)\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 = 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                    del sys.modules[self.ns.__name__]\n                self.ns = load_script(ctx, self.fullpath)\n                if self.ns:\n                    # We're already running, so we have to explicitly register and\n                    # configure the addon\n                    ctx.master.addons.register(self.ns)\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 running(self):\n        self.is_running = True\n\n    @command.command(\"script.run\")\n    def script_run(self, flows: typing.Sequence[flow.Flow], path: str) -> 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"}]}