From 402b41100b6d506331445d42193369959a893a4d Mon Sep 17 00:00:00 2001
From: ssongliu <73214554+ssongliu@users.noreply.github.com>
Date: Fri, 7 Mar 2025 19:06:59 +0800
Subject: [PATCH] fix(container): Fix cpu surge when the container terminal
executes exit (#8096)
Refs #7574
---
backend/app/api/v1/terminal.go | 225 ++++++------------
backend/router/ro_ai.go | 1 -
backend/router/ro_container.go | 2 +-
backend/router/ro_database.go | 1 -
backend/utils/ssh/ssh.go | 55 -----
backend/utils/terminal/local_cmd.go | 14 +-
frontend/src/lang/modules/en.ts | 2 -
frontend/src/lang/modules/ja.ts | 2 -
frontend/src/lang/modules/ko.ts | 2 -
frontend/src/lang/modules/ms.ts | 2 -
frontend/src/lang/modules/pt-br.ts | 2 -
frontend/src/lang/modules/ru.ts | 2 -
frontend/src/lang/modules/tw.ts | 1 -
frontend/src/lang/modules/zh.ts | 1 -
.../src/views/ai/model/terminal/index.vue | 11 +-
.../container/container/terminal/index.vue | 11 +-
frontend/src/views/database/redis/index.vue | 8 +-
go.mod | 3 +-
go.sum | 3 -
19 files changed, 92 insertions(+), 256 deletions(-)
diff --git a/backend/app/api/v1/terminal.go b/backend/app/api/v1/terminal.go
index 1aaefc335..c92fa7d9f 100644
--- a/backend/app/api/v1/terminal.go
+++ b/backend/app/api/v1/terminal.go
@@ -10,7 +10,6 @@ import (
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
- "github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/copier"
@@ -74,7 +73,7 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
}
}
-func (b *BaseApi) RedisWsSsh(c *gin.Context) {
+func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
@@ -96,193 +95,105 @@ func (b *BaseApi) RedisWsSsh(c *gin.Context) {
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
+ source := c.Query("source")
+ var containerID string
+ var initCmd string
+ switch source {
+ case "redis":
+ containerID, initCmd, err = loadRedisInitCmd(c)
+ case "ollama":
+ containerID, initCmd, err = loadOllamaInitCmd(c)
+ case "container":
+ containerID, initCmd, err = loadContainerInitCmd(c)
+ default:
+ if wshandleError(wsConn, fmt.Errorf("not support such source %s", source)) {
+ return
+ }
+ }
+ if wshandleError(wsConn, err) {
+ return
+ }
+ pidMap := loadMapFromDockerTop(containerID)
+ slave, err := terminal.NewCommand("clear && " + initCmd)
+ if wshandleError(wsConn, err) {
+ return
+ }
+ defer killBash(containerID, strings.ReplaceAll(initCmd, fmt.Sprintf("docker exec -it %s ", containerID), ""), pidMap)
+ defer slave.Close()
+
+ tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
+ if wshandleError(wsConn, err) {
+ return
+ }
+
+ quitChan := make(chan bool, 3)
+ tty.Start(quitChan)
+ go slave.Wait(quitChan)
+
+ <-quitChan
+
+ global.LOG.Info("websocket finished")
+ if wshandleError(wsConn, err) {
+ return
+ }
+}
+
+func loadRedisInitCmd(c *gin.Context) (string, string, error) {
name := c.Query("name")
from := c.Query("from")
- commands := []string{"redis-cli"}
+ commands := "redis-cli"
database, err := databaseService.Get(name)
- if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) {
- return
+ if err != nil {
+ return "", "", fmt.Errorf("no such database in db, err: %v", err)
}
if from == "local" {
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
- if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) {
- return
+ if err != nil {
+ return "", "", fmt.Errorf("no such app in db, err: %v", err)
}
name = redisInfo.ContainerName
if len(database.Password) != 0 {
- commands = []string{"redis-cli", "-a", database.Password, "--no-auth-warning"}
+ commands = "redis-cli -a " + database.Password + " --no-auth-warning"
}
} else {
- itemPort := fmt.Sprintf("%v", database.Port)
- commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort}
+ commands = fmt.Sprintf("redis-cli -h %s -p %v", database.Address, database.Port)
if len(database.Password) != 0 {
- commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort, "-a", database.Password, "--no-auth-warning"}
+ commands = fmt.Sprintf("redis-cli -h %s -p %v -a %s --no-auth-warning", database.Address, database.Port, database.Password)
}
name = "1Panel-redis-cli-tools"
}
-
- pidMap := loadMapFromDockerTop(name)
- itemCmds := append([]string{"exec", "-it", name}, commands...)
- slave, err := terminal.NewCommand(itemCmds)
- if wshandleError(wsConn, err) {
- return
- }
- defer killBash(name, strings.Join(commands, " "), pidMap)
- defer slave.Close()
-
- tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
- if wshandleError(wsConn, err) {
- return
- }
-
- quitChan := make(chan bool, 3)
- tty.Start(quitChan)
- go slave.Wait(quitChan)
-
- <-quitChan
-
- global.LOG.Info("websocket finished")
- if wshandleError(wsConn, err) {
- return
- }
+ return name, fmt.Sprintf("docker exec -it %s %s", name, commands), nil
}
-func (b *BaseApi) OllamaWsSsh(c *gin.Context) {
- wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
- if err != nil {
- global.LOG.Errorf("gin context http handler failed, err: %v", err)
- return
- }
- defer wsConn.Close()
-
- if global.CONF.System.IsDemo {
- if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
- return
- }
- }
-
- cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
- if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
- return
- }
- rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
- if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
- return
- }
+func loadOllamaInitCmd(c *gin.Context) (string, string, error) {
name := c.Query("name")
if cmd.CheckIllegal(name) {
- if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) {
- return
- }
+ return "", "", fmt.Errorf("ollama model %s contains illegal characters", name)
}
- container, err := service.LoadContainerName()
- if wshandleError(wsConn, errors.WithMessage(err, " load container name for ollama failed")) {
- return
- }
- commands := []string{"ollama", "run", name}
-
- pidMap := loadMapFromDockerTop(container)
- fmt.Println("pidMap")
- for k, v := range pidMap {
- fmt.Println(k, v)
- }
- itemCmds := append([]string{"exec", "-it", container}, commands...)
- slave, err := terminal.NewCommand(itemCmds)
- if wshandleError(wsConn, err) {
- return
- }
- defer killBash(container, strings.Join(commands, " "), pidMap)
- defer slave.Close()
-
- tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
- if wshandleError(wsConn, err) {
- return
- }
-
- quitChan := make(chan bool, 3)
- tty.Start(quitChan)
- go slave.Wait(quitChan)
-
- <-quitChan
-
- global.LOG.Info("websocket finished")
- if wshandleError(wsConn, err) {
- return
+ ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
+ if err != nil {
+ return "", "", fmt.Errorf("no such app in db, err: %v", err)
}
+ containerName := ollamaInfo.ContainerName
+ return containerName, fmt.Sprintf("docker exec -it %s ollama run %s", containerName, name), nil
}
-func (b *BaseApi) ContainerWsSsh(c *gin.Context) {
- wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
- if err != nil {
- global.LOG.Errorf("gin context http handler failed, err: %v", err)
- return
- }
- defer wsConn.Close()
-
- if global.CONF.System.IsDemo {
- if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
- return
- }
- }
-
+func loadContainerInitCmd(c *gin.Context) (string, string, error) {
containerID := c.Query("containerid")
command := c.Query("command")
user := c.Query("user")
- if len(command) == 0 || len(containerID) == 0 {
- if wshandleError(wsConn, errors.New("error param of command or containerID")) {
- return
- }
- }
- cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
- if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
- return
- }
- rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
- if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
- return
- }
-
- cmds := []string{"exec", containerID, command}
- if len(user) != 0 {
- cmds = []string{"exec", "-u", user, containerID, command}
- }
if cmd.CheckIllegal(user, containerID, command) {
- if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) {
- return
- }
+ return "", "", fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
}
- stdout, err := cmd.ExecWithCheck("docker", cmds...)
- if wshandleError(wsConn, errors.WithMessage(err, stdout)) {
- return
+ if len(command) == 0 || len(containerID) == 0 {
+ return "", "", fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
}
-
- commands := []string{"exec", "-it", containerID, command}
+ command = fmt.Sprintf("docker exec -it %s %s", containerID, command)
if len(user) != 0 {
- commands = []string{"exec", "-it", "-u", user, containerID, command}
- }
- pidMap := loadMapFromDockerTop(containerID)
- slave, err := terminal.NewCommand(commands)
- if wshandleError(wsConn, err) {
- return
- }
- defer killBash(containerID, command, pidMap)
- defer slave.Close()
-
- tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, true)
- if wshandleError(wsConn, err) {
- return
+ command = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command)
}
- quitChan := make(chan bool, 3)
- tty.Start(quitChan)
- go slave.Wait(quitChan)
-
- <-quitChan
-
- global.LOG.Info("websocket finished")
- if wshandleError(wsConn, err) {
- return
- }
+ return containerID, command, nil
}
func wshandleError(ws *websocket.Conn, err error) bool {
diff --git a/backend/router/ro_ai.go b/backend/router/ro_ai.go
index 45f574a71..1fba58452 100644
--- a/backend/router/ro_ai.go
+++ b/backend/router/ro_ai.go
@@ -15,7 +15,6 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi
{
- aiToolsRouter.GET("/ollama/exec", baseApi.OllamaWsSsh)
aiToolsRouter.POST("/ollama/close", baseApi.CloseOllamaModel)
aiToolsRouter.POST("/ollama/model", baseApi.CreateOllamaModel)
aiToolsRouter.POST("/ollama/model/recreate", baseApi.RecreateOllamaModel)
diff --git a/backend/router/ro_container.go b/backend/router/ro_container.go
index 7764bda90..61274dc62 100644
--- a/backend/router/ro_container.go
+++ b/backend/router/ro_container.go
@@ -15,7 +15,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
- baRouter.GET("/exec", baseApi.ContainerWsSsh)
+ baRouter.GET("/exec", baseApi.ContainerWsSSH)
baRouter.GET("/stats/:id", baseApi.ContainerStats)
baRouter.POST("", baseApi.ContainerCreate)
diff --git a/backend/router/ro_database.go b/backend/router/ro_database.go
index c0307651d..7a3fb8726 100644
--- a/backend/router/ro_database.go
+++ b/backend/router/ro_database.go
@@ -38,7 +38,6 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/redis/persistence/conf", baseApi.LoadPersistenceConf)
cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus)
cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf)
- cmdRouter.GET("/redis/exec", baseApi.RedisWsSsh)
cmdRouter.GET("/redis/check", baseApi.CheckHasCli)
cmdRouter.POST("/redis/install/cli", baseApi.InstallCli)
cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword)
diff --git a/backend/utils/ssh/ssh.go b/backend/utils/ssh/ssh.go
index f487182d4..604d97d03 100644
--- a/backend/utils/ssh/ssh.go
+++ b/backend/utils/ssh/ssh.go
@@ -1,11 +1,8 @@
package ssh
import (
- "bytes"
"fmt"
- "io"
"strings"
- "sync"
"time"
gossh "golang.org/x/crypto/ssh"
@@ -82,58 +79,6 @@ func (c *ConnInfo) Close() {
_ = c.Client.Close()
}
-type SshConn struct {
- StdinPipe io.WriteCloser
- ComboOutput *wsBufferWriter
- Session *gossh.Session
-}
-
-func (c *ConnInfo) NewSshConn(cols, rows int) (*SshConn, error) {
- sshSession, err := c.Client.NewSession()
- if err != nil {
- return nil, err
- }
-
- stdinP, err := sshSession.StdinPipe()
- if err != nil {
- return nil, err
- }
-
- comboWriter := new(wsBufferWriter)
- sshSession.Stdout = comboWriter
- sshSession.Stderr = comboWriter
-
- modes := gossh.TerminalModes{
- gossh.ECHO: 1,
- gossh.TTY_OP_ISPEED: 14400,
- gossh.TTY_OP_OSPEED: 14400,
- }
- if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
- return nil, err
- }
- if err := sshSession.Shell(); err != nil {
- return nil, err
- }
- return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
-}
-
-func (s *SshConn) Close() {
- if s.Session != nil {
- s.Session.Close()
- }
-}
-
-type wsBufferWriter struct {
- buffer bytes.Buffer
- mu sync.Mutex
-}
-
-func (w *wsBufferWriter) Write(p []byte) (int, error) {
- w.mu.Lock()
- defer w.mu.Unlock()
- return w.buffer.Write(p)
-}
-
func makePrivateKeySigner(privateKey []byte, passPhrase []byte) (gossh.Signer, error) {
if len(passPhrase) != 0 {
return gossh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase)
diff --git a/backend/utils/terminal/local_cmd.go b/backend/utils/terminal/local_cmd.go
index 098dbcea3..d0926fd69 100644
--- a/backend/utils/terminal/local_cmd.go
+++ b/backend/utils/terminal/local_cmd.go
@@ -25,14 +25,24 @@ type LocalCommand struct {
pty *os.File
}
-func NewCommand(commands []string) (*LocalCommand, error) {
- cmd := exec.Command("docker", commands...)
+func NewCommand(initCmd string) (*LocalCommand, error) {
+ cmd := exec.Command("bash")
+ if term := os.Getenv("TERM"); term != "" {
+ cmd.Env = append(os.Environ(), "TERM="+term)
+ } else {
+ cmd.Env = append(os.Environ(), "TERM=xterm")
+ }
pty, err := pty.Start(cmd)
if err != nil {
return nil, errors.Wrapf(err, "failed to start command")
}
+ if len(initCmd) != 0 {
+ time.Sleep(100 * time.Millisecond)
+ _, _ = pty.Write([]byte(initCmd + "\n"))
+ }
+
lcmd := &LocalCommand{
closeSignal: DefaultCloseSignal,
closeTimeout: DefaultCloseTimeout,
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index b14143f8a..b8cf07d28 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -123,8 +123,6 @@ const message = {
},
msg: {
noneData: 'No data available',
- disConn:
- 'Please click the disconnect button directly to terminate the terminal connection, avoiding the use of exit commands like {0}.',
delete: `This operation delete can't be undone. Do you want to continue?`,
clean: `This operation clean can't be undone. Do you want to continue?`,
deleteTitle: 'Delete',
diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts
index fba0f8921..6ecd05be8 100644
--- a/frontend/src/lang/modules/ja.ts
+++ b/frontend/src/lang/modules/ja.ts
@@ -122,8 +122,6 @@ const message = {
},
msg: {
noneData: '利用可能なデータはありません',
- disConn:
- '端末接続を切断するには、{0} のような終了コマンドを使用せずに、直接切断ボタンをクリックしてください',
delete: `この操作削除は元に戻すことはできません。続けたいですか?`,
clean: `この操作は取り消すことはできません。続けたいですか?`,
deleteTitle: '消去',
diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts
index afa32ad75..51c37cfa2 100644
--- a/frontend/src/lang/modules/ko.ts
+++ b/frontend/src/lang/modules/ko.ts
@@ -123,8 +123,6 @@ const message = {
},
msg: {
noneData: '데이터가 없습니다',
- disConn:
- '종료 명령어인 {0} 등을 사용하지 않고 직접 연결 끊기 버튼을 클릭하여 터미널 연결을 종료해 주십시오.',
delete: `이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?`,
clean: `이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?`,
deleteTitle: '삭제',
diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts
index 3d8f580ea..c203c0990 100644
--- a/frontend/src/lang/modules/ms.ts
+++ b/frontend/src/lang/modules/ms.ts
@@ -123,8 +123,6 @@ const message = {
},
msg: {
noneData: 'Tiada data tersedia',
- disConn:
- 'Sila klik butang putus sambungan secara langsung untuk menamatkan sambungan terminal, mengelakkan penggunaan arahan keluar seperti {0}.',
delete: 'Operasi ini tidak boleh diundur. Adakah anda mahu meneruskan?',
clean: 'Operasi ini tidak boleh diundur. Adakah anda mahu meneruskan?',
deleteTitle: 'Padam',
diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts
index 9ace5189e..d5e9fde4c 100644
--- a/frontend/src/lang/modules/pt-br.ts
+++ b/frontend/src/lang/modules/pt-br.ts
@@ -123,8 +123,6 @@ const message = {
},
msg: {
noneData: 'Nenhum dado disponível',
- disConn:
- 'Por favor, clique diretamente no botão de desconexão para encerrar a conexão do terminal, evitando o uso de comandos de saída como {0}.',
delete: 'Esta operação de exclusão não pode ser desfeita. Deseja continuar?',
clean: 'Esta operação de limpeza não pode ser desfeita. Deseja continuar?',
deleteTitle: 'Excluir',
diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts
index 18e8d9fd4..aa52c903a 100644
--- a/frontend/src/lang/modules/ru.ts
+++ b/frontend/src/lang/modules/ru.ts
@@ -124,8 +124,6 @@ const message = {
},
msg: {
noneData: 'Нет данных',
- disConn:
- 'Пожалуйста, нажмите кнопку отключения, чтобы разорвать соединение с терминалом, избегая использования команд выхода, таких как {0}.',
delete: 'Эта операция удаления не может быть отменена. Хотите продолжить?',
clean: 'Эта операция очистки не может быть отменена. Хотите продолжить?',
deleteTitle: 'Удалить',
diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts
index 2e3e466db..b0e19e579 100644
--- a/frontend/src/lang/modules/tw.ts
+++ b/frontend/src/lang/modules/tw.ts
@@ -123,7 +123,6 @@ const message = {
},
msg: {
noneData: '暫無資料',
- disConn: '請直接點選斷開按鈕斷開終端連接,避免使用 {0} 等退出指令。',
delete: '刪除 操作不可復原,是否繼續?',
clean: '清空 操作不可復原,是否繼續?',
deleteTitle: '刪除',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index ea7d690b6..f669e4cc8 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -121,7 +121,6 @@ const message = {
Rollbacking: '快照回滚中,请稍候...',
},
msg: {
- disConn: '请直接点击断开按钮断开终端连接,避免使用 {0} 等退出命令',
noneData: '暂无数据',
delete: '删除 操作不可回滚,是否继续?',
clean: '清空 操作不可回滚,是否继续?',
diff --git a/frontend/src/views/ai/model/terminal/index.vue b/frontend/src/views/ai/model/terminal/index.vue
index 7d28c8b64..1d29669e7 100644
--- a/frontend/src/views/ai/model/terminal/index.vue
+++ b/frontend/src/views/ai/model/terminal/index.vue
@@ -16,12 +16,7 @@
-
-
- {{ $t('commons.msg.disConn', ['/bye exit']) }}
-
-
-
+