Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
language: go
go:
- 1.7
- 1.8
- tip
script:
- make lint
Expand Down
10 changes: 10 additions & 0 deletions testdata/ignore_sigterm_with_exit3.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env perl
use strict;
use warnings;

$SIG{TERM} = sub {
exit 3
};
sleep 3;


60 changes: 37 additions & 23 deletions timeout.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"os"
"os/exec"
"reflect"
"runtime"

"syscall"
Expand Down Expand Up @@ -55,8 +56,9 @@ func (err *Error) Error() string {

// ExitStatus stores exit information of the command
type ExitStatus struct {
Code int
typ exitType
Code int
Signaled bool
typ exitType
}

// IsTimedOut returns the command timed out or not
Expand Down Expand Up @@ -90,7 +92,7 @@ type exitType int

// exit types
const (
exitTypeNormal exitType = iota + 1
exitTypeNormal exitType = iota
exitTypeTimedOut
exitTypeKilled
)
Expand Down Expand Up @@ -188,38 +190,50 @@ func (tio *Timeout) RunCommand() (chan ExitStatus, error) {
func (tio *Timeout) handleTimeout() (ex ExitStatus) {
cmd := tio.getCmd()
exitChan := getExitChan(cmd)
select {
case exitCode := <-exitChan:
ex.Code = exitCode
ex.typ = exitTypeNormal
return ex
case <-time.After(tio.Duration):
tio.terminate()
ex.typ = exitTypeTimedOut
cases := []reflect.SelectCase{
{ // 0: command exit
Chan: reflect.ValueOf(exitChan),
Dir: reflect.SelectRecv,
},
{ // 1: timed out and send signal
Chan: reflect.ValueOf(time.After(tio.Duration)),
Dir: reflect.SelectRecv,
},
}

if tio.KillAfter > 0 {
select {
case ex.Code = <-exitChan:
case <-time.After(tio.KillAfter):
// 2: send KILL signal
cases = append(cases, reflect.SelectCase{
Chan: reflect.ValueOf(time.After(tio.Duration + tio.KillAfter)),
Dir: reflect.SelectRecv,
})
}
for {
chosen, recv, _ := reflect.Select(cases)
switch chosen {
case 0:
if st, ok := recv.Interface().(syscall.WaitStatus); ok {
ex.Code = wrapcommander.WaitStatusToExitCode(st)
ex.Signaled = st.Signaled()
}
return ex
case 1:
tio.terminate()
ex.typ = exitTypeTimedOut
case 2:
tio.killall()
// just to make sure
cmd.Process.Kill()
ex.Code = exitKilled
ex.typ = exitTypeKilled
}
} else {
ex.Code = <-exitChan
}

return ex
}

func getExitChan(cmd *exec.Cmd) chan int {
ch := make(chan int)
func getExitChan(cmd *exec.Cmd) chan syscall.WaitStatus {
ch := make(chan syscall.WaitStatus)
go func() {
err := cmd.Wait()
ch <- wrapcommander.ResolveExitCode(err)
st, _ := wrapcommander.ErrorToWaitStatus(err)
ch <- st
}()
return ch
}
18 changes: 17 additions & 1 deletion timeout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func TestKillAfter(t *testing.T) {
Duration: 1 * time.Second,
KillAfter: 1 * time.Second,
}
exit := tio.RunSimple(true)
exit := tio.RunSimple(false)

if exit != 137 {
t.Errorf("something wrong: %v", exit)
Expand All @@ -110,6 +110,22 @@ func TestKillAfterNotKilled(t *testing.T) {
}
}

func TestIgnoreSignal(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skipf("skip test on Windows")
}
tio := &Timeout{
Cmd: exec.Command("perl", "testdata/ignore_sigterm_with_exit3.pl"),
Signal: syscall.SIGTERM,
Duration: 1 * time.Second,
}
exit := tio.RunSimple(true)

if exit != 3 {
t.Errorf("something wrong: %v", exit)
}
}

func TestCommandCannotBeInvoked(t *testing.T) {
if runtime.GOOS == "windows" {
// TODO cmd return 125 for this case
Expand Down