Skip to content

Commit d20ccd1

Browse files
author
ymqytw
committed
cp exec util from kubernetes/kubernetes
1 parent 18ef3a1 commit d20ccd1

File tree

4 files changed

+466
-0
lines changed

4 files changed

+466
-0
lines changed

exec/doc.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
Copyright 2017 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package exec provides an injectable interface and implementations for running commands.
18+
package exec // import "k8s.io/utils/exec"

exec/exec.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
Copyright 2017 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package exec
18+
19+
import (
20+
"io"
21+
osexec "os/exec"
22+
"syscall"
23+
"time"
24+
)
25+
26+
// ErrExecutableNotFound is returned if the executable is not found.
27+
var ErrExecutableNotFound = osexec.ErrNotFound
28+
29+
// Interface is an interface that presents a subset of the os/exec API. Use this
30+
// when you want to inject fakeable/mockable exec behavior.
31+
type Interface interface {
32+
// Command returns a Cmd instance which can be used to run a single command.
33+
// This follows the pattern of package os/exec.
34+
Command(cmd string, args ...string) Cmd
35+
36+
// LookPath wraps os/exec.LookPath
37+
LookPath(file string) (string, error)
38+
}
39+
40+
// Cmd is an interface that presents an API that is very similar to Cmd from os/exec.
41+
// As more functionality is needed, this can grow. Since Cmd is a struct, we will have
42+
// to replace fields with get/set method pairs.
43+
type Cmd interface {
44+
// Run runs the command to the completion.
45+
Run() error
46+
// CombinedOutput runs the command and returns its combined standard output
47+
// and standard error. This follows the pattern of package os/exec.
48+
CombinedOutput() ([]byte, error)
49+
// Output runs the command and returns standard output, but not standard err
50+
Output() ([]byte, error)
51+
SetDir(dir string)
52+
SetStdin(in io.Reader)
53+
SetStdout(out io.Writer)
54+
SetStderr(out io.Writer)
55+
// Stops the command by sending SIGTERM. It is not guaranteed the
56+
// process will stop before this function returns. If the process is not
57+
// responding, an internal timer function will send a SIGKILL to force
58+
// terminate after 10 seconds.
59+
Stop()
60+
}
61+
62+
// ExitError is an interface that presents an API similar to os.ProcessState, which is
63+
// what ExitError from os/exec is. This is designed to make testing a bit easier and
64+
// probably loses some of the cross-platform properties of the underlying library.
65+
type ExitError interface {
66+
String() string
67+
Error() string
68+
Exited() bool
69+
ExitStatus() int
70+
}
71+
72+
// Implements Interface in terms of really exec()ing.
73+
type executor struct{}
74+
75+
// New returns a new Interface which will os/exec to run commands.
76+
func New() Interface {
77+
return &executor{}
78+
}
79+
80+
// Command is part of the Interface interface.
81+
func (executor *executor) Command(cmd string, args ...string) Cmd {
82+
return (*cmdWrapper)(osexec.Command(cmd, args...))
83+
}
84+
85+
// LookPath is part of the Interface interface
86+
func (executor *executor) LookPath(file string) (string, error) {
87+
return osexec.LookPath(file)
88+
}
89+
90+
// Wraps exec.Cmd so we can capture errors.
91+
type cmdWrapper osexec.Cmd
92+
93+
func (cmd *cmdWrapper) SetDir(dir string) {
94+
cmd.Dir = dir
95+
}
96+
97+
func (cmd *cmdWrapper) SetStdin(in io.Reader) {
98+
cmd.Stdin = in
99+
}
100+
101+
func (cmd *cmdWrapper) SetStdout(out io.Writer) {
102+
cmd.Stdout = out
103+
}
104+
105+
func (cmd *cmdWrapper) SetStderr(out io.Writer) {
106+
cmd.Stderr = out
107+
}
108+
109+
// Run is part of the Cmd interface.
110+
func (cmd *cmdWrapper) Run() error {
111+
return (*osexec.Cmd)(cmd).Run()
112+
}
113+
114+
// CombinedOutput is part of the Cmd interface.
115+
func (cmd *cmdWrapper) CombinedOutput() ([]byte, error) {
116+
out, err := (*osexec.Cmd)(cmd).CombinedOutput()
117+
if err != nil {
118+
return out, handleError(err)
119+
}
120+
return out, nil
121+
}
122+
123+
func (cmd *cmdWrapper) Output() ([]byte, error) {
124+
out, err := (*osexec.Cmd)(cmd).Output()
125+
if err != nil {
126+
return out, handleError(err)
127+
}
128+
return out, nil
129+
}
130+
131+
// Stop is part of the Cmd interface.
132+
func (cmd *cmdWrapper) Stop() {
133+
c := (*osexec.Cmd)(cmd)
134+
if c.ProcessState.Exited() {
135+
return
136+
}
137+
c.Process.Signal(syscall.SIGTERM)
138+
time.AfterFunc(10*time.Second, func() {
139+
if c.ProcessState.Exited() {
140+
return
141+
}
142+
c.Process.Signal(syscall.SIGKILL)
143+
})
144+
}
145+
146+
func handleError(err error) error {
147+
if ee, ok := err.(*osexec.ExitError); ok {
148+
// Force a compile fail if exitErrorWrapper can't convert to ExitError.
149+
var x ExitError = &ExitErrorWrapper{ee}
150+
return x
151+
}
152+
if ee, ok := err.(*osexec.Error); ok {
153+
if ee.Err == osexec.ErrNotFound {
154+
return ErrExecutableNotFound
155+
}
156+
}
157+
return err
158+
}
159+
160+
// ExitErrorWrapper is an implementation of ExitError in terms of os/exec ExitError.
161+
// Note: standard exec.ExitError is type *os.ProcessState, which already implements Exited().
162+
type ExitErrorWrapper struct {
163+
*osexec.ExitError
164+
}
165+
166+
var _ ExitError = ExitErrorWrapper{}
167+
168+
// ExitStatus is part of the ExitError interface.
169+
func (eew ExitErrorWrapper) ExitStatus() int {
170+
ws, ok := eew.Sys().(syscall.WaitStatus)
171+
if !ok {
172+
panic("can't call ExitStatus() on a non-WaitStatus exitErrorWrapper")
173+
}
174+
return ws.ExitStatus()
175+
}
176+
177+
// CodeExitError is an implementation of ExitError consisting of an error object
178+
// and an exit code (the upper bits of os.exec.ExitStatus).
179+
type CodeExitError struct {
180+
Err error
181+
Code int
182+
}
183+
184+
var _ ExitError = CodeExitError{}
185+
186+
func (e CodeExitError) Error() string {
187+
return e.Err.Error()
188+
}
189+
190+
func (e CodeExitError) String() string {
191+
return e.Err.Error()
192+
}
193+
194+
func (e CodeExitError) Exited() bool {
195+
return true
196+
}
197+
198+
func (e CodeExitError) ExitStatus() int {
199+
return e.Code
200+
}

