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 @@ - - - - +