//go:build linux package version import ( "bufio" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "testing" "time" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" ) const updaterPTYRoleEnv = "MPV_MANAGER_TEST_UPDATER_PTY_ROLE" func environmentWithUpdaterPTYRole(role string) []string { environment := make([]string, 0, len(os.Environ())+1) for _, entry := range os.Environ() { name, _, _ := strings.Cut(entry, "=") if name != updaterPTYRoleEnv { environment = append(environment, entry) } } return append(environment, updaterPTYRoleEnv+"="+role) } func TestUpdaterTerminalDescriptorsSurviveHelperRelaunch(t *testing.T) { switch os.Getenv(updaterPTYRoleEnv) { case "helper": child := attachUpdateTerminal(exec.Command(os.Args[0], "-test.run=^TestUpdaterTerminalDescriptorsSurviveHelperRelaunch$")) child.Env = environmentWithUpdaterPTYRole("child") if err := child.Run(); err != nil { t.Fatalf("PTY child failed: %v", err) } return case "child": if _, err := unix.IoctlGetTermios(int(os.Stdout.Fd()), unix.TCGETS); err != nil { t.Fatalf("relaunch output is not a terminal: %v", err) } fmt.Fprintln(os.Stdout, "FIRST-FRAME") line, err := bufio.NewReader(os.Stdin).ReadString('\n') if err != nil { t.Fatalf("read terminal input: %v", err) } fmt.Fprintln(os.Stdout, "INPUT:"+strings.TrimSpace(line)) return } masterFD, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_CLOEXEC, 0) if err != nil { t.Skipf("open native PTY: %v", err) } master := os.NewFile(uintptr(masterFD), "ptmx") require.NotNil(t, master) defer master.Close() require.NoError(t, unix.IoctlSetPointerInt(masterFD, unix.TIOCSPTLCK, 0)) ptyNumber, err := unix.IoctlGetInt(masterFD, unix.TIOCGPTN) require.NoError(t, err) slave, err := os.OpenFile(filepath.Join("/dev/pts", strconv.Itoa(ptyNumber)), os.O_RDWR, 0) require.NoError(t, err) helper := exec.Command(os.Args[0], "-test.run=^TestUpdaterTerminalDescriptorsSurviveHelperRelaunch$") helper.Env = environmentWithUpdaterPTYRole("helper") func() { oldStdin, oldStdout, oldStderr := os.Stdin, os.Stdout, os.Stderr defer func() { os.Stdin, os.Stdout, os.Stderr = oldStdin, oldStdout, oldStderr }() os.Stdin, os.Stdout, os.Stderr = slave, slave, slave attachUpdateTerminal(helper) }() helper.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 0} require.NoError(t, helper.Start()) require.NoError(t, slave.Close()) lines := make(chan string, 16) go func() { scanner := bufio.NewScanner(master) for scanner.Scan() { lines <- scanner.Text() } close(lines) }() waitForLine := func(fragment string) { t.Helper() deadline := time.After(8 * time.Second) for { select { case line, ok := <-lines: if !ok { t.Fatalf("PTY closed before %q", fragment) } if strings.Contains(line, fragment) { return } case <-deadline: _ = helper.Process.Kill() t.Fatalf("timed out waiting for PTY output %q", fragment) } } } waitForLine("FIRST-FRAME") _, err = master.Write([]byte("probe-input\n")) require.NoError(t, err) waitForLine("INPUT:probe-input") require.NoError(t, helper.Wait()) }