prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Interrupting an `(Interactive)Process` should attempt to kill the process gracefully
  Currently, both `Process` and `InteractiveProcess` will send `KILL` when they are dropped (via `tokio`s facilities for `kill_on_drop`). But in both cases, kill doesn't give the underlying process time to do its own teardown, and this can lead to resource leaks. `pants` itself has [concerns](https://github.com/pantsbuild/pants/issues/12996) with being killed without getting a TERM first, and end user processes like `uvicorn` would like to be able to run their user shutdown hooks before dying.
  
  To do this, we will likely need to implement a workaround for the lack of `AsyncDrop`: by essentially adding an optional `TERM` and synchronous wait in a `Drop` guard wrapped around relevant processes, or optionally gracefully interrupting graph nodes.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `testprojects/src/python/coordinated_runs/waiter.py`
  Content:
  ```
  1 # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
  2 # Licensed under the Apache License, Version 2.0 (see LICENSE).
  3 
  4 
  5 import os
  6 import sys
  7 import time
  8 from multiprocessing import Process
  9 
  10 waiting_for_file = sys.argv[1]
  11 pid_file = sys.argv[2]
  12 child_pid_file = sys.argv[3]
  13 attempts = 60
  14 
  15 
  16 def run_child():
  17     while True:
  18         print("Child running...")
  19         time.sleep(1)
  20 
  21 
  22 child = Process(target=run_child, daemon=True)
  23 child.start()
  24 
  25 with open(child_pid_file, "w") as pf:
  26     pf.write(str(child.pid))
  27 
  28 with open(pid_file, "w") as pf:
  29     pf.write(str(os.getpid()))
  30 
  31 try:
  32     while not os.path.isfile(waiting_for_file):
  33         if attempts <= 0:
  34             raise Exception("File was never written.")
  35         attempts -= 1
  36         sys.stderr.write("Waiting for file {}\n".format(waiting_for_file))
  37         time.sleep(1)
  38 finally:
  39     child.terminate()
  40
  ```
  --- 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/testprojects/src/python/coordinated_runs/waiter.py b/testprojects/src/python/coordinated_runs/waiter.py
  --- a/testprojects/src/python/coordinated_runs/waiter.py
  +++ b/testprojects/src/python/coordinated_runs/waiter.py
  @@ -7,11 +7,6 @@
   import time
   from multiprocessing import Process
   
  -waiting_for_file = sys.argv[1]
  -pid_file = sys.argv[2]
  -child_pid_file = sys.argv[3]
  -attempts = 60
  -
   
   def run_child():
       while True:
  @@ -19,21 +14,46 @@
           time.sleep(1)
   
   
  -child = Process(target=run_child, daemon=True)
  -child.start()
  +def main():
  +    waiting_for_file = sys.argv[1]
  +    pid_file = sys.argv[2]
  +    child_pid_file = sys.argv[3]
  +    cleanup_wait_time = int(sys.argv[4])
  +    attempts = 60
   
  -with open(child_pid_file, "w") as pf:
  -    pf.write(str(child.pid))
  +    child = Process(target=run_child, daemon=True)
  +    child.start()
   
  -with open(pid_file, "w") as pf:
  -    pf.write(str(os.getpid()))
  +    with open(child_pid_file, "w") as pf:
  +        pf.write(str(child.pid))
   
  -try:
  -    while not os.path.isfile(waiting_for_file):
  -        if attempts <= 0:
  -            raise Exception("File was never written.")
  -        attempts -= 1
  -        sys.stderr.write("Waiting for file {}\n".format(waiting_for_file))
  -        time.sleep(1)
  -finally:
  -    child.terminate()
  +    with open(pid_file, "w") as pf:
  +        pf.write(str(os.getpid()))
  +
  +    try:
  +        while not os.path.isfile(waiting_for_file):
  +            if attempts <= 0:
  +                raise Exception("File was never written.")
  +            attempts -= 1
  +            sys.stderr.write("Waiting for file {}\n".format(waiting_for_file))
  +            sys.stderr.flush()
  +            time.sleep(1)
  +
  +    except KeyboardInterrupt:
  +        sys.stderr.write("keyboard int received\n")
  +        sys.stderr.flush()
  +
  +    finally:
  +        sys.stderr.write("waiter cleaning up\n")
  +        sys.stderr.flush()
  +
  +        child.terminate()
  +        if cleanup_wait_time > 0:
  +            time.sleep(cleanup_wait_time)
  +
  +        sys.stderr.write("waiter cleanup complete\n")
  +        sys.stderr.flush()
  +
  +
  +if __name__ == "__main__":
  +    main()
  
