You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang编写的SSH客户端执行部分命令时挂起如何解决?

问题描述

我用Golang编写的SSH客户端远程连接虚拟路由器执行命令时,部分命令可正常运行,但执行另一些命令会出现客户端挂起、路由器会话未关闭且Host字段不显示地址的情况。使用Putty或Windows自带SSH客户端则无此问题,请问该如何解决?

客户端代码
package main

import (
    "fmt"
    "log"
    "os"
    "strings"

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

func main() {
    if len(os.Args) < 4 {
        log.Fatalf("Usage: %s <user> <host:port> <command>", os.Args[0])
    }

    client, session, err := connectToHost(os.Args[1], os.Args[2])
    if err != nil {
        panic(err)
    }

    command := strings.Join(os.Args[3:], " ")
    log.Printf("Exec command '%s'", command)
    out, err := session.CombinedOutput(command)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(out))
    client.Close()
}

func connectToHost(user, host string) (*ssh.Client, *ssh.Session, error) {
    var pass string
    fmt.Print("Password: ")
    fmt.Scanf("%s\n", &pass)

    sshConfig := &ssh.ClientConfig{
        User: user,
        Auth: []ssh.AuthMethod{ssh.Password(pass)},
    }
    sshConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey()

    client, err := ssh.Dial("tcp", host, sshConfig)
    if err != nil {
        return nil, nil, err
    }

    session, err := client.NewSession()
    if err != nil {
        client.Close()
        return nil, nil, err
    }

    return client, session, nil
}
解决方向

1. 分配PTY终端

部分路由器命令依赖交互式终端(PTY)才能正常执行,默认的CombinedOutput不会分配PTY,可在创建Session后添加PTY请求:

session, err := client.NewSession()
if err != nil {
    client.Close()
    return nil, nil, err
}
// 新增PTY分配逻辑
modes := ssh.TerminalModes{
    ssh.ECHO:          0,     // 关闭回显
    ssh.TTY_OP_ISPEED: 14400, // 输入速率
    ssh.TTY_OP_OSPEED: 14400, // 输出速率
}
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
    session.Close()
    client.Close()
    return nil, nil, fmt.Errorf("请求PTY失败: %v", err)
}

2. 替换CombinedOutput,手动处理流并设置超时

CombinedOutput会等待命令完全结束才返回,若命令进入交互式状态或未正确关闭输出流,就会导致挂起。可手动处理输出流并添加超时控制:

// 替换原CombinedOutput的代码段
import (
    "context"
    "io"
    "time"
)

// ...

stdout, err := session.StdoutPipe()
if err != nil {
    panic(err)
}
stderr, err := session.StderrPipe()
if err != nil {
    panic(err)
}

if err := session.Start(command); err != nil {
    panic(err)
}

// 设置10秒超时
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

var output strings.Builder
var errOutput strings.Builder

// 异步读取输出
go func() {
    io.Copy(&output, stdout)
}()
go func() {
    io.Copy(&errOutput, stderr)
}()

// 等待命令结束或超时
select {
case <-ctx.Done():
    session.Signal(ssh.SIGINT)
    session.Wait()
    fmt.Println("命令执行超时")
    fmt.Println("标准输出:", output.String())
    fmt.Println("错误输出:", errOutput.String())
case <-func() chan struct{} {
    ch := make(chan struct{})
    go func() {
        session.Wait()
        close(ch)
    }()
    return ch
}():
    fmt.Println("标准输出:", output.String())
    if errOutput.Len() > 0 {
        fmt.Println("错误输出:", errOutput.String())
    }
}

3. 显式关闭Session

当前代码仅关闭Client,未显式关闭Session,可能导致路由器会话残留。建议在获取Session后立即注册关闭操作:

client, session, err := connectToHost(os.Args[1], os.Args[2])
if err != nil {
    panic(err)
}
defer func() {
    session.Close()
    client.Close()
}()

4. 指定命令执行的Shell环境

部分命令需要特定Shell环境才能运行,可尝试在命令前指定路由器支持的Shell(如sh -c):

command := fmt.Sprintf("sh -c '%s'", strings.Join(os.Args[3:], " "))

内容的提问来源于stack exchange,提问作者vostbur

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 05:32:42