使用pty和io的SSH脚本复制到带有菜单的流输出

问题描述

我有两个需要帮助的问题。我想我已经将其限制在goroutine中使用io.Copy了。行为就像需要使用Enter键“重新激活” stdin。

  1. 在循环的第一次迭代中,所有操作均按预期进行。它登录到localhost,运行命令,goroutine打印stdout和stderr。但是在随后的迭代中,我必须首先按Enter键以“退出goroutine”,然后在提示符下输入响应。我怀疑这与os.Stdin有关。有人可以提供一些建议吗?

  2. 是否可以在不中断脚本的情况下向ssh命令发送sigterm?例如,我可以中断ping输出,然后将其放回菜单提示符吗?

感谢您可以提供的任何帮助。这是我在这里的第一篇文章。

package main

import (
    "fmt"
    "io"
    "log"
    "net"
    "os"
    "time"

    "golang.org/x/crypto/ssh"
)

var (
    stdout,stderr io.Reader
    stdin          io.WriteCloser
    timeout        = 30 * time.Second
)

func sshConfig() *ssh.ClientConfig {
    config := &ssh.ClientConfig{
        User: MYUSER,Auth: []ssh.AuthMethod{
            ssh.Password(MYPASS),},HostKeyCallback: ssh.HostKeyCallback(func(hostname string,remote net.Addr,key ssh.PublicKey) error { return nil }),Timeout:         timeout,}
    return config
}

func options() {
  menuString := `
  Enter 1 for "uptime"
  Enter 2 for "ping -c 20 www.google.com"
  Type exit to exit
  `
    for i := 0; i <= 5; i++ {
        var num,cmd string
        fmt.Println(menuString)
        if i == 0 {
            fmt.Print("Select an option from above: ")
        } else {
            fmt.Println("You'll have to press enter before entering your option. Why?")
            fmt.Print("Select an option from above: ")
        }

        fmt.Scanln(&num)
        switch num {
        case "1":
            cmd = "uptime"
            connect(cmd)
        case "2":
            cmd = "ping -c 20 www.google.com"
            connect(cmd)
        case "exit":
            fmt.Println("exiting")
            os.Exit(0)
        default:
            fmt.Println("Invalid response")
            fmt.Println("Select an option from above: ")
        }
    }
}

func connect(c string) {
    config := sshConfig()
    conn,err := ssh.Dial("tcp","127.0.0.1:22",config)
    defer conn.Close()
    if err != nil {
        log.Fatal("dial target error:",err)
    }
    session,err := conn.NewSession()
    defer session.Close()
    if err != nil {
        log.Fatalf("session failed:%v",err)
    }
    defer session.Close()
    modes := ssh.TerminalModes{
        ssh.ECHO:          0,// disable echoing
        ssh.TTY_OP_ISPEED: 14400,// input speed = 14.4kbaud
        ssh.TTY_OP_OSPEED: 14400,// output speed = 14.4kbaud
    }
    err = session.RequestPty("xterm",80,40,modes)
    if err != nil {
        log.Fatalf("Pty request failed:%v",err)
    }

    stdin,err = session.StdinPipe()
    if err != nil {
        log.Fatalf("Unable to setup stdin for session: %v",err)
    }
    stdout,err = session.StdoutPipe()
    if err != nil {
        log.Fatalf("Unable to setup stdout for session: %v",err)
    }
    stderr,err = session.StderrPipe()
    if err != nil {
        log.Fatalf("Unable to setup stderr for session: %v",err)
    }

    quit := make(chan bool)
    go func() {
        for {
            select {
            case <-quit:
                fmt.Println("exiting goroutine")
                return
            default:
                fmt.Println("go routine is running")
                io.Copy(os.Stdout,stdout)
                io.Copy(stdin,os.Stdin)
                io.Copy(os.Stderr,stderr)
            }
        }
    }()

    if err := session.Run(c); err != nil {
        log.Fatal("Failed to run: " + err.Error())
    }
    fmt.Println("closing the channel")
    close(quit)
    fmt.Println("the channel is closed")
}

func main() {
    options()
}

解决方法

首先,io.Copy是一个阻塞调用,它将io.writer的缓冲区复制到io.reader,直到在src上达到EOF或发生错误为止。 Here's来自golang的io模块的摘录:

// copyBuffer is the actual implementation of Copy and CopyBuffer.
// if buf is nil,one is allocated.

