mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
114 lines
3.2 KiB
Go
114 lines
3.2 KiB
Go
//go:build !windows
|
|||
|
|
|
||
|
|
package pty
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"os"
|
||
|
|
"os/exec"
|
||
|
|
"strings"
|
||
|
|
"syscall"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/safedep/ptyx"
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
)
|
||
|
|
|
||
|
|
const ttyHelperEnv = "PMG_TEST_TTY_HELPER"
|
||
|
|
|
||
|
|
// Reproduces https://github.com/safedep/pmg/issues/322: a pmg process started
|
||
|
|
// as a background job (`pmg npm run test &`) still has the TTY on
|
||
|
|
// stdin/stdout, but it is not in the terminal's foreground process group.
|
||
|
|
// Treating it as interactive makes the PTY session call tcsetattr, which
|
||
|
|
// stops the process with SIGTTOU. IsInteractiveTerminal must report false in
|
||
|
|
// that situation.
|
||
|
|
func TestIsInteractiveTerminalBackgroundJob(t *testing.T) {
|
||
|
|
exe, err := os.Executable()
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
// The helper runs on a fresh PTY as the foreground process group, then
|
||
|
|
// moves itself to the background, checking IsInteractiveTerminal in both
|
||
|
|
// states.
|
||
|
|
sess, err := ptyx.Spawn(ctx, ptyx.SpawnOpts{
|
||
|
|
Prog: exe,
|
||
|
|
Args: []string{"-test.run", "^TestIsInteractiveTerminalTTYHelper$", "-test.v"},
|
||
|
|
Env: append(envWithoutCI(), ttyHelperEnv+"=1"),
|
||
|
|
Cols: 80,
|
||
|
|
Rows: 24,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
defer func() {
|
||
|
|
require.NoError(t, sess.Close())
|
||
|
|
}()
|
||
|
|
|
||
|
|
var output bytes.Buffer
|
||
|
|
copyDone := make(chan struct{})
|
||
|
|
go func() {
|
||
|
|
defer close(copyDone)
|
||
|
|
_, _ = io.Copy(&output, sess.PtyReader())
|
||
|
|
}()
|
||
|
|
|
||
|
|
waitErr := sess.Wait()
|
||
|
|
|
||
|
|
// The buffer must not be read until the copy goroutine is done, both to
|
||
|
|
// avoid a data race and to ensure all helper output has been captured.
|
||
|
|
select {
|
||
|
|
case <-copyDone:
|
||
|
|
case <-time.After(5 * time.Second):
|
||
|
|
t.Fatal("timed out waiting for pty output copy to finish")
|
||
|
|
}
|
||
|
|
|
||
|
|
out := output.String()
|
||
|
|
require.NoError(t, waitErr, "helper failed, output:\n%s", out)
|
||
|
|
assert.Contains(t, out, "foreground_interactive=true", "expected interactive in foreground, output:\n%s", out)
|
||
|
|
assert.Contains(t, out, "background_interactive=false", "expected non-interactive in background, output:\n%s", out)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestIsInteractiveTerminalTTYHelper is not a real test. It is re-executed by
|
||
|
|
// TestIsInteractiveTerminalBackgroundJob on a PTY where it starts as the
|
||
|
|
// foreground process group.
|
||
|
|
func TestIsInteractiveTerminalTTYHelper(t *testing.T) {
|
||
|
|
if os.Getenv(ttyHelperEnv) != "1" {
|
||
|
|
t.Skip("helper for TestIsInteractiveTerminalBackgroundJob")
|
||
|
|
}
|
||
|
|
|
||
|
|
fmt.Printf("foreground_interactive=%v\n", IsInteractiveTerminal())
|
||
|
|
|
||
|
|
// Hand the terminal's foreground process group to a child, like a shell
|
||
|
|
// does for the foreground job. This process then becomes a background job
|
||
|
|
// on the TTY, matching `pmg npm run test &`.
|
||
|
|
cmd := exec.Command("sleep", "30")
|
||
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||
|
|
Setpgid: true,
|
||
|
|
Foreground: true,
|
||
|
|
Ctty: int(os.Stdin.Fd()),
|
||
|
|
}
|
||
|
|
require.NoError(t, cmd.Start())
|
||
|
|
defer func() {
|
||
|
|
_ = cmd.Process.Kill()
|
||
|
|
_, _ = cmd.Process.Wait()
|
||
|
|
}()
|
||
|
|
|
||
|
|
fmt.Printf("background_interactive=%v\n", IsInteractiveTerminal())
|
||
|
|
}
|
||
|
|
|
||
|
|
func envWithoutCI() []string {
|
||
|
|
env := make([]string, 0, len(os.Environ()))
|
||
|
|
for _, entry := range os.Environ() {
|
||
|
|
if strings.HasPrefix(entry, "CI=") {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
env = append(env, entry)
|
||
|
|
}
|
||
|
|
|
||
|
|
return env
|
||
|
|
}
|