Files

300 lines
9.6 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package wayland
import (
"context"
"errors"
"fmt"
"sync"
"time"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/sessionbus"
"github.com/godbus/dbus/v5"
)
const (
managerName = "org.freedesktop.a11y.Manager"
managerPath = dbus.ObjectPath("/org/freedesktop/a11y/Manager")
monitorInterface = "org.freedesktop.a11y.KeyboardMonitor"
)
// godbus's default handler can reorder signals after a channel fills. Keyboard
// events need a bounded FIFO: overflow terminates the backend and releases grabs.
type signalQueue struct {
keyboard *Keyboard
signals chan queuedSignal
overflow chan struct{}
once sync.Once
}
type queuedSignal struct {
signal *dbus.Signal
generation uint64
}
func (queue *signalQueue) DeliverSignal(_ string, _ string, signal *dbus.Signal) {
if signal.Path != managerPath && signal.Name != "org.freedesktop.DBus.NameOwnerChanged" {
return
}
generation := queue.keyboard.secureGeneration.Load()
if signal.Name == monitorInterface+".KeyEvent" && queue.keyboard.secure.Load() {
return
}
select {
case queue.signals <- queuedSignal{signal: signal, generation: generation}:
default:
queue.once.Do(func() { close(queue.overflow) })
}
}
func (*signalQueue) Terminate() {}
type keyboardOwner struct {
keyboard *Keyboard
connection *dbus.Conn
owner string
configuration keyboardConfiguration
policy keyboardPolicy
bindings input.CommandBindings
router *input.CommandRouter
held map[uint16]uint32
captured map[uint16]bool
learning bool
started time.Time
sequence uint32
generation uint64
}
func (owner *keyboardOwner) call(ctx context.Context, method string, arguments ...any) error {
deadline, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
return owner.connection.Object(owner.owner, managerPath).CallWithContext(deadline, monitorInterface+"."+method, 0, arguments...).Err
}
func (owner *keyboardOwner) requested_policy() keyboardPolicy {
keyboard := owner.keyboard
if !keyboard.enabled.Load() || keyboard.secure.Load() {
owner.learning = false
keyboard.learn.Store(false)
}
return keyboardPolicy{enabled: keyboard.enabled.Load(), browse: keyboard.browse.Load(), button: keyboard.nativeButton.Load(), secure: keyboard.secure.Load(), learn: owner.learning}
}
func (owner *keyboardOwner) retire_secure_generation() {
generation := owner.keyboard.secureGeneration.Load()
if generation == owner.generation {
return
}
// A complete lock/unlock can occur before the owner wakes. Retire held
// keys even when the final secure boolean is unchanged.
owner.generation = generation
owner.router.Reset()
clear(owner.held)
clear(owner.captured)
owner.learning = false
owner.keyboard.learn.Store(false)
owner.keyboard.active.Store(false)
}
func (owner *keyboardOwner) configure(ctx context.Context, configuration keyboardConfiguration, policy keyboardPolicy) error {
symbols, grabs, bindings := make_grabs(configuration, policy)
if err := owner.call(ctx, "SetKeyGrabs", symbols, grabs); err != nil {
return err
}
if policy.learn != owner.policy.learn {
method := "UngrabKeyboard"
if policy.learn {
method = "GrabKeyboard"
}
if err := owner.call(ctx, method); err != nil {
return err
}
}
owner.configuration, owner.policy, owner.bindings = configuration, policy, bindings
owner.router = input.NewCommandRouter(symbols, bindings)
owner.keyboard.applied.Store(policy.enabled && !policy.secure)
owner.keyboard.browseApplied.Store(policy.browse && policy.enabled && !policy.secure)
owner.keyboard.learnEcho.Store(policy.learn)
return nil
}
func (keyboard *Keyboard) run(ctx context.Context, configuration keyboardConfiguration, setup chan<- error) {
defer close(keyboard.done)
defer keyboard.applied.Store(false)
defer keyboard.browseApplied.Store(false)
defer keyboard.active.Store(false)
defer keyboard.learn.Store(false)
defer keyboard.learnEcho.Store(false)
queue := &signalQueue{keyboard: keyboard, signals: make(chan queuedSignal, 256), overflow: make(chan struct{})}
connection, err := sessionbus.Connect(keyboard.logger, "Wayland keyboard", dbus.WithSignalHandler(queue))
if err != nil {
setup <- err
return
}
defer connection.Close() // Also releases grabs if setup only partly succeeded.
owner := &keyboardOwner{keyboard: keyboard, connection: connection, configuration: configuration,
held: make(map[uint16]uint32), captured: make(map[uint16]bool), started: time.Now()}
deadline, cancel := context.WithTimeout(ctx, callTimeout)
err = connection.BusObject().CallWithContext(deadline, "org.freedesktop.DBus.GetNameOwner", 0, managerName).Store(&owner.owner)
cancel()
if err != nil {
setup <- fmt.Errorf("shared accessibility keyboard interface unavailable: %w", err)
return
}
if err := connection.AddMatchSignal(dbus.WithMatchSender("org.freedesktop.DBus"), dbus.WithMatchInterface("org.freedesktop.DBus"), dbus.WithMatchMember("NameOwnerChanged"), dbus.WithMatchArg(0, managerName)); err != nil {
setup <- err
return
}
if err := connection.AddMatchSignal(dbus.WithMatchSender(owner.owner), dbus.WithMatchObjectPath(managerPath), dbus.WithMatchInterface(monitorInterface), dbus.WithMatchMember("KeyEvent")); err != nil {
setup <- err
return
}
// Some implementations authorize watches through the client's well-known
// accessibility name. Use a Cthulhu identity, never claim another reader's.
if result, err := connection.RequestName("org.stormux.Cthulhu.KeyboardMonitor", dbus.NameFlagDoNotQueue); err != nil || result != dbus.RequestNameReplyPrimaryOwner {
if err == nil {
err = errors.New("Cthulhu keyboard name is already owned")
}
setup <- err
return
}
policy := owner.requested_policy()
if !policy.secure {
if err := owner.call(ctx, "WatchKeyboard"); err != nil {
setup <- err
return
}
}
if err := owner.configure(ctx, configuration, policy); err != nil {
setup <- err
return
}
keyboard.logger.Info("using shared Wayland accessibility keyboard interface")
setup <- nil
if err := owner.loop(ctx, queue); err != nil && ctx.Err() == nil {
keyboard.fail(err)
}
}
func (owner *keyboardOwner) loop(ctx context.Context, queue *signalQueue) error {
keyboard := owner.keyboard
var pending *keyboardUpdate
defer func() {
if pending != nil {
pending.result <- errors.New("Wayland keyboard stopped")
}
}()
timer := time.NewTicker(20 * time.Millisecond)
defer timer.Stop()
var drainUntil time.Time
for {
stopping := ctx.Err() != nil
if stopping {
if len(owner.captured) == 0 {
return nil
}
if drainUntil.IsZero() {
drainUntil = time.Now().Add(callTimeout)
}
if time.Now().After(drainUntil) {
return nil
}
}
owner.retire_secure_generation()
policy := owner.requested_policy()
if policy.secure != owner.policy.secure {
owner.learning, policy.learn = false, false
keyboard.learn.Store(false)
clear(owner.held)
clear(owner.captured)
keyboard.active.Store(false)
method := "WatchKeyboard"
if policy.secure {
method = "UnwatchKeyboard"
}
if err := owner.call(ctx, method); err != nil {
return err
}
}
if !stopping && len(owner.held) == 0 {
if pending != nil {
update := pending
pending = nil
err := update.ctx.Err()
if err == nil {
err = owner.configure(update.ctx, update.configuration, policy)
}
if err != nil {
// SetKeyGrabs may have reached the compositor before a timeout.
// Restore the complete previous configuration before reporting failure.
if rollback := owner.configure(ctx, owner.configuration, policy); rollback != nil {
update.result <- errors.Join(err, rollback)
return rollback
}
} else {
keyboard.mutex.Lock()
keyboard.configuration = update.configuration
keyboard.mutex.Unlock()
}
update.result <- err
} else if policy != owner.policy {
if err := owner.configure(ctx, owner.configuration, policy); err != nil {
return err
}
}
}
updates := keyboard.updates
if pending != nil || stopping {
updates = nil
}
var canceled <-chan struct{}
if pending != nil {
canceled = pending.ctx.Done()
}
select {
case <-timer.C:
case <-keyboard.wake:
case <-canceled:
pending.result <- pending.ctx.Err()
pending = nil
case update := <-updates:
pending = &update
case <-queue.overflow:
return errors.New("Wayland keyboard signal queue overflow")
case <-owner.connection.Context().Done():
return errors.New("Wayland keyboard connection closed")
case queued := <-queue.signals:
signal := queued.signal
if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" && signal.Sender == "org.freedesktop.DBus" {
var name, previous, next string
if dbus.Store(signal.Body, &name, &previous, &next) == nil && name == managerName && previous == owner.owner && next != owner.owner {
return errors.New("Wayland accessibility keyboard service disappeared; restart Cthulhu after the service returns")
}
continue
}
if signal.Sender != owner.owner || signal.Path != managerPath || signal.Name != monitorInterface+".KeyEvent" {
continue
}
if queued.generation != keyboard.secureGeneration.Load() {
continue
}
var key keyEvent
if err := dbus.Store(signal.Body, &key.released, &key.state, &key.symbol, &key.character, &key.code); err != nil {
return fmt.Errorf("invalid accessibility key event: %w", err)
}
if stopping {
if key.released {
delete(owner.captured, key.code)
}
continue
}
if err := owner.handle_key(key); err != nil {
return err
}
}
}
}