verification_info: {"golden_diff": "diff --git a/testprojects/src/python/coordinated_runs/waiter.py b/testprojects/src/python/coordinated_runs/waiter.py\n--- a/testprojects/src/python/coordinated_runs/waiter.py\n+++ b/testprojects/src/python/coordinated_runs/waiter.py\n@@ -7,11 +7,6 @@\n import time\n from multiprocessing import Process\n \n-waiting_for_file = sys.argv[1]\n-pid_file = sys.argv[2]\n-child_pid_file = sys.argv[3]\n-attempts = 60\n-\n \n def run_child():\n     while True:\n@@ -19,21 +14,46 @@\n         time.sleep(1)\n \n \n-child = Process(target=run_child, daemon=True)\n-child.start()\n+def main():\n+    waiting_for_file = sys.argv[1]\n+    pid_file = sys.argv[2]\n+    child_pid_file = sys.argv[3]\n+    cleanup_wait_time = int(sys.argv[4])\n+    attempts = 60\n \n-with open(child_pid_file, \"w\") as pf:\n-    pf.write(str(child.pid))\n+    child = Process(target=run_child, daemon=True)\n+    child.start()\n \n-with open(pid_file, \"w\") as pf:\n-    pf.write(str(os.getpid()))\n+    with open(child_pid_file, \"w\") as pf:\n+        pf.write(str(child.pid))\n \n-try:\n-    while not os.path.isfile(waiting_for_file):\n-        if attempts <= 0:\n-            raise Exception(\"File was never written.\")\n-        attempts -= 1\n-        sys.stderr.write(\"Waiting for file {}\\n\".format(waiting_for_file))\n-        time.sleep(1)\n-finally:\n-    child.terminate()\n+    with open(pid_file, \"w\") as pf:\n+        pf.write(str(os.getpid()))\n+\n+    try:\n+        while not os.path.isfile(waiting_for_file):\n+            if attempts <= 0:\n+                raise Exception(\"File was never written.\")\n+            attempts -= 1\n+            sys.stderr.write(\"Waiting for file {}\\n\".format(waiting_for_file))\n+            sys.stderr.flush()\n+            time.sleep(1)\n+\n+    except KeyboardInterrupt:\n+        sys.stderr.write(\"keyboard int received\\n\")\n+        sys.stderr.flush()\n+\n+    finally:\n+        sys.stderr.write(\"waiter cleaning up\\n\")\n+        sys.stderr.flush()\n+\n+        child.terminate()\n+        if cleanup_wait_time > 0:\n+            time.sleep(cleanup_wait_time)\n+\n+        sys.stderr.write(\"waiter cleanup complete\\n\")\n+        sys.stderr.flush()\n+\n+\n+if __name__ == \"__main__\":\n+    main()\n", "issue": "Interrupting an `(Interactive)Process` should attempt to kill the process gracefully\nCurrently, both `Process` and `InteractiveProcess` will send `KILL` when they are dropped (via `tokio`s facilities for `kill_on_drop`). But in both cases, kill doesn't give the underlying process time to do its own teardown, and this can lead to resource leaks. `pants` itself has [concerns](https://github.com/pantsbuild/pants/issues/12996) with being killed without getting a TERM first, and end user processes like `uvicorn` would like to be able to run their user shutdown hooks before dying.\r\n\r\nTo do this, we will likely need to implement a workaround for the lack of `AsyncDrop`: by essentially adding an optional `TERM` and synchronous wait in a `Drop` guard wrapped around relevant processes, or optionally gracefully interrupting graph nodes.\n", "before_files": [{"content": "# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).\n# Licensed under the Apache License, Version 2.0 (see LICENSE).\n\n\nimport os\nimport sys\nimport time\nfrom multiprocessing import Process\n\nwaiting_for_file = sys.argv[1]\npid_file = sys.argv[2]\nchild_pid_file = sys.argv[3]\nattempts = 60\n\n\ndef run_child():\n    while True:\n        print(\"Child running...\")\n        time.sleep(1)\n\n\nchild = Process(target=run_child, daemon=True)\nchild.start()\n\nwith open(child_pid_file, \"w\") as pf:\n    pf.write(str(child.pid))\n\nwith open(pid_file, \"w\") as pf:\n    pf.write(str(os.getpid()))\n\ntry:\n    while not os.path.isfile(waiting_for_file):\n        if attempts <= 0:\n            raise Exception(\"File was never written.\")\n        attempts -= 1\n        sys.stderr.write(\"Waiting for file {}\\n\".format(waiting_for_file))\n        time.sleep(1)\nfinally:\n    child.terminate()\n", "path": "testprojects/src/python/coordinated_runs/waiter.py"}], "after_files": [{"content": "# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).\n# Licensed under the Apache License, Version 2.0 (see LICENSE).\n\n\nimport os\nimport sys\nimport time\nfrom multiprocessing import Process\n\n\ndef run_child():\n    while True:\n        print(\"Child running...\")\n        time.sleep(1)\n\n\ndef main():\n    waiting_for_file = sys.argv[1]\n    pid_file = sys.argv[2]\n    child_pid_file = sys.argv[3]\n    cleanup_wait_time = int(sys.argv[4])\n    attempts = 60\n\n    child = Process(target=run_child, daemon=True)\n    child.start()\n\n    with open(child_pid_file, \"w\") as pf:\n        pf.write(str(child.pid))\n\n    with open(pid_file, \"w\") as pf:\n        pf.write(str(os.getpid()))\n\n    try:\n        while not os.path.isfile(waiting_for_file):\n            if attempts <= 0:\n                raise Exception(\"File was never written.\")\n            attempts -= 1\n            sys.stderr.write(\"Waiting for file {}\\n\".format(waiting_for_file))\n            sys.stderr.flush()\n            time.sleep(1)\n\n    except KeyboardInterrupt:\n        sys.stderr.write(\"keyboard int received\\n\")\n        sys.stderr.flush()\n\n    finally:\n        sys.stderr.write(\"waiter cleaning up\\n\")\n        sys.stderr.flush()\n\n        child.terminate()\n        if cleanup_wait_time > 0:\n            time.sleep(cleanup_wait_time)\n\n        sys.stderr.write(\"waiter cleanup complete\\n\")\n        sys.stderr.flush()\n\n\nif __name__ == \"__main__\":\n    main()\n", "path": "testprojects/src/python/coordinated_runs/waiter.py"}]}