forked from Gitlink/forgeplus-react
ssh-2
This commit is contained in:
parent
80cae93045
commit
a27c74b9aa
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "educoder",
|
||||
"name": "forge",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
|
@ -3888,11 +3888,6 @@
|
|||
"randomfill": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"crypto-js": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/crypto-js/download/crypto-js-4.0.0.tgz",
|
||||
"integrity": "sha1-KQSrJnep0EKFai6i74DekuSjbcw="
|
||||
},
|
||||
"crypto-random-string": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
|
||||
|
@ -20480,6 +20475,16 @@
|
|||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"xterm": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npm.taobao.org/xterm/download/xterm-4.8.1.tgz",
|
||||
"integrity": "sha1-FVoXKaQ+Gom0BlJOIsVjQznjnKE="
|
||||
},
|
||||
"xterm-addon-fit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npm.taobao.org/xterm-addon-fit/download/xterm-addon-fit-0.4.0.tgz",
|
||||
"integrity": "sha1-BuDF0KaqrPsAnvVl76HIHpPZAZM="
|
||||
},
|
||||
"y18n": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
|
||||
|
|
|
@ -111,7 +111,9 @@
|
|||
"webpack-dev-server": "^3.10.3",
|
||||
"webpack-manifest-plugin": "^2.2.0",
|
||||
"whatwg-fetch": "2.0.3",
|
||||
"wrap-md-editor": "^0.2.20"
|
||||
"wrap-md-editor": "^0.2.20",
|
||||
"xterm": "4.8.1",
|
||||
"xterm-addon-fit": "0.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --max_old_space_size=15360 scripts/start.js",
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||
import { Spin , Menu } from "antd";
|
||||
import { FlexAJ, AlignCenter } from "../Component/layout";
|
||||
import axios from "axios";
|
||||
import CodeSSH from './ssh/Index';
|
||||
|
||||
export default ({
|
||||
data,
|
||||
|
@ -68,10 +69,10 @@ export default ({
|
|||
}
|
||||
return (
|
||||
<Spin spinning={spining}>
|
||||
{/* <Menu className="devopsNav" onClick={(e)=>{setNav(e.key)}} selectedKeys={[nav]} mode="horizontal">
|
||||
<Menu className="devopsNav" onClick={(e)=>{setNav(e.key)}} selectedKeys={[nav]} mode="horizontal">
|
||||
<Menu.Item key={'0'} value="0">文件</Menu.Item>
|
||||
<Menu.Item key={'1'} value="1">命令行</Menu.Item>
|
||||
</Menu> */}
|
||||
</Menu>
|
||||
{
|
||||
nav === "0" &&
|
||||
<div className="rightMainContent">
|
||||
|
@ -111,7 +112,9 @@ export default ({
|
|||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
nav === "1" && <CodeSSH />
|
||||
}
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import React, { useState } from 'react';
|
||||
import XmlPanel from './XmlPanel';
|
||||
import mediator from './mediator';
|
||||
|
||||
function Index(){
|
||||
const [ sshConfigData ,setSshConfigData ] = useState(undefined);
|
||||
|
||||
return(
|
||||
<div>
|
||||
<XmlPanel sshConfigData={sshConfigData || {}} sid={1} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Index;
|
|
@ -0,0 +1,218 @@
|
|||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import 'xterm/css/xterm.css';
|
||||
import mediator from './mediator';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
function getColsAndRows(width, height, term) {
|
||||
let w = term._core._renderService.dimensions.actualCellWidth || 9.5;
|
||||
let h = term._core._renderService.dimensions.actualCellHeight || 18;
|
||||
const rows = Math.floor(height / h);
|
||||
const cols = Math.floor(width / w);
|
||||
return [cols, rows];
|
||||
}
|
||||
|
||||
function onLayout(term, el) {
|
||||
const ro = new ResizeObserver(entries => {
|
||||
for (let entry of entries) {
|
||||
if (entry.target.offsetHeight > 0 || entry.target.offsetWidth > 0) {
|
||||
const [cols, rows] = getColsAndRows(
|
||||
entry.target.offsetWidth,
|
||||
entry.target.offsetHeight,
|
||||
term,
|
||||
);
|
||||
console.log('cols, rows', cols, rows);
|
||||
term.resize(cols, rows);
|
||||
mediator.publish('ssh-xterm-resize', {
|
||||
columns: cols,
|
||||
rows: rows,
|
||||
width: entry.target.offsetWidth,
|
||||
height: entry.target.offsetHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
ro.observe(el);
|
||||
return ro;
|
||||
}
|
||||
|
||||
const TimeTicket = 30000;
|
||||
|
||||
//建立 websockt 来交互
|
||||
//根据容器大小计算行数和列数并做到自适应
|
||||
//socket 与 term 需要分开初始化 因为socket 可能重置连接
|
||||
//mediator 监听消息,如果和id匹配,则建立连接,重置,或关闭连接
|
||||
|
||||
export default ({ sshConfigData, sid }) => {
|
||||
const [term, setTerm] = useState(null);
|
||||
|
||||
const { ws_url, password, port } = sshConfigData;
|
||||
const el = useRef();
|
||||
const socket = useRef();
|
||||
const isFirstConnected = useRef(false);
|
||||
|
||||
//term init
|
||||
useEffect(() => {
|
||||
if (el.current && ws_url) {
|
||||
const term = new Terminal({ fontSize: 16, rendererType: 'dom' });
|
||||
term.open(el.current);
|
||||
|
||||
term.onData(data => {
|
||||
if (socket.current) {
|
||||
if (socket.current.readyState === 1) {
|
||||
socket.current.send(JSON.stringify({ tp: 'client', data: data }));
|
||||
mediator.publish('on-operating-ssh'); //有操作则自动延时
|
||||
} else {
|
||||
//断开连接后重连
|
||||
// socket.current = null
|
||||
// mediator.publish('create-socket', sid)
|
||||
}
|
||||
}
|
||||
});
|
||||
term.write('Connecting...');
|
||||
setTerm(term);
|
||||
const ro = onLayout(term, el.current);
|
||||
return () => {
|
||||
term.dispose();
|
||||
ro.unobserve(el.current);
|
||||
};
|
||||
}
|
||||
}, [ws_url, el.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (term && ws_url) {
|
||||
function createSocket() {
|
||||
const socketInstance = new WebSocket(ws_url);
|
||||
socket.current = socketInstance;
|
||||
|
||||
socketInstance.onopen = () => {
|
||||
let container = term.element.parentElement;
|
||||
if (container) {
|
||||
let width = container.offsetWidth;
|
||||
let height = container.offsetHeight;
|
||||
console.log('init', {
|
||||
tp: 'init',
|
||||
data: {
|
||||
...sshConfigData,
|
||||
secret: password,
|
||||
width,
|
||||
height,
|
||||
rows: term.rows,
|
||||
columns: term.cols,
|
||||
},
|
||||
});
|
||||
socketInstance.send(
|
||||
JSON.stringify({
|
||||
tp: 'init',
|
||||
data: {
|
||||
...sshConfigData,
|
||||
secret: password,
|
||||
width,
|
||||
height,
|
||||
rows: term.rows,
|
||||
columns: term.cols,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
term.focus();
|
||||
};
|
||||
socketInstance.onerror = error => {
|
||||
console.log(
|
||||
'------in socket error----',
|
||||
error,
|
||||
socketInstance,
|
||||
ws_url,
|
||||
);
|
||||
//连接报错后,重新请求资源
|
||||
// mediator.publish('on-recreate-socket')
|
||||
};
|
||||
socketInstance.onmessage = event => {
|
||||
if (!isFirstConnected.current) {
|
||||
term.write('\r');
|
||||
// term.focus()
|
||||
setTimeout(() => {
|
||||
// term.clear();
|
||||
}, 1000);
|
||||
}
|
||||
isFirstConnected.current = true;
|
||||
console.log('event:', event);
|
||||
|
||||
const data = Base64.decode(event.data.toString());
|
||||
let w = term._core._renderService.dimensions.actualCellWidth || 9.5;
|
||||
|
||||
console.log('data:', data, w, term);
|
||||
term.write(data);
|
||||
};
|
||||
|
||||
socketInstance.onclose = evt => {
|
||||
if (tid) {
|
||||
clearInterval(tid);
|
||||
}
|
||||
term.write('\r\nconnection closed');
|
||||
};
|
||||
}
|
||||
|
||||
const tid = setInterval(() => {
|
||||
if (socket.current) {
|
||||
socket.current.send(JSON.stringify({ tp: 'h' }));
|
||||
}
|
||||
}, TimeTicket);
|
||||
|
||||
const unSubCreate = mediator.subscribe('create-socket', id => {
|
||||
if (sid === id) {
|
||||
if (socket.current && socket.current.readyState === 1) {
|
||||
term.focus();
|
||||
} else {
|
||||
createSocket();
|
||||
}
|
||||
term.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const unSubClose = mediator.subscribe('close-socket', id => {
|
||||
if (sid === id) {
|
||||
if (socket.current) {
|
||||
socket.current.close();
|
||||
isFirstConnected.current = false;
|
||||
term.clear();
|
||||
}
|
||||
socket.current = null;
|
||||
}
|
||||
});
|
||||
|
||||
const unSubResize = mediator.subscribe('ssh-xterm-resize', option => {
|
||||
if (socket.current && socket.current.readyState === 1) {
|
||||
socket.current.send(
|
||||
JSON.stringify({ tp: 'resize', data: { ...option } }),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const unSubAddTime = mediator.subscribe('ssh-add-connect-time', () => {
|
||||
if (socket.current && socket.current.readyState === 1) {
|
||||
socket.current.send(JSON.stringify({ tp: 'overtime' }));
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unSubClose();
|
||||
unSubCreate();
|
||||
unSubResize();
|
||||
unSubAddTime();
|
||||
if (socket.current) {
|
||||
socket.current.close();
|
||||
isFirstConnected.current = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [term, ws_url, port]);
|
||||
|
||||
return (
|
||||
<div ref={el} className="xterm-panel">
|
||||
{!ws_url ? <p style={{ color: '#fff' }}>正在连接命令行服务...</p> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
function Mediator(obj) {
|
||||
const channels = {};
|
||||
|
||||
const mediator = {
|
||||
subscribe: function(channel, cb) {
|
||||
if (!channels[channel]) {
|
||||
channels[channel] = [];
|
||||
}
|
||||
channels[channel].push(cb);
|
||||
return this.unsubscribe.bind(null, channel, cb);
|
||||
},
|
||||
|
||||
unsubscribe: function(channel, cb) {
|
||||
let rs = channels[channel];
|
||||
let index = -1;
|
||||
if (rs) {
|
||||
for (let i = 0; i < rs.length; i++) {
|
||||
if (rs[i].name === cb.name) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
channels[channel].splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
publish: function(channel) {
|
||||
if (!channels[channel]) {
|
||||
return false;
|
||||
}
|
||||
const args = Array.prototype.slice.call(arguments, 1);
|
||||
channels[channel].forEach(subscription => {
|
||||
subscription.apply(null, args);
|
||||
});
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
return obj ? Object.assign(obj, mediator) : mediator;
|
||||
}
|
||||
const mediator = new Mediator();
|
||||
export default mediator;
|
Loading…
Reference in New Issue