You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			114 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			114 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Python
		
	
| # Copyright (c) Microsoft Corporation. All rights reserved.
 | |
| # Licensed under the MIT License. See LICENSE in the project root
 | |
| # for license information.
 | |
| 
 | |
| 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:
 | |
|             # Can happen if running under pythonw.exe.
 | |
|             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)
 | |
| 
 | |
|         # Flush any remaining data in the incremental decoder.
 | |
|         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  # channel to adapter is already closed
 | |
| 
 | |
|         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:
 | |
|                     # This means that the output stream was closed from the other end.
 | |
|                     # Do the same to the debuggee, so that it knows as well.
 | |
|                     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()
 |