Issue #14310: inter-process socket duplication for windows
This commit is contained in:
parent
0f9eec19ee
commit
10f383a937
|
@ -680,6 +680,16 @@ The module :mod:`socket` exports the following constants and functions:
|
|||
.. versionadded:: 3.3
|
||||
|
||||
|
||||
.. function:: fromshare(data)
|
||||
|
||||
Instantiate a socket from data obtained from :meth:`~socket.share`.
|
||||
The socket is assumed to be in blocking mode.
|
||||
|
||||
Availability: Windows.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
|
||||
.. data:: SocketType
|
||||
|
||||
This is a Python type object that represents the socket object type. It is the
|
||||
|
@ -1082,6 +1092,21 @@ correspond to Unix system calls applicable to sockets.
|
|||
are disallowed. If *how* is :const:`SHUT_RDWR`, further sends and receives are
|
||||
disallowed.
|
||||
|
||||
|
||||
.. method:: socket.share(process_id)
|
||||
|
||||
:platform: Windows
|
||||
|
||||
Duplacet a socket and prepare it for sharing with a target process. The
|
||||
target process must be provided with *process_id*. The resulting bytes object
|
||||
can then be passed to the target process using some form of interprocess
|
||||
communication and the socket can be recreated there using :func:`fromshare`.
|
||||
Once this method has been called, it is safe to close the socket since
|
||||
the operating system has already duplicated it for the target process.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
|
||||
Note that there are no methods :meth:`read` or :meth:`write`; use
|
||||
:meth:`~socket.recv` and :meth:`~socket.send` without *flags* argument instead.
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ Functions:
|
|||
socket() -- create a new socket object
|
||||
socketpair() -- create a pair of new socket objects [*]
|
||||
fromfd() -- create a socket object from an open file descriptor [*]
|
||||
fromshare() -- create a socket object from data received from socket.share() [*]
|
||||
gethostname() -- return the current hostname
|
||||
gethostbyname() -- map a hostname to its IP number
|
||||
gethostbyaddr() -- map an IP number or hostname to DNS info
|
||||
|
@ -209,7 +210,6 @@ class socket(_socket.socket):
|
|||
self._closed = True
|
||||
return super().detach()
|
||||
|
||||
|
||||
def fromfd(fd, family, type, proto=0):
|
||||
""" fromfd(fd, family, type[, proto]) -> socket object
|
||||
|
||||
|
@ -219,6 +219,14 @@ def fromfd(fd, family, type, proto=0):
|
|||
nfd = dup(fd)
|
||||
return socket(family, type, proto, nfd)
|
||||
|
||||
if hasattr(_socket.socket, "share"):
|
||||
def fromshare(info):
|
||||
""" fromshare(info) -> socket object
|
||||
|
||||
Create a socket object from a the bytes object returned by
|
||||
socket.share(pid).
|
||||
"""
|
||||
return socket(0, 0, 0, info)
|
||||
|
||||
if hasattr(_socket, "socketpair"):
|
||||
|
||||
|
|
|
@ -26,6 +26,10 @@ try:
|
|||
import fcntl
|
||||
except ImportError:
|
||||
fcntl = False
|
||||
try:
|
||||
import multiprocessing
|
||||
except ImportError:
|
||||
multiprocessing = False
|
||||
|
||||
HOST = support.HOST
|
||||
MSG = 'Michael Gilfix was here\u1234\r\n'.encode('utf-8') ## test unicode string and carriage return
|
||||
|
@ -4643,6 +4647,106 @@ class NonblockConstantTest(unittest.TestCase):
|
|||
socket.setdefaulttimeout(t)
|
||||
|
||||
|
||||
@unittest.skipUnless(os.name == "nt", "Windows specific")
|
||||
@unittest.skipUnless(multiprocessing, "need multiprocessing")
|
||||
class TestSocketSharing(SocketTCPTest):
|
||||
# This must be classmethod and not staticmethod or multiprocessing
|
||||
# won't be able to bootstrap it.
|
||||
@classmethod
|
||||
def remoteProcessServer(cls, q):
|
||||
# Recreate socket from shared data
|
||||
sdata = q.get()
|
||||
message = q.get()
|
||||
|
||||
s = socket.fromshare(sdata)
|
||||
s2, c = s.accept()
|
||||
|
||||
# Send the message
|
||||
s2.sendall(message)
|
||||
s2.close()
|
||||
s.close()
|
||||
|
||||
def testShare(self):
|
||||
# Transfer the listening server socket to another process
|
||||
# and service it from there.
|
||||
|
||||
# Create process:
|
||||
q = multiprocessing.Queue()
|
||||
p = multiprocessing.Process(target=self.remoteProcessServer, args=(q,))
|
||||
p.start()
|
||||
|
||||
# Get the shared socket data
|
||||
data = self.serv.share(p.pid)
|
||||
|
||||
# Pass the shared socket to the other process
|
||||
addr = self.serv.getsockname()
|
||||
self.serv.close()
|
||||
q.put(data)
|
||||
|
||||
# The data that the server will send us
|
||||
message = b"slapmahfro"
|
||||
q.put(message)
|
||||
|
||||
# Connect
|
||||
s = socket.create_connection(addr)
|
||||
# listen for the data
|
||||
m = []
|
||||
while True:
|
||||
data = s.recv(100)
|
||||
if not data:
|
||||
break
|
||||
m.append(data)
|
||||
s.close()
|
||||
received = b"".join(m)
|
||||
self.assertEqual(received, message)
|
||||
p.join()
|
||||
|
||||
def testShareLength(self):
|
||||
data = self.serv.share(os.getpid())
|
||||
self.assertRaises(ValueError, socket.fromshare, data[:-1])
|
||||
self.assertRaises(ValueError, socket.fromshare, data+b"foo")
|
||||
|
||||
def compareSockets(self, org, other):
|
||||
# socket sharing is expected to work only for blocking socket
|
||||
# since the internal python timout value isn't transfered.
|
||||
self.assertEqual(org.gettimeout(), None)
|
||||
self.assertEqual(org.gettimeout(), other.gettimeout())
|
||||
|
||||
self.assertEqual(org.family, other.family)
|
||||
self.assertEqual(org.type, other.type)
|
||||
# If the user specified "0" for proto, then
|
||||
# internally windows will have picked the correct value.
|
||||
# Python introspection on the socket however will still return
|
||||
# 0. For the shared socket, the python value is recreated
|
||||
# from the actual value, so it may not compare correctly.
|
||||
if org.proto != 0:
|
||||
self.assertEqual(org.proto, other.proto)
|
||||
|
||||
def testShareLocal(self):
|
||||
data = self.serv.share(os.getpid())
|
||||
s = socket.fromshare(data)
|
||||
try:
|
||||
self.compareSockets(self.serv, s)
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
def testTypes(self):
|
||||
families = [socket.AF_INET, socket.AF_INET6]
|
||||
types = [socket.SOCK_STREAM, socket.SOCK_DGRAM]
|
||||
for f in families:
|
||||
for t in types:
|
||||
source = socket.socket(f, t)
|
||||
try:
|
||||
data = source.share(os.getpid())
|
||||
shared = socket.fromshare(data)
|
||||
try:
|
||||
self.compareSockets(source, shared)
|
||||
finally:
|
||||
shared.close()
|
||||
finally:
|
||||
source.close()
|
||||
|
||||
|
||||
def test_main():
|
||||
tests = [GeneralModuleTests, BasicTCPTest, TCPCloserTest, TCPTimeoutTest,
|
||||
TestExceptions, BufferIOTest, BasicTCPTest2, BasicUDPTest, UDPTimeoutTest ]
|
||||
|
@ -4699,6 +4803,7 @@ def test_main():
|
|||
# These are slow when setitimer() is not available
|
||||
InterruptedRecvTimeoutTest,
|
||||
InterruptedSendTimeoutTest,
|
||||
TestSocketSharing,
|
||||
])
|
||||
|
||||
thread_info = support.threading_setup()
|
||||
|
|
|
@ -232,6 +232,9 @@ Library
|
|||
- Issue #14210: pdb now has tab-completion not only for command names, but
|
||||
also for their arguments, wherever possible.
|
||||
|
||||
- Issue #14310: Sockets can now be with other processes on Windows using
|
||||
the api socket.socket.share() and socket.fromshare().
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
|
|
|
@ -3771,6 +3771,34 @@ PyDoc_STRVAR(sock_ioctl_doc,
|
|||
Control the socket with WSAIoctl syscall. Currently supported 'cmd' values are\n\
|
||||
SIO_RCVALL: 'option' must be one of the socket.RCVALL_* constants.\n\
|
||||
SIO_KEEPALIVE_VALS: 'option' is a tuple of (onoff, timeout, interval).");
|
||||
#endif
|
||||
|
||||
#if defined(MS_WINDOWS)
|
||||
static PyObject*
|
||||
sock_share(PySocketSockObject *s, PyObject *arg)
|
||||
{
|
||||
WSAPROTOCOL_INFO info;
|
||||
DWORD processId;
|
||||
int result;
|
||||
|
||||
if (!PyArg_ParseTuple(arg, "I", &processId))
|
||||
return NULL;
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
result = WSADuplicateSocket(s->sock_fd, processId, &info);
|
||||
Py_END_ALLOW_THREADS
|
||||
if (result == SOCKET_ERROR)
|
||||
return set_error();
|
||||
return PyBytes_FromStringAndSize((const char*)&info, sizeof(info));
|
||||
}
|
||||
PyDoc_STRVAR(sock_share_doc,
|
||||
"share(process_id) -> bytes\n\
|
||||
\n\
|
||||
Share the socket with another process. The target process id\n\
|
||||
must be provided and the resulting bytes object passed to the target\n\
|
||||
process. There the shared socket can be instantiated by calling\n\
|
||||
socket.fromshare().");
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
|
@ -3802,6 +3830,10 @@ static PyMethodDef sock_methods[] = {
|
|||
#if defined(MS_WINDOWS) && defined(SIO_RCVALL)
|
||||
{"ioctl", (PyCFunction)sock_ioctl, METH_VARARGS,
|
||||
sock_ioctl_doc},
|
||||
#endif
|
||||
#if defined(MS_WINDOWS)
|
||||
{"share", (PyCFunction)sock_share, METH_VARARGS,
|
||||
sock_share_doc},
|
||||
#endif
|
||||
{"listen", (PyCFunction)sock_listen, METH_O,
|
||||
listen_doc},
|
||||
|
@ -3930,13 +3962,40 @@ sock_initobj(PyObject *self, PyObject *args, PyObject *kwds)
|
|||
return -1;
|
||||
|
||||
if (fdobj != NULL && fdobj != Py_None) {
|
||||
fd = PyLong_AsSocket_t(fdobj);
|
||||
if (fd == (SOCKET_T)(-1) && PyErr_Occurred())
|
||||
return -1;
|
||||
if (fd == INVALID_SOCKET) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
"can't use invalid socket value");
|
||||
return -1;
|
||||
#ifdef MS_WINDOWS
|
||||
/* recreate a socket that was duplicated */
|
||||
if (PyBytes_Check(fdobj)) {
|
||||
WSAPROTOCOL_INFO info;
|
||||
if (PyBytes_GET_SIZE(fdobj) != sizeof(info)) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"socket descriptor string has wrong size, "
|
||||
"should be %zu bytes.", sizeof(info));
|
||||
return -1;
|
||||
}
|
||||
memcpy(&info, PyBytes_AS_STRING(fdobj), sizeof(info));
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
fd = WSASocket(FROM_PROTOCOL_INFO, FROM_PROTOCOL_INFO,
|
||||
FROM_PROTOCOL_INFO, &info, 0, WSA_FLAG_OVERLAPPED);
|
||||
Py_END_ALLOW_THREADS
|
||||
if (fd == INVALID_SOCKET) {
|
||||
set_error();
|
||||
return -1;
|
||||
}
|
||||
family = info.iAddressFamily;
|
||||
type = info.iSocketType;
|
||||
proto = info.iProtocol;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
fd = PyLong_AsSocket_t(fdobj);
|
||||
if (fd == (SOCKET_T)(-1) && PyErr_Occurred())
|
||||
return -1;
|
||||
if (fd == INVALID_SOCKET) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
"can't use invalid socket value");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
Loading…
Reference in New Issue