// 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 . package control import ( "encoding/json" "fmt" "io" "os" "path/filepath" "sort" "strconv" "strings" "syscall" "time" ) type instance struct { PID int `json:"pid"` ConnectionHandoff bool `json:"connection_handoff,omitempty"` PPID int `json:"ppid"` SocketFiles []string `json:"socket_files"` MainSocket bool `json:"main_socket"` ScreenDriver string `json:"screen_driver"` KeyboardDriver string `json:"keyboard_driver"` WindowID string `json:"x11_window_id"` CreatedAt float64 `json:"created_at"` UpdatedAt float64 `json:"updated_at"` uid int } func (s *Server) registry_dir() string { if s.options.RegistryDirectory != "" { return s.options.RegistryDirectory } return filepath.Join(os.TempDir(), "fenrirscreenreader-instances") } func (s *Server) register() error { dir := s.registry_dir() if err := os.Mkdir(dir, os.ModeSticky|0777); err != nil && !os.IsExist(err) { return err } info, err := os.Lstat(dir) if err != nil { return err } if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { return fmt.Errorf("invalid instance registry %s", dir) } // Only change permissions on a directory created/owned by this reader's UID. if stat, ok := info.Sys().(*syscall.Stat_t); ok && int(stat.Uid) == os.Geteuid() { if err := os.Chmod(dir, os.ModeSticky|0777); err != nil { return err } info, err = os.Lstat(dir) if err != nil { return err } } if info.Mode()&os.ModeSticky == 0 && info.Mode().Perm()&0022 != 0 { return fmt.Errorf("instance registry lacks sticky permission: %s", dir) } now := float64(time.Now().UnixNano()) / 1e9 data := instance{ConnectionHandoff: true, PID: os.Getpid(), PPID: os.Getppid(), CreatedAt: now, UpdatedAt: now, ScreenDriver: "vcsaDriver", KeyboardDriver: "evdevDriver"} if s.options.TerminalMode { data.ScreenDriver, data.KeyboardDriver, data.WindowID = "ptyDriver", "x11Driver", s.options.WindowID } for _, endpoint := range s.endpoint_snapshot() { data.SocketFiles = append(data.SocketFiles, endpoint.path) data.MainSocket = data.MainSocket || endpoint.path == s.main_path() } encoded, err := json.Marshal(data) if err != nil { return err } file, err := os.CreateTemp(dir, ".instance-*") if err != nil { return err } defer os.Remove(file.Name()) if _, err := file.Write(append(encoded, '\n')); err != nil { file.Close() return err } if err := file.Chmod(0644); err != nil { file.Close() return err } if err := file.Close(); err != nil { return err } path := filepath.Join(dir, strconv.Itoa(os.Getpid())+".json") if err := os.Rename(file.Name(), path); err != nil { return err } s.registryPath = path return nil } func (s *Server) refresh_registry() { ticker := time.NewTicker(200 * time.Millisecond) lastRefresh := time.Now() defer ticker.Stop() for { select { case <-s.ctx.Done(): return case <-ticker.C: if s.claim_main() || time.Since(lastRefresh) >= 10*time.Second { _ = s.register() lastRefresh = time.Now() } } } } func (s *Server) instances() []instance { entries, _ := os.ReadDir(s.registry_dir()) var instances []instance for _, entry := range entries { if s.ctx.Err() != nil { break } if !strings.HasSuffix(entry.Name(), ".json") { continue } path := filepath.Join(s.registry_dir(), entry.Name()) file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0) if err != nil { continue } info, err := file.Stat() if err != nil || !info.Mode().IsRegular() { file.Close() continue } var data instance err = json.NewDecoder(io.LimitReader(file, 16384)).Decode(&data) file.Close() stat, ok := info.Sys().(*syscall.Stat_t) if err != nil || !ok || data.PID <= 1 || entry.Name() != strconv.Itoa(data.PID)+".json" || time.Since(time.Unix(0, int64(data.UpdatedAt*1e9))) > 30*time.Second { continue } if err := syscall.Kill(data.PID, 0); err != nil && err != syscall.EPERM { continue } data.uid = int(stat.Uid) instances = append(instances, data) } sort.Slice(instances, func(a, b int) bool { return instances[a].PID < instances[b].PID }) return instances } func (s *Server) list() string { var lines []string for _, data := range s.instances() { window, main := data.WindowID, "no" if window == "" { window = "none" } if data.MainSocket { main = "yes" } lines = append(lines, fmt.Sprintf("pid=%d ppid=%d screen=%s keyboard=%s main_socket=%s x11_window_id=%s sockets=%s", data.PID, data.PPID, data.ScreenDriver, data.KeyboardDriver, main, window, strings.Join(data.SocketFiles, ", "))) } if len(lines) == 0 { return "No Fenrir instances registered" } return strings.Join(lines, "\n") } func parent_pid(pid int) int { data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) if err != nil { return 0 } end := strings.LastIndexByte(string(data), ')') if end < 0 { return 0 } fields := strings.Fields(string(data[end+1:])) if len(fields) < 2 { return 0 } parent, _ := strconv.Atoi(fields[1]) return parent }