内容简介:最近在写一个跳板机登录的小工具,其中涉及到了用 Go 来进行交互式执行命令,简单地说就是弄个终端出来;一开始随便 Google 了一下,copy 下来基本上就是能跑了…但是后来发现了一些各种各样的小问题,强迫症的我实在受不了,最后翻了一下 Teleport 的源码,从中学到了不少有用的知识,这里记录一下不想看太多可以直接跳转到拿代码一开始随便 Google 出来的代码,copy 上就直接跑;代码基本如下:
最近在写一个跳板机登录的小工具,其中涉及到了用 Go 来进行交互式执行命令,简单地说就是弄个终端出来;一开始随便 Google 了一下,copy 下来基本上就是能跑了…但是后来发现了一些各种各样的小问题,强迫症的我实在受不了,最后翻了一下 Teleport 的源码,从中学到了不少有用的知识,这里记录一下
一、原始版本
不想看太多可以直接跳转到拿代码
1.1、样例代码
一开始随便 Google 出来的代码,copy 上就直接跑;代码基本如下:
func main() {
// 创建 ssh 配置
sshConfig := &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
// 创建 client
client, err := ssh.Dial("tcp", "192.168.1.20:22", sshConfig)
checkErr(err)
defer client.Close()
// 获取 session
session, err := client.NewSession()
checkErr(err)
defer session.Close()
// 拿到当前终端文件描述符
fd := int(os.Stdin.Fd())
termWidth, termHeight, err := terminal.GetSize(fd)
// request pty
err = session.RequestPty("xterm-256color", termHeight, termWidth, ssh.TerminalModes{})
checkErr(err)
// 对接 std
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin
err = session.Shell()
checkErr(err)
err = session.Wait()
checkErr(err)
}
func checkErr(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
1.2、遇到的问题
以上代码跑起来后,基本上遇到了以下问题:
ls
二、改进代码
2.1、回显问题
关于回显问题,实际上解决方案很简单,设置当前终端进入 raw
模式即可;代码如下:
// 拿到当前终端文件描述符 fd := int(os.Stdin.Fd()) // make raw state, err := terminal.MakeRaw(fd) checkErr(err) defer terminal.Restore(fd, state)
代码很简单,网上一大堆,But…基本没有文章详细说这个 raw
模式到底是个啥玩意;好在万能的 StackOverflow 对于不熟悉 Linux 的人给出了一个很清晰的解释: What’s the difference between a “raw” and a “cooked” device driver?
大致意思就是说
在终端处于 Cooked
模式时,当你输入一些字符后,默认是被当前终端 cache 住的,在你敲了回车之前这些文本都在 cache 中,这样允许应用程序做一些处理,比如捕获 Cntl-D
等按键,这时候就会出现敲回车后本地终端帮你打印了一下,导致出现类似回显的效果;当设置终端为 raw
模式后,所有的输入将不被 cache,而是发送到应用程序,在我们的代码中表现为通过 io.Copy
直接发送到了远端 shell 程序
2.2、终端大小问题
当本地调整了终端大小后,远程终端毫无反应;后来发现在 *ssh.Session
上有一个 WindowChange
方法,用于向远端发送窗口调整事件;解决方案就是启动一个 goroutine
在后台不断监听窗口改变事件,然后调用 WindowChange
即可;代码如下:
go func() {
// 监听窗口变更事件
sigwinchCh := make(chan os.Signal, 1)
signal.Notify(sigwinchCh, syscall.SIGWINCH)
fd := int(os.Stdin.Fd())
termWidth, termHeight, err := terminal.GetSize(fd)
if err != nil {
fmt.Println(err)
}
for {
select {
// 阻塞读取
case sigwinch := <-sigwinchCh:
if sigwinch == nil {
return
}
currTermWidth, currTermHeight, err := terminal.GetSize(fd)
// 判断一下窗口尺寸是否有改变
if currTermHeight == termHeight && currTermWidth == termWidth {
continue
}
// 更新远端大小
session.WindowChange(currTermHeight, currTermWidth)
if err != nil {
fmt.Printf("Unable to send window-change reqest: %s.", err)
continue
}
termWidth, termHeight = currTermWidth, currTermHeight
}
}
}()
2.3、Tmux 标题以及回显不换行
这两个问题实际上都是由于我们直接对接了 stderr
、 stdout
和 stdin
造成的,实际上我们应当启动一个异步的管道式复制行为,并且最好带有 buf 的发送;代码如下:
stdin, err := session.StdinPipe()
checkErr(err)
stdout, err := session.StdoutPipe()
checkErr(err)
stderr, err := session.StderrPipe()
checkErr(err)
go io.Copy(os.Stderr, stderr)
go io.Copy(os.Stdout, stdout)
go func() {
buf := make([]byte, 128)
for {
n, err := os.Stdin.Read(buf)
if err != nil {
fmt.Println(err)
return
}
if n > 0 {
_, err = stdin.Write(buf[:n])
if err != nil {
checkErr(err)
}
}
}
}()
三、完整代码
type SSHTerminal struct {
Session *ssh.Session
exitMsg string
stdout io.Reader
stdin io.Writer
stderr io.Reader
}
func main() {
sshConfig := &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", "192.168.1.20:22", sshConfig)
if err != nil {
fmt.Println(err)
}
defer client.Close()
err = New(client)
if err != nil {
fmt.Println(err)
}
}
func (t *SSHTerminal) updateTerminalSize() {
go func() {
// SIGWINCH is sent to the process when the window size of the terminal has
// changed.
sigwinchCh := make(chan os.Signal, 1)
signal.Notify(sigwinchCh, syscall.SIGWINCH)
fd := int(os.Stdin.Fd())
termWidth, termHeight, err := terminal.GetSize(fd)
if err != nil {
fmt.Println(err)
}
for {
select {
// The client updated the size of the local PTY. This change needs to occur
// on the server side PTY as well.
case sigwinch := <-sigwinchCh:
if sigwinch == nil {
return
}
currTermWidth, currTermHeight, err := terminal.GetSize(fd)
// Terminal size has not changed, don't do anything.
if currTermHeight == termHeight && currTermWidth == termWidth {
continue
}
t.Session.WindowChange(currTermHeight, currTermWidth)
if err != nil {
fmt.Printf("Unable to send window-change reqest: %s.", err)
continue
}
termWidth, termHeight = currTermWidth, currTermHeight
}
}
}()
}
func (t *SSHTerminal) interactiveSession() error {
defer func() {
if t.exitMsg == "" {
fmt.Fprintln(os.Stdout, "the connection was closed on the remote side on ", time.Now().Format(time.RFC822))
} else {
fmt.Fprintln(os.Stdout, t.exitMsg)
}
}()
fd := int(os.Stdin.Fd())
state, err := terminal.MakeRaw(fd)
if err != nil {
return err
}
defer terminal.Restore(fd, state)
termWidth, termHeight, err := terminal.GetSize(fd)
if err != nil {
return err
}
termType := os.Getenv("TERM")
if termType == "" {
termType = "xterm-256color"
}
err = t.Session.RequestPty(termType, termHeight, termWidth, ssh.TerminalModes{})
if err != nil {
return err
}
t.updateTerminalSize()
t.stdin, err = t.Session.StdinPipe()
if err != nil {
return err
}
t.stdout, err = t.Session.StdoutPipe()
if err != nil {
return err
}
t.stderr, err = t.Session.StderrPipe()
go io.Copy(os.Stderr, t.stderr)
go io.Copy(os.Stdout, t.stdout)
go func() {
buf := make([]byte, 128)
for {
n, err := os.Stdin.Read(buf)
if err != nil {
fmt.Println(err)
return
}
if n > 0 {
_, err = t.stdin.Write(buf[:n])
if err != nil {
fmt.Println(err)
t.exitMsg = err.Error()
return
}
}
}
}()
err = t.Session.Shell()
if err != nil {
return err
}
err = t.Session.Wait()
if err != nil {
return err
}
return nil
}
func New(client *ssh.Client) error {
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
s := SSHTerminal{
Session: session,
}
return s.interactiveSession()
}
转载请注明出处,本文采用 CC4.0 协议授权
以上所述就是小编给大家介绍的《Go ssh 交互式执行命令》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- EnterpriseDB: 交互式命令行安装
- 哭了!好美!交互式《几何原本》再现江湖
- 前端交互式图表绘制库GoJS介绍
- CVPR 2018:一种交互式纹理迁移通用框架
- Cockpit 182 发布,交互式服务器管理界面
- Node.js REPL(交互式解释器)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
超级连接者:破解新互联时代的成功密码
伊桑•祖克曼(ETHAN ZUCKERMAN) / 林玮、张晨 / 浙江人民出版社 / 2018-8-1 / CNY 72.90
● 我们生活在一个互联互通的世界,我们需要辩证地看待某些事件,发现隐藏在背后的真相。着眼当下,看清彼此之间的联系,而非凭空幻想未来世界联系之紧密。数字世界主义要求我们承担起责任,让隐藏的联系变成现实。 ● 我们对世界的看法是局限的、不完整的、带有偏见的。如果我们想要改变从这个广阔的世界所获取的信息,我们需要做出结构性的改变。 ● 建立联系是一种新的力量。无论是在国家层面、企业层面还是个......一起来看看 《超级连接者:破解新互联时代的成功密码》 这本书的介绍吧!
UNIX 时间戳转换
UNIX 时间戳转换
HSV CMYK 转换工具
HSV CMYK互换工具