mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
202 lines
5.5 KiB
Go
202 lines
5.5 KiB
Go
package terminal
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
// Half-open detection uses protocol level ping/pong, which browsers answer
|
|
// without JavaScript, so background tab timer throttling cannot trip it.
|
|
const (
|
|
pingInterval = 30 * time.Second
|
|
pongWait = 75 * time.Second
|
|
writeWait = 30 * time.Second
|
|
)
|
|
|
|
var errAttachmentClosed = errors.New("terminal attachment is closed")
|
|
|
|
// attachment is one websocket connection bound to a Session.
|
|
type attachment struct {
|
|
sess *Session
|
|
ws *websocket.Conn
|
|
|
|
writeMu sync.Mutex
|
|
cursor uint64 // ring offset of the next byte to send; guarded by writeMu
|
|
|
|
done chan struct{}
|
|
closeOnce sync.Once
|
|
revalidateRequested atomic.Bool
|
|
}
|
|
|
|
// Run reads client messages until the websocket fails or this attachment is closed.
|
|
// On return the session is detached: cleanly if the client sent close code 1000.
|
|
func (a *attachment) Run() {
|
|
clean := false
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
global.LOG.Errorf("[A panic occurred during receive ws message, error message: %v", r)
|
|
}
|
|
a.close(websocket.CloseNormalClosure, "")
|
|
a.sess.detach(a, clean, a.revalidateRequested.Load(), a.cursorOffset())
|
|
}()
|
|
|
|
_ = a.ws.SetReadDeadline(time.Now().Add(pongWait))
|
|
a.ws.SetPongHandler(func(string) error {
|
|
return a.ws.SetReadDeadline(time.Now().Add(pongWait))
|
|
})
|
|
go a.pingLoop()
|
|
go a.revalidateLoop()
|
|
|
|
// close() shuts the websocket, which is what ends this loop.
|
|
for {
|
|
_, wsData, err := a.ws.ReadMessage()
|
|
if err != nil {
|
|
clean = websocket.IsCloseError(err, websocket.CloseNormalClosure)
|
|
return
|
|
}
|
|
_ = a.ws.SetReadDeadline(time.Now().Add(pongWait))
|
|
msgObj := WsMsg{}
|
|
_ = json.Unmarshal(wsData, &msgObj)
|
|
switch msgObj.Type {
|
|
case WsMsgResize:
|
|
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
|
a.sess.resize(msgObj.Cols, msgObj.Rows)
|
|
}
|
|
case WsMsgCmd:
|
|
decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data)
|
|
if err != nil {
|
|
global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err)
|
|
}
|
|
if isEnterInput(decodeBytes) {
|
|
interceptor := a.sess.ensureAIInterceptor()
|
|
if interceptor != nil {
|
|
interceptor.SetCurrentLine(msgObj.Line)
|
|
}
|
|
if generated, handled := interceptor.HandleEnter(a.notifyAIThinking, a.notifyAIDone, a.notifyAIError); handled {
|
|
if payload, err := buildAIPastePayload(generated); err != nil {
|
|
global.LOG.Errorf("ai generated command rejected before ssh.stdin pipe write, err: %v", err)
|
|
} else {
|
|
a.sess.writeInput(payload)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
a.sess.writeInput(decodeBytes)
|
|
case WsMsgHeartbeat:
|
|
if err := a.write(wsData); err != nil {
|
|
global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *attachment) cursorOffset() uint64 {
|
|
a.writeMu.Lock()
|
|
defer a.writeMu.Unlock()
|
|
return a.cursor
|
|
}
|
|
|
|
func (a *attachment) revalidateLoop() {
|
|
timer := time.NewTimer(revalidateInterval)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-a.done:
|
|
return
|
|
case <-timer.C:
|
|
a.revalidateRequested.Store(true)
|
|
a.sess.markRevalidation(a, a.cursorOffset())
|
|
a.close(CloseCodeRevalidate, "terminal authorization revalidation required")
|
|
}
|
|
}
|
|
|
|
// pingLoop keeps the read deadline honest; a ping that cannot be sent ends the attachment.
|
|
func (a *attachment) pingLoop() {
|
|
tick := time.NewTicker(pingInterval)
|
|
defer tick.Stop()
|
|
for {
|
|
select {
|
|
case <-a.done:
|
|
return
|
|
case <-tick.C:
|
|
if err := a.ws.WriteControl(websocket.PingMessage, nil, time.Now().Add(writeWait)); err != nil {
|
|
a.close(websocket.CloseInternalServerErr, "ping failed")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// write sends one text message, serialized against every other writer.
|
|
func (a *attachment) write(data []byte) error {
|
|
a.writeMu.Lock()
|
|
defer a.writeMu.Unlock()
|
|
return a.writeLocked(data)
|
|
}
|
|
|
|
// writeLocked sends one text message; the caller owns writeMu.
|
|
func (a *attachment) writeLocked(data []byte) error {
|
|
select {
|
|
case <-a.done:
|
|
return errAttachmentClosed
|
|
default:
|
|
}
|
|
_ = a.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
|
return a.ws.WriteMessage(websocket.TextMessage, data)
|
|
}
|
|
|
|
// close sends a close frame and tears the websocket down. Idempotent.
|
|
func (a *attachment) close(code int, reason string) {
|
|
a.closeOnce.Do(func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
global.LOG.Errorf("a panic occurred during close ws attachment, error message: %v", r)
|
|
}
|
|
}()
|
|
close(a.done)
|
|
sendClose(a.ws, code, reason)
|
|
_ = a.ws.Close()
|
|
})
|
|
}
|
|
|
|
func (a *attachment) notifyAIThinking() {
|
|
if err := a.writeAINotice("info", i18n.GetMsgByKeyAndLang(a.sess.lang, "TerminalAIThinking")); err != nil {
|
|
global.LOG.Errorf("write terminal ai thinking message failed, err: %v", err)
|
|
}
|
|
}
|
|
|
|
func (a *attachment) notifyAIDone(message string) {
|
|
if err := a.writeAINotice("success", message); err != nil {
|
|
global.LOG.Errorf("write terminal ai done message failed, err: %v", err)
|
|
}
|
|
}
|
|
|
|
func (a *attachment) notifyAIError(message string) {
|
|
if err := a.writeAINotice("error", message); err != nil {
|
|
global.LOG.Errorf("write terminal ai error message failed, err: %v", err)
|
|
}
|
|
}
|
|
|
|
func (a *attachment) writeAINotice(level, message string) error {
|
|
if strings.TrimSpace(message) == "" {
|
|
return nil
|
|
}
|
|
wsData, err := json.Marshal(WsMsg{
|
|
Type: WsMsgAINotice,
|
|
Level: strings.TrimSpace(level),
|
|
Message: strings.TrimSpace(message),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return a.write(wsData)
|
|
}
|