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",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
|
@ -3888,11 +3888,6 @@
|
||||||
"randomfill": "^1.0.3"
|
"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": {
|
"crypto-random-string": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
|
"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==",
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
"dev": true
|
"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": {
|
"y18n": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
|
||||||
|
|
|
@ -111,7 +111,9 @@
|
||||||
"webpack-dev-server": "^3.10.3",
|
"webpack-dev-server": "^3.10.3",
|
||||||
"webpack-manifest-plugin": "^2.2.0",
|
"webpack-manifest-plugin": "^2.2.0",
|
||||||
"whatwg-fetch": "2.0.3",
|
"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": {
|
"scripts": {
|
||||||
"start": "node --max_old_space_size=15360 scripts/start.js",
|
"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 { Spin , Menu } from "antd";
|
||||||
import { FlexAJ, AlignCenter } from "../Component/layout";
|
import { FlexAJ, AlignCenter } from "../Component/layout";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import CodeSSH from './ssh/Index';
|
||||||
|
|
||||||
export default ({
|
export default ({
|
||||||
data,
|
data,
|
||||||
|
@ -68,10 +69,10 @@ export default ({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Spin spinning={spining}>
|
<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={'0'} value="0">文件</Menu.Item>
|
||||||
<Menu.Item key={'1'} value="1">命令行</Menu.Item>
|
<Menu.Item key={'1'} value="1">命令行</Menu.Item>
|
||||||
</Menu> */}
|
</Menu>
|
||||||
{
|
{
|
||||||
nav === "0" &&
|
nav === "0" &&
|
||||||
<div className="rightMainContent">
|
<div className="rightMainContent">
|
||||||
|
@ -111,7 +112,9 @@ export default ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
nav === "1" && <CodeSSH />
|
||||||
|
}
|
||||||
</Spin>
|
</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