Files
fenrir/internal/console/readiness_linux.go

119 lines
3.1 KiB
Go

// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
//go:build linux && cgo
package console
import (
"errors"
"sync"
"syscall"
)
// readinessWatcher reports readiness without reading the observed descriptors.
// The application loop owns device reads, acknowledgement and descriptor lifetime.
type readinessWatcher struct {
fd int
mask uint32
wake [2]int
events chan error
stop chan struct{}
workers sync.WaitGroup
}
func newReadinessWatcher(mask uint32) (*readinessWatcher, error) {
fd, err := syscall.EpollCreate1(syscall.EPOLL_CLOEXEC)
if err != nil {
return nil, err
}
w := &readinessWatcher{fd: fd, mask: mask, events: make(chan error, 1), stop: make(chan struct{})}
if err := syscall.Pipe2(w.wake[:], syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil {
syscall.Close(fd)
return nil, err
}
event := syscall.EpollEvent{Fd: int32(w.wake[0]), Events: syscall.EPOLLIN}
if err := syscall.EpollCtl(fd, syscall.EPOLL_CTL_ADD, w.wake[0], &event); err != nil {
syscall.Close(w.wake[0])
syscall.Close(w.wake[1])
syscall.Close(fd)
return nil, err
}
w.workers.Add(1)
go w.watch()
return w, nil
}
func (w *readinessWatcher) add(fd int) error {
return w.control(syscall.EPOLL_CTL_ADD, fd)
}
func (w *readinessWatcher) arm(fd int) error {
return w.control(syscall.EPOLL_CTL_MOD, fd)
}
func (w *readinessWatcher) control(operation, fd int) error {
event := syscall.EpollEvent{Fd: int32(fd), Events: w.mask | syscall.EPOLLERR | syscall.EPOLLHUP | syscall.EPOLLONESHOT}
return syscall.EpollCtl(w.fd, operation, fd, &event)
}
func (w *readinessWatcher) notify() {
select {
case w.events <- nil:
default:
}
}
func (w *readinessWatcher) watch() {
defer w.workers.Done()
var events [16]syscall.EpollEvent
for {
select {
case <-w.stop:
return
default:
}
// Device readiness and shutdown wake epoll directly; there is no polling timer.
count, err := syscall.EpollWait(w.fd, events[:], -1)
if errors.Is(err, syscall.EINTR) {
continue
}
if err != nil {
select {
case w.events <- err:
case <-w.stop:
}
return
}
if count > 0 {
for _, event := range events[:count] {
if int(event.Fd) == w.wake[0] {
return
}
}
w.notify()
}
}
}
func (w *readinessWatcher) close() error {
close(w.stop)
_, err := syscall.Write(w.wake[1], []byte{1})
w.workers.Wait()
return errors.Join(err, syscall.Close(w.wake[0]), syscall.Close(w.wake[1]), syscall.Close(w.fd))
}