diff --git a/close_native_test.go b/close_native_test.go new file mode 100644 index 0000000..0f3361c --- /dev/null +++ b/close_native_test.go @@ -0,0 +1,176 @@ +//go:build linux && !baremetal && !tinygo.wasm + +package net + +import ( + "errors" + "sync" + "syscall" + "testing" + "time" +) + +type closeCountNetdev struct { + nopNetdev + calls int +} + +func (d *closeCountNetdev) Close(int) error { + d.calls++ + return nil +} + +func TestCloseConcurrent(t *testing.T) { + previous := netdev + defer func() { netdev = previous }() + d := &closeCountNetdev{} + netdev = d + c := &TCPConn{fd: 42} + var wg sync.WaitGroup + results := make(chan error, 32) + for i := 0; i < 32; i++ { + wg.Add(1) + go func() { defer wg.Done(); results <- c.Close() }() + } + wg.Wait() + close(results) + success := 0 + for err := range results { + if err == nil { + success++ + } else if !errors.Is(err, ErrClosed) { + t.Fatal(err) + } + } + if success != 1 || d.calls != 1 { + t.Fatalf("success=%d device calls=%d", success, d.calls) + } +} + +func TestCloseDescriptorReuse(t *testing.T) { + for _, kind := range []string{"listener", "tcp", "udp"} { + t.Run(kind, func(t *testing.T) { + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) + if err != nil { + t.Fatal(err) + } + var closeSocket func() error + switch kind { + case "listener": + closeSocket = (&listener{fd: fd}).Close + case "tcp": + closeSocket = (&TCPConn{fd: fd}).Close + case "udp": + closeSocket = (&UDPConn{fd: fd}).Close + } + if err := closeSocket(); err != nil { + t.Fatal(err) + } + replacement, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) + if err != nil { + t.Fatal(err) + } + if replacement != fd { + if err := syscall.Dup3(replacement, fd, 0); err != nil { + syscall.Close(replacement) + t.Fatal(err) + } + syscall.Close(replacement) + } + defer syscall.Close(fd) + err = closeSocket() + if !errors.Is(err, ErrClosed) { + t.Errorf("second Close = %v, want ErrClosed", err) + } + if _, err := syscall.Getsockname(fd); err != nil { + t.Fatalf("second Close damaged replacement socket: %v", err) + } + }) + } +} + +func TestCloseBlockedAccept(t *testing.T) { + l, err := Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + done := make(chan error, 1) + go func() { + c, err := l.Accept() + if c != nil { + c.Close() + } + done <- err + }() + time.Sleep(25 * time.Millisecond) + if err := l.Close(); err != nil { + t.Fatal(err) + } + select { + case err := <-done: + if err == nil { + t.Fatal("Accept succeeded after close") + } + case <-time.After(time.Second): + t.Fatal("Accept did not stop after close") + } +} + +func TestCloseBlockedRead(t *testing.T) { + pair, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(pair[1]) + // Every socket this netdev creates is SOCK_NONBLOCK; a hand-made fd must + // match, or Read blocks in the kernel where the poller cannot wake it. + if err := syscall.SetNonblock(pair[0], true); err != nil { + t.Fatal(err) + } + c := &TCPConn{fd: pair[0], net: "tcp"} + done := make(chan error, 1) + go func() { _, err := c.Read(make([]byte, 1)); done <- err }() + time.Sleep(25 * time.Millisecond) + if err := c.Close(); err != nil { + t.Fatal(err) + } + select { + case err := <-done: + if err == nil { + t.Fatal("Read succeeded after close") + } + case <-time.After(time.Second): + t.Fatal("Read did not stop after close") + } +} + +func TestCloseBlockedWrite(t *testing.T) { + pair, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(pair[1]) + if err := syscall.SetsockoptInt(pair[0], syscall.SOL_SOCKET, syscall.SO_SNDBUF, 4096); err != nil { + t.Fatal(err) + } + // See TestCloseBlockedRead: the fd must be non-blocking to park on the + // poller rather than in the kernel. + if err := syscall.SetNonblock(pair[0], true); err != nil { + t.Fatal(err) + } + c := &TCPConn{fd: pair[0], net: "tcp"} + done := make(chan error, 1) + go func() { _, err := c.Write(make([]byte, 1<<20)); done <- err }() + time.Sleep(25 * time.Millisecond) + if err := c.Close(); err != nil { + t.Fatal(err) + } + select { + case err := <-done: + if err == nil { + t.Fatal("Write succeeded after close") + } + case <-time.After(time.Second): + t.Fatal("Write did not stop after close") + } +} diff --git a/net.go b/net.go index 8da88b0..44a594a 100644 --- a/net.go +++ b/net.go @@ -9,9 +9,25 @@ package net import ( "errors" "io" + "sync" "time" ) +type closeGuard struct { + mu sync.Mutex + closed bool +} + +func (c *closeGuard) close(fd int) error { + c.mu.Lock() + defer c.mu.Unlock() + if c.closed { + return ErrClosed + } + c.closed = true + return netdev.Close(fd) +} + // Addr represents a network end point address. // // The two methods [Addr.Network] and [Addr.String] conventionally return strings diff --git a/netdev.go b/netdev.go index 8822262..6583d34 100644 --- a/netdev.go +++ b/netdev.go @@ -33,6 +33,21 @@ const ( var netdev netdever = &nopNetdev{} // (useNetdev is go:linkname'd from tinygo/drivers package) +// errPollInterrupted is returned from a netdev's Recv/Send when a concurrent +// deadline change interrupted a blocked operation. The net package retries the +// operation with the fresh deadline; the error never escapes to callers. +var errPollInterrupted = errors.New("net: I/O interrupted by deadline change") + +// pollInterrupt asks a netdev that supports it to wake goroutines blocked in +// Recv (write=false) or Send (write=true) on sockfd so they re-evaluate a +// just-changed deadline. Netdevs without that ability ignore deadline changes +// on in-flight I/O, as before. +func pollInterrupt(sockfd int, write bool) { + if p, ok := netdev.(interface{ PollInterrupt(sockfd int, write bool) }); ok { + p.PollInterrupt(sockfd, write) + } +} + func useNetdev(dev netdever) { netdev = dev } diff --git a/netdev_native.go b/netdev_native.go index e522a08..e20564b 100644 --- a/netdev_native.go +++ b/netdev_native.go @@ -90,7 +90,9 @@ func (*hostNetdev) Socket(domain, stype, protocol int) (int, error) { protocol = syscall.IPPROTO_TCP } - fd, err := syscall.Socket(domain, stype, protocol) + // Non-blocking so the poller (netpoll_native.go) can park goroutines on + // EAGAIN instead of pinning a thread in a blocking syscall. + fd, err := syscall.Socket(domain, stype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, protocol) if err != nil { return -1, err } @@ -121,30 +123,70 @@ func (n *hostNetdev) Connect(sockfd int, host string, ip netip.AddrPort) error { sa := sockaddrFromParts(addr, ip.Port()) for { err := syscall.Connect(sockfd, sa) - if err == syscall.EINTR { + switch err { + case nil: + return nil + case syscall.EINTR: continue + case syscall.EINPROGRESS, syscall.EALREADY, syscall.EAGAIN: + // Non-blocking connect in progress: wait for the socket to become + // writable, then read the pending error via SO_ERROR. + if werr := poller.wait(sockfd, true, time.Time{}); werr != nil { + return werr + } + soErr, gerr := syscall.GetsockoptInt(sockfd, syscall.SOL_SOCKET, syscall.SO_ERROR) + if gerr != nil { + return gerr + } + if soErr != 0 { + return syscall.Errno(soErr) + } + return nil + default: + return err } - return err } } +// PollInterrupt wakes any goroutine parked in Recv/Send on sockfd so it +// re-evaluates its deadline. The net package calls this (through an optional +// interface) when a deadline is changed on a connection with I/O in flight. +func (*hostNetdev) PollInterrupt(sockfd int, write bool) { + poller.interrupt(sockfd, write) +} + func (*hostNetdev) Listen(sockfd int, backlog int) error { return syscall.Listen(sockfd, backlog) } func (*hostNetdev) Accept(sockfd int) (int, netip.AddrPort, error) { - nfd, sa, err := syscall.Accept(sockfd) - if err != nil { - return -1, netip.AddrPort{}, err - } - var raddr netip.AddrPort - switch s := sa.(type) { - case *syscall.SockaddrInet4: - raddr = netip.AddrPortFrom(netip.AddrFrom4(s.Addr), uint16(s.Port)) - case *syscall.SockaddrInet6: - raddr = netip.AddrPortFrom(netip.AddrFrom16(s.Addr), uint16(s.Port)) + for { + // Accept4 with SOCK_NONBLOCK|SOCK_CLOEXEC keeps accepted sockets + // non-blocking too, so their reads/writes also go through the poller. + nfd, sa, err := syscall.Accept4(sockfd, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC) + switch err { + case nil: + case syscall.EINTR: + continue + case syscall.EAGAIN: // == EWOULDBLOCK on Linux + // No pending connection: park until the listener is readable or the + // listening fd is closed (which unblocks accept for shutdown). + if werr := poller.wait(sockfd, false, time.Time{}); werr != nil { + return -1, netip.AddrPort{}, werr + } + continue + default: + return -1, netip.AddrPort{}, err + } + var raddr netip.AddrPort + switch s := sa.(type) { + case *syscall.SockaddrInet4: + raddr = netip.AddrPortFrom(netip.AddrFrom4(s.Addr), uint16(s.Port)) + case *syscall.SockaddrInet6: + raddr = netip.AddrPortFrom(netip.AddrFrom16(s.Addr), uint16(s.Port)) + } + return nfd, raddr, nil } - return nfd, raddr, nil } func (*hostNetdev) Send(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { @@ -156,16 +198,18 @@ func (*hostNetdev) Send(sockfd int, buf []byte, flags int, deadline time.Time) ( if expired(deadline) { return total, timeoutError{} } - if err := setSockTimeout(sockfd, syscall.SO_SNDTIMEO, deadline); err != nil { - return total, err - } n, err := syscall.Write(sockfd, buf[total:]) if err != nil { if err == syscall.EINTR { continue } if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { - return total, timeoutError{} + // Send buffer full: park until writable, the deadline expires, + // or the fd is closed. + if werr := poller.wait(sockfd, true, deadline); werr != nil { + return total, werr + } + continue } return total, err } @@ -181,9 +225,6 @@ func (*hostNetdev) Recv(sockfd int, buf []byte, flags int, deadline time.Time) ( if expired(deadline) { return 0, timeoutError{} } - if err := setSockTimeout(sockfd, syscall.SO_RCVTIMEO, deadline); err != nil { - return 0, err - } for { n, err := syscall.Read(sockfd, buf) @@ -192,7 +233,12 @@ func (*hostNetdev) Recv(sockfd int, buf []byte, flags int, deadline time.Time) ( continue } if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { - return 0, timeoutError{} + // Nothing to read yet: park until readable, the deadline + // expires, or the fd is closed. + if werr := poller.wait(sockfd, false, deadline); werr != nil { + return 0, werr + } + continue } return n, err } @@ -208,6 +254,10 @@ func (*hostNetdev) Recv(sockfd int, buf []byte, flags int, deadline time.Time) ( } func (*hostNetdev) Close(sockfd int) error { + // Wake any goroutines parked on this fd (with errPollClosed) before closing + // it, so a blocked Accept/Recv/Send returns promptly on shutdown instead of + // hanging — which is what lets graceful shutdown and Ctrl+C complete. + poller.close(sockfd) return syscall.Close(sockfd) } diff --git a/netpoll_native.go b/netpoll_native.go new file mode 100644 index 0000000..89765f1 --- /dev/null +++ b/netpoll_native.go @@ -0,0 +1,296 @@ +//go:build linux && !baremetal && !nintendoswitch && !wasm_unknown && !tinygo.wasm + +// TINYGO: A small epoll-based network poller for the host (native) netdev. +// +// The netdev sockets are non-blocking. When a syscall would block (EAGAIN), the +// calling goroutine registers its interest with this poller and blocks on a +// channel until the fd is ready, its deadline passes, or the fd is closed. A +// single background goroutine runs epoll_wait and wakes the parked goroutines. +// +// This replaces the previous blocking-syscall + SO_*TIMEO approach so that: +// - a blocked read/accept parks the goroutine instead of pinning a thread; +// - closing an fd unblocks every goroutine parked on it (real cancellation), +// which is what lets a graceful shutdown — and Ctrl+C — actually complete. + +package net + +import ( + "errors" + "sync" + "syscall" + "time" +) + +// errPollClosed is returned to goroutines parked on an fd when that fd is closed. +var errPollClosed = errors.New("net: use of closed network connection") + +// pollDesc holds the goroutines currently waiting on one fd, split by direction. +type pollDesc struct { + fd int + readers []chan error + writers []chan error + inEpoll bool + + // readInterrupt/writeInterrupt record an interrupt() that arrived while no + // waiter was parked in that direction, so the next wait() in that direction + // returns immediately instead of parking with a deadline that may already + // be stale. This closes the race between a deadline change and a goroutine + // that captured the old deadline but has not parked yet. + readInterrupt bool + writeInterrupt bool +} + +type netPoller struct { + once sync.Once + err error // set if the poller failed to initialize + epfd int + + mu sync.Mutex + fds map[int]*pollDesc +} + +var poller netPoller + +func (p *netPoller) init() { + p.once.Do(func() { + epfd, err := syscall.EpollCreate1(syscall.EPOLL_CLOEXEC) + if err != nil { + p.err = err + return + } + p.epfd = epfd + p.fds = make(map[int]*pollDesc) + go p.loop() + }) +} + +// events returns the epoll interest mask for pd given its current waiters. +func (pd *pollDesc) events() uint32 { + var ev uint32 + if len(pd.readers) != 0 { + ev |= syscall.EPOLLIN | syscall.EPOLLRDHUP + } + if len(pd.writers) != 0 { + ev |= syscall.EPOLLOUT + } + if ev != 0 { + ev |= syscall.EPOLLONESHOT + } + return ev +} + +// arm (re)programs epoll for pd. Must be called with p.mu held. +func (p *netPoller) arm(pd *pollDesc) { + ev := pd.events() + if ev == 0 { + if pd.inEpoll { + syscall.EpollCtl(p.epfd, syscall.EPOLL_CTL_DEL, pd.fd, nil) + pd.inEpoll = false + } + if !pd.readInterrupt && !pd.writeInterrupt { + delete(p.fds, pd.fd) + } + return + } + event := &syscall.EpollEvent{Events: ev, Fd: int32(pd.fd)} + if pd.inEpoll { + syscall.EpollCtl(p.epfd, syscall.EPOLL_CTL_MOD, pd.fd, event) + } else { + if err := syscall.EpollCtl(p.epfd, syscall.EPOLL_CTL_ADD, pd.fd, event); err == nil { + pd.inEpoll = true + } + } +} + +// wait blocks until fd is ready in the requested direction (write=EPOLLOUT, +// otherwise EPOLLIN), the deadline expires, or the fd is closed. +func (p *netPoller) wait(fd int, write bool, deadline time.Time) error { + p.init() + if p.err != nil { + return p.err + } + + ch := make(chan error, 1) + + p.mu.Lock() + pd := p.fds[fd] + if pd == nil { + pd = &pollDesc{fd: fd} + p.fds[fd] = pd + } + if (write && pd.writeInterrupt) || (!write && pd.readInterrupt) { + // An interrupt arrived before we parked; consume it and let the caller + // re-evaluate its deadline. + if write { + pd.writeInterrupt = false + } else { + pd.readInterrupt = false + } + p.arm(pd) + p.mu.Unlock() + return errPollInterrupted + } + if write { + pd.writers = append(pd.writers, ch) + } else { + pd.readers = append(pd.readers, ch) + } + p.arm(pd) + p.mu.Unlock() + + var timeout <-chan time.Time + if !deadline.IsZero() { + d := time.Until(deadline) + if d <= 0 { + p.cancelWaiter(fd, write, ch) + return timeoutError{} + } + t := time.NewTimer(d) + defer t.Stop() + timeout = t.C + } + + select { + case err := <-ch: + return err + case <-timeout: + p.cancelWaiter(fd, write, ch) + return timeoutError{} + } +} + +// cancelWaiter removes a single waiter channel that gave up (deadline expired) +// before the poller signalled it. +func (p *netPoller) cancelWaiter(fd int, write bool, ch chan error) { + p.mu.Lock() + defer p.mu.Unlock() + pd := p.fds[fd] + if pd == nil { + return + } + if write { + pd.writers = removeChan(pd.writers, ch) + } else { + pd.readers = removeChan(pd.readers, ch) + } + p.arm(pd) +} + +// interrupt wakes every goroutine parked on fd in the given direction with +// errPollInterrupted so it re-evaluates its deadline (a deadline change on a +// connection must take effect on I/O that is already blocked — net/http's +// abortPendingRead relies on this). If no waiter is parked yet, the interrupt +// is remembered and consumed by the next wait() in that direction. +func (p *netPoller) interrupt(fd int, write bool) { + if p.err != nil { + return + } + p.mu.Lock() + if p.fds == nil { + // Poller never started: nothing can be parked, and any future wait() + // will capture the new deadline anyway. + p.mu.Unlock() + return + } + pd := p.fds[fd] + if pd == nil { + pd = &pollDesc{fd: fd} + p.fds[fd] = pd + } + var chs []chan error + if write { + chs, pd.writers = pd.writers, nil + if len(chs) == 0 { + pd.writeInterrupt = true + } + } else { + chs, pd.readers = pd.readers, nil + if len(chs) == 0 { + pd.readInterrupt = true + } + } + p.arm(pd) + p.mu.Unlock() + + for _, ch := range chs { + ch <- errPollInterrupted + } +} + +// close wakes every goroutine parked on fd with errPollClosed and stops polling +// it. It must be called just before the fd is actually closed. +func (p *netPoller) close(fd int) { + if p.err != nil { + return + } + p.mu.Lock() + pd := p.fds[fd] + if pd == nil { + p.mu.Unlock() + return + } + if pd.inEpoll { + syscall.EpollCtl(p.epfd, syscall.EPOLL_CTL_DEL, fd, nil) + } + delete(p.fds, fd) + readers, writers := pd.readers, pd.writers + pd.readers, pd.writers = nil, nil + p.mu.Unlock() + + for _, ch := range readers { + ch <- errPollClosed + } + for _, ch := range writers { + ch <- errPollClosed + } +} + +// loop is the poller's background goroutine: it waits for epoll events and wakes +// the corresponding parked goroutines. +func (p *netPoller) loop() { + events := make([]syscall.EpollEvent, 64) + for { + n, err := syscall.EpollWait(p.epfd, events, -1) + if err != nil { + if err == syscall.EINTR { + continue + } + return + } + p.mu.Lock() + for i := 0; i < n; i++ { + e := events[i] + pd := p.fds[int(e.Fd)] + if pd == nil { + continue + } + // On error/hangup, wake everyone so they observe the real result. + hup := e.Events&(syscall.EPOLLERR|syscall.EPOLLHUP|syscall.EPOLLRDHUP) != 0 + if hup || e.Events&syscall.EPOLLIN != 0 { + for _, ch := range pd.readers { + ch <- nil + } + pd.readers = nil + } + if hup || e.Events&syscall.EPOLLOUT != 0 { + for _, ch := range pd.writers { + ch <- nil + } + pd.writers = nil + } + // EPOLLONESHOT disabled the fd; re-arm for any remaining waiters. + pd.inEpoll = true // it is still registered, just disarmed + p.arm(pd) + } + p.mu.Unlock() + } +} + +func removeChan(s []chan error, ch chan error) []chan error { + for i, c := range s { + if c == ch { + return append(s[:i], s[i+1:]...) + } + } + return s +} diff --git a/tcpsock.go b/tcpsock.go index 4e3c864..d23b850 100644 --- a/tcpsock.go +++ b/tcpsock.go @@ -143,6 +143,7 @@ func TCPAddrFromAddrPort(addr netip.AddrPort) *TCPAddr { // TCPConn is an implementation of the [Conn] interface for TCP network // connections. type TCPConn struct { + closer closeGuard fd int net string laddr *TCPAddr @@ -208,6 +209,11 @@ func (c *TCPConn) SyscallConn() (syscall.RawConn, error) { func (c *TCPConn) Read(b []byte) (int, error) { n, err := netdev.Recv(c.fd, b, 0, c.readDeadline) + for err == errPollInterrupted { + // A concurrent deadline change interrupted the wait; retry with the + // fresh deadline. Nothing was read when the wait was interrupted. + n, err = netdev.Recv(c.fd, b, 0, c.readDeadline) + } // Turn the -1 socket error into 0 and let err speak for error if n < 0 { n = 0 @@ -219,19 +225,26 @@ func (c *TCPConn) Read(b []byte) (int, error) { } func (c *TCPConn) Write(b []byte) (int, error) { - n, err := netdev.Send(c.fd, b, 0, c.writeDeadline) - // Turn the -1 socket error into 0 and let err speak for error - if n < 0 { - n = 0 + total := 0 + for { + n, err := netdev.Send(c.fd, b[total:], 0, c.writeDeadline) + if n > 0 { + total += n + } + if err == errPollInterrupted { + // A concurrent deadline change interrupted the wait; keep sending + // the remainder under the fresh deadline. + continue + } + if err != nil { + err = &OpError{Op: "write", Net: c.net, Source: c.laddr, Addr: c.raddr, Err: err} + } + return total, err } - if err != nil { - err = &OpError{Op: "write", Net: c.net, Source: c.laddr, Addr: c.raddr, Err: err} - } - return n, err } func (c *TCPConn) Close() error { - return netdev.Close(c.fd) + return c.closer.close(c.fd) } func (c *TCPConn) LocalAddr() Addr { @@ -245,6 +258,8 @@ func (c *TCPConn) RemoteAddr() Addr { func (c *TCPConn) SetDeadline(t time.Time) error { c.readDeadline = t c.writeDeadline = t + pollInterrupt(c.fd, false) + pollInterrupt(c.fd, true) return nil } @@ -284,11 +299,13 @@ func (c *TCPConn) SetKeepAlivePeriod(d time.Duration) error { func (c *TCPConn) SetReadDeadline(t time.Time) error { c.readDeadline = t + pollInterrupt(c.fd, false) return nil } func (c *TCPConn) SetWriteDeadline(t time.Time) error { c.writeDeadline = t + pollInterrupt(c.fd, true) return nil } @@ -352,8 +369,9 @@ type onlyWriter struct { } type listener struct { - fd int - laddr *TCPAddr + closer closeGuard + fd int + laddr *TCPAddr } func (l *listener) Accept() (Conn, error) { @@ -371,7 +389,7 @@ func (l *listener) Accept() (Conn, error) { } func (l *listener) Close() error { - return netdev.Close(l.fd) + return l.closer.close(l.fd) } func (l *listener) Addr() Addr { diff --git a/udpsock.go b/udpsock.go index bf69aee..bcf1af0 100644 --- a/udpsock.go +++ b/udpsock.go @@ -127,6 +127,7 @@ func UDPAddrFromAddrPort(addr netip.AddrPort) *UDPAddr { // UDPConn is the implementation of the Conn and PacketConn interfaces // for UDP network connections. type UDPConn struct { + closer closeGuard fd int net string laddr *UDPAddr @@ -269,6 +270,9 @@ func (c *UDPConn) SyscallConn() (syscall.RawConn, error) { func (c *UDPConn) Read(b []byte) (int, error) { n, err := netdev.Recv(c.fd, b, 0, c.readDeadline) + for err == errPollInterrupted { + n, err = netdev.Recv(c.fd, b, 0, c.readDeadline) + } // Turn the -1 socket error into 0 and let err speak for error if n < 0 { n = 0 @@ -281,6 +285,9 @@ func (c *UDPConn) Read(b []byte) (int, error) { func (c *UDPConn) Write(b []byte) (int, error) { n, err := netdev.Send(c.fd, b, 0, c.writeDeadline) + for err == errPollInterrupted { + n, err = netdev.Send(c.fd, b, 0, c.writeDeadline) + } // Turn the -1 socket error into 0 and let err speak for error if n < 0 { n = 0 @@ -297,6 +304,9 @@ func (c *UDPConn) Write(b []byte) (int, error) { // remote address (c.raddr) is returned as the source. func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error) { n, err := netdev.Recv(c.fd, b, 0, c.readDeadline) + for err == errPollInterrupted { + n, err = netdev.Recv(c.fd, b, 0, c.readDeadline) + } if n < 0 { n = 0 } @@ -321,6 +331,9 @@ func (c *UDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err // connected remote regardless of addr, mirroring netdev.Send. func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error) { n, err := netdev.Send(c.fd, b, 0, c.writeDeadline) + for err == errPollInterrupted { + n, err = netdev.Send(c.fd, b, 0, c.writeDeadline) + } if n < 0 { n = 0 } @@ -386,7 +399,7 @@ func (c *UDPConn) WriteMsgUDP(b, oob []byte, addr *UDPAddr) (n, oobn int, err er } func (c *UDPConn) Close() error { - return netdev.Close(c.fd) + return c.closer.close(c.fd) } func (c *UDPConn) LocalAddr() Addr { @@ -400,15 +413,19 @@ func (c *UDPConn) RemoteAddr() Addr { func (c *UDPConn) SetDeadline(t time.Time) error { c.readDeadline = t c.writeDeadline = t + pollInterrupt(c.fd, false) + pollInterrupt(c.fd, true) return nil } func (c *UDPConn) SetReadDeadline(t time.Time) error { c.readDeadline = t + pollInterrupt(c.fd, false) return nil } func (c *UDPConn) SetWriteDeadline(t time.Time) error { c.writeDeadline = t + pollInterrupt(c.fd, true) return nil }