From fae1c3d542bc347bf6e402ce82ac4b20fa02ee73 Mon Sep 17 00:00:00 2001 From: Qu Xuan Date: Mon, 13 Jul 2020 15:34:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96webconsole=E4=BB=A3?= =?UTF-8?q?=E7=A0=81&bugfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/webconsole/command/command.go | 16 +++- pkg/webconsole/command/ssh_command.go | 109 +++++++++++++++-------- pkg/webconsole/server/tty_server.go | 100 ++++++++------------- pkg/webconsole/session/pty_session.go | 35 ++++++-- pkg/webconsole/session/remote_console.go | 18 +++- 5 files changed, 162 insertions(+), 116 deletions(-) diff --git a/pkg/webconsole/command/command.go b/pkg/webconsole/command/command.go index 4b4e1bbccb..d98b93a9b1 100644 --- a/pkg/webconsole/command/command.go +++ b/pkg/webconsole/command/command.go @@ -29,8 +29,10 @@ type ICommand interface { GetProtocol() string GetCommand() *exec.Cmd Cleanup() error - GetData(string) (isShow bool, ouput string, command string) + Reconnect() + IsNeedShowInfo() bool ShowInfo() string + Scan(d byte, send func(msg string)) } type BaseCommand struct { @@ -56,14 +58,22 @@ func (c BaseCommand) GetCommand() *exec.Cmd { return exec.Command(c.name, c.args...) } -func (c BaseCommand) GetData(comand string) (isShow bool, ouput string, command string) { - return true, "", "" +func (c BaseCommand) Scan(byte, func(msg string)) { + return +} + +func (c BaseCommand) IsNeedShowInfo() bool { + return false } func (c BaseCommand) ShowInfo() string { return "" } +func (c BaseCommand) Reconnect() { + return +} + func (c BaseCommand) Cleanup() error { log.Infof("BaseCommand Cleanup do nothing") return nil diff --git a/pkg/webconsole/command/ssh_command.go b/pkg/webconsole/command/ssh_command.go index b42aa079aa..a3255ae3d0 100644 --- a/pkg/webconsole/command/ssh_command.go +++ b/pkg/webconsole/command/ssh_command.go @@ -21,7 +21,6 @@ import ( "net" "os" "os/exec" - "strings" "time" "yunion.io/x/jsonutils" @@ -36,12 +35,15 @@ import ( type SSHtoolSol struct { *BaseCommand - IP string - Port int - Username string - reTry int - showInfo string - keyFile string + IP string + Port int + username string + password string + failed int + showInfo string + keyFile string + buffer []byte + needShowInfo bool } func getCommand(ctx context.Context, userCred mcclient.TokenCredential, ip string, port int) (string, *BaseCommand, error) { @@ -80,6 +82,7 @@ func getCommand(ctx context.Context, userCred mcclient.TokenCredential, ip strin cmd.AppendArgs("-o", "GlobalKnownHostsFile=/dev/null") cmd.AppendArgs("-o", "UserKnownHostsFile=/dev/null") cmd.AppendArgs("-o", "PasswordAuthentication=no") + cmd.AppendArgs("-o", "BatchMode=yes") // 强制禁止密码登录 cmd.AppendArgs("-p", fmt.Sprintf("%d", port)) cmd.AppendArgs(fmt.Sprintf("%s@%s", ansible.PUBLIC_CLOUD_ANSIBLE_USER, ip)) return filename, cmd, nil @@ -104,13 +107,15 @@ func NewSSHtoolSolCommand(ctx context.Context, userCred mcclient.TokenCredential } return &SSHtoolSol{ - BaseCommand: cmd, - IP: ip, - Port: port, - Username: "", - reTry: 0, - showInfo: fmt.Sprintf("%s login: ", ip), - keyFile: keyFile, + BaseCommand: cmd, + IP: ip, + Port: port, + username: "", + failed: 0, + showInfo: fmt.Sprintf("%s login: ", ip), + keyFile: keyFile, + buffer: []byte{}, + needShowInfo: true, }, nil } @@ -120,6 +125,18 @@ func (c *SSHtoolSol) GetCommand() *exec.Cmd { cmd.Env = append(cmd.Env, "TERM=xterm-256color") return cmd } + if len(c.username) > 0 && len(c.password) > 0 { + args := []string{ + o.Options.SshpassToolPath, "-p", c.password, + o.Options.SshToolPath, "-p", fmt.Sprintf("%d", c.Port), fmt.Sprintf("%s@%s", c.username, c.IP), + "-oGlobalKnownHostsFile=/dev/null", "-oUserKnownHostsFile=/dev/null", "-oStrictHostKeyChecking=no", + "-oPreferredAuthentications=password", "-oPubkeyAuthentication=no", //密码登录时,避免搜寻秘钥登录 + "-oNumberOfPasswordPrompts=1", + } + cmd := exec.Command(args[0], args[1:]...) + cmd.Env = append(cmd.Env, "TERM=xterm-256color") + return cmd + } return nil } @@ -144,32 +161,52 @@ func (c *SSHtoolSol) Connect() error { return nil } -func (c *SSHtoolSol) GetData(data string) (isShow bool, ouput string, command string) { - if len(c.Username) == 0 { - if len(data) == 0 { - //用户名不能为空 - return true, c.showInfo, "" +func (c *SSHtoolSol) Scan(d byte, send func(msg string)) { + switch d { + case '\r': // 换行 + send("\r\n") + if len(c.username) == 0 { + c.username = string(c.buffer) + c.needShowInfo = true + } else if len(c.password) == 0 { + c.password = string(c.buffer) + } + c.buffer = []byte{} + case '\u007f': // 退格 + if len(c.buffer) > 1 { + c.buffer = c.buffer[:len(c.buffer)-1] + } + send("\b \b") + default: + c.buffer = append(c.buffer, d) + if len(c.username) == 0 { + send(string(d)) } - c.Username = data - return false, "Password:", "" } - args := []string{ - o.Options.SshpassToolPath, "-p", data, - o.Options.SshToolPath, "-p", fmt.Sprintf("%d", c.Port), fmt.Sprintf("%s@%s", c.Username, c.IP), - "-oGlobalKnownHostsFile=/dev/null", "-oUserKnownHostsFile=/dev/null", "-oStrictHostKeyChecking=no", - "-oPreferredAuthentications=password", "-oPubkeyAuthentication=no", //密码登录时,避免搜寻秘钥登录 - } - return true, "", strings.Join(args, " ") + return +} + +func (c *SSHtoolSol) Reconnect() { + c.needShowInfo, c.username, c.password = true, "", "" +} + +func (c *SSHtoolSol) IsNeedShowInfo() bool { + return c.needShowInfo } func (c *SSHtoolSol) ShowInfo() string { - c.Username = "" - c.reTry++ - if c.reTry == 3 { - c.reTry = 0 - //清屏 - time.Sleep(1 * time.Second) - return "\033c " + c.showInfo + c.BaseCommand = nil + c.needShowInfo = false + if len(c.username) == 0 { + if c.failed >= 3 { + c.failed = 0 + return "\033c " + c.showInfo // 清屏 + } + c.failed++ + return c.showInfo } - return c.showInfo + if len(c.password) == 0 { + return "Password:" + } + return "" } diff --git a/pkg/webconsole/server/tty_server.go b/pkg/webconsole/server/tty_server.go index b3a152383d..2b2d4dee8b 100644 --- a/pkg/webconsole/server/tty_server.go +++ b/pkg/webconsole/server/tty_server.go @@ -15,11 +15,6 @@ package server import ( - "os/exec" - "strconv" - "strings" - "time" - socketio "github.com/googollee/go-socket.io" "github.com/kr/pty" @@ -74,77 +69,52 @@ func (server *TTYServer) initEventHandler(s *session.SSession) { func initSocketHandler(so socketio.Socket, p *session.Pty) { // handle read go func() { - buf := make([]byte, 1024) - for { - if p.IsOk { - if p.Cmd == nil || p.Cmd.Process == nil { - p.IsOk = false - } else if p.Pty == nil { - p.IsOk = false - } else if n, err := p.Pty.Read(buf); err != nil { - p.IsOk = false - } else { - so.Emit(OUTPUT_EVENT, string(buf[0:n])) - } - if !p.IsOk { - err := p.Stop() - //之前有cmd命令运行,并且正常退出,认为程序是正常退出的 - if err == nil && p.Cmd != nil { - so.Disconnect() - return - } + for !p.Exit { + if p.IsInShellMode() { + data, err := p.Read() + if err != nil { + log.Errorf("[%s] read data error: %v", so.Id(), err) + err = p.Stop() if err != nil { - log.Warningf("stop tty error: %v", err) - } - if info := p.Session.ShowInfo(); len(info) > 0 { - so.Emit(OUTPUT_EVENT, info) + log.Warningf("[%s] stop tty error: %v", so.Id(), err) } + p.Session.Reconnect() + } else { + so.Emit(OUTPUT_EVENT, string(data)) + } + continue + } + if p.Session.IsNeedShowInfo() { + info := p.Session.ShowInfo() + if len(info) > 0 { + so.Emit(OUTPUT_EVENT, info) } - } else if p.Exit { - return - } else { - //避免goroutine死循环导致主进程卡死 - time.Sleep(time.Microsecond * 50) } } }() // handle write so.On(INPUT_EVENT, func(data string) { - if !p.IsOk { - if data == "\r" { - p.Show, p.Output, p.Command = p.Session.GetData(p.Buffer) - so.Emit(OUTPUT_EVENT, "\r\n") - if len(p.Output) > 0 { - so.Emit(OUTPUT_EVENT, p.Output) - } - if len(p.Command) > 0 { - log.Infof("exec: %s", p.Command) - args := strings.Split(p.Command, " ") - cmd := exec.Command(args[0], args[1:]...) - cmd.Env = append(cmd.Env, "TERM=xterm-256color") - if _pty, err := pty.Start(cmd); err != nil { - so.Emit(OUTPUT_EVENT, err.Error()+"\r\n") - log.Errorf("exec error: %v", err) - } else { - p.Pty, p.Cmd, p.IsOk = _pty, cmd, true - if p.OriginSize != nil { - p.Resize(p.OriginSize) - } + if !p.IsInShellMode() { + for _, d := range []byte(data) { + p.Session.Scan(d, func(msg string) { + if len(msg) > 0 { + so.Emit(OUTPUT_EVENT, msg) } - } - p.Buffer, data = "", "" - } else if data == "\u007f" { - //退格处理 - if len(p.Buffer) > 0 { - p.Buffer = p.Buffer[:len(p.Buffer)-1] - data = "\b \b" - } - } else if strconv.IsPrint([]rune(data)[0]) { - p.Buffer += data + }) } - if p.Show && len(data) > 0 { - so.Emit(OUTPUT_EVENT, data) + cmd := p.Session.GetCommand() + if cmd != nil { + pty, err := pty.Start(cmd) + if err != nil { + log.Errorf("failed to start cmd: %v, error: %v", cmd, err) + so.Emit(OUTPUT_EVENT, err.Error()+"\r\n") + return + } + p.Pty, p.Cmd = pty, cmd + if p.OriginSize != nil { + p.Resize(p.OriginSize) + } } } else { p.Pty.Write([]byte(data)) diff --git a/pkg/webconsole/session/pty_session.go b/pkg/webconsole/session/pty_session.go index 00ed441738..f7dfd744cc 100644 --- a/pkg/webconsole/session/pty_session.go +++ b/pkg/webconsole/session/pty_session.go @@ -33,11 +33,6 @@ type Pty struct { sizeCh chan os.Signal size *pty.Winsize OriginSize *pty.Winsize - Show bool - IsOk bool - Buffer string - Output string - Command string Exit bool } @@ -46,8 +41,6 @@ func NewPty(session *SSession) (p *Pty, err error) { p = &Pty{ Session: session, Cmd: cmd, - Show: true, - IsOk: true, Exit: false, Pty: nil, } @@ -55,6 +48,8 @@ func NewPty(session *SSession) (p *Pty, err error) { if cmd != nil { p.Pty, err = pty.Start(p.Cmd) if err != nil { + log.Errorf("start cmd error: %v", err) + p.Cmd = nil return } } @@ -65,6 +60,25 @@ func NewPty(session *SSession) (p *Pty, err error) { return } +func (p *Pty) IsInShellMode() bool { + if p.Cmd == nil || p.Cmd.Process == nil { + return false + } + return true +} + +func (p *Pty) Read() ([]byte, error) { + if !p.IsInShellMode() { + return nil, errors.Error("not in shell mode") + } + buf := make([]byte, 1024) + n, err := p.Pty.Read(buf) + if err != nil { + return nil, errors.Wrap(err, "Pty.Read") + } + return buf[0:n], nil +} + func (p *Pty) startResizeMonitor() { go func() { for range p.sizeCh { @@ -99,6 +113,7 @@ func (p *Pty) Stop() (err error) { }() defer func() { if p.Cmd != nil && p.Cmd.Process != nil { + log.Debugf("[%s] stop cmd", p.Session.Id) err := p.Cmd.Process.Signal(os.Kill) if err != nil { errs = append(errs, err) @@ -109,6 +124,7 @@ func (p *Pty) Stop() (err error) { if err != nil { errs = append(errs, err) log.Errorf("Wait command error: %v", err) + return } } }() @@ -120,8 +136,11 @@ func (p *Pty) Stop() (err error) { errs = append(errs, err) return } - p.Pty = nil } }() + + defer func() { + p.Cmd, p.Pty = nil, nil + }() return } diff --git a/pkg/webconsole/session/remote_console.go b/pkg/webconsole/session/remote_console.go index dca3d1bd37..12496b5bef 100644 --- a/pkg/webconsole/session/remote_console.go +++ b/pkg/webconsole/session/remote_console.go @@ -90,14 +90,24 @@ func (info *RemoteConsoleInfo) Cleanup() error { return nil } -// GetData implements ISessionData interface +// Connect implements ISessionData interface func (info *RemoteConsoleInfo) Connect() error { return nil } -// GetData implements ISessionData interface -func (info *RemoteConsoleInfo) GetData(s string) (bool, string, string) { - return false, "", "" +// IsNeedShowInfo implements ISessionData interface +func (info *RemoteConsoleInfo) IsNeedShowInfo() bool { + return false +} + +// Reconnect implements ISessionData interface +func (info *RemoteConsoleInfo) Reconnect() { + return +} + +// Scan implements ISessionData interface +func (info *RemoteConsoleInfo) Scan(byte, func(string)) { + return } // ShowInfo implements ISessionData interface