Принудительно закрыть сеанс клиента SSH

Я написал SSH-клиент для подключения к сетевым устройствам и установил тайм-аут с помощью «выбрать», когда выполнение команды занимает более 25 секунд. Я заметил, что на некоторых устройствах есть другая IOS, и они не могут сбросить сеанс SSH с ними с помощью метода Close() после того, как истечет время ожидания, и это приведет к утечке goroutinge. Мне нужно поддерживать работу клиента и отключить сеанс, чтобы быть готовым к следующей команде. похоже, что в это время горутина не завершается навсегда! У вас есть идеи?

    go func() {
       r <- s.Run(cmd)
    }()

    select {
       case err := <-r:
         return err
       case <-time.After(time.Duration(timeout) * time.Second):
         s.Close()
         return fmt.Errorf("timeout after %d seconds", timeout)
    }

Через профилирование кучи я увидел следующее: 2,77 ГБ 99,44% 99,44% 2,77 ГБ 99,44% bytes.makeSlice

     0     0% 99.44%     2.77GB 99.44%  bytes.(*Buffer).ReadFrom

     0     0% 99.44%     2.77GB 99.44%  golang.org/x/crypto/ssh.(*Session).start.func1

     0     0% 99.44%     2.77GB 99.44%  golang.org/x/crypto/ssh.(*Session).stdout.func1

     0     0% 99.44%     2.77GB 99.44%  io.Copy

     0     0% 99.44%     2.77GB 99.44%  io.copyBuffer

     0     0% 99.44%     2.78GB 99.93%  runtime.goexit

ROUTINE ======================== runtime.goвыйдите в /usr/local/go/src/runtime/asm_amd64.s

     0     2.78GB (flat, cum) 99.93% of Total

     .          .   1993:   RET

     .          .   1994:

     .          .   1995:// The top-most function running on a goroutine

     .          .   1996:// returns to goexit+PCQuantum.

     .          .   1997:TEXT runtime·goexit(SB),NOSPLIT,$0-0

     .     2.78GB   1998:   BYTE    $0x90   // NOP

     .          .   1999:   CALL    runtime·goexit1(SB) // does not return

     .          .   2000:   // traceback from goexit1 must hit code range of goexit

     .          .   2001:   BYTE    $0x90   // NOP

     .          .   2002:


     .          .   2003:TEXT runtime·prefetcht0(SB),NOSPLIT,$0-8

person Mehrdad    schedule 04.05.2016    source источник
comment
Буферизируется ли канал r? По истечении тайм-аута получатель не может принять результат от s.Run. Вам все еще может понадобиться убить удаленный процесс; Вы пытались использовать s.Signal, чтобы убить его по истечении времени ожидания?   -  person JimB    schedule 05.05.2016
comment
Да, я пробовал s.Signal(ssh.SIGKILL), но ничего не произошло! У меня есть эта проблема для нескольких устройств, а не для всех.   -  person Mehrdad    schedule 06.05.2016
comment
Опять же, буферизуется ли r?   -  person JimB    schedule 06.05.2016
comment
это не буферизовано, я пробовал с полировкой, но то же самое.   -  person Mehrdad    schedule 06.05.2016
comment
Я заметил, что io.copy зависает в блоках ниже, когда я пытаюсь закрыть сеанс. github.com/golang/crypto/blob/master/ ssh/session.go#L468-L492   -  person Mehrdad    schedule 06.05.2016
comment
Его нужно буферизовать, иначе горутина не сможет вернуться.   -  person JimB    schedule 06.05.2016
comment
Не закрывайте сеанс, чтобы прервать его, завершите процесс и подождите (Выполнить вызывает «Подождите вас»)   -  person JimB    schedule 06.05.2016
comment
Можете ли вы объяснить мне, почему нам нужен буферизованный канал здесь?   -  person Mehrdad    schedule 06.05.2016
comment
горутина не может вернуться, пока не завершится отправка r. Вы можете либо буферизовать канал и игнорировать значение, либо получить его снова по истечении тайм-аута.   -  person JimB    schedule 06.05.2016
comment
Я считаю, что процесс, запущенный s.Run, не остановлен должным образом, поэтому он не может освободить ресурс. Вы пытались отправить sigterm/sig kill этому процессу? godoc.org/golang.org/x/crypto/ssh#Session. Signal и вместо Run используйте Start и подождите, как stackoverflow.com/questions/11886531/   -  person ahmy    schedule 31.01.2018
comment
@ahmy Вы должны превратить это в полный ответ. Вызов Start, а затем Wait - это путь.   -  person Mihai Todor    schedule 04.01.2019


Ответы (1)


Канал r блокирует возврат процедуры Go, так как он не очищается. Я написал адаптированную версию вашего кода и вставил группу ожидания, чтобы продемонстрировать проблему:

func main() {
    var wg sync.WaitGroup // This is only added for demonstration purposes
    s := new(clientSession)

    r := make(chan error)

    go func(s *clientSession) {
        wg.Add(1)
        r <- s.Run()
        wg.Done() // Will only be called after s.Run() is able to return
    }(s)

    fmt.Println("Client has been opened")

    select {
    case err := <-r:
        fmt.Println(err)
    case <-time.After(1 * time.Second):
        s.Close()
        fmt.Println("Timed out, closing")
    }

    wg.Wait() // Waits until wg.Done() is called.

    fmt.Println("Main finished successfully")
}

Кажется, что игровая площадка Go завершает работу программы, поэтому я создал суть с полным запуском. способный код. Когда мы запускаем incorrect.go:

$ go run incorrect.go
Client has been opened
Timed out, closing
fatal error: all goroutines are asleep - deadlock!
....

Это потому, что наш код заблокирован на строке wg.Wait(). Это продемонстрировало, что wg.Done() в подпрограмме Go никогда не достигается.

Как отмечают комментарии, здесь может помочь буферизованный канал. Но только если вас больше не волнует ошибка, после вызова s.Close()

r := make(chan error, 1)

buffered.go работает правильно, но ошибка теряется:

$ go run buffered.go
Client has been opened
Timed out, closing
Main finished successfully

Другим вариантом будет слить канал ровно 1 раз:

select {
    case err := <-r:
        fmt.Println(err)
    case <-time.After(1 * time.Second):
        s.Close()
        fmt.Println("Timed out, closing")
        fmt.Println(<-r)
    }

Или заключив select в цикл for (без буферизованного канала):

X:
    for {
        select {
        case err := <-r:
            fmt.Println(err)
            break X // because we are in main(). Normally `return err`
        case <-time.After(1 * time.Second):
            s.Close()
            fmt.Println("Timed out, closing")
        }
    }

Когда мы запускаем drain.go, мы также видим вывод ошибки:

$ go run incorrect.go
Client has been opened
Timed out, closing
Run() closed
Main finished successfully

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

person Tim    schedule 15.02.2020