Format: text/x-python
⬇ Download File (3.8 KB)
   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  
   5  import codecs
   6  import os
   7  import threading
   8  
   9  from debugpy import launcher
  10  from debugpy.common import log
  11  
  12  
  13  class CaptureOutput(object):
  14      """Captures output from the specified file descriptor, and tees it into another
  15      file descriptor while generating DAP "output" events for it.
  16      """
  17  
  18      instances = {}
  19      """Keys are output categories, values are CaptureOutput instances."""
  20  
  21      def __init__(self, whose, category, fd, stream):
  22          assert category not in self.instances
  23          self.instances[category] = self
  24          log.info("Capturing {0} of {1}.", category, whose)
  25  
  26          self.category = category
  27          self._whose = whose
  28          self._fd = fd
  29          self._decoder = codecs.getincrementaldecoder("utf-8")(errors="surrogateescape")
  30  
  31          if stream is None:
  32              # Can happen if running under pythonw.exe.
  33              self._stream = None
  34          else:
  35              self._stream = stream.buffer
  36              encoding = stream.encoding
  37              if encoding is None or encoding == "cp65001":
  38                  encoding = "utf-8"
  39              try:
  40                  self._encode = codecs.getencoder(encoding)
  41              except Exception:
  42                  log.swallow_exception(
  43                      "Unsupported {0} encoding {1!r}; falling back to UTF-8.",
  44                      category,
  45                      encoding,
  46                      level="warning",
  47                  )
  48                  self._encode = codecs.getencoder("utf-8")
  49              else:
  50                  log.info("Using encoding {0!r} for {1}", encoding, category)
  51  
  52          self._worker_thread = threading.Thread(target=self._worker, name=category)
  53          self._worker_thread.start()
  54  
  55      def __del__(self):
  56          fd = self._fd
  57          if fd is not None:
  58              try:
  59                  os.close(fd)
  60              except Exception:
  61                  pass
  62  
  63      def _worker(self):
  64          while self._fd is not None:
  65              try:
  66                  s = os.read(self._fd, 0x1000)
  67              except Exception:
  68                  break
  69              if not len(s):
  70                  break
  71              self._process_chunk(s)
  72  
  73          # Flush any remaining data in the incremental decoder.
  74          self._process_chunk(b"", final=True)
  75  
  76      def _process_chunk(self, s, final=False):
  77          s = self._decoder.decode(s, final=final)
  78          if len(s) == 0:
  79              return
  80  
  81          try:
  82              launcher.channel.send_event(
  83                  "output", {"category": self.category, "output": s.replace("\r\n", "\n")}
  84              )
  85          except Exception:
  86              pass  # channel to adapter is already closed
  87  
  88          if self._stream is None:
  89              return
  90  
  91          try:
  92              s, _ = self._encode(s, "surrogateescape")
  93              size = len(s)
  94              i = 0
  95              while i < size:
  96                  written = self._stream.write(s[i:])
  97                  self._stream.flush()
  98                  if written == 0:
  99                      # This means that the output stream was closed from the other end.
 100                      # Do the same to the debuggee, so that it knows as well.
 101                      os.close(self._fd)
 102                      self._fd = None
 103                      break
 104                  i += written
 105          except Exception:
 106              log.swallow_exception("Error printing {0!r} to {1}", s, self.category)
 107  
 108  
 109  def wait_for_remaining_output():
 110      """Waits for all remaining output to be captured and propagated."""
 111      for category, instance in CaptureOutput.instances.items():
 112          log.info("Waiting for remaining {0} of {1}.", category, instance._whose)
 113          instance._worker_thread.join()
 114