|
|
|
|
|
|
|
|
|
import codecs |
|
import os |
|
import threading |
|
|
|
from debugpy import launcher |
|
from debugpy.common import log |
|
|
|
|
|
class CaptureOutput(object): |
|
"""Captures output from the specified file descriptor, and tees it into another |
|
file descriptor while generating DAP "output" events for it. |
|
""" |
|
|
|
instances = {} |
|
"""Keys are output categories, values are CaptureOutput instances.""" |
|
|
|
def __init__(self, whose, category, fd, stream): |
|
assert category not in self.instances |
|
self.instances[category] = self |
|
log.info("Capturing {0} of {1}.", category, whose) |
|
|
|
self.category = category |
|
self._whose = whose |
|
self._fd = fd |
|
self._decoder = codecs.getincrementaldecoder("utf-8")(errors="surrogateescape") |
|
|
|
if stream is None: |
|
|
|
self._stream = None |
|
else: |
|
self._stream = stream.buffer |
|
encoding = stream.encoding |
|
if encoding is None or encoding == "cp65001": |
|
encoding = "utf-8" |
|
try: |
|
self._encode = codecs.getencoder(encoding) |
|
except Exception: |
|
log.swallow_exception( |
|
"Unsupported {0} encoding {1!r}; falling back to UTF-8.", |
|
category, |
|
encoding, |
|
level="warning", |
|
) |
|
self._encode = codecs.getencoder("utf-8") |
|
else: |
|
log.info("Using encoding {0!r} for {1}", encoding, category) |
|
|
|
self._worker_thread = threading.Thread(target=self._worker, name=category) |
|
self._worker_thread.start() |
|
|
|
def __del__(self): |
|
fd = self._fd |
|
if fd is not None: |
|
try: |
|
os.close(fd) |
|
except Exception: |
|
pass |
|
|
|
def _worker(self): |
|
while self._fd is not None: |
|
try: |
|
s = os.read(self._fd, 0x1000) |
|
except Exception: |
|
break |
|
if not len(s): |
|
break |
|
self._process_chunk(s) |
|
|
|
|
|
self._process_chunk(b"", final=True) |
|
|
|
def _process_chunk(self, s, final=False): |
|
s = self._decoder.decode(s, final=final) |
|
if len(s) == 0: |
|
return |
|
|
|
try: |
|
launcher.channel.send_event( |
|
"output", {"category": self.category, "output": s.replace("\r\n", "\n")} |
|
) |
|
except Exception: |
|
pass |
|
|
|
if self._stream is None: |
|
return |
|
|
|
try: |
|
s, _ = self._encode(s, "surrogateescape") |
|
size = len(s) |
|
i = 0 |
|
while i < size: |
|
written = self._stream.write(s[i:]) |
|
self._stream.flush() |
|
if written == 0: |
|
|
|
|
|
os.close(self._fd) |
|
self._fd = None |
|
break |
|
i += written |
|
except Exception: |
|
log.swallow_exception("Error printing {0!r} to {1}", s, self.category) |
|
|
|
|
|
def wait_for_remaining_output(): |
|
"""Waits for all remaining output to be captured and propagated.""" |
|
for category, instance in CaptureOutput.instances.items(): |
|
log.info("Waiting for remaining {0} of {1}.", category, instance._whose) |
|
instance._worker_thread.join() |
|
|