实现可运行的xterm.js方案
This commit is contained in:
parent
86ef89ff21
commit
29fb520e48
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"next-terminal/pkg/utils"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -26,22 +27,29 @@ type Recorder struct {
|
||||
timestamp int
|
||||
}
|
||||
|
||||
func NewRecorder(filename string) (recorder *Recorder, err error) {
|
||||
func NewRecorder(dir string) (recorder *Recorder, filename string, err error) {
|
||||
recorder = &Recorder{}
|
||||
|
||||
if utils.FileExists(filename) {
|
||||
if err := os.RemoveAll(filename); err != nil {
|
||||
return nil, err
|
||||
if utils.FileExists(dir) {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(dir, 0777); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
filename = path.Join(dir, "recording.cast")
|
||||
|
||||
var file *os.File
|
||||
file, err = os.Create(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
recorder.file = file
|
||||
return recorder, nil
|
||||
return recorder, filename, nil
|
||||
}
|
||||
|
||||
func (recorder *Recorder) Close() {
|
||||
|
@ -38,7 +38,13 @@ func SessionPagingEndpoint(c echo.Context) error {
|
||||
|
||||
for i := 0; i < len(items); i++ {
|
||||
if status == model.Disconnected && len(items[i].Recording) > 0 {
|
||||
recording := items[i].Recording + "/recording"
|
||||
|
||||
var recording string
|
||||
if items[i].Protocol == "rdp" || items[i].Protocol == "vnc" {
|
||||
recording = items[i].Recording + "/recording"
|
||||
} else {
|
||||
recording = items[i].Recording
|
||||
}
|
||||
|
||||
if utils.FileExists(recording) {
|
||||
logrus.Debugf("检测到录屏文件[%v]存在", recording)
|
||||
@ -107,12 +113,14 @@ func CloseSessionById(sessionId string, code int, reason string) {
|
||||
observable, _ := global.Store.Get(sessionId)
|
||||
if observable != nil {
|
||||
logrus.Debugf("会话%v创建者退出", observable.Subject.Tunnel.UUID)
|
||||
_ = observable.Subject.Tunnel.Close()
|
||||
observable.Subject.Close()
|
||||
|
||||
for i := 0; i < len(observable.Observers); i++ {
|
||||
_ = observable.Observers[i].Tunnel.Close()
|
||||
observable.Observers[i].Close()
|
||||
CloseWebSocket(observable.Observers[i].WebSocket, code, reason)
|
||||
logrus.Debugf("强制踢出会话%v的观察者", observable.Observers[i].Tunnel.UUID)
|
||||
}
|
||||
|
||||
CloseWebSocket(observable.Subject.WebSocket, code, reason)
|
||||
}
|
||||
global.Store.Del(sessionId)
|
||||
@ -529,8 +537,15 @@ func SessionRecordingEndpoint(c echo.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recording := path.Join(session.Recording, "recording")
|
||||
logrus.Debugf("读取录屏文件:%s", recording)
|
||||
|
||||
var recording string
|
||||
if session.Protocol == "rdp" || session.Protocol == "vnc" {
|
||||
recording = session.Recording + "/recording"
|
||||
} else {
|
||||
recording = session.Recording
|
||||
}
|
||||
|
||||
logrus.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording))
|
||||
return c.File(recording)
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,10 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"net/http"
|
||||
"next-terminal/pkg/guacd"
|
||||
"next-terminal/pkg/model"
|
||||
"next-terminal/pkg/utils"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@ -65,7 +67,7 @@ type WindowSize struct {
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
func SSHEndpoint(c echo.Context) error {
|
||||
func SSHEndpoint(c echo.Context) (err error) {
|
||||
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
|
||||
if err != nil {
|
||||
logrus.Errorf("升级为WebSocket协议失败:%v", err.Error())
|
||||
@ -149,15 +151,18 @@ func SSHEndpoint(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := Message{
|
||||
Type: Connected,
|
||||
Content: "Connect to server successfully.\r\n",
|
||||
}
|
||||
_ = WriteMessage(ws, msg)
|
||||
|
||||
recorder, err := NewRecorder("./" + sessionId + ".cast")
|
||||
var recorder *Recorder
|
||||
var recording string
|
||||
property, _ := model.FindPropertyByName(guacd.RecordingPath)
|
||||
if property.Value != "" {
|
||||
dir := path.Join(property.Value, sessionId)
|
||||
recorder, recording, err = NewRecorder(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
msg := Message{
|
||||
Type: Closed,
|
||||
Content: "创建录屏文件失败 :( " + err.Error(),
|
||||
}
|
||||
return WriteMessage(ws, msg)
|
||||
}
|
||||
|
||||
header := &Header{
|
||||
@ -173,6 +178,17 @@ func SSHEndpoint(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := model.UpdateSessionById(&model.Session{Recording: recording}, sessionId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
msg := Message{
|
||||
Type: Connected,
|
||||
Content: "Connect to server successfully.\r\n",
|
||||
}
|
||||
_ = WriteMessage(ws, msg)
|
||||
|
||||
var mut sync.Mutex
|
||||
var active = true
|
||||
|
||||
@ -181,7 +197,10 @@ func SSHEndpoint(c echo.Context) error {
|
||||
mut.Lock()
|
||||
if !active {
|
||||
logrus.Debugf("会话: %v -> %v 关闭", sshClient.LocalAddr().String(), sshClient.RemoteAddr().String())
|
||||
if recorder != nil {
|
||||
recorder.Close()
|
||||
}
|
||||
CloseSessionById(sessionId, Normal, "正常退出")
|
||||
break
|
||||
}
|
||||
mut.Unlock()
|
||||
@ -192,8 +211,10 @@ func SSHEndpoint(c echo.Context) error {
|
||||
}
|
||||
if n > 0 {
|
||||
s := string(p)
|
||||
if recorder != nil {
|
||||
// 录屏
|
||||
_ = recorder.WriteData(s)
|
||||
}
|
||||
msg := Message{
|
||||
Type: Data,
|
||||
Content: s,
|
||||
|
@ -145,6 +145,7 @@ func TunEndpoint(c echo.Context) error {
|
||||
}
|
||||
|
||||
tun := global.Tun{
|
||||
Protocol: configuration.Protocol,
|
||||
Tunnel: tunnel,
|
||||
WebSocket: ws,
|
||||
}
|
||||
|
@ -3,16 +3,28 @@ package global
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"next-terminal/pkg/guacd"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Tun struct {
|
||||
Protocol string
|
||||
Tunnel *guacd.Tunnel
|
||||
SshClient *ssh.Client
|
||||
SftpClient *sftp.Client
|
||||
WebSocket *websocket.Conn
|
||||
}
|
||||
|
||||
func (r *Tun) Close() {
|
||||
if r.Protocol == "rdp" || r.Protocol == "vnc" {
|
||||
_ = r.Tunnel.Close()
|
||||
} else {
|
||||
_ = r.SshClient.Close()
|
||||
_ = r.SftpClient.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type Observable struct {
|
||||
Subject *Tun
|
||||
Observers []Tun
|
||||
|
38
web/public/asciinema.html
Normal file
38
web/public/asciinema.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<link rel="stylesheet" type="text/css" href="asciinema-player.css"/>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<asciinema-player id='asciinema-player' src=""></asciinema-player>
|
||||
<script src="asciinema-player.js"></script>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
|
||||
const server = 'http://localhost:8088';
|
||||
|
||||
function getQueryVariable(variable) {
|
||||
const query = window.location.search.substring(1);
|
||||
const vars = query.split("&");
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
const pair = vars[i].split("=");
|
||||
if (pair[0] === variable) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let sessionId = getQueryVariable('sessionId');
|
||||
document.getElementById('asciinema-player').setAttribute('src', `${server}/sessions/${sessionId}/recording`);
|
||||
</script>
|
||||
</html>
|
@ -8,6 +8,7 @@ import {getToken, isEmpty} from "../../utils/utils";
|
||||
import {FitAddon} from 'xterm-addon-fit';
|
||||
import "./Access.css"
|
||||
import request from "../../common/request";
|
||||
import {message} from "antd";
|
||||
|
||||
class AccessSSH extends Component {
|
||||
|
||||
@ -111,8 +112,8 @@ class AccessSSH extends Component {
|
||||
switch (msg['type']) {
|
||||
case 'connected':
|
||||
term.clear();
|
||||
console.log(msg['content'])
|
||||
this.onWindowResize();
|
||||
this.updateSessionStatus(sessionId);
|
||||
break;
|
||||
case 'data':
|
||||
term.write(msg['content']);
|
||||
@ -152,6 +153,13 @@ class AccessSSH extends Component {
|
||||
return result['data']['id'];
|
||||
}
|
||||
|
||||
updateSessionStatus = async (sessionId) => {
|
||||
let result = await request.post(`/sessions/${sessionId}/connect`);
|
||||
if (result['code'] !== 1) {
|
||||
message.error(result['message']);
|
||||
}
|
||||
}
|
||||
|
||||
terminalSize() {
|
||||
return {
|
||||
cols: Math.floor(this.state.width / 7.5),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
@ -64,6 +63,7 @@ class OfflineSession extends Component {
|
||||
delBtnLoading: false,
|
||||
users: [],
|
||||
assets: [],
|
||||
selectedRow: {},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -123,10 +123,10 @@ class OfflineSession extends Component {
|
||||
this.loadTableData(queryParams)
|
||||
};
|
||||
|
||||
showPlayback = (sessionId) => {
|
||||
showPlayback = (row) => {
|
||||
this.setState({
|
||||
playbackVisible: true,
|
||||
playbackSessionId: sessionId
|
||||
selectedRow: row
|
||||
});
|
||||
};
|
||||
|
||||
@ -289,7 +289,7 @@ class OfflineSession extends Component {
|
||||
<div>
|
||||
<Button type="link" size='small'
|
||||
disabled={disabled}
|
||||
onClick={() => this.showPlayback(record.id)}>回放</Button>
|
||||
onClick={() => this.showPlayback(record)}>回放</Button>
|
||||
<Button type="link" size='small' onClick={() => {
|
||||
confirm({
|
||||
title: '您确定要删除此会话吗?',
|
||||
@ -488,7 +488,30 @@ class OfflineSession extends Component {
|
||||
destroyOnClose
|
||||
maskClosable={false}
|
||||
>
|
||||
<Playback sessionId={this.state.playbackSessionId}/>
|
||||
{
|
||||
this.state.selectedRow['protocol'] === 'rdp' || this.state.selectedRow['protocol'] === 'vnc' ?
|
||||
<Playback sessionId={this.state.selectedRow['id']}/>
|
||||
:
|
||||
<iframe
|
||||
style={{
|
||||
width: '100%',
|
||||
// height: this.state.iFrameHeight,
|
||||
overflow: 'visible'
|
||||
}}
|
||||
onLoad={() => {
|
||||
// const obj = ReactDOM.findDOMNode(this);
|
||||
// this.setState({
|
||||
// "iFrameHeight": obj.contentWindow.document.body.scrollHeight + 'px'
|
||||
// });
|
||||
}}
|
||||
ref="iframe"
|
||||
src={'./asciinema.html?sessionId=' + this.state.selectedRow['id']}
|
||||
width="100%"
|
||||
height={window.innerHeight * 0.8}
|
||||
frameBorder="0"
|
||||
/>
|
||||
}
|
||||
|
||||
</Modal> : undefined
|
||||
}
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<link rel="stylesheet" type="text/css" href="asciinema-player.css"/>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<asciinema-player src="./903dfd65-838c-453f-9866-eadd5725321b.cast"></asciinema-player>
|
||||
<script src="asciinema-player.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user