188 lines
5.7 KiB
Go
188 lines
5.7 KiB
Go
// 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")
|
|
}
|