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
|
.. 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
|
.. data:: SocketType
|
||||||
|
|
||||||
This is a Python type object that represents the socket object type. It is the
|
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
|
are disallowed. If *how* is :const:`SHUT_RDWR`, further sends and receives are
|
||||||
disallowed.
|
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
|
Note that there are no methods :meth:`read` or :meth:`write`; use
|
||||||
:meth:`~socket.recv` and :meth:`~socket.send` without *flags* argument instead.
|
:meth:`~socket.recv` and :meth:`~socket.send` without *flags* argument instead.
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ Functions:
|
||||||
socket() -- create a new socket object
|
socket() -- create a new socket object
|
||||||
socketpair() -- create a pair of new socket objects [*]
|
socketpair() -- create a pair of new socket objects [*]
|
||||||
fromfd() -- create a socket object from an open file descriptor [*]
|
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
|
gethostname() -- return the current hostname
|
||||||
gethostbyname() -- map a hostname to its IP number
|
gethostbyname() -- map a hostname to its IP number
|
||||||
gethostbyaddr() -- map an IP number or hostname to DNS info
|
gethostbyaddr() -- map an IP number or hostname to DNS info
|
||||||
|
@ -209,7 +210,6 @@ class socket(_socket.socket):
|
||||||
self._closed = True
|
self._closed = True
|
||||||
return super().detach()
|
return super().detach()
|
||||||
|
|
||||||
|
|
||||||
def fromfd(fd, family, type, proto=0):
|
def fromfd(fd, family, type, proto=0):
|
||||||
""" fromfd(fd, family, type[, proto]) -> socket object
|
""" fromfd(fd, family, type[, proto]) -> socket object
|
||||||
|
|
||||||
|
@ -219,6 +219,14 @@ def fromfd(fd, family, type, proto=0):
|
||||||
nfd = dup(fd)
|
nfd = dup(fd)
|
||||||
return socket(family, type, proto, nfd)
|
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"):
|
if hasattr(_socket, "socketpair"):
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,10 @@ try:
|
||||||
import fcntl
|
import fcntl
|
||||||
except ImportError:
|
except ImportError:
|
||||||
fcntl = False
|
fcntl = False
|
||||||
|
try:
|
||||||
|
import multiprocessing
|
||||||
|
except ImportError:
|
||||||
|
multiprocessing = False
|
||||||
|
|
||||||
HOST = support.HOST
|
HOST = support.HOST
|
||||||
MSG = 'Michael Gilfix was here\u1234\r\n'.encode('utf-8') ## test unicode string and carriage return
|
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)
|
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():
|
def test_main():
|
||||||
tests = [GeneralModuleTests, BasicTCPTest, TCPCloserTest, TCPTimeoutTest,
|
tests = [GeneralModuleTests, BasicTCPTest, TCPCloserTest, TCPTimeoutTest,
|
||||||
TestExceptions, BufferIOTest, BasicTCPTest2, BasicUDPTest, UDPTimeoutTest ]
|
TestExceptions, BufferIOTest, BasicTCPTest2, BasicUDPTest, UDPTimeoutTest ]
|
||||||
|
@ -4699,6 +4803,7 @@ def test_main():
|
||||||
# These are slow when setitimer() is not available
|
# These are slow when setitimer() is not available
|
||||||
InterruptedRecvTimeoutTest,
|
InterruptedRecvTimeoutTest,
|
||||||
InterruptedSendTimeoutTest,
|
InterruptedSendTimeoutTest,
|
||||||
|
TestSocketSharing,
|
||||||
])
|
])
|
||||||
|
|
||||||
thread_info = support.threading_setup()
|
thread_info = support.threading_setup()
|
||||||
|
|
|
@ -232,6 +232,9 @@ Library
|
||||||
- Issue #14210: pdb now has tab-completion not only for command names, but
|
- Issue #14210: pdb now has tab-completion not only for command names, but
|
||||||
also for their arguments, wherever possible.
|
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
|
Build
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
@ -3771,6 +3771,34 @@ PyDoc_STRVAR(sock_ioctl_doc,
|
||||||
Control the socket with WSAIoctl syscall. Currently supported 'cmd' values are\n\
|
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_RCVALL: 'option' must be one of the socket.RCVALL_* constants.\n\
|
||||||
SIO_KEEPALIVE_VALS: 'option' is a tuple of (onoff, timeout, interval).");
|
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
|
#endif
|
||||||
|
|
||||||
|
@ -3802,6 +3830,10 @@ static PyMethodDef sock_methods[] = {
|
||||||
#if defined(MS_WINDOWS) && defined(SIO_RCVALL)
|
#if defined(MS_WINDOWS) && defined(SIO_RCVALL)
|
||||||
{"ioctl", (PyCFunction)sock_ioctl, METH_VARARGS,
|
{"ioctl", (PyCFunction)sock_ioctl, METH_VARARGS,
|
||||||
sock_ioctl_doc},
|
sock_ioctl_doc},
|
||||||
|
#endif
|
||||||
|
#if defined(MS_WINDOWS)
|
||||||
|
{"share", (PyCFunction)sock_share, METH_VARARGS,
|
||||||
|
sock_share_doc},
|
||||||
#endif
|
#endif
|
||||||
{"listen", (PyCFunction)sock_listen, METH_O,
|
{"listen", (PyCFunction)sock_listen, METH_O,
|
||||||
listen_doc},
|
listen_doc},
|
||||||
|
@ -3930,13 +3962,40 @@ sock_initobj(PyObject *self, PyObject *args, PyObject *kwds)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
if (fdobj != NULL && fdobj != Py_None) {
|
if (fdobj != NULL && fdobj != Py_None) {
|
||||||
fd = PyLong_AsSocket_t(fdobj);
|
#ifdef MS_WINDOWS
|
||||||
if (fd == (SOCKET_T)(-1) && PyErr_Occurred())
|
/* recreate a socket that was duplicated */
|
||||||
return -1;
|
if (PyBytes_Check(fdobj)) {
|
||||||
if (fd == INVALID_SOCKET) {
|
WSAPROTOCOL_INFO info;
|
||||||
PyErr_SetString(PyExc_ValueError,
|
if (PyBytes_GET_SIZE(fdobj) != sizeof(info)) {
|
||||||
"can't use invalid socket value");
|
PyErr_Format(PyExc_ValueError,
|
||||||
return -1;
|
"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 {
|
else {
|
||||||
|
|
Loading…
Reference in New Issue