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.5 KiB165 linesraw
1//go:build darwin || freebsd || netbsd || openbsd || dragonfly
2// +build 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// newPollReader creates a new pollReader for the given io.Reader.
19func newPollReader(reader io.Reader) (pollReader, error) {
20	file, ok := reader.(File)
21	if !ok {
22		return newFallbackReader(reader)
23	}
24
25	// kqueue returns instantly when polling /dev/tty so fallback to select
26	if file.Name() == "/dev/tty" {
27		return newSelectPollReader(reader)
28	}
29
30	kQueue, err := unix.Kqueue()
31	if err != nil {
32		return nil, fmt.Errorf("create kqueue: %w", err)
33	}
34
35	r := &kqueueReader{
36		reader: reader,
37		file:   file,
38		kQueue: kQueue,
39	}
40
41	r.cancelSignalReader, r.cancelSignalWriter, err = os.Pipe()
42	if err != nil {
43		_ = unix.Close(kQueue)
44		return nil, err
45	}
46
47	unix.SetKevent(&r.kQueueEvents[0], int(file.Fd()), unix.EVFILT_READ, unix.EV_ADD)
48	unix.SetKevent(&r.kQueueEvents[1], int(r.cancelSignalReader.Fd()), unix.EVFILT_READ, unix.EV_ADD)
49
50	return r, nil
51}
52
53// kqueueReader implements pollReader using the BSD kqueue API.
54type kqueueReader struct {
55	reader             io.Reader
56	file               File
57	cancelSignalReader *os.File
58	cancelSignalWriter *os.File
59	kQueue             int
60	kQueueEvents       [2]unix.Kevent_t
61	mu                 sync.Mutex
62	canceled           bool
63}
64
65// Read reads data from the underlying reader.
66func (r *kqueueReader) Read(p []byte) (int, error) {
67	r.mu.Lock()
68	if r.canceled {
69		r.mu.Unlock()
70		return 0, ErrCanceled
71	}
72	r.mu.Unlock()
73
74	return r.reader.Read(p)
75}
76
77// Poll waits for data to be available to read with the given timeout.
78func (r *kqueueReader) Poll(timeout time.Duration) (bool, error) {
79	r.mu.Lock()
80	if r.canceled {
81		r.mu.Unlock()
82		return false, ErrCanceled
83	}
84	r.mu.Unlock()
85
86	events := make([]unix.Kevent_t, 1)
87
88	var ts *unix.Timespec
89	if timeout >= 0 {
90		t := unix.NsecToTimespec(timeout.Nanoseconds())
91		ts = &t
92	}
93
94	for {
95		n, err := unix.Kevent(r.kQueue, r.kQueueEvents[:], events, ts)
96		if errors.Is(err, unix.EINTR) {
97			continue // try again if the syscall was interrupted
98		}
99
100		if err != nil {
101			return false, fmt.Errorf("kevent: %w", err)
102		}
103
104		if n == 0 {
105			return false, nil // timeout
106		}
107
108		break
109	}
110
111	ident := uint64(events[0].Ident)
112	switch ident {
113	case uint64(r.file.Fd()):
114		return true, nil
115	case uint64(r.cancelSignalReader.Fd()):
116		// remove signal from pipe
117		var b [1]byte
118		_, errRead := r.cancelSignalReader.Read(b[:])
119		if errRead != nil {
120			return false, fmt.Errorf("reading cancel signal: %w", errRead)
121		}
122		return false, ErrCanceled
123	}
124
125	return false, fmt.Errorf("unknown error")
126}
127
128// Cancel cancels any ongoing poll or read operations.
129func (r *kqueueReader) Cancel() bool {
130	r.mu.Lock()
131	r.canceled = true
132	r.mu.Unlock()
133
134	// send cancel signal
135	_, err := r.cancelSignalWriter.Write([]byte{'c'})
136	return err == nil
137}
138
139// Close closes the reader and releases any resources.
140func (r *kqueueReader) Close() error {
141	var errMsgs []string
142
143	// close kqueue
144	err := unix.Close(r.kQueue)
145	if err != nil {
146		errMsgs = append(errMsgs, fmt.Sprintf("closing kqueue: %v", err))
147	}
148
149	// close pipe
150	err = r.cancelSignalWriter.Close()
151	if err != nil {
152		errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", err))
153	}
154
155	err = r.cancelSignalReader.Close()
156	if err != nil {
157		errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal reader: %v", err))
158	}
159
160	if len(errMsgs) > 0 {
161		return fmt.Errorf("%s", strings.Join(errMsgs, ", "))
162	}
163
164	return nil
165}