mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 03:07:20 +08:00
feat: 终端支持ping
This commit is contained in:
@@ -22,6 +22,11 @@ type MessageResize struct {
|
||||
Rows uint `json:"rows"`
|
||||
}
|
||||
|
||||
// MessagePing ping 消息
|
||||
type MessagePing struct {
|
||||
Ping bool `json:"ping"`
|
||||
}
|
||||
|
||||
// Turn PTY 终端
|
||||
type Turn struct {
|
||||
ctx context.Context
|
||||
@@ -78,6 +83,7 @@ func (t *Turn) Close() {
|
||||
// Handle 从 WebSocket 读取输入写入 PTY
|
||||
func (t *Turn) Handle(ctx context.Context) error {
|
||||
var resize MessageResize
|
||||
var ping MessagePing
|
||||
|
||||
go func() { _ = t.Pipe(ctx) }()
|
||||
|
||||
@@ -92,6 +98,12 @@ func (t *Turn) Handle(ctx context.Context) error {
|
||||
return fmt.Errorf("failed to read ws message: %w", err)
|
||||
}
|
||||
|
||||
// 判断是否是 ping 消息
|
||||
if err = json.Unmarshal(data, &ping); err == nil && ping.Ping {
|
||||
_ = t.ws.Write(ctx, websocket.MessageText, []byte(`{"pong":true}`))
|
||||
continue
|
||||
}
|
||||
|
||||
// 判断是否是 resize 消息
|
||||
if err = json.Unmarshal(data, &resize); err == nil {
|
||||
if resize.Resize && resize.Columns > 0 && resize.Rows > 0 {
|
||||
|
||||
@@ -17,6 +17,10 @@ type MessageResize struct {
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
type MessagePing struct {
|
||||
Ping bool `json:"ping"`
|
||||
}
|
||||
|
||||
type Turn struct {
|
||||
ctx context.Context
|
||||
stdin io.WriteCloser
|
||||
@@ -72,6 +76,7 @@ func (t *Turn) Close() {
|
||||
|
||||
func (t *Turn) Handle(ctx context.Context) error {
|
||||
var resize MessageResize
|
||||
var ping MessagePing
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -84,6 +89,12 @@ func (t *Turn) Handle(ctx context.Context) error {
|
||||
return fmt.Errorf("reading ws message err: %v", err)
|
||||
}
|
||||
|
||||
// 判断是否是 ping 消息
|
||||
if err = json.Unmarshal(data, &ping); err == nil && ping.Ping {
|
||||
_ = t.ws.Write(ctx, websocket.MessageText, []byte(`{"pong":true}`))
|
||||
continue
|
||||
}
|
||||
|
||||
// 判断是否是 resize 消息
|
||||
if err = json.Unmarshal(data, &resize); err == nil {
|
||||
if resize.Resize && resize.Columns > 0 && resize.Rows > 0 {
|
||||
|
||||
@@ -34,6 +34,9 @@ interface TerminalTab {
|
||||
ws: WebSocket | null
|
||||
element: HTMLElement | null
|
||||
connected: boolean
|
||||
latency: number
|
||||
pingTimer: ReturnType<typeof setInterval> | null
|
||||
lastPingTime: number
|
||||
}
|
||||
|
||||
// 状态
|
||||
@@ -159,7 +162,10 @@ const addTab = async (hostId: number) => {
|
||||
webglAddon: null,
|
||||
ws: null,
|
||||
element: null,
|
||||
connected: false
|
||||
connected: false,
|
||||
latency: 0,
|
||||
pingTimer: null,
|
||||
lastPingTime: 0
|
||||
}
|
||||
tabs.value.push(tab)
|
||||
activeTabId.value = tabId
|
||||
@@ -253,6 +259,18 @@ const initTerminal = async (tabId: string) => {
|
||||
|
||||
tab.ws.onmessage = (ev) => {
|
||||
const data: ArrayBuffer | string = ev.data
|
||||
// 检查是否是 pong 响应
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
const json = JSON.parse(data)
|
||||
if (json.pong) {
|
||||
handlePong(tab)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 不是 JSON,正常处理
|
||||
}
|
||||
}
|
||||
tab.terminal?.write(typeof data === 'string' ? data : new Uint8Array(data))
|
||||
}
|
||||
|
||||
@@ -288,6 +306,9 @@ const initTerminal = async (tabId: string) => {
|
||||
tab.terminal.focus()
|
||||
tab.connected = true
|
||||
|
||||
// 启动延迟检测
|
||||
startPingTimer(tab)
|
||||
|
||||
tab.ws.onclose = () => {
|
||||
tab.connected = false
|
||||
tab.terminal?.write('\r\n' + $gettext('Connection closed. Please refresh.'))
|
||||
@@ -308,6 +329,10 @@ const initTerminal = async (tabId: string) => {
|
||||
// 销毁标签
|
||||
const disposeTab = (tab: TerminalTab) => {
|
||||
try {
|
||||
if (tab.pingTimer) {
|
||||
clearInterval(tab.pingTimer)
|
||||
tab.pingTimer = null
|
||||
}
|
||||
tab.ws?.close()
|
||||
tab.terminal?.dispose()
|
||||
tab.fitAddon = null
|
||||
@@ -402,6 +427,43 @@ const applyFontSettings = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 启动延迟检测定时器
|
||||
const startPingTimer = (tab: TerminalTab) => {
|
||||
// 立即执行一次
|
||||
sendPing(tab)
|
||||
// 每3秒检测一次
|
||||
tab.pingTimer = setInterval(() => {
|
||||
sendPing(tab)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 发送 ping
|
||||
const sendPing = (tab: TerminalTab) => {
|
||||
if (tab.ws?.readyState === WebSocket.OPEN) {
|
||||
tab.lastPingTime = performance.now()
|
||||
tab.ws.send(JSON.stringify({ ping: true }))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 pong 响应
|
||||
const handlePong = (tab: TerminalTab) => {
|
||||
if (tab.lastPingTime > 0) {
|
||||
tab.latency = Math.round(performance.now() - tab.lastPingTime)
|
||||
tab.lastPingTime = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染标签标题
|
||||
const renderTabLabel = (tab: TerminalTab) => {
|
||||
const latencyColor = tab.connected ? '#18a058' : '#d03050'
|
||||
const icon = tab.connected ? '✓' : '✗'
|
||||
return h('span', { class: 'tab-label' }, [
|
||||
h('span', { style: { color: latencyColor, marginRight: '4px' } }, `${tab.latency} ms`),
|
||||
h('span', { style: { color: latencyColor, marginRight: '4px' } }, icon),
|
||||
tab.name
|
||||
])
|
||||
}
|
||||
|
||||
// 全屏切换
|
||||
const toggleFullscreen = async () => {
|
||||
const container = terminalContainer.value
|
||||
@@ -479,7 +541,7 @@ onUnmounted(() => {
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:name="tab.id"
|
||||
:tab="tab.name"
|
||||
:tab="renderTabLabel(tab)"
|
||||
display-directive="show:lazy"
|
||||
>
|
||||
</n-tab-pane>
|
||||
|
||||
Reference in New Issue
Block a user