okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Ayman Bagabasfeat: add new poll reader for cross-platform I/O polling (#71)04a6149

upstream
3.2 KiB156 linesraw
1//go:build solaris || darwin || freebsd || netbsd || openbsd || dragonfly
2// +build solaris darwin freebsd netbsd openbsd dragonfly
3
4package uv
5
6import (
7	"errors"
8	"fmt"
9	"io"
10	"os"
11	"strings"
12	"sync"
13	"time"
14
15	"golang.org/x/sys/unix"
16)
17
18// newSelectPollReader creates a new SelectReader for the given io.Reader.
19func newSelectPollReader(reader io.Reader) (pollReader, error) {
20	file, ok := reader.(File)
21	if !ok || file.Fd() >= unix.FD_SETSIZE {
22		return newFallbackReader(reader)
23	}
24
25	r := &selectReader{
26		reader: reader,
27		file:   file,
28	}
29
30	var err error
31	r.cancelSignalReader, r.cancelSignalWriter, err = os.Pipe()
32	if err != nil {
33		return nil, err
34	}
35
36	return r, nil
37}
38
39// selectReader implements pollReader using the POSIX select API.
40type selectReader struct {
41	reader             io.Reader
42	file               File
43	cancelSignalReader *os.File
44	cancelSignalWriter *os.File
45	mu                 sync.Mutex
46	canceled           bool
47}
48
49// Read reads data from the underlying reader.
50func (r *selectReader) Read(p []byte) (int, error) {
51	r.mu.Lock()
52	if r.canceled {
53		r.mu.Unlock()
54		return 0, ErrCanceled
55	}
56	r.mu.Unlock()
57
58	return r.reader.Read(p)
59}
60
61// Poll waits for data to be available to read with the given timeout.
62func (r *selectReader) Poll(timeout time.Duration) (bool, error) {
63	r.mu.Lock()
64	if r.canceled {
65		r.mu.Unlock()
66		return false, ErrCanceled
67	}
68	r.mu.Unlock()
69
70	for {
71		readerFd := int(r.file.Fd())
72		abortFd := int(r.cancelSignalReader.Fd())
73
74		maxFd := readerFd
75		if abortFd > maxFd {
76			maxFd = abortFd
77		}
78
79		// this is a limitation of the select syscall
80		if maxFd >= unix.FD_SETSIZE {
81			return false, fmt.Errorf("cannot select on file descriptor %d which is larger than %d", maxFd, unix.FD_SETSIZE)
82		}
83
84		fdSet := &unix.FdSet{}
85		fdSet.Set(readerFd)
86		fdSet.Set(abortFd)
87
88		var tv *unix.Timeval
89		if timeout >= 0 {
90			t := unix.NsecToTimeval(timeout.Nanoseconds())
91			tv = &t
92		}
93
94		n, err := unix.Select(maxFd+1, fdSet, nil, nil, tv)
95		if errors.Is(err, unix.EINTR) {
96			continue // try again if the syscall was interrupted
97		}
98
99		if err != nil {
100			return false, fmt.Errorf("select: %w", err)
101		}
102
103		if n == 0 {
104			return false, nil // timeout
105		}
106
107		if fdSet.IsSet(abortFd) {
108			// remove signal from pipe
109			var b [1]byte
110			_, readErr := r.cancelSignalReader.Read(b[:])
111			if readErr != nil {
112				return false, fmt.Errorf("reading cancel signal: %w", readErr)
113			}
114			return false, ErrCanceled
115		}
116
117		if fdSet.IsSet(readerFd) {
118			return true, nil
119		}
120
121		return false, fmt.Errorf("select returned without setting a file descriptor")
122	}
123}
124
125// Cancel cancels any ongoing poll or read operations.
126func (r *selectReader) Cancel() bool {
127	r.mu.Lock()
128	r.canceled = true
129	r.mu.Unlock()
130
131	// send cancel signal
132	_, err := r.cancelSignalWriter.Write([]byte{'c'})
133	return err == nil
134}
135
136// Close closes the reader and releases any resources.
137func (r *selectReader) Close() error {
138	var errMsgs []string
139
140	// close pipe
141	err := r.cancelSignalWriter.Close()
142	if err != nil {
143		errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", err))
144	}
145
146	err = r.cancelSignalReader.Close()
147	if err != nil {
148		errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal reader: %v", err))
149	}
150
151	if len(errMsgs) > 0 {
152		return fmt.Errorf("%s", strings.Join(errMsgs, ", "))
153	}
154
155	return nil
156}