204 lines
5.8 KiB
Go
204 lines
5.8 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>.
|
|
|
|
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
|
|
}
|