RAHHH
This commit is contained in:
@@ -0,0 +1,662 @@
|
||||
# -*- test-case-name: twisted.test.test_internet,twisted.internet.test.test_posixbase -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Posix reactor base class
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
import sys
|
||||
from typing import Sequence
|
||||
|
||||
from zope.interface import classImplements, implementer
|
||||
|
||||
from twisted.internet import error, tcp, udp
|
||||
from twisted.internet.base import ReactorBase
|
||||
from twisted.internet.interfaces import (
|
||||
IConnector,
|
||||
IHalfCloseableDescriptor,
|
||||
IReactorFDSet,
|
||||
IReactorMulticast,
|
||||
IReactorProcess,
|
||||
IReactorSocket,
|
||||
IReactorSSL,
|
||||
IReactorTCP,
|
||||
IReactorUDP,
|
||||
IReactorUNIX,
|
||||
IReactorUNIXDatagram,
|
||||
)
|
||||
from twisted.internet.main import CONNECTION_DONE, CONNECTION_LOST
|
||||
from twisted.internet.protocol import ClientFactory
|
||||
from twisted.python import failure, log
|
||||
from twisted.python.runtime import platform, platformType
|
||||
from ._signals import (
|
||||
SignalHandling,
|
||||
_ChildSignalHandling,
|
||||
_IWaker,
|
||||
_MultiSignalHandling,
|
||||
_Waker,
|
||||
)
|
||||
|
||||
# Exceptions that doSelect might return frequently
|
||||
_NO_FILEDESC = error.ConnectionFdescWentAway("File descriptor lost")
|
||||
|
||||
|
||||
try:
|
||||
from twisted.protocols import tls as _tls
|
||||
except ImportError:
|
||||
tls = None
|
||||
else:
|
||||
tls = _tls
|
||||
|
||||
try:
|
||||
from twisted.internet import ssl as _ssl
|
||||
except ImportError:
|
||||
ssl = None
|
||||
else:
|
||||
ssl = _ssl
|
||||
|
||||
unixEnabled = platformType == "posix"
|
||||
|
||||
processEnabled = False
|
||||
if unixEnabled:
|
||||
from twisted.internet import process, unix
|
||||
|
||||
processEnabled = True
|
||||
|
||||
|
||||
if platform.isWindows():
|
||||
try:
|
||||
import win32process
|
||||
|
||||
processEnabled = True
|
||||
except ImportError:
|
||||
win32process = None
|
||||
|
||||
|
||||
class _DisconnectSelectableMixin:
|
||||
"""
|
||||
Mixin providing the C{_disconnectSelectable} method.
|
||||
"""
|
||||
|
||||
def _disconnectSelectable(
|
||||
self,
|
||||
selectable,
|
||||
why,
|
||||
isRead,
|
||||
faildict={
|
||||
error.ConnectionDone: failure.Failure(error.ConnectionDone()),
|
||||
error.ConnectionLost: failure.Failure(error.ConnectionLost()),
|
||||
},
|
||||
):
|
||||
"""
|
||||
Utility function for disconnecting a selectable.
|
||||
|
||||
Supports half-close notification, isRead should be boolean indicating
|
||||
whether error resulted from doRead().
|
||||
"""
|
||||
self.removeReader(selectable)
|
||||
f = faildict.get(why.__class__)
|
||||
if f:
|
||||
if (
|
||||
isRead
|
||||
and why.__class__ == error.ConnectionDone
|
||||
and IHalfCloseableDescriptor.providedBy(selectable)
|
||||
):
|
||||
selectable.readConnectionLost(f)
|
||||
else:
|
||||
self.removeWriter(selectable)
|
||||
selectable.connectionLost(f)
|
||||
else:
|
||||
self.removeWriter(selectable)
|
||||
selectable.connectionLost(failure.Failure(why))
|
||||
|
||||
|
||||
@implementer(IReactorTCP, IReactorUDP, IReactorMulticast)
|
||||
class PosixReactorBase(_DisconnectSelectableMixin, ReactorBase):
|
||||
"""
|
||||
A basis for reactors that use file descriptors.
|
||||
|
||||
@ivar _childWaker: L{None} or a reference to the L{_SIGCHLDWaker}
|
||||
which is used to properly notice child process termination.
|
||||
"""
|
||||
|
||||
_childWaker = None
|
||||
|
||||
# Callable that creates a waker, overrideable so that subclasses can
|
||||
# substitute their own implementation:
|
||||
def _wakerFactory(self) -> _IWaker:
|
||||
return _Waker()
|
||||
|
||||
def installWaker(self):
|
||||
"""
|
||||
Install a `waker' to allow threads and signals to wake up the IO thread.
|
||||
|
||||
We use the self-pipe trick (http://cr.yp.to/docs/selfpipe.html) to wake
|
||||
the reactor. On Windows we use a pair of sockets.
|
||||
"""
|
||||
if not self.waker:
|
||||
self.waker = self._wakerFactory()
|
||||
self._internalReaders.add(self.waker)
|
||||
self.addReader(self.waker)
|
||||
|
||||
def _signalsFactory(self) -> SignalHandling:
|
||||
"""
|
||||
Customize reactor signal handling to support child processes on POSIX
|
||||
platforms.
|
||||
"""
|
||||
baseHandling = super()._signalsFactory()
|
||||
# If we're on a platform that uses signals for process event signaling
|
||||
if platformType == "posix":
|
||||
# Compose ...
|
||||
return _MultiSignalHandling(
|
||||
(
|
||||
# the base signal handling behavior ...
|
||||
baseHandling,
|
||||
# with our extra SIGCHLD handling behavior.
|
||||
_ChildSignalHandling(
|
||||
self._addInternalReader,
|
||||
self._removeInternalReader,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Otherwise just use the base behavior
|
||||
return baseHandling
|
||||
|
||||
# IReactorProcess
|
||||
|
||||
def spawnProcess(
|
||||
self,
|
||||
processProtocol,
|
||||
executable,
|
||||
args=(),
|
||||
env={},
|
||||
path=None,
|
||||
uid=None,
|
||||
gid=None,
|
||||
usePTY=0,
|
||||
childFDs=None,
|
||||
):
|
||||
if platformType == "posix":
|
||||
if usePTY:
|
||||
if childFDs is not None:
|
||||
raise ValueError(
|
||||
"Using childFDs is not supported with usePTY=True."
|
||||
)
|
||||
return process.PTYProcess(
|
||||
self, executable, args, env, path, processProtocol, uid, gid, usePTY
|
||||
)
|
||||
else:
|
||||
return process.Process(
|
||||
self,
|
||||
executable,
|
||||
args,
|
||||
env,
|
||||
path,
|
||||
processProtocol,
|
||||
uid,
|
||||
gid,
|
||||
childFDs,
|
||||
)
|
||||
elif platformType == "win32":
|
||||
if uid is not None:
|
||||
raise ValueError("Setting UID is unsupported on this platform.")
|
||||
if gid is not None:
|
||||
raise ValueError("Setting GID is unsupported on this platform.")
|
||||
if usePTY:
|
||||
raise ValueError("The usePTY parameter is not supported on Windows.")
|
||||
if childFDs:
|
||||
raise ValueError("Customizing childFDs is not supported on Windows.")
|
||||
|
||||
if win32process:
|
||||
from twisted.internet._dumbwin32proc import Process
|
||||
|
||||
return Process(self, processProtocol, executable, args, env, path)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"spawnProcess not available since pywin32 is not installed."
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"spawnProcess only available on Windows or POSIX."
|
||||
)
|
||||
|
||||
# IReactorUDP
|
||||
|
||||
def listenUDP(self, port, protocol, interface="", maxPacketSize=8192):
|
||||
"""Connects a given L{DatagramProtocol} to the given numeric UDP port.
|
||||
|
||||
@returns: object conforming to L{IListeningPort}.
|
||||
"""
|
||||
p = udp.Port(port, protocol, interface, maxPacketSize, self)
|
||||
p.startListening()
|
||||
return p
|
||||
|
||||
# IReactorMulticast
|
||||
|
||||
def listenMulticast(
|
||||
self, port, protocol, interface="", maxPacketSize=8192, listenMultiple=False
|
||||
):
|
||||
"""Connects a given DatagramProtocol to the given numeric UDP port.
|
||||
|
||||
EXPERIMENTAL.
|
||||
|
||||
@returns: object conforming to IListeningPort.
|
||||
"""
|
||||
p = udp.MulticastPort(
|
||||
port, protocol, interface, maxPacketSize, self, listenMultiple
|
||||
)
|
||||
p.startListening()
|
||||
return p
|
||||
|
||||
# IReactorUNIX
|
||||
|
||||
def connectUNIX(self, address, factory, timeout=30, checkPID=0):
|
||||
assert unixEnabled, "UNIX support is not present"
|
||||
c = unix.Connector(address, factory, timeout, self, checkPID)
|
||||
c.connect()
|
||||
return c
|
||||
|
||||
def listenUNIX(self, address, factory, backlog=50, mode=0o666, wantPID=0):
|
||||
assert unixEnabled, "UNIX support is not present"
|
||||
p = unix.Port(address, factory, backlog, mode, self, wantPID)
|
||||
p.startListening()
|
||||
return p
|
||||
|
||||
# IReactorUNIXDatagram
|
||||
|
||||
def listenUNIXDatagram(self, address, protocol, maxPacketSize=8192, mode=0o666):
|
||||
"""
|
||||
Connects a given L{DatagramProtocol} to the given path.
|
||||
|
||||
EXPERIMENTAL.
|
||||
|
||||
@returns: object conforming to L{IListeningPort}.
|
||||
"""
|
||||
assert unixEnabled, "UNIX support is not present"
|
||||
p = unix.DatagramPort(address, protocol, maxPacketSize, mode, self)
|
||||
p.startListening()
|
||||
return p
|
||||
|
||||
def connectUNIXDatagram(
|
||||
self, address, protocol, maxPacketSize=8192, mode=0o666, bindAddress=None
|
||||
):
|
||||
"""
|
||||
Connects a L{ConnectedDatagramProtocol} instance to a path.
|
||||
|
||||
EXPERIMENTAL.
|
||||
"""
|
||||
assert unixEnabled, "UNIX support is not present"
|
||||
p = unix.ConnectedDatagramPort(
|
||||
address, protocol, maxPacketSize, mode, bindAddress, self
|
||||
)
|
||||
p.startListening()
|
||||
return p
|
||||
|
||||
# IReactorSocket (no AF_UNIX on Windows)
|
||||
|
||||
if unixEnabled:
|
||||
_supportedAddressFamilies: Sequence[socket.AddressFamily] = (
|
||||
socket.AF_INET,
|
||||
socket.AF_INET6,
|
||||
socket.AF_UNIX,
|
||||
)
|
||||
else:
|
||||
_supportedAddressFamilies = (
|
||||
socket.AF_INET,
|
||||
socket.AF_INET6,
|
||||
)
|
||||
|
||||
def adoptStreamPort(self, fileDescriptor, addressFamily, factory):
|
||||
"""
|
||||
Create a new L{IListeningPort} from an already-initialized socket.
|
||||
|
||||
This just dispatches to a suitable port implementation (eg from
|
||||
L{IReactorTCP}, etc) based on the specified C{addressFamily}.
|
||||
|
||||
@see: L{twisted.internet.interfaces.IReactorSocket.adoptStreamPort}
|
||||
"""
|
||||
if addressFamily not in self._supportedAddressFamilies:
|
||||
raise error.UnsupportedAddressFamily(addressFamily)
|
||||
|
||||
if unixEnabled and addressFamily == socket.AF_UNIX:
|
||||
p = unix.Port._fromListeningDescriptor(self, fileDescriptor, factory)
|
||||
else:
|
||||
p = tcp.Port._fromListeningDescriptor(
|
||||
self, fileDescriptor, addressFamily, factory
|
||||
)
|
||||
p.startListening()
|
||||
return p
|
||||
|
||||
def adoptStreamConnection(self, fileDescriptor, addressFamily, factory):
|
||||
"""
|
||||
@see:
|
||||
L{twisted.internet.interfaces.IReactorSocket.adoptStreamConnection}
|
||||
"""
|
||||
if addressFamily not in self._supportedAddressFamilies:
|
||||
raise error.UnsupportedAddressFamily(addressFamily)
|
||||
|
||||
if unixEnabled and addressFamily == socket.AF_UNIX:
|
||||
return unix.Server._fromConnectedSocket(fileDescriptor, factory, self)
|
||||
else:
|
||||
return tcp.Server._fromConnectedSocket(
|
||||
fileDescriptor, addressFamily, factory, self
|
||||
)
|
||||
|
||||
def adoptDatagramPort(
|
||||
self, fileDescriptor, addressFamily, protocol, maxPacketSize=8192
|
||||
):
|
||||
if addressFamily not in (socket.AF_INET, socket.AF_INET6):
|
||||
raise error.UnsupportedAddressFamily(addressFamily)
|
||||
|
||||
p = udp.Port._fromListeningDescriptor(
|
||||
self, fileDescriptor, addressFamily, protocol, maxPacketSize=maxPacketSize
|
||||
)
|
||||
p.startListening()
|
||||
return p
|
||||
|
||||
# IReactorTCP
|
||||
|
||||
def listenTCP(self, port, factory, backlog=50, interface=""):
|
||||
p = tcp.Port(port, factory, backlog, interface, self)
|
||||
p.startListening()
|
||||
return p
|
||||
|
||||
def connectTCP(
|
||||
self,
|
||||
host: str,
|
||||
port: int,
|
||||
factory: "ClientFactory",
|
||||
timeout: float = 30.0,
|
||||
bindAddress: tuple[str, int] | None = None,
|
||||
) -> IConnector:
|
||||
c = tcp.Connector(host, port, factory, timeout, bindAddress, self)
|
||||
c.connect()
|
||||
return c
|
||||
|
||||
# IReactorSSL (sometimes, not implemented)
|
||||
|
||||
def connectSSL(
|
||||
self, host, port, factory, contextFactory, timeout=30, bindAddress=None
|
||||
):
|
||||
if tls is not None:
|
||||
tlsFactory = tls.TLSMemoryBIOFactory(contextFactory, True, factory)
|
||||
return self.connectTCP(host, port, tlsFactory, timeout, bindAddress)
|
||||
elif ssl is not None:
|
||||
c = ssl.Connector(
|
||||
host, port, factory, contextFactory, timeout, bindAddress, self
|
||||
)
|
||||
c.connect()
|
||||
return c
|
||||
else:
|
||||
assert False, "SSL support is not present"
|
||||
|
||||
def listenSSL(self, port, factory, contextFactory, backlog=50, interface=""):
|
||||
if tls is not None:
|
||||
tlsFactory = tls.TLSMemoryBIOFactory(contextFactory, False, factory)
|
||||
port = self.listenTCP(port, tlsFactory, backlog, interface)
|
||||
port._type = "TLS"
|
||||
return port
|
||||
elif ssl is not None:
|
||||
p = ssl.Port(port, factory, contextFactory, backlog, interface, self)
|
||||
p.startListening()
|
||||
return p
|
||||
else:
|
||||
assert False, "SSL support is not present"
|
||||
|
||||
def _removeAll(self, readers, writers):
|
||||
"""
|
||||
Remove all readers and writers, and list of removed L{IReadDescriptor}s
|
||||
and L{IWriteDescriptor}s.
|
||||
|
||||
Meant for calling from subclasses, to implement removeAll, like::
|
||||
|
||||
def removeAll(self):
|
||||
return self._removeAll(self._reads, self._writes)
|
||||
|
||||
where C{self._reads} and C{self._writes} are iterables.
|
||||
"""
|
||||
removedReaders = set(readers) - self._internalReaders
|
||||
for reader in removedReaders:
|
||||
self.removeReader(reader)
|
||||
|
||||
removedWriters = set(writers)
|
||||
for writer in removedWriters:
|
||||
self.removeWriter(writer)
|
||||
|
||||
return list(removedReaders | removedWriters)
|
||||
|
||||
|
||||
class _PollLikeMixin:
|
||||
"""
|
||||
Mixin for poll-like reactors.
|
||||
|
||||
Subclasses must define the following attributes::
|
||||
|
||||
- _POLL_DISCONNECTED - Bitmask for events indicating a connection was
|
||||
lost.
|
||||
- _POLL_IN - Bitmask for events indicating there is input to read.
|
||||
- _POLL_OUT - Bitmask for events indicating output can be written.
|
||||
|
||||
Must be mixed in to a subclass of PosixReactorBase (for
|
||||
_disconnectSelectable).
|
||||
"""
|
||||
|
||||
def _doReadOrWrite(self, selectable, fd, event):
|
||||
"""
|
||||
fd is available for read or write, do the work and raise errors if
|
||||
necessary.
|
||||
"""
|
||||
why = None
|
||||
inRead = False
|
||||
if event & self._POLL_DISCONNECTED and not (event & self._POLL_IN):
|
||||
# Handle disconnection. But only if we finished processing all
|
||||
# the pending input.
|
||||
if fd in self._reads:
|
||||
# If we were reading from the descriptor then this is a
|
||||
# clean shutdown. We know there are no read events pending
|
||||
# because we just checked above. It also might be a
|
||||
# half-close (which is why we have to keep track of inRead).
|
||||
inRead = True
|
||||
why = CONNECTION_DONE
|
||||
else:
|
||||
# If we weren't reading, this is an error shutdown of some
|
||||
# sort.
|
||||
why = CONNECTION_LOST
|
||||
else:
|
||||
# Any non-disconnect event turns into a doRead or a doWrite.
|
||||
try:
|
||||
# First check to see if the descriptor is still valid. This
|
||||
# gives fileno() a chance to raise an exception, too.
|
||||
# Ideally, disconnection would always be indicated by the
|
||||
# return value of doRead or doWrite (or an exception from
|
||||
# one of those methods), but calling fileno here helps make
|
||||
# buggy applications more transparent.
|
||||
if selectable.fileno() == -1:
|
||||
# -1 is sort of a historical Python artifact. Python
|
||||
# files and sockets used to change their file descriptor
|
||||
# to -1 when they closed. For the time being, we'll
|
||||
# continue to support this anyway in case applications
|
||||
# replicated it, plus abstract.FileDescriptor.fileno
|
||||
# returns -1. Eventually it'd be good to deprecate this
|
||||
# case.
|
||||
why = _NO_FILEDESC
|
||||
else:
|
||||
if event & self._POLL_IN:
|
||||
# Handle a read event.
|
||||
why = selectable.doRead()
|
||||
inRead = True
|
||||
if not why and event & self._POLL_OUT:
|
||||
# Handle a write event, as long as doRead didn't
|
||||
# disconnect us.
|
||||
why = selectable.doWrite()
|
||||
inRead = False
|
||||
except BaseException:
|
||||
# Any exception from application code gets logged and will
|
||||
# cause us to disconnect the selectable.
|
||||
why = sys.exc_info()[1]
|
||||
log.err()
|
||||
if why:
|
||||
self._disconnectSelectable(selectable, why, inRead)
|
||||
|
||||
|
||||
@implementer(IReactorFDSet)
|
||||
class _ContinuousPolling(_PollLikeMixin, _DisconnectSelectableMixin):
|
||||
"""
|
||||
Schedule reads and writes based on the passage of time, rather than
|
||||
notification.
|
||||
|
||||
This is useful for supporting polling filesystem files, which C{epoll(7)}
|
||||
does not support.
|
||||
|
||||
The implementation uses L{_PollLikeMixin}, which is a bit hacky, but
|
||||
re-implementing and testing the relevant code yet again is unappealing.
|
||||
|
||||
@ivar _reactor: The L{EPollReactor} that is using this instance.
|
||||
|
||||
@ivar _loop: A C{LoopingCall} that drives the polling, or L{None}.
|
||||
|
||||
@ivar _readers: A C{set} of C{FileDescriptor} objects that should be read
|
||||
from.
|
||||
|
||||
@ivar _writers: A C{set} of C{FileDescriptor} objects that should be
|
||||
written to.
|
||||
"""
|
||||
|
||||
# Attributes for _PollLikeMixin
|
||||
_POLL_DISCONNECTED = 1
|
||||
_POLL_IN = 2
|
||||
_POLL_OUT = 4
|
||||
|
||||
def __init__(self, reactor):
|
||||
self._reactor = reactor
|
||||
self._loop = None
|
||||
self._readers = set()
|
||||
self._writers = set()
|
||||
|
||||
def _checkLoop(self):
|
||||
"""
|
||||
Start or stop a C{LoopingCall} based on whether there are readers and
|
||||
writers.
|
||||
"""
|
||||
if self._readers or self._writers:
|
||||
if self._loop is None:
|
||||
from twisted.internet.task import _EPSILON, LoopingCall
|
||||
|
||||
self._loop = LoopingCall(self.iterate)
|
||||
self._loop.clock = self._reactor
|
||||
# LoopingCall seems unhappy with timeout of 0, so use very
|
||||
# small number:
|
||||
self._loop.start(_EPSILON, now=False)
|
||||
elif self._loop:
|
||||
self._loop.stop()
|
||||
self._loop = None
|
||||
|
||||
def iterate(self):
|
||||
"""
|
||||
Call C{doRead} and C{doWrite} on all readers and writers respectively.
|
||||
"""
|
||||
for reader in list(self._readers):
|
||||
self._doReadOrWrite(reader, reader, self._POLL_IN)
|
||||
for writer in list(self._writers):
|
||||
self._doReadOrWrite(writer, writer, self._POLL_OUT)
|
||||
|
||||
def addReader(self, reader):
|
||||
"""
|
||||
Add a C{FileDescriptor} for notification of data available to read.
|
||||
"""
|
||||
self._readers.add(reader)
|
||||
self._checkLoop()
|
||||
|
||||
def addWriter(self, writer):
|
||||
"""
|
||||
Add a C{FileDescriptor} for notification of data available to write.
|
||||
"""
|
||||
self._writers.add(writer)
|
||||
self._checkLoop()
|
||||
|
||||
def removeReader(self, reader):
|
||||
"""
|
||||
Remove a C{FileDescriptor} from notification of data available to read.
|
||||
"""
|
||||
try:
|
||||
self._readers.remove(reader)
|
||||
except KeyError:
|
||||
return
|
||||
self._checkLoop()
|
||||
|
||||
def removeWriter(self, writer):
|
||||
"""
|
||||
Remove a C{FileDescriptor} from notification of data available to
|
||||
write.
|
||||
"""
|
||||
try:
|
||||
self._writers.remove(writer)
|
||||
except KeyError:
|
||||
return
|
||||
self._checkLoop()
|
||||
|
||||
def removeAll(self):
|
||||
"""
|
||||
Remove all readers and writers.
|
||||
"""
|
||||
result = list(self._readers | self._writers)
|
||||
# Don't reset to new value, since self.isWriting and .isReading refer
|
||||
# to the existing instance:
|
||||
self._readers.clear()
|
||||
self._writers.clear()
|
||||
return result
|
||||
|
||||
def getReaders(self):
|
||||
"""
|
||||
Return a list of the readers.
|
||||
"""
|
||||
return list(self._readers)
|
||||
|
||||
def getWriters(self):
|
||||
"""
|
||||
Return a list of the writers.
|
||||
"""
|
||||
return list(self._writers)
|
||||
|
||||
def isReading(self, fd):
|
||||
"""
|
||||
Checks if the file descriptor is currently being observed for read
|
||||
readiness.
|
||||
|
||||
@param fd: The file descriptor being checked.
|
||||
@type fd: L{twisted.internet.abstract.FileDescriptor}
|
||||
@return: C{True} if the file descriptor is being observed for read
|
||||
readiness, C{False} otherwise.
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
return fd in self._readers
|
||||
|
||||
def isWriting(self, fd):
|
||||
"""
|
||||
Checks if the file descriptor is currently being observed for write
|
||||
readiness.
|
||||
|
||||
@param fd: The file descriptor being checked.
|
||||
@type fd: L{twisted.internet.abstract.FileDescriptor}
|
||||
@return: C{True} if the file descriptor is being observed for write
|
||||
readiness, C{False} otherwise.
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
return fd in self._writers
|
||||
|
||||
|
||||
if tls is not None or ssl is not None:
|
||||
classImplements(PosixReactorBase, IReactorSSL)
|
||||
if unixEnabled:
|
||||
classImplements(PosixReactorBase, IReactorUNIX, IReactorUNIXDatagram)
|
||||
if processEnabled:
|
||||
classImplements(PosixReactorBase, IReactorProcess)
|
||||
if getattr(socket, "fromfd", None) is not None:
|
||||
classImplements(PosixReactorBase, IReactorSocket)
|
||||
|
||||
__all__ = ["PosixReactorBase"]
|
||||
Reference in New Issue
Block a user