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 6e69737768d5e15..66063fb1fbca957 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -438,6 +438,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`.) + + symtable -------- diff --git a/Lib/socketserver.py b/Lib/socketserver.py index ec389457ef5b1c1..e57e53b9ce83c0c 100644 --- a/Lib/socketserver.py +++ b/Lib/socketserver.py @@ -463,7 +463,13 @@ def server_bind(self): May be overridden. """ - if self.allow_reuse_address and hasattr(socket, "SO_REUSEADDR"): + # On Windows, SO_REUSEADDR is not supported with AF_UNIX: setting + # it succeeds but the following bind() fails with WSAEOPNOTSUPP. + if ( + self.allow_reuse_address and hasattr(socket, "SO_REUSEADDR") + and not (sys.platform == "win32" and hasattr(socket, "AF_UNIX") + and self.address_family == socket.AF_UNIX) + ): self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Since Linux 6.12.9, SO_REUSEPORT is not allowed # on other address families than AF_INET/AF_INET6. diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 36e0880bc088189..c642cd145d237d1 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -5906,11 +5906,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 18afdca23163a1e..40f86eb1315a217 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -2042,6 +2042,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 3fdfffdb213efc2..354bcf90522a466 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -1036,6 +1036,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)) @@ -1073,6 +1076,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 e86c221d7f70259..da38db25319b2aa 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 ccc7cce86883c89..9682c2a7d4e0dd5 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 aff66c8efedbbc0..e5f0b8986297a65 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', + "socket file is a regular file on Windows") def test_is_socket_true(self): P = self.cls(self.base, 'mysock') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -2877,6 +2879,25 @@ 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', "Windows-specific behavior") + 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_shutil.py b/Lib/test/test_shutil.py index ed5d15ecc7ddad6..606fcb9399a6ef7 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1559,6 +1559,8 @@ def test_copyfile_named_pipe(self): os.remove(TESTFN) @socket_helper.skip_unless_bind_unix_socket + @unittest.skipIf(sys.platform == 'win32', + 'socket file is not a special file on Windows') def test_copyfile_socket(self): sock_path = os.path.join(self.mkdtemp(), 'sock') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -1604,6 +1606,8 @@ def test_copyfile_symlink_to_named_pipe(self): @os_helper.skip_unless_symlink @socket_helper.skip_unless_bind_unix_socket + @unittest.skipIf(sys.platform == 'win32', + 'socket file is not a special file on Windows') def test_copyfile_symlink_to_socket(self): sock_path = os.path.join(self.mkdtemp(), 'sock') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 7bb50f7b8aa47e2..09da1a5d2f854e6 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -5321,7 +5321,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) @@ -6378,10 +6378,20 @@ def bind(self, sock, path): else: raise + @unittest.skipIf(sys.platform == 'win32', + 'getsockname() raises OSError on an unbound socket on ' + 'Windows') 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) @@ -6396,6 +6406,10 @@ def testBytesAddr(self): self.addCleanup(os_helper.unlink, path) self.assertEqual(self.sock.getsockname(), path) + @unittest.skipIf(sys.platform == 'win32', + 'Windows replaces invalid UTF-8 sequences in the path ' + 'with U+FFFD, so the socket file is created under a ' + 'different name') def testSurrogateescapeBind(self): # Test binding to a valid non-ASCII pathname, with the # non-ASCII bytes supplied using surrogateescape encoding. @@ -6405,6 +6419,10 @@ def testSurrogateescapeBind(self): self.addCleanup(os_helper.unlink, path) self.assertEqual(self.sock.getsockname(), path) + @unittest.skipIf(sys.platform == 'win32', + 'Windows replaces invalid UTF-8 sequences in the path ' + 'with U+FFFD, so the socket file is created under a ' + 'different name') def testUnencodableAddr(self): # Test binding to a pathname that cannot be encoded in the # file system encoding. @@ -6417,10 +6435,18 @@ def testUnencodableAddr(self): @unittest.skipIf(sys.platform in ('linux', 'android'), 'Linux behavior is tested by TestLinuxAbstractNamespace') + @unittest.skipIf(sys.platform == 'win32', + 'Windows allows binding to an 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..05bd2c04a16c439 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", + "AF_UNIX with SOCK_DGRAM 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", + "AF_UNIX with SOCK_DGRAM 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", + "AF_UNIX with SOCK_DGRAM 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..7329fcbc3718e76 100644 --- a/Lib/test/test_stat.py +++ b/Lib/test/test_stat.py @@ -220,6 +220,8 @@ def test_devices(self): break @socket_helper.skip_unless_bind_unix_socket + @unittest.skipIf(sys.platform == 'win32', + 'socket file is a regular file on Windows') def test_socket(self): with socket.socket(socket.AF_UNIX) as s: s.bind(TESTFN) @@ -227,6 +229,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', 'Windows-specific behavior') + 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 73ae1c942daba48..e9c81389251c35b 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"}, @@ -1920,7 +1922,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..db7b3ee4476e553 100644 --- a/Modules/socketmodule.h +++ b/Modules/socketmodule.h @@ -96,6 +96,8 @@ typedef int socklen_t; #ifdef HAVE_SYS_UN_H # include +#elif defined(HAVE_AFUNIX_H) +# include #else # undef AF_UNIX #endif diff --git a/PC/pyconfig.h b/PC/pyconfig.h index 2381ed3772b109e..3e4b35965b1f57f 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. */ +#ifdef __has_include +# if __has_include() +# define HAVE_AFUNIX_H 1 +# endif +#endif + /* Define if you have the header file. */ /* #define HAVE_SYS_UTIME_H 1 */