]> Cypherpunks repositories - gostls13.git/commitdiff
net,os: support converting between *os.File and net.Conn on Windows
authorqmuntal <quimmuntal@gmail.com>
Fri, 25 Apr 2025 12:11:31 +0000 (14:11 +0200)
committerQuim Muntal <quimmuntal@gmail.com>
Mon, 5 May 2025 11:05:18 +0000 (04:05 -0700)
The runtime poller and os.NewFile recently gained support for
disassociating the handle from the runtime poller IOCP (see CL 664455).
This was the main blocker for allowing the conversion between *os.File
and net.Conn.

Implementing the conversion is now trivial. The only remaining work,
implemented in this CL, is improving os.NewFile to also support
socket handles and updating some build tags so that Windows can share
almost the same net's File implementation as Unix.

There is one important limitation, though: the duplicated socket handle
returned by the various File methods in the net package is not
usable on other process. If someone needs to pass a socket handle to
another process, they should manually call the WSADuplicateSocket
Windows API passing the process ID of the target process.

Fixes #9503.
Fixes #10350.
Updates #19098.

Cq-Include-Trybots: luci.golang.try:gotip-windows-amd64-race,gotip-windows-amd64-longtest,gotip-windows-arm64
Change-Id: Ic43cadaac2662b925d57a9d362ddc7ae21d1b56e
Reviewed-on: https://go-review.googlesource.com/c/go/+/668195
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
24 files changed:
doc/next/6-stdlib/99-minor/net/10350.md [new file with mode: 0644]
doc/next/6-stdlib/99-minor/net/9503.md [new file with mode: 0644]
src/internal/syscall/windows/net_windows.go
src/internal/syscall/windows/syscall_windows.go
src/internal/syscall/windows/zsyscall_windows.go
src/net/error_test.go
src/net/fd_posix.go
src/net/fd_unix.go
src/net/fd_windows.go
src/net/file.go
src/net/file_posix.go [new file with mode: 0644]
src/net/file_test.go
src/net/file_unix.go
src/net/file_windows.go
src/net/main_plan9_test.go
src/net/main_unix_test.go
src/net/main_wasm_test.go
src/net/main_windows_test.go
src/net/mockserver_test.go
src/net/net.go
src/net/tcpsock.go
src/net/unixsock.go
src/net/unixsock_test.go
src/os/file_windows.go

