// 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 ( "context" "encoding/json" "fmt" "io" "net" "os" "path/filepath" "strings" "testing" "time" ) func private_options(t *testing.T) Options { t.Helper() directory, err := os.MkdirTemp("/tmp", "fenrir-remote-") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.RemoveAll(directory) }) return Options{Directory: directory, RegistryDirectory: filepath.Join(directory, "registry")} } func connect_remote(t *testing.T, path string) *net.UnixConn { t.Helper() connection, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: path, Net: "unix"}) if err != nil { t.Fatal(err) } connection.SetDeadline(time.Now().Add(2 * time.Second)) t.Cleanup(func() { connection.Close() }) return connection } func TestPythonSocketProtocolAndRegistry(t *testing.T) { options := private_options(t) server, err := Open(context.Background(), options) if err != nil { t.Fatal(err) } defer server.Close() connection := connect_remote(t, server.main_path()) if _, err := io.WriteString(connection, "command say Existing Bash script\n"); err != nil { t.Fatal(err) } select { case request := <-server.Requests(): if request.Text != "command say Existing Bash script" || request.UID != os.Geteuid() || request.PID != os.Getpid() { t.Fatal(request) } request.Reply <- "" case <-time.After(time.Second): t.Fatal("command was not delivered") } for _, command := range []string{"LIST", "command ls"} { connection := connect_remote(t, server.main_path()) io.WriteString(connection, command+"\n") response, err := io.ReadAll(connection) if err != nil || !strings.Contains(string(response), "screen=vcsaDriver keyboard=evdevDriver main_socket=yes") { t.Fatal(string(response), err) } } info, err := os.Stat(server.main_path()) if err != nil || info.Mode().Perm() != 0666 { t.Fatal(info, err) } instances := server.instances() if len(instances) != 1 || instances[0].PID != os.Getpid() { t.Fatal(instances) } } func TestRequestTimeoutReturnsErrorInsteadOfSilentSuccess(t *testing.T) { server, err := Open(context.Background(), private_options(t)) if err != nil { t.Fatal(err) } defer server.Close() connection := connect_remote(t, server.main_path()) if _, err := io.WriteString(connection, "SETTING SAVE\n"); err != nil { t.Fatal(err) } select { case <-server.Requests(): // Deliberately leave the admitted operation pending. case <-time.After(time.Second): t.Fatal("request not admitted") } response, err := io.ReadAll(connection) if err != nil || !strings.Contains(string(response), "timed out; completion is unknown") { t.Fatal("timeout looked like success", string(response), err) } } func TestFullRequestQueueReturnsErrorInsteadOfSilentSuccess(t *testing.T) { server, err := Open(context.Background(), private_options(t)) if err != nil { t.Fatal(err) } defer server.Close() for range cap(server.requests) { server.requests <- Request{} } connection := connect_remote(t, server.main_path()) if _, err := io.WriteString(connection, "SETTING SAVE\n"); err != nil { t.Fatal(err) } response, err := io.ReadAll(connection) if err != nil || !strings.Contains(string(response), "request queue is full") { t.Fatal("full queue looked like success", string(response), err) } } func TestCancelledRequestReturnsUnknownCompletion(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() server, err := Open(ctx, private_options(t)) if err != nil { t.Fatal(err) } defer server.Close() connection := connect_remote(t, server.main_path()) if _, err := io.WriteString(connection, "SETTING SAVE\n"); err != nil { t.Fatal(err) } select { case <-server.Requests(): case <-time.After(time.Second): t.Fatal("request not admitted") } cancel() response, err := io.ReadAll(connection) if err != nil || !strings.Contains(string(response), "cancelled; completion is unknown") { t.Fatal("cancellation looked like success", string(response), err) } } func TestSocketCoexistenceAndCleanupNeverReplaceActiveReader(t *testing.T) { options := private_options(t) server, err := Open(context.Background(), options) if err != nil { t.Fatal(err) } defer server.Close() if second, err := Open(context.Background(), options); err == nil { second.Close() t.Fatal("replaced active socket") } // A failed startup must not remove the first reader's registry entry. if len(server.instances()) != 1 { t.Fatal("active registry entry removed") } if connection, err := net.Dial("unix", server.main_path()); err != nil { t.Fatal("main socket lost", err) } else { connection.Close() } } func TestSocketRejectsSymlinksAndUnrelatedFiles(t *testing.T) { for _, symlink := range []bool{false, true} { options := private_options(t) path := filepath.Join(options.Directory, "fenrirscreenreader-daemon.sock") if symlink { if err := os.Symlink("/dev/null", path); err != nil { t.Fatal(err) } } else if err := os.WriteFile(path, []byte("preserve"), 0600); err != nil { t.Fatal(err) } if server, err := Open(context.Background(), options); err == nil { server.Close() t.Fatal("accepted unrelated file") } if _, err := os.Lstat(path); err != nil { t.Fatal("unrelated file removed", err) } } } func TestSilentAndOversizedClientsDoNotBlockOtherRequests(t *testing.T) { server, err := Open(context.Background(), private_options(t)) if err != nil { t.Fatal(err) } defer server.Close() silent := connect_remote(t, server.main_path()) defer silent.Close() oversized := connect_remote(t, server.main_path()) io.WriteString(oversized, strings.Repeat("x", 9000)) good := connect_remote(t, server.main_path()) io.WriteString(good, "COMMAND INTERRUPT\n") select { case request := <-server.Requests(): if request.Text != "COMMAND INTERRUPT" { t.Fatal(request.Text) } request.Reply <- "" case <-time.After(time.Second): t.Fatal("silent client blocked reader") } select { case request := <-server.Requests(): t.Fatal("invalid request admitted", request.Text) case <-time.After(300 * time.Millisecond): } } func TestStaleSocketReplacementAndPrivateInstanceSelection(t *testing.T) { options := private_options(t) path := filepath.Join(options.Directory, "stale.sock") old, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"}) if err != nil { t.Fatal(err) } old.SetUnlinkOnClose(false) old.Close() endpoint, err := listen(path) if err != nil { t.Fatal(err) } endpoint.listener.Close() if err := os.Mkdir(options.RegistryDirectory, 0700); err != nil { t.Fatal(err) } pid := os.Getppid() privatePath := filepath.Join(options.Directory, fmt.Sprintf("fenrirscreenreader-%d.sock", pid)) listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: privatePath, Net: "unix"}) if err != nil { t.Fatal(err) } defer listener.Close() data, err := json.Marshal(instance{PID: pid, ScreenDriver: "ptyDriver", ConnectionHandoff: true, SocketFiles: []string{privatePath}, UpdatedAt: float64(time.Now().UnixNano()) / 1e9}) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(options.RegistryDirectory, fmt.Sprintf("%d.json", pid)), data, 0600); err != nil { t.Fatal(err) } options.TerminalMode = true server := &Server{ctx: context.Background(), options: options} target, err := server.route_target(pid, os.Geteuid()) if err != nil || target != privatePath { t.Fatal("registered ancestor instance not selected", target, err) } target, err = server.route_target(pid, os.Geteuid()+1) if err == nil || target != "" { t.Fatal("forwarded to another user's instance", target, err) } }