优化批量执行指令和windows接入发送组合键
This commit is contained in:
parent
ba982dacd0
commit
038f59d155
102
pkg/api/ssh.go
102
pkg/api/ssh.go
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@ -46,6 +47,22 @@ func (w *NextWriter) Read() ([]byte, int, error) {
|
|||||||
return buf, read, err
|
return buf, read, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
Data = "data"
|
||||||
|
Resize = "resize"
|
||||||
|
Closed = "closed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowSize struct {
|
||||||
|
Height int `json:"height"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
}
|
||||||
|
|
||||||
func SSHEndpoint(c echo.Context) error {
|
func SSHEndpoint(c echo.Context) error {
|
||||||
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
|
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -60,12 +77,22 @@ func SSHEndpoint(c echo.Context) error {
|
|||||||
sshClient, err := CreateSshClient(assetId)
|
sshClient, err := CreateSshClient(assetId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("创建SSH客户端失败:%v", err.Error())
|
logrus.Errorf("创建SSH客户端失败:%v", err.Error())
|
||||||
|
msg := Message{
|
||||||
|
Type: Closed,
|
||||||
|
Content: err.Error(),
|
||||||
|
}
|
||||||
|
err := WriteMessage(ws, msg)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := sshClient.NewSession()
|
session, err := sshClient.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("创建SSH会话失败:%v", err.Error())
|
logrus.Errorf("创建SSH会话失败:%v", err.Error())
|
||||||
|
msg := Message{
|
||||||
|
Type: Closed,
|
||||||
|
Content: err.Error(),
|
||||||
|
}
|
||||||
|
err := WriteMessage(ws, msg)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
@ -93,15 +120,39 @@ func SSHEndpoint(c echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
msg := Message{
|
||||||
|
Type: Data,
|
||||||
|
Content: "Connect to server successfully.",
|
||||||
|
}
|
||||||
|
_ = WriteMessage(ws, msg)
|
||||||
|
|
||||||
|
var mut sync.Mutex
|
||||||
|
var active = true
|
||||||
|
|
||||||
|
go func() {
|
||||||
for true {
|
for true {
|
||||||
|
mut.Lock()
|
||||||
|
if !active {
|
||||||
|
logrus.Debugf("会话: %v -> %v 关闭", sshClient.LocalAddr().String(), sshClient.RemoteAddr().String())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
mut.Unlock()
|
||||||
|
|
||||||
p, n, err := b.Read()
|
p, n, err := b.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
WriteByteMessage(ws, p)
|
msg := Message{
|
||||||
|
Type: Data,
|
||||||
|
Content: string(p),
|
||||||
|
}
|
||||||
|
message, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("生成Json失败 %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
WriteByteMessage(ws, message)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Duration(100) * time.Millisecond)
|
time.Sleep(time.Duration(100) * time.Millisecond)
|
||||||
}
|
}
|
||||||
@ -110,16 +161,53 @@ func SSHEndpoint(c echo.Context) error {
|
|||||||
for true {
|
for true {
|
||||||
_, message, err := ws.ReadMessage()
|
_, message, err := ws.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// web socket会话关闭后主动关闭ssh会话
|
||||||
|
_ = session.Close()
|
||||||
|
mut.Lock()
|
||||||
|
active = false
|
||||||
|
mut.Unlock()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg Message
|
||||||
|
err = json.Unmarshal(message, &msg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("解析Json失败: %v, 原始字符串:%v", err, string(message))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err = stdinPipe.Write(message)
|
|
||||||
if err != nil {
|
switch msg.Type {
|
||||||
logrus.Debugf("Tunnel write: %v", err)
|
case Resize:
|
||||||
|
var winSize WindowSize
|
||||||
|
err = json.Unmarshal([]byte(msg.Content), &winSize)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("解析SSH会话窗口大小失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := session.WindowChange(winSize.Height, winSize.Height); err != nil {
|
||||||
|
logrus.Warnf("更改SSH会话窗口大小失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case Data:
|
||||||
|
_, err = stdinPipe.Write([]byte(msg.Content))
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("SSH会话写入失败: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WriteMessage(ws *websocket.Conn, msg Message) error {
|
||||||
|
message, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("生成Json失败 %v", err)
|
||||||
|
}
|
||||||
|
WriteByteMessage(ws, message)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func CreateSshClient(assetId string) (*ssh.Client, error) {
|
func CreateSshClient(assetId string) (*ssh.Client, error) {
|
||||||
asset, err := model.FindAssetById(assetId)
|
asset, err := model.FindAssetById(assetId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -195,10 +283,6 @@ func CreateSshClient(assetId string) (*ssh.Client, error) {
|
|||||||
return sshClient, nil
|
return sshClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteMessage(ws *websocket.Conn, message string) {
|
|
||||||
WriteByteMessage(ws, []byte(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteByteMessage(ws *websocket.Conn, p []byte) {
|
func WriteByteMessage(ws *websocket.Conn, p []byte) {
|
||||||
err := ws.WriteMessage(websocket.TextMessage, p)
|
err := ws.WriteMessage(websocket.TextMessage, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -89,6 +89,7 @@ func TunEndpoint(c echo.Context) error {
|
|||||||
configuration.SetParameter(guacd.DisableBitmapCaching, propertyMap[guacd.DisableBitmapCaching])
|
configuration.SetParameter(guacd.DisableBitmapCaching, propertyMap[guacd.DisableBitmapCaching])
|
||||||
configuration.SetParameter(guacd.DisableOffscreenCaching, propertyMap[guacd.DisableOffscreenCaching])
|
configuration.SetParameter(guacd.DisableOffscreenCaching, propertyMap[guacd.DisableOffscreenCaching])
|
||||||
configuration.SetParameter(guacd.DisableGlyphCaching, propertyMap[guacd.DisableGlyphCaching])
|
configuration.SetParameter(guacd.DisableGlyphCaching, propertyMap[guacd.DisableGlyphCaching])
|
||||||
|
configuration.SetParameter("server-layout", "en-us-qwerty")
|
||||||
break
|
break
|
||||||
case "ssh":
|
case "ssh":
|
||||||
if len(session.PrivateKey) > 0 && session.PrivateKey != "-" {
|
if len(session.PrivateKey) > 0 && session.PrivateKey != "-" {
|
||||||
|
20
web/package-lock.json
generated
20
web/package-lock.json
generated
@ -2537,14 +2537,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz",
|
||||||
"integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA=="
|
"integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA=="
|
||||||
},
|
},
|
||||||
"@types/http-proxy": {
|
|
||||||
"version": "1.17.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz",
|
|
||||||
"integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==",
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/istanbul-lib-coverage": {
|
"@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
|
||||||
@ -7972,18 +7964,6 @@
|
|||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"http-proxy-middleware": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==",
|
|
||||||
"requires": {
|
|
||||||
"@types/http-proxy": "^1.17.4",
|
|
||||||
"http-proxy": "^1.18.1",
|
|
||||||
"is-glob": "^4.0.1",
|
|
||||||
"lodash": "^4.17.20",
|
|
||||||
"micromatch": "^4.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"http-signature": {
|
"http-signature": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
"react-scripts": "^4.0.0",
|
"react-scripts": "^4.0.0",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^3.9.7",
|
||||||
"xterm": "^4.9.0",
|
"xterm": "^4.9.0",
|
||||||
"xterm-addon-web-links": "^0.4.0"
|
"xterm-addon-web-links": "^0.4.0",
|
||||||
|
"xterm-addon-fit": "^0.4.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
@ -28,7 +28,7 @@ import {
|
|||||||
CloudUploadOutlined,
|
CloudUploadOutlined,
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
DesktopOutlined,
|
DesktopOutlined, ExpandOutlined,
|
||||||
FileZipOutlined,
|
FileZipOutlined,
|
||||||
FolderAddOutlined,
|
FolderAddOutlined,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
@ -36,7 +36,7 @@ import {
|
|||||||
UploadOutlined
|
UploadOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import Upload from "antd/es/upload";
|
import Upload from "antd/es/upload";
|
||||||
import {download, getToken} from "../../utils/utils";
|
import {download, exitFull, getToken, requestFullScreen} from "../../utils/utils";
|
||||||
import './Access.css'
|
import './Access.css'
|
||||||
import Draggable from 'react-draggable';
|
import Draggable from 'react-draggable';
|
||||||
|
|
||||||
@ -87,7 +87,9 @@ class Access extends Component {
|
|||||||
confirmLoading: false,
|
confirmLoading: false,
|
||||||
uploadVisible: false,
|
uploadVisible: false,
|
||||||
uploadLoading: false,
|
uploadLoading: false,
|
||||||
startTime: new Date()
|
startTime: new Date(),
|
||||||
|
fullScreen: false,
|
||||||
|
fullScreenBtnText: '进入全屏'
|
||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
@ -345,6 +347,7 @@ class Access extends Component {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('--------------------')
|
||||||
console.log(keysym)
|
console.log(keysym)
|
||||||
this.state.client.sendKeyEvent(1, keysym);
|
this.state.client.sendKeyEvent(1, keysym);
|
||||||
if (keysym === 65288) {
|
if (keysym === 65288) {
|
||||||
@ -370,6 +373,24 @@ class Access extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fullScreen = () => {
|
||||||
|
let fs = this.state.fullScreen;
|
||||||
|
if(fs){
|
||||||
|
exitFull();
|
||||||
|
this.setState({
|
||||||
|
fullScreen: false,
|
||||||
|
fullScreenBtnText: '进入全屏'
|
||||||
|
})
|
||||||
|
}else {
|
||||||
|
requestFullScreen(document.documentElement);
|
||||||
|
this.setState({
|
||||||
|
fullScreen: true,
|
||||||
|
fullScreenBtnText: '退出全屏'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
showClipboard = () => {
|
showClipboard = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
clipboardVisible: true
|
clipboardVisible: true
|
||||||
@ -823,11 +844,18 @@ class Access extends Component {
|
|||||||
<Menu.Item key="2" icon={<FileZipOutlined/>} onClick={this.showFileSystem}>
|
<Menu.Item key="2" icon={<FileZipOutlined/>} onClick={this.showFileSystem}>
|
||||||
文件管理
|
文件管理
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item key="3" icon={<ExpandOutlined />} onClick={this.fullScreen}>
|
||||||
|
{this.state.fullScreenBtnText}
|
||||||
|
</Menu.Item>
|
||||||
<SubMenu title="发送快捷键" icon={<DesktopOutlined/>}>
|
<SubMenu title="发送快捷键" icon={<DesktopOutlined/>}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={() => this.sendCombinationKey(['65507', '65513', '65535'])}>Ctrl+Alt+Delete</Menu.Item>
|
onClick={() => this.sendCombinationKey(['65507', '65513', '65535'])}>Ctrl+Alt+Delete</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={() => this.sendCombinationKey(['65507', '65513', '65288'])}>Ctrl+Alt+Backspace</Menu.Item>
|
onClick={() => this.sendCombinationKey(['65507', '65513', '65288'])}>Ctrl+Alt+Backspace</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => this.sendCombinationKey(['65515', '114'])}>Windows+R</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => this.sendCombinationKey(['65515'])}>Windows</Menu.Item>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
@ -892,22 +920,6 @@ class Access extends Component {
|
|||||||
</Affix>
|
</Affix>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
|
||||||
{/*{*/}
|
|
||||||
{/* this.state.protocol === 'ssh' || this.state.protocol === 'rdp' ?*/}
|
|
||||||
{/* <Affix style={{position: 'absolute', top: 50, right: 50}}>*/}
|
|
||||||
{/* <Button*/}
|
|
||||||
{/* shape="circle"*/}
|
|
||||||
{/* icon={<FolderOpenOutlined/>}*/}
|
|
||||||
{/* onClick={() => {*/}
|
|
||||||
{/* this.showFileSystem();*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/* </Affix>*/}
|
|
||||||
{/* : null*/}
|
|
||||||
{/*}*/}
|
|
||||||
|
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={title}
|
title={title}
|
||||||
placement="right"
|
placement="right"
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import "xterm/css/xterm.css"
|
import "xterm/css/xterm.css"
|
||||||
import {Terminal} from "xterm";
|
import {Terminal} from "xterm";
|
||||||
import {AttachAddon} from 'xterm-addon-attach';
|
|
||||||
import qs from "qs";
|
import qs from "qs";
|
||||||
import {wsServer} from "../../common/constants";
|
import {wsServer} from "../../common/constants";
|
||||||
import "./Console.css"
|
import "./Console.css"
|
||||||
import {getToken} from "../../utils/utils";
|
import {getToken} from "../../utils/utils";
|
||||||
|
import {FitAddon} from 'xterm-addon-fit'
|
||||||
|
|
||||||
|
|
||||||
function getGeometry(width, height) {
|
function getGeometry(width, height) {
|
||||||
const cols = Math.floor(width / 9);
|
const cols = Math.floor(width / 9);
|
||||||
const rows = Math.floor(height / 17);
|
const rows = Math.floor(height / 17) - 1;
|
||||||
return [cols, rows];
|
return [cols, rows];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,9 +18,11 @@ class Console extends Component {
|
|||||||
|
|
||||||
state = {
|
state = {
|
||||||
containerOverflow: 'hidden',
|
containerOverflow: 'hidden',
|
||||||
containerWidth: 0,
|
width: 0,
|
||||||
containerHeight: 0,
|
height: 0,
|
||||||
term: null,
|
term: undefined,
|
||||||
|
webSocket: undefined,
|
||||||
|
fitAddon: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -30,9 +32,6 @@ class Console extends Component {
|
|||||||
let width = this.props.width;
|
let width = this.props.width;
|
||||||
let height = this.props.height;
|
let height = this.props.height;
|
||||||
|
|
||||||
// let width = Math.floor(window.innerWidth * scale);
|
|
||||||
// let height = Math.floor(window.innerHeight * scale);
|
|
||||||
|
|
||||||
let params = {
|
let params = {
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
@ -41,66 +40,127 @@ class Console extends Component {
|
|||||||
|
|
||||||
let paramStr = qs.stringify(params);
|
let paramStr = qs.stringify(params);
|
||||||
|
|
||||||
let [cols, rows] = getGeometry(width, height);
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
let lineHeight = 1;
|
||||||
|
if (ua.includes('windows')) {
|
||||||
|
lineHeight = 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
let term = new Terminal({
|
let term = new Terminal({
|
||||||
cols: cols,
|
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
|
||||||
rows: rows,
|
fontSize: 14,
|
||||||
// screenKeys: true,
|
lineHeight: lineHeight,
|
||||||
// fontFamily: 'menlo',
|
theme: {
|
||||||
|
background: '#1b1b1b'
|
||||||
|
},
|
||||||
|
rightClickSelectsWord: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// let fitAddon = new FitAddon();
|
let fitAddon = new FitAddon();
|
||||||
// term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
term.open(this.refs.terminal);
|
term.open(this.refs.terminal);
|
||||||
// fitAddon.fit();
|
|
||||||
|
|
||||||
term.writeln('正在努力连接服务器中...');
|
term.writeln('Trying to connect to the server ...');
|
||||||
term.onResize(e => {
|
term.onResize(e => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
term.onData(data => {
|
||||||
|
let webSocket = this.state.webSocket;
|
||||||
|
if (webSocket !== undefined) {
|
||||||
|
webSocket.send(JSON.stringify({type: 'data', content: data}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let token = getToken();
|
let token = getToken();
|
||||||
|
|
||||||
let webSocket = new WebSocket(wsServer + '/ssh?X-Auth-Token=' + token + '&' + paramStr);
|
let webSocket = new WebSocket(wsServer + '/ssh?X-Auth-Token=' + token + '&' + paramStr);
|
||||||
term.loadAddon(new AttachAddon(webSocket));
|
|
||||||
this.props.appendWebsocket(webSocket);
|
this.props.appendWebsocket({'id': assetId, 'ws': webSocket});
|
||||||
|
|
||||||
webSocket.onopen = (e => {
|
webSocket.onopen = (e => {
|
||||||
term.clear();
|
this.onWindowResize();
|
||||||
term.focus();
|
|
||||||
|
|
||||||
if (command !== '') {
|
|
||||||
webSocket.send(command + String.fromCharCode(13));
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
webSocket.onerror = (e) => {
|
||||||
|
term.writeln("Failed to connect to server.");
|
||||||
|
}
|
||||||
|
webSocket.onclose = (e) => {
|
||||||
|
term.writeln("Connection is closed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let executedCommand = false
|
||||||
|
webSocket.onmessage = (e) => {
|
||||||
|
let msg = JSON.parse(e.data);
|
||||||
|
switch (msg['type']) {
|
||||||
|
case 'data':
|
||||||
|
term.write(msg['content']);
|
||||||
|
break;
|
||||||
|
case 'closed':
|
||||||
|
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
|
||||||
|
webSocket.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!executedCommand) {
|
||||||
|
if (command !== '') {
|
||||||
|
let webSocket = this.state.webSocket;
|
||||||
|
if (webSocket !== undefined && webSocket.readyState === WebSocket.OPEN) {
|
||||||
|
webSocket.send(JSON.stringify({type: 'data', content: command + String.fromCharCode(13)}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executedCommand = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
term: term,
|
term: term,
|
||||||
containerWidth: width,
|
fitAddon: fitAddon,
|
||||||
containerHeight: height
|
webSocket: webSocket,
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
});
|
});
|
||||||
|
|
||||||
// window.addEventListener('resize', this.onWindowResize);
|
window.addEventListener('resize', this.onWindowResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
let webSocket = this.state.webSocket;
|
||||||
|
if (webSocket) {
|
||||||
|
webSocket.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onWindowResize = (e) => {
|
onWindowResize = (e) => {
|
||||||
let term = this.state.term;
|
let term = this.state.term;
|
||||||
if (term) {
|
let fitAddon = this.state.fitAddon;
|
||||||
const [cols, rows] = getGeometry(this.state.containerWidth, this.state.containerHeight);
|
let webSocket = this.state.webSocket;
|
||||||
term.resize(cols, rows);
|
if (term && fitAddon && webSocket) {
|
||||||
|
|
||||||
|
let height = term.cols;
|
||||||
|
let width = term.rows;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fitAddon.fit();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
term.focus();
|
||||||
|
if(webSocket.readyState === WebSocket.OPEN){
|
||||||
|
webSocket.send(JSON.stringify({type: 'resize', content: JSON.stringify({height, width})}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div ref='terminal' style={{
|
<div ref='terminal' id='terminal' style={{
|
||||||
overflow: this.state.containerOverflow,
|
overflow: this.state.containerOverflow,
|
||||||
width: this.state.containerWidth,
|
width: this.state.width,
|
||||||
height: this.state.containerHeight,
|
height: this.state.height,
|
||||||
backgroundColor: 'black'
|
backgroundColor: '#1b1b1b'
|
||||||
}}/>
|
}}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -249,6 +249,8 @@ class Asset extends Component {
|
|||||||
} else {
|
} else {
|
||||||
asset['tags'] = asset['tags'].split(',');
|
asset['tags'] = asset['tags'].split(',');
|
||||||
}
|
}
|
||||||
|
}else {
|
||||||
|
asset['tags'] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import {List, Card, Input, PageHeader} from "antd";
|
import {Card, Input, List, PageHeader, Popconfirm} from "antd";
|
||||||
import Console from "../access/Console";
|
import Console from "../access/Console";
|
||||||
import {itemRender} from "../../utils/utils";
|
import {itemRender} from "../../utils/utils";
|
||||||
import Logout from "../user/Logout";
|
import Logout from "../user/Logout";
|
||||||
|
import './Command.css'
|
||||||
|
|
||||||
const {Search} = Input;
|
const {Search} = Input;
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -25,7 +27,8 @@ class BatchCommand extends Component {
|
|||||||
|
|
||||||
state = {
|
state = {
|
||||||
webSockets: [],
|
webSockets: [],
|
||||||
assets: []
|
assets: [],
|
||||||
|
active: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -66,7 +69,13 @@ class BatchCommand extends Component {
|
|||||||
<div className="page-search">
|
<div className="page-search">
|
||||||
<Search ref={this.commandRef} placeholder="请输入指令" onSearch={value => {
|
<Search ref={this.commandRef} placeholder="请输入指令" onSearch={value => {
|
||||||
for (let i = 0; i < this.state.webSockets.length; i++) {
|
for (let i = 0; i < this.state.webSockets.length; i++) {
|
||||||
this.state.webSockets[i].send(value + String.fromCharCode(13))
|
let ws = this.state.webSockets[i]['ws'];
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'data',
|
||||||
|
content: value + String.fromCharCode(13)
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.commandRef.current.setValue('');
|
this.commandRef.current.setValue('');
|
||||||
}} enterButton='执行'/>
|
}} enterButton='执行'/>
|
||||||
@ -78,10 +87,23 @@ class BatchCommand extends Component {
|
|||||||
dataSource={this.state.assets}
|
dataSource={this.state.assets}
|
||||||
renderItem={item => (
|
renderItem={item => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Card title={item.name}>
|
<Card title={item.name}
|
||||||
|
className={this.state.active === item['id'] ? 'command-active' : ''}
|
||||||
|
onClick={() => {
|
||||||
|
if (this.state.active === item['id']) {
|
||||||
|
this.setState({
|
||||||
|
active: undefined
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
active: item['id']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Console assetId={item.id} command={this.state.command}
|
<Console assetId={item.id} command={this.state.command}
|
||||||
width={(window.innerWidth - 350) / 2}
|
width={(window.innerWidth - 350) / 2}
|
||||||
height={400}
|
height={420}
|
||||||
appendWebsocket={this.appendWebsocket}/>
|
appendWebsocket={this.appendWebsocket}/>
|
||||||
</Card>
|
</Card>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
4
web/src/components/command/Command.css
Normal file
4
web/src/components/command/Command.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.command-active {
|
||||||
|
box-shadow: 0 0 0 2px #1890FF;
|
||||||
|
outline: 2px solid #1890FF;
|
||||||
|
}
|
@ -78,7 +78,7 @@ export const cloneObj = (obj, ignoreFields) => {
|
|||||||
export function download(url) {
|
export function download(url) {
|
||||||
let aElement = document.createElement('a');
|
let aElement = document.createElement('a');
|
||||||
aElement.setAttribute('download', '');
|
aElement.setAttribute('download', '');
|
||||||
aElement.setAttribute('target', '_blank');
|
// aElement.setAttribute('target', '_blank');
|
||||||
aElement.setAttribute('href', url);
|
aElement.setAttribute('href', url);
|
||||||
aElement.click();
|
aElement.click();
|
||||||
}
|
}
|
||||||
@ -177,3 +177,36 @@ export function difference(a, b) {
|
|||||||
let bSet = new Set(b)
|
let bSet = new Set(b)
|
||||||
return Array.from(new Set(a.concat(b).filter(v => !aSet.has(v) || !bSet.has(v))))
|
return Array.from(new Set(a.concat(b).filter(v => !aSet.has(v) || !bSet.has(v))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function requestFullScreen(element) {
|
||||||
|
// 判断各种浏览器,找到正确的方法
|
||||||
|
const requestMethod = element.requestFullScreen || //W3C
|
||||||
|
element.webkitRequestFullScreen || //FireFox
|
||||||
|
element.mozRequestFullScreen || //Chrome等
|
||||||
|
element.msRequestFullScreen; //IE11
|
||||||
|
if (requestMethod) {
|
||||||
|
requestMethod.call(element);
|
||||||
|
} else if (typeof window.ActiveXObject !== "undefined") { //for Internet Explorer
|
||||||
|
const wScript = new window.ActiveXObject("WScript.Shell");
|
||||||
|
if (wScript !== null) {
|
||||||
|
wScript.SendKeys("{F11}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//退出全屏 判断浏览器种类
|
||||||
|
export function exitFull() {
|
||||||
|
// 判断各种浏览器,找到正确的方法
|
||||||
|
const exitMethod = document.exitFullscreen || //W3C
|
||||||
|
document.mozCancelFullScreen || //FireFox
|
||||||
|
document.webkitExitFullscreen || //Chrome等
|
||||||
|
document.webkitExitFullscreen; //IE11
|
||||||
|
if (exitMethod) {
|
||||||
|
exitMethod.call(document);
|
||||||
|
} else if (typeof window.ActiveXObject !== "undefined") { //for Internet Explorer
|
||||||
|
const wScript = new window.ActiveXObject("WScript.Shell");
|
||||||
|
if (wScript !== null) {
|
||||||
|
wScript.SendKeys("{F11}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user