sessions.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. # Copyright (c) Microsoft Corporation. All rights reserved.
  2. # Licensed under the MIT License. See LICENSE in the project root
  3. # for license information.
  4. import itertools
  5. import os
  6. import signal
  7. import threading
  8. import time
  9. from debugpy import common
  10. from debugpy.common import log, util
  11. from debugpy.adapter import components, launchers, servers
  12. _lock = threading.RLock()
  13. _sessions = set()
  14. _sessions_changed = threading.Event()
  15. class Session(util.Observable):
  16. """A debug session involving a client, an adapter, a launcher, and a debug server.
  17. The client and the adapter are always present, and at least one of launcher and debug
  18. server is present, depending on the scenario.
  19. """
  20. _counter = itertools.count(1)
  21. def __init__(self):
  22. from debugpy.adapter import clients
  23. super().__init__()
  24. self.lock = threading.RLock()
  25. self.id = next(self._counter)
  26. self._changed_condition = threading.Condition(self.lock)
  27. self.client = components.missing(self, clients.Client)
  28. """The client component. Always present."""
  29. self.launcher = components.missing(self, launchers.Launcher)
  30. """The launcher componet. Always present in "launch" sessions, and never
  31. present in "attach" sessions.
  32. """
  33. self.server = components.missing(self, servers.Server)
  34. """The debug server component. Always present, unless this is a "launch"
  35. session with "noDebug".
  36. """
  37. self.no_debug = None
  38. """Whether this is a "noDebug" session."""
  39. self.pid = None
  40. """Process ID of the debuggee process."""
  41. self.debug_options = {}
  42. """Debug options as specified by "launch" or "attach" request."""
  43. self.is_finalizing = False
  44. """Whether finalize() has been invoked."""
  45. self.observers += [lambda *_: self.notify_changed()]
  46. def __str__(self):
  47. return f"Session[{self.id}]"
  48. def __enter__(self):
  49. """Lock the session for exclusive access."""
  50. self.lock.acquire()
  51. return self
  52. def __exit__(self, exc_type, exc_value, exc_tb):
  53. """Unlock the session."""
  54. self.lock.release()
  55. def register(self):
  56. with _lock:
  57. _sessions.add(self)
  58. _sessions_changed.set()
  59. def notify_changed(self):
  60. with self:
  61. self._changed_condition.notify_all()
  62. # A session is considered ended once all components disconnect, and there
  63. # are no further incoming messages from anything to handle.
  64. components = self.client, self.launcher, self.server
  65. if all(not com or not com.is_connected for com in components):
  66. with _lock:
  67. if self in _sessions:
  68. log.info("{0} has ended.", self)
  69. _sessions.remove(self)
  70. _sessions_changed.set()
  71. def wait_for(self, predicate, timeout=None):
  72. """Waits until predicate() becomes true.
  73. The predicate is invoked with the session locked. If satisfied, the method
  74. returns immediately. Otherwise, the lock is released (even if it was held
  75. at entry), and the method blocks waiting for some attribute of either self,
  76. self.client, self.server, or self.launcher to change. On every change, session
  77. is re-locked and predicate is re-evaluated, until it is satisfied.
  78. While the session is unlocked, message handlers for components other than
  79. the one that is waiting can run, but message handlers for that one are still
  80. blocked.
  81. If timeout is not None, the method will unblock and return after that many
  82. seconds regardless of whether the predicate was satisfied. The method returns
  83. False if it timed out, and True otherwise.
  84. """
  85. def wait_for_timeout():
  86. time.sleep(timeout)
  87. wait_for_timeout.timed_out = True
  88. self.notify_changed()
  89. wait_for_timeout.timed_out = False
  90. if timeout is not None:
  91. thread = threading.Thread(
  92. target=wait_for_timeout, name="Session.wait_for() timeout"
  93. )
  94. thread.daemon = True
  95. thread.start()
  96. with self:
  97. while not predicate():
  98. if wait_for_timeout.timed_out:
  99. return False
  100. self._changed_condition.wait()
  101. return True
  102. def finalize(self, why, terminate_debuggee=None):
  103. """Finalizes the debug session.
  104. If the server is present, sends "disconnect" request with "terminateDebuggee"
  105. set as specified request to it; waits for it to disconnect, allowing any
  106. remaining messages from it to be handled; and closes the server channel.
  107. If the launcher is present, sends "terminate" request to it, regardless of the
  108. value of terminate; waits for it to disconnect, allowing any remaining messages
  109. from it to be handled; and closes the launcher channel.
  110. If the client is present, sends "terminated" event to it.
  111. If terminate_debuggee=None, it is treated as True if the session has a Launcher
  112. component, and False otherwise.
  113. """
  114. if self.is_finalizing:
  115. return
  116. self.is_finalizing = True
  117. log.info("{0}; finalizing {1}.", why, self)
  118. if terminate_debuggee is None:
  119. terminate_debuggee = bool(self.launcher)
  120. try:
  121. self._finalize(why, terminate_debuggee)
  122. except Exception:
  123. # Finalization should never fail, and if it does, the session is in an
  124. # indeterminate and likely unrecoverable state, so just fail fast.
  125. log.swallow_exception("Fatal error while finalizing {0}", self)
  126. os._exit(1)
  127. log.info("{0} finalized.", self)
  128. def _finalize(self, why, terminate_debuggee):
  129. # If the client started a session, and then disconnected before issuing "launch"
  130. # or "attach", the main thread will be blocked waiting for the first server
  131. # connection to come in - unblock it, so that we can exit.
  132. servers.dont_wait_for_first_connection()
  133. if self.server:
  134. if self.server.is_connected:
  135. if terminate_debuggee and self.launcher and self.launcher.is_connected:
  136. # If we were specifically asked to terminate the debuggee, and we
  137. # can ask the launcher to kill it, do so instead of disconnecting
  138. # from the server to prevent debuggee from running any more code.
  139. self.launcher.terminate_debuggee()
  140. else:
  141. # Otherwise, let the server handle it the best it can.
  142. try:
  143. self.server.channel.request(
  144. "disconnect", {"terminateDebuggee": terminate_debuggee}
  145. )
  146. except Exception:
  147. pass
  148. self.server.detach_from_session()
  149. if self.launcher and self.launcher.is_connected:
  150. # If there was a server, we just disconnected from it above, which should
  151. # cause the debuggee process to exit, unless it is being replaced in situ -
  152. # so let's wait for that first.
  153. if self.server and not self.server.connection.process_replaced:
  154. log.info('{0} waiting for "exited" event...', self)
  155. if not self.wait_for(
  156. lambda: self.launcher.exit_code is not None,
  157. timeout=common.PROCESS_EXIT_TIMEOUT,
  158. ):
  159. log.warning('{0} timed out waiting for "exited" event.', self)
  160. # Terminate the debuggee process if it's still alive for any reason -
  161. # whether it's because there was no server to handle graceful shutdown,
  162. # or because the server couldn't handle it for some reason - unless the
  163. # process is being replaced in situ.
  164. if not (self.server and self.server.connection.process_replaced):
  165. self.launcher.terminate_debuggee()
  166. # Wait until the launcher message queue fully drains. There is no timeout
  167. # here, because the final "terminated" event will only come after reading
  168. # user input in wait-on-exit scenarios. In addition, if the process was
  169. # replaced in situ, the launcher might still have more output to capture
  170. # from its replacement.
  171. log.info("{0} waiting for {1} to disconnect...", self, self.launcher)
  172. self.wait_for(lambda: not self.launcher.is_connected)
  173. try:
  174. self.launcher.channel.close()
  175. except Exception:
  176. log.swallow_exception()
  177. if self.client:
  178. if self.client.is_connected:
  179. # Tell the client that debugging is over, but don't close the channel until it
  180. # tells us to, via the "disconnect" request.
  181. body = {}
  182. if self.client.restart_requested:
  183. body["restart"] = True
  184. try:
  185. self.client.channel.send_event("terminated", body)
  186. except Exception:
  187. pass
  188. if (
  189. self.client.start_request is not None
  190. and self.client.start_request.command == "launch"
  191. and not (self.server and self.server.connection.process_replaced)
  192. ):
  193. servers.stop_serving()
  194. log.info(
  195. '"launch" session ended - killing remaining debuggee processes.'
  196. )
  197. pids_killed = set()
  198. if self.launcher and self.launcher.pid is not None:
  199. # Already killed above.
  200. pids_killed.add(self.launcher.pid)
  201. while True:
  202. conns = [
  203. conn
  204. for conn in servers.connections()
  205. if conn.pid not in pids_killed
  206. ]
  207. if not len(conns):
  208. break
  209. for conn in conns:
  210. log.info("Killing {0}", conn)
  211. try:
  212. os.kill(conn.pid, signal.SIGTERM)
  213. except Exception:
  214. log.swallow_exception("Failed to kill {0}", conn)
  215. pids_killed.add(conn.pid)
  216. def get(pid):
  217. with _lock:
  218. return next((session for session in _sessions if session.pid == pid), None)
  219. def wait_until_ended():
  220. """Blocks until all sessions have ended.
  221. A session ends when all components that it manages disconnect from it.
  222. """
  223. while True:
  224. with _lock:
  225. if not len(_sessions):
  226. return
  227. _sessions_changed.clear()
  228. _sessions_changed.wait()
  229. def report_sockets():
  230. if not _sessions:
  231. return
  232. session = sorted(_sessions, key=lambda session: session.id)[0]
  233. client = session.client
  234. if client is not None:
  235. client.report_sockets()