mirror of https://github.com/k3s-io/k3s
253 lines
6.6 KiB
Go
253 lines
6.6 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package exec
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
osexec "os/exec"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// ErrExecutableNotFound is returned if the executable is not found.
|
|
var ErrExecutableNotFound = osexec.ErrNotFound
|
|
|
|
// Interface is an interface that presents a subset of the os/exec API. Use this
|
|
// when you want to inject fakeable/mockable exec behavior.
|
|
type Interface interface {
|
|
// Command returns a Cmd instance which can be used to run a single command.
|
|
// This follows the pattern of package os/exec.
|
|
Command(cmd string, args ...string) Cmd
|
|
|
|
// CommandContext returns a Cmd instance which can be used to run a single command.
|
|
//
|
|
// The provided context is used to kill the process if the context becomes done
|
|
// before the command completes on its own. For example, a timeout can be set in
|
|
// the context.
|
|
CommandContext(ctx context.Context, cmd string, args ...string) Cmd
|
|
|
|
// LookPath wraps os/exec.LookPath
|
|
LookPath(file string) (string, error)
|
|
}
|
|
|
|
// Cmd is an interface that presents an API that is very similar to Cmd from os/exec.
|
|
// As more functionality is needed, this can grow. Since Cmd is a struct, we will have
|
|
// to replace fields with get/set method pairs.
|
|
type Cmd interface {
|
|
// Run runs the command to the completion.
|
|
Run() error
|
|
// CombinedOutput runs the command and returns its combined standard output
|
|
// and standard error. This follows the pattern of package os/exec.
|
|
CombinedOutput() ([]byte, error)
|
|
// Output runs the command and returns standard output, but not standard err
|
|
Output() ([]byte, error)
|
|
SetDir(dir string)
|
|
SetStdin(in io.Reader)
|
|
SetStdout(out io.Writer)
|
|
SetStderr(out io.Writer)
|
|
SetEnv(env []string)
|
|
|
|
// StdoutPipe and StderrPipe for getting the process' Stdout and Stderr as
|
|
// Readers
|
|
StdoutPipe() (io.ReadCloser, error)
|
|
StderrPipe() (io.ReadCloser, error)
|
|
|
|
// Start and Wait are for running a process non-blocking
|
|
Start() error
|
|
Wait() error
|
|
|
|
// Stops the command by sending SIGTERM. It is not guaranteed the
|
|
// process will stop before this function returns. If the process is not
|
|
// responding, an internal timer function will send a SIGKILL to force
|
|
// terminate after 10 seconds.
|
|
Stop()
|
|
}
|
|
|
|
// ExitError is an interface that presents an API similar to os.ProcessState, which is
|
|
// what ExitError from os/exec is. This is designed to make testing a bit easier and
|
|
// probably loses some of the cross-platform properties of the underlying library.
|
|
type ExitError interface {
|
|
String() string
|
|
Error() string
|
|
Exited() bool
|
|
ExitStatus() int
|
|
}
|
|
|
|
// Implements Interface in terms of really exec()ing.
|
|
type executor struct{}
|
|
|
|
// New returns a new Interface which will os/exec to run commands.
|
|
func New() Interface {
|
|
return &executor{}
|
|
}
|
|
|
|
// Command is part of the Interface interface.
|
|
func (executor *executor) Command(cmd string, args ...string) Cmd {
|
|
return (*cmdWrapper)(osexec.Command(cmd, args...))
|
|
}
|
|
|
|
// CommandContext is part of the Interface interface.
|
|
func (executor *executor) CommandContext(ctx context.Context, cmd string, args ...string) Cmd {
|
|
return (*cmdWrapper)(osexec.CommandContext(ctx, cmd, args...))
|
|
}
|
|
|
|
// LookPath is part of the Interface interface
|
|
func (executor *executor) LookPath(file string) (string, error) {
|
|
return osexec.LookPath(file)
|
|
}
|
|
|
|
// Wraps exec.Cmd so we can capture errors.
|
|
type cmdWrapper osexec.Cmd
|
|
|
|
var _ Cmd = &cmdWrapper{}
|
|
|
|
func (cmd *cmdWrapper) SetDir(dir string) {
|
|
cmd.Dir = dir
|
|
}
|
|
|
|
func (cmd *cmdWrapper) SetStdin(in io.Reader) {
|
|
cmd.Stdin = in
|
|
}
|
|
|
|
func (cmd *cmdWrapper) SetStdout(out io.Writer) {
|
|
cmd.Stdout = out
|
|
}
|
|
|
|
func (cmd *cmdWrapper) SetStderr(out io.Writer) {
|
|
cmd.Stderr = out
|
|
}
|
|
|
|
func (cmd *cmdWrapper) SetEnv(env []string) {
|
|
cmd.Env = env
|
|
}
|
|
|
|
func (cmd *cmdWrapper) StdoutPipe() (io.ReadCloser, error) {
|
|
r, err := (*osexec.Cmd)(cmd).StdoutPipe()
|
|
return r, handleError(err)
|
|
}
|
|
|
|
func (cmd *cmdWrapper) StderrPipe() (io.ReadCloser, error) {
|
|
r, err := (*osexec.Cmd)(cmd).StderrPipe()
|
|
return r, handleError(err)
|
|
}
|
|
|
|
func (cmd *cmdWrapper) Start() error {
|
|
err := (*osexec.Cmd)(cmd).Start()
|
|
return handleError(err)
|
|
}
|
|
|
|
func (cmd *cmdWrapper) Wait() error {
|
|
err := (*osexec.Cmd)(cmd).Wait()
|
|
return handleError(err)
|
|
}
|
|
|
|
// Run is part of the Cmd interface.
|
|
func (cmd *cmdWrapper) Run() error {
|
|
err := (*osexec.Cmd)(cmd).Run()
|
|
return handleError(err)
|
|
}
|
|
|
|
// CombinedOutput is part of the Cmd interface.
|
|
func (cmd *cmdWrapper) CombinedOutput() ([]byte, error) {
|
|
out, err := (*osexec.Cmd)(cmd).CombinedOutput()
|
|
return out, handleError(err)
|
|
}
|
|
|
|
func (cmd *cmdWrapper) Output() ([]byte, error) {
|
|
out, err := (*osexec.Cmd)(cmd).Output()
|
|
return out, handleError(err)
|
|
}
|
|
|
|
// Stop is part of the Cmd interface.
|
|
func (cmd *cmdWrapper) Stop() {
|
|
c := (*osexec.Cmd)(cmd)
|
|
|
|
if c.Process == nil {
|
|
return
|
|
}
|
|
|
|
c.Process.Signal(syscall.SIGTERM)
|
|
|
|
time.AfterFunc(10*time.Second, func() {
|
|
if !c.ProcessState.Exited() {
|
|
c.Process.Signal(syscall.SIGKILL)
|
|
}
|
|
})
|
|
}
|
|
|
|
func handleError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
switch e := err.(type) {
|
|
case *osexec.ExitError:
|
|
return &ExitErrorWrapper{e}
|
|
case *osexec.Error:
|
|
if e.Err == osexec.ErrNotFound {
|
|
return ErrExecutableNotFound
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// ExitErrorWrapper is an implementation of ExitError in terms of os/exec ExitError.
|
|
// Note: standard exec.ExitError is type *os.ProcessState, which already implements Exited().
|
|
type ExitErrorWrapper struct {
|
|
*osexec.ExitError
|
|
}
|
|
|
|
var _ ExitError = &ExitErrorWrapper{}
|
|
|
|
// ExitStatus is part of the ExitError interface.
|
|
func (eew ExitErrorWrapper) ExitStatus() int {
|
|
ws, ok := eew.Sys().(syscall.WaitStatus)
|
|
if !ok {
|
|
panic("can't call ExitStatus() on a non-WaitStatus exitErrorWrapper")
|
|
}
|
|
return ws.ExitStatus()
|
|
}
|
|
|
|
// CodeExitError is an implementation of ExitError consisting of an error object
|
|
// and an exit code (the upper bits of os.exec.ExitStatus).
|
|
type CodeExitError struct {
|
|
Err error
|
|
Code int
|
|
}
|
|
|
|
var _ ExitError = CodeExitError{}
|
|
|
|
func (e CodeExitError) Error() string {
|
|
return e.Err.Error()
|
|
}
|
|
|
|
func (e CodeExitError) String() string {
|
|
return e.Err.Error()
|
|
}
|
|
|
|
// Exited is to check if the process has finished
|
|
func (e CodeExitError) Exited() bool {
|
|
return true
|
|
}
|
|
|
|
// ExitStatus is for checking the error code
|
|
func (e CodeExitError) ExitStatus() int {
|
|
return e.Code
|
|
}
|