From 9ffc601cf067ec3f8504dee11eee47e078dbe0a0 Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:37:07 +0800 Subject: [PATCH] feat: Change the local connection mode of the terminal (#8409) --- agent/app/api/v2/terminal.go | 105 ++++++++++++++---- agent/router/ro_host.go | 2 + agent/utils/terminal/local_cmd.go | 11 +- core/app/service/host.go | 25 +---- frontend/src/lang/modules/en.ts | 1 - frontend/src/lang/modules/ja.ts | 1 - frontend/src/lang/modules/ko.ts | 1 - frontend/src/lang/modules/ms.ts | 1 - frontend/src/lang/modules/pt-br.ts | 2 - frontend/src/lang/modules/ru.ts | 2 - frontend/src/lang/modules/zh-Hant.ts | 1 - frontend/src/lang/modules/zh.ts | 1 - frontend/src/views/terminal/host/index.vue | 8 +- .../src/views/terminal/host/operate/index.vue | 12 +- .../views/terminal/terminal/host-create.vue | 61 +--------- .../src/views/terminal/terminal/index.vue | 75 ++++++------- 16 files changed, 138 insertions(+), 171 deletions(-) diff --git a/agent/app/api/v2/terminal.go b/agent/app/api/v2/terminal.go index 864b21224..2d71d4a95 100644 --- a/agent/app/api/v2/terminal.go +++ b/agent/app/api/v2/terminal.go @@ -18,6 +18,55 @@ import ( "github.com/pkg/errors" ) +func (b *BaseApi) WsSSH(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.Base.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 + } + name, err := loadExecutor() + if wshandleError(wsConn, err) { + return + } + slave, err := terminal.NewCommand(name) + if wshandleError(wsConn, err) { + return + } + 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 (b *BaseApi) ContainerWsSSH(c *gin.Context) { wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) if err != nil { @@ -42,7 +91,7 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) { } source := c.Query("source") var containerID string - var initCmd string + var initCmd []string switch source { case "redis": containerID, initCmd, err = loadRedisInitCmd(c) @@ -59,11 +108,11 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) { return } pidMap := loadMapFromDockerTop(containerID) - slave, err := terminal.NewCommand("clear && " + initCmd) + slave, err := terminal.NewCommand("docker", initCmd...) if wshandleError(wsConn, err) { return } - defer killBash(containerID, strings.ReplaceAll(initCmd, fmt.Sprintf("docker exec -it %s ", containerID), ""), pidMap) + defer killBash(containerID, strings.ReplaceAll(strings.Join(initCmd, " "), fmt.Sprintf("exec -it %s ", containerID), ""), pidMap) defer slave.Close() tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false) @@ -83,62 +132,63 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) { } } -func loadRedisInitCmd(c *gin.Context) (string, string, error) { +func loadRedisInitCmd(c *gin.Context) (string, []string, error) { name := c.Query("name") from := c.Query("from") - commands := "redis-cli" + commands := []string{"exec", "-it"} database, err := databaseService.Get(name) if err != nil { - return "", "", fmt.Errorf("no such database in db, err: %v", err) + return "", nil, fmt.Errorf("no such database in db, err: %v", err) } if from == "local" { redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"}) if err != nil { - return "", "", fmt.Errorf("no such app in db, err: %v", err) + return "", nil, fmt.Errorf("no such app in db, err: %v", err) } name = redisInfo.ContainerName + commands = append(commands, []string{name, "redis-cli"}...) if len(database.Password) != 0 { - commands = "redis-cli -a " + database.Password + " --no-auth-warning" + commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...) } } else { - commands = fmt.Sprintf("redis-cli -h %s -p %v", database.Address, database.Port) - if len(database.Password) != 0 { - 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" + commands = append(commands, []string{name, "redis-cli", "-h", database.Address, "-p", fmt.Sprintf("%v", database.Port)}...) + if len(database.Password) != 0 { + commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...) + } } - return name, fmt.Sprintf("docker exec -it %s %s", name, commands), nil + return name, commands, nil } -func loadOllamaInitCmd(c *gin.Context) (string, string, error) { +func loadOllamaInitCmd(c *gin.Context) (string, []string, error) { name := c.Query("name") if cmd.CheckIllegal(name) { - return "", "", fmt.Errorf("ollama model %s contains illegal characters", name) + return "", nil, fmt.Errorf("ollama model %s contains illegal characters", name) } ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"}) if err != nil { - return "", "", fmt.Errorf("no such app in db, err: %v", err) + return "", nil, 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 + return containerName, []string{"exec", "-it", containerName, "ollama", "run", name}, nil } -func loadContainerInitCmd(c *gin.Context) (string, string, error) { +func loadContainerInitCmd(c *gin.Context) (string, []string, error) { containerID := c.Query("containerid") command := c.Query("command") user := c.Query("user") if cmd.CheckIllegal(user, containerID, command) { - return "", "", fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID) + return "", nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID) } if len(command) == 0 || len(containerID) == 0 { - return "", "", fmt.Errorf("error param of command: %s or containerID: %s", command, containerID) + return "", nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID) } - command = fmt.Sprintf("docker exec -it %s %s", containerID, command) + commands := []string{"exec", "-it", containerID, command} if len(user) != 0 { - command = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command) + commands = []string{"exec", "-it", "-u", user, containerID, command} } - return containerID, command, nil + return containerID, commands, nil } func wshandleError(ws *websocket.Conn, err error) bool { @@ -204,3 +254,12 @@ var upGrader = websocket.Upgrader{ return true }, } + +func loadExecutor() (string, error) { + std, err := cmd.RunDefaultWithStdoutBashC("echo $SHELL") + if err != nil { + return "", fmt.Errorf("load default executor failed, err: %s", std) + } + + return strings.ReplaceAll(std, "\n", ""), nil +} diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index 4e599c716..d155bd853 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -46,5 +46,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/tool/supervisor/process", baseApi.OperateProcess) hostRouter.GET("/tool/supervisor/process", baseApi.GetProcess) hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile) + + hostRouter.GET("/exec", baseApi.WsSSH) } } diff --git a/agent/utils/terminal/local_cmd.go b/agent/utils/terminal/local_cmd.go index 20c5c07c9..8551ca560 100644 --- a/agent/utils/terminal/local_cmd.go +++ b/agent/utils/terminal/local_cmd.go @@ -25,22 +25,20 @@ type LocalCommand struct { pty *os.File } -func NewCommand(initCmd string) (*LocalCommand, error) { - cmd := exec.Command("bash") +func NewCommand(name string, arg ...string) (*LocalCommand, error) { + cmd := exec.Command(name, arg...) if term := os.Getenv("TERM"); term != "" { cmd.Env = append(os.Environ(), "TERM="+term) } else { cmd.Env = append(os.Environ(), "TERM=xterm") } + homeDir, _ := os.UserHomeDir() + cmd.Dir = homeDir 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, @@ -99,4 +97,5 @@ func (lcmd *LocalCommand) Wait(quitChan chan bool) { global.LOG.Errorf("ssh session wait failed, err: %v", err) setQuit(quitChan) } + setQuit(quitChan) } diff --git a/core/app/service/host.go b/core/app/service/host.go index 1dfc63758..ec2c4fc9e 100644 --- a/core/app/service/host.go +++ b/core/app/service/host.go @@ -79,16 +79,9 @@ func (u *HostService) TestLocalConn(id uint) bool { host model.Host err error ) - if id == 0 { - host, err = hostRepo.Get(hostRepo.WithByAddr("127.0.0.1")) - if err != nil { - return false - } - } else { - host, err = hostRepo.Get(repo.WithByID(id)) - if err != nil { - return false - } + host, err = hostRepo.Get(repo.WithByID(id)) + if err != nil { + return false } var connInfo ssh.ConnInfo if err := copier.Copy(&connInfo, &host); err != nil { @@ -206,11 +199,7 @@ func (u *HostService) SearchForTree(search dto.SearchForTree) ([]dto.HostTree, e func (u *HostService) GetHostByID(id uint) (*dto.HostInfo, error) { var item dto.HostInfo var host model.Host - if id == 0 { - host, _ = hostRepo.Get(repo.WithByName("local")) - } else { - host, _ = hostRepo.Get(repo.WithByID(id)) - } + host, _ = hostRepo.Get(repo.WithByID(id)) if host.ID == 0 { return nil, buserr.New("ErrRecordNotFound") } @@ -246,9 +235,6 @@ func (u *HostService) GetHostByID(id uint) (*dto.HostInfo, error) { } func (u *HostService) Create(req dto.HostOperate) (*dto.HostInfo, error) { - if req.Name == "local" { - return nil, buserr.New("ErrRecordExist") - } hostItem, _ := hostRepo.Get(hostRepo.WithByAddr(req.Addr), hostRepo.WithByUser(req.User), hostRepo.WithByPort(req.Port)) if hostItem.ID != 0 { return nil, buserr.New("ErrRecordExist") @@ -305,9 +291,6 @@ func (u *HostService) Delete(ids []uint) error { if host.ID == 0 { return buserr.New("ErrRecordNotFound") } - if host.Name == "local" { - return errors.New("the local connection information cannot be deleted!") - } if err := hostRepo.Delete(repo.WithByID(id)); err != nil { return err } diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 64dd7e2e0..cf6a75dfb 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1092,7 +1092,6 @@ const message = { terminal: { local: 'Local', localHelper: 'The `local` name is used only for system local identification', - connLocalErr: 'Unable to automatically authenticate, please fill in the local server login information!', testConn: 'Test connection', saveAndConn: 'Save and Connect', connTestOk: 'Connection information available', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 5f82837f1..6470537c5 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -1050,7 +1050,6 @@ const message = { terminal: { local: 'ローカル', localHelper: 'ローカル名はシステムのローカル識別にのみ使用されます。', - connLocalErr: '自動的に認証できない場合は、ローカルサーバーのログイン情報を入力してください。', testConn: 'テスト接続', saveAndConn: '保存して接続します', connTestOk: '利用可能な接続情報', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 3529a6af3..58e870ca3 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -1043,7 +1043,6 @@ const message = { terminal: { local: '로컬', localHelper: '로컬 이름은 시스템 로컬 식별에만 사용됩니다.', - connLocalErr: '자동 인증에 실패했습니다. 로컬 서버 로그인 정보를 입력해주세요.', testConn: '연결 테스트', saveAndConn: '저장 후 연결', connTestOk: '연결 정보가 유효합니다.', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 785453280..809de4a66 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -1079,7 +1079,6 @@ const message = { terminal: { local: 'Tempatan', localHelper: 'Nama tempatan hanya digunakan untuk pengenalan sistem tempatan.', - connLocalErr: 'Tidak dapat mengesahkan secara automatik, sila isi maklumat log masuk pelayan tempatan.', testConn: 'Uji sambungan', saveAndConn: 'Simpan dan sambung', connTestOk: 'Maklumat sambungan tersedia', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index b47e8d0d7..47ddf6c99 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -1069,8 +1069,6 @@ const message = { terminal: { local: 'Local', localHelper: 'O nome local é usado apenas para identificação local do sistema.', - connLocalErr: - 'Não foi possível autenticar automaticamente, por favor, preencha as informações de login do servidor local.', testConn: 'Testar conexão', saveAndConn: 'Salvar e conectar', connTestOk: 'Informações de conexão disponíveis', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index e1affa5f9..fb938414c 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -1074,8 +1074,6 @@ const message = { terminal: { local: 'Локальный', localHelper: 'Локальное имя используется только для локальной идентификации системы.', - connLocalErr: - 'Невозможно автоматически аутентифицироваться, пожалуйста, заполните информацию для входа на локальный сервер.', testConn: 'Проверить подключение', saveAndConn: 'Сохранить и подключиться', connTestOk: 'Информация о подключении доступна', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index c7d3337dd..65140d2ad 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -1040,7 +1040,6 @@ const message = { terminal: { local: '本機', localHelper: 'local 名稱僅用於系統本機標識', - connLocalErr: '無法自動認證,請填寫本地服務器的登錄信息!', testConn: '連接測試', saveAndConn: '保存並連接', connTestOk: '連接信息可用', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index cb9445935..732acfe54 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1038,7 +1038,6 @@ const message = { terminal: { local: '本机', localHelper: 'local 名称仅用于系统本机标识', - connLocalErr: '无法自动认证,请填写本地服务器的登录信息!', testConn: '连接测试', saveAndConn: '保存并连接', connTestOk: '连接信息可用', diff --git a/frontend/src/views/terminal/host/index.vue b/frontend/src/views/terminal/host/index.vue index 44356943e..b0037f737 100644 --- a/frontend/src/views/terminal/host/index.vue +++ b/frontend/src/views/terminal/host/index.vue @@ -34,7 +34,7 @@ :data="data" @search="search" > - + @@ -106,9 +106,6 @@ const acceptParams = () => { search(); }; -function selectable(row) { - return row.name !== 'local'; -} const dialogRef = ref(); const onOpenDialog = async ( title: string, @@ -174,9 +171,6 @@ const buttons = [ }, { label: i18n.global.t('commons.button.delete'), - disabled: (row: any) => { - return row.name === 'local'; - }, click: (row: Host.Host) => { onBatchDelete(row); }, diff --git a/frontend/src/views/terminal/host/operate/index.vue b/frontend/src/views/terminal/host/operate/index.vue index 09c26f500..96dbf0947 100644 --- a/frontend/src/views/terminal/host/operate/index.vue +++ b/frontend/src/views/terminal/host/operate/index.vue @@ -61,8 +61,7 @@ - local - + @@ -103,12 +102,10 @@ const drawerVisible = ref(false); const dialogData = ref({ title: '', }); -const itemName = ref(); const groupList = ref(); const acceptParams = (params: DialogProps): void => { dialogData.value = params; - itemName.value = params.rowData.name; title.value = i18n.global.t('commons.button.' + dialogData.value.title); drawerVisible.value = true; loadGroups(); @@ -126,14 +123,7 @@ const rules = reactive({ port: [Rules.requiredInput, Rules.port], user: [Rules.requiredInput], authMode: [Rules.requiredSelect], - name: [{ validator: checkName, trigger: 'blur' }], }); -function checkName(rule: any, value: any, callback: any) { - if (value === 'local' && dialogData.value.title !== 'edit') { - return callback(new Error(i18n.global.t('terminal.localHelper'))); - } - callback(); -} const loadGroups = async () => { const res = await getGroupList('host'); diff --git a/frontend/src/views/terminal/terminal/host-create.vue b/frontend/src/views/terminal/terminal/host-create.vue index 27bc4d9f2..60d941c10 100644 --- a/frontend/src/views/terminal/terminal/host-create.vue +++ b/frontend/src/views/terminal/terminal/host-create.vue @@ -1,14 +1,6 @@