diff --git a/.gitignore b/.gitignore index 36946b0..af17a25 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -coverage.txt \ No newline at end of file +coverage.txt +testdata/stubcmd \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 4d41f3b..47b41b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,19 +3,43 @@ matrix: include: - go: 1.12.x os: linux + env: GO111MODULE=on + after_success: + - codecov - go: master os: linux + env: GO111MODULE=on - go: 1.12.x os: osx + env: GO111MODULE=on - go: master os: osx + env: GO111MODULE=on + - go: 1.12.x + os: windows + env: + - GO111MODULE=on + before_install: + - echo $TRAVIS_GO_VERSION + - echo $TERM + script: + - go test -v + - go: master + os: windows + env: + - GO111MODULE=on + before_install: + - echo $TRAVIS_GO_VERSION + - echo $TERM + script: + - go test -v + after_script: + - ps aux allow_failures: - go: master -env: GO111MODULE=on -install: +before_install: - echo $TRAVIS_GO_VERSION + - echo $TERM - sudo pip install codecov script: - make ci -after_script: - - codecov diff --git a/Makefile b/Makefile index ad000cf..4b5f6c0 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ default: test ci: depsdev test sec test: - go test ./... -coverprofile=coverage.txt -covermode=count + go test -v ./... -coverprofile=coverage.txt -covermode=count sec: gosec ./... diff --git a/exec_test.go b/exec_test.go index bdcc251..3efb09d 100644 --- a/exec_test.go +++ b/exec_test.go @@ -4,15 +4,33 @@ import ( "bytes" "context" "fmt" - "math/rand" + "os" "os/exec" - "strconv" + "runtime" "strings" "syscall" "testing" "time" ) +var ( + shellcmd = `/bin/sh` + shellflag = "-c" + stubCmd = `./testdata/stubcmd` +) + +func init() { + if runtime.GOOS == "windows" { + shellcmd = "cmd" + shellflag = "/c" + stubCmd = `.\testdata\stubcmd.exe` + } + err := exec.Command("go", "build", "-o", stubCmd, "testdata/stubcmd.go").Run() + if err != nil { + panic(err) + } +} + func TestCommand(t *testing.T) { tests := gentests(false) for _, tt := range tests { @@ -25,14 +43,13 @@ func TestCommand(t *testing.T) { cmd.Stderr = &stderr err := cmd.Run() if err != nil { - t.Fatalf("%v", err) + t.Errorf("%v: %v", tt.cmd, err) } - if strings.TrimSuffix(stdout.String(), "\n") != tt.want { - t.Errorf("%s: want = %#v, got = %#v", tt.name, tt.want, stdout.String()) + if strings.TrimRight(stdout.String(), "\n\r") != tt.want { + t.Errorf("%v: want = %#v, got = %#v", tt.cmd, tt.want, stdout.String()) } - _, err = exec.Command("bash", "-c", fmt.Sprintf("ps aux | grep %s | grep -v grep", tt.want)).Output() - if err == nil { - t.Errorf("%s", "the process has not exited") + if checkprocess() { + t.Errorf("%v: %s", tt.cmd, "the process has not exited") } } } @@ -50,14 +67,13 @@ func TestCommandContext(t *testing.T) { cmd.Stderr = &stderr err := cmd.Run() if err != nil { - t.Fatalf("%s: %v", tt.name, err) + t.Fatalf("%v: %v", tt.cmd, err) } - if strings.TrimSuffix(stdout.String(), "\n") != tt.want { - t.Errorf("%s: want = %#v, got = %#v", tt.name, tt.want, stdout.String()) + if strings.TrimRight(stdout.String(), "\n\r") != tt.want { + t.Errorf("%v: want = %#v, got = %#v", tt.cmd, tt.want, stdout.String()) } - _, err = exec.Command("bash", "-c", fmt.Sprintf("ps aux | grep %s | grep -v grep", tt.want)).Output() - if err == nil { - t.Errorf("%s", "the process has not exited") + if checkprocess() { + t.Errorf("%v: %s", tt.cmd, "the process has not exited") } } } @@ -76,16 +92,17 @@ func TestCommandContextCancel(t *testing.T) { cmd.Stderr = &stderr err := cmd.Start() if err != nil { - t.Fatalf("%v", err) + t.Fatalf("%v: %v", tt.cmd, err) } go func() { cmd.Wait() }() - time.Sleep(100 * time.Millisecond) + if !checkprocess() && !tt.processFinished { + t.Fatalf("%v: %s", tt.cmd, "the process has been exited") + } cancel() - _, err = exec.Command("bash", "-c", fmt.Sprintf("ps aux | grep %s | grep -v grep", tt.want)).Output() - if err == nil { - t.Errorf("%s", "the process has not exited") + if checkprocess() { + t.Errorf("%v: %s", tt.cmd, "the process has not exited") } } } @@ -102,19 +119,26 @@ func TestTerminateCommand(t *testing.T) { cmd.Stderr = &stderr err := cmd.Start() if err != nil { - t.Fatalf("%v", err) + t.Fatalf("%v: %v", tt.cmd, err) } go func() { cmd.Wait() }() - time.Sleep(100 * time.Millisecond) - err = TerminateCommand(cmd, syscall.SIGTERM) + if !checkprocess() && !tt.processFinished { + t.Fatalf("%v: %s", tt.cmd, "the process has been exited") + } + if runtime.GOOS == "windows" { + sig := os.Interrupt + err = TerminateCommand(cmd, sig) + } else { + sig := syscall.SIGTERM + err = TerminateCommand(cmd, sig) + } if err != nil && !tt.processFinished { - t.Fatalf("%s: %v", tt.name, err) + t.Errorf("%v: %v", tt.cmd, err) } - _, err = exec.Command("bash", "-c", fmt.Sprintf("ps aux | grep %s | grep -v grep", tt.want)).Output() - if err == nil { - t.Errorf("%s", "the process has not exited") + if checkprocess() { + t.Errorf("%v: %s", tt.cmd, "the process has not exited") } } } @@ -131,19 +155,20 @@ func TestKillCommand(t *testing.T) { cmd.Stderr = &stderr err := cmd.Start() if err != nil { - t.Fatalf("%v", err) + t.Fatalf("%v: %v", tt.cmd, err) } go func() { cmd.Wait() }() - time.Sleep(100 * time.Millisecond) + if !checkprocess() && !tt.processFinished { + t.Fatalf("%v: %s", tt.cmd, "the process has been exited") + } err = KillCommand(cmd) if err != nil && !tt.processFinished { - t.Fatalf("%s: %v", tt.name, err) + t.Fatalf("%v: %v", tt.cmd, err) } - _, err = exec.Command("bash", "-c", fmt.Sprintf("ps aux | grep %s | grep -v grep", tt.want)).Output() - if err == nil { - t.Errorf("%s", "the process has not exited") + if checkprocess() { + t.Errorf("%v: %s", tt.cmd, "the process has not exited") } } } @@ -155,22 +180,27 @@ type testcase struct { processFinished bool } +func checkprocess() bool { + time.Sleep(500 * time.Millisecond) + var ( + out []byte + err error + ) + if runtime.GOOS == "windows" { + out, err = exec.Command("cmd", "/c", "tasklist | grep stubcmd.exe | grep -v grep").Output() + } else { + out, err = exec.Command("bash", "-c", "ps aux | grep stubcmd | grep -v grep").Output() + } + return (err == nil || strings.TrimRight(string(out), "\n\r") != "") +} + func gentests(withSleepTest bool) []testcase { tests := []testcase{} - r := random() - tests = append(tests, testcase{"echo", []string{"echo", r}, r, true}) - r = random() - tests = append(tests, testcase{"bash -c echo", []string{"bash", "-c", fmt.Sprintf("echo %s", r)}, r, true}) + tests = append(tests, testcase{"echo", []string{stubCmd, "-echo", "!!!"}, "!!!", true}) + tests = append(tests, testcase{"sh -c echo", []string{shellcmd, shellflag, fmt.Sprintf("%s -echo %s", stubCmd, "!!!")}, "!!!", true}) if withSleepTest { - r = "123456" - tests = append(tests, testcase{"sleep", []string{"sleep", r}, r, false}) - r = "654321" - tests = append(tests, testcase{"bash -c sleep", []string{"bash", "-c", fmt.Sprintf("sleep %s && echo %s", r, r)}, r, false}) + tests = append(tests, testcase{"sleep", []string{stubCmd, "-sleep", "10", "-echo", "!!!"}, "!!!", false}) + tests = append(tests, testcase{"sh -c sleep", []string{shellcmd, shellflag, fmt.Sprintf("%s -sleep %s -echo %s", stubCmd, "10", "!!!")}, "!!!", false}) } return tests } - -func random() string { - rand.Seed(time.Now().UnixNano()) - return strconv.Itoa(rand.Int()) -} diff --git a/exec_unix.go b/exec_unix.go index 0af7cb5..e044a5b 100644 --- a/exec_unix.go +++ b/exec_unix.go @@ -1,3 +1,5 @@ +// +build !windows + package exec import ( @@ -30,7 +32,7 @@ func terminate(cmd *exec.Cmd, sig os.Signal) error { return syscall.Kill(cmd.Process.Pid, syssig) // fallback } if syssig != syscall.SIGKILL && syssig != syscall.SIGCONT { - return syscall.Kill(-cmd.Process.Pid, syscall.SIGCONT) + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGCONT) } return nil } diff --git a/exec_windows.go b/exec_windows.go new file mode 100644 index 0000000..41c4845 --- /dev/null +++ b/exec_windows.go @@ -0,0 +1,71 @@ +package exec + +import ( + "errors" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "syscall" +) + +// MEMO: Sending Interrupt on Windows is not implemented. +var defaultSignal = os.Interrupt + +// Reference code: +// https://github.com/Songmu/timeout/blob/517fff103abc7d0e88a663609515d8bb55f6535d/timeout_windows.go +func command(name string, arg ...string) *exec.Cmd { + // #nosec + cmd := exec.Command(name, arg...) + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + cmd.SysProcAttr.CreationFlags = syscall.CREATE_UNICODE_ENVIRONMENT | 0x00000200 + return cmd +} + +func terminate(cmd *exec.Cmd, sig os.Signal) error { + if os.Getenv("TERM") == "cygwin" { + return killall(cmd) // fallback + } + if err := cmd.Process.Signal(sig); err != nil { + return killall(cmd) // fallback + } + return nil +} + +func killall(cmd *exec.Cmd) error { + var err error + wpid := cmd.Process.Pid + if os.Getenv("TERM") == "cygwin" { + wpid, err = winpid(cmd.Process.Pid) + if err != nil { + return err + } + } + + return exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(wpid)).Run() + // return psutil.TerminateTree(cmd.Process.Pid, 0) +} + +// winpid convert cygwin pid -> windows pid +func winpid(pid int) (int, error) { + winpidCmd := exec.Command("cat", fmt.Sprintf("/proc/%d/winpid", pid)) + out, err := winpidCmd.Output() + if err != nil { + out, err = exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid)).Output() + if err != nil { + return pid, err + } + if !strings.Contains(string(out), strconv.Itoa(pid)) { + return pid, errors.New("process does not exist") + } + return pid, nil + } + winpid, err := strconv.Atoi(strings.TrimRight(string(out), "\n\r")) + if err != nil { + return pid, err + } + return winpid, nil +} diff --git a/testdata/stubcmd.go b/testdata/stubcmd.go new file mode 100644 index 0000000..05ebe37 --- /dev/null +++ b/testdata/stubcmd.go @@ -0,0 +1,23 @@ +package main + +import ( + "flag" + "fmt" + "time" +) + +func main() { + var ( + sleep = flag.Int("sleep", 0, "sleep sec") + echo = flag.String("echo", "", "echo string") + ) + flag.Parse() + + if *sleep > 0 { + time.Sleep(time.Duration(*sleep) * time.Second) + } + + if *echo != "" { + fmt.Printf("%s\n", *echo) + } +}