Skip to content

Commit

Permalink
Merge pull request #2 from k1LoW/windows
Browse files Browse the repository at this point in the history
Support Windows
  • Loading branch information
k1LoW authored Jul 28, 2019
2 parents 5d29bb1 + a9e2b6b commit e1fcc6c
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 52 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
coverage.txt
coverage.txt
testdata/stubcmd
32 changes: 28 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./...
Expand Down
120 changes: 75 additions & 45 deletions exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
}
}
}
Expand All @@ -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")
}
}
}
Expand All @@ -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")
}
}
}
Expand All @@ -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")
}
}
}
Expand All @@ -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")
}
}
}
Expand All @@ -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())
}
4 changes: 3 additions & 1 deletion exec_unix.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !windows

package exec

import (
Expand Down Expand Up @@ -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
}
Expand Down
71 changes: 71 additions & 0 deletions exec_windows.go
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 23 additions & 0 deletions testdata/stubcmd.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit e1fcc6c

Please sign in to comment.