src/eric7/DebugClients/Python/AsyncFile.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8954
c8b027c654bc
child 9221
bf71ee032bb4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/DebugClients/Python/AsyncFile.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,378 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing an asynchronous file like socket interface for the
+debugger.
+"""
+
+import socket
+import threading
+import contextlib
+
+from DebugUtilities import prepareJsonCommand
+
+
+def AsyncPendingWrite(file):
+    """
+    Module function to check for data to be written.
+    
+    @param file The file object to be checked
+    @type file
+    @return Flag indicating if there is data waiting
+    @rtype int
+    """
+    try:
+        pending = file.pendingWrite()
+    except Exception:
+        pending = 0
+
+    return pending
+
+
+class AsyncFile:
+    """
+    Class wrapping a socket object with a file interface.
+    """
+    MAX_TRIES = 10
+    
+    BUFSIZE = 2 ** 14           # 16 kBytes
+    CMD_BUFSIZE = 2 ** 12       # 4 kBytes
+    
+    def __init__(self, sock, mode, name):
+        """
+        Constructor
+        
+        @param sock the socket object being wrapped
+        @type socket
+        @param mode mode of this file
+        @type str
+        @param name name of this file
+        @type str
+        """
+        # Initialise the attributes.
+        self.closed = False
+        self.sock = sock
+        self.mode = mode
+        self.name = name
+        self.nWriteErrors = 0
+        self.encoding = "utf-8"
+        self.errors = None
+        self.newlines = None
+        self.line_buffering = False
+        
+        self.writeLock = threading.RLock()
+        self.wpending = []
+
+    def __checkMode(self, mode):
+        """
+        Private method to check the mode.
+        
+        This method checks, if an operation is permitted according to
+        the mode of the file. If it is not, an OSError is raised.
+        
+        @param mode the mode to be checked
+        @type string
+        @exception OSError raised to indicate a bad file descriptor
+        """
+        if mode != self.mode:
+            raise OSError((9, '[Errno 9] Bad file descriptor'))
+
+    def pendingWrite(self):
+        """
+        Public method that returns the number of strings waiting to be written.
+        
+        @return the number of strings to be written
+        @rtype int
+        """
+        return len(self.wpending)
+
+    def close(self, closeit=False):
+        """
+        Public method to close the file.
+        
+        @param closeit flag to indicate a close ordered by the debugger code
+        @type bool
+        """
+        if closeit and not self.closed:
+            self.flush()
+            self.sock.close()
+            self.closed = True
+
+    def flush(self):
+        """
+        Public method to write all pending entries.
+        """
+        self.writeLock.acquire()
+        while self.wpending:
+            try:
+                buf = self.wpending.pop(0)
+            except IndexError:
+                break
+            
+            try:
+                with contextlib.suppress(UnicodeEncodeError,
+                                         UnicodeDecodeError):
+                    buf = buf.encode('utf-8', 'backslashreplace')
+                self.sock.sendall(buf)
+                self.nWriteErrors = 0
+            except OSError:
+                self.nWriteErrors += 1
+                if self.nWriteErrors > AsyncFile.MAX_TRIES:
+                    self.wpending = []    # delete all output
+        self.writeLock.release()
+
+    def isatty(self):
+        """
+        Public method to indicate whether a tty interface is supported.
+        
+        @return always false
+        @rtype bool
+        """
+        return False
+
+    def fileno(self):
+        """
+        Public method returning the file number.
+        
+        @return file number
+        @rtype int
+        """
+        try:
+            return self.sock.fileno()
+        except OSError:
+            return -1
+
+    def readable(self):
+        """
+        Public method to check, if the stream is readable.
+        
+        @return flag indicating a readable stream
+        @rtype bool
+        """
+        return self.mode == "r"
+    
+    def read_p(self, size=-1):
+        """
+        Public method to read bytes from this file.
+        
+        @param size maximum number of bytes to be read
+        @type int
+        @return the bytes read
+        @rtype str
+        """
+        self.__checkMode('r')
+
+        if size < 0:
+            size = AsyncFile.BUFSIZE
+
+        return self.sock.recv(size).decode('utf8', 'backslashreplace')
+
+    def read(self, size=-1):
+        """
+        Public method to read bytes from this file.
+        
+        @param size maximum number of bytes to be read
+        @type int
+        @return the bytes read
+        @rtype str
+        """
+        self.__checkMode('r')
+
+        buf = input()       # secok
+        if size >= 0:
+            buf = buf[:size]
+        return buf
+    
+    def readCommand(self):
+        """
+        Public method to read a length prefixed command string.
+        
+        @return command string
+        @rtype str
+        """
+        # The command string is prefixed by a 9 character long length field.
+        length = self.sock.recv(9)
+        length = int(length)
+        data = b''
+        while len(data) < length:
+            remaining = length - len(data)
+            newBytes = self.sock.recv(min(remaining, AsyncFile.CMD_BUFSIZE))
+            data += newBytes
+            if newBytes[-1] == b'\n':
+                break
+        
+        # step 2: convert the data
+        return data.decode('utf8', 'backslashreplace')
+    
+    def readline_p(self, size=-1):
+        """
+        Public method to read a line from this file.
+        
+        <b>Note</b>: This method will not block and may return
+        only a part of a line if that is all that is available.
+        
+        @param size maximum number of bytes to be read
+        @type int
+        @return one line of text up to size bytes
+        @rtype str
+        """
+        self.__checkMode('r')
+
+        if size < 0:
+            size = AsyncFile.BUFSIZE
+
+        # The integration of the debugger client event loop and the connection
+        # to the debugger relies on the two lines of the debugger command being
+        # delivered as two separate events.  Therefore we make sure we only
+        # read a line at a time.
+        line = self.sock.recv(size, socket.MSG_PEEK)
+
+        eol = line.find(b'\n')
+        size = eol + 1 if eol >= 0 else len(line)
+
+        # Now we know how big the line is, read it for real.
+        return self.sock.recv(size).decode('utf8', 'backslashreplace')
+
+    def readlines(self, sizehint=-1):
+        """
+        Public method to read all lines from this file.
+        
+        @param sizehint hint of the numbers of bytes to be read
+        @type int
+        @return list of lines read
+        @rtype list of str
+        """
+        self.__checkMode('r')
+
+        lines = []
+        room = sizehint
+
+        line = self.readline_p(room)
+        linelen = len(line)
+
+        while linelen > 0:
+            lines.append(line)
+
+            if sizehint >= 0:
+                room -= linelen
+
+                if room <= 0:
+                    break
+
+            line = self.readline_p(room)
+            linelen = len(line)
+
+        return lines
+
+    def readline(self, sizehint=-1):
+        """
+        Public method to read one line from this file.
+        
+        @param sizehint hint of the numbers of bytes to be read
+        @type int
+        @return one line read
+        @rtype str
+        """
+        self.__checkMode('r')
+
+        line = input() + '\n'           # secok
+        if sizehint >= 0:
+            line = line[:sizehint]
+        return line
+    
+    def seekable(self):
+        """
+        Public method to check, if the stream is seekable.
+        
+        @return flag indicating a seekable stream
+        @rtype bool
+        """
+        return False
+    
+    def seek(self, offset, whence=0):
+        """
+        Public method to move the filepointer.
+        
+        @param offset offset to move the filepointer to
+        @type int
+        @param whence position the offset relates to
+        @type int
+        @exception OSError This method is not supported and always raises an
+        OSError.
+        """
+        raise OSError((29, '[Errno 29] Illegal seek'))
+
+    def tell(self):
+        """
+        Public method to get the filepointer position.
+        
+        @exception OSError This method is not supported and always raises an
+        OSError.
+        """
+        raise OSError((29, '[Errno 29] Illegal seek'))
+
+    def truncate(self, size=-1):
+        """
+        Public method to truncate the file.
+        
+        @param size size to truncate to
+        @type int
+        @exception OSError This method is not supported and always raises an
+        OSError.
+        """
+        raise OSError((29, '[Errno 29] Illegal seek'))
+
+    def writable(self):
+        """
+        Public method to check, if a stream is writable.
+        
+        @return flag indicating a writable stream
+        @rtype bool
+        """
+        return self.mode == "w"
+    
+    def write(self, s):
+        """
+        Public method to write a string to the file.
+        
+        @param s text to be written
+        @type str, bytes or bytearray
+        """
+        self.__checkMode('w')
+        
+        self.writeLock.acquire()
+        if isinstance(s, (bytes, bytearray)):
+            # convert to string to send it
+            s = repr(s)
+        
+        cmd = prepareJsonCommand("ClientOutput", {
+            "text": s,
+            "debuggerId": "",
+        })
+        self.wpending.append(cmd)
+        self.flush()
+        self.writeLock.release()
+    
+    def write_p(self, s):
+        """
+        Public method to write a json-rpc 2.0 coded string to the file.
+        
+        @param s text to be written
+        @type str
+        """
+        self.__checkMode('w')
+
+        self.wpending.append(s)
+        self.flush()
+
+    def writelines(self, lines):
+        """
+        Public method to write a list of strings to the file.
+        
+        @param lines list of texts to be written
+        @type list of str
+        """
+        self.write("".join(lines))

eric ide

mercurial