func copyBuffer(dst Writer,src Reader,buf []byte) (written int64,err error) {
    // If the reader has a WriteTo method,use it to do the copy.
    // Avoids an allocation and a copy.
    if wt,ok := src.(WriterTo); ok {
        return wt.WriteTo(dst)
    }
    // Similarly,if the writer has a ReadFrom method,use it to do the copy.
    if rt,ok := dst.(ReaderFrom); ok {
        return rt.ReadFrom(src)
    }
    if buf == nil {
        size := 32 * 1024
        if l,ok := src.(*LimitedReader); ok && int64(size) > l.N {
            if l.N < 1 {
                size = 1
            } else {
                size = int(l.N)
            }
        }
        buf = make([]byte,size)
    }
    for {
        nr,er := src.Read(buf)
        if nr > 0 {
            nw,ew := dst.Write(buf[0:nr])
            if nw > 0 {
                written += int64(nw)
            }
            if ew != nil {
                err = ew
                break
            }
            if nr != nw {
                err = ErrShortWrite
                break
            }
        }
        if er != nil {
            if er != EOF {
                err = er
            }
            break
        }
    }
    return written,err
}

现在您的问题了
当在第一次迭代中在会话的stdin和stderr上都进行io.copy调用时,它将等待,直到stderr和stdin上都存在要复制的消息并被阻止。 只需替换您的goroutine即可执行复制标准输出,即可在每次迭代中获得预期的行为。同样,stderr,stdin也需要由自己的goroutine管理:

go func() {
        for {
            io.Copy(os.Stdout,stdout)
        }
    }()

SigTerm将用于终止会话,存在一个称为interrupt的posix信号,可用于中断cmd。 Here's列出所有POSIX信号,表示使用的ssh库。

这是中断信号和预期行为的演示:

package main

import (
    "fmt"
    "io"
    "log"
    "net"
    "os"
    "time"

    "golang.org/x/crypto/ssh"
)

var (
    stdout,stderr io.Reader
    stdin          io.WriteCloser
    timeout        = 30 * time.Second
)

const (
    MYUSER = ""
    MYPASS = ""
)

func sshConfig() *ssh.ClientConfig {
    config := &ssh.ClientConfig{
        User: MYUSER,Auth: []ssh.AuthMethod{
            ssh.Password(MYPASS),},HostKeyCallback: ssh.HostKeyCallback(func(hostname string,remote net.Addr,key ssh.PublicKey) error { return nil }),Timeout:         timeout,}
    return config
}

func options() {
    menuString := `
  Enter 1 for "uptime"
  Enter 2 for "ping -c 20 www.google.com"
  Type exit to exit
  `
    for i := 0; i <= 5; i++ {
        var num,cmd string
        fmt.Println(menuString)
        if i == 0 {
            fmt.Print("Select an option from above: ")
        } else {
            fmt.Println("You'll have to press enter before entering your option. Why?")
            fmt.Print("Select an option from above: ")
        }

        fmt.Scanln(&num)
        switch num {
        case "1":
            cmd = "uptime"
            connect(cmd)
        case "2":
            cmd = "ping -c 20 www.google.com"
            connect(cmd)
        case "exit":
            fmt.Println("exiting")
            os.Exit(0)
        default:
            fmt.Println("Invalid response")
            fmt.Println("Select an option from above: ")
        }
    }
}

func connect(c string) {
    config := sshConfig()
    conn,err := ssh.Dial("tcp","127.0.0.1:22",config)
    defer conn.Close()
    if err != nil {
        log.Fatal("dial target error:",err)
    }
    session,err := conn.NewSession()
    defer session.Close()
    if err != nil {
        log.Println("session failed:%v",err)
    }
    defer session.Close()
    modes := ssh.TerminalModes{
        ssh.ECHO:          0,// disable echoing
        ssh.TTY_OP_ISPEED: 14400,// input speed = 14.4kbaud
        ssh.TTY_OP_OSPEED: 14400,// output speed = 14.4kbaud
    }
    err = session.RequestPty("xterm",80,40,modes)
    if err != nil {
        log.Fatalf("Pty request failed:%v",err)
    }

    stdin,err = session.StdinPipe()
    if err != nil {
        log.Fatalf("Unable to setup stdin for session: %v",err)
    }
    stdout,err = session.StdoutPipe()
    if err != nil {
        log.Fatalf("Unable to setup stdout for session: %v",err)
    }
    stderr,err = session.StderrPipe()
    if err != nil {
        log.Fatalf("Unable to setup stderr for session: %v",err)
    }

    go func() {
        for {
            io.Copy(os.Stdout,stdout)
        }
    }()

    c = "echo 'go is awesome' && sleep 10 && " + c
    go func() {
        time.Sleep(1 * time.Second)
        session.Signal(ssh.SIGINT)
    }()

    if err := session.Run(c); err != nil {
        log.Println("Failed to run: " + err.Error())
    }
    fmt.Println("closing the channel")
    fmt.Println("the channel is closed")
}

func main() {
    options()
}

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...