Current section

Files

Jump to
ex_cmd go_src executor.go
Raw

go_src/executor.go

package main
import (
"encoding/binary"
"errors"
"io"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
const SendInput = 1
const SendOutput = 2
const Output = 3
const Input = 4
const CloseInput = 5
const OutputEOF = 6
const CommandEnv = 7
const Pid = 8
const StartError = 9
const CloseOutput = 10
const Kill = 11
const ExitStatus = 12
// This size is *NOT* related to pipe buffer size
// 4 bytes for payload length + 1 byte for tag
const BufferSize = (1 << 16) - 5
// fixed buffer for IO
var buf = make([]byte, BufferSize+5)
type Packet struct {
tag uint8
data []byte
}
type InputDispatcher func(Packet)
type OutPacket func() (Packet, bool)
func execute(workdir string, args []string, stderrConfig string) error {
writerDone := make(chan struct{})
// must be buffered so that function can close without blocking
stdinClose := make(chan struct{}, 1)
kill := make(chan bool, 1)
sigs := make(chan os.Signal)
// Capture common signals.
// Setting notify for SIGPIPE is important to capture and without that
// we won't be able to handle abrupt beam vm terminations
// Also, SIGPIPE behavior in golang is complex,
//
// see: https://pkg.go.dev/os/signal@go1.22.4#hdr-SIGPIPE
signal.Notify(sigs, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGPIPE)
proc := exec.Command(args[0], args[1:]...)
proc.Dir = workdir
proc.Env = append(os.Environ(), readEnvFromStdin()...)
runPipeline(proc, writerDone, stdinClose, kill, stderrConfig)
err := waitPipelineTermination(proc, sigs, stdinClose, writerDone, kill)
if err == nil {
writeCmdExitCode(0)
return nil
}
if exitError, ok := err.(*exec.ExitError); ok {
writeCmdExitCode(exitError.ExitCode())
return nil
} else {
// unknown error
writeCmdExitCode(-2)
return err
}
}
func runPipeline(proc *exec.Cmd, writerDone chan struct{}, stdinClose chan struct{}, kill chan<- bool, stderrConfig string) {
cmdInput := make(chan []byte, 1)
cmdOutputDemand := make(chan Packet)
cmdInputDemand := make(chan Packet)
cmdOutput := startCommandPipeline(proc, cmdInput, cmdInputDemand, cmdOutputDemand, stderrConfig)
// go handleSignals(input, outputDemand, done)
go stdinReader(cmdInput, cmdOutputDemand, writerDone, stdinClose, kill)
go stdoutWriter(proc.Process.Pid, cmdOutput, cmdInputDemand, writerDone)
}
func stdinReader(cmdInput chan<- []byte, cmdOutputDemand chan<- Packet, writerDone <-chan struct{}, stdinClose chan<- struct{}, kill chan<- bool) {
// closeChan := closeInputHandler(input)
cmdInputClosed := false
killCommand := false
defer func() {
if !cmdInputClosed {
close(cmdInput)
}
close(cmdOutputDemand)
}()
for {
select {
case <-writerDone:
return
default:
}
packet, readErr := readPacketFromStdin()
if readErr == io.EOF {
close(stdinClose)
return
} else if readErr != nil {
fatal(readErr)
}
switch packet.tag {
case Kill:
if !killCommand {
kill <- true
killCommand = true
}
case CloseInput:
if !cmdInputClosed {
close(cmdInput)
cmdInputClosed = true
} else {
logger.Printf("close on closed command input")
}
case SendOutput:
cmdOutputDemand <- packet
case CloseOutput:
cmdOutputDemand <- packet
case Input:
if cmdInputClosed {
fatal("trying to send input on closed command input stream")
}
cmdInput <- packet.data
}
}
}
func stdoutWriter(pid int, cmdStdout <-chan []byte, cmdInputDemand <-chan Packet, writerDone chan<- struct{}) {
var ok bool
var packet Packet
var buf [4]byte
var data []byte
cmdOutputClosed := false
defer func() {
if !cmdOutputClosed {
writePacketToStdout(OutputEOF, make([]byte, 0))
}
logger.Printf("writerDone")
close(writerDone)
}()
// we first write pid before writing anything
writeUint32Be(buf[:], uint32(pid))
writePacketToStdout(Pid, buf[:])
for {
select {
case packet, ok = <-cmdInputDemand:
if !ok {
return
}
writePacketToStdout(packet.tag, packet.data)
case data, ok = <-cmdStdout:
if cmdOutputClosed == true {
fatal("data on closed cmdOutput stream")
}
if !ok {
fatal("error on cmdStdout")
}
if len(data) > BufferSize {
fatal("Invalid payloadLen")
} else if len(data) == 0 {
// if we are getting EOF then we are done here
// there won't be anymore command coming
writePacketToStdout(OutputEOF, make([]byte, 0))
cmdOutputClosed = true
return
} else {
writePacketToStdout(Output, data)
}
}
}
}
func waitPipelineTermination(proc *exec.Cmd, sigs <-chan os.Signal, stdinClose <-chan struct{}, writerDone <-chan struct{}, kill <-chan bool) error {
timeout := 1 * time.Second
select {
case sig := <-sigs:
logger.Printf("Received OS Signal: %v\n", sig)
case <-stdinClose:
// When stdin closes it imply that VM is down or process GenServer
// is killed so we must prepare for termination.
//
// Not that stdin close for middleware is different from stdin close
// for the external program (CloseInput)
case <-writerDone:
case <-kill:
timeout = 0
}
cmdExit := make(chan error)
go func() {
cmdExit <- proc.Wait()
}()
return safeExit(proc, cmdExit, kill, timeout)
}
func safeExit(proc *exec.Cmd, procErr <-chan error, kill <-chan bool, timeout time.Duration) error {
logger.Printf("Attempt graceful exit\n")
select {
case err := <-procErr:
logger.Printf("Cmd completed with err: %v", err)
return err
case <-kill:
if err := proc.Process.Kill(); err != nil {
logger.Fatal("failed to kill process: ", err)
return err
}
logger.Println("process killed by user signal")
select {
case err := <-procErr:
return err
default:
return errors.New("Killed")
}
case <-time.After(timeout):
if err := proc.Process.Kill(); err != nil {
logger.Fatal("failed to kill process: ", err)
return err
}
logger.Println("process killed as exit timeout reached")
select {
case err := <-procErr:
return err
default:
return errors.New("Killed")
}
}
}
func writeStartError(reason string) {
writePacketToStdout(StartError, []byte(reason))
}
func writeCmdExitCode(code int) {
exitCode := uint32(code)
logger.Printf("Command exited with exit status: %v", exitCode)
binExitCode := make([]byte, 4)
binary.BigEndian.PutUint32(binExitCode, exitCode)
writePacketToStdout(ExitStatus, binExitCode)
}
func readEnvFromStdin() []string {
// first packet must be env
packet, err := readPacketFromStdin()
if err != nil {
fatal(err)
}
if packet.tag != CommandEnv {
fatal("First packet must be command Env")
}
var env []string
var length int
data := packet.data
for i := 0; i < len(data); {
length = int(binary.BigEndian.Uint16(data[i : i+2]))
i += 2
entry := string(data[i : i+length])
env = append(env, entry)
i += length
}
logger.Printf("Command Env: %v\n", env)
return env
}
func readPacketFromStdin() (Packet, error) {
var readErr error
var length uint32
var tag uint8
buf := make([]byte, BufferSize)
length, readErr = readUint32(os.Stdin)
if readErr == io.EOF {
return Packet{}, io.EOF
} else if readErr != nil {
return Packet{}, readErr
}
dataLen := length - 1
if dataLen < 0 || dataLen > BufferSize { // payload must be atleast tag size
return Packet{}, errors.New("input payload size is invalid")
}
tag, readErr = readUint8(os.Stdin)
if readErr != nil {
return Packet{}, readErr
}
_, readErr = io.ReadFull(os.Stdin, buf[:dataLen])
if readErr != nil {
return Packet{}, readErr
}
return Packet{tag, buf[:dataLen]}, nil
}
func writePacketToStdout(tag uint8, data []byte) {
payloadLen := len(data) + 1
writeUint32Be(buf[:4], uint32(payloadLen))
writeUint8Be(buf[4:5], tag)
copy(buf[5:], data)
_, writeErr := os.Stdout.Write(buf[:payloadLen+4])
if writeErr != nil {
switch writeErr.(type) {
// ignore broken pipe or closed pipe errors here.
// currently readCommandStdout closes output chan, making the
// flow break.
case *os.PathError:
logger.Printf("os.PathError: %v\n", writeErr)
return
default:
fatal(writeErr)
}
}
// logger.Printf("stdout written bytes: %v\n", bytesWritten)
}
func readUint32(stdin io.Reader) (uint32, error) {
var buf [4]byte
bytesRead, readErr := io.ReadFull(stdin, buf[:])
if readErr != nil {
return 0, io.EOF
} else if bytesRead == 0 {
return 0, readErr
}
return binary.BigEndian.Uint32(buf[:]), nil
}
func readUint8(stdin io.Reader) (uint8, error) {
var buf [1]byte
bytesRead, readErr := io.ReadFull(stdin, buf[:])
if readErr != nil {
return 0, io.EOF
} else if bytesRead == 0 {
return 0, readErr
}
return uint8(buf[0]), nil
}
func writeUint32Be(data []byte, num uint32) {
binary.BigEndian.PutUint32(data, num)
}
func writeUint8Be(data []byte, num uint8) {
data[0] = byte(num)
}