diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 836aa91bb0885b1..33358b00bcf6752 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -378,6 +378,10 @@ The AF_* and SOCK_* constants are now :class:`AddressFamily` and defined then this protocol is unsupported. More constants may be available depending on the system. + .. versionchanged:: next + :const:`AF_UNIX` is now available on Windows. It requires Windows 10 + version 1803 or newer. + .. data:: AF_UNSPEC :const:`AF_UNSPEC` means that diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 64a986f2487d5cb..b3ef0d5585d777f 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -150,6 +150,14 @@ shlex (Contributed by Jay Berry in :gh:`148846`.) +socket +------ + +* Unix domain sockets (:const:`~socket.AF_UNIX`) are now supported on Windows, + which requires Windows 10 version 1803 or newer. + (Contributed by An Long in :gh:`77589`.) + + tkinter ------- diff --git a/Lib/http/server.py b/Lib/http/server.py index ebc85052aecb900..bec3e361b4e4779 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -117,6 +117,14 @@ class HTTPServer(socketserver.TCPServer): allow_reuse_address = True # Seems to make sense in testing environment allow_reuse_port = False + def __init__(self, *args, **kwargs): + if sys.platform == 'win32' and hasattr(socket, 'AF_UNIX') and\ + self.address_family == socket.AF_UNIX: + # reuse address with AF_UNIX is not supported on Windows + self.allow_reuse_address = False + + super().__init__(*args, **kwargs) + def server_bind(self): """Override server_bind to store the server name.""" socketserver.TCPServer.server_bind(self) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 115a187a8a85882..b64270a925df095 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -5744,11 +5744,6 @@ def test_invalid_family(self): with self.assertRaises(ValueError): multiprocessing.connection.Listener(r'\\.\test') - @unittest.skipUnless(WIN32, "skipped on non-Windows platforms") - def test_invalid_family_win32(self): - with self.assertRaises(ValueError): - multiprocessing.connection.Listener('/var/test.pipe') - # # Issue 12098: check sys.flags of child matches that for parent # diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index e59bc25668b4cba..86ebba64aed657c 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1942,6 +1942,8 @@ def test_create_datagram_endpoint_sock(self): self.assertEqual('CLOSED', protocol.state) @unittest.skipUnless(hasattr(socket, 'AF_UNIX'), 'No UNIX Sockets') + @unittest.skipIf(sys.platform == 'win32', 'AF_UNIX support for asyncio is ' + 'not implemented on Windows for now') def test_create_datagram_endpoint_sock_unix(self): fut = self.loop.create_datagram_endpoint( lambda: MyDatagramProto(create_future=True, loop=self.loop), diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index 919d543b0329e9f..5fc9556f9920f1c 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -1035,6 +1035,9 @@ def test_create_server_reuse_port(self): server.close() def _make_unix_server(self, factory, **kwargs): + if sys.platform == 'win32': + raise unittest.SkipTest('AF_UNIX support for asyncio is not ' + 'implemented on Windows for now') path = test_utils.gen_unix_socket_path() self.addCleanup(lambda: os.path.exists(path) and os.unlink(path)) @@ -1072,6 +1075,8 @@ def test_create_unix_server(self): server.close() @unittest.skipUnless(hasattr(socket, 'AF_UNIX'), 'No UNIX Sockets') + @unittest.skipIf(sys.platform == 'win32', 'AF_UNIX support for asyncio is ' + 'not implemented on Windows for now') def test_create_unix_server_path_socket_error(self): proto = MyProto(loop=self.loop) sock = socket.socket() diff --git a/Lib/test/test_asyncio/utils.py b/Lib/test/test_asyncio/utils.py index 62cfcf8ceb5f2a8..8fb87c95a5af85e 100644 --- a/Lib/test/test_asyncio/utils.py +++ b/Lib/test/test_asyncio/utils.py @@ -243,6 +243,9 @@ def gen_unix_socket_path(): @contextlib.contextmanager def unix_socket_path(): + if sys.platform == 'win32': + raise unittest.SkipTest('AF_UNIX support for asyncio is not ' + 'implemented on Windows for now') path = gen_unix_socket_path() try: yield path @@ -255,6 +258,9 @@ def unix_socket_path(): @contextlib.contextmanager def run_test_unix_server(*, use_ssl=False): + if sys.platform == 'win32': + raise unittest.SkipTest('AF_UNIX support for asyncio is not ' + 'implemented on Windows for now') with unix_socket_path() as path: yield from _run_test_server(address=path, use_ssl=use_ssl, server_cls=SilentUnixWSGIServer, diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index fcd3da61a078aec..5d81209a67245b3 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -2105,6 +2105,8 @@ def test_output(self): self.assertEqual(self.log_output, b'<11>h\xc3\xa4m-sp\xc3\xa4m') def test_udp_reconnection(self): + if self.server_exception: + self.skipTest(self.server_exception) logger = logging.getLogger("slh") self.sl_hdlr.close() self.handled.clear() diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 2cb4876f5c6400a..0a22916f90c2589 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -2860,6 +2860,8 @@ def test_is_socket_false(self): @unittest.skipIf( is_wasi, "Cannot create socket on WASI." ) + @unittest.skipIf(sys.platform == 'win32', + "detecting if file is socket is not supported by Windows") def test_is_socket_true(self): P = self.cls(self.base, 'mysock') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -2876,6 +2878,26 @@ def test_is_socket_true(self): self.assertIs(self.cls(self.base, 'mysock\udfff').is_socket(), False) self.assertIs(self.cls(self.base, 'mysock\x00').is_socket(), False) + @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "Unix sockets required") + @unittest.skipUnless(sys.platform == 'win32', + "socket file on Windows is a normal file") + def test_is_socket_on_windows(self): + P = self.cls(self.base, 'mysock') + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(sock.close) + try: + sock.bind(str(P)) + except OSError as e: + if (isinstance(e, PermissionError) or + "AF_UNIX path too long" in str(e)): + self.skipTest("cannot bind Unix socket: " + str(e)) + raise + self.assertFalse(P.is_socket()) + self.assertFalse(P.is_fifo()) + self.assertTrue(P.is_file()) + self.assertIs(self.cls(self.base, 'mysock\udfff').is_socket(), False) + self.assertIs(self.cls(self.base, 'mysock\x00').is_socket(), False) + def test_is_block_device_false(self): P = self.cls(self.base) self.assertFalse((P / 'fileA').is_block_device()) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 63c465e3bfea16d..9e82589bc84e13f 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -5275,7 +5275,7 @@ def __init__(self, methodName='runTest'): def _check_defaults(self, sock): self.assertIsInstance(sock, socket.socket) - if hasattr(socket, 'AF_UNIX'): + if sys.platform != 'win32' and hasattr(socket, 'AF_UNIX'): self.assertEqual(sock.family, socket.AF_UNIX) else: self.assertEqual(sock.family, socket.AF_INET) @@ -6332,10 +6332,19 @@ def bind(self, sock, path): else: raise + @unittest.skipIf(sys.platform == 'win32', + 'Windows will raise Error if is not bound') def testUnbound(self): # Issue #30205 (note getsockname() can return None on OS X) self.assertIn(self.sock.getsockname(), ('', None)) + @unittest.skipUnless(sys.platform == 'win32', + 'Windows-specific behavior') + def test_unbound_on_windows(self): + with self.assertRaises(OSError) as cm: + self.sock.getsockname() + self.assertEqual(cm.exception.winerror, errno.WSAEINVAL) + def testStrAddr(self): # Test binding to and retrieving a normal string pathname. path = os.path.abspath(os_helper.TESTFN) @@ -6350,6 +6359,8 @@ def testBytesAddr(self): self.addCleanup(os_helper.unlink, path) self.assertEqual(self.sock.getsockname(), path) + @unittest.skipIf(sys.platform == 'win32', + 'surrogateescape file path is not supported on Windows') def testSurrogateescapeBind(self): # Test binding to a valid non-ASCII pathname, with the # non-ASCII bytes supplied using surrogateescape encoding. @@ -6359,6 +6370,9 @@ def testSurrogateescapeBind(self): self.addCleanup(os_helper.unlink, path) self.assertEqual(self.sock.getsockname(), path) + @unittest.skipIf(sys.platform == 'win32', + 'Windows has a bug which can\'t unlink sock file with ' + 'TESTFN_UNENCODABLE in its name') def testUnencodableAddr(self): # Test binding to a pathname that cannot be encoded in the # file system encoding. @@ -6371,10 +6385,18 @@ def testUnencodableAddr(self): @unittest.skipIf(sys.platform in ('linux', 'android'), 'Linux behavior is tested by TestLinuxAbstractNamespace') + @unittest.skipIf(sys.platform == 'win32', + 'Windows allow bind on empty path') def testEmptyAddress(self): # Test that binding empty address fails. self.assertRaises(OSError, self.sock.bind, "") + @unittest.skipUnless(sys.platform == 'win32', + 'Windows-specific behavior') + def test_empty_address_on_windows(self): + self.sock.bind('') + self.assertEqual(self.sock.getsockname(), '') + class BufferIOTest(SocketConnectedTest): """ diff --git a/Lib/test/test_socketserver.py b/Lib/test/test_socketserver.py index ca33a9a6dac8d34..6c0476517d90ff9 100644 --- a/Lib/test/test_socketserver.py +++ b/Lib/test/test_socketserver.py @@ -8,6 +8,7 @@ import select import signal import socket +import sys import threading import unittest import socketserver @@ -223,6 +224,8 @@ def test_ForkingUDPServer(self): self.dgram_examine) @requires_unix_sockets + @unittest.skipIf(sys.platform == "win32", + "Unix with Datagram is not supported on Windows") @unittest.skipIf(test.support.is_apple_mobile and test.support.on_github_actions, "gh-140702: Test fails regularly on iOS simulator on GitHub Actions") def test_UnixDatagramServer(self): @@ -231,6 +234,8 @@ def test_UnixDatagramServer(self): self.dgram_examine) @requires_unix_sockets + @unittest.skipIf(sys.platform == "win32", + "Unix with Datagram is not supported on Windows") @unittest.skipIf(test.support.is_apple_mobile and test.support.on_github_actions, "gh-140702: Test fails regularly on iOS simulator on GitHub Actions") def test_ThreadingUnixDatagramServer(self): @@ -240,6 +245,8 @@ def test_ThreadingUnixDatagramServer(self): @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @requires_unix_sockets + @unittest.skipIf(sys.platform == "win32", + "Unix with Datagram is not supported on Windows") @requires_forking def test_ForkingUnixDatagramServer(self): self.run_server(socketserver.ForkingUnixDatagramServer, diff --git a/Lib/test/test_stat.py b/Lib/test/test_stat.py index a83f7d076f027ef..05efd3759fb1280 100644 --- a/Lib/test/test_stat.py +++ b/Lib/test/test_stat.py @@ -220,6 +220,7 @@ def test_devices(self): break @socket_helper.skip_unless_bind_unix_socket + @unittest.skipIf(sys.platform == 'win32', "didn't work on Windows") def test_socket(self): with socket.socket(socket.AF_UNIX) as s: s.bind(TESTFN) @@ -227,6 +228,15 @@ def test_socket(self): self.assertEqual(modestr[0], 's') self.assertS_IS("SOCK", st_mode) + @socket_helper.skip_unless_bind_unix_socket + @unittest.skipUnless(sys.platform == 'win32', "didn't work on Windows") + def test_socket_on_windows(self): + with socket.socket(socket.AF_UNIX) as s: + s.bind(TESTFN) + st_mode, modestr = self.get_mode() + self.assertNotEqual(modestr[0], 's') + self.assertS_IS("REG", st_mode) + def test_module_attributes(self): for key, value in self.stat_struct.items(): modvalue = getattr(self.statmod, key) diff --git a/Misc/NEWS.d/next/Windows/2025-08-05-23-59-59.gh-issue-77589.soytRy.rst b/Misc/NEWS.d/next/Windows/2025-08-05-23-59-59.gh-issue-77589.soytRy.rst new file mode 100644 index 000000000000000..63c85a574163a16 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2025-08-05-23-59-59.gh-issue-77589.soytRy.rst @@ -0,0 +1,2 @@ +Unix domain sockets (:const:`socket.AF_UNIX`) are now supported on Windows, +which requires Windows 10 version 1803 or newer. Patch by An Long. diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index 3e82af3194d053a..1fd00ed7b86bf11 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -336,6 +336,8 @@ typedef struct { /* IMPORTANT: make sure the list ordered by descending build_number */ static FlagRuntimeInfo win_runtime_flags[] = { + /* available starting with Windows 10 1803 */ + {17134, "AF_UNIX"}, /* available starting with Windows 10 1709 */ {16299, "TCP_KEEPIDLE"}, {16299, "TCP_KEEPINTVL"}, @@ -1910,7 +1912,7 @@ getsockaddrarg(PySocketSockObject *s, PyObject *args, addr->sun_path[path.len] = 0; /* including the tailing NUL */ - *len_ret = path.len + offsetof(struct sockaddr_un, sun_path) + 1; + *len_ret = (int)path.len + offsetof(struct sockaddr_un, sun_path) + 1; } addr->sun_family = s->sock_family; memcpy(addr->sun_path, path.buf, path.len); diff --git a/Modules/socketmodule.h b/Modules/socketmodule.h index ac770889ae87f2c..7a343d808020289 100644 --- a/Modules/socketmodule.h +++ b/Modules/socketmodule.h @@ -96,6 +96,8 @@ typedef int socklen_t; #ifdef HAVE_SYS_UN_H # include +#elif HAVE_AFUNIX_H +# include #else # undef AF_UNIX #endif diff --git a/PC/pyconfig.h b/PC/pyconfig.h index 2381ed3772b109e..eb8814345626792 100644 --- a/PC/pyconfig.h +++ b/PC/pyconfig.h @@ -673,6 +673,13 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* Define if you have the header file. */ /* #define HAVE_SYS_UN_H 1 */ +/* Define if you have the header file. */ +#if defined(__has_include) && __has_include() + #define HAVE_AFUNIX_H 1 +#else + #define HAVE_AFUNIX_H 0 +#endif + /* Define if you have the header file. */ /* #define HAVE_SYS_UTIME_H 1 */