dph19153
dph19153
2018-02-26 04:36

使用通道捕获Go​​routine的输出和错误

  • error-handling
  • concurrency

I have a for-loop that calls a function runCommand() which runs a remote command on a switch and prints the output. The function is called in a goroutine on each iteration and I am using a sync.Waitgroup to synchronize the goroutines. Now, I need a way to capture the output and any errors of my runCommand() function into a channel. I have read many articles and watched a lot of videos on using channels with goroutines, but this is the first time I have ever written a concurrent application and I can't seem to wrap my head around the idea.

Basically, my program takes in a list of hostnames from the command line then asynchronously connects to each host, runs a configuration command on it, and prints the output. It is ok for my program to continue configuring the remaining hosts if one has an error.

How would I idiomatically send the output or error(s) of each call to runCommand() to a channel then receive the output or error(s) for printing?

Here is my code:

package main

import (
    "fmt"
    "golang.org/x/crypto/ssh"
    "os"
    "time"
    "sync"
)

func main() {
    hosts := os.Args[1:]
    clientConf := configureClient("user", "password")

    var wg sync.WaitGroup
    for _, host := range hosts {
        wg.Add(1)
        go runCommand(host, &clientConf, &wg)
    }
    wg.Wait()

    fmt.Println("Configuration complete!")
}

// Run a remote command
func runCommand(host string, config *ssh.ClientConfig, wg *sync.WaitGroup) {
    defer wg.Done()
    // Connect to the client
    client, err := ssh.Dial("tcp", host+":22", config)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer client.Close()
    // Create a session
    session, err := client.NewSession()
    if err != nil {
        fmt.Println(err)
        return
    }
    defer session.Close()
    // Get the session output
    output, err := session.Output("show lldp ne")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Print(string(output))
    fmt.Printf("Connection to %s closed.
", host)
}

// Set up client configuration
func configureClient(user, password string) ssh.ClientConfig {
    var sshConf ssh.Config
    sshConf.SetDefaults()
    // Append supported ciphers
    sshConf.Ciphers = append(sshConf.Ciphers, "aes128-cbc", "aes256-cbc", "3des-cbc", "des-cbc", "aes192-cbc")
    // Create client config
    clientConf := &ssh.ClientConfig{
        Config:          sshConf,
        User:            user,
        Auth:            []ssh.AuthMethod{ssh.Password(password)},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Timeout:         time.Second * 5,
    }
    return *clientConf
}

EDIT: I got rid of the Waitgroup, as suggested, and now I need to keep track of which output belongs to which host by printing the hostname before printing its output and printing a Connection to <host> closed. message when the gorouttine completes. For example:

$ go run main.go host1[,host2[,...]]
Connecting to <host1>
[Output]
...
[Error]
Connection to <host1> closed.

Connecting to <host2>
...
Connection to <host2> closed.

Configuration complete!

I know the above won't necessarily process host1 and host2 in order, But I need to print the correct host value for the connecting and closing messages before and after the output/error(s), respectively. I tried defering printing the closing message in the runCommand() function, but the message is printed out before the output/error(s). And printing the closing message in the for-loop after each goroutine call doesn't work as expected either.

Updated code:

package main

import (
    "fmt"
    "golang.org/x/crypto/ssh"
    "os"
    "time"
)

type CmdResult struct {
    Host string
    Output string
    Err error
}

func main() {
    start := time.Now()

    hosts := os.Args[1:]
    clientConf := configureClient("user", "password")
    results := make(chan CmdResult)

    for _, host := range hosts {
        go runCommand(host, &clientConf, results)
    }
    for i := 0; i < len(hosts); i++ {
        output := <- results
        fmt.Println(output.Host)
        if output.Output != "" {
            fmt.Printf("%s
", output.Output)
        }
        if output.Err != nil {
            fmt.Printf("Error: %v
", output.Err)
        }
    }
    fmt.Printf("Configuration complete! [%s]
", time.Since(start).String())
}

// Run a remote command
func runCommand(host string, config *ssh.ClientConfig, ch chan CmdResult) {
    // This is printing before the output/error(s).
    // Does the same when moved to the bottom of this function.
    defer fmt.Printf("Connection to %s closed.
", host)

    // Connect to the client
    client, err := ssh.Dial("tcp", host+":22", config)
    if err != nil {
        ch <- CmdResult{host, "", err}
        return
    }
    defer client.Close()
    // Create a session
    session, err := client.NewSession()
    if err != nil {
        ch <- CmdResult{host, "", err}
        return
    }
    defer session.Close()
    // Get the session output
    output, err := session.Output("show lldp ne")
    if err != nil {
        ch <- CmdResult{host, "", err}
        return
    }
    ch <- CmdResult{host, string(output), nil}
}

// Set up client configuration
func configureClient(user, password string) ssh.ClientConfig {
    var sshConf ssh.Config
    sshConf.SetDefaults()
    // Append supported ciphers
    sshConf.Ciphers = append(sshConf.Ciphers, "aes128-cbc", "aes256-cbc", "3des-cbc", "des-cbc", "aes192-cbc")
    // Create client config
    clientConf := &ssh.ClientConfig{
        Config:          sshConf,
        User:            user,
        Auth:            []ssh.AuthMethod{ssh.Password(password)},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Timeout:         time.Second * 5,
    }
    return *clientConf
}
  • 点赞
  • 回答
  • 收藏
  • 复制链接分享

1条回答

为你推荐