// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build unix

package syscall_test

import (
	"bytes"
	"fmt"
	"internal/testenv"
	"io"
	"math/rand"
	"os"
	"os/exec"
	"os/signal"
	"strconv"
	"syscall"
	"testing"
	"time"
)

type command struct {
	pipe io.WriteCloser
	proc *exec.Cmd
	test *testing.T
}

func (c *command) Info() (pid, pgrp int) {
	pid = c.proc.Process.Pid

	pgrp, err := syscall.Getpgid(pid)
	if err != nil {
		c.test.Fatal(err)
	}

	return
}

func (c *command) Start() {
	if err := c.proc.Start(); err != nil {
		c.test.Fatal(err)
	}
}

func (c *command) Stop() {
	c.pipe.Close()
	if err := c.proc.Wait(); err != nil {
		c.test.Fatal(err)
	}
}

func create(t *testing.T) *command {
	testenv.MustHaveExec(t)

	proc := exec.Command("cat")
	stdin, err := proc.StdinPipe()
	if err != nil {
		t.Fatal(err)
	}

	return &command{stdin, proc, t}
}

func parent() (pid, pgrp int) {
	return syscall.Getpid(), syscall.Getpgrp()
}

func TestZeroSysProcAttr(t *testing.T) {
	ppid, ppgrp := parent()

	cmd := create(t)

	cmd.Start()
	defer cmd.Stop()

	cpid, cpgrp := cmd.Info()

	if cpid == ppid {
		t.Fatalf("Parent and child have the same process ID")
	}

	if cpgrp != ppgrp {
		t.Fatalf("Child is not in parent's process group")
	}
}

func TestSetpgid(t *testing.T) {
	ppid, ppgrp := parent()

	cmd := create(t)

	cmd.proc.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	cmd.Start()
	defer cmd.Stop()

	cpid, cpgrp := cmd.Info()

	if cpid == ppid {
		t.Fatalf("Parent and child have the same process ID")
	}

	if cpgrp == ppgrp {
		t.Fatalf("Parent and child are in the same process group")
	}

	if cpid != cpgrp {
		t.Fatalf("Child's process group is not the child's process ID")
	}
}

func TestPgid(t *testing.T) {
	ppid, ppgrp := parent()

	cmd1 := create(t)

	cmd1.proc.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	cmd1.Start()
	defer cmd1.Stop()

	cpid1, cpgrp1 := cmd1.Info()

	if cpid1 == ppid {
		t.Fatalf("Parent and child 1 have the same process ID")
	}

	if cpgrp1 == ppgrp {
		t.Fatalf("Parent and child 1 are in the same process group")
	}

	if cpid1 != cpgrp1 {
		t.Fatalf("Child 1's process group is not its process ID")
	}

	cmd2 := create(t)

	cmd2.proc.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: true,
		Pgid:    cpgrp1,
	}
	cmd2.Start()
	defer cmd2.Stop()

	cpid2, cpgrp2 := cmd2.Info()

	if cpid2 == ppid {
		t.Fatalf("Parent and child 2 have the same process ID")
	}

	if cpgrp2 == ppgrp {
		t.Fatalf("Parent and child 2 are in the same process group")
	}

	if cpid2 == cpgrp2 {
		t.Fatalf("Child 2's process group is its process ID")
	}

	if cpid1 == cpid2 {
		t.Fatalf("Child 1 and 2 have the same process ID")
	}

	if cpgrp1 != cpgrp2 {
		t.Fatalf("Child 1 and 2 are not in the same process group")
	}
}

func TestForeground(t *testing.T) {
	signal.Ignore(syscall.SIGTTIN, syscall.SIGTTOU)
	defer signal.Reset()

	tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
	if err != nil {
		t.Skipf("Can't test Foreground. Couldn't open /dev/tty: %s", err)
	}
	defer tty.Close()

	ttyFD := int(tty.Fd())

	fpgrp, err := syscall.Tcgetpgrp(ttyFD)
	if err != nil {
		t.Fatalf("Tcgetpgrp failed: %v", err)
	}
	if fpgrp == 0 {
		t.Fatalf("Foreground process group is zero")
	}

	ppid, ppgrp := parent()

	cmd := create(t)

	cmd.proc.SysProcAttr = &syscall.SysProcAttr{
		Ctty:       ttyFD,
		Foreground: true,
	}
	cmd.Start()

	cpid, cpgrp := cmd.Info()

	if cpid == ppid {
		t.Fatalf("Parent and child have the same process ID")
	}

	if cpgrp == ppgrp {
		t.Fatalf("Parent and child are in the same process group")
	}

	if cpid != cpgrp {
		t.Fatalf("Child's process group is not the child's process ID")
	}

	cmd.Stop()

	// This call fails on darwin/arm64. The failure doesn't matter, though.
	// This is just best effort.
	syscall.Tcsetpgrp(ttyFD, fpgrp)
}

