229 lines
6.3 KiB
Go
229 lines
6.3 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
package wine
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/sessionbus"
|
|
"github.com/godbus/dbus/v5"
|
|
"log/slog"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type reviewProvider struct {
|
|
path dbus.ObjectPath
|
|
notice ReviewFocusNotice
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
trees map[string]*atspi.AccessibleTree
|
|
}
|
|
type ReviewRegistry struct {
|
|
logger *slog.Logger
|
|
mutex sync.Mutex
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
monitor, service *dbus.Conn
|
|
signals chan *dbus.Signal
|
|
done chan struct{}
|
|
changes chan struct{}
|
|
providers map[string]*reviewProvider
|
|
captureGate chan struct{}
|
|
captureCall func(context.Context, string, dbus.ObjectPath, ReviewFocusNotice) (map[string]dbus.Variant, error)
|
|
}
|
|
|
|
func NewReviewRegistry(logger *slog.Logger) *ReviewRegistry {
|
|
return &ReviewRegistry{logger: logger, changes: make(chan struct{}, 1), providers: make(map[string]*reviewProvider), captureGate: make(chan struct{}, 1)}
|
|
}
|
|
func (r *ReviewRegistry) notify() {
|
|
select {
|
|
case r.changes <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
func (r *ReviewRegistry) Changes() <-chan struct{} { return r.changes }
|
|
func (r *ReviewRegistry) Start(ctx context.Context) error {
|
|
if ctx == nil {
|
|
return errors.New("Wine review requires a context")
|
|
}
|
|
conn, err := sessionbus.Connect(r.logger, "wine-review")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
options := []dbus.MatchOption{dbus.WithMatchSender("org.freedesktop.DBus"), dbus.WithMatchInterface("org.freedesktop.DBus"), dbus.WithMatchMember("NameOwnerChanged")}
|
|
signals := make(chan *dbus.Signal, 64)
|
|
conn.Signal(signals)
|
|
if err = conn.AddMatchSignal(options...); err != nil {
|
|
conn.RemoveSignal(signals)
|
|
conn.Close()
|
|
return err
|
|
}
|
|
r.mutex.Lock()
|
|
if r.ctx != nil {
|
|
r.mutex.Unlock()
|
|
conn.RemoveSignal(signals)
|
|
conn.Close()
|
|
return errors.New("Wine review already started")
|
|
}
|
|
r.ctx, r.cancel = context.WithCancel(ctx)
|
|
r.monitor = conn
|
|
r.signals = signals
|
|
r.done = make(chan struct{})
|
|
lifetime, done := r.ctx, r.done
|
|
r.mutex.Unlock()
|
|
go func() {
|
|
defer close(done)
|
|
for {
|
|
select {
|
|
case <-lifetime.Done():
|
|
return
|
|
case <-conn.Context().Done():
|
|
r.retireAll()
|
|
return
|
|
case signal := <-signals:
|
|
if signal != nil && len(signal.Body) == 3 {
|
|
name, ok := signal.Body[0].(string)
|
|
old, _ := signal.Body[1].(string)
|
|
next, _ := signal.Body[2].(string)
|
|
if ok && name == old && next == "" {
|
|
r.disappeared(name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// The service connection is borrowed because native capture accepts only the
|
|
// current owner of Cthulhu's well-known name. Registry never closes it.
|
|
func (r *ReviewRegistry) BindWineReviewConnection(conn *dbus.Conn) {
|
|
r.mutex.Lock()
|
|
r.service = conn
|
|
if conn == nil {
|
|
for key, p := range r.providers {
|
|
p.cancel()
|
|
delete(r.providers, key)
|
|
}
|
|
}
|
|
r.notify()
|
|
r.mutex.Unlock()
|
|
}
|
|
func (r *ReviewRegistry) Stop(ctx context.Context) error {
|
|
r.mutex.Lock()
|
|
if r.cancel != nil {
|
|
r.cancel()
|
|
}
|
|
conn, signals, done := r.monitor, r.signals, r.done
|
|
for key, p := range r.providers {
|
|
p.cancel()
|
|
delete(r.providers, key)
|
|
}
|
|
r.ctx = nil
|
|
r.cancel = nil
|
|
r.monitor = nil
|
|
r.service = nil
|
|
r.signals = nil
|
|
r.done = nil
|
|
r.notify()
|
|
r.mutex.Unlock()
|
|
if conn != nil {
|
|
conn.RemoveSignal(signals)
|
|
conn.RemoveMatchSignal(dbus.WithMatchSender("org.freedesktop.DBus"), dbus.WithMatchInterface("org.freedesktop.DBus"), dbus.WithMatchMember("NameOwnerChanged"))
|
|
conn.Close()
|
|
}
|
|
if done != nil {
|
|
select {
|
|
case <-done:
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func (r *ReviewRegistry) RegisterProvider(sender string, path dbus.ObjectPath, version uint32) bool {
|
|
if !strings.HasPrefix(sender, ":") || !dbus.ObjectPath("/"+strings.ReplaceAll(sender[1:], ".", "/")).IsValid() || path != ReviewObjectPath || version != ReviewProtocolVersion {
|
|
return false
|
|
}
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
if r.ctx == nil || r.ctx.Err() != nil {
|
|
return false
|
|
}
|
|
if old := r.providers[sender]; old != nil {
|
|
old.cancel()
|
|
}
|
|
ctx, cancel := context.WithCancel(r.ctx)
|
|
r.providers[sender] = &reviewProvider{path: path, ctx: ctx, cancel: cancel, trees: make(map[string]*atspi.AccessibleTree)}
|
|
r.notify()
|
|
return true
|
|
}
|
|
func (r *ReviewRegistry) PresentFocus(sender string, notice ReviewFocusNotice) bool {
|
|
if !reviewString(notice.Instance, 256, false) || !reviewString(notice.Window, 256, false) || !reviewString(notice.Focus, 256, false) || notice.Revision == 0 || !reviewString(notice.Title, 4096, true) || !reviewString(notice.Role, 4096, false) || !notice.Bounds.Valid() {
|
|
return false
|
|
}
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
p := r.providers[sender]
|
|
if p == nil || p.ctx.Err() != nil {
|
|
return false
|
|
}
|
|
notice.Sender = sender
|
|
notice.Path = p.path
|
|
if p.notice == notice {
|
|
return true
|
|
}
|
|
if p.notice.Instance == notice.Instance && p.notice.Revision > notice.Revision {
|
|
return false
|
|
}
|
|
if p.notice.Instance != notice.Instance || p.notice.Window != notice.Window {
|
|
// Only the active native window can be captured. Retiring older
|
|
// windows bounds retained text and preserves the current tree's identity.
|
|
p.trees = make(map[string]*atspi.AccessibleTree)
|
|
}
|
|
p.cancel()
|
|
p.ctx, p.cancel = context.WithCancel(r.ctx)
|
|
p.notice = notice
|
|
r.notify()
|
|
return true
|
|
}
|
|
func (r *ReviewRegistry) Focuses() []ReviewFocusNotice {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
var notices []ReviewFocusNotice
|
|
for _, p := range r.providers {
|
|
if p.notice.Focus != "" && p.ctx.Err() == nil {
|
|
notices = append(notices, p.notice)
|
|
}
|
|
}
|
|
sort.Slice(notices, func(i, j int) bool { return notices[i].Sender < notices[j].Sender })
|
|
return notices
|
|
}
|
|
func (r *ReviewRegistry) Current(id atspi.ObjectID) bool {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
p := r.providers[id.BusName]
|
|
return IsReviewID(id) && p != nil && p.ctx.Err() == nil && p.notice.ID() == id
|
|
}
|
|
func (r *ReviewRegistry) disappeared(sender string) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
if p := r.providers[sender]; p != nil {
|
|
p.cancel()
|
|
delete(r.providers, sender)
|
|
r.notify()
|
|
}
|
|
}
|
|
func (r *ReviewRegistry) retireAll() {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
for sender, p := range r.providers {
|
|
p.cancel()
|
|
delete(r.providers, sender)
|
|
}
|
|
r.notify()
|
|
}
|