exec/exec_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
Copyright 2017 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package exec
18+
19+
import (
20+
osexec "os/exec"
21+
"testing"
22+
)
23+
24+
func TestExecutorNoArgs(t *testing.T) {
25+
ex := New()
26+
27+
cmd := ex.Command("true")
28+
out, err := cmd.CombinedOutput()
29+
if err != nil {
30+
t.Errorf("expected success, got %v", err)
31+
}
32+
if len(out) != 0 {
33+
t.Errorf("expected no output, got %q", string(out))
34+
}
35+
36+
cmd = ex.Command("false")
37+
out, err = cmd.CombinedOutput()
38+
if err == nil {
39+
t.Errorf("expected failure, got nil error")
40+
}
41+
if len(out) != 0 {
42+
t.Errorf("expected no output, got %q", string(out))
43+
}
44+
ee, ok := err.(ExitError)
45+
if !ok {
46+
t.Errorf("expected an ExitError, got %+v", err)
47+
}
48+
if ee.Exited() {
49+
if code := ee.ExitStatus(); code != 1 {
50+
t.Errorf("expected exit status 1, got %d", code)
51+
}
52+
}
53+
54+
cmd = ex.Command("/does/not/exist")
55+
out, err = cmd.CombinedOutput()
56+
if err == nil {
57+
t.Errorf("expected failure, got nil error")
58+
}
59+
if ee, ok := err.(ExitError); ok {
60+
t.Errorf("expected non-ExitError, got %+v", ee)
61+
}
62+
}
63+
64+
func TestExecutorWithArgs(t *testing.T) {
65+
ex := New()
66+
67+
cmd := ex.Command("echo", "stdout")
68+
out, err := cmd.CombinedOutput()
69+
if err != nil {
70+
t.Errorf("expected success, got %+v", err)
71+
}
72+
if string(out) != "stdout\n" {
73+
t.Errorf("unexpected output: %q", string(out))
74+
}
75+
76+
cmd = ex.Command("/bin/sh", "-c", "echo stderr > /dev/stderr")
77+
out, err = cmd.CombinedOutput()
78+
if err != nil {
79+
t.Errorf("expected success, got %+v", err)
80+
}
81+
if string(out) != "stderr\n" {
82+
t.Errorf("unexpected output: %q", string(out))
83+
}
84+
}
85+
86+
func TestLookPath(t *testing.T) {
87+
ex := New()
88+
89+
shExpected, _ := osexec.LookPath("sh")
90+
sh, _ := ex.LookPath("sh")
91+
if sh != shExpected {
92+
t.Errorf("unexpected result for LookPath: got %s, expected %s", sh, shExpected)
93+
}
94+
}
95+
96+
func TestExecutableNotFound(t *testing.T) {
97+
exec := New()
98+
cmd := exec.Command("fake_executable_name")
99+
_, err := cmd.CombinedOutput()
100+
if err != ErrExecutableNotFound {
101+
t.Errorf("Expected error ErrExecutableNotFound but got %v", err)
102+
}
103+
}

0 commit comments

Comments
 (0)