func TestForegroundSignal(t *testing.T) {
	tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
	if err != nil {
		t.Skipf("couldn't open /dev/tty: %s", err)
	}
	defer tty.Close()

	ttyFD := int(tty.Fd())

	fpgrp, err := syscall.Tcgetpgrp(ttyFD)
	if err != nil {
		t.Fatalf("Tcgetpgrp failed: %v", err)
	}
	if fpgrp == 0 {
		t.Fatalf("Foreground process group is zero")
	}

	defer func() {
		signal.Ignore(syscall.SIGTTIN, syscall.SIGTTOU)
		syscall.Tcsetpgrp(ttyFD, fpgrp)
		signal.Reset()
	}()

	ch1 := make(chan os.Signal, 1)
	ch2 := make(chan bool)

	signal.Notify(ch1, syscall.SIGTTIN, syscall.SIGTTOU)
	defer signal.Stop(ch1)

	cmd := create(t)

	go func() {
		cmd.proc.SysProcAttr = &syscall.SysProcAttr{
			Ctty:       ttyFD,
			Foreground: true,
		}
		cmd.Start()
		cmd.Stop()
		close(ch2)
	}()

	timer := time.NewTimer(30 * time.Second)
	defer timer.Stop()
	for {
		select {
		case sig := <-ch1:
			t.Errorf("unexpected signal %v", sig)
		case <-ch2:
			// Success.
			return
		case <-timer.C:
			t.Fatal("timed out waiting for child process")
		}
	}
}

// Test a couple of cases that SysProcAttr can't handle. Issue 29458.
func TestInvalidExec(t *testing.T) {
	t.Parallel()
	t.Run("SetCtty-Foreground", func(t *testing.T) {
		t.Parallel()
		cmd := create(t)
		cmd.proc.SysProcAttr = &syscall.SysProcAttr{
			Setctty:    true,
			Foreground: true,
			Ctty:       0,
		}
		if err := cmd.proc.Start(); err == nil {
			t.Error("expected error setting both SetCtty and Foreground")
		}
	})
	t.Run("invalid-Ctty", func(t *testing.T) {
		t.Parallel()
		cmd := create(t)
		cmd.proc.SysProcAttr = &syscall.SysProcAttr{
			Setctty: true,
			Ctty:    3,
		}
		if err := cmd.proc.Start(); err == nil {
			t.Error("expected error with invalid Ctty value")
		}
	})
}

// TestExec is for issue #41702.
func TestExec(t *testing.T) {
	cmd := exec.Command(testenv.Executable(t), "-test.run=^TestExecHelper$")
	cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=2")
	o, err := cmd.CombinedOutput()
	if err != nil {
		t.Errorf("%s\n%v", o, err)
	}
}

// TestExecHelper is used by TestExec. It does nothing by itself.
// In testing on macOS 10.14, this used to fail with
// "signal: illegal instruction" more than half the time.
func TestExecHelper(t *testing.T) {
	if os.Getenv("GO_WANT_HELPER_PROCESS") != "2" {
		return
	}

	// We don't have to worry about restoring these values.
	// We are in a child process that only runs this test,
	// and we are going to call syscall.Exec anyhow.
	os.Setenv("GO_WANT_HELPER_PROCESS", "3")

	stop := time.Now().Add(time.Second)
	for i := 0; i < 100; i++ {
		go func(i int) {
			r := rand.New(rand.NewSource(int64(i)))
			for time.Now().Before(stop) {
				r.Uint64()
			}
		}(i)
	}

	time.Sleep(10 * time.Millisecond)

	argv := []string{os.Args[0], "-test.run=^TestExecHelper$"}
	syscall.Exec(os.Args[0], argv, os.Environ())

	t.Error("syscall.Exec returned")
}

// Test that rlimit values are restored by exec.
func TestRlimitRestored(t *testing.T) {
	if os.Getenv("GO_WANT_HELPER_PROCESS") != "" {
		fmt.Println(syscall.OrigRlimitNofile().Cur)
		os.Exit(0)
	}

	orig := syscall.OrigRlimitNofile()
	if orig == nil {
		t.Skip("skipping test because rlimit not adjusted at startup")
	}

	exe := testenv.Executable(t)
	cmd := testenv.Command(t, exe, "-test.run=^TestRlimitRestored$")
	cmd = testenv.CleanCmdEnv(cmd)
	cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1")

	out, err := cmd.CombinedOutput()
	if len(out) > 0 {
		t.Logf("%s", out)
	}
	if err != nil {
		t.Fatalf("subprocess failed: %v", err)
	}
	s := string(bytes.TrimSpace(out))
	v, err := strconv.ParseUint(s, 10, 64)
	if err != nil {
		t.Fatalf("could not parse %q as number: %v", s, v)
	}

	if v != uint64(orig.Cur) {
		t.Errorf("exec rlimit = %d, want %d", v, orig)
	}
}

func TestForkExecNilArgv(t *testing.T) {
	defer func() {
		if p := recover(); p != nil {
			t.Fatal("forkExec panicked")
		}
	}()

	// We don't really care what the result of forkExec is, just that it doesn't
	// panic, so we choose something we know won't actually spawn a process (probably).
	syscall.ForkExec("/dev/null", nil, nil)
}
