другой вызов с использованием одного и того же канала - PullRequest
0 голосов
/ 23 октября 2019

Какую версию Go вы используете (go version)?

$ go version 1.13.1

Воспроизводится ли эта проблема в последнем выпуске?

Я не уверен.

Какую операционную систему и архитектуру процессора вы используете (go env)?

$ go env
GO111MODULE="auto"
GOARCH="amd64"
GOBIN="/usr/local/go/bin"
GOCACHE="/data/xieyixin/.cache/go-build"
GOENV="/data/xieyixin/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/data/xieyixin/go"
GOPRIVATE=""
GOPROXY="http://10.0.12.201:8989/"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/data/xieyixin/hxagent/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build474907248=/tmp/go-build"

Что вы делали?

Я пишу функцию для выполнения команды exec, иЯ самостоятельно проверяю время ожидания. И я проверяю это так

package utils

import (
    "bytes"
    "context"
    "log"
    "os/exec"
    "syscall"
    "time"
)

func ExecCommand(command string, timeout time.Duration) (string, error) {
    log.Printf("command:%v, timeout:%v", command, timeout)
    var (
        cmd    *exec.Cmd
        stdout bytes.Buffer
        stderr bytes.Buffer
        result string
        err    error
        //timeouterr error
    )
    ctx, cancelFn := context.WithTimeout(context.Background(), timeout)
    defer cancelFn()

    cmd = exec.Command("bash", "-c", "--", command)
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

    var waitDone = make(chan struct{})
    defer func() {
        log.Printf("waitDone addr:%v\n", &waitDone)
        log.Printf("close waitdone channel\n")
        close(waitDone)
    }()
    go func() {
        err = cmd.Run()
        log.Printf("waitDone addr:%v\n", &waitDone)
        waitDone <- struct{}{}
    }()

    select {
    case <-ctx.Done():
        log.Printf("timeout to kill process, %v", cmd.Process.Pid)
        syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
        result = convertStr(stdout)
        err = ctx.Err()
    case <-waitDone:
        if err != nil {
            result = convertStr(stderr)
        } else {
            result = convertStr(stdout)
        }
    }

    log.Printf("result:%v,err:%v", result, err)

    return result, err
}

func convertStr(buffer bytes.Buffer) string {
    data := buffer.String()
    return data
}
package utils

import (
    "context"
    "testing"
    "time"
)

func TestExecCommand(t *testing.T) {
    tests := []struct {
        command string
        timeout time.Duration
        wantErr string
        want    string
    }{
        {
            command: "sleep 10",
            timeout: time.Second * 5,
            wantErr: context.DeadlineExceeded.Error(),
        },
        {
            command: "watch -n 1 date +%s",
            timeout: time.Second * 10,
            wantErr: context.DeadlineExceeded.Error(),
            want:    "timeout, but still have result.",
        },
        {
            command: "hostname",
            timeout: time.Second * 5,
            wantErr: "",
            want:    "anything result would be fine.",
        },
    }

    for _, tt := range tests {
        // got panic here. 
        // send on closed channel.
        got, gotErr := ExecCommand(tt.command, tt.timeout)
        if gotErr == nil {
            if tt.wantErr == "" {
                t.Logf("succeed")
            } else {
                t.Errorf("failed case: %+v, got:%v, gotErr:%v\n", tt, got, gotErr)
            }
        } else if gotErr.Error() == tt.wantErr {
            t.Logf("succeed")
        } else {
            t.Errorf("failed case: %+v, got:%v, gotErr:%v\n", tt, got, gotErr)
        }

    }
}

Что вы ожидали увидеть?

тест в порядке.

Что вы видели вместо этого?

паника: отправить по закрытому каналу.

edit1: причина, по которой я сам управляю контекстом [https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773]

edit2: здесь больше путаницы.

мне это кажется немного понятным. но у меня все еще есть вопрос. [https://golang.org/src/os/exec/exec.go?s=11462:11489#L440],

if c.ctx != nil {
        c.waitDone = make(chan struct{}) // here
        go func() {
            select {
            case <-c.ctx.Done():
                c.Process.Kill()
            case <-c.waitDone: // and here
            }
        }()
    }

, как вы можете видеть, это похоже на код @Cerise Limón. почему мы должны писать этот канал. это необходимо?

Ответы [ 2 ]

3 голосов
/ 23 октября 2019

Вы закрываете канал при выходе из функции ExecCommand. Так как вы отправляете сообщение в программе, нет гарантии, что оно будет отправлено до выхода из функции. На самом деле, все время я бежал, это случилось после. Ваш тест будет работать нормально без первого исправления.

    defer func() {
        log.Printf("waitDone addr:%v\n", &waitDone)
        log.Printf("close waitdone channel\n")
        close(waitDone) // <- here 
    }()

    go func() {
        err = cmd.Run()
        log.Printf("waitDone addr:%v\n", &waitDone)
        waitDone <- struct{}{}  // <- and here
    }()

Обновление: @ Cerise-Limón указал, что вы можете использовать контекст в вызове cmd. Поскольку вы уже используете контекст тайм-аута, это будет соответствовать perfeclty

cmd = exec.CommandContext(ctx, "bash", "-c", "--", command)
// cmd = exec.Command("bash", "-c", "--", command)

Это может уберечь вас от использования этой сложной логики для проверки тайм-аута.

0 голосов
/ 24 октября 2019

Попробуйте использовать exec.CommandContext вместо написания этого кода самостоятельно.

В сценарии, в котором время ожидания контекста истекает до завершения команды, функция ExecCommand может закрытьканал перед запуском goroutine на канал. Это вызывает панику.

Поскольку приложение не получает на waitDone после выполнения close(waitDone), нет смысла закрывать канал.

Другая проблема возникает, если код для закрытия канала будет удален. Поскольку waitDone является небуферизованным каналом, в сценарии тайм-аута программа Run будет блокироваться навсегда при отправке на waitDone.

Вызов cmd.Run() запускает процедуру для копирования данных в stdout и stderr. Нет никакой гарантии, что эти программы были выполнены до ExecCommand вызовов convertStr(stdout) или convertStr(stderr).

Вот одно исправление для всего этого:

func ExecCommand(command string, timeout time.Duration) (string, error) {
    log.Printf("command:%v, timeout:%v", command, timeout)
    ctx, cancelFn := context.WithTimeout(context.Background(), timeout)
    defer cancelFn()

    var stdout, stderr bytes.Buffer

    cmd := exec.Command("bash", "-c", "--", command)
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

    err := cmd.Start()
    if err != nil {
        return "", err
    }

    go func() {
        <-ctx.Done()
        if ctx.Err() == context.DeadlineExceeded {
            log.Printf("timeout to kill process, %v", cmd.Process.Pid)
            syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
        }
    }()

    err = cmd.Wait()
    var result string
    if err != nil {
        result = stderr.String()
    } else {
        result = stdout.String()
    }
}
...