1.flask-sock初始化
# 由于flask 项目启动就要注册websocket,则使用app项目启动反向蓝图注册
##### 下方单独使用文件夹websockets做管理
# 文件名:sock_manager.py ### sock实例初始化
from flask_sock import Sock
# 创建 Sock 实例
sock = Sock()
def init_sock(app):
"""初始化 Sock 实例"""
sock.init_app(app)
# 文件名:ssh_connect.py ### websocket终端交互,保持长连接防止终端,并且设定15分钟终端无操作则自动退出
import time
import paramiko
import threading
from applications.websockets.sock_manager import sock
from applications.models import Ssh_connect
from flask import Blueprint, request
import logging
import io
import simple_websocket
logger = logging.getLogger(__name__)
bp = Blueprint('connect', __name__, url_prefix='/connect')
class SSHConnection:
def __init__(self, host, port, username, password, private_keys):
self.host = host
self.port = port
self.username = username
self.password = password
self.private_keys = private_keys
self.ssh_client = None
self.ssh_channel = None
self.last_activity_time = time.time()
self.timeout = 15 * 60 # 控制服务器多少分钟没接收前端请求则主动断开连接
self.lock = threading.Lock()
self.active = True
def init_ssh_connection(self, cols=220, rows=80):
self.ssh_client = paramiko.SSHClient()
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh_client.load_system_host_keys()
try:
if self.private_keys:
private_key = paramiko.RSAKey(file_obj=io.StringIO(self.private_keys))
self.ssh_client.connect(self.host, port=self.port, username=self.username, pkey=private_key)
logger.info("SSH connection established using public key.")
else:
self.ssh_client.connect(self.host, port=self.port, username=self.username, password=self.password, look_for_keys=False)
logger.info("SSH connection established using password.")
self.ssh_channel = self.ssh_client.invoke_shell(term='xterm-256color')
self.ssh_channel.settimeout(0.1)
self.ssh_channel.resize_pty(width=cols, height=rows)
except paramiko.AuthenticationException:
logger.error("Authentication failed, please verify your credentials.")
except paramiko.SSHException as e:
logger.error(f"Unable to establish SSH connection: {e}")
except Exception as e:
logger.error(f"连接错误,请检查账号密码以及ip信息是否正确: {e}")
if not self.ssh_channel:
logger.error("SSH channel could not be created.")
def stream_output(self, ws):
buffer_size = 1024 * 1024 # 1 MB buffer size
while self.active:
if self.ssh_channel and self.ssh_channel.recv_ready():
try:
output = self.ssh_channel.recv(buffer_size).decode('utf-8')
if output:
ws.send(output)
with self.lock:
self.last_activity_time = time.time()
except Exception as e:
logger.error(f"Error streaming output: {e}")
break
time.sleep(0.1)
def handle_commands(self, ws):
while self.active:
try:
command = ws.receive()
if command:
logger.info(f"Received command: {command}")
if self.ssh_channel:
try:
self.ssh_channel.send(command)
with self.lock:
self.last_activity_time = time.time()
except Exception as e:
logger.error(f"Error sending command to SSH: {e}")
ws.send(f"Error: {e}")
except simple_websocket.errors.ConnectionClosed as e:
logger.error(f"WebSocket closed: {e}")
break
except Exception as e:
logger.error(f"WebSocket error: {e}")
ws.send(f"WebSocket error: {e}")
break
def monitor_timeout(self, ws):
while self.active:
time.sleep(10) # Check every 10 seconds
with self.lock:
if time.time() - self.last_activity_time > self.timeout:
ws.send("终端在15分钟内没有任何操作,主动断开连接,请重新连接机器!")
self.close()
break
def close(self):
self.active = False
if self.ssh_channel:
self.ssh_channel.close()
if self.ssh_client:
self.ssh_client.close()
logger.info("SSH connection closed.")
@sock.route('/ws/term')
def term(ws):
id = request.args.get('id')
if not id:
ws.send("Error: Missing ID")
logger.error("Error: Missing ID")
return
try:
id = int(id)
except ValueError:
ws.send("Error: Invalid ID format")
logger.error("Error: Invalid ID format")
return
record = Ssh_connect.query.filter_by(id=id).first()
if not record:
ws.send("Error: Record not found")
logger.error("Error: Record not found")
return
host = record.ipaddr
port = int(record.ports)
username = record.users
password = record.passwd
private_keys = record.secerts
ssh_connection = SSHConnection(host, port, username, password, private_keys)
ssh_connection.init_ssh_connection()
if ssh_connection.ssh_channel:
try:
ws.send('Connection established.\r\n')
except Exception as e:
logger.error(f"Error sending initial message: {e}")
return
output_thread = threading.Thread(target=ssh_connection.stream_output, args=(ws,))
command_thread = threading.Thread(target=ssh_connection.handle_commands, args=(ws,))
timeout_thread = threading.Thread(target=ssh_connection.monitor_timeout, args=(ws,))
output_thread.start()
command_thread.start()
timeout_thread.start()
try:
while ssh_connection.active:
time.sleep(1)
except KeyboardInterrupt:
logger.info("Server shutting down.")
finally:
ssh_connection.close()
output_thread.join(timeout=5)
command_thread.join(timeout=5)
timeout_thread.join(timeout=5)
# 文件名:__init__.py ## 项目init模仿蓝图方式
# applications/websockets/__init__.py
from flask import Flask
from applications.websockets.sock_manager import init_sock
from applications.websockets.ssh_connect import bp as ssh_connect
def register_websockets_bps(app: Flask):
# 初始化 sock
init_sock(app)
# 注册 WebSocket 蓝图
app.register_blueprint(ssh_connect)
2.在flask 项目蓝图以及配置引入位置统一注册
# 我的项目是在applications 下的 __init__.py 做了注册
import os
from flask import Flask
from applications.common.script import init_script
from applications.config import BaseConfig
from applications.extensions import init_plugs
from applications.view import init_bps
from applications.websockets import register_websockets_bps # 导入websocket注册
from applications.logging_utils import setup_logging
def create_app():
app = Flask(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
# 引入配置
app.config.from_object(BaseConfig)
# 设置日志
setup_logging(app.config)
# 注册flask组件
init_plugs(app)
# 注册蓝图
init_bps(app)
# 注册websocket
register_websockets_bps(app) # 注册配置的websocket
# 注册命令
init_script(app)
return app
3.界面增删机器配置以及连接后端操作
#下方是路由位置绑定的,由于我使用的是layui老式传统都是后端渲染前端,可以按照自己的方式进行修改
from flask import Blueprint, render_template, request, jsonify
from applications.common.utils.http import success_api
from applications.common.utils.validate import str_escape
from applications.extensions.init_sqlalchemy import db
from applications.models import Ssh_connect
bp = Blueprint('ssh', __name__, url_prefix='/ssh') # 这是蓝图绑定,我是蓝图绑定蓝图会有多级目录,要使用则需要已下方的前端访问地址为准
# SSH 配置信息
ssh_client = None
ssh_channel = None
# 渲染前端
@bp.get('/')
def main():
return render_template('system/ssh/main.html')
@bp.get('/info')
def info():
page = int(request.args.get('page', 1))
limit = int(request.args.get('limit', 10))
# 计算分页参数
offset = (page - 1) * limit
total = db.session.query(db.func.count(Ssh_connect.id)).scalar()
configs = Ssh_connect.query.offset(offset).limit(limit).all()
# 构造响应数据
response = {
'code': 0,
'msg': '',
'count': total,
'data': [
{'id': c.id, 'remark': c.info, 'ipaddr': c.ipaddr, 'ports': c.ports, 'users': c.users, 'passwd': c.passwd,
'secerts': c.secerts}
for c in configs]
}
return jsonify(response)
# 渲染添加
@bp.get('/add')
def ssh_add():
return render_template('system/ssh/add.html')
# 处理添加信息
@bp.post('/add')
def ssh_add_put():
try:
data = request.get_json()
new_config = Ssh_connect(
info=data.get('remark'),
ipaddr=data.get('ipaddr'),
ports=data.get('ports'),
users=data.get('users'),
passwd=data.get('passwd'),
secerts=data.get('secerts')
)
db.session.add(new_config)
db.session.commit()
return jsonify({'code': 0, 'message': '配置添加成功'})
except Exception as e:
db.session.rollback()
return jsonify({'code': 1, 'message': str(e)}), 500
# 编辑配置信息
@bp.get('/edit/<int:_id>')
def ssh_edit(_id):
dict_get = Ssh_connect.query.filter_by(id=_id).first()
if dict_get:
dict_list = {
'success': True,
'data': {
'id': dict_get.id,
'remark': dict_get.info,
'ipaddr': dict_get.ipaddr,
'ports': dict_get.ports,
'users': dict_get.users,
'passwd': dict_get.passwd,
'secrets': dict_get.secerts
}
}
return render_template('/system/ssh/edit.html', dict=dict_list)
# 更新数据
@bp.put('/edit/update')
def ssh_edit_put():
req_json = request.get_json(force=True)
id = str_escape(req_json.get("dictId"))
info = str_escape(req_json.get("remark"))
ipaddr = str_escape(req_json.get("ipaddr"))
ports = str_escape(req_json.get("ports"))
users = str_escape(req_json.get("users"))
passwd = str_escape(req_json.get("passwd"))
secerts = str_escape(req_json.get("secrets"))
dict_type = Ssh_connect.query.filter_by(id=id).first()
if not dict_type:
return jsonify({"SUCCESS": False, "msg": "字典数据没找到"})
dict_type.info = info
dict_type.ipaddr = ipaddr
dict_type.ports = ports
dict_type.users = users
dict_type.passwd = passwd
dict_type.secerts = secerts
db.session.commit()
return success_api(msg="更新成功")
# 删除机器配置
@bp.delete('/delete/<int:_id>')
def ssh_delete(_id):
try:
record = Ssh_connect.query.filter_by(id=_id).first()
print(record)
if record:
db.session.delete(record)
db.session.commit()
return jsonify(success=True, msg="删除成功")
else:
return jsonify(success=False, msg='记录未找到')
except Exception as e:
# 处理异常
print(f"删除失败: {str(e)}")
db.session.rollback()
return jsonify(success=False, msg="删除失败")
# 连接机器页面渲染
@bp.get('/connect/<int:id>')
def ssh_connects(id):
return render_template('/system/ssh/terminal.html', id=id)
由于不擅长前端,前端界面有点丑陋,就献丑了吧图片发出来,连接的操作基本与xshell 或者crt等操作终端差不多,这个只是功能只是给开发或者测试使用,账号权限会有限制

4.前端代码信息
# 主界面 main
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>设备管理</title>
<link rel="stylesheet" href="{{ url_for('static', filename='system/component/layui/css/layui.css') }}">
<style>
.layui-container {
padding: 20px;
display: flex;
flex-direction: column;
position: relative;
}
.layui-table {
width: 100%;
table-layout: auto;
margin-top: 40px;
}
.layui-table .layui-table-cell {
text-align: center;
word-break: break-word;
}
.password-hidden {
color: #999;
}
.btn-add-config {
position: absolute;
top: 10px;
left: 20px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-size: 14px;
cursor: pointer;
}
.btn-add-config:hover {
background-color: #45a049;
}
@media (max-width: 768px) {
.layui-table th, .layui-table td {
font-size: 12px;
padding: 8px;
}
.btn-add-config {
font-size: 12px;
padding: 8px 16px;
}
}
</style>
</head>
<body>
<div class="layui-container">
<button class="btn-add-config" id="addConfigButton">新增配置</button>
<table class="layui-table" id="device-table" lay-filter="device-table-filter">
<tbody>
<!-- 数据由 JavaScript 动态填充 -->
</tbody>
</table>
</div>
<script src="{{ url_for('static', filename='system/component/layui/layui.js') }}"></script>
<script src="{{ url_for('static', filename='system/component/pear/module/tinymce/tinymce/plugins/kityformula-editor/kityformula/js/jquery-3.6.0.min.js') }}"></script>
<script>
layui.use(['table', 'jquery', 'layer'], function(){
var table = layui.table;
var $ = layui.$;
var layer = layui.layer;
// 请求数据
$.ajax({
url: '/system/ssh/info', // 这是请求后端的接口
method: 'GET',
success: function(res) {
if (res.code === 0) {
table.render({
elem: '#device-table',
height: 315,
page: true,
data: res.data,
cols: [[
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'remark', title: '备注信息', width: 80, sort: true},
{field: 'ipaddr', title: 'IP 地址', minWidth: 120},
{field: 'ports', title: '端口', width: 80},
{field: 'users', title: '用户', minWidth: 100},
{field: 'passwd', title: '密码', minWidth: 120, templet: function(d) {
return d.passwd ? '<span class="password-hidden">******</span>' : '未配置';
}},
{field: 'secerts', title: '密钥', minWidth: 120, templet: function(d) {
return d.secerts ? '<span class="password-hidden">******</span>' : '未配置';
}},
{title: '操作', toolbar: '#action-buttons', width: 260}
]],
done: function(res, curr, count){
// 额外处理逻辑
}
});
} else {
alert('数据请求失败');
}
},
error: function() {
alert('请求错误');
}
});
// 监听操作按钮事件
table.on('tool(device-table-filter)', function(obj){
var data = obj.data;
var layEvent = obj.event;
if (layEvent === 'connect') {
// 打开新页面并传递 ID
window.open('/system/ssh/connect/' + data.id, '_blank');
} else if (layEvent === 'edit') {
layer.open({
type: 2,
title: '编辑配置',
content: '/system/ssh/edit/' + data.id,
area: ['70%', '70%'],
maxWidth: 800,
maxHeight: 600
});
} else if (layEvent === 'delete') {
layer.confirm('确定删除此记录吗?', {icon: 3, title:'提示'}, function(index){
$.ajax({
url: '/system/ssh/delete/' + data.id,
type: 'DELETE',
success: function(response) {
if (response.success) {
obj.del(); // 删除行
layer.msg('删除成功', {icon: 1});
} else {
layer.msg('删除失败: ' + response.msg, {icon: 5});
}
},
error: function() {
layer.msg('请求错误', {icon: 5});
}
});
layer.close(index);
});
}
});
// 新增配置按钮点击事件
$('#addConfigButton').on('click', function() {
layer.open({
type: 2,
title: '新增配置',
content: '/system/ssh/add',
area: ['70%', '70%'],
maxWidth: 800,
maxHeight: 600
});
});
});
</script>
<script type="text/html" id="action-buttons">
<div class="layui-btn-group">
<button class="layui-btn layui-btn-normal layui-btn-sm" lay-event="connect">连接</button>
<button class="layui-btn layui-btn-warm layui-btn-sm" lay-event="edit">编辑</button>
<button class="layui-btn layui-btn-danger layui-btn-sm" lay-event="delete">删除</button>
</div>
</script>
</body>
</html>
##################
// 添加用户配置
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>配置表单</title>
<link rel="stylesheet" href="{{ url_for('static', filename='system/component/layui/css/layui.css') }}" integrity="sha512-..." crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
body {
font-family: Arial, sans-serif;
background-color: #f2f2f2;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.layui-form-item {
margin-bottom: 20px;
}
.layui-form-label {
width: 120px;
}
.layui-input {
width: calc(100% - 140px);
}
.layui-form-item .layui-input-block {
margin-left: 140px;
}
.layui-btn {
margin-top: 20px;
background-color: #4caf50;
color: #fff;
}
.layui-btn:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="container">
<form class="layui-form" lay-filter="config-form">
<div class="layui-form-item">
<label class="layui-form-label">备注信息</label>
<div class="layui-input-block">
<input type="text" name="remark" required lay-verify="required" placeholder="请输入机器备注信息" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">IP 地址</label>
<div class="layui-input-block">
<input type="text" name="ipaddr" required lay-verify="required|ip" placeholder="请输入 IP 地址" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">端口</label>
<div class="layui-input-block">
<input type="text" name="ports" required lay-verify="required" placeholder="请输入端口" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户</label>
<div class="layui-input-block">
<input type="text" name="users" required lay-verify="required" placeholder="请输入用户名" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="password" name="passwd" placeholder="请输入密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密钥</label>
<div class="layui-input-block">
<input type="text" name="secerts" placeholder="请输入密钥" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="submit-form">提交</button>
</div>
</div>
</form>
</div>
<script src="{{ url_for('static', filename='system/component/layui/layui.js') }}" integrity="sha512-..." crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="{{ url_for('static', filename='system/component/pear/module/tinymce/tinymce/plugins/kityformula-editor/kityformula/js/jquery-3.6.0.min.js') }}"></script>
<script>
layui.use(['form', 'jquery', 'layer'], function(){
var form = layui.form;
var $ = layui.$;
var layer = layui.layer;
// 自定义 IP 地址验证规则
form.verify({
ip: function(value) {
var ipPattern = /^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)$/;
if (!ipPattern.test(value)) {
return '请输入有效的 IP 地址';
}
}
});
// 监听表单提交事件
form.on('submit(submit-form)', function(data){
// 获取密码和密钥字段的值
var passwd = data.field.passwd;
var secerts = data.field.secrets;
// 验证密码和密钥字段
if (!passwd && !secerts) {
layer.msg('密码和密钥不能同时为空!', {icon: 5});
return false; // 阻止表单的默认提交行为
}
// 发送表单数据的 AJAX 请求
$.ajax({
url: '/system/ssh/add',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data.field),
success: function(response) {
if (response.code === 0) {
layer.msg('配置添加成功!', {icon: 1});
setTimeout(function() {
var index = parent.layer.getFrameIndex(window.name); // 获取弹出层索引
parent.layer.close(index); // 关闭当前弹出层
parent.location.reload(); // 刷新父窗口
}, 1000);
} else {
layer.msg('配置添加失败: ' + response.message, {icon: 5});
}
},
error: function() {
layer.msg('请求错误', {icon: 5});
}
});
return false; // 阻止表单的默认提交行为
});
});
</script>
</body>
</html>
##################
// 修改配置信息
<!DOCTYPE html>
<html>
<head>
<title>编辑设备配置</title>
{% include 'system/common/header.html' %}
<link rel="stylesheet" href="{{ url_for('static', filename='system/admin/css/other/dict.css') }}"/>
<style>
.custom-textarea {
width: 100%;
box-sizing: border-box;
overflow-wrap: break-word;
white-space: pre-wrap;
height: 200px; /* 根据需要调整高度 */
}
</style>
</head>
<body>
<form class="layui-form" action="">
<div class="mainBox">
<div class="main-container">
<div class="layui-form-item layui-hide">
<label class="layui-form-label">ID</label>
<div class="layui-input-block">
<input type="text" id="dict-id" value="{{ dict.data.id }}" name="dictId" lay-verify="title"
autocomplete="off" placeholder="ID" class="layui-input" readonly>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">备注信息</label>
<div class="layui-input-block">
<input type="text" id="dict-remark" value="{{ dict.data.remark }}" name="remark"
lay-verify="title" autocomplete="off" placeholder="备注信息" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">IP 地址</label>
<div class="layui-input-block">
<input type="text" id="dict-ipaddr" value="{{ dict.data.ipaddr }}" name="ipaddr"
lay-verify="title" autocomplete="off" placeholder="IP 地址" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">端口</label>
<div class="layui-input-block">
<input type="text" id="dict-ports" value="{{ dict.data.ports }}" name="ports"
lay-verify="title" autocomplete="off" placeholder="端口" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户</label>
<div class="layui-input-block">
<input type="text" id="dict-users" value="{{ dict.data.users }}" name="users"
lay-verify="title" autocomplete="off" placeholder="用户" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="text" id="dict-passwd" value="{{ dict.data.passwd }}" name="passwd"
lay-verify="title" autocomplete="off" placeholder="密码" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密钥</label>
<div class="layui-input-block">
<textarea id="dict-secrets" name="secrets" lay-verify="title"
autocomplete="off" placeholder="密钥" class="layui-textarea custom-textarea">{{ dict.data.secrets }}</textarea>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="button-container">
<button type="submit" class="pear-btn pear-btn-primary pear-btn-sm" lay-submit="" lay-filter="dict-update">
<i class="layui-icon layui-icon-ok"></i> 提交
</button>
<button type="reset" class="pear-btn pear-btn-sm">
<i class="layui-icon layui-icon-refresh"></i> 重置
</button>
</div>
</div>
</form>
{% include 'system/common/footer.html' %}
<script>
layui.use(['form', 'jquery', 'layer'], function () {
let form = layui.form;
let $ = layui.jquery;
let layer = layui.layer;
form.on('submit(dict-update)', function (data) {
$.ajax({
url: '/system/ssh/edit/update',
data: JSON.stringify(data.field),
dataType: 'json',
contentType: 'application/json',
type: 'PUT',
success: function (result) {
if (result.success) {
layer.msg('更新成功', {icon: 1, time: 1000}, function () {
let index = parent.layer.getFrameIndex(window.name); // 获取当前弹出层的索引
parent.layer.close(index); // 关闭弹出层
if (parent.layui && parent.layui.table) {
parent.layui.table.reload('device-table'); // 刷新主页面表格
parent.location.reload(); // 刷新父窗口
}
});
} else {
layer.msg('更新失败: ' + result.msg, {icon: 2, time: 1000});
}
},
error: function () {
layer.msg('请求失败,请稍后再试', {icon: 2});
}
});
return false; // 阻止表单的默认提交
});
});
</script>
</body>
</html>
下方是终端前端代码引入的xterm.js 是5.2版本的。关联其他引入则手动查询一下关联的版本
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Terminal</title>
<link rel="stylesheet" href="{{ url_for('static', filename='terminal/xterm.css') }}">
<style>
html, body {
margin: 0;
height: 100vh;
width: 100vw;
overflow: hidden; /* Ensure no overflow is hidden */
}
#terminal {
width: 100vw; /* Full viewport width */
height: 100vh; /* Full viewport height */
display: flex;
box-sizing: border-box; /* Include padding and border in width and height */
font-family: monospace;
overflow: auto; /* Ensure overflow is visible */
white-space: pre-wrap; /* Preserve whitespace formatting */
}
</style>
</head>
<body>
<div id="terminal"></div>
<script src="{{ url_for('static', filename='terminal/xterm.js') }}"></script>
<script src="{{ url_for('static', filename='terminal/xterm-addon-fit.min.js') }}"></script>
<script src="{{ url_for('static', filename='terminal/xterm-addon-web-links.min.js') }}"></script>
<script>
let terminal;
let fitAddon;
function setupWebSocket() {
const id = {{ id|tojson }};
const socket = new WebSocket(`ws://${window.location.host}/ws/term?id=${id}`);
socket.onopen = () => {
console.log('WebSocket connection established.');
terminal.focus();
};
socket.onmessage = (event) => {
console.log('Received message:', event.data);
terminal.write(event.data);
};
socket.onerror = (error) => {
console.error('WebSocket Error:', error);
};
socket.onclose = (event) => {
console.log('WebSocket closed:', event);
setTimeout(setupWebSocket, 5000); // Reconnect if needed
};
terminal = new Terminal({
cursorBlink: true,
fontSize: 14,
theme: {
background: '#000000',
foreground: '#ffffff'
},
lineHeight: 1.2,
scrollback: 1000
});
fitAddon = new FitAddon.FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(document.getElementById('terminal'));
fitAddon.fit();
terminal.onData(data => {
console.log('Sending data:', data);
socket.send(data);
});
window.addEventListener('resize', () => {
fitAddon.fit();
});
}
function copySelectedText() {
const selectedText = window.getSelection().toString();
if (selectedText) {
navigator.clipboard.writeText(selectedText).then(() => {
console.log('Text copied to clipboard:', selectedText);
}).catch(err => {
console.error('Failed to copy text:', err);
alert('Failed to copy text. Please try manually.');
});
}
}
// Set up WebSocket and terminal
setupWebSocket();
// Add event listeners for text selection and copy
document.getElementById('terminal').addEventListener('mouseup', copySelectedText);
document.getElementById('terminal').addEventListener('dblclick', copySelectedText);
</script>
</body>
</html>