// Copyright 2017 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.

// +build !windows

package gps

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"syscall"
	"time"

	"github.com/pkg/errors"
	"golang.org/x/sys/unix"
)

type cmd struct {
	// ctx is provided by the caller; SIGINT is sent when it is cancelled.
	ctx context.Context
	Cmd *exec.Cmd
}

func commandContext(ctx context.Context, name string, arg ...string) cmd {
	c := exec.Command(name, arg...)

	// Force subprocesses into their own process group, rather than being in the
	// same process group as the dep process. Because Ctrl-C sent from a
	// terminal will send the signal to the entire currently running process
	// group, this allows us to directly manage the issuance of signals to
	// subprocesses.
	c.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: true,
		Pgid:    0,
	}

	return cmd{ctx: ctx, Cmd: c}
}

// CombinedOutput is like (*os/exec.Cmd).CombinedOutput except that it
// terminates subprocesses gently (via os.Interrupt), but resorts to Kill if
// the subprocess fails to exit after 1 minute.
func (c cmd) CombinedOutput() ([]byte, error) {
	// Adapted from (*os/exec.Cmd).CombinedOutput
	if c.Cmd.Stdout != nil {
		return nil, errors.New("exec: Stdout already set")
	}
	if c.Cmd.Stderr != nil {
		return nil, errors.New("exec: Stderr already set")
	}
	var b bytes.Buffer
	c.Cmd.Stdout = &b
	c.Cmd.Stderr = &b
	if err := c.Cmd.Start(); err != nil {
		return nil, err
	}

	// Adapted from (*os/exec.Cmd).Start
	waitDone := make(chan struct{})
	defer close(waitDone)
	go func() {
		select {
		case <-c.ctx.Done():
			if err := c.Cmd.Process.Signal(os.Interrupt); err != nil {
				// If an error comes back from attempting to signal, proceed
				// immediately to hard kill.
				_ = unix.Kill(-c.Cmd.Process.Pid, syscall.SIGKILL)
			} else {
				defer time.AfterFunc(time.Minute, func() {
					_ = unix.Kill(-c.Cmd.Process.Pid, syscall.SIGKILL)
				}).Stop()
				<-waitDone
			}
		case <-waitDone:
		}
	}()

	err := c.Cmd.Wait()
	return b.Bytes(), err
}
