""" Java java.nio.channels APIs implemented in Python. java.nio.channels package - Channel, Selector, SocketChannel, ServerSocketChannel, FileChannel, etc. Desktop stub wrapping Python sockets. """ from __future__ import annotations from .._core import Stub, AndroidOnlyError import socket as _socket import select as _select import threading as _threading import os as _os from typing import List, Optional, Set __all__ = [ "Channel", "ReadableByteChannel", "WritableByteChannel", "ByteChannel", "ScatteringByteChannel", "GatheringByteChannel", "InterruptibleChannel", "SelectableChannel", "SelectionKey", "Selector", "ServerSocketChannel", "SocketChannel", "DatagramChannel", "Pipe", "FileChannel", "FileChannelMapMode", "AsynchronousChannelGroup", "AsynchronousServerSocketChannel", "AsynchronousSocketChannel", "AsynchronousFileChannel", "CompletionHandler", "AsynchronousChannel", "AsynchronousByteChannel", ] # ============================================================ # Channel # ============================================================ class Channel(Stub): """ java.nio.channels.Channel A nexus for I/O operations. """ def __init__(self): super().__init__() self._open = True def isOpen(self) -> bool: return self._open def close(self): self._open = False def toString(self) -> str: return f"{self.__class__.__name__}@{hex(id(self))}[open={self._open}]" def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(self) & 0x7FFFFFFF class ReadableByteChannel(Channel): """A channel that can read bytes.""" def read(self, dst) -> int: raise NotImplementedError class WritableByteChannel(Channel): """A channel that can write bytes.""" def write(self, src) -> int: raise NotImplementedError class ByteChannel(ReadableByteChannel, WritableByteChannel): """A channel that can both read and write bytes.""" pass class ScatteringByteChannel(ReadableByteChannel): """A channel that can read bytes into a sequence of buffers.""" def read(self, dsts, offset=None, length=None) -> int: total = 0 buffers = dsts[offset:offset + length] if offset is not None else dsts for buf in buffers: total += self.read(buf) return total class GatheringByteChannel(WritableByteChannel): """A channel that can write bytes from a sequence of buffers.""" def write(self, srcs, offset=None, length=None) -> int: total = 0 buffers = srcs[offset:offset + length] if offset is not None else srcs for buf in buffers: total += self.write(buf) return total class InterruptibleChannel(Channel): """A channel that can be asynchronously closed and interrupted.""" pass # ============================================================ # SelectionKey # ============================================================ class SelectionKey(Stub): """ java.nio.channels.SelectionKey A token representing the registration of a SelectableChannel with a Selector. """ OP_ACCEPT: int = 1 << 4 # 16 OP_CONNECT: int = 1 << 3 # 8 OP_READ: int = 1 << 0 # 1 OP_WRITE: int = 1 << 2 # 4 def __init__(self, channel: "SelectableChannel", selector: "Selector", interest_ops: int = 0): super().__init__() self._channel = channel self._selector = selector self._interest_ops = interest_ops self._ready_ops = 0 self._valid = True self._attachment = None def channel(self) -> "SelectableChannel": return self._channel def selector(self) -> "Selector": return self._selector def interestOps(self, ops: int = None): if ops is not None: self._interest_ops = ops return self return self._interest_ops def readyOps(self) -> int: return self._ready_ops def isValid(self) -> bool: return self._valid def isReadable(self) -> bool: return (self._ready_ops & SelectionKey.OP_READ) != 0 def isWritable(self) -> bool: return (self._ready_ops & SelectionKey.OP_WRITE) != 0 def isConnectable(self) -> bool: return (self._ready_ops & SelectionKey.OP_CONNECT) != 0 def isAcceptable(self) -> bool: return (self._ready_ops & SelectionKey.OP_ACCEPT) != 0 def cancel(self): self._valid = False def attach(self, ob: Any) -> Any: old = self._attachment self._attachment = ob return old def attachment(self) -> Any: return self._attachment def toString(self) -> str: return (f"SelectionKey@{hex(id(self))}[interest={self._interest_ops}, " f"ready={self._ready_ops}, valid={self._valid}]") def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(id(self)) & 0x7FFFFFFF # ============================================================ # SelectableChannel # ============================================================ class SelectableChannel(Channel): """ java.nio.channels.SelectableChannel A channel that can be multiplexed via a Selector. """ def __init__(self): super().__init__() self._blocking = True self._lock = _threading.Lock() self._keys = {} def register(self, sel: "Selector", ops: int, att: Any = None) -> SelectionKey: key = SelectionKey(self, sel, ops) if att is not None: key.attach(att) self._keys[sel] = key sel._register_key(key) return key def configureBlocking(self, block: bool) -> "SelectableChannel": self._blocking = block return self def isBlocking(self) -> bool: return self._blocking def blockingLock(self) -> Any: return self._lock def keyFor(self, sel: "Selector") -> Optional[SelectionKey]: return self._keys.get(sel) def provider(self): return None def validOps(self) -> int: return 0 # ============================================================ # Selector # ============================================================ class Selector(Stub): """ java.nio.channels.Selector A selector for selectable channels. """ def __init__(self): super().__init__() self._open = True self._keys = {} self._selected_keys = set() def _register_key(self, key: SelectionKey): self._keys[key.channel()] = key @staticmethod def open() -> "Selector": return Selector() def isOpen(self) -> bool: return self._open def provider(self): return None def keys(self) -> Set[SelectionKey]: return set(self._keys.values()) def selectedKeys(self) -> Set[SelectionKey]: return self._selected_keys def selectNow(self) -> int: """Non-blocking select. Returns number of ready channels.""" return self._do_select(0) def select(self, timeout: int = 0) -> int: """Blocking select with optional timeout (ms).""" return self._do_select(timeout) def _do_select(self, timeout_ms: int) -> int: """Internal select implementation using Python select module.""" read_fds = [] write_fds = [] for ch, key in self._keys.items(): ops = key.interestOps() if hasattr(ch, '_socket') and ch._socket: fd = ch._socket if ops & SelectionKey.OP_READ: read_fds.append(fd) if ops & SelectionKey.OP_WRITE: write_fds.append(fd) timeout_s = timeout_ms / 1000.0 if timeout_ms > 0 else None ready_count = 0 try: r_ready, w_ready, _ = _select.select(read_fds, write_fds, [], timeout_s if timeout_s else 0) except (ValueError, _socket.error): return 0 self._selected_keys.clear() for fd in r_ready: for ch, key in self._keys.items(): if hasattr(ch, '_socket') and ch._socket is fd: key._ready_ops |= SelectionKey.OP_READ self._selected_keys.add(key) ready_count += 1 for fd in w_ready: for ch, key in self._keys.items(): if hasattr(ch, '_socket') and ch._socket is fd: key._ready_ops |= SelectionKey.OP_WRITE self._selected_keys.add(key) ready_count += 1 return ready_count def wakeup(self): """Wakes up a selector that is blocked on a select operation.""" pass def close(self): self._open = False self._keys.clear() self._selected_keys.clear() def toString(self) -> str: return f"Selector@{hex(id(self))}[open={self._open}]" def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(self) & 0x7FFFFFFF # ============================================================ # ServerSocketChannel # ============================================================ class ServerSocketChannel(SelectableChannel): """ java.nio.channels.ServerSocketChannel A selectable channel for stream-oriented listening sockets. """ def __init__(self): super().__init__() self._socket = None self._server_socket = None self._local_address = None @staticmethod def open() -> "ServerSocketChannel": return ServerSocketChannel() def bind(self, local: Any, backlog: int = 50): """Binds the channel's socket to a local address.""" if self._socket is None: self._socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) if hasattr(local, 'getPort') and hasattr(local, 'getHostName'): addr = (local.getHostName(), local.getPort()) elif isinstance(local, tuple): addr = local else: addr = ('0.0.0.0', 0) self._socket.bind(addr) self._socket.listen(backlog) self._local_address = self._socket.getsockname() return self def accept(self) -> Optional["SocketChannel"]: """Accepts a connection.""" if self._socket is None: return None try: conn, addr = self._socket.accept() sc = SocketChannel() sc._socket = conn sc._connected = True sc._remote_address = addr return sc except (BlockingIOError, _socket.error): return None def socket(self): """Returns the underlying ServerSocket.""" return self._server_socket def getLocalAddress(self): """Returns the local address of this channel.""" return self._local_address def setOption(self, name, value): if self._socket: self._socket.setsockopt(_socket.SOL_SOCKET, name, value) return self def getOption(self, name): if self._socket: return self._socket.getsockopt(_socket.SOL_SOCKET, name) return None def supportsOption(self, name) -> bool: return True def validOps(self) -> int: return SelectionKey.OP_ACCEPT def close(self): super().close() if self._socket: self._socket.close() def toString(self) -> str: return f"ServerSocketChannel@{hex(id(self))}[local={self._local_address}]" # ============================================================ # SocketChannel # ============================================================ class SocketChannel(SelectableChannel): """ java.nio.channels.SocketChannel A selectable channel for stream-oriented connecting sockets. """ def __init__(self): super().__init__() self._socket = None self._connected = False self._connection_pending = False self._remote_address = None self._local_address = None self._input_shutdown = False self._output_shutdown = False @staticmethod def open() -> "SocketChannel": return SocketChannel() def connect(self, remote: Any) -> bool: """Connects this channel's socket to the remote address.""" if self._socket is None: self._socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) if hasattr(remote, 'getHostName') and hasattr(remote, 'getPort'): addr = (remote.getHostName(), remote.getPort()) elif isinstance(remote, tuple): addr = remote else: addr = (str(remote), 0) try: self._socket.connect(addr) self._connected = True self._remote_address = addr self._local_address = self._socket.getsockname() return True except _socket.error: self._connection_pending = True return False def finishConnect(self) -> bool: """Finishes the connection process.""" self._connected = True self._connection_pending = False return True def isConnected(self) -> bool: return self._connected def isConnectionPending(self) -> bool: return self._connection_pending def read(self, dst) -> int: """Reads bytes from this channel into the given buffer.""" if not self._connected or self._socket is None: return -1 try: data = self._socket.recv(4096) if not data: return -1 from ..nio import ByteBuffer if isinstance(dst, ByteBuffer): for b in data: dst.put(b) return len(data) except (_socket.error, BlockingIOError): return 0 def write(self, src) -> int: """Writes bytes to this channel from the given buffer.""" if not self._connected or self._socket is None: return 0 from ..nio import ByteBuffer if isinstance(src, ByteBuffer): data = bytearray() for i in range(src.position(), src.limit()): data.append(src.get(i)) try: return self._socket.send(bytes(data)) except _socket.error: return 0 def socket(self): return self._socket def getRemoteAddress(self): return self._remote_address def getLocalAddress(self): return self._local_address def shutdownInput(self): self._input_shutdown = True if self._socket: try: self._socket.shutdown(_socket.SHUT_RD) except _socket.error: pass def shutdownOutput(self): self._output_shutdown = True if self._socket: try: self._socket.shutdown(_socket.SHUT_WR) except _socket.error: pass def setOption(self, name, value): if self._socket: self._socket.setsockopt(_socket.SOL_SOCKET, name, value) return self def getOption(self, name): if self._socket: return self._socket.getsockopt(_socket.SOL_SOCKET, name) return None def supportsOption(self, name) -> bool: return True def validOps(self) -> int: return SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT def close(self): super().close() self._connected = False if self._socket: self._socket.close() def toString(self) -> str: return f"SocketChannel@{hex(id(self))}[connected={self._connected}]" # ============================================================ # DatagramChannel # ============================================================ class DatagramChannel(SelectableChannel): """ java.nio.channels.DatagramChannel A selectable channel for datagram-oriented sockets. """ def __init__(self): super().__init__() self._socket = None self._connected = False self._remote_address = None self._local_address = None @staticmethod def open() -> "DatagramChannel": return DatagramChannel() def bind(self, local: Any): if self._socket is None: self._socket = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM) if hasattr(local, 'getPort'): addr = ('0.0.0.0', local.getPort()) elif isinstance(local, tuple): addr = local else: addr = ('0.0.0.0', 0) self._socket.bind(addr) self._local_address = self._socket.getsockname() return self def connect(self, remote: Any): if self._socket is None: self._socket = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM) if hasattr(remote, 'getHostName') and hasattr(remote, 'getPort'): addr = (remote.getHostName(), remote.getPort()) elif isinstance(remote, tuple): addr = remote else: addr = (str(remote), 0) self._socket.connect(addr) self._connected = True self._remote_address = addr def disconnect(self): self._connected = False self._remote_address = None def isConnected(self) -> bool: return self._connected def receive(self, dst) -> Any: """Receives a datagram.""" if self._socket is None: return None try: data, addr = self._socket.recvfrom(65535) from ..nio import ByteBuffer if isinstance(dst, ByteBuffer): for b in data: dst.put(b) return addr except (_socket.error, BlockingIOError): return None def send(self, src, target) -> int: """Sends a datagram.""" if self._socket is None: return 0 from ..nio import ByteBuffer if isinstance(src, ByteBuffer): data = bytearray() for i in range(src.position(), src.limit()): data.append(src.get(i)) if hasattr(target, 'getHostName') and hasattr(target, 'getPort'): addr = (target.getHostName(), target.getPort()) else: addr = target try: return self._socket.sendto(bytes(data), addr) except _socket.error: return 0 return 0 def read(self, dst) -> int: """Reads a datagram (connected mode).""" return self.receive(dst) def write(self, src) -> int: """Writes a datagram (connected mode).""" return self.send(src, self._remote_address) def socket(self): return self._socket def getRemoteAddress(self): return self._remote_address def getLocalAddress(self): return self._local_address def setOption(self, name, value): if self._socket: self._socket.setsockopt(_socket.SOL_SOCKET, name, value) return self def getOption(self, name): if self._socket: return self._socket.getsockopt(_socket.SOL_SOCKET, name) return None def supportsOption(self, name) -> bool: return True def validOps(self) -> int: return SelectionKey.OP_READ | SelectionKey.OP_WRITE def close(self): super().close() if self._socket: self._socket.close() def toString(self) -> str: return f"DatagramChannel@{hex(id(self))}[connected={self._connected}]" # ============================================================ # Pipe # ============================================================ class Pipe(Stub): """ java.nio.channels.Pipe A pair of channels that implements a unidirectional pipe. """ class SourceChannel(SelectableChannel): """The readable end of the pipe.""" def __init__(self, r_fd): super().__init__() self._r_fd = r_fd def read(self, dst) -> int: data = _os.read(self._r_fd, 4096) from ..nio import ByteBuffer if isinstance(dst, ByteBuffer): for b in data: dst.put(b) return len(data) def validOps(self) -> int: return SelectionKey.OP_READ def close(self): super().close() _os.close(self._r_fd) class SinkChannel(SelectableChannel): """The writable end of the pipe.""" def __init__(self, w_fd): super().__init__() self._w_fd = w_fd def write(self, src) -> int: from ..nio import ByteBuffer if isinstance(src, ByteBuffer): data = bytearray() for i in range(src.position(), src.limit()): data.append(src.get(i)) return _os.write(self._w_fd, bytes(data)) return 0 def validOps(self) -> int: return SelectionKey.OP_WRITE def close(self): super().close() _os.close(self._w_fd) @staticmethod def open() -> "Pipe": pipe = Pipe() r, w = _os.pipe() pipe._source = Pipe.SourceChannel(r) pipe._sink = Pipe.SinkChannel(w) return pipe def source(self) -> SourceChannel: return self._source def sink(self) -> SinkChannel: return self._sink def toString(self) -> str: return f"Pipe@{hex(id(self))}" def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(self) & 0x7FFFFFFF # ============================================================ # FileChannel MapMode # ============================================================ class FileChannelMapMode(Stub): """ java.nio.channels.FileChannel.MapMode A file mapping mode. """ READ_ONLY: "FileChannelMapMode" = None READ_WRITE: "FileChannelMapMode" = None PRIVATE: "FileChannelMapMode" = None def __init__(self, name: str): super().__init__() self._name = name def toString(self) -> str: return self._name def equals(self, other) -> bool: return isinstance(other, FileChannelMapMode) and self._name == other._name def hashCode(self) -> int: return hash(self._name) & 0x7FFFFFFF FileChannelMapMode.READ_ONLY = FileChannelMapMode("READ_ONLY") FileChannelMapMode.READ_WRITE = FileChannelMapMode("READ_WRITE") FileChannelMapMode.PRIVATE = FileChannelMapMode("PRIVATE") # ============================================================ # FileChannel # ============================================================ class FileChannel(Channel): """ java.nio.channels.FileChannel A channel for reading, writing, mapping, and manipulating a file. """ IMPL_OPEN_LOCKING: bool = True def __init__(self, file_path: str = None, mode: str = 'r'): super().__init__() self._file_path = file_path self._mode = mode self._fd = None self._position = 0 if file_path: self._fd = _os.open(file_path, self._get_os_flags(mode)) @staticmethod def open(path: Any, options=None, attrs=None) -> "FileChannel": """Opens a file channel.""" return FileChannel(str(path)) def _get_os_flags(self, mode: str) -> int: flags = 0 if 'r' in mode and 'w' not in mode and '+' not in mode: flags = _os.O_RDONLY elif 'w' in mode or '+' in mode: flags = _os.O_RDWR | _os.O_CREAT return flags def read(self, dst) -> int: """Reads bytes from this channel into the given buffer.""" if self._fd is None: return -1 try: data = _os.read(self._fd, 4096) if not data: return -1 from ..nio import ByteBuffer if isinstance(dst, ByteBuffer): for b in data: dst.put(b) self._position += len(data) return len(data) except OSError: return -1 def read(self, dsts, offset: int = 0, length: int = 0) -> int: total = 0 bufs = dsts[offset:offset + length] if length else dsts for buf in bufs: n = self.read(buf) if n < 0: break total += n return total def write(self, src) -> int: """Writes bytes to this channel from the given buffer.""" if self._fd is None: return 0 from ..nio import ByteBuffer if isinstance(src, ByteBuffer): data = bytearray() for i in range(src.position(), src.limit()): data.append(src.get(i)) try: written = _os.write(self._fd, bytes(data)) self._position += written return written except OSError: return 0 return 0 def write(self, srcs, offset: int = 0, length: int = 0) -> int: total = 0 bufs = srcs[offset:offset + length] if length else srcs for buf in bufs: total += self.write(buf) return total def position(self) -> int: """Returns this channel's file position.""" if self._fd is not None: self._position = _os.lseek(self._fd, 0, _os.SEEK_CUR) return self._position def position(self, new_position: int) -> "FileChannel": """Sets this channel's file position.""" if self._fd is not None: _os.lseek(self._fd, new_position, _os.SEEK_SET) self._position = new_position return self def size(self) -> int: """Returns the current size of this channel's file.""" if self._fd is not None: return _os.lseek(self._fd, 0, _os.SEEK_END) return 0 def truncate(self, size: int) -> "FileChannel": """Truncates the file to the given size.""" if self._fd is not None: _os.ftruncate(self._fd, size) return self def force(self, meta_data: bool): """Forces any updates to this channel's file to be written to storage.""" if self._fd is not None: _os.fsync(self._fd) def transferTo(self, position: int, count: int, target: WritableByteChannel) -> int: """Transfers bytes from this channel to the given writable channel.""" old_pos = self.position() self.position(position) from ..nio import ByteBuffer buf = ByteBuffer.allocate(count) n = self.read(buf) buf.flip() target.write(buf) self.position(old_pos) return n def transferFrom(self, src: ReadableByteChannel, position: int, count: int) -> int: """Transfers bytes from the given readable channel to this channel.""" old_pos = self.position() self.position(position) from ..nio import ByteBuffer buf = ByteBuffer.allocate(count) n = src.read(buf) buf.flip() self.write(buf) self.position(old_pos) return n def map(self, mode: FileChannelMapMode, position: int, size: int): """Maps a region of this channel's file into memory (stub).""" return None def lock(self, position: int = 0, size: int = 0, shared: bool = False): """Locks a region of this channel's file (stub).""" return None def tryLock(self, position: int = 0, size: int = 0, shared: bool = False): """Attempts to lock a region of this channel's file (stub).""" return None def close(self): super().close() if self._fd is not None: _os.close(self._fd) self._fd = None def toString(self) -> str: return f"FileChannel@{hex(id(self))}[path={self._file_path}]" def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(self) & 0x7FFFFFFF # ============================================================ # AsynchronousChannel stubs # ============================================================ class CompletionHandler(Stub): """ java.nio.channels.CompletionHandler A handler for consuming the result of an asynchronous I/O operation. """ def completed(self, result, attachment): raise NotImplementedError def failed(self, exc: Exception, attachment): raise NotImplementedError class AsynchronousChannel(Channel): """An asynchronous channel for I/O operations.""" pass class AsynchronousByteChannel(AsynchronousChannel): """An asynchronous channel that can read and write bytes.""" pass class AsynchronousChannelGroup(Stub): """ java.nio.channels.AsynchronousChannelGroup A grouping of asynchronous channels. """ def __init__(self): super().__init__() self._channels = [] def shutdown(self): pass def shutdownNow(self): pass def awaitTermination(self, timeout: int, unit) -> bool: return True def isShutdown(self) -> bool: return False def isTerminated(self) -> bool: return False def toString(self) -> str: return f"AsynchronousChannelGroup@{hex(id(self))}" def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(self) & 0x7FFFFFFF class AsynchronousServerSocketChannel(AsynchronousChannel): """An asynchronous channel for stream-oriented listening sockets.""" @staticmethod def open(group: AsynchronousChannelGroup = None) -> "AsynchronousServerSocketChannel": return AsynchronousServerSocketChannel() def bind(self, local: Any) -> "AsynchronousServerSocketChannel": return self def accept(self, attachment=None, handler: CompletionHandler = None): pass def getLocalAddress(self): return None def setOption(self, name, value): return self def close(self): pass def toString(self) -> str: return f"AsynchronousServerSocketChannel@{hex(id(self))}" class AsynchronousSocketChannel(AsynchronousByteChannel): """An asynchronous channel for stream-oriented connecting sockets.""" @staticmethod def open(group: AsynchronousChannelGroup = None) -> "AsynchronousSocketChannel": return AsynchronousSocketChannel() def connect(self, remote: Any, attachment=None, handler: CompletionHandler = None): pass def read(self, dst, attachment=None, handler: CompletionHandler = None): pass def write(self, src, attachment=None, handler: CompletionHandler = None): pass def getRemoteAddress(self): return None def getLocalAddress(self): return None def shutdownInput(self): pass def shutdownOutput(self): pass def close(self): pass def toString(self) -> str: return f"AsynchronousSocketChannel@{hex(id(self))}" class AsynchronousFileChannel(AsynchronousChannel): """An asynchronous channel for reading, writing, and manipulating a file.""" @staticmethod def open(path: Any, options=None, executor=None, attrs=None) -> "AsynchronousFileChannel": return AsynchronousFileChannel() def read(self, dst, position: int, attachment=None, handler: CompletionHandler = None): pass def write(self, src, position: int, attachment=None, handler: CompletionHandler = None): pass def lock(self, position: int = 0, size: int = 0, shared: bool = False, attachment=None, handler: CompletionHandler = None): pass def tryLock(self, position: int = 0, size: int = 0, shared: bool = False): return None def size(self) -> int: return 0 def truncate(self, size: int) -> "AsynchronousFileChannel": return self def force(self, meta_data: bool): pass def close(self): pass def isOpen(self) -> bool: return True def toString(self) -> str: return f"AsynchronousFileChannel@{hex(id(self))}" def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(self) & 0x7FFFFFFF