// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package control import ( "context" "encoding/json" "fmt" "io" "net" "os" "os/exec" "path/filepath" "strings" "syscall" "testing" "time" "unsafe" ) // The caller lives inside a private, real Screen session. A separate test // socket coordinates requests without typing into any terminal. func TestScreenCallerProcess(t *testing.T) { path := os.Getenv("FENRIR_SCREEN_TEST_COORDINATOR") if path == "" { return } listener, err := net.Listen("unix", path) if err != nil { os.Exit(1) } defer listener.Close() for { connection, err := listener.Accept() if err != nil { os.Exit(1) } var request processCommand if json.NewDecoder(connection).Decode(&request) != nil { connection.Close() continue } reply := processReply{PID: os.Getpid()} target, err := net.DialTimeout("unix", request.Path, time.Second) if err == nil { target.SetDeadline(time.Now().Add(2 * time.Second)) _, err = io.WriteString(target, request.Text+"\n") if err == nil { var data []byte data, err = io.ReadAll(target) reply.Response = string(data) } target.Close() } if err != nil { reply.Error = err.Error() } json.NewEncoder(connection).Encode(reply) connection.Close() } } func TestNativeScreenReattachmentRoutesSayAndNotify(t *testing.T) { screen, err := exec.LookPath("screen") if err != nil { t.Skip("GNU Screen not installed") } options := private_options(t) // The shared owner and an unrelated old-window reader must never receive // messages intended for the terminal displaying the reattached session. t.Setenv("DISPLAY", ":fixture") t.Setenv("WINDOWID", "old") start_reader_process(t, options.Directory, "tty") start_reader_process(t, options.Directory, "terminal") options.TerminalMode, options.WindowID = true, "current" server, err := Open(context.Background(), options) if err != nil { t.Fatal(err) } defer server.Close() go func() { for { select { case request := <-server.Requests(): data, _ := json.Marshal(requestReceipt{os.Getpid(), request.PID, request.UID, request.Text}) request.Reply <- string(data) case <-server.ctx.Done(): return } } }() screenDirectory := filepath.Join(options.Directory, "screen") if err := os.Mkdir(screenDirectory, 0700); err != nil { t.Fatal(err) } coordinator := filepath.Join(options.Directory, "caller.sock") session := fmt.Sprintf("fenrir-test-%d", os.Getpid()) environment := append(os.Environ(), "SCREENDIR="+screenDirectory, "TERM=xterm", "STY=", "FENRIR_SCREEN_TEST_COORDINATOR="+coordinator) start := exec.Command(screen, "-dmS", session, "-c", "/dev/null", os.Args[0], "-test.run=^TestScreenCallerProcess$") start.Env = environment if output, err := start.CombinedOutput(); err != nil { t.Fatal(string(output), err) } t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() stop := exec.CommandContext(ctx, screen, "-S", session, "-X", "quit") stop.Env = environment _ = stop.Run() }) send := func(text string) processReply { t.Helper() connection, err := net.DialTimeout("unix", coordinator, time.Second) if err != nil { t.Fatal(err) } defer connection.Close() connection.SetDeadline(time.Now().Add(3 * time.Second)) if err := json.NewEncoder(connection).Encode(processCommand{Path: server.main_path(), Text: text}); err != nil { t.Fatal(err) } var reply processReply if err := json.NewDecoder(connection).Decode(&reply); err != nil || reply.Error != "" { t.Fatal(reply, err) } return reply } deadline := time.Now().Add(3 * time.Second) for { if _, err := os.Stat(coordinator); err == nil { break } if time.Now().After(deadline) { t.Fatal("private Screen caller did not start") } time.Sleep(10 * time.Millisecond) } if reply := send("COMMAND SAY detached"); !strings.Contains(reply.Response, "unique attached reader") { for pid := reply.PID; pid > 1; pid = parent_pid(pid) { command, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) executable, _ := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid)) t.Logf("ancestor %d command %q executable %q", pid, command, executable) } t.Fatal("detached Screen used the old window", reply) } fd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK|syscall.O_CLOEXEC, 0) if err != nil { t.Fatal(err) } master := os.NewFile(uintptr(fd), "private Screen display") defer master.Close() var unlock int32 if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&unlock))); err != 0 { t.Fatal(err) } var number uint32 if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&number))); err != 0 { t.Fatal(err) } slave, err := os.OpenFile(fmt.Sprintf("/dev/pts/%d", number), os.O_RDWR|syscall.O_NOCTTY, 0) if err != nil { t.Fatal(err) } defer slave.Close() attach := exec.Command(screen, "-r", session) attach.Env = append(environment, "WINDOWID=current") attach.Stdin, attach.Stdout, attach.Stderr = slave, slave, slave attach.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 0} if err := attach.Start(); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = attach.Process.Kill(); _ = attach.Wait() }) deadline = time.Now().Add(3 * time.Second) for { reply := send("COMMAND SAY attached") if !strings.HasPrefix(reply.Response, "Remote routing failed:") { check_receipt(t, reply, os.Getpid(), "COMMAND SAY attached") break } if time.Now().After(deadline) { t.Fatal("reattached Screen did not find its reader", reply) } time.Sleep(10 * time.Millisecond) } check_receipt(t, send("COMMAND NOTIFY notification"), os.Getpid(), "COMMAND NOTIFY notification") }