diff --git a/doc/next/6-stdlib/99-minor/net/10350.md b/doc/next/6-stdlib/99-minor/net/10350.md
new file mode 100644 (file)
index 0000000..7290112
--- /dev/null
@@ -0,0 +1,3 @@
+On Windows, the [TCPConn.File], [UDPConn.File], [UnixConn.File],
+[IPConn.File], [TCPListener.File], and [UnixListener.File]
+methods are now supported.
\ No newline at end of file
diff --git a/doc/next/6-stdlib/99-minor/net/9503.md b/doc/next/6-stdlib/99-minor/net/9503.md
new file mode 100644 (file)
index 0000000..d2aef10
--- /dev/null
@@ -0,0 +1,2 @@
+On Windows, the [FileConn], [FilePacketConn], [FileListener]
+functions are now supported.
index 9fa5ecf8408321943e0085b294ea475ede4487d4..023ddaaa8c1af683a62f24dfb6d96593d9af2906 100644 (file)
@@ -18,6 +18,7 @@ func WSASendtoInet4(s syscall.Handle, bufs *syscall.WSABuf, bufcnt uint32, sent
 func WSASendtoInet6(s syscall.Handle, bufs *syscall.WSABuf, bufcnt uint32, sent *uint32, flags uint32, to *syscall.SockaddrInet6, overlapped *syscall.Overlapped, croutine *byte) (err error)
 
 const (
+       SO_TYPE                                = 0x1008
        SIO_TCP_INITIAL_RTO                    = syscall.IOC_IN | syscall.IOC_VENDOR | 17
        TCP_INITIAL_RTO_UNSPECIFIED_RTT        = ^uint16(0)
        TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS = ^uint8(1)
index b6692166cc460fbae723b159abe96b0e4932b440..20e6ae57a8cd0183849f4ec5fcbb9bf3a47beef0 100644 (file)
@@ -268,6 +268,7 @@ type WSAMsg struct {
 }
 
 //sys  WSASocket(af int32, typ int32, protocol int32, protinfo *syscall.WSAProtocolInfo, group uint32, flags uint32) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = ws2_32.WSASocketW
+//sys  WSADuplicateSocket(s syscall.Handle, processID uint32, info *syscall.WSAProtocolInfo) (err error) [failretval!=0] = ws2_32.WSADuplicateSocketW
 //sys  WSAGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
 
 func loadWSASendRecvMsg() error {
index c53c517198ed1d82bfccbdcdd6306e464e6676bc..8dcb377c3e7e8d9f7ad7652df43617761db8f204 100644 (file)
@@ -106,6 +106,7 @@ var (
        procCreateEnvironmentBlock            = moduserenv.NewProc("CreateEnvironmentBlock")
        procDestroyEnvironmentBlock           = moduserenv.NewProc("DestroyEnvironmentBlock")
        procGetProfilesDirectoryW             = moduserenv.NewProc("GetProfilesDirectoryW")
+       procWSADuplicateSocketW               = modws2_32.NewProc("WSADuplicateSocketW")
        procWSAGetOverlappedResult            = modws2_32.NewProc("WSAGetOverlappedResult")
        procWSASocketW                        = modws2_32.NewProc("WSASocketW")
 )
@@ -591,6 +592,14 @@ func GetProfilesDirectory(dir *uint16, dirLen *uint32) (err error) {
        return
 }
 
+func WSADuplicateSocket(s syscall.Handle, processID uint32, info *syscall.WSAProtocolInfo) (err error) {
+       r1, _, e1 := syscall.Syscall(procWSADuplicateSocketW.Addr(), 3, uintptr(s), uintptr(processID), uintptr(unsafe.Pointer(info)))
+       if r1 != 0 {
+               err = errnoErr(e1)
+       }
+       return
+}
+
 func WSAGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) {
        var _p0 uint32
        if wait {
index f82e8633464176f95c49c1148729e63054cf9ff0..ff254336211606c9c41bbcc4dabf097e9984cfc5 100644 (file)
@@ -736,11 +736,6 @@ third:
 }
 
 func TestFileError(t *testing.T) {
-       switch runtime.GOOS {
-       case "windows":
-               t.Skipf("not supported on %s", runtime.GOOS)
-       }
-
        f, err := os.CreateTemp("", "go-nettest")
        if err != nil {
                t.Fatal(err)
index ffb9bcf8b9e8ca06b68f01217727ca5b5a6bd226..93e6b5378eb75de217b40f9d2d2f10cd8fa8505b 100644 (file)
@@ -26,6 +26,17 @@ type netFD struct {
        raddr       Addr
 }
 
+func (fd *netFD) name() string {
+       var ls, rs string
+       if fd.laddr != nil {
+               ls = fd.laddr.String()
+       }
+       if fd.raddr != nil {
+               rs = fd.raddr.String()
+       }
+       return fd.net + ":" + ls + "->" + rs
+}
+
 func (fd *netFD) setAddr(laddr, raddr Addr) {
        fd.laddr = laddr
        fd.raddr = raddr
index a8d3a253a97b108945306f99dddaf5a025a44f71..8d3858d8bea783fb457da0b4179af2eea2d83398 100644 (file)
@@ -41,17 +41,6 @@ func (fd *netFD) init() error {
        return fd.pfd.Init(fd.net, true)
 }
 
-func (fd *netFD) name() string {
-       var ls, rs string
-       if fd.laddr != nil {
-               ls = fd.laddr.String()
-       }
-       if fd.raddr != nil {
-               rs = fd.raddr.String()
-       }
-       return fd.net + ":" + ls + "->" + rs
-}
-
 func (fd *netFD) connect(ctx context.Context, la, ra syscall.Sockaddr) (rsa syscall.Sockaddr, ret error) {
        // Do not need to call fd.writeLock here,
        // because fd is not yet accessible to user,
index 4ad8e0204fde402f9fb424ea25b7280d642889f4..a23be0501f72f6aeb5b65339301a5b96f2d46a5e 100644 (file)
@@ -233,9 +233,23 @@ func (fd *netFD) accept() (*netFD, error) {
        return netfd, nil
 }
 
-// Unimplemented functions.
-
 func (fd *netFD) dup() (*os.File, error) {
-       // TODO: Implement this, perhaps using internal/poll.DupCloseOnExec.
-       return nil, syscall.EWINDOWS
+       // Disassociate the IOCP from the socket,
+       // it is not safe to share a duplicated handle
+       // that is associated with IOCP.
+       if err := fd.pfd.DisassociateIOCP(); err != nil {
+               return nil, err
+       }
+       var h syscall.Handle
+       var syserr error
+       err := fd.pfd.RawControl(func(fd uintptr) {
+               h, syserr = dupSocket(syscall.Handle(fd))
+       })
+       if err != nil {
+               err = syserr
+       }
+       if err != nil {
+               return nil, err
+       }
+       return os.NewFile(uintptr(h), fd.name()), nil
 }
index c13332c188aeef25def410a3d1b9833256540cb3..3e33c9afad65845eeb3a85a378d038d42a230c6b 100644 (file)
@@ -6,7 +6,7 @@ package net
 
 import "os"
 
-// BUG(mikio): On JS and Windows, the FileConn, FileListener and
+// BUG(mikio): On JS, the FileConn, FileListener and
 // FilePacketConn functions are not implemented.
 
 type fileAddr string
diff --git a/src/net/file_posix.go b/src/net/file_posix.go
new file mode 100644 (file)
index 0000000..132d03e
--- /dev/null
@@ -0,0 +1,104 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build unix || windows
+
+package net
+
+import (
+       "internal/poll"
+       "os"
+       "syscall"
+)
+
+func newFileFD(f *os.File) (*netFD, error) {
+       s, err := dupFileSocket(f)
+       if err != nil {
+               return nil, err
+       }
+       family := syscall.AF_UNSPEC
+       sotype, err := syscall.GetsockoptInt(s, syscall.SOL_SOCKET, _SO_TYPE)
+       if err != nil {
+               poll.CloseFunc(s)
+               return nil, os.NewSyscallError("getsockopt", err)
+       }
+       lsa, _ := syscall.Getsockname(s)
+       rsa, _ := syscall.Getpeername(s)
+       switch lsa.(type) {
+       case *syscall.SockaddrInet4:
+               family = syscall.AF_INET
+       case *syscall.SockaddrInet6:
+               family = syscall.AF_INET6
+       case *syscall.SockaddrUnix:
+               family = syscall.AF_UNIX
+       default:
+               poll.CloseFunc(s)
+               return nil, syscall.EPROTONOSUPPORT
+       }
+       fd, err := newFD(s, family, sotype, "")
+       if err != nil {
+               poll.CloseFunc(s)
+               return nil, err
+       }
+       laddr := fd.addrFunc()(lsa)
+       raddr := fd.addrFunc()(rsa)
+       fd.net = laddr.Network()
+       if err := fd.init(); err != nil {
+               fd.Close()
+               return nil, err
+       }
+       fd.setAddr(laddr, raddr)
+       return fd, nil
+}
+
+func fileConn(f *os.File) (Conn, error) {
+       fd, err := newFileFD(f)
+       if err != nil {
+               return nil, err
+       }
+       switch fd.laddr.(type) {
+       case *TCPAddr:
+               return newTCPConn(fd, defaultTCPKeepAliveIdle, KeepAliveConfig{}, testPreHookSetKeepAlive, testHookSetKeepAlive), nil
+       case *UDPAddr:
+               return newUDPConn(fd), nil
+       case *IPAddr:
+               return newIPConn(fd), nil
+       case *UnixAddr:
+               return newUnixConn(fd), nil
+       }
+       fd.Close()
+       return nil, syscall.EINVAL
+}
+
+func fileListener(f *os.File) (Listener, error) {
+       fd, err := newFileFD(f)
+       if err != nil {
+               return nil, err
+       }
+       switch laddr := fd.laddr.(type) {
+       case *TCPAddr:
+               return &TCPListener{fd: fd}, nil
+       case *UnixAddr:
+               return &UnixListener{fd: fd, path: laddr.Name, unlink: false}, nil
+       }
+       fd.Close()
+       return nil, syscall.EINVAL
+}
+
+func filePacketConn(f *os.File) (PacketConn, error) {
+       fd, err := newFileFD(f)
+       if err != nil {
+               return nil, err
+       }
+       switch fd.laddr.(type) {
+       case *UDPAddr:
+               return newUDPConn(fd), nil
+       case *IPAddr:
+               return newIPConn(fd), nil
+       case *UnixAddr:
+               return newUnixConn(fd), nil
+       }
+       fd.Close()
+       return nil, syscall.EINVAL
+}
index c517af50c5ee2e11cf10664650154fb81aa5526c..b5d007d6cfc4d574f8b25bbc0966acc6b00676f2 100644 (file)
@@ -29,7 +29,7 @@ var fileConnTests = []struct {
 
 func TestFileConn(t *testing.T) {
        switch runtime.GOOS {
-       case "plan9", "windows", "js", "wasip1":
+       case "plan9", "js", "wasip1":
                t.Skipf("not supported on %s", runtime.GOOS)
        }
 
@@ -130,7 +130,7 @@ var fileListenerTests = []struct {
 
 func TestFileListener(t *testing.T) {
        switch runtime.GOOS {
-       case "plan9", "windows", "js", "wasip1":
+       case "plan9", "js", "wasip1":
                t.Skipf("not supported on %s", runtime.GOOS)
        }
 
@@ -222,7 +222,7 @@ var filePacketConnTests = []struct {
 
 func TestFilePacketConn(t *testing.T) {
        switch runtime.GOOS {
-       case "plan9", "windows", "js", "wasip1":
+       case "plan9", "js", "wasip1":
                t.Skipf("not supported on %s", runtime.GOOS)
        }
 
@@ -289,7 +289,7 @@ func TestFilePacketConn(t *testing.T) {
 // Issue 24483.
 func TestFileCloseRace(t *testing.T) {
        switch runtime.GOOS {
-       case "plan9", "windows", "js", "wasip1":
+       case "plan9", "js", "wasip1":
                t.Skipf("not supported on %s", runtime.GOOS)
        }
        if !testableNetwork("tcp") {
index c0212cef65dba65f351ae317aa9fea61450f0e5c..d56da9003747ab0f94b10b175ec5719a7d5fe8df 100644 (file)
@@ -12,7 +12,9 @@ import (
        "syscall"
 )
 
-func dupSocket(f *os.File) (int, error) {
+const _SO_TYPE = syscall.SO_TYPE
+
+func dupFileSocket(f *os.File) (int, error) {
        s, call, err := poll.DupCloseOnExec(int(f.Fd()))
        if err != nil {
                if call != "" {
@@ -26,94 +28,3 @@ func dupSocket(f *os.File) (int, error) {
        }
        return s, nil
 }
-
-func newFileFD(f *os.File) (*netFD, error) {
-       s, err := dupSocket(f)
-       if err != nil {
-               return nil, err
-       }
-       family := syscall.AF_UNSPEC
-       sotype, err := syscall.GetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_TYPE)
-       if err != nil {
-               poll.CloseFunc(s)
-               return nil, os.NewSyscallError("getsockopt", err)
-       }
-       lsa, _ := syscall.Getsockname(s)
-       rsa, _ := syscall.Getpeername(s)
-       switch lsa.(type) {
-       case *syscall.SockaddrInet4:
-               family = syscall.AF_INET
-       case *syscall.SockaddrInet6:
-               family = syscall.AF_INET6
-       case *syscall.SockaddrUnix:
-               family = syscall.AF_UNIX
-       default:
-               poll.CloseFunc(s)
-               return nil, syscall.EPROTONOSUPPORT
-       }
-       fd, err := newFD(s, family, sotype, "")
-       if err != nil {
-               poll.CloseFunc(s)
-               return nil, err
-       }
-       laddr := fd.addrFunc()(lsa)
-       raddr := fd.addrFunc()(rsa)
-       fd.net = laddr.Network()
-       if err := fd.init(); err != nil {
-               fd.Close()
-               return nil, err
-       }
-       fd.setAddr(laddr, raddr)
-       return fd, nil
-}
-
-func fileConn(f *os.File) (Conn, error) {
-       fd, err := newFileFD(f)
-       if err != nil {
-               return nil, err
-       }
-       switch fd.laddr.(type) {
-       case *TCPAddr:
-               return newTCPConn(fd, defaultTCPKeepAliveIdle, KeepAliveConfig{}, testPreHookSetKeepAlive, testHookSetKeepAlive), nil
-       case *UDPAddr:
-               return newUDPConn(fd), nil
-       case *IPAddr:
-               return newIPConn(fd), nil
-       case *UnixAddr:
-               return newUnixConn(fd), nil
-       }
-       fd.Close()
-       return nil, syscall.EINVAL
-}
-
-func fileListener(f *os.File) (Listener, error) {
-       fd, err := newFileFD(f)
-       if err != nil {
-               return nil, err
-       }
-       switch laddr := fd.laddr.(type) {
-       case *TCPAddr:
-               return &TCPListener{fd: fd}, nil
-       case *UnixAddr:
-               return &UnixListener{fd: fd, path: laddr.Name, unlink: false}, nil
-       }
-       fd.Close()
-       return nil, syscall.EINVAL
-}
-
-func filePacketConn(f *os.File) (PacketConn, error) {
-       fd, err := newFileFD(f)
-       if err != nil {
-               return nil, err
-       }
-       switch fd.laddr.(type) {
-       case *UDPAddr:
-               return newUDPConn(fd), nil
-       case *IPAddr:
-               return newIPConn(fd), nil
-       case *UnixAddr:
-               return newUnixConn(fd), nil
-       }
-       fd.Close()
-       return nil, syscall.EINVAL
-}
index 241fa17617c2bf20073d275c479e01ed288dde58..bd7e2bf4806bf8388ee9c1867d072712a8083035 100644 (file)
@@ -5,21 +5,28 @@
 package net
 
 import (
+       "internal/syscall/windows"
        "os"
        "syscall"
 )
 
-func fileConn(f *os.File) (Conn, error) {
-       // TODO: Implement this
-       return nil, syscall.EWINDOWS
-}
+const _SO_TYPE = windows.SO_TYPE
 
-func fileListener(f *os.File) (Listener, error) {
-       // TODO: Implement this
-       return nil, syscall.EWINDOWS
+func dupSocket(h syscall.Handle) (syscall.Handle, error) {
+       var info syscall.WSAProtocolInfo
+       err := windows.WSADuplicateSocket(h, uint32(syscall.Getpid()), &info)
+       if err != nil {
+               return 0, err
+       }
+       return windows.WSASocket(-1, -1, -1, &info, 0, windows.WSA_FLAG_OVERLAPPED|windows.WSA_FLAG_NO_HANDLE_INHERIT)
 }
 
-func filePacketConn(f *os.File) (PacketConn, error) {
-       // TODO: Implement this
-       return nil, syscall.EWINDOWS
+func dupFileSocket(f *os.File) (syscall.Handle, error) {
+       // The resulting handle should not be associated to an IOCP, else the IO operations
+       // will block an OS thread, and that's not what net package users expect.
+       h, err := dupSocket(syscall.Handle(f.Fd()))
+       if err != nil {
+               return 0, err
+       }
+       return h, nil
 }
index 2bc5be88be6481b815b422cdfdde60882e60210f..ca5f19adc149ac026fcc48f9c4e848991bfed822 100644 (file)
@@ -4,6 +4,8 @@
 
 package net
 
+import "os/exec"
+
 func installTestHooks() {}
 
 func uninstallTestHooks() {}
@@ -14,3 +16,5 @@ func forceCloseSockets() {}
 func enableSocketConnect() {}
 
 func disableSocketConnect(network string) {}
+
+func addCmdInheritedHandle(cmd *exec.Cmd, fd uintptr) {}
index e7a5b4fe9ad4eface4f990349e5e9abfc80099be..49d15b963e26301886211a10cab0212003735282 100644 (file)
@@ -6,7 +6,10 @@
 
 package net
 
-import "internal/poll"
+import (
+       "internal/poll"
+       "os/exec"
+)
 
 var (
        // Placeholders for saving original socket system calls.
@@ -53,3 +56,5 @@ func forceCloseSockets() {
                poll.CloseFunc(s)
        }
 }
+
+func addCmdInheritedHandle(cmd *exec.Cmd, fd uintptr) {}
index b8196bb283dc1095923189bbcfef18cff67fcc43..2dcfdabb3b444bc520b35cf79ef11389dc47eb52 100644 (file)
@@ -6,8 +6,12 @@
 
 package net
 
+import "os/exec"
+
 func installTestHooks() {}
 
 func uninstallTestHooks() {}
 
 func forceCloseSockets() {}
+
+func addCmdInheritedHandle(cmd *exec.Cmd, fd uintptr) {}
index bc024c0bbd82d01939bc0fd191739875a4423f4f..425030133535ca90c007e40c87f41ee496f2ad3f 100644 (file)
@@ -4,7 +4,11 @@
 
 package net
 
-import "internal/poll"
+import (
+       "internal/poll"
+       "os/exec"
+       "syscall"
+)
 
 var (
        // Placeholders for saving original socket system calls.
@@ -40,3 +44,14 @@ func forceCloseSockets() {
                poll.CloseFunc(s)
        }
 }
+
+func addCmdInheritedHandle(cmd *exec.Cmd, fd uintptr) {
+       // Inherited handles are not inherited by default in Windows.
+       // We need to set the handle inheritance flag explicitly.
+       // See https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa#parameters
+       // for more details.
+       if cmd.SysProcAttr == nil {
+               cmd.SysProcAttr = &syscall.SysProcAttr{}
+       }
+       cmd.SysProcAttr.AdditionalInheritedHandles = append(cmd.SysProcAttr.AdditionalInheritedHandles, syscall.Handle(fd))
+}
index 63802c575e2a72889ceb367eb95fbf0d3bea3d65..66b8cbe40da5369c9722040446c5c63c03b83972 100644 (file)
@@ -509,6 +509,10 @@ func packetTransceiver(c PacketConn, wb []byte, dst Addr, ch chan<- error) {
 func spawnTestSocketPair(t testing.TB, net string) (client, server Conn) {
        t.Helper()
 
+       if !testableNetwork(net) {
+               t.Skipf("network %q not supported", net)
+       }
+
        ln := newLocalListener(t, net)
        defer ln.Close()
        var cerr, serr error
@@ -536,13 +540,6 @@ func spawnTestSocketPair(t testing.TB, net string) (client, server Conn) {
 
 func startTestSocketPeer(t testing.TB, conn Conn, op string, chunkSize, totalSize int) (func(t testing.TB), error) {
        t.Helper()
-
-       if runtime.GOOS == "windows" {
-               // TODO(panjf2000): Windows has not yet implemented FileConn,
-               //              remove this when it's implemented in https://go.dev/issues/9503.
-               t.Fatalf("startTestSocketPeer is not supported on %s", runtime.GOOS)
-       }
-
        f, err := conn.(interface{ File() (*os.File, error) }).File()
        if err != nil {
                return nil, err
@@ -556,7 +553,14 @@ func startTestSocketPeer(t testing.TB, conn Conn, op string, chunkSize, totalSiz
                "GO_NET_TEST_TRANSFER_TOTAL_SIZE=" + strconv.Itoa(totalSize),
                "TMPDIR=" + os.Getenv("TMPDIR"),
        }
-       cmd.ExtraFiles = append(cmd.ExtraFiles, f)
+       if runtime.GOOS == "windows" {
+               // Windows doesn't support ExtraFiles
+               fd := f.Fd()
+               cmd.Env = append(cmd.Env, "GO_NET_TEST_TRANSFER_FD="+strconv.FormatUint(uint64(fd), 10))
+               addCmdInheritedHandle(cmd, fd)
+       } else {
+               cmd.ExtraFiles = append(cmd.ExtraFiles, f)
+       }
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
 
@@ -586,7 +590,17 @@ func init() {
        }
        defer os.Exit(0)
 
-       f := os.NewFile(uintptr(3), "splice-test-conn")
+       var fd uintptr
+       if runtime.GOOS == "windows" {
+               v, err := strconv.ParseUint(os.Getenv("GO_NET_TEST_TRANSFER_FD"), 10, 0)
+               if err != nil {
+                       log.Fatal(err)
+               }
+               fd = uintptr(v)
+       } else {
+               fd = uintptr(3)
+       }
+       f := os.NewFile(fd, "splice-test-conn")
        defer f.Close()
 
        conn, err := FileConn(f)
index 917bef4d54861c1ee37b9c6f1a724abc6634d4c1..72f5772155098290391aa58cf03e084a3af72fbb 100644 (file)
@@ -308,6 +308,9 @@ func (c *conn) SetWriteBuffer(bytes int) error {
 // The returned os.File's file descriptor is different from the connection's.
 // Attempting to change properties of the original using this duplicate
 // may or may not have the desired effect.
+//
+// On Windows, the returned os.File's file descriptor is not usable
+// on other processes.
 func (c *conn) File() (f *os.File, err error) {
        f, err = c.fd.dup()
        if err != nil {
index 92966b705be4a84563c97381f0519f3900167739..1b11a03f65ca4ffb7c759eb1c72d68a7400acdc5 100644 (file)
@@ -417,6 +417,9 @@ func (l *TCPListener) SetDeadline(t time.Time) error {
 // The returned os.File's file descriptor is different from the
 // connection's. Attempting to change properties of the original
 // using this duplicate may or may not have the desired effect.
+//
+// On Windows, the returned os.File's file descriptor is not
+// usable on other processes.
 func (l *TCPListener) File() (f *os.File, err error) {
        if !l.ok() {
                return nil, syscall.EINVAL
index 13d499b2085dd3ed312bfa2782debd5ffdb87a78..c93ef91d5730e6a674b2c7a9a21f1066cee8055d 100644 (file)
@@ -297,6 +297,9 @@ func (l *UnixListener) SetDeadline(t time.Time) error {
 // The returned [os.File]'s file descriptor is different from the
 // connection's. Attempting to change properties of the original
 // using this duplicate may or may not have the desired effect.
+//
+// On Windows, the returned os.File's file descriptor is not
+// usable on other processes.
 func (l *UnixListener) File() (f *os.File, err error) {
        if !l.ok() {
                return nil, syscall.EINVAL
index 1bbe53db10db2fbc10e9587d9812e07fab704704..6758afddcaae0cdc40f9205df0a22c380f5b2158 100644 (file)
@@ -398,9 +398,6 @@ func TestUnixUnlink(t *testing.T) {
 
        // FileListener should not.
        t.Run("FileListener", func(t *testing.T) {
-               if runtime.GOOS == "windows" {
-                       t.Skip("skipping: FileListener not implemented on windows")
-               }
                l := listen(t)
                f, _ := l.File()
                l1, _ := FileListener(f)
@@ -448,9 +445,6 @@ func TestUnixUnlink(t *testing.T) {
        })
 
        t.Run("FileListener/SetUnlinkOnClose(true)", func(t *testing.T) {
-               if runtime.GOOS == "windows" {
-                       t.Skip("skipping: FileListener not implemented on windows")
-               }
                l := listen(t)
                f, _ := l.File()
                l1, _ := FileListener(f)
@@ -464,9 +458,6 @@ func TestUnixUnlink(t *testing.T) {
        })
 
        t.Run("FileListener/SetUnlinkOnClose(false)", func(t *testing.T) {
-               if runtime.GOOS == "windows" {
-                       t.Skip("skipping: FileListener not implemented on windows")
-               }
                l := listen(t)
                f, _ := l.File()
                l1, _ := FileListener(f)
index c97307371c88ad1c59d3cb32c26d339bffd9fa6a..d1d3124eed2d470adb53781f23e41584e36925f6 100644 (file)
@@ -82,14 +82,49 @@ func newConsoleFile(h syscall.Handle, name string) *File {
        return newFile(h, name, "console", false)
 }
 
+var wsaLoaded atomic.Bool
+
+// isWSALoaded returns true if the ws2_32.dll module is loaded.
+func isWSALoaded() bool {
+       // ws2_32.dll may be delay loaded, we can only short-circuit
+       // if we know it is loaded.
+       if wsaLoaded.Load() {
+               return true
+       }
+       var ws2_32_dll = [...]uint16{'w', 's', '2', '_', '3', '2', '.', 'd', 'l', 'l', 0}
+       _, err := windows.GetModuleHandle(unsafe.SliceData(ws2_32_dll[:]))
+       wsaLoaded.Store(err == nil)
+       return err == nil
+}
+
 // newFileFromNewFile is called by [NewFile].
 func newFileFromNewFile(fd uintptr, name string) *File {
        h := syscall.Handle(fd)
        if h == syscall.InvalidHandle {
                return nil
        }
+       kind := "file"
+       var sotype int
+       if t, err := syscall.GetFileType(h); err == nil && t == syscall.FILE_TYPE_PIPE {
+               kind = "pipe"
+               // Windows reports sockets as FILE_TYPE_PIPE.
+               // We need to call getsockopt and check the socket type to distinguish between sockets and pipes.
+               // If the call fails, we assume it's a pipe.
+               // Avoid calling getsockopt if the WSA module is not loaded, it is a heavy dependency
+               // and sockets can only be created using that module.
+               if isWSALoaded() {
+                       if sotype, err = syscall.GetsockoptInt(h, syscall.SOL_SOCKET, windows.SO_TYPE); err == nil {
+                               kind = "net"
+                       }
+               }
+       }
        nonBlocking, _ := windows.IsNonblock(syscall.Handle(fd))
-       return newFile(h, name, "file", nonBlocking)
+       f := newFile(h, name, kind, nonBlocking)
+       if kind == "net" {
+               f.pfd.IsStream = sotype == syscall.SOCK_STREAM
+               f.pfd.ZeroReadIsEOF = sotype != syscall.SOCK_DGRAM && sotype != syscall.SOCK_RAW
+       }
+       return f
 }
 
 func epipecheck(file *File, e error) {