gotty/backend/localcommand/local_command.go

136 lines
2.8 KiB
Go

package localcommand
import (
"os"
"os/exec"
"strings"
"syscall"
"time"
"github.com/creack/pty"
"github.com/pkg/errors"
)
const (
DefaultCloseSignal = syscall.SIGINT
DefaultCloseTimeout = 10 * time.Second
)
type LocalCommand struct {
command string
argv []string
closeSignal syscall.Signal
closeTimeout time.Duration
cmd *exec.Cmd
pty *os.File
ptyClosed chan struct{}
}
func New(command string, argv []string, headers map[string][]string, options ...Option) (*LocalCommand, error) {
cmd := exec.Command(command, argv...)
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
// Combine headers into key=value pairs to set as env vars
// Prefix the headers with "http_" so we don't overwrite any other env vars
// which potentially has the same name and to bring these closer to what
// a (F)CGI server would proxy to a backend service
// Replace hyphen with underscore and make them all upper case
for key, values := range headers {
h := "HTTP_" + strings.Replace(strings.ToUpper(key), "-", "_", -1) + "=" + strings.Join(values, ",")
// log.Printf("Adding header: %s", h)
cmd.Env = append(cmd.Env, h)
}
pty, err := pty.Start(cmd)
if err != nil {
// todo close cmd?
return nil, errors.Wrapf(err, "failed to start command `%s`", command)
}
ptyClosed := make(chan struct{})
lcmd := &LocalCommand{
command: command,
argv: argv,
closeSignal: DefaultCloseSignal,
closeTimeout: DefaultCloseTimeout,
cmd: cmd,
pty: pty,
ptyClosed: ptyClosed,
}
for _, option := range options {
option(lcmd)
}
// When the process is closed by the user,
// close pty so that Read() on the pty breaks with an EOF.
go func() {
defer func() {
lcmd.pty.Close()
close(lcmd.ptyClosed)
}()
lcmd.cmd.Wait()
}()
return lcmd, nil
}
func (lcmd *LocalCommand) Read(p []byte) (n int, err error) {
return lcmd.pty.Read(p)
}
func (lcmd *LocalCommand) Write(p []byte) (n int, err error) {
return lcmd.pty.Write(p)
}
func (lcmd *LocalCommand) Close() error {
if lcmd.cmd != nil && lcmd.cmd.Process != nil {
lcmd.cmd.Process.Signal(lcmd.closeSignal)
}
for {
select {
case <-lcmd.ptyClosed:
return nil
case <-lcmd.closeTimeoutC():
lcmd.cmd.Process.Signal(syscall.SIGKILL)
}
}
}
func (lcmd *LocalCommand) WindowTitleVariables() map[string]interface{} {
return map[string]interface{}{
"command": lcmd.command,
"argv": lcmd.argv,
"pid": lcmd.cmd.Process.Pid,
}
}
func (lcmd *LocalCommand) ResizeTerminal(width int, height int) error {
window := pty.Winsize{
Rows: uint16(height),
Cols: uint16(width),
X: 0,
Y: 0,
}
err := pty.Setsize(lcmd.pty, &window)
if err != nil {
return err
} else {
return nil
}
}
func (lcmd *LocalCommand) closeTimeoutC() <-chan time.Time {
if lcmd.closeTimeout >= 0 {
return time.After(lcmd.closeTimeout)
}
return make(chan time.Time)
}