Merge pull request #1345 from AkihiroSuda/fix-kill-warning

connhelper: try sending SIGTERM before SIGKILL
This commit is contained in:
Tõnis Tiigi 2018-09-28 08:22:13 -07:00 committed by GitHub
commit 00c0c7e12f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 67 additions and 16 deletions

View File

@ -10,8 +10,10 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/docker/cli/cli/connhelper/ssh" "github.com/docker/cli/cli/connhelper/ssh"
@ -82,6 +84,8 @@ func newCommandConn(ctx context.Context, cmd string, args ...string) (net.Conn,
// commandConn implements net.Conn // commandConn implements net.Conn
type commandConn struct { type commandConn struct {
cmd *exec.Cmd cmd *exec.Cmd
cmdExited bool
cmdWaitErr error
cmdMutex sync.Mutex cmdMutex sync.Mutex
stdin io.WriteCloser stdin io.WriteCloser
stdout io.ReadCloser stdout io.ReadCloser
@ -102,26 +106,74 @@ func (c *commandConn) killIfStdioClosed() error {
if !stdioClosed { if !stdioClosed {
return nil return nil
} }
var err error return c.kill()
}
// killAndWait tries sending SIGTERM to the process before sending SIGKILL.
func killAndWait(cmd *exec.Cmd) error {
var werr error
if runtime.GOOS != "windows" {
werrCh := make(chan error)
go func() { werrCh <- cmd.Wait() }()
cmd.Process.Signal(syscall.SIGTERM)
select {
case werr = <-werrCh:
case <-time.After(3 * time.Second):
cmd.Process.Kill()
werr = <-werrCh
}
} else {
cmd.Process.Kill()
werr = cmd.Wait()
}
return werr
}
// kill returns nil if the command terminated, regardless to the exit status.
func (c *commandConn) kill() error {
var werr error
c.cmdMutex.Lock() c.cmdMutex.Lock()
// NOTE: maybe already killed here if c.cmdExited {
if err = c.cmd.Process.Kill(); err == nil { werr = c.cmdWaitErr
err = c.cmd.Wait() } else {
} werr = killAndWait(c.cmd)
if err != nil { c.cmdWaitErr = werr
// err is typically "os: process already finished". c.cmdExited = true
// we check ProcessState here instead of `strings.Contains(err, "os: process already finished")`
if c.cmd.ProcessState.Exited() {
err = nil
}
} }
c.cmdMutex.Unlock() c.cmdMutex.Unlock()
return err if werr == nil {
return nil
}
wExitErr, ok := werr.(*exec.ExitError)
if ok {
if wExitErr.ProcessState.Exited() {
return nil
}
}
return errors.Wrapf(werr, "connhelper: failed to wait")
} }
func (c *commandConn) onEOF(eof error) error { func (c *commandConn) onEOF(eof error) error {
// when we got EOF, the command is going to be terminated
var werr error
c.cmdMutex.Lock() c.cmdMutex.Lock()
werr := c.cmd.Wait() if c.cmdExited {
werr = c.cmdWaitErr
} else {
werrCh := make(chan error)
go func() { werrCh <- c.cmd.Wait() }()
select {
case werr = <-werrCh:
c.cmdWaitErr = werr
c.cmdExited = true
case <-time.After(10 * time.Second):
c.cmdMutex.Unlock()
c.stderrMu.Lock()
stderr := c.stderr.String()
c.stderrMu.Unlock()
return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, eof, stderr)
}
}
c.cmdMutex.Unlock() c.cmdMutex.Unlock()
if werr == nil { if werr == nil {
return eof return eof
@ -136,7 +188,6 @@ func ignorableCloseError(err error) bool {
errS := err.Error() errS := err.Error()
ss := []string{ ss := []string{
os.ErrClosed.Error(), os.ErrClosed.Error(),
"process already finished",
} }
for _, s := range ss { for _, s := range ss {
if strings.Contains(errS, s) { if strings.Contains(errS, s) {
@ -154,7 +205,7 @@ func (c *commandConn) CloseRead() error {
c.stdioClosedMu.Lock() c.stdioClosedMu.Lock()
c.stdoutClosed = true c.stdoutClosed = true
c.stdioClosedMu.Unlock() c.stdioClosedMu.Unlock()
if err := c.killIfStdioClosed(); err != nil && !ignorableCloseError(err) { if err := c.killIfStdioClosed(); err != nil {
logrus.Warnf("commandConn.CloseRead: %v", err) logrus.Warnf("commandConn.CloseRead: %v", err)
} }
return nil return nil
@ -176,7 +227,7 @@ func (c *commandConn) CloseWrite() error {
c.stdioClosedMu.Lock() c.stdioClosedMu.Lock()
c.stdinClosed = true c.stdinClosed = true
c.stdioClosedMu.Unlock() c.stdioClosedMu.Unlock()
if err := c.killIfStdioClosed(); err != nil && !ignorableCloseError(err) { if err := c.killIfStdioClosed(); err != nil {
logrus.Warnf("commandConn.CloseWrite: %v", err) logrus.Warnf("commandConn.CloseWrite: %v", err)
} }
return nil return nil