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

main
3.1 KiB158 linesraw
1package uv
2
3import (
4	"bufio"
5	"io"
6	"sync"
7	"time"
8)
9
10// newFallbackReader creates a new fallbackReader for the given io.Reader.
11func newFallbackReader(reader io.Reader) (pollReader, error) {
12	return &fallbackReader{
13		reader:     bufio.NewReader(reader),
14		cancelChan: make(chan struct{}),
15		dataChan:   make(chan struct{}, 1),
16	}, nil
17}
18
19// fallbackReader implements pollReader using goroutines and buffered I/O.
20// This is a fallback implementation that works on all platforms.
21type fallbackReader struct {
22	reader     *bufio.Reader
23	cancelChan chan struct{}
24	dataChan   chan struct{}
25	mu         sync.Mutex
26	canceled   bool
27	started    bool
28}
29
30// Read reads data from the underlying reader.
31func (r *fallbackReader) Read(p []byte) (int, error) {
32	r.mu.Lock()
33	if r.canceled {
34		r.mu.Unlock()
35		return 0, ErrCanceled
36	}
37	r.mu.Unlock()
38
39	n, err := r.reader.Read(p)
40	// If we get an error during a concurrent cancel, prefer ErrCanceled
41	if err != nil {
42		r.mu.Lock()
43		if r.canceled {
44			r.mu.Unlock()
45			return 0, ErrCanceled
46		}
47		r.mu.Unlock()
48	}
49	return n, err
50}
51
52// Poll waits for data to be available to read with the given timeout.
53// This implementation starts a background goroutine to check for buffered
54// data availability.
55func (r *fallbackReader) Poll(timeout time.Duration) (bool, error) {
56	r.mu.Lock()
57	if r.canceled {
58		r.mu.Unlock()
59		return false, ErrCanceled
60	}
61
62	// Start the background reader goroutine if not already started
63	if !r.started {
64		r.started = true
65		go r.checkBuffered()
66	}
67	r.mu.Unlock()
68
69	if timeout < 0 {
70		// Wait indefinitely
71		select {
72		case <-r.dataChan:
73			// Put it back for next poll/read
74			select {
75			case r.dataChan <- struct{}{}:
76			default:
77			}
78			return true, nil
79		case <-r.cancelChan:
80			return false, ErrCanceled
81		}
82	}
83
84	// Wait with timeout
85	timer := time.NewTimer(timeout)
86	defer timer.Stop()
87
88	select {
89	case <-r.dataChan:
90		// Put it back for next poll/read
91		select {
92		case r.dataChan <- struct{}{}:
93		default:
94		}
95		return true, nil
96	case <-timer.C:
97		return false, nil
98	case <-r.cancelChan:
99		return false, ErrCanceled
100	}
101}
102
103// checkBuffered runs in a background goroutine to signal when data is available.
104func (r *fallbackReader) checkBuffered() {
105	for {
106		select {
107		case <-r.cancelChan:
108			return
109		default:
110		}
111
112		// Check if data is buffered
113		r.mu.Lock()
114		if r.canceled {
115			r.mu.Unlock()
116			return
117		}
118		r.mu.Unlock()
119
120		// Peek at one byte to check if data is available
121		// This will block until data arrives
122		_, err := r.reader.Peek(1)
123		if err != nil {
124			// If error (including EOF), stop the goroutine
125			return
126		}
127
128		// Signal that data is available
129		select {
130		case r.dataChan <- struct{}{}:
131		case <-r.cancelChan:
132			return
133		}
134
135		// Wait a bit before checking again to avoid busy loop
136		time.Sleep(10 * time.Millisecond)
137	}
138}
139
140// Cancel cancels any ongoing poll or read operations.
141func (r *fallbackReader) Cancel() bool {
142	r.mu.Lock()
143	if r.canceled {
144		r.mu.Unlock()
145		return false
146	}
147	r.canceled = true
148	r.mu.Unlock()
149
150	close(r.cancelChan)
151	return true
152}
153
154// Close closes the reader and releases any resources.
155func (r *fallbackReader) Close() error {
156	r.Cancel()
157	return nil
158}