// Package wine supervises the optional installed Wine Access accessibility // helper. Native Wine objects stay in that process; Go receives D-Bus messages. package wine import ( "context" "errors" "log/slog" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" ) const managedKey = "CTHULHU_WINE_ACCESS_MANAGED" type prefix struct { path, loader string environment map[string]string } type process struct { cmd *exec.Cmd done chan struct{} started time.Time seen time.Time failures int next time.Time } type Manager struct { logger *slog.Logger helper string procRoot string cancel context.CancelFunc done chan struct{} } func New(logger *slog.Logger) *Manager { if logger == nil { logger = slog.New(slog.DiscardHandler) } return &Manager{logger: logger, helper: helperPath(), procRoot: "/proc"} } func helperPath() string { return findHelper([]string{ filepath.Join(os.Getenv("HOME"), ".local", "libexec", "wine-access", "wine-access.exe"), filepath.Join(os.Getenv("HOME"), ".local", "libexec", "cthulhu", "wine", "cthulhu-wine-access.exe"), "/usr/local/libexec/wine-access/wine-access.exe", "/usr/libexec/wine-access/wine-access.exe", "/usr/libexec/cthulhu/wine/cthulhu-wine-access.exe", }) } func findHelper(candidates []string) string { if path := os.Getenv("CTHULHU_WINE_ACCESS_HELPER"); executable(path) { return path } for _, path := range candidates { if executable(path) { return path } } for _, name := range []string{"wine-access.exe", "cthulhu-wine-access.exe"} { if path, err := exec.LookPath(name); err == nil { return path } } return "" } func executable(path string) bool { if path == "" { return false } info, err := os.Stat(path) return err == nil && info.Mode().IsRegular() && info.Mode().Perm()&0111 != 0 } func (manager *Manager) Start(ctx context.Context) error { if manager.helper == "" { manager.logger.Info("Wine accessibility helper unavailable") return nil } if manager.cancel != nil { return errors.New("Wine accessibility manager already started") } workerCtx, cancel := context.WithCancel(ctx) manager.cancel = cancel manager.done = make(chan struct{}) go manager.run(workerCtx) return nil } func (manager *Manager) Stop(ctx context.Context) error { if manager.cancel == nil { return nil } manager.cancel() select { case <-manager.done: return nil case <-ctx.Done(): return ctx.Err() } } func (manager *Manager) run(ctx context.Context) { defer close(manager.done) active := make(map[string]*process) ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() manager.poll(active) for { select { case <-ctx.Done(): for _, child := range active { stop(child) } return case <-ticker.C: manager.poll(active) } } } func (manager *Manager) poll(active map[string]*process) { now := time.Now() discovered := discover(manager.procRoot, uint32(os.Getuid())) for path, source := range discovered { child := active[path] if child == nil { child = &process{} active[path] = child } child.seen = now if child.cmd != nil { select { case <-child.done: if now.Sub(child.started) < 10*time.Second { child.failures++ } else { child.failures = 0 } child.next = now.Add(backoff(child.failures)) child.cmd = nil default: continue } } if now.Before(child.next) { continue } if err := manager.launch(child, source, now); err != nil { child.failures++ child.next = now.Add(backoff(child.failures)) manager.logger.Warn("start Wine accessibility helper", "error", err) } } for path, child := range active { if _, found := discovered[path]; !found && now.Sub(child.seen) >= 10*time.Second { stop(child) delete(active, path) } } } func backoff(failures int) time.Duration { if failures > 6 { failures = 6 } return time.Duration(1<